Evian Zhang's
naive blog

macOS上的汇编入门(十二)——调试

随着我们编写的汇编程序越来越复杂,往往就需要调试。对于汇编语言而言,常见的调试器有LLDB和GDB. 由于我比较喜欢用LLVM系列的产品,因此,在这篇文章中我主要介绍的是LLDB来调试汇编语言的方法。关于详细的LLDB的使用方法,大家可去官网 lldb.llvm.org 查看。

同时,在新版本的macOS上使用GDB是一件比较麻烦的事,需要证书签名。关于使用方法请参见我之前的文章 在macOS10.14上使用GDB教程

为了演示LLDB的调试,我们首先有一个汇编程序 test.s :

# test.s
	.text
	.globl	_main
_main:
	movq	$0x2000001, %rax
	movq	$0, %rdi
	syscall

为了以源码级别调试程序,我们需要在汇编时加入调试选项 -g . 也就是说,我们在终端下依次键入下面语句:

as test.s -g -o test.o
ld test.o -o test -lSystem

这样就可以将调试信息储存在 test.o 中以便我们接下来的调试。

载入程序

首先是将程序载入LLDB. 假设我们要调试的可执行程序是 test . 那么,我们在终端下键入

lldb test

即可进入LLDB调试界面,同时会出现

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

的提示语句。

我们输入 quit 即可退出LLDB的调试界面:

(lldb) quit

运行程序

接下来,我们可以输入 run 来执行这个程序:

(lldb) run
Process 1512 launched: '/Users/evian/Downloads/test' (x86_64)
Process 1512 exited with status = 0 (0x00000000)

程序顺利执行,没有发生错误。

但是,如果我们在某个地方写错了,比如说写成了 movq $0x2001, %rax , 那么汇编、链接时并不会发生错误,但在终端下运行时则会出现以下的错误报告:

./test
[1]    1556 segmentation fault  ./test

这让我们摸不着头脑,段错误是为什么会出现呢?这时,在LLDB中一个简单的 run 就可以让我们找到答案:

(lldb) run
Process 1573 launched: '/Users/evian/Downloads/test' (x86_64)
Process 1573 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_SYSCALL (code=8193, subcode=0x1)
    frame #0: 0x0000000100000fb8 test
    0x100000fb8: addl   %eax, (%rax)
    0x100000fba: addb   %al, (%rax)
    0x100000fbc: sbbb   $0x0, %al
    0x100000fbe: addb   %al, (%rax)
Target 0: (test) stopped.

注意看到其中的 stop reason = EXC_SYSCALL . 这就说明是我们在系统调用时出现了问题。

run 除了直接运行以外,还可以传命令行参数。比如说,我们在终端下想这样运行:

./test helloworld 114514

也就是将两个命令行参数传递给 test . 那么,我们在LLDB中也可以用 run 模拟这种传递过程:

(lldb) run helloworld 114514

即可。

设置断点

LLDB的功能远不止直接执行程序这么简单。接下来的工作,都需要我们首先设置断点。比如说,我想让程序在执行完 movq %0x2000001, %rax 后停下来,也就是不继续执行 movq %0, %rdi . 这时应当怎么办呢?

首先,我们找到 movq %0, %rdi 所在的行数,是第6行。因此,我们需要在第6行设置断点。在某行设置断点的意思就是在某行之前设置断点,当程序遇到断点时就会自动停下来,不继续执行。因此,我们在LLDB中输入并得到反馈:

(lldb) breakpoint set --file test.s --line 6
Breakpoint 1: where = test`main + 7, address = 0x0000000100000faf

程序就自动设置了一个断点。这句话的意思就是在名叫 test.s 的文件的第6行设置断点。

接下来,我们如果直接 run ,会出现:

(lldb) run
Process 1669 launched: '/Users/evian/Downloads/test' (x86_64)
Process 1669 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100000faf test`main at test.s:6
   3   		.globl	_main
   4   	_main:
   5   		movq	$0x2000001, %rax
-> 6   		movq	$0, %rdi
   7   		syscall
Target 0: (test) stopped.

也就直接在第6行停了下来。我们可以用 continue 让其继续执行:

(lldb) continue
Process 1669 resuming
Process 1669 exited with status = 0 (0x00000000)

或者利用 nexti 进行单步调试:

(lldb) nexti
Process 1680 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100000fb6 test`main at test.s:7
   4   	_main:
   5   		movq	$0x2000001, %rax
   6   		movq	$0, %rdi
-> 7   		syscall
Target 0: (test) stopped.

所谓单步调试,就是指在当前指令的下一个指令处再设置一个断点,然后继续执行。实际效果也就相当于又往后执行了一个指令,然后停止。

寄存器与内存

在进程停止在某个断点处时,我们还可以读取此时寄存器和内存的值。

利用 register read 可以阅读大部分常用寄存器的值:

(lldb) register read
General Purpose Registers:
       rax = 0x0000000002000001
       rbx = 0x0000000000000000
       rcx = 0x00007ffeefbff910
       rdx = 0x00007ffeefbff7e8
       rdi = 0x0000000000000001
       rsi = 0x00007ffeefbff7d8
       rbp = 0x00007ffeefbff7c8
       rsp = 0x00007ffeefbff7b8
        r8 = 0x0000000000000000
        r9 = 0x0000000000000000
       r10 = 0x0000000000000000
       r11 = 0x0000000000000000
       r12 = 0x0000000000000000
       r13 = 0x0000000000000000
       r14 = 0x0000000000000000
       r15 = 0x0000000000000000
       rip = 0x0000000100000faf  test`main + 7
    rflags = 0x0000000000000246
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

如果输入 register read —all , 则会输出所有寄存器的值。

此外,我们还可以单独查看某个寄存器,比如说

(lldb) register read rsp
     rsp = 0x00007ffeefbff7b8

就会返回rsp内存储的值。

同时,我们也可以查看内存中的值。我们刚刚查看到了此时栈顶指针位于 0x00007ffeefbff7b8 . 因此,我们利用

(lldb) memory read 0x00007ffeefbff7b8
0x7ffeefbff7b8: 35 fc 2e 6d ff 7f 00 00 35 fc 2e 6d ff 7f 00 00  5�.m�...5�.m�...
0x7ffeefbff7c8: 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00  ................

就获得了当前栈顶的内容。

可以在哪看到这系列文章

我在我的 GitHub 上, 知乎专栏 上和 CSDN 上同步更新。

上一篇文章: macOS上的汇编入门(十一)——系统调用

下一篇文章: macOS上的汇编入门(十三)——从编译到执行