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上的汇编入门(十三)——从编译到执行