LLDB

5,692 阅读13分钟

日常开发中我们经常使用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 ,然后添加点击方法。

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

断点结果

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 状态。

禁用1组中1个断点

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

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

help breakpoint

9、给某一个方法设置断点

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

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

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

给一个方法设置断点

2、现在需要给特定文件里的一个方法设置断点,比如 ViewController.m 里的 -(void)touchesBegan:(NSSet<UITouch *> *)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: 的方法被下了断点,一些系统方法也被下了断点。

设置包含字符串的断点

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

设置一个文件包含字符串的断点

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 方法

help p

既然 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 <Foundation/Foundation.h>

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
<ViewController: 0x7fb2d5d07c20>

(lldb) po self.dataArray
<__NSArrayM 0x600003705440>(

)

(lldb) p [self.dataArray addObject:[Person new]];
(lldb) po self.dataArray
<__NSArrayM 0x600003705440>(
<Person: 0x60000395b820>
)

(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<UITouch *> *)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 1frame select 3 直接跳转相应的队栈。

frame select

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

image.png

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

代码修改

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

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

修改走过的队栈参数

但是一定要修改呢?

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

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<UITouch *> *)touches withEvent:(UIEvent *)event 代码如下:

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

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

p1创建完毕

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

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

name的内存断点

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

触发内存断点

队栈信息查看

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

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

指针地址下内存断点

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

  • 查看内存断点:watchpoint list
  • 删除内存断点:watchpoint delete

image.png

2、给一个断点添加多指令

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

-(void)touchesBegan:(NSSet<UITouch *> *)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) 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

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

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

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

脚本断点

4、image指令

  • image list :查看所有加载的库
  • image lookup -t 类名 查看一个类的头文件信息

5、方法断点

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

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

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地址

方法断点测试

ASLR

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

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

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

测试结果

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