android 如何分析应用的内存(三)
接上文
细节部分包括如下
- native部分
- 寄存器内容是什么。如pc指向何处,sp指向何处
- 指定地址内容是什么。如变量a对应的内容
- 线程堆栈内容是什么。如主线程的堆栈,UI线程的堆栈
- 堆区的对象有哪些。
- java部分
- 线程堆栈有什么内容。
- 堆中对象分配情况
其中介绍了native部分的前三个。
这些都需要使用工具才能查看,部分内容还需要写代码才能查看,因此先介绍了xdd工具,
接着介绍了gdb工具,接下来介绍lldb。
前一篇文章中,介绍的gdb,是GCC项目的调试器。
从android NDKr13中推荐使用LLVM项目,到NDK r18全面拥抱LLVM项目之后
android更推荐使用的调试器为LLVM项目的LLDB.
工具篇LLDB
本小节主要为两大内容:
- lldb的命令行调试
- lldb在AS上面的调试
前者更加适用于Framework工程师,MMI工程师
后者更加适用于Android应用工程师,但我也推荐Android应用工程师阅读本小节内容。
理由有三:
- 部分应用因为不知名原因,导致AS调试经常断连
- 部分应用使用了第三方库,第三方库中一些SIGxx,可能导致AS没办法正确显示。
- 不支持AS进行lldb调试的设备。如何查看请看,LLDB的GUI篇
LLDB的命令行
在进行调试之前,依然要根据前一篇文章所提及的步骤,对so库进行处理
1. 在合适的地方,加上-g选项,打开调试。可以是Android.mk也可以是build.gradle,还可以是CMake,依据自己的项目编译脚本而定
2. 去掉apk里面的编译优化,如下:
buildTypes {
debug {
minifyEnabled false
shrinkResources false
}
}
packagingOptions{
doNotStrip "*/arm64-v8a/*.so"
}
跟gdb一样,LLDB依然有server和client两部分。
server运行在android平台,client运行在PC平台
第一步:将server拷贝到手机中
NDK中的工具链文件夹中,已经将LLDB放入其中,位置如下:
NDK目录/llvm/prebuilt/对应平台/lib64/clang/版本号/lib/linux/androidABI/lldb-server
注意:在不同版本的NDK包中,可能路径不一样,可升级NDK包进行查看
将其push到手机中如下:
## push到/data/local
adb push lldb-server /data/local/
## 赋予可执行权限
adb shell chmod 777 /data/local/lldb-server
第二步:运行server
./lldb-server p --listen "*:5039" --server
## 其中p表示,使用platform命令,即pc端可使用platform进行连接
## 除了p以外,还可以使用g。表示使用gdb远程协议,此处不做介绍
下面介绍lldb-server的p模式下的命令参数
--server:运行在服务器模式,这样可以操作多个连接。如果没有这个选项,仅仅接受一个连接,并在完成
之后自动退出
--listen <host>:<port>:监听的主机名字和端口,如果端口位置为0,则会使用一个随机端口。上面命
令的星号,表示所有主机,端口为5039.即,使用一个可用主机的5039号端口。
--socketf-file <path>:将正在监听的端口号写入这个文件中,当--listen指定的端口号为0时有用。
--log-file <path>:将log输出到指定的文件中,如果没有,则输出到stderr
第三部:设置端口转发命令
adb forward tcp:5039 tcp:5039
## 同gdb一样,需要设置端口转发命令,将android端的5039转发到pc端的5039
第四步:使用client命令连接server
lldb的client命令在
NDK目录/toolchains/llvm/prebuilt/pc平台/bin/lldb
注意:在不同版本的NDK包中,可能路径不一样,可升级NDK包进行查看
运行如下命令
.\lldb.cmd
## 进入lldb的命令行界面。
## 注意,同目录下还会有一个lldb.exe命令,该命令为lldb.cmd要使用的命令,直接运行它可能会出现
## 一些python路径找不到的情况 。因此,在lldb.cmd中进行了正确的配置。
## 同样的,如果实在linux环境下,应该运行的是lldb.sh而不是直接运行lldb(包括mac)
第五步:在lldb中选择正确的平台插件
platform list
## 查看支持的平台有哪些,输出如下:
(lldb) platform list
Available platforms:
host: Local Windows user platform plug-in.
remote-linux: Remote Linux user platform plug-in.
remote-freebsd: Remote FreeBSD user platform plug-in.
remote-netbsd: Remote NetBSD user platform plug-in.
remote-openbsd: Remote OpenBSD user platform plug-in.
remote-ios: Remote iOS platform plug-in.
remote-macosx: Remote Mac OS X user platform plug-in.
host: Local Mac OS X user platform plug-in.
remote-windows: Remote Windows user platform plug-in.
remote-gdb-server: A platform that uses the GDB remote protocol as the communication transport.
remote-android: Remote Android user platform plug-in.
选择正确的平台插件,我们调试Android,就应该选择remote-android.如果不选择,则为本机的平台(此处为windows)
platform select remote-android
## 运行之后,它会提示,如下:
(lldb) platform select remote-android
Platform: remote-android
Connected: no
##此处显示还未连接,故下一步就是进入连接
第六步:连接远端server
platform connect connect://P1008N19120251:5039
## 其中P1008N19120251表示adb devices列出的sn号
## 5039是对应的端口号,输出如下:
(lldb) platform connect connect://P1008N19120251:5039
Platform: remote-android
Triple: aarch64-unknown-linux-android
OS Version: 27 (3.18.71-perf)
Hostname: localhost
Connected: yes
WorkingDir: /data/local
Kernel: #1 SMP PREEMPT Tue Aug 30 19:49:21 CST 2022
第七步:与具体的程序进行连接
process attach --pid 123
## 与pid为123的程序建立连接
process attach --name programName
## 与programName建立连接 (注意Android平台不可用)
process attach --name programName --waitfor
## 一旦programName启动,就和它进行连接(注意Android平台不可用)
一旦连接成功输出大致如下:
(lldb) process attach --pid 20469
Process 20469 stopped
* thread #1, name = 'findpiano.piano', stop reason = signal SIGSTOP
frame #0: 0x000000704c878be4 libc.so`__epoll_pwait + 8
libc.so`__epoll_pwait:
-> 0x704c878be4 <+8>: cmn x0, #0x1, lsl #12 ; =0x1000
0x704c878be8 <+12>: cneg x0, x0, hi
0x704c878bec <+16>: b.hi 0x704c829f44 ; __set_errno_internal
0x704c878bf0 <+20>: ret
thread #2, name = 'Jit thread pool', stop reason = signal SIGSTOP
//省略若干类似log
thread #114, name = 'pool-15-thread-', stop reason = signal SIGSTOP
frame #0: 0x000000704c829b2c libc.so`syscall + 28
libc.so`syscall:
-> 0x704c829b2c <+28>: svc #0
0x704c829b30 <+32>: cmn x0, #0x1, lsl #12 ; =0x1000
0x704c829b34 <+36>: cneg x0, x0, hi
0x704c829b38 <+40>: b.hi 0x704c829f44 ; __set_errno_internal
Executable module set to "C:\Users\wanbiao\.lldb\module_cache\remote-android\.cache\27FE2319-A0C9-B3CD-A572-C955E7D6E716\app_process64".
第八步:进入真正的调试阶段
- 如何设置源码
A,先查看调试表中的源码路径
image lookup -vn functionname
## 查看functionname对应的信息,其中就包括对应的源码路径位置。
参数说明:
-v: 输出详细信息
-n: 后面跟的是待查找的符号名字,如函数名
-a: 后面跟一个地址表达式
-r: 后面跟一个正则表达式,按照正则表达式查找
-t: 后面跟一个类型名,按照类型名查找
注意:在ubunt系统中,-vn 应该分开写成 -v -n
举例如下:(从此处开始,后面的例子,用图片展示)
从图片可以看到,notifyChannel的源文件是
/Users/biaowan/StudioProjects/FindAndroidPianoApp/rom.sdk/src/main/cpp/findmidiserver/core/MidiPort.cpp
这是我在Mac上面的编译源文件。
B,将上面找到的路径,映射到本地的源文件路径:
settings set target.source-map 源码中的路径 本地路径
## 设置源码中的路径,映射为本地路径
settings show target.source-map
## 查看源码中的路径映射关系
source list -f xxx.cpp
## 查看源文件
注意注意:非常得不幸,lldb在ndk24以下的版本中target.source-map没有作用,而在Ubuntu和MAC上面运行良好
如若发现不能正常工作,可切换到ndk 25.2.9519653版本中。亲测有效,例子如下:
补充知识:如何查看一个so库中,对应的源文件路径都有哪些呢?
在lldb中并没有找到对应的命令,若有人知道,请告诉我。
而我经常使用的命令是:
gdb
## 运行gdb,进入gdb命令行
file xx.so
## 加载xx.so文件
info sources
## 打印所有的可能的源文件
- 如何设置断点
breakpoint set --file myfile.cpp --line 42
## 再文件的第几行,设置断点
breakpoint set --name myFunction
## 再函数处,设置断点
breakpoint set --address 0x12345678
## 在地址处设置,端点
- 如何查看断点
breakpoint list
举例如下:
设置断点的命令依然还有很多,可以查看https://lldb.llvm.org/use/tutorial.html
- 如何设置watchpoint
因为在我正在使用的版本不支持watchpoint,所以此处不做介绍,后面可能出一篇专门的lldb的文章,到时候会详细介绍,为了达到类似的效果,我们使用条件断点,如下
- 如何设置条件断点
在断点后面,加上–conditoin “表达式”,举例如下:
breakpoint set --file MidiPort.cpp --line 304 --condition "buff[1]=0x9f"
## 当buff[1]等于0x9f时,断点停止在304行
因为mac和ubuntu的lldb可以使用tab进行补全命令,因此后续的举例将直接在mac或者ubuntu中进行
- 如何查看线程
thread list
## 列出素有的线程,包括线程id,线程名等等
thread info 线程id
## 查看对应线程的详细信息,包括线程ID,状态,寄存器等
thread backtrace 线程id
## 查看对应线程的调用栈
thread select 线程id
## 切换到对应的线程
- 如何进行单步调试
step ## 单步执行,进入函数的调用
next ## 单步执行,跳过函数的调用
finish ## 在当前函数中,执行到返回语句
thread step-in ## 在当前线程中,执行单步调试,进入函数中
thread step-over ## 在当前线程中,执行单步调试,跳过函数调用
thread step-out ## 在当前线程中,执行单步调试,从当前函数返回
- 如何读取变量的值
print (简写p) ## 打印变量
print -f format 变量 ## 以format格式打印变量
## format 有binary、decimal、hex、octal、float、char 等
## -r 表示以原始格式打印
express ## 计算并打印表达式的值
## 它所具有的参数,同print一样,有-f,-r
## 两者之间的区别就是express会计算表达式
- 如何切换栈帧
thread backtrace
## 打印当前的线程栈
frame select number
## 切换到number对应的栈帧中
- 如何查看已经加载的共享库
image list
## 查看所有的已经加载的共享库
image list -b xx.so
## 查看某个共享库
- 如何添加符号表
target symbols add /path/to/symbol/file
## 将指定的路径添加到符号表搜索路径中。
- 如何查看当前堆栈的所有变量
frame variable
## 查看当前堆栈的所有变量
frame variable --show-types <type>
## 只查看特定类型的变量
frame variable myVariable
## 只查看myVariable变量
- 如何查看寄存器的值
register read
## 查看所有寄存器的值
register read --format hex
## 以十六进制查看
## --format 可以有binary、decimal、hex、float 等
register read sp
## 读取sp寄存器的值
- 如何查看某个内存地址
memory read address
## 查看某个地址的内容
memory read -c n address
## 查看address开始的n个字节
memroy read -c n address --format format
## 以format格式查看,format的值有binary、decimal、hex、float
- 如何修改某个内存的值
memory write address value
## 写入value到某个内存中
- 如何查看当前线程的全局变量
target variable
LLDB分析coredump
第一步当然是加载corefile。以上一篇的core.27055为例。格式如下
lldb.sh <执行文件> -c <corefile>
一旦加载成功,则可以使用上面介绍的命令
LLDB的及使调试技术
LLDB的及时调试,同GDB一样,包括两部分
- 在程序启动的时候,就连接上lldb
- 在程序崩溃的时候,就连接上lldb
但这两部分的操作同GDB一样,再此不再过多介绍。
到目前为止,所有文章所使用的Android版本最高为8.1,因此对于其后出现的一些调试技术,可能稍微有差异
但这些差异并不是这些技术的落后,而是对这些技术的一层层封装。读者可放心使用
因为LLDB的GUI调试,会有大量的图片产生,因此,将这部分放入下一篇文章中。
在下一篇文章中,将会有两种GUI的使用。分别是AS和VScode