作者回复: 很好的发现!这里为了便于理解我没有画出实际在 x86-64 平台上的栈布局。但实际情况是 1、2 与 8、7 之间有 8 字节 padding,然后又由于 push 在这里是按照 8 字节存放数据,所以 8 和 7 每个数字实际“占用”了 8 字节。
作者回复: 是这样的,这里可能确实会有些让人误解。本质上都是传值,只不过一个是指针的值,一个是源值拷贝后的值。这里其实想表达的是说“将源值拷贝后的新值”,以及“源值对应的指针”这两种不同方式。
作者回复: 大部分理解都是对的哈。不过关于 rbp 里面保存的内容,只要知道它是上一个栈帧需要使用的、用来计算栈高度的信息就好。而 rsp 指向的是栈内存的顶部,也不用把它跟栈帧混在一起理解哈。因为其实所谓的“帧首”很难去界定,理论上按照小端序理解,可以认为“当前函数返回地址+8”(64位)的位置是上一个栈帧的开头。但一般很少这么讲。
作者回复: 回答正确!
作者回复: 好问题!这是由于在 x86-64 平台上,push 指令在通常情况下会以当前平台的寄存器宽度(这里为 8 字节)为单位来调整 rsp。
作者回复: 很好的问题,下面是回答哈: 针对第一个问题:需要注意的是按照 SysV 调用约定,整型实参会先使用 rdi、rsi 等寄存器按顺序存放。所以这里只要函数在被调用前,这些寄存器中有按顺序存放的值就可以,汇编层面实际先存储哪个这个取决于编译器的选择。而超过寄存器数量以外的参数需要按照从右到左的方向被放入栈中,所以会从最右侧的 8 开始 push。至于为何 x 的值会被先放到 eax,这里我没看到比较特殊的优化策略,应该是取决于编译器的具体实现。 针对第二个问题:清理实际上只会恢复栈的可见状态到函数被调用之前,其中最重要的就是 rsp 的值,但栈中存放的值实际上不会通过可见的指令进行诸如“清 0”等处理。比如下面这个例子: #include <stdio.h> void foo(void) { int x; printf("%d\n", x); x = 10; } int main(void) { foo(); foo(); return 0; } 至于你提到的“局部变量建议要置 0”可以举个例子说明一下不?
作者回复: 是的,push 指令会自动修改 rsp 中的值。对于它的执行,你可以理解为两步:第一步是减小 rsp 中的值,“腾出”足够的栈内存;第二步是把值放入这块内存中。
作者回复: __fastcall 是微软提出的一种特殊的调用约定;而 SysV 本质上是一套 ABI,它内部包含有对 SysV 调用约定的具体描述。除此之外,还有很多其他有关 ABI 的细节内容。
作者回复: 这里 rdi 保存的是 foo 函数第一个传入参数的值(也就是 1),这个是 SysV 调用约定中规定的。不过这里确实可以在第 33 行直接将 1 存放到 rdi 中,而不需要通过 rax 暂存,具体为什么这么做跟编译器的寄存器分配策略有关。
作者回复: 回答正确!