【问题标题】:How does gdb read the register values of a program / process it's debugging? How are registers associated with a process?gdb 如何读取它正在调试的程序/进程的寄存器值?寄存器如何与进程关联?
【发布时间】:2018-07-24 22:36:14
【问题描述】:

我用c++写了一个小程序:

#include<iostream>
using namespace std;

    int main(){
    int x=10;
    int y=20;
    cout<< x+y <<endl;
    return 0;
    }

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

(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

我知道这些是寄存器及其值,但我想知道registers 如何/为什么与process 相关联。随着操作系统调度不同的进程,寄存器的值应该不断变化吗?我提到了命令info registers & 这是我发现的,但这仍然令人困惑。

info registers -> 打印所有寄存器的名称和值,除了 浮点和向量寄存器(在选定的堆栈帧中)。

【问题讨论】:

  • 这不是关于汇编和调试器的问题,而是更多关于操作系统和多任务如何工作的问题。简而言之,操作系统保留所有寄存器的副本。每个执行线程一个副本。
  • @anekix Google 搜索上下文切换.
  • @anekix 您需要阅读上下文切换的详细信息,而不仅仅是名称。保存寄存器值是上下文切换最基本的步骤之一。
  • @anekix 正确。
  • 调试器显示特定调试线程的寄存器状态,而不是调试器实现或其他任务的 CPU 寄存器的实际值,因此如果您已对调试代码设置断点,则寄存器状态不应更改全部(但它显示的是该断点线程的已保存寄存器状态,而不是物理 CPU 寄存器,调试器本身使用它们只是为了产生该输出,因此它们包含完全不相关的值)。

标签: c++ debugging assembly operating-system cpu-registers


【解决方案1】:

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

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

【讨论】:

  • 这是一个巨大的简化。操作系统在每个进程未运行时为其保存寄存器,并提供用于读取/写入其他进程保存的寄存器状态和内存的 API。在 Linux 中,这个 API 被称为ptrace;它是 GDB 用来读取寄存器值和单步执行的系统调用。但是是的,使用ptrace,GDB 通过内核从内存 读取目标进程的保存寄存器值。 ptrace 也是 strace 用来跟踪系统调用的。 Playing with ptrace, Part I.
  • 我敢肯定,简化它是经过深思熟虑的。 MSalters 说了你刚才所做的一切,除了列出与这个问题完全不相关或不必要的特定技术术语。
【解决方案2】:

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

请参阅What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code? 了解 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, Part Istrace ./my_program 对系统编程非常有用,尤其是在从手写 asm 进行系统调用以解码您实际传递的参数和返回值时。


脚注:

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

【讨论】:

  • 感谢您的详细解释:)
猜你喜欢
  • 2021-04-08
  • 2011-09-14
  • 2012-05-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-28
  • 2016-08-26
相关资源
最近更新 更多