如何在同一个 CPU 上使用调试器 运行 读取 CPU 寄存器?

How is it possible to read the CPU registers using a debugger running on the same CPU?

在学习汇编时,我按以下方式使用 GDB:

gdb ./a.out (a is a compiled C script that only prints hello world)
break main
run
info registers

为什么我自己使用相同的CPU打印寄存器,却能看到程序使用的寄存器? GDB(或操作系统)的使用不应该覆盖寄存器并且只显示被覆盖的寄存器吗? 我能想到的唯一答案就是我的CPU是双核,其中一个内核正在使用,另一个保留给程序。

操作系统维护每个执行线程的寄存器状态。当您检查 gdb 中的寄存器时,调试器实际上要求 OS 从保存的状态中读取寄存器值。你的程序在那个时间点不是 运行,而是调试器。

假设您的系统上没有其他进程。以下是所发生情况的简化视图:

  1. 调试器启动并获取 cpu
  2. 调试器要求 OS 加载您的程序
  3. 调试器要求 OS 放置断点
  4. 调试器要求 OS 开始执行您的程序。 OS 保存 gdb 寄存器状态并将控制转移到您的程序。
  5. 您的程序遇到了断点。 OS 取得控制权,保存程序的寄存器状态,重新加载 gdb 寄存器并将 cpu 返回给 gdb。
  6. 调试器要求 OS 从保存的状态中读取程序的寄存器。

请注意,此机制是多任务操作系统的正常职责的一部分,并非特定于调试。当 OS 调度程序决定应该执行一个不同的程序时,它会保存当前状态并加载另一个。这称为上下文切换,每秒可能发生多次,给人一种程序同时执行的错觉,即使您只有一个 cpu 核心。

回到过去的单任务处理时代 OSses,唯一可能妨碍程序执行的事情就是中断。现在,中断处理程序遇到了您正在谈论的相同问题,您的程序正在计算某些东西,用户按下一个键 - 中断 - 中断服务例程 做一些工作但是 不得修改进程中的单个寄存器。这就是主要原因,首先发明了堆栈。通常的 80x86 DOS 中断服务例程如下所示:

push ax
push cx
push dx
push bx
push si
push di
push bp
// no need to push sp
[do actual work, caller registers avaiable on stack if needed]
pop bp
pop di
pop si
pop bx
pop dx
pop cx
pop ax
iret

这甚至是如此普遍,以至于创建了新的指令对 pushapopa(对于 push/pop 所有人)来简化这项任务。

在今天的 CPUs 中,操作系统和应用程序之间的地址 space 隔离,CPUs 提供一些任务状态系统并允许操作系统切换任务(中断可能仍会像上面概述的那样工作,但也可以通过任务切换来处理)。所有现代 OSses 都使用这种任务状态系统,其中 CPU 在进程未被主动执行时保存进程的所有寄存器。就像 Jester 已经解释过的那样,gdb 只是向 OS 询问要调试的进程的这个值,然后打印它们。