gdb如何启动汇编编译程序并一次一行?
How does gdb start an assembly compiled program and step one line at a time?
Valgrind 在他们的文档页面上说了以下内容
Your program is then run on a synthetic CPU provided by the Valgrind core
但是 GDB 似乎没有这样做。它似乎启动了一个独立执行的单独进程。据我所知,也没有 c 库。这是我所做的
- 使用clang或gcc编译
gcc -g tiny.s -nostdlib
(-g
似乎是必需的)
gdb ./a.out
- 写
starti
- 多次按
s
你会看到它会打印出“Test1\n”而不打印 test2。您也可以在不终止 gdb 的情况下终止进程。 GDB 会说“程序收到信号 SIGTERM,已终止”。永远不会写 Test2
gdb如何启动进程并让它一次只执行一行?
.text
.intel_syntax noprefix
.globl _start
.p2align 4, 0x90
.type _start,@function
_start:
lea rsi, [rip + .s1]
mov edi, 1
mov edx, 6
mov eax, 1
syscall
lea rsi, [rip + .s2]
mov edi, 1
mov edx, 6
mov eax, 1
syscall
mov eax, 60
xor edi, edi
syscall
.s1:
.ascii "Test1\n"
.s2:
.ascii "Test2\n"
starti
实施
对于想要启动另一个进程的进程,它通常会执行 fork/exec,就像 shell 一样。但是在新的进程中,GDB 不会直接进行execve系统调用。
相反,它调用 ptrace(PTRACE_TRACEME)
等待父进程附加到它,因此在子进程进行 execve()
系统调用之前 GDB(父进程)已经附加进程开始执行指定的可执行文件。
另请注意 execve(2)
man page:
If the current program is being ptraced, a SIGTRAP signal is sent
to it after a successful execve().
这就是内核调试 API 支持在新执行的进程中执行第一个 user-space 指令之前停止的方式。 即starti
想要什么。这不依赖于设置断点;无论如何,直到 execve 之后才会发生这种情况,并且使用 ASLR 甚至在 execve 选择基地址之后才知道正确的地址。 (GDB 默认禁用 ASLR,但如果您告诉它不要禁用 ASLR,它仍然有效。)
如果您在 run
之前手动设置断点,或者使用 start
在 main
上设置一次性断点,这也是 GDB 使用的方法。在 starti
命令存在之前,模拟该功能的一个技巧是在 run
之前设置一个无效的断点,因此 GDB 会在出现该错误时停止,让您在该点进行控制。
如果您 strace -f -o gdb.trace gdb ./foo
或其他,您将看到 GDB 的一些功能。 (嵌套跟踪显然不起作用,因此 运行在 strace 下使用 GDB 意味着 GDB 的 ptrace 系统调用失败,但我们可以看到它在导致该错误之前做了什么。)
...
231566 execve("/usr/bin/gdb", ["gdb", "./foo"], 0x7ffca2416e18 /* 57 vars */) = 0
# the initial GDB process is PID 231566.
... whole bunch of stuff
231566 write(1, "Starting program: /tmp/foo \n", 28) = 28
231566 personality(0xffffffff) = 0 (PER_LINUX)
231566 personality(PER_LINUX|ADDR_NO_RANDOMIZE) = 0 (PER_LINUX)
231566 personality(0xffffffff) = 0x40000 (PER_LINUX|ADDR_NO_RANDOMIZE)
231566 vfork( <unfinished ...>
# 231584 is the new PID created by vfork that would go on to execve the new PID
231584 openat(AT_FDCWD, "/proc/self/fd", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 13
231584 newfstatat(13, "", {st_mode=S_IFDIR|0500, st_size=0, ...}, AT_EMPTY_PATH) = 0
231584 getdents64(13, 0x558403e20360 /* 16 entries */, 32768) = 384
231584 close(3) = 0
... all these FDs
231584 close(12) = 0
231584 getdents64(13, 0x558403e20360 /* 0 entries */, 32768) = 0
231584 close(13) = 0
231584 getpid() = 231584
231584 getpid() = 231584
231584 setpgid(231584, 231584) = 0
231584 ptrace(PTRACE_TRACEME) = -1 EPERM (Operation not permitted)
231584 write(2, "warning: ", 9) = 9
231584 write(2, "Could not trace the inferior pro"..., 37) = 37
231584 write(2, "\n", 1) = 1
231584 write(2, "warning: ", 9) = 9
231584 write(2, "ptrace", 6) = 6
231584 write(2, ": ", 2) = 2
231584 write(2, "Operation not permitted", 23) = 23
231584 write(2, "\n", 1) = 1
# gotta love unbuffered stderr
231584 exit_group(127) = ?
231566 <... vfork resumed>) = 231584 # in the parent
231584 +++ exited with 127 +++
# then the parent is running again
231566 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=231584, si_uid=1000, si_status=127, si_utime=0, si_stime=0} ---
231566 rt_sigreturn({mask=[]}) = 231584
... then I typed "quit" and hit return
有一些早期的 clone
系统调用在主 GDB 进程中创建更多线程,但这些调用直到尝试 ptrace(PTRACE_TRACEME)
的 vforked PID 之后才退出。它们都只是线程,因为它们使用 clone
和 CLONE_VM
。 /usr/bin/iconv
vfork
/ execve
之前有一个。
烦人的是,现代 Linux 已经转向比 16 位更宽的 PID,因此数字对于人类思维来说变得太大了。
step
实施:
不像 stepi
会在支持它的 ISA 上使用 PTRACE_SINGLESTEP
(例如 x86,其中内核可以使用 TF 陷阱标志,但有趣的是不能使用 ARM),step
基于源代码行号<-> 地址调试信息。这对 asm 来说通常毫无意义,除非你想跳过宏扩展或其他东西。
但对于 step
,GDB 将使用 ptrace(PTRACE_POKETEXT)
在指令的第一个字节上写入 int3
调试中断操作码,然后 ptrace(PTRACE_CONT)
让执行运行 在子进程中,直到遇到断点或其他信号。 (然后在这条指令需要执行的时候放回原来的操作码字节)。它放置断点的位置是通过查找 DWARF 中行号的下一个地址或可执行文件中的 STAB 调试信息(元数据)找到的。 这就是为什么只有 stepi
(又名 si
)在没有调试信息时有效。
或者它可能会使用 PTRACE_SINGLESTEP
一两次作为优化,如果它看到它接近的话。
(我通常只使用 si
或 ni
来调试 asm,而不是 s
或 n
。layout reg
也很好,当 GDB 不不会崩溃。有关更多 GDB asm 调试提示,请参阅 x86 tag wiki 的底部。)
如果您想问 x86 ISA 如何支持调试,而不是 Linux 内核 API 通过目标无关 API 公开这些功能,请参阅相关问答:
- Why Single Stepping Instruction on X86?
- How to tell length of an x86-64 instruction opcode using CPU itself?
还有 How does a debugger work? 有一些 Windowsy 答案。
Valgrind 在他们的文档页面上说了以下内容
Your program is then run on a synthetic CPU provided by the Valgrind core
但是 GDB 似乎没有这样做。它似乎启动了一个独立执行的单独进程。据我所知,也没有 c 库。这是我所做的
- 使用clang或gcc编译
gcc -g tiny.s -nostdlib
(-g
似乎是必需的) gdb ./a.out
- 写
starti
- 多次按
s
你会看到它会打印出“Test1\n”而不打印 test2。您也可以在不终止 gdb 的情况下终止进程。 GDB 会说“程序收到信号 SIGTERM,已终止”。永远不会写 Test2
gdb如何启动进程并让它一次只执行一行?
.text
.intel_syntax noprefix
.globl _start
.p2align 4, 0x90
.type _start,@function
_start:
lea rsi, [rip + .s1]
mov edi, 1
mov edx, 6
mov eax, 1
syscall
lea rsi, [rip + .s2]
mov edi, 1
mov edx, 6
mov eax, 1
syscall
mov eax, 60
xor edi, edi
syscall
.s1:
.ascii "Test1\n"
.s2:
.ascii "Test2\n"
starti
实施
对于想要启动另一个进程的进程,它通常会执行 fork/exec,就像 shell 一样。但是在新的进程中,GDB 不会直接进行execve系统调用。
相反,它调用 ptrace(PTRACE_TRACEME)
等待父进程附加到它,因此在子进程进行 execve()
系统调用之前 GDB(父进程)已经附加进程开始执行指定的可执行文件。
另请注意 execve(2)
man page:
If the current program is being ptraced, a SIGTRAP signal is sent to it after a successful execve().
这就是内核调试 API 支持在新执行的进程中执行第一个 user-space 指令之前停止的方式。 即starti
想要什么。这不依赖于设置断点;无论如何,直到 execve 之后才会发生这种情况,并且使用 ASLR 甚至在 execve 选择基地址之后才知道正确的地址。 (GDB 默认禁用 ASLR,但如果您告诉它不要禁用 ASLR,它仍然有效。)
如果您在 run
之前手动设置断点,或者使用 start
在 main
上设置一次性断点,这也是 GDB 使用的方法。在 starti
命令存在之前,模拟该功能的一个技巧是在 run
之前设置一个无效的断点,因此 GDB 会在出现该错误时停止,让您在该点进行控制。
如果您 strace -f -o gdb.trace gdb ./foo
或其他,您将看到 GDB 的一些功能。 (嵌套跟踪显然不起作用,因此 运行在 strace 下使用 GDB 意味着 GDB 的 ptrace 系统调用失败,但我们可以看到它在导致该错误之前做了什么。)
...
231566 execve("/usr/bin/gdb", ["gdb", "./foo"], 0x7ffca2416e18 /* 57 vars */) = 0
# the initial GDB process is PID 231566.
... whole bunch of stuff
231566 write(1, "Starting program: /tmp/foo \n", 28) = 28
231566 personality(0xffffffff) = 0 (PER_LINUX)
231566 personality(PER_LINUX|ADDR_NO_RANDOMIZE) = 0 (PER_LINUX)
231566 personality(0xffffffff) = 0x40000 (PER_LINUX|ADDR_NO_RANDOMIZE)
231566 vfork( <unfinished ...>
# 231584 is the new PID created by vfork that would go on to execve the new PID
231584 openat(AT_FDCWD, "/proc/self/fd", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 13
231584 newfstatat(13, "", {st_mode=S_IFDIR|0500, st_size=0, ...}, AT_EMPTY_PATH) = 0
231584 getdents64(13, 0x558403e20360 /* 16 entries */, 32768) = 384
231584 close(3) = 0
... all these FDs
231584 close(12) = 0
231584 getdents64(13, 0x558403e20360 /* 0 entries */, 32768) = 0
231584 close(13) = 0
231584 getpid() = 231584
231584 getpid() = 231584
231584 setpgid(231584, 231584) = 0
231584 ptrace(PTRACE_TRACEME) = -1 EPERM (Operation not permitted)
231584 write(2, "warning: ", 9) = 9
231584 write(2, "Could not trace the inferior pro"..., 37) = 37
231584 write(2, "\n", 1) = 1
231584 write(2, "warning: ", 9) = 9
231584 write(2, "ptrace", 6) = 6
231584 write(2, ": ", 2) = 2
231584 write(2, "Operation not permitted", 23) = 23
231584 write(2, "\n", 1) = 1
# gotta love unbuffered stderr
231584 exit_group(127) = ?
231566 <... vfork resumed>) = 231584 # in the parent
231584 +++ exited with 127 +++
# then the parent is running again
231566 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=231584, si_uid=1000, si_status=127, si_utime=0, si_stime=0} ---
231566 rt_sigreturn({mask=[]}) = 231584
... then I typed "quit" and hit return
有一些早期的 clone
系统调用在主 GDB 进程中创建更多线程,但这些调用直到尝试 ptrace(PTRACE_TRACEME)
的 vforked PID 之后才退出。它们都只是线程,因为它们使用 clone
和 CLONE_VM
。 /usr/bin/iconv
vfork
/ execve
之前有一个。
烦人的是,现代 Linux 已经转向比 16 位更宽的 PID,因此数字对于人类思维来说变得太大了。
step
实施:
不像 stepi
会在支持它的 ISA 上使用 PTRACE_SINGLESTEP
(例如 x86,其中内核可以使用 TF 陷阱标志,但有趣的是不能使用 ARM),step
基于源代码行号<-> 地址调试信息。这对 asm 来说通常毫无意义,除非你想跳过宏扩展或其他东西。
但对于 step
,GDB 将使用 ptrace(PTRACE_POKETEXT)
在指令的第一个字节上写入 int3
调试中断操作码,然后 ptrace(PTRACE_CONT)
让执行运行 在子进程中,直到遇到断点或其他信号。 (然后在这条指令需要执行的时候放回原来的操作码字节)。它放置断点的位置是通过查找 DWARF 中行号的下一个地址或可执行文件中的 STAB 调试信息(元数据)找到的。 这就是为什么只有 stepi
(又名 si
)在没有调试信息时有效。
或者它可能会使用 PTRACE_SINGLESTEP
一两次作为优化,如果它看到它接近的话。
(我通常只使用 si
或 ni
来调试 asm,而不是 s
或 n
。layout reg
也很好,当 GDB 不不会崩溃。有关更多 GDB asm 调试提示,请参阅 x86 tag wiki 的底部。)
如果您想问 x86 ISA 如何支持调试,而不是 Linux 内核 API 通过目标无关 API 公开这些功能,请参阅相关问答:
- Why Single Stepping Instruction on X86?
- How to tell length of an x86-64 instruction opcode using CPU itself?
还有 How does a debugger work? 有一些 Windowsy 答案。