lldb工具使用

注意
本文最后更新于 2024-05-22,文中内容可能已过时。

原文链接:https://ovea-y.cn/lldb_tool_manul/

背景介绍

有时会在native程序中遇到预期外的错误,此时可以通过加log的方式一步步定位,这样逐步缩小范围,但是需要重新编译,并且无法知道当前以及之后正在执行什么指令,因此需要学会使用lldb进行native调试。由于代码一般在编译时进行了优化,去除了符号,因此需要会一些汇编指令的作用。

简单说明

首先需要说明的是,llvm本身就是支持跨平台编译的编译器,不需要使用其他的编译器(虽然它们开箱即用,但是Android目前用的是llvm,不是gcc了)。下面说明一下llvm进行交叉编译的方法。

llvm进行交叉编译的关键就是工具链,在编译的时候通过-target指定具体使用的工具链。

构建可以在手机上运行的程序:

clang++ –static -g -O0 -target aarch64-linux-gnu [源文件]

由于lldb不能直接在AArch64架构上使用,所以需要借助中间工具lldb-server(从Android Studio安装NDK后获取)。

r4t3t2t2t2

位置:/home/ovea/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/14.0.1/lib/linux/aarch64

bash

adb root
adb push lldb-server /cache/
adb shell
\# cd cache
\# chmod +x lldb-server
\# ./lldb-server p --server --listen unix-abstract:///cache/debug.sock

lldb 查看可以远程链接的平台 (lldb) platform list

text

output:

Available platforms:
host: Local Linux 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.

选择平台 (lldb) platform select remote-android 查看链接状况 (lldb) platform status 建立链接 (lldb) platform connect unix-abstract-connect:///cache/debug.sock

text

output:

  Platform: remote-android
    Triple: aarch64-unknown-linux-android
OS Version: 31 (4.14.186-perf-g58ba26b0aedc)
  Hostname: localhost
 Connected: yes
WorkingDir: /cache
    Kernel: #1 SMP PREEMPT Mon Mar 28 17:27:22 CST 2022

然后进行调试就可以了

调试的方法有两种

file [二进制文件]

attach [Pid]

查找资料时看到的一些简写指令,通过help [简写指令]的方式找到全名。

Pasted image 20231112173414

💡下面内容请按照顺序阅读,不要跳过,里面包含了一些小实例(跳出死循环,对多线程操作)

指令 使用示例 说明 详细信息
file file a.out 绑定需要调试的二进制程序 Pasted image 20231112173849
breakpoint breakpoint set –name main 对main函数打断点,即当程序执行到mian函数的时候停止,此时可以进行调试
list list 查看后续代码,但是基本上用不到,优化后大部分符号就没有了,也就看不到任何后续要执行的代码了 Pasted image 20231112173939
breakpoint breakpoint list 查看已有的断点信息 Pasted image 20231112173959
breakpoint breakpoint delete 1 删除编号为1的断点(不加编号是删除所有断点) Pasted image 20231112174105
run run a.out 运行程序,遇到断点或指令执行完毕停止 Pasted image 20231112174140
next next 执行下一行代码 Pasted image 20231112174204
frame frame variable 查看当前调用栈所有局部变量 Pasted image 20231112174301
target target variable 查看全局变量信息 Pasted image 20231112174332
expression expression s 使用它查看变量信息、修改变量的值,以及直接调用函数 Pasted image 20231112174354
step step 单步调试到函数内部 但是如果对应的函数没有符号,会直接跳过,无法进入(此时可以通过单步调试汇编代码的方式进入)
continue continue 继续执行到下一个断点或程序终止
disassemble disassemble 查看后续要执行的汇编指令 Pasted image 20231112174418
thread thread step-inst

thread step-out
指令级别的调试

(step-inst是遇到bl等跳转指令时跳转到对应的位置)

(thread step-out会从当前栈回到上一层)
Pasted image 20231112174528
Pasted image 20231112174709
Pasted image 20231112174827
通过指令级别的单步调试,从这里就可以看出为何无法step in printf方法了,使用static编译后没有对应函数的符号了,只剩下了汇编指令
breakpoint Pasted image 20231112175016
这个需要在构建的时候使用-g -O0生成符号和调试信息,这里line不能是29,29不是真正可用的断点位置
breakpoint set –file A.cpp –line 30 –condition ‘i==500’
条件断点(对循环使用)

需要有符号信息
Pasted image 20231112175144
Pasted image 20231112175233
breakpoint breakpoint set –address 0x4013d8 –condition ‘$x8 == 500’ 条件断点(对循环使用,且没有任何符号可用) 此时条件非常复杂了,首先对程序肯定有的main方法打断点,执行后发现只有汇编指令,其他符号都已经被优化掉了
Pasted image 20231112175412
Pasted image 20231112175455
现在注意到这里很像是for循环进行+1的地方,这说明x8寄存器存储的是for循环的变量i(在0x4013d4从栈上取值之后)

breakpoint set –address 0x4013d8 –condition ‘$x8 == 500’
因此对代码段虚拟地址0x4013d8设置条件断点,当x8寄存器的值为500的时候停下
Pasted image 20231112175609
使用continue执行后,可以确定它已经准确停在了i == 500的位置
frame frame select 0 显示当前执行的位置信息(当前位置是0,它的调用者是1,以此类推) Pasted image 20231112175719
print print i 查看当前变量i的值 Pasted image 20231112175803
register read register read 查看寄存器信息 Pasted image 20231112175910
register write register write x26 1000 写寄存器信息 Pasted image 20231112175953
expression

print
print/x $lr

expression/x $lr
查看lr寄存器的16进制信息

(/x规定了输出格式,如果不指定默认10进制输出)
Pasted image 20231112180050
memory read memory read –format x –count 10 $sp 打印从栈底开始连续10个字 Pasted image 20231112180139
image list image list 查看加载段的信息 Pasted image 20231112180247
memory memory read –size 16 –format x –count 10 $sp 读取虚拟内存信息的值



size指定了每行输出的字节数



format指定了输出的格式



count指定了需要输出的行数
Pasted image 20231112180345
attach 借助attach指令,学习如何跳出死循环和调试正在运行的程序

(下面这个代码无论如何都会被llvm构建时优化掉sleep后面的内容)

Pasted image 20231112180450

请使用下面的代码构建,并使用-O0参数

Pasted image 20231112180539

attach 1924
1. 调试正在运行的进程

2. 定位循环和跳出循环
查看当前所有运行的进程

platform process list

Pasted image 20231112180709

Pasted image 20231112180803

调试1924这个进程

attach 1924

Pasted image 20231112180917

检查当前所处的位置后,可以看到现在还在执行sleep方法,此时我们准备返回上一层调用栈

finsh

Pasted image 20231112181049

Pasted image 20231112181133

回到main函数后不再执行finish返回上一层调用栈(后面就是程序结束了)

Pasted image 20231112181229

回到main后发现while(true) {sleep(1);} 后面的指令都被优化掉了,及时-O0也会被优化掉…

再使用新代码构建之后,重新回到main函数所在的位置。

Pasted image 20231112181323

红框是执行循环sleep的地方

Pasted image 20231112181410

我们需要在cbz x8, 0x4013e0的时候,让x8寄存器的值为0,此时会跳出sleep的判断,执行后面的代码

breakpoint set –address 0x4013b8

continue

Pasted image 20231112181540

当执行指令到0x4013b8这个位置的时候,修改x8寄存器的信息

register write x8 0

Pasted image 20231112181640

然后删除断点继续执行程序

breakpoint delete

continue

Pasted image 20231112181754

Pasted image 20231112181838

后续的程序被继续执行了~
thread Pasted image 20231112181923 最后一部分内容,对多线程的调试方法



thread需要对应的动态链接库支持,因此此处的代码是借助Recovery模块的配置信息进行编译的



为了方便大家学习,我编译了一个Android S的版本





这里需要做的事情是:

让两个子线程跳出死循环,让主线程可以继续执行
platform process list

查看当前正在运行的进程

Pasted image 20231112182048

attach 3101

Pasted image 20231112182146

现在主线程在等待子线程结束,而且thread 1是主线程,thread 2、thread 3是子线程,都在执行nanosleep方法

现在选择2号线程和3号线程,分别让它们结束死循环

thread select 2

finish

finish

此处不小心执行了两次finish让线程执行了,进程开始继续执行,使用以下指令打断目标进程执行

process interrupt

Pasted image 20231112182337

查看进程目前的状态

thread backtrace all

Pasted image 20231112182436

再次选择线程2

thread select 2

Pasted image 20231112182535

finish

Pasted image 20231112182637

finish

Pasted image 20231112182746

disassemble

Pasted image 20231112182844

这次是在0x5f77679078打断点,并且将x8寄存器的值修改为0

breakpoint set –address 0x5f77679078

continue

register write x8 0

Pasted image 20231112183018

去除断点后继续执行

breakpoint delete 2

Pasted image 20231112183120

continue

之后进程会继续执行,因此需要将它再次打断

process interrupt

Pasted image 20231112183228

原来的线程3的编号变成2了,后面就继续对这个线程进行同样的操作,就可以让主线程跳过死循环继续执行了。

Pasted image 20231112183314

Pasted image 20231112183350

// TODO

zlib问题分析

对Recovery进行调试,找到在Recovery中和在Ubuntu中执行同一个压缩文件进行压缩产生不同结果的原因。

在上面这个文档中还未找到真正的原因,目前已经确认和对应平台的指令相关,但是对那么多DEFLATE块(每块3-4万次)执行过insert_string,就这一个DEFLATE块在执行了了insert_string时在IA-64和ARM64上产生了不同的结果,所以需要找到产生不同结果时CPU的状态,以及高通和MTK在相同的条件下,是否会产生相同的结果。

Android对压缩文件进行压缩有两部分需要分析,一部分是在IA-64平台(Ubuntu)上生成的imgdiff,另一部分是在ARM64平台(MIUI)上生成的updater,它们使用的是zlib进行压缩操作。

目前的问题在于,两个不同的平台使用同样的库方法对同样的文件压缩成zip包产生的结果不一致。

最后定位它们在构造哈夫曼树的时候,构造的树从某一处数据处理开始有差异了,最后相同的地方是下面这个log显示的部分,他们可以作为条件断点的条件值。

Groovy

====++++ deflate_slow_debug E left: strstart: 48  prev_length: 0  s->match_length: 2  prev_match: 41  s->match_start: 41  hash_head: 0

之后我们使用lldb进行实际应用将分为两个部分,IA-64平台(Ubuntu)和ARM64平台(MIUI)。

此处可以获取本次调试需要的一些材料:

Plain

Name:Native调试
Address:https://kpan.mioffice.cn/webfolder/ext/QI9euUd49l4%40?n=0.12483618629039439
Password:S12f
  1. 之前for循环因为代码结构简单,因此可以快速判断变量i使用哪个寄存器进行装载,这次z_stream是个结构体,对它进行判断有一定的难度。

好在我提前针对出问题的chunk块进行了代码层面的判断,当chunk块的特征值(chunk块所有字节之和)为6034891时,会进入我特别准备好的debug函数(出问题函数名为deflate_slow_debug),大大降低了调试难度。关于特征值这部分内容,请翻阅文档:Android S 机型增量升级失败

  1. 在Android 平台进行调试的时候,updater是作为Recovery的子进程运行的,它的执行阶段很多,我没有像imgdiff那样大量地缩小问题范围,因此精准地控制它运行的位置将会是个挑战。(其实也可以缩小范围,把updater改造成只包含进行压缩的代码,并且脱离recovery。但是为了积累调试大型项目的经验,为以后可能的问题做准备,因此此处不打算进行缩减)

  2. Recovery选择连接小米助手会中断shell,导致无法进行调试,因此需要一种方法应用OTA升级

确切的说,是对imgdiff再次压缩chunk分析,确保同样的原始文件在进行第二次压缩时可以得到和第一次完全一致的压缩块,模拟在updater(OTA包中的脚本解释器)进行压缩时的操作

CSS

imgdiff_debug -z old.apk new.apk new.patch

C#

(lldb) file imgdiff_debug
(lldb) breakpoint set --name deflate_slow_debug
(lldb) run -z old.apk new.apk new.patch

Pasted image 20231112183545

现在停在了deflate_slow_debug这里

Pasted image 20231112183554

Pasted image 20231112183605

右边有些贴心的信息可以用于参考,通过这些信息可以再次确认我们已经位于deflate_slow_debug函数中了。

Pasted image 20231112183615

现在我们需要知道___lldb_unnamed_symbol1324是什么方法,可以用于参考定位(根据后面的代码的情况,还有当前imgdiff信息输出的情况,这个符号应该是printf,但是需要证据证明它)

💡 call的时候,会将当前调用者的地址压栈,后续通过ret返回调用点,现在再看___lldb_unnamed_symbol1324段的汇编指令,发现它使用的是jmp方法直接跳转的,jmp是强制跳转,不会记录任何信息,因此后续在ret返回的时候直接回到了deflate_slow_debug方法栈的调用点

Pasted image 20231112183755

对___lldb_unnamed_symbol1324反编译后,可以发现在执行以上代码,第一行和第三行是可能执行的后续代码。

Pasted image 20231112183803

但是对第三行0x55555567d550反编译,发现超出了函数的范围

现在对第一行进行分析,jmpq *0x2382(%rip),这个就是跳转到rip+0x2382存储的位置,0x2382(%rip)的值为0x555555680088(就下一条指令的地址+0x2382,当前指令执行前rip会移动到下一条指令位置),然后我们读取这个地址的信息

Pasted image 20231112183858

要跳转的地址是0x00007ffff7cbf8f0,毕竟是小端对齐(高位的数据会放在低位),也可以直接格式化数据像下面这样

Pasted image 20231112183906

让我们看看0x00007ffff7cbf8f0地址是什么内容

Pasted image 20231112183912

现在可以确定___lldb_unnamed_symbol1324是printf函数

Pasted image 20231112183921

Pasted image 20231112183932

Pasted image 20231112183939

Pasted image 20231112183947

Pasted image 20231112183954

imgdiff_debug text段长度是0x00000000000a0654,起始位置是0x0000000000088ec0,装载的虚拟内存地址是0x0000555555554000(此时用这个地址加上ELF text段的开始地址就可以获取的text段在虚拟内存的地址了,后续步骤是我想从另一个角度再验证一下,确保这个想法是正确的),main函数的位置在text段的0x0000000000088ff0处,与text段起始位置偏移0x130,mian函数在虚拟内存中的存储位置是0x5555555dcff0,那么虚拟内存中text段的位置是0x5555555DCEC0 - 0x55555567D5140x55555567d550所处的位置确实已经超出了text段的范围

Pasted image 20231112184129

目前没有办法找到0x55555567d550地址会执行的指令,实际上它有可能在正常情况下根本就不会执行,因此现在使用step-in向后执行指令,观察程序在这块执行的逻辑。

Pasted image 20231112184136

Pasted image 20231112184143

单步调试跳转后,确认了___lldb_unnamed_symbol1324确实执行的是printf方法!上面的推断是正确的!

Pasted image 20231112184152

打印出了deflate_slow_debug方法的第一条信息,后续我们要使用它的信息去验证变量信息是否正确。

Pasted image 20231112184159

0x55555567d550处的指令可能是异常处理相关的。

Pasted image 20231112184206

可以确认,目前可以正常获取变量信息,它们将作为条件断点使用。

由于此次编译imgdiff保留了原本符号的名称,因此没有必要去找执行insert_string真正的方法了。

是一个內联函数,让我们看看它的代码:

Pasted image 20231112184213

insert_string最后实际执行的应该是insert_string_c或者insert_string_simd,但是很不巧,它们也都是內联函数,现在我们通过确认insert_string_cinsert_string_simd符号存在的情况来看imgdiff究竟执行的是哪个代码。

Pasted image 20231112184221

imgdiff使用的是insert_string_c,由于这个方法是內联函数,因此我们只能通过对address打断点的方式观察它的执行过程了(insert_string_c在內联展开之后,由于编译器进行了优化,所以它的汇编代码散布在很多不同的区域中)。

当执行地址为0x555555673fd9、0x555555673fc2、0x555555674212、0x55555567406d,且s->strstart == 42921时,停止程序运行

Lisp

#define UPDATE_HASH(s, h, c) (h = (((h) << s->hash_shift) ^ (c)) & s->hash_mask)

我们在调试的时候需要注意h (hash)的值

Recovery选择连接小米助手会中断shell,导致无法进行调试,因此我们不打算通过sideload进行OTA包的安装操作,而是通过指令(Miui Recovery被设计成可以多次执行)。

Apache

adb root; adb shell;
mkdir /tmp
mount -t tmpfs -o size=2000m tmpfs /tmp
exit
adb push miui-blockota-merlin_pre_global-22.3.24-22.3.25-17008cfb07-12.0.zip /tmp/
adb root; adb shell;
recovery --update_package=/tmp/miui-blockota-merlin_pre_global-22.3.24-22.3.25-17008cfb07-12.0.zip --export_validate=ovea
ps -ef | grep recovery

Pasted image 20231112184231

这样就可以对Updater进行调试了!~ 

有个小技巧,在升级进度到27%的时候进行打断时,它离我们的目标代码最接近。

参考资料

官方

https://lldb.llvm.org/use/map.html

https://clang.llvm.org/docs/CrossCompilation.html

https://source.android.com/devices/tech/debug/gdb

https://lldb.llvm.org/use/tutorial.html

https://llvm.org/devmtg/2016-03/Tutorials/LLDB-tutorial.pdf

其他(建议和官方文档参考着看,以官方文档为主)

https://static.linaro.org/connect/bud17/Presentations/BUD17-310%20-%20Introducing%20LLDB%20for%20Linux%20on%20Arm%20and%20AArch64.pdf

https://casatwy.com/shi-yong-lldbdiao-shi-cheng-xu-er.html

https://zhuanlan.zhihu.com/p/106415182

https://github.com/tuoxie007/play_with_llvm/blob/master/ch03.md

https://blog.csdn.net/qq_23542165/article/details/121275404

https://liangmc.com/archives/03lldb%E6%B1%87%E7%BC%96%E8%B0%83%E8%AF%95md

https://juejin.cn/post/6872764160640450574

https://www.jianshu.com/p/1005ccfe8fea

http://blog.sina.com.cn/s/blog_6af9566301013xp4.html

原文链接:https://ovea-y.cn/lldb_tool_manul/

相关内容