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 でコンパイル。main() を GDB で逆アセンブルすると、次のようになります。

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 を実行するのはなぜですか? データを buffer[0] に移動すると、割り当てられたスタック フレームの外側のデータを使用し、rbp を使用して参照しているように見えるので、サブ rsp,0x98 のポイントは何ですか?

別の質問ですが、これらの行は何をしますか?

0x00000000004004d7 <+11>:    mov    DWORD PTR [rbp-0x104],edi
0x00000000004004dd <+17>:    mov    QWORD PTR [rbp-0x110],rsi

RDI ではなく EDI を保存する必要があるのはなぜですか? ただし、Cコードで割り当てられたバッファの最大範囲外にこれを移動していることがわかります。また興味深いのは、2 つの変数の間のデルタが非常に大きい理由です。EDI はわずか 4 バイトなので、2 つの変数を 12 バイトで区切る必要があるのはなぜですか?

4

1 に答える 1

25

Linux (およびその他のいくつかの OS、特にWindows ではなく、独自の異なる ABI を持つ) で使用される x86-64 ABIは、スタック ポインターの下に 128 バイトの「レッド ゾーン」を定義します。割り込みハンドラ。(図 3.3 および §3.2.2 を参照してください。)

したがって、リーフ関数 (つまり、他に何も呼び出さない関数) は、この領域を必要に応じて使用できcallます。スタック ポインターにデータを配置するようなことはしません。また、シグナルまたは割り込みハンドラーは、ABI に従い、何かを格納する前にスタック ポインターを少なくとも 128 バイト追加します。

(符号付き 8 ビット ディスプレイスメントでは、より短い命令エンコーディングを使用できるため、レッド ゾーンのポイントは、これらの短い命令を使用してリーフ関数がアクセスできるローカル データの量が増えることです。)

それがここで起こっていることです。

しかし...このコードは、これらの短いエンコーディングを使用していません(rbpではなくからのオフセットを使用していrspます)。なぜだめですか?それは節約でもediあり、rsi完全に不必要です-なぜではなく節約しているのかと尋ねますedirdi、なぜそれを節約するのですか?

答えは、最適化が有効にされていないため、コンパイラが非常に厄介なコードを生成しているということです。最適化を有効にすると、関数全体が次のように崩壊する可能性があります。

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 に答える