随着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
的冰山一角,更多的使用还需要大家自己去发掘,在此只是抛砖引玉,做了一些整理。