简体   繁体   中英

How does gdb start an assembly compiled program and step one line at a time?

Valgrind says the following on their documentation page

Your program is then run on a synthetic CPU provided by the Valgrind core

However GDB doesn't seem to do that. It seems to launch a separate process which executes independently. There's also no c library from what I can tell. Here's what I did

  • Compile using clang or gcc gcc -g tiny.s -nostdlib ( -g seems to be required)
  • gdb ./a.out
  • Write starti
  • Press s a bunch of times

You'll see it'll print out "Test1\\n" without printing test2. You can also kill the process without terminating gdb. GDB will say "Program received signal SIGTERM, Terminated." and won't ever write Test2

How does gdb start the process and have it execute only one line at a time?

    .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 implementation

As usual for a process that wants to start another process, it does a fork/exec, like a shell does. But in the new process, GDB doesn't just make an execve system call right away.

Instead, it calls ptrace(PTRACE_TRACEME) to wait for the parent process to attach to it, so GDB (the parent) is already attached before the child process makes an execve() system call to make this process start executing the specified executable file.

Also note in the execve(2) man page :

If the current program is being ptraced, a SIGTRAP signal is sent to it after a successful execve().

So that's how the kernel debugging API supports stopping before the first user-space instruction is executed in a newly-execed process. ie exactly what starti wants. This doesn't depend on setting a breakpoint; that can't happen until after execve anyway, and with ASLR the correct address isn't even known until after execve picks a base address. (GDB by default disables ASLR, but it still works if you tell it not to disable ASLR.)


If you strace -f -o gdb.trace gdb ./foo or something, you'll see some of what GDB does. (Nested tracing apparently doesn't work, so running GDB under strace means GDB's ptrace system call fails, but we can see what it does leading up to that.)

...
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

There some earlier clone system calls to create more threads in the main GDB process, but those didn't exit until after the vforked PID that attempted ptrace(PTRACE_TRACEME) . They were all just threads since they used clone with CLONE_VM . There was one earlier vfork / execve of /usr/bin/iconv .

Annoyingly, modern Linux has moved to PIDs wider than 16-bit so the numbers get inconveniently large for human minds.


step implementation:

Unlike stepi which would use PTRACE_SINGLESTEP on ISAs that support it (eg x86 where the kernel can use the TF trap flag, but interestingly not ARM), step is based on source-level line number <-> address debug info . That's usually pointless for asm, unless you want to step past macro expansions or something.

But for step , GDB will use ptrace(PTRACE_POKETEXT) to write an int3 debug-break opcode over the first byte of an instruction, then ptrace(PTRACE_CONT) to let execution run in the child process until it hits a breakpoint or other signal. (Then put back the original opcode byte when this instruction needs to execute). The place at which it puts that breakpoint is something it finds by looking for the next address of a line-number in the DWARF or STABS debug info (metadata) in the executable. That's why only stepi (aka si ) works when you don't have debug info.

Or possibly it would use PTRACE_SINGLESTEP one or two times as an optimization if it saw it was close.

(I normally only use si or ni for debugging asm, not s or n . layout reg is also nice, when GDB doesn't crash. See the bottom of the x86 tag wiki for more GDB asm debugging tips.)


If you meant to ask how the x86 ISA supports debugging, rather than the Linux kernel API which exposes those features via a target-independent API, see the related Q&As:

Also How does a debugger work? has some Windowsy answers.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM