【问题标题】:How does gdb start an assembly compiled program and step one line at a time?gdb 如何启动一个汇编编译程序并一次执行一行?
【发布时间】:2021-12-14 02:58:40
【问题描述】:

Valgrind 在他们的文档页面上说以下内容

然后您的程序将在 Valgrind 内核提供的合成 CPU 上运行

但是 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"

【问题讨论】:

    标签: linux assembly gdb x86-64 ptrace


    【解决方案1】:

    starti 实现

    对于一个想要启动另一个进程的进程,它像往常一样执行 fork/exec,就像 shell 一样。但在新进程中,GDB 只是立即进行 execve 系统调用。

    相反,它调用ptrace(PTRACE_TRACEME) 等待父进程附加到它,因此在子进程发出execve() 系统调用以使该进程开始执行指定的可执行文件之前,GDB(父)已经附加文件。

    还要注意execve(2) man page:

    如果正在跟踪当前程序,则发送 SIGTRAP 信号 在成功执行 execve() 后对其进行处理。

    这就是内核调试 API 支持在新执行的进程中执行第一条用户空间指令之前停止的方式。即正是 starti 想要的。这不取决于设置断点;无论如何,直到 execve 之后才会发生这种情况,并且使用 ASLR 甚至在 execve 选择基地址之后才知道正确的地址。 (GDB 默认禁用 ASLR,但如果你告诉它不要禁用 ASLR,它仍然有效。)

    如果您在run 之前手动设置断点,或者使用startmain 上设置一次性断点,这也是 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 之后才退出。它们都只是线程,因为它们使用了cloneCLONE_VM/usr/bin/iconv 中有一个较早的 vfork / execve

    令人讨厌的是,现代 Linux 已迁移到超过 16 位的 PID,因此数字对于人类的大脑来说变得非常大。


    step 实现:

    stepi 不同,PTRACE_SINGLESTEP 在支持它的 ISA 上使用(例如 x86,内核可以使用 TF 陷阱标志,但有趣的是 ARM 不能使用),step 基于源代码行number 地址调试信息。这对于 asm 来说通常是没有意义的,除非你想跳过宏扩展或其他东西。

    但是对于step,GDB 将使用ptrace(PTRACE_POKETEXT) 在指令的第一个字节上写入int3 debug-break 操作码,然后ptrace(PTRACE_CONT) 让执行在子进程中运行,直到遇到断点或其他信号。 (然后在需要执行该指令时放回原始操作码字节)。它放置断点的位置是它通过在可执行文件中的 DWARF 或 STABS 调试信息(元数据)中查找行号的下一个地址来找到的。 这就是为什么只有stepi(又名si)在您没有调试信息的情况下有效。

    或者它可能会使用PTRACE_SINGLESTEP 一到两次作为优化,如果它看到它很接近。

    (我通常只使用sini 来调试asm,而不是snlayout reg 也很好,当 GDB 不崩溃时。见底部x86 tag wiki了解更多 GDB asm 调试技巧。)


    如果您想询问 x86 ISA 如何支持调试,而不是 Linux 内核 API 通过与目标无关的 API 公开这些功能,请参阅相关问答:

    另外How does a debugger work?也有一些Windowsy的答案。

    【讨论】:

    • 哦该死的,我应该知道是你的质量:P 我会在几个小时内接受,这样其他人就可以有发言权(和机会)
    • @EricStotch:刚刚完成更新,因为我注意到您询问了stepstarti
    最近更新 更多