lldb
简介
LLDB 是 Low Lever Debugger 的简称,翻译成中文应该叫做底层调试器,它是 LLVM 项目的调试器组件。LLVM 是构架编译器 compiler 的框架系统,以 C++ 编写而成,用于优化以任意程序语言编写的程序的编译时间 compile-time、链接时间 link-time、运行时间 run-time 以及空闲时间 idle-time,对开发者保持开放,并兼容已有脚本。如果你了解过 Swift 语言及其作者,那么一定对 Chris Lattner 博士不陌生,那这里为什么要提到他呢?没错 LLVM 开发最初就是由 Chris Lattner 博士主持开展的。
LLDB 支持调试 C、Objective-C 和 C++ 编写的程序。Swift 社区维护了一个版本,增加了对该语言的支持。默认内置于 Xcode 中,LLDB 提供了一组广泛的命令,旨在与老版本的 GDB 命令兼容。除了使用标准配置以外,还可以很容易的自定义 LLDB 命令以满足实际需要。可以在 Xcode 的控制器进入 lldb 调试模式后,输入 help 可以查看所有 Debugger commands,也可以在这个 网站查询
breakpoint 命令
断点命令,在平时使用 Xcode 开发的过程中,我们设置断点一般都是通过在界面上点击代码所在的行数设置的。其实也可以使用 lldb 的命令来设置断点。
根据名字设置断点
设置C函数名断点
在 touchedBegan 方法中,我们从界面设置了一个断点,进入了 lldb 调试模式,再输入以下命令设置函数断点:
breakpoint set -n "test1",-n 是 –name 的缩写,然后点击继续按钮,或者输入 lldb 命令 c 继续执行,可以看到其实 Xcode 集成的很多的调试功能就是从 lldb 这来的。调试下一步就在 lldb 输入 n 或者 s,n 遇到子函数不会进入,s 遇到子函数会进入。
设置 OC 方法名断点
先搭建个如图所示的简单界面
然后暂停程序,在控制台输入如下命令:
breakpoint set -n "-[ViewController save:]" -n "-[ViewController pause:]" -n "-[ViewController continue:]"
这样就设置了一组断点,它有三个断点。可以使用以下命令查看当前所有断点:
breakpoint list
禁用和启用断点
禁用断点,后面的数字是 breakpoint list 显示的编号,可以同时禁用 1 组,也可以单独禁用组里的某个
breakpoint disable 编号
启用断点 breakpoint enalbe 编号
删除断点
删除断点 breakpoint delete 编号,这里有个小细节,我们无法删除一组断点里面的某一个,只能删除一整组断点。如果 delete 后面跟的是某个组里面的某个断点,等同于禁用这个断点。如果不输入编号,那就相当于删除所有断点
根据方法设置断点
刚刚我们设置的断点,是某个类的某些具体方法,我们也可以设置与类无关的方法断点:
breakpoint set --selector touchesBegan:withEvent:
可以看到设置了 98 个断点,但我们的项目中没有明显调用这个方法,其实是系统库 UIKit 中用到了这个方法,lldb 断到系统库里面去了,可以使用 breakpoint list 查看所有 98 个断点
如果不想这样把断点断到系统库里面去,可以指定文件设置方法断点,我们在 ViewController 里加上 touchesBegan: 方法:
breakpoint set --file ViewController.m --selector touchesBegan:withEvent:
正则匹配设置断点
可以输入 help breakpoint set 看到 -r 参数的介绍
1 | Set the breakpoint by function name, evaluating a regular-expression to find the function name(s). |
breakpoint set -r save: 这样就会根据 -r 后面的参数去寻找所有能匹配到的方法
我们明明只有在 ViewController 里面有一个 save: 方法,为什么显示有 11 处断点呢,使用 breakpoint list 查看
可以看到除了第一个是我们工程里面的断点,其他的断点都不在我们的工程里。都断到系统库里去了。
同样的,我们可以配合指定文件来使用这个命令
breakpoint set -r save: --file ViewController.m
断点执行命令
breakpoint command add 编号 可以给断点加一些命令,这样断点来到的时候,可以自动执行这些命令,能否为我们节省一些操作
以上这些所有的 lldb 命令,都可以缩写,比如:
breakpoint set -r save: --file ViewController.m可以缩写成b -r save: -f ViewController.mbreakpoint list可以缩写成break libreakpoint disable 8.1可以缩写成bre dis 8.1breakpoint enable 8.1可以缩写成bre en 8.1
反正你可以去各种尝试,多一个少一个字母或许都能行,比较随意,比起普通的命令错一个字符都不行还是蛮牛逼的。
expression 命令
我们平时在 lldb 控制台里面干的最多的是什么?po 打印某个对象吧,那么这个 po 到底是什么意思呢?可以在终端输入如下命令查看一下:
help po
其实我们平时使用最多的 po 指令就是 expression -O 指令的缩写。也有人喜欢用 p 打印,那么 p 又是什么呢,输入 help p 发现 p 就是 expression 指令的缩写,直接查看 help expression 可以查看到 -O 的意思,就是指定语言的 description 方法,所以我们平时使用 po OC 对象,就是执行 OC 对象的 description 方法。
通过 help po 我们知道就是在主线程执行表达式,那么我们可以试试在 lldb 中修改一些常见的属性,比如 self.view.backgroundColor。首先来到 touchesBegan: 断点,在 lldb 输入如下指令
p self.view.backgroundColor = [UIColor redColor];
发现确实执行了这句代码,但是却报了一个让人疑惑的错误,而且过掉断点之后并没有任何效果,当然了明明执行都报错了,怎么可能会有效果呢。。。但是这一行代码,在程序中运行的话,肯定是没有问题的,我们查看 UIView 的头文件也可以发现它确定是有 backgroundColor 这个属性的。至于为什么我在网上搜索半天没有找到任何答案,感觉可能是 lldb 的 bug?虽然这句代码无效,但是我们依然可以尝试使用其他的方式来修改,比如:
p [self.view setValue:[UIColor blueColor] forKey:@"backgroundColor"]
KVC 还是牛逼啊,还有一种办法,我们知道 UIView 真正用来显示的是它的 layer,修改 layer:
p self.view.layer.backgroundColor = [UIColor yellowColor].CGColor
再来一个案例,我么创建一个 persons 数组,存放一组 Person 模型,然后通过 lldb 的 expression 命令动态添加一个 Person。
可以看到 lldb 的 expression 命令后面可以跟多条代码,用来动态调试真是太方便了
bt 命令
进入 lldb 输入 help bt 可以看到,bt 是查看当前线程调用栈的意思
新建以下这四个方法,并给 demo4 方法下个断点,然后点击屏幕进入断点
输入 bt 可以看到当前方法的调用栈
输入 up 可以查看调用的上一个方法,连源码都能看到,当然在逆向中是看不到源码的,只能看到汇编代码。输入 down 可以返回到下一个方法
如果嫌一步一步的 up,down 太慢了,也可以直接使用 frame select 编号 直接到位
这样上下切换栈帧是为什么呢,当然是为了查看当前栈的一些信息,比如当前栈用到的变量。使用 frame variable 查看,我们知道 Objective-C 方法有两个隐藏的参数 self 和 _cmd, event 就是 demo1 的显示参数。如果不知道 frame variable 是什么意思,也可以通过 lldb 的帮助文档查看 help frame variable
thread return 命令
当我们调试到某一帧的时候,如果不想让程序之后的代码,可以使用 thread return 线程返回,需要注意 return 后面根据实际情况返回对应的值,例如:
如果没有使用 thread return NO 命令,那么肯定是打印正在运行的,当断点来到 isRuning 的时候,我们强制返回了 NO,所以在调试过程中改变了程序的执行流程
以上所有方式在逆向中都没法使用。。。因为逆向的项目我们都拿不到符号
那么逆向应该如何玩 lldb?内存断点
watchpoint 内存断点
内存断点,不仅可以给方法断点,还可以给变量断点
watchpoint 根据变量名设置内存断点
watchpoint set variable p1->_name如图所示,Person 类有一个 name 属性,但是我们设置内存断点的时候,属性是没法用的,因为属性的本质是 getter 和 setter 加下划线的成员变量嘛。所以需要使用 p1->_name 设置成功之后,当程序中有给 p1.name 赋值的地方,都会来到断点。下图马上给 p1.name 赋值 lldb 就提示 Watchpoint 1 hit 命中了。然后还会显示 old value 和 new value
watchpoint 根据内存地址设置内存断点
watchpoint set expression 地址
还是刚刚的例子,我们在 p1 实例化之后,它在内存中的地址就确定了,那么它的成员变量的地址也是确定的,我们可以拿到 p1.name 的内存地址,根据它的内存地址设置断点。当程序对 p1.name 进行修改的时候,都会来到断点,可以看到下面的断点来了两次,一次是在 viewDidload 中,另一次在 touchesBegan 里面进行的修改
target stop-hook 命令
这个命令有点类似 breakpoint 的断点执行命令,只不过 breakpoint 的断点执行命令需要指定某个断点,而这个命令是全局的,只要是断点来了,就会执行后面的命令
通过 log 可以看到 stop-hook 命令一样可以 add、delte、disable、enable、list 具体每条命令怎么使用,可以继续使用 help 命令查看,比如 add,帮助文档很详细
我们这里全局添加一条 frame variable 指令,然后走到断点试试
可以看到断点一过来就会自动执行 frame variable 指令,打印参数变量
这里关于删除 stop-hook 指令,它还有一个快捷的命令 undisplay 编号 相比 target stop-hook delete 编号 要简短不少
.lldbinit文件
可以将一些经常使用的命令配置到 .lldbinit 文件里面,这样就不用每次都去添加一些命令。这个文件一般放在用户目录下,点开头的代表是隐藏文件,如果没有可以自己新建一个。如图我们在里面添加这样一句指令
再次运行项目,运行到断点就可以看到相关打印
image 指令
这个是查看当前进程加载的镜像相关信息,什么是镜像,一个 Mach-O 文件就是一个镜像。我们的 APP 也是一个镜像。我们先来查看一下我们 APP 中的某个类的信息
image look up -t name
image list
输入 help image list 查看命令的介绍
可以看到 image list 意思是列出当前可执行文件和依赖的共享库镜像,在逆向中经常使用这个命令来获取我们 APP 在内存中的位置。
逆向中下内存地址断点
如图我们在 touchesBegan: 方法中调用了 demo4 方法
现在我们将 APP 的 Mach-O 文件用 hopper 打开,假设对我们的 demo4 方法很感兴趣,想要下一个断点。
hopper 这里的地址只是这个方法相对于我们的 APP 首地址的偏移加上了虚拟地址,想要获取到这个方法在我们手机里的真实内存地址,还需要加上我们 APP 首地址在我们内存中的地址。通过 image list 命令查看第一个就是我们 APP 的首地址了。
9544
虚拟地址在我们 Mach-O 文件的 __PAGEZERO 段可以看到
那么这个方法在我们手机内存中的地址就是:
0x102964000 + 0x100005b8c - 0x100000000 = 0x102969B8C
我们进入 lldb 模式,输入以下内存地址断点
breakpoint set --address 0x102969B8C
断点设置成功,然后我们退出 lldb 模式,点击屏幕,会发现断点果然来了
这里提一下 ASLR,是一种防范内存损坏漏洞被利用的计算机安全技术,就是我们的 APP 每次加载进手机的真实内存的时候,位置不是固定的,所以我们每次 APP 运行起来后通过 image list 看到的我们 APP 的首地址是随机的。
我们 APP 的 Mach-O 文件中的内容,相对于 Mach-O 文件的首地址来说都是固定不变的,APP 加载进内存之后首地址就确定了,那么 Mach-O 文件中的内容,比如方法实现,函数实现,常量等数据在内存中的位置都可以找到了。。。
可以在 ViewController.m 文件中定义一个全局变量,然后我们在 Mach-O 文件看能否找到它。如图定义一个全局变量,然后运行起来之后,打印它所在的地址,和当前 APP 的首地址,它的地址减去 APP 的首地址就是它相对于我们 APP 的 Mach-O 文件的偏移了。
0x102b959f0 - 0x102b8c000 = 0x99F0
再使用 MachOView 查看我们 APP 的 Mach-O 文件,找到地址 0x99F0,看看是不是这个 a 的值 0x123456678
有些同学可能会好奇,这个怎么是这个样子,其实这是因为机器的存取数据方式决定的,有些是大端模式,而有些机器是小端模式,这里贴一下百度的 大小端模式
从这里也可以看出我们的全局变量是存放在__DATA,__data节的。
