LLDB基本使用

LLDB是一个有着REPL(Read-Eval-Print-Loop)的特性和C++、Python插件的开源调试器。LLDB内置于Xcode,存在于主窗口底部的控制台中。调试器允许你在程序运行的特定时暂停它,你可以查看变量的值,执行自定的指令,并且按照你所认为合适的步骤来操作程序的进展。

LLDB命令结构

<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]
  • command和subcommand:LLDB调试命令的名称。命令和子命令按层级结构来排列,一个命令为其子命令创建上下文,子命令又为其子命令创建上下文,依此类推。
  • action:在前面的命令序列上下文中执行的一些操作。
  • options:行为修改器(action modifiers)。
  • argument:不同命令上下文表示不同意义的参数。

LLBD命令解析后再执行。上面这些元素之间通过空格分割,如果某一元素自身含有空格,则可以使用双引用;而如果元素中又包含双引号,则可以使用反斜杠;或者元素使用单引号。

(lldb) command [subcommand] -option "some \"quoted\" string"
(lldb) command [subcommand] -option 'some "quoted" string'

命令选项

LLDB中的命令选项有规范形式和缩写形式两种格式。以设置断点的命令breakpoint set为例,括号中的是规范形式:

breakpoint set
   -M <method> (--method <method>)
   -S <selector> (--selector <selector>)
   -b <function-name> (--basename <function-name>)
   -f <filename> (--file <filename>)
   -l <linenum> (--line <linenum>)
   -n <function-name> (--name <function-name>)

各选项的顺序是任意的。如果后面的参数是以”-“开头的,则在选项后面添加”–”作为选项的终止信号。

(lldb) process launch --stop-at-entry -- -program_arg_1 value -program_arg_2 value

–stop-at-entry是命令选项,-program_arg_1和-program_arg_2是参数,中间使用”–”区分。

唯一匹配原则

LLDB的命令遵循唯一匹配原则:假如根据前n个字母已经能唯一匹配到某个命令,则只写前n个字母等效于写下完整的命令,比如:

breakpoint set -n main
等价于
br s -n main

~/.lldbinit

LLDB有了一个启动时加载的文件~/.lldbinit,每次启动都会加载。所以初始化的事儿,比如命令别名,都可以写入其中,但由于这事lldb还没有开始调试,所以有些操作无法进行,比如设置断点。


在命令行中调试

程序:
$ lldb /Projects/Sketch/build/Debug/Sketch.app
或者
$ lldb
(lldb) file /Projects/Sketch/build/Debug/Sketch.app

一旦指定了调试哪个程序,并为其设置了一些断点后,就可以开始运行程序了
(lldb) process launch
(lldb) run

同样可以使用进程ID或进程名来连接一个已经运行的程序
(lldb) process attach --pid 123
(lldb) process attach --name Sketch
断点:
如果想在某个文件中的某行设置一个断点
(lldb) breakpoint set --file foo.c --line 12

如果想给某个C函数设置断点
(lldb) breakpoint set --name foo

如果想给C++中所有命名为foo的方法设置断点
(lldb) breakpoint set --method foo

如果想给Objec中所有命名为alignLeftEdges:的选择器设置断点
(lldb) breakpoint set --selector alignLeftEdges:

如果想在一个特定的可执行库中某个C函数设置断点
(lldb) breakpoint set --shlib foo.dylib --name foo

如果想查看所有断点
(lldb) breakpoint list
Current breakpoints:
1: file = , line = , locations = , resolved = , hit count = 
  1.1: where = , address = , resolved, hit count = 

每个断点都有一个整数标识,如上所示的1。每个断点有至少一个断点位置,每个位置也会有一个标识,如上所示的1.1。resolved标识表示当与之相关的文件地址被加载到程序进行调试时,其位置是已解析的。例如,如果在共享库中设置的断点之后被卸载了,则断点的位置还会保留,但其不能再被解析。

观察点:

Watchpoint是一个用来监听变量的值的变化或者内存地址的变化的工具,发生变化时会在debugger中触发一个暂停。对于那些不知道如何准确跟踪的状态问题,可以利用这个工具来解决。值得注意的是,硬件资源对观察点有数目限制,一般为4个。对那些设置失败的观察点需要禁用或者删除来释放资源。

添加变量观察点
(lldb)watchpoint set variable

添加地址观察点
(lldb)watchpoint set expression

查看所有观察点
(lldb)watchpoint list
控制:
启动程序后,LLDB允许程序在到达断点前继续运行。LLDB中流程控制的命令都在thread命令层级中
(lldb)thread continue (=c)
(lldb)thread step-in (=s)
(lldb)thread step-over (=n)
(lldb)thread step-out (=finish)
(lldb)thread step-inst (=si =stepi)
(lldb)thread step-over-inst (=ni =nexti)

使得当前函数立即以某值返回(可能会使内存出错)
(lldb)thread return 

LLDB还提供了run until line按步调度模式
(lldb) thread until 100
这条命令会运行当前线程,直到当前帧(frame)所在文件的第100行。
线程:
获取当前所有线程
(lldb) thread list
* thread #1: tid = , where = , stop reason = , queue =
  thread #1: tid = , where = , stop reason = , queue = 
  thread #1: tid = , where = , stop reason = , queue = 

星号(*)表示thread #1为当前线程。获取线程调用栈
(lldb) thread backtrace (=bt)

获取当前线程信息
(lldb) thread info

获取调用栈当前帧可见变量
(lldb) frame variable

获取调用栈当前帧信息
(lldb) frame info

切换调用栈的当前帧
(lldb) frame select (=f)
内存:
读出指定地址内存内容
(lldb)memory read
(lldb)x (=memory read)

写入指定地址内存内容
(lldb)memory write
表达式:

expression/格式

在LLDB中执行一个表达式
(lldb)expression

执行没有返回值的表达式
(lldb)call (='expression --')

执行基本类型的表达式
(lldb)p (='expression --')

执行objc对象类型的表达式
(lldb)po (='expression -o --')

同p
(lldb)print (='expression --')
寄存器:
获取调用栈当前帧的寄存器值
(lldb) register read

异常中断时,通过访问寄存器(Mac和Simulator用$rax,Device用$r0)获得异常信息
(lldb) po [$rax class]
(lldb) po [$rax name]
(lldb) po [$rax reason]
目标模块:
(lldb)image (=target modules)

查看程序和依赖共享库
(lldb)image list

查看程序和依赖共享库的信息
(lldb)image lookup

当程序崩溃时,可以使用image来查找崩溃所在的具体位置
NSArray *array = @[@1, @2];
NSLog(@"item 3: %@", array[2]);
这段代码运行后会抛出异常:
 0 CoreFoundation   0x00007fff8e06f66c __exceptionPreprocess + 172
 1 libobjc.A.dylib  0x00007fff886ad76e objc_exception_throw + 43
 2 CoreFoundation   0x00007fff8df487de -[__NSArrayI objectAtIndex:] + 190
 3 test             0x0000000100000de0 main + 384
 4 libdyld.dylib    0x00007fff8f1b65c9 start + 1
根据以上信息,能判断崩溃在模块test中,要想知道具体文件具体位置
(lldb) image lookup --address 0x0000000100000de0
Address: test[0x0000000100000de0] (test.__TEXT.__text + 384)
Summary: test`main + 384 at main.m:23

当想查看一个符号的信息时,可以使用image来查找
(lldb) image lookup --name

当想查看一个类型的信息时,可以使用image来查找
(lldb) image lookup --type
目标暂停钩子:

使用LLDB进行调试时,大多数时候需要让程序暂停,不管是breakpoint还是watchpoint。需要在每次stop时执行的命令可以使用暂停钩子实现(只对breakpoint和watchpoint的程序stop生效,对直接点击Xcode上的pause或者debug view hierarchy不会生效):

添加暂停钩子
(lldb) target stop-hook add (=display)

删除暂停钩子
(lldb) target stop-hook delete (=undisplay)

查看钩子所有暂停钩子
(lldb) target stop-hook list

启用暂停钩子
(lldb) target stop-hook enable

禁用暂停钩子
(lldb) target stop-hook disable
目标符号:

程序要运行之前,都会编译成二进制文件,同时会生成dSYM文件(记录了源代码和二进制映射关系),这样对代码打断点就会对应到二进制上。当Xcode找不着dSYM文件时无法调试,通过手动地将对应的dSYM文件添加到对应的目标,建立源代码和二进制的关映射系:

(lldb) image symbol add (=add-sym)
别名系统:

LLDB的别名机制能为常用的命令创建一个别名,方便使用:

(lldb) breakpoint set --file foo.c --line 12
等价于:
(lldb) command alias bfl breakpoint set -f %1 -l %2
(lldb) bfl foo.c 12
如果不想要已有的别名了,可以取消:
(lldb) command unalias b
常见问题:

LLDB的expression parser有一个bug,不兼容非ASCII字符,需要处理一下才行,否则会报错“An Objective-C constant string’s string initializer is not an array”

(lldb) expression @"哈哈"
Internal error [IRForTarget]: An Objective-C constant string's string initializer is not an array
error: 0 errors parsing expression
error: The expression could not be prepared to run in the target
(lldb) expression [NSString stringWithUTF8String:"哈哈"]
(__NSCFString *) $5 = 0x0000618000025ca0 @"哈哈"

类型不明或不匹配

(lldb) p NSLog(@"%@",[self.view  viewWithTag:1001])
error: 'NSLog' has unknown return type; cast the call to its declared return type
error: 1 errors parsing expression

如果在使用LLDB命令中发现有unknown type的类似错误(多见于id类型),就必须显式声明类型。
p (void)NSLog(@"%@",[self.view  viewWithTag:1001])

找不到方法

(lldb) po self.view.frame
error: unsupported expression with unknown type
error: unsupported expression with unknown type
error: 2 errors parsing expression

无法通过点属性访问的方法
(lldb) p (CGRect)[self.view frame]
(CGRect) $0 = origin=(x=0, y=0) size=(width=320, height=480)

不过已经有解决方法了


在Xcode中调试

Xcode集成了LLDB(代替了原来的GDB),在Xcode启动Run时自动将App关联到LLDB并启动调试。LLDB的调试窗口在ConsoleView,那里可以输入LLDB的命令进行调试。下面从Xcode的UI调试说起。

一般断点(Normal Breakpoint):

快捷键Command+\

在代码编译器左边的行数栏处,可以用鼠标点击生成一个断点标签。

右键标签对断点进行编辑、禁用启用、删除。

在编辑里,可以添加响应条件、忽略次数、执行动作和暂停继续。这些都可以在命令行设置。

执行的动作包括执行Apple脚本、截取GPU参数、LLDB命令、日志信息、Shell命令和发声。

异常断点(Exception Breakpoint):

Objc中的异常是一个常被忽略的地方,大部分错误信息,系统框架都会以异常的形式throw出来。添加异常断点后,每次当程序发生了异常时,会被中断并定位在异常产生的代码,有利于跟踪定位。

在编辑里,可以选择异常种类(Objc和C++)、响应时机、执行动作和暂停继续。

符号断点(Symbolic Breakpoint):

快捷键Option+Command+\

符号断点是指在某个特定的函数或方法开始的地方,暂停程序执行,通过这种方式添加断点,就不需要知道在源文件中添加,也不需要知道断点设置在文件的第几行。

在编辑里,可以选择响应符号(C函数或C++方法或Objc选择器的名字)、响应模块、相应条件、忽略次数、执行动作和暂停继续。

常用的符号有**”objc_exception_throw”(等价于异常断点)和“-[NSObject(NSObject) doesNotRecognizeSelector:]”**(跟踪objc方法调用失败)。

测试断点:

在测试用例编辑器或者断点导航栏里添加,这个类型的断点会在测试用例失败时,暂停程序执行并定位在失败的地方。

观察点:

观察点也可以在Xcode设置,不过不像断点在编辑器里,观察点在Variable View里。右键点击想要观察的点,选择Watch。

观察点同样列在了断点导航器里面进行管理。注意,Xcode默认是创建的读写观察,如果想要只读和只写观察,还是得用命令行。同时,无效的观察点记得及时禁用或者删除,因为观察点数目有限很珍贵,也可以减少不必要的中断。

内存:

除了在命令行里使用x或者memory read读取指定地址的内存外,也可以在Xcode设置。同观察点一样,内存查看在Variable View里。右键点击想要观察的变量,选择View Memory。

或者Debug->Debug Workflow->View Memory或者快捷键Shift+Command+M。这里你可以输入任何想要查看的内存地址,查看的篇幅,以后翻页。

界面:

Xcode6前有许多第三方的界面调试工具,比如最著名的Reveal。Xcode6后提供了DebugViewHierarchy功能,它可以让开发者在程序运行时,动态的查看当前界面的显示情况,包括视图的层次,控件的大小和位置,而且会以3D效果显示当前视图的层次。虽然功能没有Reveal丰富,但是足够使用,界面调试以后再专门学习介绍。

或者在调试导航栏里选择查看方法为UI。


总结

对于lldb相关的调试项,在Xcode开始就可以设置好一些断点,比如全局的异常断点,常用的符号断点-[NSObject(NSObject) doesNotRecognizeSelector:],并设置好action和sound,这样能在运行时截获大部分常见的错误,也能获取详细的信息和形象的提醒。在命令行中使用Chisel能帮助提升调试效率。


延伸阅读


LLDB基本使用
https://hllovesgithub.github.io/2015/12/19/2015-12-19-LLDB基本使用/
作者
Hu Liang
发布于
2015年12月19日
许可协议