美文网首页
i 19:Xcode调试器LLDB

i 19:Xcode调试器LLDB

作者: 纵横而乐 | 来源:发表于2015-08-05 10:19 被阅读942次

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

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"]

相关文章

  • i 19:Xcode调试器LLDB

    相信大家肯定都有过为了调试而添加打印变量,或者使用直接常量代替函数调用结果,或者更改判断条件以进入某特定分支的调试...

  • lldb调试和chisel、DerekSelander、cycr

    0x01 LLDB Xcode 5发布之后,LLDB调试器已经取代了GDB,成为了Xcode工程中默认的调试器。它...

  • LLDB杂谈

    LLDB 在Xcode5中,LLDB正式取代之前的GDB成为Xcode的标准调试器。和LLVM协同工作。LLDB兼...

  • LLDB调试和实战

    LLDB LLDB官方文档教程 Xcode4.0开始,编译器改用LLVM,调试器从gdb改为LLDB LLDB全称...

  • LLDB学习笔记

    转载 与调试器共舞 - LLDB 的华尔兹: https://objccn.io/issue-19-2/ 推荐:i...

  • LLDB调试工具简介:1-常用命令

    LLDB简介 LLDB是LLVM下的调试器。Xcode从4.0开始编译器开始改用LLVM,相应的调试器也从gdb改...

  • iOS | LLDB调试基础篇(一)

    LLDB简介 LLDB是一个调试器,在此之前,Xcode使用的是GDB调试器和GCC编译器,后来因为GCC官方对苹...

  • Help & Apropos

    LLDB是新一代高性能调试器,是Xcode的默认调试器,并且支持调试C/C++程序。LLDB命令繁多,下面开始接少...

  • LLDB

    随着Xcode 5的发布,LLDB调试器已经取代了GDB,成为了Xcode工程中默认的调试器。它与LLVM编译器一...

  • iOS_LLDB 调试命令

    GDB 和LLDB LLDB 在xcode4.3或者之后的版本里面默认的调试器,在这之前用的就是GDB。 LLDB...

网友评论

      本文标题:i 19:Xcode调试器LLDB

      本文链接:https://www.haomeiwen.com/subject/wbmvqttx.html