LLDB原理与调试实践

发布时间 2023-04-27 04:18:14作者: 吴建明wujianming

LLDB原理与调试实践

使用LLDB
  LLDB(Low Level Debugger)项目以LLVM基础设施构造一个调试器。它作为Mac OS X系统的Xcode 5调试器,正在活跃地开发和发布。由于2011年开发之初就被置于Xcode范围之外,LLDB一直未发布一个稳定的版本,直到写作本文之时。你可以从这个链接获取LLDB源代码:http://releases.llvm.org/3.4/lldb-3.4.src.tar.gz。如同其它依赖LLVM的项目,将它集成到LLVM编译系统中,就可以轻松地编译它。这就是说,将源代码放在LLVM tools文件夹,如下所示:

$ wget http://releases.llvm.org/3.4/lldb-3.4.src.tar.gz
$ tar xvf lldb-3.4.src.tar.gz
$ mv lldb-3.4 llvm/tools/lldb
  或者你可从SVN仓库获取最新版本:

$ cd llvm/tools
$ svn checkout http://llvm.org/svn/llvm-project/lldb/trunk lldb
  或者如你所愿从GIT镜像获取:

$ cd llvm/tools
$ git clone http://llvm.org/git/llvm-project/lldb.git
注解

在GNU/Linux系统上,LLDB还在试验之中。

  编译LLDB之前,必须先解决软件依赖:Swig,libedit(仅针对Linux),和Python。举例来说,在Ubuntu系统上,你可以用以下命令解决这些依赖:

$ sudo apt-get install swig libedit-dev python
  记住,像本章介绍的其它项目一样,你需要重新生成LLVM编译文件,以使LLDB能够编译。请仿照第1章(编译和安装LLVM)中描述的从源代码编译LLVM的步骤。

  当你新安装lldb之后,为了简单测试,以参数-v运行它,打印它的版本:

$ lldb -v
lldb version 3.4 ( revision )
LLDB调试练习
  为了见识怎么使用LLDB,我们将发起一个调试会话以分析Clang程序。Clang程序包含很多C++符号(symbol),我们可以探查它们。如果你用默认选项编译LLVM/Clang项目,得到的Clang程序就包含调试符号。所谓默认选项,就是当你运行配置脚本生成LLVM Makefile时省略–enable-optimized参数,或者当你运行CMake时设置-DCMAKE_BUILD_TYPE=”Debug”,这是默认的编译类型。

  如果你熟悉GDB,你可能对一个表感兴趣,它将常用的GDB命令映射到相应的LLDB命令,见http://lldb.llvm.org/lldb-gdb.html。

  像GDB那样,我们以待调试可执行程序的路径为命令行参数启动LLDB:

$ lldb where-your-llvm-is-installed/bin/clang
Current executable set to 'where-your-llvm-is-installed/bin/clang' (x86_64).
(lldb) break main
Breakpoint 1: where = clang`main + 48 at driver.cpp:293, address = 0x00000001000109e0
  我们的命令行参数是Clang程序的路径,这样开始调试它。我们以参数-v运行程序,这应该打印Clang的版本:

(lldb) run -v
  LLDB停在断点之后,我们可以用next命令随意地单步通过每一行C++代码。如同GDB,LLDB接受任意命令缩写,例如n代表next,只要没有歧义:

(lldb) n
  为了查看LLDB如何打印C++对象,单步通过直到到达声明argv或ArgAllocator对象后的代码行,并打印它:

(lldb) n
(lldb) p ArgAllocator
(llvm::SpecificBumpPtrAllocator<char>) $0 = {
Allocator = {
SlabSize = 4096
SizeThreshld = 4096
DefaultSlabAllocator = (Allocator = llvm::MallocAllocator @ 0x00007f85f1497f68)
Allocator = 0x0000007fffbff200
CurSlab = 0x0000000000000000
CurPtr = 0x0000000000000000
End = 0x0000000000000000
BytesAllocated = 0
}
}
  当你玩够了之后,用q命令退出调试器:

(lldb) q
Quitting LLDB will kill one or more processes. Do you really want to proceed: [Y/n] y

LLDB基础知识

LLDB(Low Level Debugger)项目以LLVM基础设施构造一个调试器。LLDB是一个有着REPL 的特性和C++, Python插件的开源高性能调试器,Mac OS X上Xcode的默认调试器,支持在桌面和iOS设备和模拟器上调试。

LLDB绑定在Xcode 内部,存在于主窗口底部的控制台中,可以在需要 时暂停程序,查看变量的值,执行特定的指令,并按指定的步骤来操作程序的进展。

获取来源

通过以下github链接,可以得到LLDB源:

https://github.com/llvm/llvm-project/tree/main/lldb

或者

https://github.com/llvm-mirror/lldb

2.1.2 LLDB控制台

Xcode中内嵌了LLDB控制台,在Xcode中代码的下方,可以看到LLDB控制台。

LLDB控制台平时会输出一些log信息。如果想输入命令调试,必须让程序进入暂停状态。让程序进入暂停状态的方式主要有2种:

  1. 断点或者watchpoint: 在代码中设置一个断点(watchpoint),当程序运行到断点位置的时候,会进入stop状态。
  2. 直接暂停,控制台上方有一个暂停按钮,上图红框已标出,点击即可暂停程序。

LLDB语法

在使用LLDB之前,来先看看LLDB的语法,了解语法可以帮助清晰的使用LLDB:

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

现在,对上面的命令解释一下:

  1. <command>(命令)和<subcommand>(子命令):LLDB调试命令的名称。命令和子命令按层级结构来排列:一个命令对象为跟随其的子命令对象创建一个上下文,子命令又为其子命令创建一个上下文,依此类推。
  2. <action>:执行命令的操作
  3. <options>:命令选项
  4. <arguement>:命令的参数
  5. []:表示命令是可选的,可以有也可以没有

举个例子,假设给main方法设置一个断点,使用下面的命令:

  1. breakpoint set -n main

这个命令对应到上面的语法就是:

  1. command: breakpoint 表示断点命令
  2. action: set 表示设置断点
  3. option: -n 表示根据方法name设置断点
  4. arguement: mian 表示方法名为mian

原始(raw)命令

LLDB支持不带命令选项(options)的原始(raw)命令,原始命令会将命令后面的所有东西当做参数(arguement)传递。不过很多原始命令也可以带命令选项,当使用命令选项的时候,需要在命令选项后面加--区分命令选项和参数。

例如,常用的expression就是raw命令,一般情况下使用expression打印一个东西是这样的:

  1. (lldb) expression count
  2. (int) $2 = 4

当想打印一个对象的时候。需要使用-O命令选项,应该用--将命令选项和参数区分:

  1. (lldb) expression -O -- self
  2. <ViewController: 0x7f9000f17660>

唯一匹配原则

LLDB的命令遵循唯一匹配原则:假如根据前n个字母已经能唯一匹配到某个命令,则只写前n个字母等效于写下完整的命令。
e.g: 前面提到设置断点的命令,可以使用唯一匹配原则简写,下面2条命令等效:

  1. breakpoint set -n main
  2. br s -n main

~/.lldbinit

LLDB有了一个启动时加载的文件~/.lldbinit,每次启动都会加载。所以一些初始化的事儿,可以写入~/.lldbinit中,比如给命令定义别名等。但是由于这时候程序还没有真正运行,也有部分操作无法在里面玩,比如设置断点。

LLDB命令

Expression

expression命令的作用是执行一个表达式,并将表达式返回的结果输出。expression的完整语法是这样的:

  1. expression <cmd-options> -- <expr>
  2. <cmd-options>:命令选项,一般情况下使用默认的即可,不需要特别标明。
  3. --: 命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,--可以省略。
  4. <expr>: 要执行的表达式。

说expression是LLDB里面最重要的命令都不为过。因为能实现2个功能。

在代码运行过程中,可以通过执行某个表达式来动态改变程序运行的轨迹。
假如在运行过程中,突然想把self.view颜色改成红色,看看效果。不必写下代码,重新run,只需暂停程序,用expression改变颜色,再刷新一下界面,就能看到效果。

  1. // 改变颜色
  2.   (lldb) expression -- self.view.backgroundColor = [UIColor redColor]
  3.   // 刷新界面
  4.   (lldb) expression -- (void)[CATransaction flush]

也就是说,可以通过expression来打印东西。
假如想打印self.view:

  1.    (lldb) expression -- self.view
  2.     (UIView *) $1 = 0x00007fe322c18a10

p & print & call

一般情况下,直接用expression还是用得比较少的,更多时候用的是p、print、call。这三个命令其实都是expression --的别名(--表示不再接受命令选项,详情见前面原始(raw)命令这一节)。

  1. print: 打印某个东西,可以是变量和表达式。
  2. p: 可以看做是print的简写。
  3. call: 调用某个方法。

表面上看起来,可能有不一样的地方,实际都是执行某个表达式(变量也当做表达式),将执行的结果输出到控制台上。所以可以用p调用某个方法,也可以用call打印东西。
    例如,下面代码效果相同:

  1. (lldb) expression -- self.view
  2. (UIView *) $5 = 0x00007fb2a40344a0
  3. (lldb) p self.view
  4. (UIView *) $6 = 0x00007fb2a40344a0
  5. (lldb) print self.view
  6. (UIView *) $7 = 0x00007fb2a40344a0
  7. (lldb) call self.view
  8. (UIView *) $8 = 0x00007fb2a40344a0
  9. (lldb) e self.view
  10. (UIView *) $9 = 0x00007fb2a40344a0
  11. 根据唯一匹配原则,如果没有自己添加特殊的命令别名。e也可以表示expression的意思。原始命令默认没有命令选项,所以e也能带给同样的效果

po

众所周知,OC里所有的对象都是用指针表示的,所以一般打印的时候,打印出来的是对象的指针,而不是对象本身。如果想打印对象。需要使用命令选项:-O。为了更方便的使用,LLDB为expression -O --定义了一个别名:po。

  1. (lldb) expression -- self.view
  2. (UIView *) $13 = 0x00007fb2a40344a0
  3. (lldb) expression -O -- self.view
  4. <UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>
  5. (lldb) po self.view
  6. <UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>

thread

thread backtrace & bt

有时候想要了解线程堆栈信息,可以使用thread backtrace。
thread backtrace作用是将线程的堆栈打印出来。现在来看看语法。

  1. thread backtrace [-c <count>] [-s <frame-index>] [-e <boolean>]

thread backtrace后面跟的都是命令选项:

-c:设置打印堆栈的帧数(frame)。
-s:设置从哪个帧(frame)开始打印。
-e:是否显示额外的回溯。

实际上,一般不需要使用这些命令选项。
例如,当发生crash的时候,可以使用thread backtrace查看堆栈调用。

  1. (lldb) thread backtrace
  2. * thread #1: tid = 0xdd42, 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11, queue = ‘com.apple.main-thread‘, stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
  3.     frame #0: 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11
  4.   * frame #1: 0x000000010aa9f75e TLLDB`-[ViewController viewDidLoad](self=0x00007fa270e1f440, _cmd="viewDidLoad") + 174 at ViewController.m:23
  5.     frame #2: 0x000000010ba67f98 UIKit`-[UIViewController loadViewIfRequired] + 1198
  6.     frame #3: 0x000000010ba682e7 UIKit`-[UIViewController view] + 27
  7.     frame #4: 0x000000010b93eab0 UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 61
  8.     frame #5: 0x000000010b93f199 UIKit`-[UIWindow _setHidden:forced:] + 282
  9.     frame #6: 0x000000010b950c2e UIKit`-[UIWindow makeKeyAndVisible] + 42

可以看到crash发生在-[ViewController viewDidLoad]中的第23行,只需检查这行代码是不是干了什么非法的事儿就可以了。

LLDB还为backtrace专门定义了一个别名:bt,其效果与thread backtrace相同,如果不想写那么长一串字母,直接写下bt即可。

  1. (lldb) bt
  2. thread return
  3. Debug的时候,也许会因为各种原因,不想让代码执行某个方法,或者要直接返回一个想要的值。这时候就该thread return上场了。

thread return可以接受一个表达式,调用命令之后直接从当前的frame返回表达式的值。

例如,有一个someMethod方法,默认情况下是返回YES。想要让其返回NO。

只需在方法的开始位置加一个断点,当程序中断的时候,输入命令即可:

  1. (lldb) thread return NO

效果相当于在断点位置直接调用return NO;,不会执行断点后面的代码。

c & n & s & finish

一般在调试程序的时候,经常用到下面这4个按钮:

 

 喜欢用触摸板的人,可能会觉得点击这4个按钮比较费劲。其实LLDB命令也可以完成上面的操作,而且如果不输入命令,直接按Enter键,LLDB会自动执行上次的命令。按一下Enter就能达到想要的效果,顿时感觉很惬意!

现在来看看对应这4个按钮的LLDB命令:

  1. c/ continue/ thread continue: 这三个命令效果都等同于上图中第一个按钮的。表示程序继续运行。
  2. n/ next/ thread step-over: 这三个命令效果等同于上图第二个按钮。表示单步运行。
  3. s/ step/ thread step-in: 这三个命令效果等同于上图第三个按钮。表示进入某个方法。
  4. finish/ step-out: 这两个命令效果等同于第四个按钮。表示直接走完当前方法,返回到上层frame。

thread其他不常用的命令

thread 相关的还有其他一些不常用的命令,这里就简单介绍一下即可,如果需要了解更多,可以使用命令help thread查阅。

  1. thread jump: 直接让程序跳到某一行。由于ARC下编译器实际插入了不少retain,release命令。跳过一些代码不执行很可能会造成对象内存混乱发生crash。
  2. thread list: 列出所有的线程。
  3. thread select: 选择某个线程。
  4. thread until: 传入一个line的参数,让程序执行到这行的时候暂停。
  5. thread info: 输出当前线程的信息。

frame

前面提到过很多次frame(帧)。可能有的朋友对frame这个概念还不太了解。随便打个断点。

 在控制台上输入命令bt,可以打印出来所有的frame。如果仔细观察,这些frame和左边红框里的堆栈是一致的。平时看到的左边的堆栈就是frame。

frame variable

平时Debug的时候,经常做的事就是查看变量的值,通过frame variable命令,可以打印出当前frame的所有变量。

  1. (lldb) frame variable
  2. (ViewController *) self = 0x00007fa158526e60
  3. (SEL) _cmd = "text:"
  4. (BOOL) ret = YES
  5. (int) a = 3

可以看到,将self,_cmd,ret,a等本地变量都打印了出来。

如果要需要打印指定变量,也可以给frame variable传入参数:

  1. (lldb) frame variable self->_string
  2. (NSString *) self->_string = nil

不过frame variable只接受变量作为参数,不接受表达式,也就是说,无法使用frame variable self.string,因为self.string是调用string的getter方法。所以一般打印指定变量,更喜欢用p或者po。

其他不常用命令

一般frame variable打印所有变量用得比较多,frame还有2个不怎么常用的命令:

frame info: 查看当前frame的信息。

  1. (lldb) frame info
  2. frame #0: 0x0000000101bf87d5 TLLDB`-[ViewController text:](self=0x00007fa158526e60, _cmd="text:", ret=YES) + 37 at

frame select: 选择某个frame。

  1. (lldb) frame select 1
  2. frame #1: 0x0000000101bf872e TLLDB`-[ViewController viewDidLoad](self=0x00007fa158526e60, _cmd="viewDidLoad") + 78 at ViewController.m:23
  3.    20
  4.    21      - (void)viewDidLoad {
  5.    22          [super viewDidLoad];
  6. -> 23          [self text:YES];
  7.    24          NSLog(@"1");
  8.    25          NSLog(@"2");
  9.    26          NSLog(@"3");

当选择frame 1的时候,会把frame1的信息和代码打印出来。不过一般都是直接在Xcode左边点击某个frame,这样更方便。

breakpoint

调试过程中,用得最多的可能就是断点了。LLDB中的断点命令也非常强大。

breakpoint set

breakpoint set命令用于设置断点,LLDB提供了很多种设置断点的方式:

使用-n根据方法名设置断点:

例如,想给所有类中的viewWillAppear:设置一个断点:

  1.    (lldb) breakpoint set -n viewWillAppear:
  2.     Breakpoint 13: 33 locations.

使用-f指定文件

例如,只需要给ViewController.m文件中的viewDidLoad设置断点:

  1.  (lldb) breakpoint set -f ViewController.m -n viewDidLoad
  2.     Breakpoint 22: where = TLLDB`-[ViewController viewDidLoad] + 20 at ViewController.m:22, address = 0x000000010272a6f4

这里需要注意,如果方法未写在文件中(比如写在category文件中,或者父类文件中),指定文件之后,将无法给这个方法设置断点。

使用-l指定文件某一行设置断点

例如,想给ViewController.m第38行设置断点。

  1. (lldb) breakpoint set -f ViewController.m -l 38
  2. Breakpoint 23: where = TLLDB`-[ViewController text:] + 37 at ViewController.m:38, address = 0x000000010272a7d5

使用-c设置条件断点

例如,text:方法接受一个ret的参数,想让ret == YES的时候程序中断:

  1. (lldb) breakpoint set -n text: -c ret == YES
  2. Breakpoint 7: where = TLLDB`-[ViewController text:] + 30 at ViewController.m:37, address = 0x0000000105ef37ce
  3. 使用-o设置单次断点
  4.  
  5. e.g: 如果刚刚那个断点只想让其中断一次:
  6. (lldb) breakpoint set -n text: -o
  7. ‘breakpoint 3‘: where = TLLDB`-[ViewController text:] + 30 at ViewController.m:37, address = 0x000000010b6f97ce

breakpoint command

有的时候,可能需要给断点添加一些命令,比如每次走到这个断点的时候,都需要打印self对象。只需要给断点添加一个po self命令,就不用每次执行断点再自己输入po self了。

breakpoint command add

breakpoint command add命令就是给断点添加命令的命令。

例如,假设需要在ViewController的viewDidLoad中查看self.view的值
首先给-[ViewController viewDidLoad]添加一个断点。

  1. (lldb) breakpoint set -n "-[ViewController viewDidLoad]"
  2. ‘breakpoint 3‘: where = TLLDB`-[ViewController viewDidLoad] + 20 at ViewController.m:23, address = 0x00000001055e6004

可以看到添加成功之后,这个breakpoint的id为3,然后给其增加一个命令:po self.view

  1. (lldb) breakpoint command add -o "po self.view" 3

-o完整写法是--one-liner,表示增加一条命令。3表示对id为3的breakpoint增加命令。

添加完命令之后,每次程序执行到这个断点就可以自动打印出self.view的值了

如果一下子想增加多条命令,比如,想在viewDidLoad中打印当前frame的所有变量,但是不想让其中断,也就是在打印完成之后,需要继续执行。可以这样做:

  1. (lldb) breakpoint command add 3
  2. Enter your debugger command(s).  Type ‘DONE‘ to end.
  3. > frame variable
  4. > continue
  5. > DONE

输入breakpoint command add 3对断点3增加命令。会要求输入增加哪些命令,输入‘DONE‘表示结束。这时候就可以输入多条命令了。

breakpoint command list

如果想查看某个断点已有的命令,可以使用breakpoint command list。
例如,查看一下刚刚的断点3已有的命令。

 

参考文献链接

https://www.cnblogs.com/jingjin/p/6239484.html

https://www.cnblogs.com/jingjin/p/6239484.html