考虑以下简单程序:
int main(int argc, char **argv)
{
char buffer[256];
buffer[0] = 0x41;
buffer[128] = 0x41;
buffer[255] = 0x41;
return 0;
}
在X86-64机器上使用GCC 4.7.0编译。用GDB拆卸Main()给出:
0x00000000004004cc <+0>: push rbp
0x00000000004004cd <+1>: mov rbp,rsp
0x00000000004004d0 <+4>: sub rsp,0x98
0x00000000004004d7 <+11>: mov DWORD PTR [rbp-0x104],edi
0x00000000004004dd <+17>: mov QWORD PTR [rbp-0x110],rsi
0x00000000004004e4 <+24>: mov BYTE PTR [rbp-0x100],0x41
0x00000000004004eb <+31>: mov BYTE PTR [rbp-0x80],0x41
0x00000000004004ef <+35>: mov BYTE PTR [rbp-0x1],0x41
0x00000000004004f3 <+39>: mov eax,0x0
0x00000000004004f8 <+44>: leave
0x00000000004004f9 <+45>: ret
当缓冲区为256字节时,为什么仅使用0x98 = 152d sub rsp?当我将数据移动到缓冲区[0]时,它似乎只是在分配的堆栈帧之外使用数据并使用RBP来引用,那么Sub RSP的点是什么,0x98?
另一个问题,这些行有什么作用?
0x00000000004004d7 <+11>: mov DWORD PTR [rbp-0x104],edi
0x00000000004004dd <+17>: mov QWORD PTR [rbp-0x110],rsi
为什么需要保存EDI而不是RDI?我看到它将其移至C代码中分配的缓冲区的最大范围之外。同样令人感兴趣的是为什么两个变量之间的三角洲如此之大。由于EDI只有4个字节,为什么它需要两个变量的12个字节分离?
linux使用的x86-64 ABI(以及其他一些OS,尤其是不是 Windows,它具有不同的ABI)定义了"红色区域"在堆栈指针下方的128个字节中,保证不会被信号或中断处理程序触摸。(见图3.3和§3.2.2。)
因此,叶子功能(即一个不称呼其他任何东西的功能)可能会将此区域用于所需的任何东西 - 它不做任何像call
那样将数据放在堆栈指针上的事情;任何信号或中断处理程序都会遵循ABI,并在存储任何内容之前至少将堆栈指针删除至少128个字节。
(较短的指令编码可用于签名的8位位移,因此红色区域的重点是增加了叶子函数可以使用这些较短的说明访问的本地数据量。)
)这就是这里发生的事情。
但是...此代码不利用这些较短的编码(它使用rbp
而不是rsp
的偏移)。为什么不?它还完全不必要地保存了edi
和rsi
- 您问为什么它保存edi
而不是rdi
,但是为什么完全保存它?
答案是编译器正在生成真正的crummy代码,因为没有启用优化。如果启用任何优化,您的整个功能可能会崩溃至:
mov eax, 0
ret
因为这实际上就是它需要做的:buffer[]
是本地的,因此对其进行的更改将永远不会可见,因此可以优化。除此之外,所有功能都需要做的是返回0。
所以,这是一个更好的例子。此功能是完全胡说八道的,但使用类似的数组,同时做得足以确保一切都不会被优化:
$ cat test.c
int foo(char *bar)
{
char tmp[256];
int i;
for (i = 0; bar[i] != 0; i++)
tmp[i] = bar[i] + i;
return tmp[1] + tmp[200];
}
通过一些优化编译,您可以看到红色区域的类似用途,除了这一次,它确实确实使用了rsp
的偏移:
$ gcc -m64 -O1 -c test.c
$ objdump -Mintel -d test.o
test.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <foo>:
0: 53 push rbx
1: 48 81 ec 88 00 00 00 sub rsp,0x88
8: 0f b6 17 movzx edx,BYTE PTR [rdi]
b: 84 d2 test dl,dl
d: 74 26 je 35 <foo+0x35>
f: 4c 8d 44 24 88 lea r8,[rsp-0x78]
14: 48 8d 4f 01 lea rcx,[rdi+0x1]
18: 4c 89 c0 mov rax,r8
1b: 89 c3 mov ebx,eax
1d: 44 28 c3 sub bl,r8b
20: 89 de mov esi,ebx
22: 01 f2 add edx,esi
24: 88 10 mov BYTE PTR [rax],dl
26: 0f b6 11 movzx edx,BYTE PTR [rcx]
29: 48 83 c0 01 add rax,0x1
2d: 48 83 c1 01 add rcx,0x1
31: 84 d2 test dl,dl
33: 75 e6 jne 1b <foo+0x1b>
35: 0f be 54 24 50 movsx edx,BYTE PTR [rsp+0x50]
3a: 0f be 44 24 89 movsx eax,BYTE PTR [rsp-0x77]
3f: 8d 04 02 lea eax,[rdx+rax*1]
42: 48 81 c4 88 00 00 00 add rsp,0x88
49: 5b pop rbx
4a: c3 ret
现在,让我们通过插入一个呼叫对另一个函数的调用来进行稍作调整,因此foo()
不再是叶子功能:
$ cat test.c
extern void dummy(void); /* ADDED */
int foo(char *bar)
{
char tmp[256];
int i;
for (i = 0; bar[i] != 0; i++)
tmp[i] = bar[i] + i;
dummy(); /* ADDED */
return tmp[1] + tmp[200];
}
现在无法使用红色区域,因此您会看到更多类似的东西最初预期:
$ gcc -m64 -O1 -c test.c
$ objdump -Mintel -d test.o
test.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <foo>:
0: 53 push rbx
1: 48 81 ec 00 01 00 00 sub rsp,0x100
8: 0f b6 17 movzx edx,BYTE PTR [rdi]
b: 84 d2 test dl,dl
d: 74 24 je 33 <foo+0x33>
f: 49 89 e0 mov r8,rsp
12: 48 8d 4f 01 lea rcx,[rdi+0x1]
16: 48 89 e0 mov rax,rsp
19: 89 c3 mov ebx,eax
1b: 44 28 c3 sub bl,r8b
1e: 89 de mov esi,ebx
20: 01 f2 add edx,esi
22: 88 10 mov BYTE PTR [rax],dl
24: 0f b6 11 movzx edx,BYTE PTR [rcx]
27: 48 83 c0 01 add rax,0x1
2b: 48 83 c1 01 add rcx,0x1
2f: 84 d2 test dl,dl
31: 75 e6 jne 19 <foo+0x19>
33: e8 00 00 00 00 call 38 <foo+0x38>
38: 0f be 94 24 c8 00 00 movsx edx,BYTE PTR [rsp+0xc8]
3f: 00
40: 0f be 44 24 01 movsx eax,BYTE PTR [rsp+0x1]
45: 8d 04 02 lea eax,[rdx+rax*1]
48: 48 81 c4 00 01 00 00 add rsp,0x100
4f: 5b pop rbx
50: c3 ret
(请注意,在第一种情况下,tmp[200]
在签名的8位位移范围内,但不在此情况下。)