简介
LLDB是个开源的内置于Xcode的具有REPL(read-eval-print-loop)特征的Debugger,其可以安装C++
或者Python
插件。
lldb与gdb命令名的对照表:http://lldb.llvm.org/lldb-gdb.html
整理一下LLDB调试器提供给我们的调试命令,更详细的内容可以查看The LLDB Debugger。
LLDB命令结构
在使用LLDB前,我们需要了解一下LLDB的命令结构及语法,这样可以尽可能地挖掘LLDB的潜能,以帮助我们更充分地利用它。
LLDB命令的语法有其通用结构,通常是以下形式的:
<command></command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]</action></subcommand></subcommand>
<command>
(命令)和<subcommand>
(子命令):LLDB调试命令的名称。命令和子命令按层级结构来排列:一个命令对象为跟随其的子命令对象创建一个上下文,子命令又为其子命令创建一个上下文,依此类推。
<action>
:执行命令的操作
<options>
:命令选项
<arguement>
:命令的参数
[]
:表示命令是可选的,可以有也可以没有
举个例子,假设我们给main方法设置一个断点,我们使用下面的命令:
breakpoint set -n main
这个命令对应到上面的语法就是:
command
:breakpoint
表示断点命令action
:set
表示设置断点option
:-n
表示根据方法name设置断点arguement
:mian
表示方法名为mian
原始(raw)命令
LLDB支持不带命令选项(options)的原始(raw)命令,原始命令会将命令后面的所有东西当做参数(arguement)传递。不过很多原始命令也可以带命令选项,当你使用命令选项的时候,需要在命令选项后面加--
区分命令选项和参数。
e.g: 常用的expression
就是raw命令,一般情况下我们使用expression
打印一个东西是这样的:
(lldb) expression count
(int) $2 = 4
当我们想打印一个对象的时候。需要使用-O
命令选项,我们应该用--
将命令选项和参数区分:
(lldb) expression -O -- self
<ViewController: 0x7f9000f17660>
唯一匹配原则
LLDB的命令遵循唯一匹配原则:假如根据前n个字母已经能唯一匹配到某个命令,则只写前n个字母等效于写下完整的命令。 e.g: 前面提到我设置断点的命令,我们可以使用唯一匹配原则简写,下面2条命令等效:
breakpoint set -n main
br s -n main
~/.lldbinit
LLDB有了一个启动时加载的文件~/.lldbinit
,每次启动都会加载。所以一些初始化的事儿,我们可以写入~/.lldbinit
中,比如给命令定义别名等。但是由于这时候程序还没有真正运行,也有部分操作无法在里面玩,比如设置断点。
LLDB命令
help
单单执行help命令会列出所有命令列表,用户加载的插件一般来说列在最后。
执行help 可以打印指定command的帮助信息,至于插件提供的命令,其帮助信息取决于插件本身的实现。
例如 help print会打印内建命令print的使用帮助。
expression
expression命令的作用是执行一个表达式,并将表达式返回的结果输出。expression的完整语法是这样的:
expression <cmd-options> -- <expr>
<cmd-options>
:命令选项,一般情况下使用默认的即可,不需要特别标明。
--
: 命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,–可以省略
<expr>
: 要执行的表达式
说expression
是LLDB里面最重要的命令都不为过。因为他能实现2个功能。
我们在代码运行过程中,可以通过执行某个表达式来动态改变程序运行的轨迹。
假如我们在运行过程中,突然想把self.view颜色改成红色,看看效果。我们不必写下代码,重新run,只需暂停程序,用expression
改变颜色,再刷新一下界面,就能看到效果
// 改变颜色
(lldb) expression -- self.view.backgroundColor = [UIColor redColor]
// 刷新界面
(lldb) expression -- (void)[CATransaction flush]
也就是说我们可以通过expression
来打印东西。
假如我们想打印self.view:
(lldb) expression -- self.view
(UIView *) $1 = 0x00007fe322c18a10
p & print & call
一般情况下,我们直接用expression还是用得比较少的,更多时候我们用的是p
、print
、call
。这三个命令其实都是expression --
的别名(--
表示不再接受命令选项,详情见前面原始(raw)命令
这一节)
print
: 打印某个东西,可以是变量和表达式p
: 可以看做是print
的简写call
: 调用某个方法。
表面上看起来他们可能有不一样的地方,实际都是执行某个表达式(变量也当做表达式),将执行的结果输出到控制台上。所以你可以用p
调用某个方法,也可以用call
打印东西
e.g: 下面代码效果相同:
(lldb) expression -- self.view
(UIView *) $5 = 0x00007fb2a40344a0
(lldb) p self.view
(UIView *) $6 = 0x00007fb2a40344a0
(lldb) print self.view
(UIView *) $7 = 0x00007fb2a40344a0
(lldb) call self.view
(UIView *) $8 = 0x00007fb2a40344a0
(lldb) e self.view
(UIView *) $9 = 0x00007fb2a40344a0
根据唯一匹配原则,如果你没有自己添加特殊的命令别名。e也可以表示expression的意思。原始命令默认没有命令选项,所以e也能带给你同样的效果
po
我们知道,OC里所有的对象都是用指针表示的,所以一般打印的时候,打印出来的是对象的指针,而不是对象本身。如果我们想打印对象。我们需要使用命令选项:-O
。为了更方便的使用,LLDB为expression -O --
定义了一个别名:po
(lldb) expression -- self.view
(UIView *) $13 = 0x00007fb2a40344a0
(lldb) expression -O -- self.view
<UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>
(lldb) po self.view
<UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>
thread
thread backtrace
& bt
有时候我们想要了解线程堆栈信息,可以使用thread backtrace
thread backtrace
作用是将线程的堆栈打印出来。我们来看看他的语法
thread backtrace [-c <count>] [-s <frame-index>] [-e <boolean>]
thread backtrace
后面跟的都是命令选项:
-c
:设置打印堆栈的帧数(frame)
-s
:设置从哪个帧(frame)开始打印
-e
:是否显示额外的回溯
实际上这些命令选项我们一般不需要使用。
e.g: 当发生crash的时候,我们可以使用thread backtrace
查看堆栈调用
(lldb) thread backtrace
* thread #1: tid = 0xdd42, 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11, queue = ‘com.apple.main-thread‘, stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
frame #0: 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11
* frame #1: 0x000000010aa9f75e TLLDB`-[ViewController viewDidLoad](self=0x00007fa270e1f440, _cmd="viewDidLoad") + 174 at ViewController.m:23
frame #2: 0x000000010ba67f98 UIKit`-[UIViewController loadViewIfRequired] + 1198
frame #3: 0x000000010ba682e7 UIKit`-[UIViewController view] + 27
frame #4: 0x000000010b93eab0 UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 61
frame #5: 0x000000010b93f199 UIKit`-[UIWindow _setHidden:forced:] + 282
frame #6: 0x000000010b950c2e UIKit`-[UIWindow makeKeyAndVisible] + 42
我们可以看到crash发生在-[ViewController viewDidLoad]
中的第23行,只需检查这行代码是不是干了什么非法的事儿就可以了。
LLDB还为backtrace专门定义了一个别名:bt
,他的效果与thread backtrace
相同,如果你不想写那么长一串字母,直接写下bt
即可
(lldb) bt
thread return
Debug的时候,也许会因为各种原因,我们不想让代码执行某个方法,或者要直接返回一个想要的值。这时候就该thread return上场了。
thread return 命令需要一个参数来指明函数强制返回时的返回值。
thread return
可以接受一个表达式,调用命令之后直接从当前的frame返回表达式的值。
e.g: 我们有一个someMethod
方法,默认情况下是返回YES。我们想要让他返回NO
我们只需在方法的开始位置加一个断点,当程序中断的时候,输入命令即可:
(lldb) thread return NO
效果相当于在断点位置直接调用return NO;
,不会执行断点后面的代码
c & n & s & finish
一般在调试程序的时候,我们经常用到下面这4个按钮:
用触摸板的孩子们可能会觉得点击这4个按钮比较费劲。其实LLDB命令也可以完成上面的操作,而且如果不输入命令,直接按Enter键,LLDB会自动执行上次的命令。按一下Enter就能达到我们想要的效果,有木有顿时感觉逼格满满的!!!
我们来看看对应这4个按钮的LLDB命令:
c
/continue
/thread continue
: 这三个命令效果都等同于上图中第一个按钮的。表示程序继续运行n
/next
/thread step-over
: 这三个命令效果等同于上图第二个按钮。表示单步运行s
/step
/thread step-in
: 这三个命令效果等同于上图第三个按钮。表示进入某个方法finish
/step-out
: 这两个命令效果等同于第四个按钮。表示直接走完当前方法,返回到上层frame
thread其他不常用的命令
thread 相关的还有其他一些不常用的命令,这里就简单介绍一下即可,如果需要了解更多,可以使用命令help thread
查阅
thread jump
: 直接让程序跳到某一行。由于ARC下编译器实际插入了不少retain,release命令。跳过一些代码不执行很可能会造成对象内存混乱发生crash。thread list
: 列出所有的线程thread select
: 选择某个线程thread until
: 传入一个line的参数,让程序执行到这行的时候暂停thread info
: 输出当前线程的信息
frame
前面我们提到过很多次frame(帧)。可能有的朋友对frame这个概念还不太了解。随便打个断点
我们在控制台上输入命令bt
,可以打印出来所有的frame。如果仔细观察,这些frame和左边红框里的堆栈是一致的。平时我们看到的左边的堆栈就是frame。
frame variable
平时Debug的时候我们经常做的事就是查看变量的值,通过frame variable
命令,可以打印出当前frame的所有变量
(lldb) frame variable
(ViewController *) self = 0x00007fa158526e60
(SEL) _cmd = "text:"
(BOOL) ret = YES
(int) a = 3
可以看到,他将self
,_cmd
,ret
,a
等本地变量都打印了出来
如果我们要需要打印指定变量,也可以给frame variable
传入参数:
(lldb) frame variable self->_string
(NSString *) self->_string = nil
不过frame variable
只接受变量作为参数,不接受表达式,也就是说我们无法使用frame variable self.string
,因为self.string
是调用string
的getter
方法。所以一般打印指定变量,我更喜欢用p
或者po
。
其他不常用命令
一般frame variable
打印所有变量用得比较多,frame还有2个不怎么常用的命令:
frame info
: 查看当前frame的信息
(lldb) frame info
frame #0: 0x0000000101bf87d5 TLLDB`-[ViewController text:](self=0x00007fa158526e60, _cmd="text:", ret=YES) + 37 at
frame select
: 选择某个frame
(lldb) frame select 1
frame #1: 0x0000000101bf872e TLLDB`-[ViewController viewDidLoad](self=0x00007fa158526e60, _cmd="viewDidLoad") + 78 at ViewController.m:23
20
21 - (void)viewDidLoad {
22 [super viewDidLoad];
-> 23 [self text:YES];
24 NSLog(@"1");
25 NSLog(@"2");
26 NSLog(@"3");
当我们选择frame 1的时候,他会把frame1的信息和代码打印出来。不过一般我都是直接在Xcode左边点击某个frame,这样更方便
breakpoint
调试过程中,我们用得最多的可能就是断点了。LLDB中的断点命令也非常强大
breakpoint set
breakpoint set
命令用于设置断点,LLDB提供了很多种设置断点的方式:
使用-n根据方法名设置断点:
e.g: 我们想给所有类中的viewWillAppear:
设置一个断点:
(lldb) breakpoint set -n viewWillAppear:
Breakpoint 13: 33 locations.
使用-f指定文件
e.g: 我们只需要给ViewController.m
文件中的viewDidLoad
设置断点:
(lldb) breakpoint set -f ViewController.m -n viewDidLoad
Breakpoint 22: where = TLLDB`-[ViewController viewDidLoad] + 20 at ViewController.m:22, address = 0x000000010272a6f4
这里需要注意,如果方法未写在文件中(比如写在category文件中,或者父类文件中),指定文件之后,将无法给这个方法设置断点。
使用-l指定文件某一行设置断点
e.g: 我们想给ViewController.m
第38行设置断点
(lldb) breakpoint set -f ViewController.m -l 38
Breakpoint 23: where = TLLDB`-[ViewController text:] + 37 at ViewController.m:38, address = 0x000000010272a7d5
使用-c设置条件断点
e.g: text:
方法接受一个ret
的参数,我们想让ret == YES
的时候程序中断:
(lldb) breakpoint set -n text: -c ret == YES
Breakpoint 7: where = TLLDB`-[ViewController text:] + 30 at ViewController.m:37, address = 0x0000000105ef37ce
使用-o设置单次断点
e.g: 如果刚刚那个断点我们只想让他中断一次:
(lldb) breakpoint set -n text: -o
‘breakpoint 3‘: where = TLLDB`-[ViewController text:] + 30 at ViewController.m:37, address = 0x000000010b6f97ce
breakpoint command
有的时候我们可能需要给断点添加一些命令,比如每次走到这个断点的时候,我们都需要打印self
对象。我们只需要给断点添加一个po self
命令,就不用每次执行断点再自己输入po self
了
breakpoint command add
breakpoint command add
命令就是给断点添加命令的命令。
e.g: 假设我们需要在ViewController
的viewDidLoad
中查看self.view
的值
我们首先给-[ViewController viewDidLoad]
添加一个断点
(lldb) breakpoint set -n "-[ViewController viewDidLoad]"
‘breakpoint 3‘: where = TLLDB`-[ViewController viewDidLoad] + 20 at ViewController.m:23, address = 0x00000001055e6004
可以看到添加成功之后,这个breakpoint
的id为3,然后我们给他增加一个命令:po self.view
(lldb) breakpoint command add -o "po self.view" 3
-o
完整写法是--one-liner
,表示增加一条命令。3
表示对id为3
的breakpoint
增加命令。
添加完命令之后,每次程序执行到这个断点就可以自动打印出self.view
的值了
如果我们一下子想增加多条命令,比如我想在viewDidLoad
中打印当前frame的所有变量,但是我们不想让他中断,也就是在打印完成之后,需要继续执行。我们可以这样玩:
(lldb) breakpoint command add 3
Enter your debugger command(s). Type ‘DONE‘ to end.
> frame variable
> continue
> DONE
输入breakpoint command add 3
对断点3增加命令。他会让你输入增加哪些命令,输入‘DONE‘表示结束。这时候你就可以输入多条命令了
breakpoint command list
如果想查看某个断点已有的命令,可以使用breakpoint command list
。
image
image命令的用法也挺多,首先可以用它来查看工程中使用的库,如下所示:
(lldb) image list
[ 0] 432A6EBF-B9D2-3850-BCB2-821B9E62B1E0 0x0000000100000000 /Users/**/Library/Developer/Xcode/DerivedData/test-byjqwkhxixddxudlnvqhrfughkra/Build/Products/Debug/test
[ 1] 65DCCB06-339C-3E25-9702-600A28291D0E 0x00007fff5fc00000 /usr/lib/dyld
[ 2] E3746EDD-DFB1-3ECB-88ED-A91AC0EF3AAA 0x00007fff8d324000 /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
[ 3] 759E155D-BC42-3D4E-869B-6F57D477177C 0x00007fff8869f000 /usr/lib/libobjc.A.dylib
[ 4] 5C161F1A-93BA-3221-A31D-F86222005B1B 0x00007fff8c75c000 /usr/lib/libSystem.B.dylib
[ 5] CBD1591C-405E-376E-87E9-B264610EBF49 0x00007fff8df0d000 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
[ 6] A260789B-D4D8-316A-9490-254767B8A5F1 0x00007fff8de36000 /usr/lib/libauto.dylib
......
我们还可以用它来查找可执行文件或共享库的原始地址,这一点还是很有用的,当我们的程序崩溃时,我们可以使用这条命令来查找崩溃所在的具体位置,如下所示:
NSArray *array = @[@1, @2];
NSLog(@"item 3: %@", array[2]);
这段代码在运行后会抛出如下异常:
2015-01-25 14:12:01.007 test[18122:76474] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 1]'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff8e06f66c __exceptionPreprocess + 172
1 libobjc.A.dylib 0x00007fff886ad76e objc_exception_throw + 43
2 CoreFoundation 0x00007fff8df487de -[__NSArrayI objectAtIndex:] + 190
3 test 0x0000000100000de0 main + 384
4 libdyld.dylib 0x00007fff8f1b65c9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
根据以上信息,我们可以判断崩溃位置是在main.m文件中,要想知道具体在哪一行,可以使用以下命令:
(lldb) image lookup --address 0x0000000100000de0
Address: test[0x0000000100000de0] (test.__TEXT.__text + 384)
Summary: test`main + 384 at main.m:23
我们还可以使用image lookup命令来查看具体的类型,如下所示:
(lldb) image lookup --type NSURL
Best match found in /Users/**/Library/Developer/Xcode/DerivedData/test-byjqwkhxixddxudlnvqhrfughkra/Build/Products/Debug/test:
id = {0x100000157}, name = "NSURL", byte-size = 40, decl = NSURL.h:17, clang_type = "@interface NSURL : NSObject{
NSString * _urlString;
NSURL * _baseURL;
void * _clients;
void * _reserved;
}
@property ( readonly,getter = absoluteString,setter = <null selector>,nonatomic ) NSString * absoluteString;
@property ( readonly,getter = relativeString,setter = <null selector>,nonatomic ) NSString * relativeString;
@property ( readonly,getter = baseURL,setter = <null selector>,nonatomic ) NSURL * baseURL;
@property ( readonly,getter = absoluteURL,setter = <null selector>,nonatomic ) NSURL * absoluteURL;
@property ( readonly,getter = scheme,setter = <null selector>,nonatomic ) NSString * scheme;
@property ( readonly,getter = resourceSpecifier,setter = <null selector>,nonatomic ) NSString * resourceSpecifier;
@property ( readonly,getter = host,setter = <null selector>,nonatomic ) NSString * host;
@property ( readonly,getter = port,setter = <null selector>,nonatomic ) NSNumber * port;
@property ( readonly,getter = user,setter = <null selector>,nonatomic ) NSString * user;
@property ( readonly,getter = password,setter = <null selector>,nonatomic ) NSString * password;
@property ( readonly,getter = path,setter = <null selector>,nonatomic ) NSString * path;
@property ( readonly,getter = fragment,setter = <null selector>,nonatomic ) NSString * fragment;
@property ( readonly,getter = parameterString,setter = <null selector>,nonatomic ) NSString * parameterString;
@property ( readonly,getter = query,setter = <null selector>,nonatomic ) NSString * query;
@property ( readonly,getter = relativePath,setter = <null selector>,nonatomic ) NSString * relativePath;
@property ( readonly,getter = fileSystemRepresentation,setter = <null selector> ) const char * fileSystemRepresentation;
@property ( readonly,getter = isFileURL,setter = <null selector>,readwrite ) BOOL fileURL;
@property ( readonly,getter = standardizedURL,setter = <null selector>,nonatomic ) NSURL * standardizedURL;
@property ( readonly,getter = filePathURL,setter = <null selector>,nonatomic ) NSURL * filePathURL;
@end"</null selector></null selector></null selector></null selector></null selector></null selector></null selector></null selector></null selector></null selector></null selector></null selector></null selector></null selector></null selector></null selector></null selector></null selector></null selector>
可以看到,输出结果中列出了NSURL的一些成员变量及属性信息。
image命令还有许多其它功能,具体可以参考Executable and Shared Library Query Commands。
命令别名
我们可以使用LLDB的别名机制来为常用的命令创建一个别名,以方便我们的使用,如下命令:
(lldb) breakpoint set --file foo.c --line 12
如果在我们的调试中需要经常用到这条命令,则每次输入这么一长串的字符一定会很让人抓狂。此时,我们就可以为这条命令创建一个别名,如下所示:
(lldb) command alias bfl breakpoint set -f %1 -l %2
这样,我们只需要按如下方式来使用它即可:
(lldb) bfl foo.c 12
如果我们不喜欢已有命令的别名,则可以使用以下命令来取消这个别名:
(lldb) command unalias b
总结
LLDB带给我们强大的调试功能,在调试过程中充分地利用它可以帮助我们极大地提高调试效率。我们可以不用写那么多的NSLog来打印一大堆的日志。所以建议在日常工作中多去使用它。当然,上面的命令只是LLDB的冰山一角,更多的使用还需要大家自己去发掘,在此只是抛砖引玉,做了一些整理。