随着Xcode 5的发布,LLDB调试器已经取代了GDB,成为了Xcode工程中默认的调试器。它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能。LLDB为Xcode提供了底层调试环境,其中包括内嵌在Xcode IDE中的位于调试区域的控制面板,在这里我们可以直接调用LLDB命令。如图1所示:
图1:位于Xcode调试区域的控制台

在本文中,我们主要整理一下LLDB调试器提供给我们的调试命令,更详细的内容可以查看The LLDB Debugger。
LLDB命令结构
在使用LLDB前,我们需要了解一下LLDB的命令结构及语法,这样可以尽可能地挖掘LLDB的潜能,以帮助我们更充分地利用它。
LLDB命令的语法有其通用结构,通常是以下形式的:
|
|
其中:
<command>(命令)和<subcommand>(子命令):LLDB调试命令的名称。命令和子命令按层级结构来排列:一个命令对象为跟随其的子命令对象创建一个上下文,子命令又为其子命令创建一个上下文,依此类推。<action>:我们想在前面的命令序列的上下文中执行的一些操作。<options>:行为修改器(action modifiers)。通常带有一些值。<argument>:根据使用的命令的上下文来表示各种不同的东西。
LLBD命令行的解析操作在执行命令之前完成。上面的这些元素之间通过空格来分割,如果某一元素自身含有空格,则可以使用双引用。而如果元素中又包含双引号,则可以使用反斜杠;或者元素使用单引号。如下所示:
|
|
这种命令解析设计规范了LLDB命令语法,并对所有命令做了个统一。
命令选项
LLDB中的命令选项有规范形式和缩写形式两种格式。以设置断点的命令breakpoint set为例,以下列表了其部分选项的格式,其中括号中的是规范形式:
|
|
各选项的顺序是任意的。如果后面的参数是以”-“开头的,则在选项后面添加”–”作为选项的终止信号,以告诉LLDB我们处理的选项的正确位置。如下命令所示:
|
|
如上所示,命令的选项是--stop-at-entry,参数是-program_arg_1和-program_arg_2,我们使用”–”将选项与参数作一下区分。
原始命令
LLDB命令解析器支持”原始(raw)”命令,即没有命令选项,命令字符串的剩余部分未经解析就传递给命令。例如,expression就是一个原始命令。
不过原始命令也可以有选项,如果命令字符串中有虚线,则在命令名与命令字符串之间放置一个选项结束符(–)来表明没有命令标记。
我们可以通过help命令的输出来查看一个命令是否是原始命令。
命令补全(Command Completion)
LLDB支持源文件名,符号名,文件名,等等的命令补全(Commmand Completion)。终端窗口中的补全是通过在命令行中输入一个制表符来初始化的。Xcode控制台中的补全与在源码编辑器中的补全方式是一样的:补全会在第三个字符被键入时自动弹出,或者通过Esc键手动弹出。
一个命令中的私有选项可以有不同的完成者(completers)。如breakpoint中的--file <path>选项作为源文件的完成者,--shlib <path>选项作为当前加载的库的完成者,等等。这些行为是特定的,例如,如果指定--shlib <path>,且以--file <path>结尾,则LLDB只会列出由--shlib <path>指定的共享类库。
Python脚本
对于高级用户来说,LLDB有一个内置的Python解析器,可以通过脚本命令来访问。调试器中的所有特性在Python解析器中都可以作为类来访问。这样,我们就可以使用LLDB-Python库来写Python函数,并通过脚本将其加载到运行会话中,以执行一些更复杂的调试操作。
在命令行中调试程序
通常我们都是在Xcode中直接使用LLDB调试器,Xcode会帮我们完成很多操作。当然,如果我们想让自己看着更Bigger,或者想了解下调试器具体的一些流程,就可以试试直接在终端使用LLDB命令来调试程序。在终端中使用LLDB调试器,我们需要了解以下内容:
- 加载程序以备调试
- 将一个运行的程序绑定到
LLDB - 设置断点和观察点
- 控制程序的执行
- 在调试的程序中导航
- 检查状态和值的变量
- 执行替代代码
了解在终端中这些操作是如何进行的,可以帮助我们更深入的了解调试器在Xcode中是如何运作的。下面我们分步来介绍一下。
指定需要调试的程序
首先我们需要设置需要调试的程序。我们可以使用如下命令做到这一点:
|
|
或者在运行lldb后,使用file命令来处理,如下所示:
|
|
设置断点
在设置完程序后,我们可能想设置一点断点来调试程序。此时我们可以使用breakpoint set命令来设置断点,这个命令简单、直观,且有智能补全,接下来我们看看它的具体操作。
如果想在某个文件中的某行设置一个断点,可使用以下命令:
|
|
如果想给某个函数设置断点,可使用以下命令:
|
|
如果想给C++中所有命名为foo的方法设置断点,可以使用以下命令:
|
|
如果想给Objective-C中所有命名为alignLeftEdges:的选择器设置断点,则可以使用以下命令:
|
|
我们可以使用--shlib <path>来将断点限定在一个特定的可执行库中:
|
|
看吧,断点设置命令还是很强大的。
如果我们想查看程序中所有的断点,则可以使用breakpoint list命令,如下所示:
|
|
从上面的输出结果可以看出,一个断点一般有两部分:
- 断点的逻辑规范,这一部分是用户提供给
breakpoint set命令的。 - 与规范匹配的断点的位置。
如上所示,通过"breakpoint set --selector alignLeftEdges:"设置的断点,其信息中会显示出所有alignLeftEdges:方法的位置。
breakpoint list命令输出列表显示每个逻辑断点都有一个整数标识,如上所示断点标识为1。而每个位置也会有一个标识,如上所示的1.1。
输出列表中另一个信息是断点位置是否是已解析的(resolved)。这个标识表示当与之相关的文件地址被加载到程序进行调试时,其位置是已解析的。例如,如果在共享库中设置的断点之后被卸载了,则断点的位置还会保留,但其不能再被解析。
不管是逻辑断点产生的所有位置,还是逻辑断点解析的任何特定位置,我们都可以使用断点触发命令来对其进行删除、禁用、设置条件或忽略计数操作。例如,如果我们想添加一个命令,以在LLDB命中断点1.1时打印跟踪栈,则可以执行以下命令
|
|
如果想更详细地了解"breakpoint command add"命令的使用,可以使用help帮助系统来查看。
设置观察点
作为断点的补充,LLDB支持观察点以在不中断程序运行的情况下监测一些变量。例如,我们可以使用以下命令来监测名为global的变量的写操作,并在(global==5)为真时停止监测:
|
|
可以使用help watchpoint来查看该命令的使用。
使用LLDB来启动程序
一旦指定了调试哪个程序,并为其设置了一些断点后,就可以开始运行程序了。我们可以使用以下命令来启动程序:
|
|
我们同样可以使用进程ID或进程名来连接一个已经运行的程序。当使用名称来连接一个程序时,LLDB支持--waitfor选项。这个选项告诉LLDB等待下一个名称为指定名称的程序出现,然后连接它。例如,下面3个命令都是用于连接Sketch程序(假定其进程ID为123):
|
|
启动或连接程序后,进程可能由于某些原因而停止,如:
|
|
注意“1 of 3 threads stopped with reasons:”及其下面一行。在多线程环境下,在内核实际返回控制权给调试器前,可能会有多个线程命中同一个断点。在这种情况下,我们可以在停止信息中看到所有因此而停止的线程。
控制程序
启动程序后,LLDB允许程序在到达断点前继续运行。LLDB中流程控制的命令都在thread命令层级中。如下所示:
|
|
另外,还有以下命令:
|
|
LLDB还提供了run until line按步调度模式,如:
|
|
这条命令会运行线程,直到当前frame到达100行。如果代码在运行的过程中跳过了100行,则当frame被弹出栈后终止执行。
查看线程状态
在进程停止后,LLDB会选择一个当前线程和线程中当前帧(frame)。很多检测状态的命令可以用于这个线程或帧。
为了检测进程的当前状态,可以从以下命令开始:
|
|
星号(*)表示thread #1为当前线程。为了获取线程的跟踪栈,可以使用以下命令:
|
|
如果想查看所有线程的调用栈,则可以使用以下命令:
|
|
查看调用栈状态
检查帧参数和本地变量的最简便的方式是使用frame variable命令:
|
|
如果没有指定任何变量名,则会显示所有参数和本地变量。如果指定参数名或变量名,则只打印指定的值。如:
|
|
frame variable命令不是一个完全的表达式解析器,但它支持一些简单的操作符,如&,*,->,[]。这个数组括号可用于指针,以将指针作为数组处理。如下所示:
|
|
frame variable命令会在变量上执行”对象打印”操作。目前,LLDB只支持Objective-C打印,使用的是对象的description方法。
如果想查看另外一帧,可以使用frame select命令,如下所示:
|
|
小结
以上所介绍的命令可以让我们在终端中直接调试程序。当然,很多命令也可以在Xcode中直接使用。这些命令可以让我们了解程序运行的状态,当然有些状态可以在Xcode中了解到。建议在调试过程中,可以多使用这些命令。
如果想了解这一过程中使用的各种命令,可以查看苹果的官方文档。
在Xcode中调试程序
对于我们日常的开发工作来说,更多的时候是在Xcode中进行调试工作。因此上面所描述的流程,其实Xcode已经帮我们完成了大部分的工作,而且很多东西也可以在Xcode里面看到。因此,我们可以把精力都集中在代码层面上。
在苹果的官方文档中列出了我们在调试中能用到的一些命令,我们在这重点讲一些常用的命令。
打印
打印变量的值可以使用print命令,该命令如果打印的是简单类型,则会列出简单类型的类型和值。如果是对象,还会打印出对象指针地址,如下所示:
|
|
在输出结果中我们还能看到类似于$0,$1这样的符号,我们可以将其看作是指向对象的一个引用,我们在控制面板中可以直接使用这个符号来操作对应的对象,这些东西存在于LLDB的全名空间中,目的是为了辅助调试。如下所示:
|
|
另外$后面的数值是递增的,每打印一个与对象相关的命令,这个值都会加1。
上面的print命令会打印出对象的很多信息,如果我们只想查看对象的值的信息,则可以使用po(print object的缩写)命令,如下所示:
|
|
当然,po命令是"exp -O --"命令的别名,使用"exp -O --"能达到同样的效果。
对于简单类型,我们还可以为其指定不同的打印格式,其命令格式是print/,如下所示:
|
|
格式的完整清单可以参考Output Formats。
expression
在开发中,我们经常会遇到这样一种情况:我们设置一个视图的背景颜色,运行后发现颜色不好看。嗯,好吧,在代码里面修改一下,再编译运行一下,嗯,还是不好看,然后再修改吧~~这样无形中浪费了我们大把的时间。在这种情况下,expression命令强大的功能就能体现出来了,它不仅会改变调试器中的值,还改变了程序中的实际值。我们先来看看实际效果,如下所示:
|
|
expression命令的功能不仅于此,正如上面的po命令,其实际也是"expression -O --"命令的别名。更详细使用可以参考Evaluating Expressions。
image
image命令的用法也挺多,首先可以用它来查看工程中使用的库,如下所示:
|
|
我们还可以用它来查找可执行文件或共享库的原始地址,这一点还是很有用的,当我们的程序崩溃时,我们可以使用这条命令来查找崩溃所在的具体位置,如下所示:
|
|
这段代码在运行后会抛出如下异常:
|
|
根据以上信息,我们可以判断崩溃位置是在main.m文件中,要想知道具体在哪一行,可以使用以下命令:
|
|
可以看到,最后定位到了main.m文件的第23行,正是我们代码所在的位置。
我们还可以使用image lookup命令来查看具体的类型,如下所示:
|
|
可以看到,输出结果中列出了NSURL的一些成员变量及属性信息。
image命令还有许多其它功能,具体可以参考Executable and Shared Library Query Commands。
流程控制
流程控制的命令实际上我们在上一小节已经讲过了,在Xcode的控制面板中同样可以使用这些命令,在此不在重复。
命令别名及帮助系统
LLDB有两个非常有用的特性,即命令别名及帮助。
命令别名
我们可以使用LLDB的别名机制来为常用的命令创建一个别名,以方便我们的使用,如下命令:
|
|
如果在我们的调试中需要经常用到这条命令,则每次输入这么一长串的字符一定会很让人抓狂。此时,我们就可以为这条命令创建一个别名,如下所示:
|
|
这样,我们只需要按如下方式来使用它即可:
|
|
是不是简单多了?
我们可以自由地创建LLDB命令的别名集合。LLDB在启动时会读取~/.lldbinit文件。这个文件中存储了command alias命令创建的别名。LLDB帮助系统会读取这个初始化文件并会列出这些别名,以让我们了解自己所设置的别名。我们可以使用"help -a"命令并在输出的后面来查看这边别名,其以下面这行开始:
|
|
如果我们不喜欢已有命令的别名,则可以使用以下命令来取消这个别名:
|
|
帮助系统
LLDB帮助系统让我们可以了解LLDB提供了哪些功能,并可以查看LLDB命令结构的详细信息。熟悉帮助系统可以让我们访问帮助系统中中命令文档。
我们可以简单地调用help命令来列出LLDB所有的顶层命令。如下所示:
|
|
如果help后面跟着某个特定的命令,则会列出该命令相关的所有信息,我们以breakpoint set为例,输出信息如下:
|
|
还有一种更直接的方式来查看LLDB有哪些功能,即使用apropos命令:它会根据关键字来搜索LLDB帮助文档,并为每个命令选取一个帮助字符串,我们以apropos file为例,其输出如下:
|
|
我们还可以使用help来了解一个命令别名的构成。如:
|
|
help命令的另一个特性是可以查看某个具体参数的使用,我们以"break command add"命令为例:
|
|
如果想了解以上输出的参数<breakpt-id>的作用,我们可以在help后面直接指定这个参数(将其放在尖括号内)来查询它的详细信息,如下所示:
|
|
帮助系统能让我们快速地了解一个LLDB命令的使用方法。经常使用它,可以让我们更快地熟悉LLDB的各项功能,所以建议多使用它。
总结
LLDB带给我们强大的调试功能,在调试过程中充分地利用它可以帮助我们极大地提高调试效率。我们可以不用写那么多的NSLog来打印一大堆的日志。所以建议在日常工作中多去使用它。当然,上面的命令只是LLDB的冰山一角,更多的使用还需要大家自己去发掘,在此只是抛砖引玉,做了一些整理。