在页面启动的时候,会从native-lib中读取字符串,然后展示在UI上,native代码如下
通过Android Studio的菜单,选择Attach process,或者直接以调试方式启动程序。 Debugger选择Auto/Native/Hybrid都可以,当然也可以自己push LLDB server到手机设备,然后手动远程连接上去,可以参见远程调试
成功连接调试器之后,Android中集成了LLDB的控制台,在控制台中我们可以通过一系列命令和调试程序进行交互。
<noun> <verb> [-options [option-value]] [argument [argument…]]
LLDB的命令都符合上述格式,argument option 以及option-value用空格隔开,LLDB支持前缀匹配,大多数情况下并不需要输入完整命令。如果参数中有空格,则需要用“”进行保护。option可以放在命令中的任意位置,不过如果argument中有以-开始的值,可以通--显示的标识option的结束。 另外还有一些转义规则请参考官方文档。
进入调试状态之后,紧接着就是下断点了,在LLDB中通过breakpoint set命令进行断点的设置。
#按文件和行号设置断点
(lldb) breakpoint set -f native-lib.cpp -l 9
Breakpoint 2: where = libnative-lib.so`getString(int) + 6 at native-lib.cpp:9, address = 0x73a3cf5a
#按函数名设置断点,不区分C函数和C++Method
(lldb) breakpoint set -n getString
Breakpoint 3: 20 locations.
#按C++方法名设置断点
(lldb) breakpoint set -M getString
Breakpoint 4: 18 locations.
#通过-s参数制定module
(lldb) breakpoint set -s libnative-lib.so -n getString
Breakpoint 5: where = libnative-lib.so`getString(int) + 6 at native-lib.cpp:9, address = 0x73a3cf5a
(lldb) breakpoint modify 2 -c 'index == 0'
(lldb) breakpoint list
Current breakpoints:
2: file = 'native-lib.cpp', line = 9, exact_match = 0, locations = 1, resolved = 1, hit count = 0
Condition: index == 0
2.1: where = libnative-lib.so`getString(int) + 6 at native-lib.cpp:9, address = 0x73a3cf5a, resolved, hit count = 0
通过-c参数告知调试器,当给定表达式的计算结果为true时,触发断点。
在逆向其他应用,或则无源码调试的时候,我们往往需要按照地址设置断点
(lldb) breakpoint set -a 0x73a3cf5a
通过-a参数指定地址,在没有其他参数的情况下,这个地址是进程空间的地址,在实际无源码调试过程中,我们可以通过静态分析工具(arm-eabi-objdump/ida)获取到需要断点指令的file offset,然后通过下面的指令设置断点。
(lldb) breakpoint set -s libnative-lib.so -a 0xf04
#查看所有断点
(lldb) breakpoint list
Current breakpoints:
2: file = 'native-lib.cpp', line = 9, exact_match = 0, locations = 1, resolved = 1, hit count = 0
2.1: where = libnative-lib.so`getString(int) + 6 at native-lib.cpp:9, address = 0x73a3cf5a, resolved, hit count = 0
#禁用断点
(lldb) breakpoint disable 2
1 breakpoints disabled.
(lldb) breakpoint list
Current breakpoints:
2: file = 'native-lib.cpp', line = 9, exact_match = 0, locations = 1 Options: disabled
2.1: where = libnative-lib.so`getString(int) + 6 at native-lib.cpp:9, address = 0x73a3cf5a, unresolved, hit count = 0
#启用断点
(lldb) breakpoint enable 2
1 breakpoints enabled.
#删除断点
(lldb) breakpoint delete 2
1 breakpoints deleted; 0 breakpoint locations disabled.
当调试器命中断点之后,我们可以开始程序的流程控制。
#源码级别的流程控制
(lldb) thread step-in
(lldb) thread step-over
(lldb) thread step-out
(lldb) thread continue
#指令级别的流程控制
(lldb) thread step-inst
(lldb) thread step-inst-over
#所有线程恢复执行
continue
#查看调用栈
(lldb) thread backtrace
* thread #3: tid = 8148, 0x73a3cf5a libnative-lib.so`getString(index=0) + 6 at native-lib.cpp:9, name = 'om.netease.demo', stop reason = breakpoint 2.1
* frame #0: 0x73a3cf5a libnative-lib.so`getString(index=0) + 6 at native-lib.cpp:9
frame #1: 0x73a3cf9c libnative-lib.so`::Java_com_netease_demo_MainActivity_stringFromLibNative(env=0x41894638, (null)=0x3770001d) + 24 at native-lib.cpp:20
frame #2: 0x409043d0 libdvm.so`dvmPlatformInvoke + 116
frame #3: 0x40934d92 libdvm.so`dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*) + 402
frame #4: 0x4090d8a8 libdvm.so`dvmJitToInterpNoChain + 696
在调试的过程中,我们往往需要查看内存变量的值,可以通过frame或则p/po/x指令来实现。frame用来查看当前栈帧的变量值,p和po用来打印表达式的结果,x用来查看内存中某个地址开始的值,对于不可打印的值,可以通过x查看。frame select指令可以用来切换当前所处的栈帧,but Android Studio使用的lldb server有问题,目前这个指令无法生效,详见issues。
(lldb) frame info
frame #0: 0x73a3cf5a libnative-lib.so`getString(index=0) + 6 at native-lib.cpp:9
(lldb) frame variable
(int) index = 0
#打印变量的值
(lldb) p index
(int) $2 = 0
(lldb) po index
0
#修改变量的值
(lldb) expression index = 1
(int) $0 = 1
expression指令用于在当前栈帧中计算表达式,我们可以用其修改变量的值,在上面的列子中,我们把index的值换成1,从而返回data[1],那考虑下如果我们期望返回一个全新的字符串,比如”New String from Lib Native”?
(lldb) thread step-out
(lldb) expression str = "New String from Lib Native"
很遗憾,这样的命令并不能达到预期的效果。要找到原因,我们可以使用disassemble命令进行反编译。
(lldb) disassemble
libnative-lib.so`::Java_com_netease_demo_MainActivity_stringFromLibNative(JNIEnv *, jobject):
0x73a3df84 <+0>: push {r7, lr}
0x73a3df86 <+2>: mov r7, sp
0x73a3df88 <+4>: sub sp, #0x18
0x73a3df8a <+6>: mov r2, r1
0x73a3df8c <+8>: mov r3, r0
0x73a3df8e <+10>: str r0, [sp, #0x14]
0x73a3df90 <+12>: str r1, [sp, #0x10]
0x73a3df92 <+14>: movs r0, #0x0
0x73a3df94 <+16>: str r2, [sp, #0x8]
0x73a3df96 <+18>: str r3, [sp, #0x4]
0x73a3df98 <+20>: blx 0x73a3ddd4 ; symbol stub for: getString(int)
-> 0x73a3df9c <+24>: str r0, [sp, #0xc]
0x73a3df9e <+26>: ldr r1, [sp, #0x14]
0x73a3dfa0 <+28>: str r0, [sp]
0x73a3dfa2 <+30>: mov r0, r1
0x73a3dfa4 <+32>: ldr r1, [sp]
0x73a3dfa6 <+34>: blx 0x73a3dde0 ; symbol stub for: _JNIEnv::NewStringUTF(char const*)
0x73a3dfaa <+38>: add sp, #0x18
0x73a3dfac <+40>: pop {r7, pc}
由此可见getString返回之后的结果从r0存储到栈上,然后加载到r1中作为NewStringUTF的参数。
#读寄存器
(lldb) register read r0
r0 = 0x73a3f628
(lldb) po (const char*)0x73a3f628
"Hello from Lib Native”
当用expression指令修改str的值,改的只是内存里面的值,r0中的地址并没有改变,所以要实现我们预期的效果,只需要修改r0中的值即可。
(lldb) expression str = "New String from Lib Native"
(const char *) $13 = 0x7437b020 "New String from Lib Native"
(lldb) register write r0 0x7437b020
#查看线程
(lldb) thread list
Process 8148 stopped
thread #1: tid = 6997, 0x40052534 libc.so`__ioctl + 8, name = 'Binder_8'
* thread #3: tid = 8148, 0x73a3cf5a libnative-lib.so`getString(index=0) + 6 at native-lib.cpp:9, name = 'om.netease.demo', stop reason = breakpoint 2.1
thread #4: tid = 8152, 0x40053844 libc.so`__futex_syscall3 + 8, name = ‘GC'
#切换线程
(lldb) thread select 3
LLDB的命令非常强大,熟练掌握常用命令对JNI开发调试过程大有裨益,本文只简单介绍了一些基本用法,后续在使用过程中对命令有问题,可以通过help或者官方文档获取帮助。
本文来自网易实践者社区,经作者唐杰授权发布。