也不是很全面或者深入的分析吧,只能算是探究过程记的一些笔记,大佬可以飘过~
工具和环境准备
分析方舟运行时用到的工具如下:
- 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:逆向工具里永远的神
基本分析方法
静态分析(读源码)
- 首先按照官方指南下载 OpenHarmony 全量源码,注意选择和你的手机相同的系统版本分支
- 将源码导入到 Source Insight,开始挑重点读(一般是动态分析遇到某个函数不懂的时候来看)
Tips: 虽然 Source Insight 可以搜索符号、追踪交叉引用,但是源码中不少符号是由宏展开或者干脆是其他语言所定义的,直接搜是搜不到的。我也不知道有没有更好的方法,这里只能给一些分析经验:
- 类里字段的 getter 和 setter 可能会用
ACCESSORS
宏来统一定义,例如 ACCESSORS(ConstantPool, CONSTANT_POOL_OFFSET, PROFILE_TYPE_INFO_OFFSET)
展开后会定义 GetConstantPool
和 SetConstantPool
两个方法,但直接搜这两个符号搜不到,所以遇到 getter/setter 时找找类里有没有 ACCESSORS
- 某个函数跳不过去时,在右边 Project Symbols 窗格里搜一下,找找线索,如果是类成员函数,先找到类再分析
- 在所有
#include
里根据符号名猜某个符号最可能来自哪个头文件,然后在 Project Files 窗格里搜这个头文件进去看
- 有些函数是 Ruby 语言定义的,后缀是
.erb
,比如 Disassembler::BytecodeInstructionToPandasmInstruction
这个函数定义在 bc_ins_to_pandasm_ins.cpp.erb
中
动态分析(调试)
- 用 DevEco Studio 编写 OpenHarmony 应用,运行一次让它安装到手机上
- 电脑上运行如下命令:
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; unsigned __int8 *v1; __int64 v2; *(_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(*)
代码块中。