gdb 如何读取正在调试的程序/进程的寄存器值?寄存器如何与进程关联?

ane*_*kix 4 c++ debugging assembly operating-system cpu-registers

我用 C++ 写了一个简短的程序:

#include<iostream>
using namespace std;

    int main(){
    int x=10;
    int y=20;
    cout<< x+y <<endl;
    return 0;
    }
Run Code Online (Sandbox Code Playgroud)

只是出于好奇,我想了解幕后的程序,所以我正在使用 gdb 并遇到info registers命令。当我使用info registersin时gdb,我得到如下输出:

(gdb) info registers
rax            0x400756 4196182
rbx            0x0  0
rcx            0x6  6
rdx            0x7fffffffd418   140737488344088
rsi            0x7fffffffd408   140737488344072
rdi            0x1  1
rbp            0x7fffffffd320   0x7fffffffd320
rsp            0x7fffffffd320   0x7fffffffd320
r8             0x7ffff7ac1e80   140737348640384
r9             0x7ffff7dcfea0   140737351843488
r10            0x7fffffffd080   140737488343168
r11            0x7ffff773a410   140737344939024
r12            0x400660 4195936
r13            0x7fffffffd400   140737488344064
r14            0x0  0
r15            0x0  0
rip            0x40075a 0x40075a <main+4>
eflags         0x246    [ PF ZF IF ]
cs             0x33 51
ss             0x2b 43
ds             0x0  0
es             0x0  0
fs             0x0  0
gs             0x0  0
Run Code Online (Sandbox Code Playgroud)

我了解这些是寄存器及其值,但我想知道的是如何/为什么registersprocess. 随着操作系统调度不同的进程,寄存器的值应该不断变化?我参考了该命令info registers,这就是我发现的,但这仍然令人困惑。

信息寄存器 -> 打印除浮点和向量寄存器之外的所有寄存器的名称和值(在选定的堆栈帧中)。

MSa*_*ers 6

寄存器一直在变化。事实上,即使调试器也会更改寄存器值,因为它必须自行运行。

但是,当您使用调试器查看程序时,调试器会暂停正在运行的进程。作为挂起的一部分,CPU 状态被保存到 RAM 中。调试器了解这一点,并且可以只查看 RAM 中的挂起状态。假设寄存器 R1 被保存到0x1234挂起时的地址,那么调试器就可以打印存储在该地址的字节。


Pet*_*des 5

每个线程/进程都有自己的寄存器值。用户空间“体系结构状态”(寄存器值)在通过系统调用或中断进入内核时保存。(这在所有操作系统上都是如此)。

请参阅如果在 64 位代码中使用 32 位 int 0x80 Linux ABI 会发生什么?查看 Linux 的系统调用入口点,其中手写的 asm 实际上将寄存器保存在进程的内核堆栈上。(在 Linux 中,每个线程都有自己的内核堆栈)。

一般来说,在多任务操作系统中,每个进程/线程都有自己的内存空间来保存状态,因此上下文切换通过从被切换到的线程恢复保存的状态来工作。这有点简化,因为存在内核状态与保存的用户空间。状态1


因此,只要进程实际上并未在 CPU 内核上运行,其寄存器值就会保存在内存中。

操作系统提供了一个 API,用于读取/写入其他进程保存的寄存器状态和内存

在Linux中,这个API就是ptrace(2)系统调用;GDB 使用它来读取寄存器值和单步执行。因此,GDB通过内核间接从内存中读取目标进程保存的寄存器值。GDB自己的代码不使用任何特殊的x86指令,甚至不从任何特殊地址加载/存储;它只是进行系统调用,因为对另一个进程状态的访问必须通过内核。(好吧,我认为一个进程可以将另一个进程的内存映射到自己的地址空间,如果 Linux 甚至有一个系统调用的话,但我认为内存读/写实际上就像寄存器访问一样通过 ptrace 。)

(我认为)如果目标进程当前正在执行(而不是挂起),当另一个进程发出ptrace读取或写入其寄存器值之一的系统调用时,内核将必须中断它,以便将其当前状态保存到内存中。GDB 通常不会发生这种情况:它仅在挂起目标进程时尝试读取寄存器值。


ptrace也是strace用来跟踪系统调用的。请参阅Linux Journal 中的 Playing with ptrace,第 I 部分。strace ./my_program对于系统编程非常有用,特别是当从手写的 asm 进行系统调用时,解码您实际传递的参数和返回值。


脚注:

  1. 在 Linux 中,到新线程的实际切换发生在内核内部,从内核上下文到内核上下文。这“仅”保存内核堆栈上的整数寄存器,设置rsp到其他线程的内核堆栈中的正确位置,然后恢复保存的寄存器。因此,有一个函数调用,当它返回时,它会在新线程的内核模式下执行,并适当设置每个 CPU 的内核变量。新线程的用户空间状态最终会恢复,就像最初从用户空间进入内核的系统调用或中断在没有调用调度程序的情况下返回一样。即来自系统调用或中断内核入口点保存的状态。惰性/急切的 FPU 状态保存是另一个复杂的问题;内核通常会避免接触 FPU,这样就可以避免在刚进入内核并返回到同一用户空间进程时保存/恢复 FPU 状态。