探究 OpenHarmony 中方舟运行时调用 ArkTS 方法的流程

Posted by HX on 2024-01-07 | 👓

也不是很全面或者深入的分析吧,只能算是探究过程记的一些笔记,大佬可以飘过~

工具和环境准备

分析方舟运行时用到的工具如下:

  • OpenHarmony 手机:我用的一加 6T,刷入 B 站大佬的 OpenHarmony 4.0 ROM,要用物理机而不是模拟器主要是 hdc(鸿蒙版 adb)好像连不上模拟器,不知道是我操作不对还是确实不支持
  • Source Insight 4:看源码用,搜索符号和交叉引用比较方便,需要付费自行解决,Gitee 看代码太痛苦了
  • DevEco Studio:不用多说,折腾鸿蒙必备,记得装 SDK,按照这个仓库 README 的第二段话的指示设置好环境变量
  • lldb:在 %OHOS_SDK_HOME%\llvm\bin 里有,而且是鸿蒙专供版哦(其实就是加了远程调试鸿蒙的支持)
  • 010Editor:搭配我写的模板,用来解析 abc 文件
  • IDA:逆向工具里永远的神

基本分析方法

静态分析(读源码)

  1. 首先按照官方指南下载 OpenHarmony 全量源码,注意选择和你的手机相同的系统版本分支
  2. 将源码导入到 Source Insight,开始挑重点读(一般是动态分析遇到某个函数不懂的时候来看)

Tips: 虽然 Source Insight 可以搜索符号、追踪交叉引用,但是源码中不少符号是由宏展开或者干脆是其他语言所定义的,直接搜是搜不到的。我也不知道有没有更好的方法,这里只能给一些分析经验:

  • 类里字段的 getter 和 setter 可能会用 ACCESSORS 宏来统一定义,例如 ACCESSORS(ConstantPool, CONSTANT_POOL_OFFSET, PROFILE_TYPE_INFO_OFFSET) 展开后会定义 GetConstantPoolSetConstantPool 两个方法,但直接搜这两个符号搜不到,所以遇到 getter/setter 时找找类里有没有 ACCESSORS
  • 某个函数跳不过去时,在右边 Project Symbols 窗格里搜一下,找找线索,如果是类成员函数,先找到类再分析
  • 在所有 #include 里根据符号名猜某个符号最可能来自哪个头文件,然后在 Project Files 窗格里搜这个头文件进去看
  • 有些函数是 Ruby 语言定义的,后缀是 .erb,比如 Disassembler::BytecodeInstructionToPandasmInstruction 这个函数定义在 bc_ins_to_pandasm_ins.cpp.erb

动态分析(调试)

  1. 用 DevEco Studio 编写 OpenHarmony 应用,运行一次让它安装到手机上
  2. 电脑上运行如下命令:
    1
    2
    3
    4
    5
    6
    hdc shell # 进入手机 shell
    bm dump -a # 列出所有安装的应用包名
    aa start -a <Ability 名> -b <包名> -D # 调试模式启动应用,运行后手机上应该启动了应用,但停在初始化页面
    hdc file send <电脑上 lldb-server 路径> /data/local/tmp(DevEco Studio 里面调试应用就可以在日志里看到 lldb-server 在手机的位置,应该是调试过手机上就会有所以不用 send?不太记得了,反正我这在 /data/local/tmp 下)
    cd /data/local/tmp
    lldb-server platform --server --listen unix-abstract:///data/local/tmp/debug.sock &

以上命令都在手机 shell 里执行,现在新起一个命令行窗口,在 shell 外运行如下命令:

1
2
3
4
5
cd %OHOS_NDK_HOME%\llvm\bin
lldb
platform select remote-ohos # 设置远程调试的系统为 OpenHarmony
platform connect unix-abstract-connect:///data/local/tmp/debug.sock
process attach -p <处于调试模式的应用 PID>

现在可以对自己感兴趣的地方下断点了:

1
2
3
4
5
6
7
br set -s <so 名> -n <函数名> # 下断点
br set --func-regex <函数名正则/关键字>
image lookup -r -n <函数名正则/关键字> # 找指定名的函数偏移
br set -s <so 名> -a <偏移>
c # 下完断点恢复程序运行

然后在 DevEco Studio 中点击菜单 Run -> Attach Debugger to Process,选择应用的进程确认,即可在 lldb 开始调试。

一些常用调试命令:

1
2
3
4
5
6
reg read x0 # 读寄存器
x/16a 0xffffffff # 读内存
x/16a $x0
bt # 调用栈

方法调用流程

分析基于 OpenHarmony 4.0 Beta2,未来版本可能有不同。

关键流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
NAPI入口 JSNApi::Execute -> ecmascript::JSPandaFileExecutor::ExecuteFromFile/Buffer -> 判断jsPandaFile->IsModule(recordInfo)
-> 判断true:SourceTextModule::Evaluate -> SourceTextModule::InnerModuleEvaluation -> SourceTextModule::ModuleExecution -> JSPandaFileExecutor::Execute(与false殊途同归)
-> 判断false:JSPandaFileExecutor::Execute -> EcmaContext::InvokeEcmaEntrypoint -> 分三种情况
-> 情况1-CJS(猜测是classic JS?):EcmaContext::CJSExecution -> 如果非AOT则EcmaInterpreter::Execute
-> 情况2-AOT:EcmaContext::InvokeEcmaAotEntrypoint
-> 情况3-非上二者(猜测是ArkTS的解释执行?):EcmaInterpreter::Execute -> 分三种情况
-> 情况1-if (thread->IsAsmInterpreter()):InterpreterAssembly::Execute(实际测试解释执行走这里)
-> InterpreterAssembly::Execute -> 根据架构跳到对应的AsmInterpreterCall::AsmInterpreterEntry(这个函数在内存里位于匿名区域,aarch64在arkcompiler\ets_runtime\ecmascript\compiler\trampoline\aarch64\asm_interpreter_call.cpp)-> AsmInterpreterCall::AsmInterpEntryDispatch -> 分三种情况
-> 情况1-functionType小于JSType::JS_FUNCTION_FIRST:认为不是callable,跳到固定函数抛出异常kungfu::RuntimeStubCSigns::ID_ThrowNotCallableException
-> 情况2-functionType介于JSType::JS_FUNCTION_FIRST与JSType::JS_FUNCTION_LAST:认为是JS函数,跳到JSCallCommonEntry
-> 情况3-functionType大于JSType::JS_FUNCTION_LAST:认为是native函数,跳到CallNativeEntry
-> 情况2-Native方法:EcmaInterpreter::ExecuteNative
-> 情况3-非上二者:EcmaInterpreter::RunInternal

(经测试,ArkTS 方法内调用别的 ArkTS 方法不会走 InterpreterAssembly::Execute,至于会不会走 JSCallCommonEntry 没有测试)

如何得到 Method 对象: 在 EcmaInterpreter::Execute 里下断点,有 Method *method = callTarget->GetCallTarget(); 这行

如何从 Method 对象得到方法名: Method->LiteralInfo 里的 MethodId 字段(16-47 位) -> 根据 ID 在 abc 中反查名字,ID 就是 abc 内的文件偏移

Method 对象结构:

1
2
3
4
5
6
7
+0x8 ConstantPool
+0x10 ProfileTypeInfo
+0x18 CallField
+0x20 NativePointerOrBytecodeArray
+0x28 CodeEntryOrLiteral
+0x30 LiteralInfo
+0x38 ExtraLiteralInfo

JSCallCommonEntry 解释执行:

一路跟,最终会进到 AsmInterpreterCall::DispatchCall,最后的一句 br 正式跳入分发器,分发器汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ROM:0000000000045850 sub_45850
ROM:0000000000045850
ROM:0000000000045850 var_30 = -0x30
ROM:0000000000045850 var_18 = -0x18
ROM:0000000000045850 var_10 = -0x10
ROM:0000000000045850 var_8 = -8
ROM:0000000000045850
ROM:0000000000045850 SUB SP, SP, #0x30
ROM:0000000000045854 STUR X20, [X29,#-0x18]
ROM:0000000000045858 MOV W9, #0xDB
ROM:000000000004585C LDR X8, [X19,#0xE38]
ROM:0000000000045860 MOV X0, X19
ROM:0000000000045864 STP X22, X23, [SP,#0x30+var_10]
ROM:0000000000045868 STR X21, [SP,#0x30+var_18]
ROM:000000000004586C STP X9, XZR, [SP,#0x30+var_30]
ROM:0000000000045870 BLR X8
ROM:0000000000045874 LDRB W8, [X20]
ROM:0000000000045878 LDP X21, X22, [SP,#0x30+var_18]
ROM:000000000004587C ADD X8, X19, X8,LSL#3
ROM:0000000000045880 LDR X23, [SP,#0x30+var_8]
ROM:0000000000045884 LDR X0, [X8,#0x1890]
ROM:0000000000045888 ADD SP, SP, #0x30 ; '0'
ROM:000000000004588C BR X0

用IDA反编译

1
2
3
4
5
6
7
8
9
10
__int64 sub_45850()
{
__int64 v0; // x19
unsigned __int8 *v1; // x20
__int64 v2; // x29
*(_QWORD *)(v2 - 24) = v1;
(*(void (__fastcall **)(__int64))(v0 + 3640))(v0);
return (*(__int64 (**)(void))(v0 + 8i64 * *v1 + 0x1890))();
}

最后一句中,v0 + 0x1890 就是 opcode handler 表的地址了,表中每个 handler 指针 8 字节,*v1 是当前 opcode。

至于这些 handlers 的源码,则在 arkcompiler\ets_runtime\ecmascript\interpreter\interpreter-inl.h 的各个 HANDLE_OPCODE(*) 代码块中。