【工具使用】Android调试利器之LLDB

达芬奇密码2018-07-16 13:48
在Android开发当中,或多或少都会接触到NDK的开发。Android Studio作为官方IDE,对NDK的支持也日趋完善,到目前为止,集成了基于CMake的构建工具,以及基于LLDB的NDK调试工具,早在Android之前,LLDB已经成为iOS开发中不可或缺的工具,本文简单介绍一下LLDB的一些常规使用,抛砖引玉,先来看一下我们的示例程序。

在页面启动的时候,会从native-lib中读取字符串,然后展示在UI上,native代码如下

进入调试状态

通过Android Studio的菜单,选择Attach process,或者直接以调试方式启动程序。 Debugger选择Auto/Native/Hybrid都可以,当然也可以自己push LLDB server到手机设备,然后手动远程连接上去,可以参见远程调试

成功连接调试器之后,Android中集成了LLDB的控制台,在控制台中我们可以通过一系列命令和调试程序进行交互。

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或者官方文档获取帮助。

本文来自网易实践者社区,经作者唐杰授权发布。