问题描述
这个问题是关于x86汇编的,但是我用C语言提供了一个示例,因为我试图检查GCC在做什么。
当我遵循各种汇编指南时,我注意到人们,至少是我正在阅读其材料的少数人,似乎习惯于将堆栈变量分配给rsp而不是rbp。
然后我检查了GCC会做什么,而且看起来一样。
在下面的反汇编中,保留了前0x10个字节,然后通过eax将调用叶的结果传递给rbp-0xc,将常数2传递给rbp-0x8,为变量“ q”在rbp-0x8和rbp之间留出了空间“。
我可以想象在另一个方向上执行此操作,首先分配一个位于rbp的地址,然后分配给rbp-0x4,即沿rbp到rsp的方向进行分配,然后在rbp-0x8和rsp之间留出一些空间,用于“ q”。
我不确定的是我所观察的情况是否应该是由于我更好地意识到并遵守的一些体系结构约束,或者仅仅是此特定实现的人工产物以及用户习惯的体现?我读过我的代码的人,我不应赋予任何意义,例如这需要在一个方向或另一个方向上进行,而只要是一致的,都无所谓。
或者也许我现在只是在阅读和编写琐碎的代码,随着时间的流逝,这会双向发展吗?
我只想知道如何在自己的汇编代码中进行操作。
所有这些都在Linux 64位GCC版本7.5.0(Ubuntu 7.5.0-3ubuntu1〜18.04)上进行。谢谢。
00000000000005fa <leaf>:
5fa: 55 push rbp
5fb: 48 89 e5 mov rbp,rsp
5fe: b8 01 00 00 00 mov eax,0x1
603: 5d pop rbp
604: c3 ret
0000000000000605 <myfunc>:
605: 55 push rbp
606: 48 89 e5 mov rbp,rsp
609: 48 83 ec 10 sub rsp,0x10
60d: b8 00 00 00 00 mov eax,0x0
612: e8 e3 ff ff ff call 5fa <leaf>
617: 89 45 f4 mov DWORD PTR [rbp-0xc],eax ; // <--- This line
61a: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2 ; // <-- And this too
621: 8b 55 f4 mov edx,DWORD PTR [rbp-0xc]
624: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
627: 01 d0 add eax,edx
629: 89 45 fc mov DWORD PTR [rbp-0x4],eax
62c: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
62f: c9 leave
630: c3 ret
这是C代码:
int leaf() {
return 1;
}
int myfunc() {
int x = leaf(); // <--- This line
int y = 2; // <-- And this too
int q = x + y;
return q;
}
int main(int argc,char *argv[]) {
return myfunc();
}
我如何编译:
gcc -O0 main.c -o main.bin
我如何拆卸它:
objdump -d -j .text -M intel main.bin
解决方法
这使零差为零,因为您必须对必须存在的局部变量执行任何操作(因为您无法将它们优化到寄存器中)。
GCC的工作意义为零;没用的间隙在哪里都没有关系(由于堆栈对齐而存在)。在这种情况下,它是[rsp]
(又名[rbp - 0x10]
)的4个字节。[rbp - 4]
的4个字节用于q
。
此外,您没有告诉GCC进行优化,因此没有理由指望GCC的选择甚至是最优的,也不是学习的有用指南。将-O3
与volatile int
当地人一起使用会更有意义。 (但是由于没有什么大的事情发生,所以实际上仍然没有帮助。)
重要的事情:
-
本地变量应该自然对齐(dword值至少4字节对齐)。 C ABI要求:alignof(int)=4。在调用之前,RSP将按16字节对齐,因此在函数项RSP-8中按16字节对齐。
-
代码大小:尽可能多的寻址模式可以使用RBP(或RSP,如果您是相对于RSP的本地地址,则使用{{sup> 1 ) {1}}。
当您只有几个标量局部变量时,这种情况就很简单了,远不及它们的128个字节。
-
您可以一起使用的所有本地语言都是相邻的,并且最好不要越过对齐边界,因此您可以通过一个qword或XMM存储最有效地将它们全部初始化。
如果您有很多本地(或数组),则在此功能(及其子级)运行时,如果有一条整个缓存行可能“冷”了,请将它们分组以进行空间定位。
-
空间局部性:您在函数中较早使用的变量在堆栈框架中应该较高(更靠近
gcc -fomit-frame-pointer
对此函数存储的返回地址) 。堆栈通常在高速缓存中很热,但是如果它在较早的加载/存储之后进行操作,则随着堆栈存储器的增长而接触新的高速缓存行的影响将略微减小。乱序的执行人员希望可以尽快获得这些以后的存储指令,并将该高速缓存未命中的存储放入管道中,以便尽早启动RFO(读取所有权),从而最大程度地减少因较早加载而阻塞存储缓冲区的时间。 >这仅在超过16个字节的边界上才有意义;您知道一个16字节对齐块中的所有内容都在同一缓存行中。
一个高速缓存行中的降序访问模式可能会触发向下预取下一个高速缓存行,但是我不确定这是否发生在实际的CPU中。如果是这样,这可能是不这样做的原因,并且倾向于首先存储到堆栈帧的底部(在RSP或您实际使用的最低红色区域地址)。
如果在另一个call
之前没有用于堆栈对齐的空间,则通常最多只有8个字节。这比缓存行小得多,因此对局部变量的空间局部性没有任何重大影响。您知道堆栈指针相对于16字节边界的对齐方式,因此,将填充保留在堆栈帧的顶部或底部是不会对潜在地接触新的缓存行造成影响的。
如果将指向本地变量的指针传递给不同的 threads ,请当心错误共享:可能会将这些本地变量至少分隔64个字节,以便它们位于不同的缓存行中,或者甚至更好128字节(L2空间预取器可能会在相邻的缓存行之间产生“破坏性干扰”。
脚注1 :在诸如call
这样的寻址模式下,x86符号扩展的8位与符号扩展的32位位移是x86-64 System V ABI选择128的原因。 RSP下方的字节[{3}}:最多提供256字节的内容,可以使用更紧凑的代码大小进行访问,包括红色区域以及RSP上方的保留空间。
PS:
请注意,您没有必须在函数的每个点上为相同的高级“变量”使用相同的内存位置。您可以将某些内容溢出/重新加载到功能某个部分的一个位置,然后再加载到该功能的另一个位置。 IDK为什么会这样,但是如果您浪费了对齐空间,那么您可以可以这样做。如果您希望一个高速缓存行较早变热(例如,在函数入口堆栈框的顶部附近),而另一高速缓存行较晚变热(在其他一些使用率较高的变量附近),则可能是这样。
“变量”是您可以随意实现的高级概念。这不是C,没有要求它有一个地址或具有相同的地址。 (实际上,C编译器会在不使用地址的情况下将变量优化到寄存器中,或者在内联后不对函数进行转义。)
这是一种脱位或至少是学徒式的转移;通常,当它不能在寄存器中时,您只会对同一事物始终使用相同的内存位置。