日常开发中我们经常使用 Xcode
的断点,这一强大的功能解决了我们开发中 99%
的难题,但是我们的断点其实只是 LLDB
中的一小部分而已。
1、什么是 LLDB?
LLDB
是英文 Low Lever Debug
的缩写,是 XCode
内置的为我们开发者提供的调试工具,它与LLVM编译器一起,存在于主窗口底部的控制台中,能够带给我们更丰富的流程控制和数据检测的调试功能。
2、 LLDB 命令行断点设置(正向开发)
新建一个工程,写了下方代码,给一个 OC
方法断点,这是我们平常使用 Xcode
界面下的断点,那现在我们尝试使用控制台 LLDB
下。
1、给函数下断点
LLDB
输入 breakpoint set -n test1
后回车发现控制台打印了一些东西:
-
Breakpoint 2
:这个断点是第二个断点 -
where LLDB调试 test1 + 11 at ViewController.m:23:5
:告诉了断点的位置 -
address = 0x000000010fb07ecb
:断点的地址
(lldb) breakpoint set -n test1
Breakpoint 2: where = LLDB调试`test1 + 11 at ViewController.m:23:5, address = 0x000000010fb07ecb
(lldb)
2、给方法下断点
上个是给函数下断点,我们现在给OC方法下断点。在界面上创建三个 UIButton
,然后添加点击方法。
LLDB
输入 :
breakpoint set -n "[ViewController onWeChatClicked:]" -n "[ViewController onQQClicked:]" -n "[ViewController onSinaClicked:]"
回车,发现控制台打印了 Breakpoint 1: 3 locations
,告诉我们:断点1:在3个位置添加了。
那我们再在LLDB
上输入breakpoint list
就能显示我们下的断点的详细信息,然后输入 c
就可以过掉断点了,点击按钮尝试一下发现和界面的功能是一样的。
(lldb) breakpoint set -n "[ViewController onWeChatClicked:]" -n "[ViewController onQQClicked:]" -n "[ViewController onSinaClicked:]"
Breakpoint 1: 3 locations.
(lldb) breakpoint list
Current breakpoints:
1: names = {'[ViewController onWeChatClicked:]', '[ViewController onWeChatClicked:]', '[ViewController onQQClicked:]', '[ViewController onQQClicked:]', '[ViewController onSinaClicked:]', '[ViewController onSinaClicked:]'}, locations = 3, resolved = 3, hit count = 0
1.1: where = LLDB调试`-[ViewController onWeChatClicked:] + 43 at ViewController.m:22:5, address = 0x0000000102ff0dab, resolved, hit count = 0
1.2: where = LLDB调试`-[ViewController onQQClicked:] + 43 at ViewController.m:27:5, address = 0x0000000102ff0dfb, resolved, hit count = 0
1.3: where = LLDB调试`-[ViewController onSinaClicked:] + 43 at ViewController.m:31:5, address = 0x0000000102ff0e4b, resolved, hit count = 0
(lldb) c
3、禁用断点
因为现在是通过 LLDB
直接下的断点,界面上已经不能对断点进行操作了,比如:禁用断点,删除断点等等。现在就需要用 LLDB
的指令去完成这些事情。
LLDB
输入:breakpoint disable 1
,代表的是禁用第一组全部断点,再经过测试,发现我们刚才3个点击事件的断点全部失效了。
断点未生效。
4、启用断点
然后我们再次启用断点,LLDB
输入:breakpoint enable 1
,就启用了第一组断点。
(lldb) breakpoint enable 1
1 breakpoints enabled.
(lldb) breakpoint list
Current breakpoints:
1: names = {'[ViewController onWeChatClicked:]', '[ViewController onWeChatClicked:]', '[ViewController onQQClicked:]', '[ViewController onQQClicked:]', '[ViewController onSinaClicked:]', '[ViewController onSinaClicked:]'}, locations = 3, resolved = 3, hit count = 3
1.1: where = LLDB调试`-[ViewController onWeChatClicked:] + 43 at ViewController.m:22:5, address = 0x0000000102ff0dab, resolved, hit count = 1
1.2: where = LLDB调试`-[ViewController onQQClicked:] + 43 at ViewController.m:27:5, address = 0x0000000102ff0dfb, resolved, hit count = 1
1.3: where = LLDB调试`-[ViewController onSinaClicked:] + 43 at ViewController.m:31:5, address = 0x0000000102ff0e4b, resolved, hit count = 1
(lldb)
5、禁用单个断点
第一组中有3个断点,现在想只禁用其中一个,LLDB
输入:breakpoint disable 1.1
,回车后发现,第一组 1.1
的断点处于 disabled
状态。
6、界面断点 LLDB 查看
在界面上下的断点同样也能在LLDB
上使用 breakpoint list
看到。
7、删除断点
添加了断点,当然还需要能删除断点。
LLDB
输入:breakpoint delete 1.1
,回车,但是控制台却打印的是 0 breakpoints deleted; 1 breakpoint locations disabled.
。
LLDB
输入breakpoint list
查看,发现 1.1
是禁用状态,这是因为想要删除只能删除一组断点,不能删除一组中的一个,就算对一组中的一个断点输入了删除指令,LLDB
只会将这个断点禁用。
既然只能删除一组,那我们在LLDB
输入:breakpoint delete 1
,回车,打印了 1 breakpoints deleted; 0 breakpoint locations disabled.
, 1个断点被删除,0个断点被禁用。
LLDB
输入:breakpoint delete
删除所有断点。
(lldb) breakpoint delete
About to delete all breakpoints, do you want to do that?: [Y/n] y
All breakpoints removed. (2 breakpoints)
(lldb)
8、查看 LLDB 的其他指令
LLDB
上 输入 : help
就可以查看其他的指令了。
比如:help breakpoint
,就是查看 breakpoint
下的所有指令。
9、给某一个方法设置断点
1、给工程内所有 -(void)touchesBegan:(NSSet
设置断点。
LLDB
上 输入 : breakpoint set --selector touchesBegan:withEvent:
,回车控制台打印了 Breakpoint 4: 94 locations.
第4组断点,一共设置了94处。
点击屏幕后,很显然这个不是刚才工程内的方法,这是因为UIKit
框架中,基本上所有的可操作的控件都会有手势,所以当前断点进入系统的方法了。
2、现在需要给特定文件里的一个方法设置断点,比如 ViewController.m
里的 -(void)touchesBegan:(NSSet
我们先删除所有的断点后在 LLDB
上 输入 :breakpoint set --file ViewController.m --selector touchesBegan:withEvent:
,回车就显示我们这个断点设置上了。
(lldb) breakpoint set --file ViewController.m --selector touchesBegan:withEvent:
Breakpoint 8: where = LLDB调试`-[ViewController touchesBegan:withEvent:] + 77 at ViewController.m:40:5, address = 0x0000000102ff0edd
(lldb)
10、给包含某一个字符串的所有方法设置断点
现在需要给包含Clicked:
的方法下断点。
LLDB
输入:breakpoint set -r Clicked:
,回车,发现有25个地方下了断点了。
breakpoint list
查看一下,看到了除了我们当前 ViewController.m
文件中包含 Clicked:
的方法被下了断点,一些系统方法也被下了断点。
当然我们也可以指定文件去下断点。比如:breakpoint set --file ViewController.m -r Clicked:
11、断点设置的简写
b -f ViewController.m -r Clicked:
等同于:breakpoint set --file ViewController.m -r Clicked:
-
b
:breakpoint set
-
-f
:--file
-
-n
:--name
但是 breakpoint list
、breakpoint disable
等只能简写成 break li
、break dis
,因为简写的 b
后面默认会带上 set
的。
3、 LLDB 一些指令的含义和进阶使用方法
我们经常 Xcode
调试的时候在 LLDB
上输入:p xxx
或者 输入了po xxx
,就获取了一个对象的值。那么 p
或者 po
含义到底是什么呢?
LLDB
输入help p
和 help po
,查看说明如下:
-
p
是expression
的简写。LLDB
上输入的p xxx
为 执行 xxx -
po
是expression -O
的缩写。再输入help expression
,po
:expression --object-description
。LLDB
上输入的po xxx
为 执行 xxx 的 description 方法
既然 p
是执行一个方法,那么 LLDB
输入:p self.view.backgroundColor = [UIColor redColor];
,回车,过掉断点 self.view
就变成红色的了。
当然也可以是使用 p
指令做更多操作。新建一个Person
类如下:
//
// Person.h
// LLDB调试
//
// Created by ABC on 2019/10/27.
// Copyright © 2019 ABC. All rights reserved.
//
#import
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic,strong) NSString *name;
@property (nonatomic,assign) int age;
@end
NS_ASSUME_NONNULL_END
然后ViewController
上有个 @property (nonatomic,strong) NSMutableArray *dataArray;
,接下来操作如下方所示(这里就不用图片了,方便复制):
(lldb) b -f ViewController.m -r touch
Breakpoint 1: where = LLDB调试`-[ViewController touchesBegan:withEvent:] + 77 at ViewController.m:43:5, address = 0x0000000106421c0d
(lldb) c
Process 2221 resuming
(lldb) po self
(lldb) po self.dataArray
<__NSArrayM 0x600003705440>(
)
(lldb) p [self.dataArray addObject:[Person new]];
(lldb) po self.dataArray
<__NSArrayM 0x600003705440>(
)
(lldb) p self.dataArray.lastObject;
(Person *) $3 = 0x000060000395b820
(lldb) p [(Person *)$3 setValue:@"张三" forKey:@"name"];
(lldb) p (Person *)self.dataArray.lastObject
(Person *) $4 = 0x000060000395b820
(lldb) p $4.name;
(__NSCFString *) $5 = 0x0000600003972d40 @"张三"
(lldb) p $4.name = @"李四";
(__NSCFString *) $6 = 0x0000600003972500 @"李四"
(lldb) p $4.name;
(__NSCFString *) $7 = 0x0000600003972500 @"李四"
(lldb) p Person *person = [Person new];person.name = @"王五";person.age = 12;[self.dataArray addObject:person];
(lldb) p self.dataArray
(__NSArrayM *) $8 = 0x0000600003705440 @"2 elements"
(lldb) p (Person *)self.dataArray.lastObject
(Person *) $9 = 0x0000600003972dc0
(lldb) p $9.name;
(__NSCFString *) $10 = 0x0000600003972da0 @"王五"
(lldb) p $9.age;
(int) $11 = 12
(lldb)
上方可以看到,我们可以使用LLDB
给一个数组里面添加对象,也可以使用 LLDB
生成一个对象,然后给对象属性赋值,最后再添加到数组里面。不得不说 LLDB
的强大。
注意:(Person *)self.dataArray.lastObject
只要拿值的时候给上对象的类加上它的类型,比如:(Person *)
,我们就可以直接使用返回的 $4
对象取其属性的值 $4.name
了。
接下来查看函数调用栈。在刚才的 ViewController.m
中添加下方代码(有相同的替换一下)。
- (void)lldbText1 {
[self lldbText2];
}
- (void)lldbText2 {
[self lldbText3];
}
- (void)lldbText3 {
[self lldbText4];
}
- (void)lldbText4 {
NSLog(@"%s",__func__);
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"点击了界面111");
NSLog(@"点击了界面222");
[self lldbText1];
}
控制台给 - (void)lldbText3;
方法设置断点:b -f ViewController.m -r lldbText3
,设置好了,点击屏幕就进入了 - (void)lldbText3;
方法,想要查看调用队栈,LLDB
输入 bt
,显示如下:
想要回到上一个方法 LLDB
输入 up
,去下一个 down
。
既然使用使用了 bt
查看了队栈,那也可以使用 frame select 1
、frame select 3
直接跳转相应的队栈。
如果想查看 lldbText2
中的参数可以使用 frame variable
,这也一定程度告诉了我们,方法中的 self
不一定是我们当前的 ViewController
。
更改一下上方代码,将 lldbText1
、lldbText2
、lldbText3
、lldbText4
都添加上参数 str
,并且给 lldbText3
设置一个断点。
运行后点击屏幕,进入断点 lldbText3
,如果我们使用 up
指令进入了 lldbText2
然后修改了 str
,lldbText4
输出的结果会更改吗?
结果是不会的,因为我们 lldbText2
方法已经走过了,相当于过去的时间我们无法修改一样。
但是一定要修改呢?
我们可以使用 thread return
但是这个方法执行过后进入了 lldbText2
,修改了 lldbText2
中的 str
后过掉断点,并没有打印出 lldbText4
,其实thread return
执行后,给当前的方法后面添加了一个return
,所以就不会往下继续执行了。
还有一些流程控制指令
$continue c
- 单步运行,将子函数当做整体一步执行
$n next
,汇编之下使用ni
- 单步运行,遇到子函数会进去
$s
,汇编之下使用si
其他指令
image list
p
b -[xxx xxx]
x
register read
po
-
stop-hook
让你在每次stop的时候去执行一些命令,只对breadpoint
,watchpoint
4、 LLDB 命令行断点设置(逆向开发)
上述的指令在逆向开发中基本没有什么用~~~~,感觉有点小崩溃。
我们正向开发的时候,是有 符号文件表 的,所有的方法 Xcode
都会帮我们解析,但是上传到 AppStore
的应用是没有符号文件的。比如我们使用 Crash
收集工具的时候(Bugly
、友盟
或者 Xcode
),都会让我们上传符号文件表,不然就无法解析,这也是对应用的一种保护措施。
没有符号表我们只能打印出一堆看不懂的东西,也就无法对其他App
进行下一步调试,也无法使用函数名称去下断点, 所以上述的指令在逆向开发中基本没有什么用。
既然正向有这么多的手段可以使用,当然逆向也有,在逆向工程中,我们可以打 内存断点。
1、内存断点的使用
修改 viewDidLoad
代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [Person new];
p1.name = @"111";
p1.age = 1;
Person *p2 = [Person new];
p2.name = @"222";
p2.age = 2;
Person *p3 = [Person new];
p3.name = @"333";
p3.age = 3;
[self.dataArray addObject:p1];
[self.dataArray addObject:p2];
[self.dataArray addObject:p3];
}
修改 touchesBegan:(NSSet
代码如下:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
Person *p1 = [self.dataArray firstObject];
p1.name = @"ABC";
}
给 [super viewDidLoad];
这行添加断点,重新运行,然后使用 n
或者 Xcode
工具让 LLDB
走到 p1
创建完毕。
p1
创建完毕后,p1
的对象已经存在堆区,指向 p1
对象的指针因为 [self.dataArray addObject:p1];
方法,在 viewDidLoad
方法走完之后依旧被保存。
现在需要给 p1
的 name
属性下断点,我们可以使用 watchpoint set variable p1->_name
给 name
下内存断点。回车后 LLDB
清楚的告诉了我们:内存断点的地址、指针占用的大小(OC
对象8字节)。
过掉断点,点击屏幕触发 touchesBegan
的 p1.name = @"ABC"
赋值方法,这样一个内存断点就触发了, po
上方打印的两个地址,我们就能看旧值和新值。然后输入 bt
查看调用堆栈,就能清楚的看到调用队栈。
我们也可以使用 p1->_name
内存的指针地址给 p1->_name
下内存断点。
- 获取
p1->_name
的内存地址:&p1->_name
- 下内存断点:使用
watchpoint set expression
加上p1->_name
的内存地址 - 点击屏幕触发赋值方法验证
同符号断点一样,内存断点可以查看和删除
- 查看内存断点:
watchpoint list
- 删除内存断点:
watchpoint delete
2、给一个断点添加多指令
修改 touchesBegan:withEvent:
中的代码如下。
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"点击了界面111");
NSLog(@"点击了界面222");
[self lldbText1:@"123"];
}
然后给 lldbText1:
添加一个符号断点 b -[ViewController lldbText1:]
。
LLDB
上输入 breakpoint list
,然后给断点1添加多个指令 breakpoint command add 1
,
然后在 >
后输入指令,每输入完成一个指令后回车进行下一个指令输入,想要结束,输入DONE
即可。
文字版方便复制
(lldb) breakpoint command add 1
Enter your debugger command(s). Type 'DONE' to end.
> po self
> p self.view.backgroundColor
> p self.view.backgroundColor = [UIColor redColor];
> DONE
过掉断点验证一下,进入断点的同时执行了刚才添加的指令,再过掉断点,看到屏幕变红色了。
删除断点1的指令集可以使用 breakpoint command delete 1
,查指令集列表 : breakpoint command list
发现已经没有了。
3、给所有断点添加多指令
上一条是给某一个断点添加多个指令,那我们逆向的时候经常需要使用一些指令,我们如果给所有断点添加多指令,只要有断点来了,就执行我们的指令,这样就方便太多了。
比如我们常使用的指令: frame variable
(查看一个方法下的所有参数)。就可以通过 target stop-hook add -o "frame variable"
给所有断点添加这样一个指令。
-
target stop-hook
目标是断点 -
add
添加 -
-o
是--one
的意思,添加一条指令 -
"frame variable"
需要添加的指令
回车,过掉断点,给touchesBegan:withEvent:
添加一个断点, 点击屏幕后进入断点后执行了指令 frame variable
。
一些其他指令
-
target stop-hook list
查看列表 -
target stop-hook delete 1
删除第一组断点 -
target stop-hook delete
删除所有断点 -
target stop-hook disable 1
禁用第一组断点 -
undisplay 1
删除第一组断点,和target stop-hook delete 1
功能一样。
4、给所有断点使用脚本添加多指令
每次Xcode
运行时, LLDB
启动就会加载一个文件 .lldbinit
,在~
目录下输入ls -a
(列出所有文件,包括隐藏文件,.
开头的就是隐藏文件),就能找到(如果没有自己创建一个即可),vi .lldbinit
,然后点击 i
添加我们刚才的指令 target stop-hook add -o "frame variable"
,键盘按 ESC
输入:wq
保存并退出。
因为是给系统的 LLDB
添加的,所以对每个工程都会有效。
然后回到我们的 Xcode
随意下一个断点。
4、image指令
-
image list
:查看所有加载的库 -
image lookup -t 类名
查看一个类的头文件信息
5、方法断点
逆向中没有符号文件,那我们怎么给方法下断点呢?
这时候我们就需要借助一个工具 Hopper Disassembler 了,将我们工程的LLDB.MachO
文件拖入Hopper Disassembler
。
需要给 ViewController
的 lldbText1:
的方法添加一个断点。该方法的的内存地址为0x100001980
。
尝试下内存断点 b -a 0x100001980
,回车 LLDB
告诉我们 :
warning: failed to set breakpoint site at 0x100001980 for breakpoint 2.1: error: 0 sending the breakpoint request
当前断点并没有下成功,那这是为什么呢?
因为我们的方法的内存地址是相对于 MachO
文件在内存中的地址计算的。
lldbText1:
在文件中的偏移其实是 0x1980
,需要正确的下到方法的内存断点上,就需要 MachO
文件在内存中的地址。
当前 Xcode
工程的 LLD
中输入 image list
,第一行就是 MachO
在内存中的地址,然后用这个地址加上 lldbText1:
的偏移量就是真实运行时 lldbText1
内存的地址。
当前运行的工程中的 MachO
的 地址为 0x102f10000
(每次运行都会变),lldbText1:
的偏移量 0x1980
,两者相加 LLDB
输入 b -a 0x102F11980
,回车,过掉断点,点击屏幕测试,断点到了 lldbText1:
。
ASLR
在计算机科学中,地址空间配置随机加载(英语:Address space layout randomization,缩写 ASLR
,又称 地址空间配置随机化 、地址空间布局随机化)是一种防范内存损坏漏洞被利用的计算机安全技术。
我们知道 :物理地址 = ASLR + 方法虚拟地址
所以我们当前运行的 MachO
内存地址为 0x102f10000
,所以 ASLR 为 0x2f10000
,然后我们直接加上lldbText1:
的函数地址 0x100001980
(我们刚才 Hopper Disassembler
获取到的虚拟地址) ,所以 LLDB
上输入b -a 0x2f10000+0x100001980
回车,测试,我们也能断到这个 lldbText1:
的方法。
以上就是我们 LLDB
在正向和逆向中的不同使用方法,有问题欢迎指出。