16

考虑以下简单程序:

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 子 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 个字节呢?

4

1 回答 1

25

Linux (和其他一些操作系统,虽然特别不是Windows,它们有自己不同的 ABI)使用的x86-64 ABI在堆栈指针下方定义了一个 128 字节的“红色区域”,保证不会被信号或中断处理程序。(参见图 3.3 和 §3.2.2。)

因此,叶函数(即不调用任何其他函数的函数)可以使用这个区域来做任何它想要的事情——它不会做任何像 acall那样将数据放在堆栈指针上的事情;并且任何信号或中断处理程序都将跟随 ABI 并在存储任何内容之前将堆栈指针至少再下降 128 个字节。

(较短的指令编码可用于有符号的 8 位位移,因此红色区域的要点是它增加了叶函数可以使用这些较短指令访问的本地数据量。)

这就是这里发生的事情。

但是......这段代码没有使用那些较短的编码(它使用的是偏移量rbp而不是rsp)。为什么不?它也在节省edi,而且rsi完全没有必要——你问为什么要节省edi而不是rdi,但为什么要节省呢?

答案是编译器正在生成非常糟糕的代码,因为没有启用优化。如果您启用任何优化,您的整个功能可能会崩溃为:

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 位位移范围内,但不在这种情况下。)

于 2012-11-03T01:26:34.053 回答