LLDB调试技巧

XCode5之后,所有XCode的项目,调试器自动配置为lldb(LLDB Quick Start Guide),替代了原来的GDBLLDB本身作为一个功能完整的调试器,既可以在通过XCodeGUI调试模块来工作,也可以单独工作。

如果能够了解LLDB本身的常用命令,那么在XCode的环境中使用它或者在XCodedebug console中使用命令都会更加得心应手。

LLDB命令基本语法

如果有GDB的经验的话,会发现LLDB的命令很相似。所有的LLDB命令都是以下的结构:

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

具体示例:

(lldb) breakpoint set --file test.c --line 12
  • <command>即上图中breakpoint部分。使用help命令可以看到LLDB定义的基本命令集。
  • subcommand通常是由动词构成,它表示一个command内的一类相关操作。比如上例中的command,也就是breakpoint还有deletedisablesubcommand。使用help <command>就能查看相关commandsubcommand
  • argument一个command或者command [subcommand]组合可能需要跟不同参数(argument)。针对不同的option还可以跟不同的参数。比如上例中test.c12就是跟在breakpoint set不同的option之后的参数。
  • option用来修饰需要进行的操作。常常在其起前面用--分割,一些option也提供简约表达形式使用-开头。比如上例中,--file这个option的简约表达式为-f

为命令设置别名(command alias

如果觉得命令的输入十分冗长、不方便,可以为常用的命令设置便于记住的别名。这样就方便使用,而不用每次输入长段的命令。比如:

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

将在某个文件(-f 之后的参数)某一行(-l 之后的参数)设置一个断点的命令用别名bfl代替。在使用:

(lldb) bfl foo.c 12

实际上就是在执行:

(lldb) breakpoint set --file foo.c --line 12

常见的一些如nextstepcontinue都是LLDB初始配置的命令别名。想要查看所有的命令,包括自定义的命令别名,在现版本的LLDB(lldb-370.0.37)中,只需要键入help命令:

...  /* 命令 */
Current command abbreviations (type 'help command alias' for more info):
... /* 命令别名 */

第二段就列出了所有的命令别名。

如果想要去掉自己设置的或者是LLDB预设的命令别名,只需要执行下列命令就可以了:

(lldb) command unalias <command-alias> 

使用LLDBhelp功能

  • 使用help help查看help命令的使用;
  • 使用help <command>查看某个command的使用;
  • 使用help <command> [subcommand]查看某个command下的子命令的使用;

命令不同形式

以在XCode中最常用的po命令来举例。LLDB的命令是存在多种形式的,实际在上面已经零散提到。

命令形式 命令内容
标准形式 expression –object-description – someVariable
简写形式 e -O – someVariable
别名形式 po someVariable

需要解释一下的是这里--object-description这个option之后用--分割了argument部分。文档解释:

Commands that accept options as well as freeform arguments, such as the expression command, must place a space-delimited double dash (–) between the last option and the first argument. This ensures that arguments resembling an option by starting with a dash (-) are interpreted as an argument.

独立使用LLDB流程

绝大部分时候,都是通过XCode间接地使用LLDB的调试功能。理解如何独立使用LLDB调试程序的流程,也能更好地理解在XCode中调试程序的技巧。

大体上,使用LLDB可以分为下面几个步骤(大致有先后顺序):

  • 确定调试的程序
  • 设置调试断点
  • 控制程序运行
  • 输出线程和stack frame信息。

在这里将文档实例照搬至此。

假设有一下的一个名为Greeter.swift的文件:

class Greeter {
    private var acquaintances: Set<String> = []

    func hasMet(personNamed name: String) -> Bool {
	return acquaintances.contains(name)
    }

    func greet(personNamed name: String) {
	if hasMet(personNamed: name) {
	    print("Hello again, \(name)!")
	} else {
	    acquaintances.insert(name)
	    print("Hello, \(name). Nice to meet you!")
	}
    }
}

let greeter = Greeter() 
greeter.greet(personNamed: "Anton")
greeter.greet(personNamed: "Mei")
greeter.greet(personNamed: "Anton")

在使用swiftc命令编译之后会产生一个名为Greeter的可执行文件:

$ swiftc -g Greeter.swift
$ ls
Greeter.dSYM
Greeter.swift
Greeter*

确定调试程序

首先,使用lldb来启动程序。

$ lldb Greeter
(lldb) target create "Greeter"
Current executable set to 'Greeter' (x86_64).

该命令就会进入lldb的调试session,能够让你输入不同的lldb命令与被调试的程序交互。

设置调试断点

在确定了调试的程序之后,通常需要设置调试断点。断点一般设置在自己需要特别关心的代码块上。

(lldb) breakpoint set --line 18
Breakpoint 1: where = Greeter`main + 70 at Greeter.swift:18, address = 0x0000000100001996

运行程序

使用process launch来运行需要调试的程序。前面lldb <program>只是开启了针对program的调试session,并没有启动被调试的程序进程。

(lldb) process launch
Process 97209 launched: 'Greeter' (x86_64)
Process 97209 stopped
* thread #1: tid = 0x1288be3, 0x0000000100001996 Greeter`main + 70 at Greeter.swift:18, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100001996 Greeter`main + 70 at Greeter.swift:18
15          }
16      }
18
-> 18      let greeter = Greeter()
19
20      greeter.greet(personNamed: "Anton")
21      greeter.greet(personNamed: "Mei")

控制程序运行

使用thread step-over跳过断点所在行,调至下一行。

(lldb) thread step-over
Process 97209 stopped
* thread #1: tid = 0x1288be3, 0x00000001000019bd Greeter`main + 109 at Greeter.swift:20, queue = 'com.apple.main-thread', stop reason = step over
  frame #0: 0x00000001000019bd Greeter`main + 109 at Greeter.swift:20
  17
  18      let greeter = Greeter()
  19
  -> 20      greeter.greet(personNamed: "Anton")
  21      greeter.greet(personNamed: "Mei")
  22      greeter.greet(personNamed: "Anton")

使用thread step-in,如果断点在函数(或者方法)处,可以进入函数(或者方法)内部;如果不在,其效果跟thread step-over一样。

输出信息

使用thread backtrace输出程序执行至目前断点处之前的栈帧顺序。

(lldb) thread backtrace
* thread #1: tid = 0x1288be3, 0x0000000100001a98 Greeter`Greeter.hasMet(name="Anton", self=0x0000000101200190) -> Bool + 24 at Greeter.swift:5, queue = 'com.apple.main-thread', stop reason = step in
   frame #0: 0x0000000100001a98 Greeter`Greeter.hasMet(name="Anton", self=0x0000000101200190) -> Bool + 24 at Greeter.swift:5
   * frame #1: 0x0000000100001be4 Greeter`Greeter.greet(name="Anton", self=0x0000000101200190) -> () + 84 at Greeter.swift:9
   frame #2: 0x00000001000019eb Greeter`main + 155 at Greeter.swift:20
   frame #3: 0x00007fff949d05ad libdyld.dylib`start + 1
   frame #4: 0x00007fff949d05ad libdyld.dylib`start + 1

使用frame variable输出当前栈帧上的所有变量。

(lldb) frame variable
(String) name = "Anton"
(Greeter.Greeter) self = 0x0000000100502920 {
acquaintances = ([0] = "Anton")
}

参考资料