做 iOS 开发的小伙伴们肯定对 gdb 和 lldb 不陌生,即便你不知道它是什么。

概念

lldb 都是 Xcode 中的调试器工具,如果你使用的是 Xcode 5 (或是 4.3,记不清了)以后的版本,那么其默认调试器已经由 gdb 替换为 lldb,新的调试器与 LLVM 编译器一起带来更加丰富的流程控制和数据检测功能,它为 Xcode 提供了底层调试环境,这篇博客所介绍的重点就是在 Xcode 中调试区的控制面板可以使用的调试指令。

关于更多的内容你或许可以参考 The LLDB Debugger官方文档

语法

在逐个介绍指令之前,首当其冲的是,命令的语法结构:

<command></command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]</action></subcommand></subcommand>

元素之间只用空格来分割,如果某个元素包含空格,则可以使用一对双引号("),如果其中已经包含双引号,则内部的双引号前要加斜杠(\)转义。或者,你也可以将含有空格的元素使用一对单引号(')包含,这样其内部的双引号将不再需要转义。

选项

通过前面介绍的内容可以看到,在调试指令中包含了一些选项(option),与 shell 类似,这些选项也同时有规范和缩写两种形式,它们的作用是等价的。

各个选项之间并没有规定顺序,但要注意的是,如果选项后面的参数以 - 开头,那么选项的末尾需要添加下划线(_)作为结束的标记。

另外,lldb 的命令解析支持使用 raw 指令,也即原始指令。你可以通过 help 来查看确认。

补全

对于 Command Completion 相信大家并不陌生,无论是终端还是 IDE 都支持此功能,但与终端通过横向制表符初始化不同,在 Xcode 的控制台,补全信息会在第三个字符被输入后自动弹出,此外你也可以使用 esc 键手动弹出。

脚本

如果你熟悉 Python,那么你还可以通过 lldb 中内置的 Python 解析器使用脚本命令来访问你的脚本。

可惜的是,由于我并不熟悉 Python,因此无法做过多的介绍。

print 指令用来打印一些简单值或对象的类型、值或地址等信息。使用十分简单,只需要按照 print var 的形式即可。

如果你只需要查看对象的值,你可以使用另外一个指令 po,它的全称是 print object,即打印对象。po 指令事实上是 exp -O — 指令的别名。

此外,对于简单类型,你还可以使用 print/ 指定打印的格式,这里有一份 格式清单

expression 指令

除了查看信息,lldb 还允许我们对值进行修改,通过 expression -O — 或其别名 expression 即可实现。例如:

(lldb) exp a = 10
(NSInteger) $0 = 10

如果你有兴趣可以参考 官方文档

image 指令

image 的用法有很多,希望你对 image 的第一反应不是图片。

常用的指令有:

  • image list: 列举工程中使用的库

  • image lookup: 查找可执行文件或共享库的原始地址

    • image lookup --address 0x00007fff8df487de: 通过奔溃抛出的错误信息找到崩溃所在位置

    • image lookup --type NSString: 查看具体的类型

如果你有兴趣,你还可以参考 官方文档

别名

前面多次提到别名这个概念,它的作用是方便我们的使用。在实际应用中,可能会有一些命令反复使用多次,于是反复输入就会浪费很多时间,尤其是命令较长时,还容易出现非预期的错误影响工作效率,这时候通过别名机制就可以很好的解决这些问题。

我们可以通过 command alias 别名 原命令 的形式为原命令定义别名。

例如我们为要设置断点:

(lldb) breakpoint Setter --file demo.c --line 200

由于要多次重复使用类似的代码,我们为其定义别名:

(lldb) command alias bfl breakpoint set -f %1 -l %2

接下来,我们只需要用别名即可继续使用:

(lldb) bfl demo 200

与 UNIX/Linux 类似,这些别名存放在 ~/.lldbinit 文件中,lldb 启动后会读取该文件,进一步识别我们定义的别名,我们可以使用 help -a 指令来查看他们。

此外,相应的,还有取消别名的指令:

(lldb) command unalias bfl

帮助

lldb 也提供了一个帮助系统,我们可以通过 help 指令来查看。

(lldb) help

当然你也可以在 help 后面加上某个指令来查看特定的帮助信息。

(lldb) help po

如果你对某些指令定义了别名,也可以通过别名查看帮助。

(lldb) command alias Meniny po
(lldb) help Meniny

此外 help 也可以用来查看某些参数的使用帮助,用法与前面相同。

终端

作为一只攻城狮,多少都有点黑窗口情节,值得庆幸的是 lldb 允许我们通过终端来进行调试,档次瞬间提升。

指定程序

要用终端调试,第一步首先要让终端知道,我们要调试哪个程序。

$ lldb /Projects/Demo/build/Debug/Demo.app
Current executable Setter to '/Projects/Demo/build/Debug/Demo.app' (x86_64).

或者,也可以在运行 lldb 后通过 file 指令处理。

$ lldb
(lldb) file /Projects/Demo/build/Debug/Demo.app
Current executable Setter to '/Projects/Demo/build/Debug/Demo.app' (x86_64).

设置断点

通常在调试过程中,我们都需要设置断点,这就要用到 breakpoint Setter 指令。

我们可以指定特定文件中的特定行号设置断点。

(lldb) breakpoint Setter --file demo.c --line 200

也可以给特定函数设置断点。

(lldb) breakpoint Setter --name demofunc

或者给 C++ 中特定名称的所有方法设置断点。

(lldb) breakpoint Setter --method cppdemo

又或者 Objective-C 中特定名称的所有 Selector 设置断点。

(lldb) breakpoint Setter --selector selectordemo:

此外,你还可以使用 --shlib 限定断点在一个特定的可执行库中。

(lldb) breakpoint Setter --shlib demo.dylib --name demo

查看断点

指令 breakpoint list 可以帮助你查看程序中的断点。

(lldb) breakpoint list
Current breakpoints:
1: name = 'selectordemo:', locations = 1, resolved = 1
  1.1: where = Demo`-[MXDemoView selectordemo:] + 33 at /Projects/Demo/MXDemoView:15, address = 0x0000000100010b2b, resolved, hit count = 0

设置观察点

除了断点,lldb 还支持在不中断程序运行的情况下监测一些变量,也即观察点。通过 watch Setter 指令即可实现。

(lldb) watch Setter var vardemo

启动程序

通过了前面的工作之后,是时候运行程序了。

(lldb) process launch
(lldb) run
(lldb) r

连接程序

当程序已经运行,我们则可以使用进程 ID 或进程名来与之进行连接。使用后者时,lldb 还支持 --waitfor 选项,也即等待下一个名称匹配的程序出现后连接。

(lldb) process attach --pid 213
(lldb) process attach --name DemoApp --waitfor

流程控制

当我们完成对某个断点位置的调试工作,就需要让程序在下一个断点之前继续运行。

(lldb) thread continue
Resuming thread 0x3c04 in process 21321
Resuming process 21321

类似的,还有一些其他指令:

(lldb) thread step-in // The same as "step" or "s" in GDB.
(lldb) thread step-inst // The same as "stepi" / "si" in GDB.
(lldb) thread step-out // The same as "finish" or "f" in GDB.
(lldb) thread step-over // The same as "next" or "n" in GDB.
(lldb) thread step-over-inst // The same as "nexti" / "ni" in GDB.

当然,还有按步调度(run until line)模式。

(lldb) thread until 20

状态检测

检测进程的当前状态:

(lldb) thread list

获取线程跟踪栈:

(lldb) thread backtrace

查看所有线程的调用栈:

(lldb) thread backtrace all

查看所有帧(frame)参数和本地变量:

(lldb) frame variable

查看指定参数名或变量:

(lldb) frame variable self

另外,frame variable 指令支持一些简单的操作符如 &, *, ->, []

(lldb) frame variable *self
(lldb) frame variable argv[0]

查看指定帧:

(lldb) frame select 2

最后,你或许有兴趣参考苹果的 官方文档