34

D、C、C++ などの言語でインライン x86 アセンブラーを使用して alloca() を実装するにはどうすればよいですか? 少し変更したバージョンを作成したいのですが、まず、標準バージョンがどのように実装されているかを知る必要があります。コンパイラは非常に多くの最適化を実行するため、コンパイラから逆アセンブリを読み取ることは役に立ちません。正規の形式が必要なだけです。

編集:難しい部分は、これに通常の関数呼び出し構文を持たせたいことだと思います。つまり、ネイキッド関数または何かを使用して、通常の alloca() のように見せます。

編集 # 2: ああ、なんと、フレ​​ーム ポインターを省略していないと推測できます。

4

11 に答える 11

58

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

sub esp, <size>

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

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

  2. さらに重要なことは、定義により、関数が終了するときに割り当てられたスペースを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 <memset@plt>
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 <memcpy@plt>
8048496: mov    eax,DWORD PTR [ebp-0x4]
8048499: mov    DWORD PTR [esp],eax           ; <- whoops! compiler still referencing via esp
804849c: call   8048368 <puts@plt>
80484a1: leave
80484a2: ret

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

于 2009-04-03T17:04:45.340 に答える
7

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

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

于 2009-04-03T16:54:03.460 に答える
5

C および C++ 標準ではalloca()、スタックを使用する必要があるとは指定されていません。これalloca()は、C または C++ 標準 (または POSIX) に含まれていないためです¹。

コンパイラはalloca()、ヒープを使用して実装することもできます。たとえば、ARM RealView (RVCT) コンパイラは、バッファを割り当てるために使用し ( alloca()Webサイトで参照されています)、関数が戻るときにバッファを解放するコードをコンパイラに発行させます。これには、スタック ポインターを操作する必要はありませんが、コンパイラのサポートが必要です。malloc()

Microsoft Visual C++ には_malloca()、スタックに十分なスペースがない場合にヒープを使用する関数がありますが、明示的な解放を必要としない_freea()とは異なり、呼び出し元が を使用する必要があります。_alloca()

alloca()(C++ デストラクタを自由に使用すると、明らかにコンパイラ サポートなしでクリーンアップを実行できますが、任意の式内でローカル変数を宣言することはできないため、 RAII を使用するマクロを作成することはできないと思います。いずれにせよalloca()、一部の式 (関数のパラメーターなど)では使用できません。)

¹ はい、alloca()単純に を呼び出すan を書くことは合法system("/usr/games/nethack")です。

于 2009-04-04T19:50:48.550 に答える
4

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

于 2009-04-04T02:14:06.923 に答える
4

継続渡しスタイル Alloca

純粋な ISO C++の可変長配列。概念実証の実装。

使用法

void foo(unsigned n)
{
    cps_alloca<Payload>(n,[](Payload *first,Payload *last)
    {
        fill(first,last,something);
    });
}

コアアイデア

template<typename T,unsigned N,typename F>
auto cps_alloca_static(F &&f) -> decltype(f(nullptr,nullptr))
{
    T data[N];
    return f(&data[0],&data[0]+N);
}

template<typename T,typename F>
auto cps_alloca_dynamic(unsigned n,F &&f) -> decltype(f(nullptr,nullptr))
{
    vector<T> data(n);
    return f(&data[0],&data[0]+n);
}

template<typename T,typename F>
auto cps_alloca(unsigned n,F &&f) -> decltype(f(nullptr,nullptr))
{
    switch(n)
    {
        case 1: return cps_alloca_static<T,1>(f);
        case 2: return cps_alloca_static<T,2>(f);
        case 3: return cps_alloca_static<T,3>(f);
        case 4: return cps_alloca_static<T,4>(f);
        case 0: return f(nullptr,nullptr);
        default: return cps_alloca_dynamic<T>(n,f);
    }; // mpl::for_each / array / index pack / recursive bsearch / etc variacion
}

ライブデモ

githubの cps_alloca

于 2013-04-20T22:10:45.653 に答える
3

alloca は、アセンブリ コードで直接実装されます。これは、スタック レイアウトを高級言語から直接制御できないためです。

また、ほとんどの実装では、パフォーマンス上の理由からスタックを調整するなど、追加の最適化を実行することに注意してください。X86 でスタック領域を割り当てる標準的な方法は次のようになります。

sub esp, XXX

一方、XXX は allcoate するバイト数です。

編集:
実装を見たい (そして MSVC を使用している) 場合は、alloca16.asm と chkstk.asm を参照してください。
最初のファイルのコードは、基本的に、必要な割り当てサイズを 16 バイト境界に合わせます。2 番目のファイルのコードは、新しいスタック領域に属するすべてのページを実際に見て、それらに触れます。これにより、OS がスタックを拡張するために使用する PAGE_GAURD 例外がトリガーされる可能性があります。

于 2009-04-03T16:39:06.507 に答える
1

Open Watcomなどのオープンソース C コンパイラのソースを調べて、自分で見つけることができます。

于 2009-04-04T13:27:22.060 に答える
1

c99 の可変長配列を使用できない場合は、void ポインターへの複合リテラル キャストを使用できます。

#define ALLOCA(sz) ((void*)((char[sz]){0}))

これは -ansi (gcc 拡張として) に対しても機能し、それが関数の引数であっても機能します。

some_func(&useful_return, ALLOCA(sizeof(struct useless_return)));

欠点は、c++ としてコンパイルすると、g++>4.6 でエラーが発生することです: 一時配列のアドレスを取得しています ... clang と icc は文句を言いません

于 2015-07-04T08:18:20.397 に答える
-1

Alloca は簡単です。スタック ポインターを上に移動するだけです。次に、この新しいブロックを指すようにすべての読み取り/書き込みを生成します

sub esp, 4
于 2009-04-03T16:30:31.803 に答える
-2

「入力」命令をお勧めします。286 以降のプロセッサで利用可能です ( 186 でも利用できた可能性があります。はっきりとは覚えていませんが、とにかく広く利用できるわけではありませんでした)。

于 2009-04-03T16:34:04.290 に答える