前言
在我们日常开发中,使用XCode
进行编码实现功能的时候,经常会打断点进行调试,同时配合lldb
的某些指令,查看变量的信息等。本篇文章将给大家讲解下lldb
的常用的调试指令
,当然,不仅仅是断点相关的。
一、lldb概述
lldb(Low Lever Debug)
👉 默认内置于Xcode
中的动态调试工具。标准的lldb
提供了一组广泛的命令
,旨在与老版本的GDB命令
兼容。 除了使用标准配置外,还可以很容易地自定义lldb
以满足实际需要。
lldb语法
<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]
-
<command>(命令)和<subcommand>(子命令)
👉 lldb调试命令的名称
-
<action>
👉 执行命令的操作
-
<options>
👉 命令选项
-
<arguement>
👉 命令的参数
-
[]
👉 表示命令是可选
的,可以有也可以没有
例如👇
breakpoint set -n test
这条lldb指令所对应的参数释义👇
-
command
👉breakpoint
表示断点命令 -
action
👉set
表示设置断点 -
option
👉-n
表示根据方法name
设置断点 -
arguement
👉test
表示方法名为test
二、断点
日常开发中,最常用的是断点调试
。但是在逆向环境中,我们并没有第三方应用的源码
,所以不可能
通过在源码上直接设置断点。这种情况,只能在XCode中lldb
上使用breakpoint指令
设置断点,或者直接在终端
进行断点设置。
案例演示
1. breakpoint指令
新建lldbDemo
工程,打开ViewController.m
文件,写入以下代码👇
#import "ViewController.h"
@implementation ViewController
void test1(){
NSLog(@"3");
}
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"1");
NSLog(@"2");
test1();
}
@end
我们使用指令对test1
下断点👇
breakpoint set -n test1
显示Breakpoint 2
,说明是设置的第2个
断点,第1个
断点是使用Xcode在touchesBegan中对NSLog(@"1")
设置的。
2. 对指定方法设置断点
还是在ViewController.m
文件中,写入以下代码👇
#import "ViewController.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (IBAction)save:(id)sender {
NSLog(@"保存");
}
- (IBAction)pause:(id)sender {
NSLog(@"暂停");
}
- (IBAction)continueGame:(id)sender {
NSLog(@"继续");
}
@end
这是第三方App
中的源码,我们当做不知道,只知道方法的名称,此时如何下断点呢?
在逆向环境
中,运行第三方程序,使用暂停
可进入lldb控制台
,相当于Debug进程
👇
我们对上面的三个方法都打上断点👇
breakpoint set -n "-[ViewController save:]" -n "-[ViewController pause:]" -n "-[ViewController continueGame:]"
Breakpoint 1: 3 locations
的释义👇
-
Breakpoint 1
👉 分组1 -
3 locations
👉 分组1下设置了3个断点
breakpoint list
还可以使用breakpoint list
查看断点👇
每个断点的信息都很详细。
3. 断点的禁用、启动和删除
- 禁用断点
breakpoint disable 1
表示将分组1
下的所有断点
禁用👇
- 启用断点
breakpoint enable 1
将分组1
下的所有断点
启用👇
也可以对单一断点禁用或启用👇
breakpoint disable 1.1
- 删除断点
- 删除
分组1
下的所有断点
breakpoint delete 1
- 删除
全部
断点
breakpoint delete
很友好,还要确认一下。
不支持
删除某单一断点,例如👇
breakpoint delete 4.1
-------------------------
0 breakpoints deleted; 1 breakpoint locations disabled.
4. 更多breakpoint指令
使用help breakpoint
指令👇
(lldb) help breakpoint
Commands for operating on breakpoints (see 'help b' for shorthand.)
Syntax: breakpoint <subcommand> [<command-options>]
The following subcommands are supported:
clear -- Delete or disable breakpoints matching the specified source
file and line.
command -- Commands for adding, removing and listing LLDB commands
executed when a breakpoint is hit.
delete -- Delete the specified breakpoint(s). If no breakpoints are
specified, delete them all.
disable -- Disable the specified breakpoint(s) without deleting them. If
none are specified, disable all breakpoints.
enable -- Enable the specified disabled breakpoint(s). If no breakpoints
are specified, enable all of them.
list -- List some or all breakpoints at configurable levels of detail.
modify -- Modify the options on a breakpoint or set of breakpoints in
the executable. If no breakpoint is specified, acts on the
last created breakpoint. With the exception of -e, -d and -i,
passing an empty argument clears the modification.
name -- Commands to manage name tags for breakpoints
read -- Read and set the breakpoints previously saved to a file with
"breakpoint write".
set -- Sets a breakpoint or set of breakpoints in the executable.
write -- Write the breakpoints listed to a file that can be read in
with "breakpoint read". If given no arguments, writes all
breakpoints.
For more help on any particular subcommand, type 'help <command> <subcommand>'.
(lldb)
也可以了解更多选项的参数设置👇
(lldb) help breakpoint set
Sets a breakpoint or set of breakpoints in the executable.
Syntax: breakpoint set <cmd-options>
Command Options Usage:
breakpoint set [-DHd] -l <linenum> [-G <boolean>] [-C <command>] [-c <expr>] [-i <count>] [-o <boolean>] [-q <queue-name>] [-t <thread-id>] [-x <thread-index>] [-T <thread-name>] [-R <address>] [-N <breakpoint-name>] [-u <column>] [-f <filename>] [-m <boolean>] [-s <shlib-name>] [-K <boolean>]
breakpoint set [-DHd] -a <address-expression> [-G <boolean>] [-C <command>] [-c <expr>] [-i <count>] [-o <boolean>] [-q <queue-name>] [-t <thread-id>] [-x <thread-index>] [-T <thread-name>] [-N <breakpoint-name>] [-s <shlib-name>]
breakpoint set [-DHd] -n <function-name> [-G <boolean>] [-C <command>] [-c <expr>] [-i <count>] [-o <boolean>] [-q <queue-name>] [-t <thread-id>] [-x <thread-index>] [-T <thread-name>] [-R <address>] [-N <breakpoint-name>] [-f <filename>] [-L <source-language>] [-s <shlib-name>] [-K <boolean>]
breakpoint set [-DHd] -F <fullname> [-G <boolean>] [-C <command>] [-c <expr>] [-i <count>] [-o <boolean>] [-q <queue-name>] [-t <thread-id>] [-x <thread-index>] [-T <thread-name>] [-R <address>] [-N <breakpoint-name>] [-f <filename>] [-L <source-language>] [-s <shlib-name>] [-K <boolean>]
breakpoint set [-DHd] -S <selector> [-G <boolean>] [-C <command>] [-c <expr>] [-i <count>] [-o <boolean>] [-q <queue-name>] [-t <thread-id>] [-x <thread-index>] [-T <thread-name>] [-R <address>] [-N <breakpoint-name>] [-f <filename>] [-L <source-language>] [-s <shlib-name>] [-K <boolean>]
breakpoint set [-DHd] -M <method> [-G <boolean>] [-C <command>] [-c <expr>] [-i <count>] [-o <boolean>] [-q <queue-name>] [-t <thread-id>] [-x <thread-index>] [-T <thread-name>] [-R <address>] [-N <breakpoint-name>] [-f <filename>] [-L <source-language>] [-s <shlib-name>] [-K <boolean>]
breakpoint set [-DHd] -r <regular-expression> [-G <boolean>] [-C <command>] [-c <expr>] [-i <count>] [-o <boolean>] [-q <queue-name>] [-t <thread-id>] [-x <thread-index>] [-T <thread-name>] [-R <address>] [-N <breakpoint-name>] [-f <filename>] [-L <source-language>] [-s <shlib-name>] [-K <boolean>]
breakpoint set [-DHd] -b <function-name> [-G <boolean>] [-C <command>] [-c <expr>] [-i <count>] [-o <boolean>] [-q <queue-name>] [-t <thread-id>] [-x <thread-index>] [-T <thread-name>] [-R <address>] [-N <breakpoint-name>] [-f <filename>] [-L <source-language>] [-s <shlib-name>] [-K <boolean>]
breakpoint set [-ADHd] -p <regular-expression> [-G <boolean>] [-C <command>] [-c <expr>] [-i <count>] [-o <boolean>] [-q <queue-name>] [-t <thread-id>] [-x <thread-index>] [-T <thread-name>] [-N <breakpoint-name>] [-f <filename>] [-m <boolean>] [-s <shlib-name>] [-X <function-name>]
breakpoint set [-DHd] -E <source-language> [-G <boolean>] [-C <command>] [-c <expr>] [-i <count>] [-o <boolean>] [-q <queue-name>] [-t <thread-id>] [-x <thread-index>] [-T <thread-name>] [-N <breakpoint-name>] [-O <type-name>] [-h <boolean>] [-w <boolean>]
breakpoint set [-DHd] -P <python-class> [-k <none>] [-v <none>] [-G <boolean>] [-C <command>] [-c <expr>] [-i <count>] [-o <boolean>] [-q <queue-name>] [-t <thread-id>] [-x <thread-index>] [-T <thread-name>] [-N <breakpoint-name>] [-f <filename>] [-s <shlib-name>]
breakpoint set [-DHd] -y <linespec> [-G <boolean>] [-C <command>] [-c <expr>] [-i <count>] [-o <boolean>] [-q <queue-name>] [-t <thread-id>] [-x <thread-index>] [-T <thread-name>] [-R <address>] [-N <breakpoint-name>] [-m <boolean>] [-s <shlib-name>] [-K <boolean>]
-A ( --all-files )
All files are searched for source pattern matches.
-C <command> ( --command <command> )
A command to run when the breakpoint is hit, can be provided more
than once, the commands will get run in order left to right.
-D ( --dummy-breakpoints )
Act on Dummy breakpoints - i.e. breakpoints set before a file is
provided, which prime new targets.
// 还有很多参数,这里不一一贴出来了。
5. 对包含字符串的符号设置断点
例如 👉 使用breakpoint set -n
,对touchesBegan
方法设置断点👇
breakpoint set -n touchesBegan
并没有设置成功,因为完整的符号是touchesBegan:withEvent:
,此时应该使用命令👇
breakpoint set -r touchesBegan:
此时就设置成功了!
还可以使用breakpoint set --selector
,对项目内指定名称的selector
设置断点👇
breakpoint set --selector touchesBegan:withEvent:
6. 指定文件设置断点
使用breakpoint set --file
,在指定文件内
设置断点👇
breakpoint set --file ViewController.m --selector touchesBegan:withEvent:
没有成功,why?因为我们的VieWController
中没定义touchesBegan:withEvent:
方法,我们加上该方法,再看看👇
果然成功了!
7. lldb强大的缩写功能
对包含touchesBegan
的符号设置断点👇
b -r touchesBegan
查看断点的列表
的简写👇
br list
disable
禁用断点的简写👇
br dis 1
当然,enable
启用断点的简写👇
br en 1
三、代码执行
除了使用lldb
指令设置断点外,常用的还可以进行代码执行
👇
-
expression
指令,用于执行代码
,缩写指令为p、exp
。 -
po
指令,意思是print object
,用于打印对象,本质上调用了对象的description
。
案例演示
1. 使用expression指令
首先,对ViewController
中的touchesBegan
设置断点👇
然后点击屏幕,进入断点,使用expression
指令执行代码👇
expression self.view.subviews
2. 设置背景色
接着上面的调试👇
expression self.view.backgroundColor = [UIColor redColor]
报错,无法直接
修改backgroundColor
属性。那么,我们换一种方式,修改layer
下的backgroundColor
属性👇
expression self.view.layer.backgroundColor = [UIColor yellowColor].CGColor
成功!
3. 对数组追加元素
首先,打开ViewController.m
文件,写入以下代码👇
#import "ViewController.h"
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age;
@end
@implementation Person
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age {
if (self = [super init]) {
self.name = name;
self.age = age;
}
return self;
}
@end
@interface ViewController ()
@property(nonatomic, strong) NSMutableArray<Person *> * models;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person * p1 = [[Person alloc] initWithName:@"one" age:1];
Person * p2 = [[Person alloc] initWithName:@"two" age:2];
Person * p3 = [[Person alloc] initWithName:@"three" age:3];
[self.models addObject:p1];
[self.models addObject:p2];
[self.models addObject:p3];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"models:%@",self.models);
}
-(NSMutableArray<Person *> *)models {
if (!_models) {
_models = [NSMutableArray array];
}
return _models;
}
@end
然后,我们对ViewController
中的touchesBegan
设置断点👇
br set -f ViewController.m -r touchesBegan
接着,点击屏幕,进入touchesBegan
方法的断点,追加数组元素👇
p [self.models addObject:[[Person alloc] initWithName:@"haha" age:4]]
使用c(continue)
指令,继续执行👇
上图可见,输出的models
数组中存储了4个
对象,说明元素追加成功
!
4. 修改数组中对象的属性
接着上面的案例,点击屏幕,进入touchesBegan
方法的断点,获取数组的第一个元素👇
p (Person *)self.models.firstObject
其中,$0
为标号,代表Person
对象,可以使用。
接着,我们使用标号,修改对象name
属性👇
p $0.name=@"123"
验证name属性是否修改成功👇
po self.models.firstObject
p ((Person *)0x600001ac2c40).name
5. 执行多行代码
点击屏幕,进入touchesBegan方法的断点,通过Option+Enter
进行换行
,在lldb
控制台写入以下代码👇
p Person * $tmp = self.models.firstObject;
p $tmp.name = @"Zang";
p $tmp.age = 18;
6. 其他流程控制的指令
- 使用
c(continue)指令
,继续执行(上面案例中使用过) - 使用
n(next)指令
,单步运行,将子函数当做整体一步执行 - 使用
s(step into)指令
,单步运行,遇到子函数会进去
四、堆栈信息
1. 查看函数调用栈
使用bt
指令,查看函数调用栈
👇
其中
-
*
符号 👉 指向当前的
函数调用栈👇
使用up
指令,查看上一个
函数👇
同时*
也指向上一个函数👇
同理,使用down
指令,查看下一个
函数
使用frame select
指令,选择指定函数
frame select 10
使用上述指令,可以将断点定位到指定函数。它的作用可以查看函数的调用者,通过汇编代码分析参数的传递。但寄存器的环境并不会发生变化,数据保存的还是最后一个函数执行完毕的结果。
2. 查看方法的参数和局部变量
首先修改ViewController.m
中的代码👇
#import "ViewController.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self lalala1:@"HAHA"];
}
-(void)lalala1:(NSString *)str{
[self lalala2:str];
}
-(void)lalala2:(NSString *)str{
NSString *str1 = @"Zang";
NSLog(@"%@:%@",str1, str);
}
@end
然后,点击屏幕,进入lalala2
方法的断点,使用frame variable
指令,查看方法的参数和局部变量👇
在逆向过程中,进入一个方法,最想看到的就是该方法的调用者、方法名称、参数
等信息,此时我们就可以使用frame variable
指令,再配合up、down、frame select
指令,查看调用栈
中其他方法的信息。
3. 修改方法的参数
接着上述案例,断点继续进入lalala1
方法中,使用frame variable
👇
然后使用p
指令,修改str的值
👇
p str = @"COOL"
使用c
指令,继续运行👇
上图可知,输出结果变为修改后
的内容。
⚠️注意:只针对当前
未执行完的方法
有效。对于已经执行完
的方法,修改里面的内容,并不影响
最终的结果。
4. 让方法直接返回,不执行里面的代码
修改ViewController.m
中的代码👇
#import "ViewController.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self check:@"HOOK"];
NSLog(@"一切正常~");
}
-(void)check:(NSString *)str{
if([str isEqualToString:@"HOOK"]){
NSLog(@"有人HOOK我...");
exit(0);
}
}
@end
然后点击屏幕,进入check
方法的断点👇
接着使用thread return
指令,让check方法直接返回,不执行里面的代码👇
thread return
使用frame variable
指令,查看thread return
指令执行后
的函数👇
此时已经回到touchesBegan:withEvent:
方法,然后使用c
指令,继续执行👇
综上,使用thread return
,绕过了check
方法。那什么场景才需要这么调试呢👇
原本执行到某方法,一进入就会
中断
。使用thread return
指令绕过方法,如果可以正常执行,证明此方法为检测方法
。后续可针对不同情况,选择Method Swizzle、fishHook、InlineHook
对其进行HOOK
,将方法直接return
。
五、内存断点
1. 在对象的属性上设置断点
回到Person
那个案例,我们修改person对象的属性👇
- (void)viewDidLoad {
[super viewDidLoad];
Person * p1 = [[Person alloc] initWithName:@"one" age:1];
p1.name = @"new";
}
在p1对象的name上设置断点,使用watchpoint
指令👇
watchpoint set variable p1->_name
使用c指令,继续运行👇
使用po指令
在逆向开发中,此案例使用的场景👇
当调用name
属性的get/set
方法,都会触发
此断点。可获取到name属性的原始值
,和即将修改的值
。配合bt
指令,查看函数调用栈
,可以跟踪name
属性的修改是由哪个方法触发的
。
2. 对属性地址设置内存断点
还是使用上面的案例,进入断点,获取name
属性的地址👇
p &p1->_name
使用watchpoint
指令,对属性地址
设置内存断点👇
watchpoint set expression 0x00006000028cb4c8
使用c指令,继续运行👇
使用po
指令👇
六、其它指令
1. 当分组下断点被触发,自动执行指令
还原Person
案例中ViewDidLoad
的代码👇
- (void)viewDidLoad {
[super viewDidLoad];
Person * p1 = [[Person alloc] initWithName:@"one" age:1];
Person * p2 = [[Person alloc] initWithName:@"two" age:2];
Person * p3 = [[Person alloc] initWithName:@"three" age:3];
[self.models addObject:p1];
[self.models addObject:p2];
[self.models addObject:p3];
}
在touchesBegan
方法设置断点👇
br set -f ViewController.m -r touchesBegan:
对分组1
断点,设置进入断点后
的执行的指令👇
br command add 1
使用c
指令,继续运行
再次点击屏幕
,进入touchesBegan
方法的断点,同时输出以下信息👇
2. 当任何断点被触发,自动执行指令
target stop-hook add -o "frame variable"
接着查看stop-hook
的指令列表👇
target stop-hook list
删除某一条指令
target stop-hook delete 1
删除全部指令
target stop-hook delete
禁用某一条指令
target stop-hook disable 1
启用某一条指令
target stop-hook enable 1
添加执行代码👇
display self.view
等同于expr -- self.view
使用该案例的场景👇
对于
frame variable
指令,基本上每个断点
触发后都要使用。
但lldb
每次重新启动
后,必须要重新配置
上述所有命令,这样才能生效
,那有没有一劳永逸
的方法呢?
3. 配置lldb初始化文件
在根目录
下,存储了lldb
的初始化文件👇
cd ~
ls -all
除了.lldb
文件外,还有一个.lldbinit
文件,它的作用是当lldb启动
,就会加载此文件
,执行文件内的指令
,那么我们可以使用vi ~/.lldbinit
,写入以下指令👇
target stop-hook add -o "frame variable"
运行项目,lldb
启动,输出以下内容👇
Stop hook #1 added.
接着进入viewDidLoad
断点,就会输出以下内容👇
(ViewController *) self = 0x0000000135f093a0
(SEL) _cmd = "viewDidLoad"
这个就是一劳永逸
!🍺🍺🍺🍺🍺🍺
总结
- 断点设置
-
breakpoint set -n xxx
:对方法/函数名称设置断点 -
breakpoint set -r xxx
:对包含字符串的符号设置断点 -
breakpoint set --selector xxx
:对项目内指定名称的selector
设置断点 -
breakpoint set --file xxx
:在指定文件中设置断点 -
breakpoint list
:查看断点列表 -
breakpoint disable
:禁用断点 -
breakpoint enable
:启用断点 -
breakpoint delete
:删除断点 - 缩写:
break
、br
,设置断点可缩写指令:b
- 代码执行
-
po
指令:意思是print object
,用于打印对象,本质上调用了对象的description
-
expression
指令:用于执行代码
◦ 缩写:exp
、p
◦ 可以使用标号
◦ 可执行多行代码 - 流程控制
◦c
(continue
)指令:继续执行
◦n
(next
)指令:单步运行,将子函数当做整体一步执行
◦ni
指令:单步运行汇编级别
◦s
指令:单步运行,遇到子函数会进去
◦si
指令:单步运行可跳转指令内部,汇编级别
◦finish
指令:直接走完当前方法,返回到上层frame
- 堆栈信息
-
bt
指令:查看函数调用栈 -
up
指令:查看上一个函数 -
down
指令:查看下一个函数 -
frame select
指令:选择指定函数 -
frame variable
指令:查看方法调用者、方法名称、参数和局部变量 -
thread return
指令:让方法直接返回,不执行里面的代码
- 内存断点:
-
watchpoint
指令,设置内存断点
- 其他指令
-
br command add
指令:给断点添加命令的命令 -
target stop-hook add -o "xxx"
:每次stop
的时候去执行一些命令,针对breadpoint
、watchpoint
- 配置
lldb
初始化文件,当lldb
启动,就会加载此文件,执行文件内的指令,一劳永逸
-
lldb
更多文档
网友评论