为什么我们仍然向后增加堆栈?


46

编译C代码并查看汇编时,所有的堆栈都像这样向后生长:

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
     popq    %rbp
    ret

-4(%rbp)-这是否意味着基本指针或堆栈指针实际上是向下移动内存地址而不是向上移动?这是为什么?

我换$5, -4(%rbp)$5, +4(%rbp),编译和运行代码并没有任何错误。那么,为什么我们还必须在内存堆栈上倒退呢?


2
请注意,-4(%rbp)它根本不会移动基本指针,并且+4(%rbp)可能无法工作。
玛格丽特·布鲁姆

14
为什么我们还必须倒退 ”-您认为前进的好处是什么?最终,没关系,您只需要选择一个即可。
Bergi

31
“为什么我们要向后增加堆栈?” -因为如果我们没有其他人会问为什么要向后malloc增加堆
slebetman

2
@MargaretBloom:显然,在OP的平台上,CRT启动代码不在乎是否main破坏了RBP。当然有可能。(是的,写入4(%rbp)将基于保存的RBP值)。实际上,这个主模块永远不会出错mov %rsp, %rbp,因此,如果OP实际测试了该内存访问,则该访问相对于调用者的RBP!如果实际上是从编译器输出中复制的,则省略了一些指令!
彼得·科德斯

1
在我看来,“向后”或“向前”(或“向下”和“向上”)取决于您的观点。如果将内存绘制为顶部地址低的一列,那么通过递减堆栈指针来增加堆栈将类似于物理堆栈。
jamesdlin

Answers:


86

这是否意味着基本指针或堆栈指针实际上是在向下移动内存地址而不是向上移动?这是为什么?

是的,这些push指令会递减堆栈指针并写入堆栈,pop反之则从堆栈中读取并递增堆栈指针。

这在某种程度上是历史性的,因为对于内存有限的机器,堆栈放置在较高的位置并向下生长,而堆放置在较低的位置并向上生长。堆与栈之间只有一个“空闲内存”缺口,并且这个缺口是共享的,任何一个缺口都可以根据需要单独增长。因此,仅当堆栈与堆冲突时程序才会用完内存,而不会留下可用内存。 

如果堆栈和堆都沿相同的方向增长,则存在两个间隙,并且堆栈不能真正进入堆的间隙(反之亦然,这也是有问题的)。

最初,处理器没有专用的堆栈处理指令。但是,随着堆栈支持被添加到硬件中,它采用了这种向下增长的模式,而今天的处理器仍然遵循这种模式。

有人可能会争辩说,在64位计算机上,有足够的地址空间来允许多个间隙-并且作为一个证据,当一个进程具有多个线程时,多个间隙必然是这种情况。尽管这不足以改变周围环境,但由于存在多个缺口系统,因此增长方向可以任意决定,因此传统/兼容性会扩大规模。


你不得不改变,以改变堆栈的方向,否则放弃使用专用的推动和弹出指令(例如CPU的协议栈处理的指令pushpopcallret,其他)。

请注意,MIPS指令集体系结构没有专用的pushpop,因此在任一方向上增加堆栈都是可行的-您可能仍希望单线程进程使用一个间隙的内存布局,但可以向上和向上增大堆栈向下。但是,如果执行此操作,则可能需要对某些C varargs代码进行源代码或底层参数传递中的调整。

(实际上,由于MIPS上没有专门的堆栈处理程序,我们可以使用pre或post增量或pre或post递减值推入堆栈,只要我们使用完全相反的方式弹出堆栈,并且还假设操作系统尊重所选的堆栈使用模型。实际上,在某些嵌入式系统和某些教育系统中,MIPS堆栈是向上增长的。)


32
这不只是pushpop大多数的架构,也是更为重要的中断处理,callret,和其他任何已经出炉,在与堆栈的互动。
重复数据删除器

3
ARM可以具有所有四种堆栈风格。
玛格丽特·布鲁姆

14
就其价值而言,我不认为“增长方向是任意的”,因为这两种选择都同样好。向下增长的特性是,缓冲区末尾溢出会阻塞早期的堆栈帧,包括保存的返回地址。长大后的属性是,缓冲区的末尾溢出只会阻塞相同或更高版本(如果缓冲区不是最新的,则可能会有更新的)存储在调用帧中,甚至可能只有未使用的空间(均假设有保护)堆叠后的页面)。因此,从安全角度看,长大了似乎非常可取的
R.,

6
@R ..:长大并不能消除缓冲区溢出漏洞,因为易受攻击的函数通常不是叶函数:它们调用其他函数,将返回地址放在缓冲区上方。从调用者那里获得指针的叶子函数可能很容易覆盖自己的返回地址。例如,如果一个函数在堆栈上分配了一个缓冲区并将其传递给gets(),或者执行了strcpy()没有内联的,则这些库函数中的返回将使用覆盖的返回地址。当前具有向下增长的堆栈,这是其调用者返回的时间。
Peter Cordes

5
@PeterCordes:确实,我的评论指出,与溢出缓冲区相比,相同级别或更新的堆栈帧仍然具有潜在的可破坏性,但这要少得多。如果Clobbering函数是由缓冲区所在的函数直接调用的叶子函数(例如strcpy),则在返回地址保存在寄存器中(除非需要溢出)的拱门上,无法访问Clobber的返回地址。
R ..

8

在您的特定系统中,堆栈从高内存地址开始,然后向下“增长”到低内存地址。(也存在从低到高的对称情况)

而且,由于您从-4和+4更改了并且可以运行,这并不意味着它是正确的。正在运行的程序的内存布局更为复杂,并且取决于许多其他因素,这些因素可能导致您没有立即在这个极其简单的程序上崩溃。


1

堆栈指针指向已分配和未分配堆栈存储器之间的边界。向下增长意味着它指向已分配堆栈空间中第一个结构的开始,其他已分配项紧随其后。使指针指向已分配结构的开始比其他方法更常见。

现在,如今在许多系统上,都有用于堆栈框架的单独寄存器,可以在某种程度上可靠地将其展开,以便找出调用链,并散布局部变量。在某些体系结构上设置此堆栈帧寄存器的方式意味着它最终指向本地变量存储后面,而不是指向其之前的堆栈指针。因此,使用该堆栈帧寄存器需要负索引。

请注意,堆栈框架及其索引是已编译计算机语言的一个方面,因此必须由编译器的代码生成器来处理“不自然”问题,而不是拙劣的汇编语言程序员。

因此,尽管有很好的历史理由选择堆栈向下扩展(如果您使用汇编语言编程并且不打扰设置适当的堆栈框架,则会保留其中的一些堆栈),但是它们变得不那么明显了。


2
“如今,在许多系统上,现在已经有单独的堆栈帧寄存器”,您已经落后了。如今,更丰富的调试信息格式已大大消除了对帧指针的需求。
彼得·格林
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.