LLDB

日常开发中我们经常使用 Xcode 的断点,这一强大的功能解决了我们开发中 99% 的难题,但是我们的断点其实只是 LLDB 中的一小部分而已。

1、什么是 LLDB?

LLDB 是英文 Low Lever Debug 的缩写,是 XCode 内置的为我们开发者提供的调试工具,它与LLVM编译器一起,存在于主窗口底部的控制台中,能够带给我们更丰富的流程控制和数据检测的调试功能。

2、 LLDB 命令行断点设置(正向开发)

新建一个工程,写了下方代码,给一个 OC 方法断点,这是我们平常使用 Xcode 界面下的断点,那现在我们尝试使用控制台 LLDB 下。

LLDB_第1张图片
断点
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_第2张图片
3个按钮

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
LLDB_第3张图片
断点结果
3、禁用断点

因为现在是通过 LLDB 直接下的断点,界面上已经不能对断点进行操作了,比如:禁用断点,删除断点等等。现在就需要用 LLDB 的指令去完成这些事情。

LLDB 输入:breakpoint disable 1,代表的是禁用第一组全部断点,再经过测试,发现我们刚才3个点击事件的断点全部失效了。

LLDB_第4张图片
禁用断点

断点未生效。

LLDB_第5张图片
禁用断点测试
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 状态。

禁用1组中1个断点
6、界面断点 LLDB 查看

在界面上下的断点同样也能在LLDB上使用 breakpoint list 看到。

LLDB_第6张图片
界面断点
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 就可以查看其他的指令了。

LLDB_第7张图片
help

比如:help breakpoint,就是查看 breakpoint下的所有指令。

LLDB_第8张图片
help breakpoint
9、给某一个方法设置断点

1、给工程内所有 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; 设置断点。

LLDB 上 输入 : breakpoint set --selector touchesBegan:withEvent:,回车控制台打印了 Breakpoint 4: 94 locations. 第4组断点,一共设置了94处。

点击屏幕后,很显然这个不是刚才工程内的方法,这是因为UIKit框架中,基本上所有的可操作的控件都会有手势,所以当前断点进入系统的方法了。

LLDB_第9张图片
给一个方法设置断点

2、现在需要给特定文件里的一个方法设置断点,比如 ViewController.m 里的 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;

我们先删除所有的断点后在 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: 的方法被下了断点,一些系统方法也被下了断点。

LLDB_第10张图片
设置包含字符串的断点

当然我们也可以指定文件去下断点。比如:breakpoint set --file ViewController.m -r Clicked:

LLDB_第11张图片
设置一个文件包含字符串的断点
11、断点设置的简写

b -f ViewController.m -r Clicked: 等同于:breakpoint set --file ViewController.m -r Clicked:

  • bbreakpoint set
  • -f--file
  • -n--name

但是 breakpoint listbreakpoint disable 等只能简写成 break libreak dis ,因为简写的 b 后面默认会带上 set 的。

3、 LLDB 一些指令的含义和进阶使用方法

我们经常 Xcode 调试的时候在 LLDB 上输入:p xxx 或者 输入了po xxx,就获取了一个对象的值。那么 p 或者 po 含义到底是什么呢?

LLDB输入help phelp po,查看说明如下:

  • pexpression 的简写。LLDB上输入的 p xxx执行 xxx
  • poexpression -O的缩写。再输入 help expressionpo : expression --object-descriptionLLDB 上输入的 po xxx执行 xxx 的 description 方法
LLDB_第12张图片
help p

既然 p 是执行一个方法,那么 LLDB 输入:p self.view.backgroundColor = [UIColor redColor]; ,回车,过掉断点 self.view 就变成红色的了。

LLDB_第13张图片
红色背景

当然也可以是使用 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_第14张图片
调用队栈查看

想要回到上一个方法 LLDB 输入 up,去下一个 down

LLDB_第15张图片
堆栈跳转

既然使用使用了 bt 查看了队栈,那也可以使用 frame select 1frame select 3 直接跳转相应的队栈。

frame select

如果想查看 lldbText2 中的参数可以使用 frame variable,这也一定程度告诉了我们,方法中的 self 不一定是我们当前的 ViewController

image.png

更改一下上方代码,将 lldbText1lldbText2lldbText3lldbText4都添加上参数 str,并且给 lldbText3 设置一个断点。

LLDB_第16张图片
代码修改

运行后点击屏幕,进入断点 lldbText3,如果我们使用 up 指令进入了 lldbText2 然后修改了 strlldbText4 输出的结果会更改吗?

结果是不会的,因为我们 lldbText2 方法已经走过了,相当于过去的时间我们无法修改一样。

LLDB_第17张图片
修改走过的队栈参数

但是一定要修改呢?

我们可以使用 thread return 但是这个方法执行过后进入了 lldbText2,修改了 lldbText2 中的 str 后过掉断点,并没有打印出 lldbText4,其实thread return 执行后,给当前的方法后面添加了一个return,所以就不会往下继续执行了。

LLDB_第18张图片
thread 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 *)touches withEvent:(UIEvent *)event 代码如下:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    Person *p1 = [self.dataArray firstObject];
    p1.name = @"ABC";
}

[super viewDidLoad]; 这行添加断点,重新运行,然后使用 n 或者 Xcode 工具让 LLDB 走到 p1 创建完毕。

LLDB_第19张图片
p1创建完毕

p1 创建完毕后,p1 的对象已经存在堆区,指向 p1 对象的指针因为 [self.dataArray addObject:p1]; 方法,在 viewDidLoad 方法走完之后依旧被保存。

现在需要给 p1name 属性下断点,我们可以使用 watchpoint set variable p1->_namename 下内存断点。回车后 LLDB 清楚的告诉了我们:内存断点的地址、指针占用的大小(OC对象8字节)。

name的内存断点

过掉断点,点击屏幕触发 touchesBeganp1.name = @"ABC" 赋值方法,这样一个内存断点就触发了, po 上方打印的两个地址,我们就能看旧值和新值。然后输入 bt 查看调用堆栈,就能清楚的看到调用队栈。

触发内存断点
LLDB_第20张图片
队栈信息查看

我们也可以使用 p1->_name 内存的指针地址给 p1->_name 下内存断点。

  • 获取 p1->_name 的内存地址:&p1->_name
  • 下内存断点:使用watchpoint set expression 加上p1->_name 的内存地址
  • 点击屏幕触发赋值方法验证
LLDB_第21张图片
指针地址下内存断点

同符号断点一样,内存断点可以查看和删除

  • 查看内存断点:watchpoint list
  • 删除内存断点:watchpoint delete
LLDB_第22张图片
image.png
2、给一个断点添加多指令

修改 touchesBegan:withEvent: 中的代码如下。

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"点击了界面111");
    NSLog(@"点击了界面222");
    [self lldbText1:@"123"];
}

然后给 lldbText1: 添加一个符号断点 b -[ViewController lldbText1:]

lldbText1 符号断点

LLDB 上输入 breakpoint list ,然后给断点1添加多个指令 breakpoint command add 1
然后在 > 后输入指令,每输入完成一个指令后回车进行下一个指令输入,想要结束,输入DONE即可。

LLDB_第23张图片
指令集

文字版方便复制

(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

过掉断点验证一下,进入断点的同时执行了刚才添加的指令,再过掉断点,看到屏幕变红色了。

LLDB_第24张图片
image.png

删除断点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

LLDB_第25张图片
stop-hook

一些其他指令

  • 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 随意下一个断点。

LLDB_第26张图片
脚本断点
4、image指令
  • image list :查看所有加载的库
  • image lookup -t 类名 查看一个类的头文件信息
5、方法断点

逆向中没有符号文件,那我们怎么给方法下断点呢?

这时候我们就需要借助一个工具 Hopper Disassembler 了,将我们工程的LLDB.MachO文件拖入Hopper Disassembler

LLDB_第27张图片
image.png
Hopper Disassembler

需要给 ViewControllerlldbText1: 的方法添加一个断点。该方法的的内存地址为0x100001980

image.png

尝试下内存断点 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:

MachO地址
LLDB_第28张图片
方法断点测试
ASLR

在计算机科学中,地址空间配置随机加载(英语:Address space layout randomization,缩写 ASLR ,又称 地址空间配置随机化地址空间布局随机化)是一种防范内存损坏漏洞被利用的计算机安全技术。

我们知道 :物理地址 = ASLR + 方法虚拟地址

所以我们当前运行的 MachO 内存地址为 0x102f10000,所以 ASLR0x2f10000,然后我们直接加上lldbText1: 的函数地址 0x100001980(我们刚才 Hopper Disassembler 获取到的虚拟地址) ,所以 LLDB 上输入b -a 0x2f10000+0x100001980 回车,测试,我们也能断到这个 lldbText1: 的方法。

LLDB_第29张图片
测试结果

以上就是我们 LLDB 在正向和逆向中的不同使用方法,有问题欢迎指出。

你可能感兴趣的:(LLDB)