LLDB详解

常用调试

你常用的调试是不是这样?

NSLog(@"%@", whatIsInsideThisThing);

或者写一个临时变量?

NSNumber *n = @6; 

或者专门写个检查器来判断?

if (1 || theBooleanAtStake) { ... }

或者专门写一个方法?

int calculateTheTrickyValue {
  return 9;
  /*
   先这样
   ...
}

是不是每次都要重新运行程序,重新开始?

代码已经够多,你还要多写几句代码,然后重新编译运行看看结果对不对。如果你用的是低配电脑,或者项目比较大,运行等待的过程那真是难受啊!

其实你不需要重新编译运行,你完全可以使用调试器(也叫控制台),即使你已经会使用打个断点,或打个全局断点,但实际上调试器还可以做更多事情。

接下来我们来重新认识Xcode中的调试器吧!看看到底能做多少事情。

LLDB

LLDB是一个带着REPL(交互式解释器)、C++特性,且带有Python插件的开源调试器。LLDB 绑定在 Xcode 内部,也就是我们常见到的控制台中(这里有一个关于调试器如何工作的总体的解释。)

咱们以前使用调试器的时候,只是简单的在Xcode中加一些断点,然后通过
(lldb) po value
来打印值对不对?但提到上面的那些缺点,这已经不能满足我们调试的要求了。
接下来我们做更酷的事!

基础

先写一个简单的代码。这里会打印字符串。然后我们在打印字符串的那行代码加个断点。相信这点大家都会,如果你的确是新手,那你点下图中22那个数字,就会看到和图中一样的断点标示。

断点

我们运行一下,接下来编译器运行到第22行的时候就停住了!
这时我们就可以使用调试器了 。先看看怎么用。
我们输入: " help "

help

(lldb) help
Debugger commands:
  apropos           -- List debugger commands related to a word or subject.
  breakpoint        -- Commands for operating on breakpoints (see 'help b' for
                       shorthand.)
  bugreport         -- Commands for creating domain-specific bug reports.
  command           -- Commands for managing custom LLDB commands.
  disassemble       -- Disassemble specified instructions in the current
                       target.  Defaults to the current function for the
                       current thread and stack frame.
  expression        -- Evaluate an expression on the current thread.  Displays
                       any returned value with LLDB's default formatting.
  frame             -- Commands for selecting and examing the current thread's
                       stack frames.
  gdb-remote        -- Connect to a process via remote GDB server.  If no host
                       is specifed, localhost is assumed.
  gui               -- Switch into the curses based GUI mode.
  help              -- Show a list of all debugger commands, or give details
                       about a specific command.
....太长就不复制了

然后调试器打印了一些文字,这都是调试器的命令和说明,先简单的看一看。

print

我们平时使用最多的就是p了,其实它是print,输入看一下:

打印结果

实际上LLDB里面搞了很多别名,比如prinprip等,都是print的别名,使用的效果和print是一样的。但是你不能用pr,因为LLDB不能区分你输入的pr是不是process

图中打印是不是还有个$0?,这个东西是用它来指向这个结果的,比如print $0 + 7也就是计算count+7的值,输入后的结果是106。任何以美元符号开头的东西都存在与LLDB的命名控件里面,它是为了帮助咱们调试而存在的。

expression

如果要改变一个值,你可用expression这个命令

改变一个值

你看,这个家伙它不仅改变了调试器中的值,它甚至还改变了程序中的值!牛逼吧。
它也有给别名e

那么现在为了简单一点,从现在开始,我们用 pe 来代替 printexpression

什么是 print 命令

print就是打印的意思,比如想打印一个变量 print sum
这里有一个很有意思的事情,你输入p count = 18expression count = 18,结果是一样样的!

输入 help print,然后向下滚动,你会发现:

'print' is an abbreviation for 'expression --'.   
(print是 `expression --` 的缩写) `--` 标示不需要参数

打印对象

尝试输入

p objects

控制台输出了

(NSString *) $7 = 0x0000000104da4040 @"red balloons"

如果打印复杂一点的的对象,你会发觉只打印了对象的指针地址

(lldb) p @[ @"foo", @"bar" ]

(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 elements" 

其实我只是想看看这个对象的 description 方法的结果。
那我就需要用-O(是字母O)这个参数了,我们输入 e -O --$8,就打印了里面的值。

(lldb) e -O -- $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)

其实 e -o -- 也有个别名,那就是 po (print object 的缩写),咱们用它更简单。看~

(lldb) po $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
(lldb) po @"lunar"
lunar
(lldb) p @"lunar"
(NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"

还可以打印这些

我们可以设置打印格式,打印格式这么写:print/p/
例子:
默认的格式

(lldb) p 16
16

十六进制:

(lldb) p/x 16
0x10

二进制 (t 代表 two):

(lldb) p/t 16
0b00000000000000000000000000010000
(lldb) p/t (char)16
0b00010000

更多格式点这里。

变量

前面讲完打印对象、又讲完修改变量值,现在我们在讲点东西。
我们可以用LLDB定义变量,是不是很酷!
咱们只需要记住,申明变量必须要以美元符开头。看例子

(lldb) e int $a = 2
(lldb) p $a * 19
(int) $0 = 38
(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday" ]
(lldb) p [$array count]
(NSUInteger) $1 = 3
(lldb) po [[$array objectAtIndex:0] uppercaseString]
SATURDAY
(lldb) p [[$array objectAtIndex:$a] characterAtIndex:0]
error: no known method '-characterAtIndex:'; cast the message send to the method's return type
error: 1 errors parsing expression

最后一个怎么回事!打印不出来?
这其实是因为没有确定返回的类型值。
解决办法很简单,前面加个类型,就像oc里面的类型强转一样。

(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0]
(char) $3 = 'M'
(lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0]
(char) $4 = 77

搞定了。

流程控制

当你通过 Xcode 的源码编辑器的侧边槽 (或者通过下面的方法) 插入一个断点,程序到达断点时会就会停止运行。

调试条上会出现四个你可以用来控制程序的执行流程的按钮。

从左到右,四个按钮分别是:continue,step over,step into,step out。

第一个,continue 按钮,会取消程序的暂停,允许程序正常执行 (要么一直执行下去,要么到达下一个断点)。在 LLDB 中,你可以使用 process continue 命令来达到同样的效果,它的别名为 continue,或者也可以缩写为 c

第二个,step over 按钮,会以黑盒的方式执行一行代码。如果所在这行代码是一个函数调用,那么就不会跳进这个函数,而是会执行这个函数,然后继续。LLDB 则可以使用 thread step-overnext,或者 n 命令。

如果你确实想跳进一个函数调用来调试或者检查程序的执行情况,那就用第三个按钮,step in,或者在LLDB中使用 thread step instep,或者 s 命令。注意,当前行不是函数调用时,nextstep 效果是一样的。

大多数人知道 cns,但是其实还有第四个按钮,step out。如果你曾经不小心跳进一个函数,但实际上你想跳过它,常见的反应是重复的运行 n 直到函数返回。其实这种情况,step out 按钮是你的救世主。它会继续执行到下一个返回语句 (直到一个堆栈帧结束) 然后再次停止。

例子

考虑下面一段程序:

代码.jpg

假如我们运行程序,让它停止在断点,然后执行下面一些列命令:

p i
n
s
p i
finish
p i
frame info

这里,frame info 会告诉你当前的行数和源码文件,以及其他一些信息;查看 help framehelp threadhelp process 来获得更多信息。这一串命令的结果会是什么?看答案之前请先想一想。

(lldb) p i
(int) $0 = 99
(lldb) n
2021-10-26 23:10:00.101318+0800 LLDBTest[19576:205894] 101 is odd!
(lldb) s
(lldb) p i 
(int) $1 = 110
(lldb) finish
2021-10-26 23:10:17.660446+0800 LLDBTest[19576:205894] 110 is even!
(lldb) p i
(int) $2 = 99
(lldb) frame info
frame #0: 0x000000010e20de3c LLDBTest`-[ViewController viewDidLoad](self=0x00007fb7762059e0, _cmd="viewDidLoad") at ViewController.m:38:5
(lldb) 

它始终在 38 行的原因是 finish 命令一直运行到 isEven() 函数的 return,然后立刻停止。注意即使它还在 38 行,其实这行已经被执行过了。

Thread Return

调试时,还有一个很棒的函数可以用来控制程序流程:thread return 。它有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。这意味这函数剩余的部分不会被执行。这会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方式 。

让我们稍微修改一下上面代码段并运行:

p i
s
thread return NO
n
p even0
frame info

看答案前思考一下。下面是答案:

(lldb) p i
(int) $0 = 99
(lldb) s
(lldb) thread return NO
(lldb) p even0
(BOOL) $2 = NO
(lldb) frame info
frame #0: 0x000000010a475df9 LLDBTest`-[ViewController viewDidLoad](self=0x00007fdf0f00d4e0, _cmd="viewDidLoad") at ViewController.m:43:5
(lldb) 

断点

我们都把断点作为一个停止程序运行,检查当前状态,追踪 bug 的方式。但是如果我们改变和断点交互的方式,很多事情都变成可能。

断点允许控制程序什么时候停止,然后允许命令的运行。

想象把断点放在函数的开头,然后用 thread return 命令重写函数的行为,然后继续。想象一下让这个过程自动化,听起来不错,不是吗?

管理断点

Xcode 提供了一系列工具来创建和管理断点。我们会一个个看过来并介绍 LLDB 中等价的命令 (是的,你可以在调试器内部添加断点)。

在 Xcode 的左侧面板,有一组按钮。其中一个看起来像断点。点击它打开断点导航,这是一个可以快速管理所有断点的面板。

在这里你可以看到所有的断点 - 在 LLDB 中通过 breakpoint list (或者 br li) 命令也做同样的事儿。你也可以点击单个断点来开启或关闭 - 在 LLDB 中使用 breakpoint enable breakpoint disable

(lldb) br li
Current breakpoints:
1: file = '/Users/issuser/Desktop/LLDBTest/LLDBTest/ViewController.m', line = 42, exact_match = 0, locations = 1, resolved = 1, hit count = 1

  1.1: where = LLDBTest`-[ViewController viewDidLoad] + 170 at ViewController.m:44:1, address = 0x0000000101f5fdfa, resolved, hit count = 1 

2: file = '/Users/issuser/Desktop/LLDBTest/LLDBTest/ViewController.m', line = 37, exact_match = 0, locations = 1, resolved = 1, hit count = 1

  2.1: where = LLDBTest`-[ViewController viewDidLoad] + 170 at ViewController.m:44:1, address = 0x0000000101f5fdfa, resolved, hit count = 1 
(lldb) br dis 1
1 breakpoints disabled.
(lldb) br li
1: file = '/Users/issuser/Desktop/LLDBTest/LLDBTest/ViewController.m', line = 42, exact_match = 0, locations = 1 Options: disabled 

  1.1: where = LLDBTest`-[ViewController viewDidLoad] + 170 at ViewController.m:44:1, address = 0x0000000101f5fdfa, unresolved, hit count = 1 

2: file = '/Users/issuser/Desktop/LLDBTest/LLDBTest/ViewController.m', line = 37, exact_match = 0, locations = 1, resolved = 1, hit count = 1

  2.1: where = LLDBTest`-[ViewController viewDidLoad] + 170 at ViewController.m:44:1, address = 0x0000000101f5fdfa, resolved, hit count = 1 

(lldb) br del 1
1 breakpoints deleted; 0 breakpoint locations disabled.
(lldb) br li
No breakpoints currently set.

创建断点

在上面的例子中,我们通过在源码页面器的滚槽 16 上点击来创建断点。你可以通过把断点拖拽出滚槽,然后释放鼠标来删除断点 (消失时会有一个非常可爱的噗的一下的动画)。你也可以在断点导航页选择断点,然后按下删除键删除。

要在调试器中创建断点,可以使用 breakpoint set 命令。

(lldb) breakpoint set -f ViewController.m -l 25
Breakpoint 5: where = LLDBTest`-[ViewController viewDidLoad] + 66 at ViewController.m:25:15, address = 0x0000000101f5fd92

也可以使用缩写形式 br。虽然 b 是一个完全不同的命令 (_regexp-break 的缩写),但恰好也可以实现和上面同样的效果。

(lldb) b ViewController.m:28
Breakpoint 6: where = LLDBTest`-[ViewController viewDidLoad] + 98 at ViewController.m:29:19, address = 0x0000000101f5fdb2

这些断点会准确的停止在函数的开始。Objective-C 的方法也完全可以:

(lldb) breakpoint set -F "-[NSArray objectAtIndex:]"
Breakpoint 5: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
(lldb) b -[NSArray objectAtIndex:]
Breakpoint 6: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
(lldb) breakpoint set -F "+[NSSet setWithObject:]"
Breakpoint 7: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820
(lldb) b +[NSSet setWithObject:]
Breakpoint 8: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820

如果想在 Xcode 的UI上创建符号断点,你可以点击断点栏左侧的 + 按钮,选择第三个选项

添加.png

这时会出现一个弹出框,你可以在里面添加例如 testAddAction 这样的符号断点。这样每次调用这个函数的时候,程序都会停止,不管是你调用还是苹果调用。

如果你 Xcode 的 UI 上右击任意断点,然后选择 "Edit Breakpoint" 的话,会有一些非常诱人的选择。

这里,断点已经被修改为只有i99 的时候才会停止。你也可以使用 "ignore" 选项来告诉断点最初的 n次调用 (并且条件为真的时候) 的时候不要停止。

接下来介绍 'Add Action' 按钮...

断点行为 (Action)

上面的例子中,你或许想知道每一次到达断点的时候 i 的值。我们可以使用 p i 作为断点行为。这样每次到达断点的时候,都会自动运行这个命令。可以看到它打印 i,接着打印了自定义的表达式。下面是在 LLDB 而不是 Xcode 的 UI 中做这些的时候,看起来的样子。

(lldb) breakpoint set -F isEven
Breakpoint 4: where = LLDBTest`-[ViewController isEven] + 26 at ViewController.m:68:5, address = 0x000000010f53ae5a
(lldb) breakpoint modify -c 'i == 99' 1
(lldb) breakpoint command add 1
Enter your debugger command(s).  Type 'DONE' to end.
> p i
> DONE
(lldb) br li 1
1: file = '/Users/issuser/Desktop/LLDBTest/LLDBTest/ViewController.m', line = 23, exact_match = 0, locations = 1, resolved = 1, hit count = 1
    Breakpoint commands:
      p i

Condition: i == 99

  1.1: where = LLDBTest`-[ViewController viewDidLoad] + 58 at ViewController.m:23:15, address = 0x000000010f53ac7a, resolved, hit count = 1 

接下来说说自动化。

赋值后继续运行

看编辑断点弹出窗口的底部,你还会看到一个选项: "Automatically continue after evaluation actions." 。它仅仅是一个选择框,但是却很强大。选中它,调试器会运行你所有的命令,然后继续运行。看起来就像没有执行任何断点一样 (除非断点太多,运行需要一段时间,拖慢了你的程序)。

这个选项框的效果和让最后断点的最后一个行为是 continue 一样。选框只是让这个操作变得更简单。调试器的输出是:

(lldb) breakpoint set -F isEven
Breakpoint 3: where = LLDBTest`-[ViewController isEven] + 26 at ViewController.m:68:5, address = 0x0000000104e13e5a
(lldb) breakpoint command add 1
Enter your debugger command(s).  Type 'DONE' to end.
> continue
> DONE
(lldb) br li 1
1: file = '/Users/issuser/Desktop/LLDBTest/LLDBTest/ViewController.m', line = 23, exact_match = 0, locations = 1, resolved = 1, hit count = 1
    Breakpoint commands:
      continue

  1.1: where = LLDBTest`-[ViewController viewDidLoad] + 58 at ViewController.m:23:15, address = 0x0000000104e13c7a, resolved, hit count = 1 

执行断点后自动继续运行,允许你完全通过断点来修改程序!你可以在某一行停止,运行一个 expression 命令来改变变量,然后继续运行。

例子

想想所谓的"打印调试"技术吧,不要这么做:

NSLog(@"%@", whatIsInsideThisThing);

而是用个打印变量的断点替换 log 语句,然后继续运行。

也不要:

int calculateTheTrickyValue {
  return 9;

  /*
   Figure this out later.
   ...
}

而是加一个使用 thread return 9 命令的断点,然后让它继续运行。

符号断点加上 action 真的很强大。你也可以在你朋友的 Xcode 工程上添加一些断点,并且加上大声朗读某些东西的 action。看看他们要花多久才能弄明白发生了什么。

完全在调试器内运行

在开始舞蹈之前,还有一件事要看一看。实际上你可以在调试器中执行任何 C/Objective-C/C++/Swift 的命令。唯一的缺点就是不能创建新函数... 这意味着不能创建新的类,block,函数,有虚拟函数的 C++ 类等等。除此之外,它都可以做。

我们可以申请分配一些字节:

(lldb) e char *$str = (char *)malloc(8)
(lldb) e (void)strcpy($str, "munkeys")
(lldb) e $str[1] = 'o'
(char) $0 = 'o'
(lldb) p $str
(char *) $str = 0x00007fd04a900040 "monkeys"

我们可以查看内存 (使用 x 命令),来看看新数组中的四个字节:

(lldb) x/1c $str
0x7fd04a900040: monk

我们也可以去掉 3 个字节 (x 命令需要斜引号,因为它只有一个内存地址的参数,而不是表达式;使用 help x 来获得更多信息):

(lldb) x/1w `$str + 3`
0x7fd04a900043: keys

做完了之后,一定不要忘了释放内存,这样才不会内存泄露。(哈,虽然这是调试器用到的内存):

(lldb) e (void)free($str)

不用断点调试

程序运行时,Xcode 的调试条上会出现暂停按钮,而不是继续按钮:

点击按钮会暂停 app (这会运行 process interrupt 命令,因为 LLDB 总是在背后运行)。这会让你可以访问调试器,但看起来可以做的事情不多,因为在当前作用域没有变量,也没有特定的代码让你看。

这就是有意思的地方。如果你正在运行 iOS app,你可以试试这个: (因为全局变量是可访问的)

(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
; layer = >
   | >
   |    | >
   |    |    | >
   |    |    |    | >

你可以看到整个层次。Chisel(Chisel是一个LLDB命令集合,用于帮助调试iOS应用程序。)中 pviews 就是这么实现的。

更新UI

有了上面的输出,我们可以获取这个 view:

(lldb) e id $myView = (id)0x7f82b1d01fd0

然后在调试器中改变它的背景色:

(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]

但是只有程序继续运行之后才会看到界面的变化。因为改变的内容必须被发送到渲染服务中,然后显示才会被更新。

渲染服务实际上是一个另外的进程 (被称作 backboardd)。这就是说即使我们正在调试的内容所在的进程被打断了,backboardd 也还是继续运行着的。

这意味着你可以运行下面的命令,而不用继续运行程序:

(lldb) e (void)[CATransaction flush]

即使你仍然在调试器中,UI 也会在模拟器或者真机上实时更新。Chisel 为此提供了一个别名叫做 caflush,这个命令被用来实现其他的快捷命令,例如 hide show 以及其他很多命令。所有 Chisel 的命令都有文档,所以安装后随意运行 help show 来看更多信息。

Push 一个 View Controller

想象一个以 UINavigationController 为 root ViewController 的应用。你可以通过下面的命令,轻松地获取它:

(lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController]

然后 push 一个 child view controller:

(lldb) e id $vc = [UIViewController new]
(lldb) e (void)[[$vc view] setBackgroundColor:[UIColor yellowColor]]
(lldb) e (void)[$vc setTitle:@"Yay!"]
(lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]

最后运行下面的命令:

(lldb) caflush // e (void)[CATransaction flush]

navigation Controller 就会立刻就被 push 到你眼前。

查找按钮的 target

想象你在调试器中有一个 $myButton 的变量,可以是创建出来的,也可以是从 UI 上抓取出来的,或者是你停止在断点时的一个局部变量。你想知道,按钮按下的时候谁会接收到按钮发出的 action。非常简单:

(lldb) po [$myButton allTargets]
{(
    
)}
(lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
<__NSArrayM 0x7fb58bd2aa40>(
_handleTap:
)

LLDB 和 Python

LLDB 有内建的,完整的 Python 支持。在LLDB中输入 script,会打开一个 Python REPL。你也可以输入一行 python 语句作为 script 命令 的参数,这可以运行 python 语句而不进入REPL:

(lldb) script import os
(lldb) script os.system("open http://www.objc.io/")

这样就允许你创造各种酷的命令。把下面的语句放到文件 ~/myCommands.py 中:

def caflushCommand(debugger, command, result, internal_dict):
  debugger.HandleCommand("e (void)[CATransaction flush]")

然后再 LLDB 中运行:

command script import ~/myCommands.py

或者把这行命令放在 /.lldbinit 里,这样每次进入 LLDB 时都会自动运行。Chisel 其实就是一个 Python 脚本的集合,这些脚本拼接 (命令) 字符串 ,然后让 LLDB 执行。很简单,不是吗?

紧握调试器这一武器

LLDB 可以做的事情很多。大多数人习惯于使用 pponsc,但实际上除此之外,LLDB 可以做的还有很多。掌握所有的命令 (实际上并不是很多),会让你在揭示代码运行时的运行状态,寻找 bug,强制执行特定的运行路径时获得更大的能力。你甚至可以构建简单的交互原型 - 比如要是现在以 modal 方式弹出一个 View Controller 会怎么样?使用调试器,一试便知。

这篇文章是为了想你展示 LLDB 的强大之处,并且鼓励你多去探索在控制台输入命令。熟悉LLDB,能给你增加更多加分技能。以下是LLDB更多相关内容:

    1. 高级lldb操作与lldbinit文件
    1. 代码模拟lldb执行流程
    1. lldb接口学习
    1. 自定义lldb命令

这些都可以在了解LLDB后去做研究。打开 LLDB,输入 help,看一看列举的命令。你尝试过多少?用了多少?但愿 NSLog 看起来不再那么吸引你去用,每次编辑再运行并不有趣而且耗时。

你可能感兴趣的:(LLDB详解)