简介
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参数的介绍
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.m
breakpoint list可以缩写成break li
breakpoint disable 8.1可以缩写成bre dis 8.1
breakpoint 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方法,所以我们平时使用poOC对象,就是执行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查看,我们知道OC方法有两个隐藏的参数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的首地址了。
虚拟地址在我们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节的。