アロカの実装

alloca の実装 実際にはコンパイラの支援が必要 .ここにいる数人は、次のように簡単だと言っています:

sub esp, <size>

残念ながら、これは全体像の半分にすぎません。はい、それは「スタックにスペースを割り当てる」ことになりますが、いくつかの問題があります。

<オール> <リ>

コンパイラが esp に関連する他の変数を参照するコードを発行した場合 ebp の代わりに (noframe ポインターを使用してコンパイルする場合に典型的です)。次に、それらの参照を調整する必要があります。フレーム ポインタを使用しても、コンパイラはこれを行うことがあります。

<リ>

さらに重要なことは、定義により、スペースは alloca で割り当てられます 関数の終了時に「解放」する必要があります。

大きなものはポイント#2です。 必要だから <size> を対称的に追加するコードを発行するコンパイラ espまで 関数のすべての終了点で。

最も可能性の高いケースは、ライブラリの作成者がコンパイラに必要なヘルプを要求できる組み込み関数をコンパイラが提供していることです。

編集:

実際、glibc (GNU による libc の実装) では。 alloca の実装 単純にこれです:

#ifdef  __GNUC__
# define __alloca(size) __builtin_alloca (size)
#endif /* GCC.  */

編集:

それについて考えた後、コンパイラが常に alloca を使用するすべての関数でフレーム ポインターを使用する 、最適化設定に関係なく。これにより、すべてのローカルを ebp で参照できるようになります。 フレームのクリーンアップは、フレーム ポインターを esp に復元することで処理されます。 .

編集:

そこで、次のような実験を行いました:

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define __alloca(p, N) \
    do { \
        __asm__ __volatile__( \
        "sub %1, %%esp \n" \
        "mov %%esp, %0  \n" \
         : "=m"(p) \
         : "i"(N) \
         : "esp"); \
    } while(0)

int func() {
    char *p;
    __alloca(p, 100);
    memset(p, 0, 100);
    strcpy(p, "hello world\n");
    printf("%s\n", p);
}

int main() {
    func();
}

残念ながら機能しません 正しく。アセンブリ出力をgccで分析した後。最適化が邪魔をしているようです。問題は、コンパイラのオプティマイザがインライン アセンブリをまったく認識していないため、予想外の順序で処理を行う習慣があり、まだ esp 経由で物事を参照する .

結果の ASM は次のとおりです。

8048454: push   ebp
8048455: mov    ebp,esp
8048457: sub    esp,0x28
804845a: sub    esp,0x64                      ; <- this and the line below are our "alloc"
804845d: mov    DWORD PTR [ebp-0x4],esp
8048460: mov    eax,DWORD PTR [ebp-0x4]
8048463: mov    DWORD PTR [esp+0x8],0x64      ; <- whoops! compiler still referencing via esp
804846b: mov    DWORD PTR [esp+0x4],0x0       ; <- whoops! compiler still referencing via esp
8048473: mov    DWORD PTR [esp],eax           ; <- whoops! compiler still referencing via esp           
8048476: call   8048338 <[email protected]>
804847b: mov    eax,DWORD PTR [ebp-0x4]
804847e: mov    DWORD PTR [esp+0x8],0xd       ; <- whoops! compiler still referencing via esp
8048486: mov    DWORD PTR [esp+0x4],0x80485a8 ; <- whoops! compiler still referencing via esp
804848e: mov    DWORD PTR [esp],eax           ; <- whoops! compiler still referencing via esp
8048491: call   8048358 <[email protected]>
8048496: mov    eax,DWORD PTR [ebp-0x4]
8048499: mov    DWORD PTR [esp],eax           ; <- whoops! compiler still referencing via esp
804849c: call   8048368 <[email protected]>
80484a1: leave
80484a2: ret

ご覧のとおり、それほど単純ではありません。残念ながら、私はあなたがコンパイラの支援を必要としているという最初の主張を支持します.


実際、コンパイラのコード生成を十分に制御できない限り、これを完全に安全に行うことはできません。ルーチンはスタックを操作する必要があり、それが返されたときにすべてが消去されますが、スタック ポインターはその場所にメモリ ブロックが残るような位置にとどまります。

問題は、スタック ポインターが関数呼び出し全体で変更されていることをコンパイラーに通知できない限り、スタック ポインターを介して他のローカル (または何でも) を参照し続けることができると判断する可能性がありますが、オフセットは


D プログラミング言語の場合、alloca() のソース コードはダウンロードに付属しています。それがどのように機能するかについては、かなりよくコメントされています。 dmd1 の場合、/dmd/src/phobos/internal/alloca.d にあります。 dmd2 の場合、/dmd/src/druntime/src/compiler/dmd/alloca.d にあります。