i 19:Xcode调试器LLDB

相信大家肯定都有过为了调试而添加打印变量,或者使用直接常量代替函数调用结果,或者更改判断条件以进入某特定分支的调试经历,但每次更改代码都需要重新编译,重新来过一遍,但其实本可以不用这样,因为我们有调试器,而且除了监视变量的值,还有很多它可以做的。

LLDB

LLDB是一个开源的以REPL为特性,并可以配置C++和python插件的调试器。它集成在xcode中,并在窗口底部的控制台中运行。调试器可以暂停程序执行,观察变量,执行自定义指令,并掌控程序的执行。如果对GDB比较熟悉的话,GDB-TO-LLDB 这个份指引应该对你了解LLDB的指令有极大助益,如果安装了Chisel这个LLDB插件的话,调试会更有趣。

基础操作

程序在断点处暂停的时候,console会打开供输入命令:

help

最简单的命令即是help,它会列出所有命令,如果忘记了help本身,help help试试

print 

用来打印值,由于 LLDB的前缀匹配,所以也可以使用prin,pri,但不可以用pr,因为还有一个命令是process,但可以用p代表print。

注意到结果里面会有$n的字样,带$前缀的标识属于LLDB命名空间,我们可以利用这个特性为自己服务

expression

修改变量值

注:有一个需要注意的是,如果使用中文字符串常量的时候,由于LLDB解析器的bug,会报错An Objective-C constant string's string initializer is not an array,需要使用[NSString stringWithUTF8String:]

list命令

list 行号       显示行号开始的数行代码(默认10行)

list 函数名    显示函数名为中心的前后十行代码

list不带参数,接着上一次list命令

print 命令

如果执行 p count = 18,会发现除了打印18之后,count的值也会变成18.它与expression count = 18执行结果相同。不同的地方在于print 命令没有参数。

想想 e -h +17 这行命令,如果将-h理解为flag的话, +17看起来并不像输入。如果理解为计算17与h的差值,那这个连字符看起来很让人困惑。

幸运的是,使用--来分隔flag与其后的输入,实际上e -- 的缩写是print。

打印对象

如果使用print objects,则输入看起来十分冗长:(NSString *) $7 = 0x0000000104da4040 @"red balloons",如果打印更复杂的结构 p @[ @"foo", @"bar" ],输出可能像这样(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects",而实际上我们想看对象的description,所以需要将对象按对象输出,使用-O 选项。

同样幸运的是,e -O --的别名是po (print object)

打印变量

可以在print命令中指定很多种格式,其形式像这样print/<fmt>,或者p/<fmt>,默认格式是这样,p 16 > 16,十六进制形式输出 p/x 16 > 0x10, 二进制 (t代表two) p/t 16 > 0b10000

还可以用p/c输出字符,p/s输出c字符串,这里是完整的输出格式

变量

在lldb中可以使用变量以减少过多的typing,但在LLDB中定义的变量必须以$开头,比如e int $i = 1,e NSArray* $a = @[@"Saturday",@"Sunday"], p [$a count], po [[$a objectAtIndex:0] uppercaseString]

而如果输入这条命令p [[$a objectAtIndex:$i] characterAtIndex:0],得到的结果是

error: no known method '-characterAtIndex:'; cast the message send to the method's return type

error: 1 errors parsing expression

因为LLDB无法识别涉及到的类型,这种情况有时候会发生,可以这样处理

p (char)[[$a objectAtIndex:$i] characterAtIndex:0]  > 'M'

或者p/d (char)[[$a objectAtIndex:$i] characterAtIndex:0] > 77

执行流控制

命中断点时,调试条上的4个按钮可供控制程序的执行流

执行流控制按钮

其意义从左到右分别是继续,越过,进入函数体,退出函数体

继续按钮的作用同LLDB的process continue,简写为continue,或者c

越过按钮的作用相当于执行完当前行的指令,就算当前当为函数调用,也不进入函数体,对应LLDB中的thread step-over,next 或者n

进入函数体按钮的作用相当于LLDB中的 thread step-in,step和s,而如果当前行非函数调用,则其与thread step-over 表现是相同的

退出函数体的作用在于能够一直执行到当前函数return之后再中断到调试器,即将当前栈帧弹出之后再停止。对应LLDB中的命令finish

frame info

输出当前代码行及其所在在源代码文件

线程return

thread return是控制执行流的另一利器,其可携带选项参数,其中将参数加载进返回寄存器,并立即执行return命令,并跳出当前栈帧,而这也意味着当前函数中剩余的语句不会被执行。这样做导致ARC的引用计数和追踪方面的问题或者在函数尾所做的清理等被跳过,但如果在进入函数体之后马上执行,则其在假装函数已经执行方面的作用还是很大的。

断点篇


断点示例代码段

LLDB列举断点: breakpoint list/br li

enable/disable 断点: breakpoint enable <breakpointID>/breakpoint disable <breakpointID>

设置断点:(在Xcode中可以在编辑区的代码行首点击添加断点,以及鼠标拖拽断点到代码行首区释放以移除断点)

breakpoint set:  比如breakpoint set -f main.m -l 16

由于b是_regexp-break的简写,所以breakpoint的缩写为br

但其实使用b设置断点,LLDB在通常情况下也是可以识别的,比如b main.m:17同样可以设置断点成功

其实也可以使用符号(C函数来设置断点,不用指定行号),比如

(lldb) b isEven

(lldb) br s -F isEven

这样可以使得调用此函数时会在其入口暂停,也可以使用oc 方法设置断点

(lldb) breakpoint set -F "-[NSArray objectAtIndex:]"

(lldb) b -[NSArray objectAtIndex:]

(lldb) breakpoint set -F "+[NSSet setWithObject:]"

(lldb) b +[NSSet setWithObject:]

如果想创建一个符号断点,可以在断点导航页中点击“+”

breakpoint navigator

同时在右键每个现有断点时都会出现包含edit breakpoint项的菜单,点击之后

edit breakpoint

Add Action的动作在如下详细介绍

Breakpoint Actions

这个功能很有用,可以在断点发生之后,马上执行你所定义的动作之后再将控制权交给你,即lldb命令行。action支持多行Debugger command,shell command,log message,使用lldb而非UI操作的方法为

lldb编辑断点

Continuing after Evaluation

在编辑断点的UI中,options项Automatically continue after evaluation actions勾选可以使得在断点处执行完预设action之后马上恢复运行,就像没断点一样。

Full Execution in the Debugger

还有一个特性是可以在debugger中运行任何C/Objective-C/C++/Swift命令,一点不足之外是不能创建新的函数,即不能创建新类,block,函数,带虚方法的C++类等,除此之外都可以。

比如可以分配数字节的空间

lldb分配内存空间

不过要注意,分配空间的时候,其作用域是与当前栈帧中当前代码行所在的作用域相同的,所以要尽量避免因作用域问题引起的执行异常

也可以使用x命令查看新数组的4个字节

(lldb) x/4c $str

0x7fd04a900040: monk

也可以查看数组第3个字节开始的内容(x命令需要反引号,需要地址作为参数):

(lldb) x/1w `$str + 3`

0x7fd04a900043: keys

但所有这些操作结束之后,确保释放这些内存,以免引起内存泄漏(在调试器中):

(lldb) e (void)free($str)

根据以上内容我们可以做的

在调试条中的暂停按钮可以暂停当前app,实际是其是执行了process interrupt,因为调试器其实一直都在执行场景之中。虽然此时的中断可能并没有暂停在你熟悉的代码上下文中,但其实可以试试这个:

(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]

更新UI

基于上述输出,可以从UI层级中选择一个来操作,假定其地址为0xadd2e55

(lldb) e id $myView = (id)0xadd2e55

(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]

走到恢复app的执行才会看到UI的变化,因为需要将这个信息通知给渲染Server显示才会更新。

render server实际是另一个进程backboardd,但在我们在调试当前进程的时候,backboardd是没有暂停的,所以其实我们可以执行(lldb) e (void)[CATransaction flush],这样就可以在不恢复执行的情况下看到UI更新。chisel提供了caflush来完成这个更新的功能(chisel还有更多方便的功能)

Pushing a View Controller

假设当前app的root vc为UINavigationController,则

(lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController](lldb) e id $vc = [UIViewController new]

(lldb) e (void)[[$vc view] setBackgroundColor:[UIColor yellowColor]]

(lldb) e (void)[$vc setTitle:@"Yay!"]

(lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]

即可以添加一个子view controller,然后执行(lldb) caflush // e (void)[CATransaction flush],就可以看见一个view controller被push进当前View Controller层级

笔者在使用presentViewController测试的时候,执行flush之后未见到UI有更新,然后执行continue才看到vc被present进来,xcode 6.4,iOS 8.3,真机和模拟器均是这样

Finding the Target of a Button

如果想知道$myButton相应的监听action有哪些,可以这样

(lldb) po [$myButton allTargets]{()}

(lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]

<__NSArrayM 0x7fb58bd2aa40>(

_handleTap:

)

这个时候,对_handleTap就想怎样就怎样设置断点

观察实例变量的更改

假定UIView的_layer被覆写,由于不涉及到方法的调用,无法设置断点。于是我们可以设置对某地址的写入,先找下_layer 变量在对象中的所在

(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))

(ptrdiff_t) $0 = 8

于是我们知道$myView+8即是我们关心的地址,于是

(lldb) watchpoint set expression -- (int *)$myView + 8

Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w

new value: 0x0000000000000000

在Chisel中,上述功能被精简为命令wivar $myView _layer

未被重载的方法上的符号化断点

假定想对-[MyViewController viewDidAppear:]的调用时机做监听,但vc本身未重载此方法,如果这样设置断点

(lldb) b -[MyViewController viewDidAppear:]

Breakpoint 1: no locations (pending).

WARNING:  Unable to resolve breakpoint to any actual locations.

但由于未重载此方法,所以不会有viewDidAppear的符号,故断点不会命中。这种情况下需要设置条件 [self isKindOfClass:[MyViewController class]],并将此条件置于UIViewController之上。但由于我们并未拥有UIViewController viewDidAppear的实现,其为apple所编写,故没有符号 ;且在此方法之内并没有self可用。由于如果在符号化断点中想用self, 那首先要知道self的位置(可能在寄存器中也可能在栈上,x86中可以在$esp+4中找到,但最少有4种硬件架构啊,亲)。难以想象了解每种硬件架构的指令集和调用规则,然后再编写在正确的父类上使用正确的条件设置断点的命令。幸运的是,使用Chisel可以很轻松地做到:

(lldb) bmessage -[MyViewController viewDidAppear:]

Setting a breakpoint at -[UIViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 0x000000010e2f4d28

Breakpoint 1: where = UIKit`-[UIViewController viewDidAppear:], address = 0x000000010e11533c

这篇文章的作者为了推广Chisel也是够拼的⊙﹏⊙

LLDB and Python

LLDB拥有全面而且是内置的Python支持,如果在LLDB中输入script,会打开Python REPL(Read-Eval-Print Loop),相当于python命令行。当然也可以使用script command执行python命令。

(lldb) script import os

(lldb) script os.system("open http://www.objc.io/")

接下来的事情就多了,可以使用py脚本文件,比如~/myCommands.py

def caflushCommand(debugger, command, result, internal_dict):

debugger.HandleCommand("e (void)[CATransaction flush]")

然后在lldb中执行

(lldb) script import ~/myCommands.py

而且,也可以将上述导入放入/.lldbinit以在每次LLDB启动时都执行,Chisel所做的也都是拼接字符串并交给LLDB执行而已。


错误处理

1 退出lldb repl的方法,使用 : 命令

2 一个需要注意的问题是在lldb中创建对象的时候,从init方法返回时,可能会报错 cast the message send to the methods return type,这时候不要慌,将对象显式转换为相应的对象类型,比如 (CGRect)[[UIView alloc] init],另外也需要 (CGRect)[view frame]

因为lldb并不支持.语法方法调用,同时还需要显式转换以及必要时候的括号将整个对象包裹起来,以告诉lldb,我们的输入表示的绝对是一个对象

3 如果在lldb中输入expr -- content.text = (NSString*)[NSString stringWithFormat:@"e"]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 你是否曾经苦恼于理解你的代码,而去尝试打印一个变量的值? NSLog(@"%@", whatIsInsideThi...
    木易林1阅读 941评论 0 4
  • 你是否曾经苦恼于理解你的代码,而去尝试打印一个变量的值? NSLog(@"%@", whatIsInsideThi...
    paraneaeee阅读 1,129评论 0 7
  • 转载 与调试器共舞 - LLDB 的华尔兹: https://objccn.io/issue-19-2/ 推荐:i...
    F麦子阅读 3,281评论 0 10
  • [转]浅谈LLDB调试器文章来源于:http://www.cocoachina.com/ios/20150126/...
    loveobjc阅读 2,410评论 2 6
  • 与调试器共舞 - LLDB 的华尔兹 nangege 2014/12/19 你是否曾经苦恼于理解你的代码,而去尝试...
    McDan阅读 870评论 0 0