一个进程如何跟踪它的本地变量。

5
据我所知,当一个进程分配本地变量时,它会将这些变量作为堆栈推送到内存中,但通过使用与堆栈指针的偏移来引用它们来访问它们(来自此线程 使用堆栈存储本地变量背后的思想是什么?)。
然而,它如何知道哪些变量具有什么偏移量呢?我这样想对吗?

这个可能会有帮助:https://cs.stackexchange.com/questions/76871/how-are-variables-stored-in-and-retrieved-from-the-program-stack - Eli Sadoff
这并不是进程本身的问题,编译器决定哪些变量要存储在堆栈中,哪些不需要。编译器甚至可能根本不使用堆栈。 - Pablo
由于ISO C不要求实现使用堆栈来实现自动存储,因此标记为[tag:assembly]。在“正常”寄存器机CPU体系结构上,使用调用堆栈来处理本地变量是所有主流C实现的实现细节,但并不是C语言本身的一部分。(使用单独的数据堆栈也很容易(C setjmp / longjmp语义类似于堆栈),但会占用另一个寄存器,以使缓冲区溢出时不可能覆盖返回地址) - Peter Cordes
3个回答

6

本地变量的偏移量被“嵌入”机器代码中作为常量。编译器完成后,程序中所引用的本地变量将被替换为编译器分配的固定内存偏移量。

例如,您声明了三个本地变量:

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本质上只是“SP”,b是“@(SP+8)”,c是“@(SP+12)”。 - Christian Bouwense
1
这是正确的。当然,确切的数值常量取决于变量的大小和对齐要求。本示例是针对一个 int 大小为 4 字节的平台。 - kfx
2
@ChristianBouwense 没错,只有当您调用另一个函数时,SP 才会发生改变,此时当前函数处于“暂停”状态。一旦其他函数返回,SP 就会恢复到调用之前的值,因此当前函数可以再次以其当前偏移量访问其本地变量。 - Sergey Kalinichenko
编译器允许通过在不同的时间让变量共享相同的偏移量来使事情变得更加复杂。假设您声明了两个变量ij。然后在第一次使用j之前使用i,并停止。之后开始使用j,再也不使用i。当ij是两个相邻循环的索引时,这种情况很常见。在这种情况下,编译器允许为ij分配相同的偏移量。 - Sergey Kalinichenko
@ChristianBouwense:SP 可以改变(例如,在堆栈参数调用约定中在函数调用之前推送参数),但编译器控制这些更改,并始终可以计算正确的偏移量放入机器代码中。您可以在 x86 上看到这一点,其中许多 32 位调用约定不在寄存器中传递任何参数,因此对非 void 函数的所有调用都会推送一些参数(或提前保留空间并简单地存储到紧挨着堆栈指针上方的空间)。请参见 gcc -O3 在 32 位 x86 上的两种方式:https://godbolt.org/g/tbxzNY。 - Peter Cordes

2
前言:以下文本以x86架构为例。其他架构处理方式不同。
它通过将它们作为堆栈推入内存来实现。它是在当前进程的堆栈上进行操作的。每个进程都有自己的堆栈。因此,每次上下文切换时,这个堆栈框架会改变,它的本地变量(在堆栈上)也会发生变化。
通常情况下定义的本地变量是相对于在EBP寄存器中保存和存在的堆栈框架引用的。这与全局定义的变量相反,后者是相对于数据段基址引用的。因此,每个进程都有自己的堆栈及其本地变量。
新的编译器可以节省EBP寄存器并使用ESP寄存器引用变量。这有两个后果:
- 一个寄存器可供使用。 - 减少一种调试可能性(调试常常使用EBP值作为对当前堆栈框架的参考来标识本地变量)。所以这使得调试变得更加困难,需要单独的调试信息文件。
因此,回答您的主要问题
一个进程如何跟踪它的本地变量?
进程跟踪他们的堆栈框架(其中包含本地变量),但不是本地变量本身。堆栈框架会随着每个进程切换而改变。本地变量仅相对于保留在寄存器EBP中的堆栈帧指针(或相对于取决于编译器设置的堆栈指针ESP)引用。

这个问题没有提到x86。在涉及到x86特定的内容之前,你应该在某个地方说“例如在x86上”。许多RISC机器没有使用基指针的惯例。例如MIPS没有push指令,只有load/store,并且相对于堆栈指针引用本地变量与引用任何其他寄存器没有区别。 - Peter Cordes

1
编译器负责记忆偏移量。这些偏移量是硬编码的。例如,将变量加载到寄存器(例如eax)中,编译器会产生类似于mov eax, [esp-4]的代码,其中esp是堆栈指针寄存器,4是偏移量。如果推入新变量,下一个mov的偏移量将会更大。所有这些都是编译时分析。另外,在某些平台上,堆栈可能会反转,因此偏移量将为正数。

几乎所有平台都有向下增长的堆栈,包括x86,因此您的示例应该是mov eax,[esp + 4]。只有System V x86-64 ABI具有红区,在其中您可以在叶函数中安全地将本地变量放置在rsp以下,即使在安装了信号处理程序的一般情况下也是如此。 (在这种情况下,您将使用[rsp-4]。尽管gcc在编译x32 ABI(长模式下的32位指针)时会错过优化,其中它甚至会对访问堆栈使用地址大小前缀,因此您实际上可以获得一些gcc版本以正确的选项发出mov eax,[esp-4] - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接