【问题标题】:How does a process keep track of its local variables进程如何跟踪其局部变量
【发布时间】:2018-07-25 10:43:45
【问题描述】:

据我所知,当一个进程分配局部变量时,它通过将它们作为堆栈推入内存来实现,但仍然通过使用堆栈指针的偏移量来引用它们(来自这个线程What is the idea behind using a stack for local variables?)。

但是,它如何知道哪些变量具有哪些偏移量?我是否以正确的方式考虑这个问题?

【问题讨论】:

  • This 可能会有所帮助。
  • 不是进程本身,编译器决定哪些变量要存入栈,哪些不存入栈。编译器甚至可能根本不使用堆栈。
  • 标记为assembly,因为ISO C不需要实现使用堆栈来实现自动存储;使用本地调用堆栈是“普通”寄存器机 CPU 架构上所有主流 C 实现通用的实现细节,但不是 C 语言本身的一部分。 (使用单独的数据堆栈也很容易(C setjmp/longjmp 语义类似于堆栈),但会占用另一个寄存器,以便在缓冲区溢出时无法覆盖返回地址)

标签: c assembly memory-management cpu-architecture


【解决方案1】:

局部变量的偏移量作为常量“烘焙”到机器代码中。到编译器完成时,您的程序称为局部变量的东西将替换为编译器分配的固定内存偏移量。

假设你声明了三个局部变量:

char a[8];
int b;
short c;

编译器为这些变量分配偏移量:a 位于偏移量0b 位于偏移量8c 位于偏移量12。假设您的代码执行b += c。编译器将其翻译成如下代码块:

LOAD    @(SP+8)
ADD     @(SP+12)
STORE   @(SP+8)

这里唯一改变的值是SP(堆栈指针)。所有偏移量都是数字常量。

【讨论】:

  • 因此,在使用局部变量的函数(或一般的局部范围)期间,SP 将是相同的,而对于处理器而言,a 本质上只是@987654334 @, b@(SP+8)c@(SP+12)?
  • 没错。当然,确切的数值常数取决于变量的大小和对齐要求。该示例适用于 int 的大小为 4 字节的平台。
  • @ChristianBouwense 没错,SP 只会在您调用另一个函数时发生变化,此时当前函数处于“暂停”状态。一旦另一个函数返回,SP 将恢复为调用前的值,因此当前函数可以再次访问其当前偏移量处的局部变量。
  • @ChristianBouwense 编译器允许通过让变量在不同时间共享相同的偏移量来使事情变得更复杂一些。假设您声明了两个变量,ij。然后在第一次使用j之前使用i,然后停止。之后你开始使用j,并且永远不要再使用i。当ij 是两个背靠背循环的索引时,这很常见。在这种情况下,允许编译器分配 ij 相同的偏移量。
  • @ChristianBouwense:SP 可以更改(例如,在堆栈参数调用约定中的函数调用之前推送参数时),但编译器控制此类更改,并且始终可以计算正确的偏移量在机器代码中。您可以在 x86 上看到这一点,其中许多 32 位调用约定不会在寄存器中传递任何参数,因此所有对非 void 函数的调用都会推送一些参数(或提前保留空间并简单地存储到正上方的空间堆栈指针)。对于 32 位 x86,请参阅gcc -O3 的两种方式:godbolt.org/g/tbxzNY
【解决方案2】:

前言:以下文字以 x86 架构为例。其他架构的处理方式确实不同。

[...] 它通过将它们作为堆栈推入内存来实现,[...]

差不多了。 它通过将它们推入内存堆栈[当前进程]来实现。每个进程都有自己的堆栈。因此,每次上下文切换时,这个堆栈帧都会改变——它的局部变量(在堆栈上)也会改变。

通常(!)本地定义的变量是相对于 堆栈帧 被引用的,并保存在 EBP 寄存器中。这与相对于 Data Segment Base 引用的 全局定义 变量相反。所以每个进程都有自己的堆栈和自己的局部变量

较新的编译器可以保留EBP 寄存器并引用与ESP 寄存器相关的变量。这有两个后果:

  • 还有一个寄存器可供使用
  • 调试的可能性较小(调试通常使用EBP 值作为当前堆栈帧 的参考来识别局部变量)。因此,如果没有单独的调试信息文件,这会使调试变得更加困难。

所以回答你的主要问题

进程如何跟踪其局部变量

进程跟踪它们的堆栈框架(其中包含局部变量),但不跟踪它们的局部变量本身。 堆栈框架随着每个进程切换而改变。 局部变量仅相对于保存在寄存器EBP中的堆栈帧指针(或相对于堆栈指针ESP ,这取决于编译器设置)。

【讨论】:

  • 问题没有提到x86。您可能应该在特定于 x86 的内容之前的某处说“例如在 x86 上”。许多 RISC 机器没有任何使用基指针的约定。例如MIPS 没有push 指令,只有加载/存储,相对于堆栈指针相对于任何其他寄存器引用本地变量没有任何缺点。
【解决方案3】:

编译器负责记忆偏移量。这些偏移量只是硬编码的。喜欢将变量加载到寄存器(例如到​​ eax)编译器会产生类似mov eax, [esp-4] 的东西,其中 esp 是堆栈指针寄存器,4 是偏移量。如果新变量将被推送下一个 mov 到 get/set 变量将有更大的偏移量。所有这些都是编译时间分析。

此外,某些平台上的堆栈可能会反转 - 因此偏移量将是正数。

【讨论】:

  • 几乎所有平台都有向下增长的堆栈,包括 x86,因此您的示例应该是 mov eax, [esp+4]。只有 System V x86-64 ABI 有一个红色区域,您可以安全地将局部变量 below rsp 放入叶函数中,即使在安装了信号处理程序的一般情况下也是如此。 (在这种情况下,您将使用[rsp-4]。尽管 gcc 在为 x32 ABI(长模式下的 32 位指针)编译时确实错过了优化,即使在访问堆栈时它也会使用地址大小前缀,所以你实际上可以得到一些 gcc 版本来使用正确的选项发出mov eax, [esp-4]
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-09-26
  • 2011-12-22
  • 1970-01-01
  • 1970-01-01
  • 2019-08-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多