61

私は高レベルのコーダーであり、アーキテクチャーは私にとってかなり新しいので、ここでアセンブリに関するチュートリアルを読むことにしました。

http://en.wikibooks.org/wiki/X86_Assembly/Print_Version

チュートリアルのはるか下で、Hello World!を変換する方法について説明します。プログラム

#include <stdio.h>

int main(void) {
    printf("Hello, world!\n");
    return 0;
}

同等のアセンブリコードが与えられ、以下が生成されました。

        .text
LC0:
        .ascii "Hello, world!\12\0"
.globl _main
_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        andl    $-16, %esp
        movl    $0, %eax
        movl    %eax, -4(%ebp)
        movl    -4(%ebp), %eax
        call    __alloca
        call    ___main
        movl    $LC0, (%esp)
        call    _printf
        movl    $0, %eax
        leave
        ret

行の1つについては、

andl    $-16, %esp

説明は:

このコードは、ESPを0xFFFFFFF0で「and」し、スタックを次に低い16バイト境界に揃えます。Mingwのソースコードを調べると、これは「_main」ルーチンに表示されるSIMD命令の場合であり、整列されたアドレスでのみ動作することがわかります。ルーチンにはSIMD命令が含まれていないため、この行は不要です。

この点はわかりません。スタックを次の16バイト境界に揃えることの意味と、それが必要な理由について誰かに説明してもらえますか?そして、andlこれをどのように達成していますか?

4

6 に答える 6

72

へのエントリでスタックが次のようになっているとします_main(スタック ポインターのアドレスは単なる例です)。

|    existing     |
|  stack content  |
+-----------------+  <--- 0xbfff1230

を押し%ebpてから 8 を引い%espて、ローカル変数用のスペースを確保します。

|    existing     |
|  stack content  |
+-----------------+  <--- 0xbfff1230
|      %ebp       |
+-----------------+  <--- 0xbfff122c
:    reserved     :
:     space       :
+-----------------+  <--- 0xbfff1224

ここで、andl命令は の下位 4 ビットをゼロにするため%esp、値減少する可能性があります。この特定の例では、追加の 4 バイトを予約する効果があります。

|    existing     |
|  stack content  |
+-----------------+  <--- 0xbfff1230
|      %ebp       |
+-----------------+  <--- 0xbfff122c
:    reserved     :
:     space       :
+ - - - - - - - - +  <--- 0xbfff1224
:   extra space   :
+-----------------+  <--- 0xbfff1220

これのポイントは、メモリ内の複数のワードに対して並列操作を実行できるいくつかの「SIMD」(Single Instruction, Multiple Data) 命令 (x86 ランドでは「ストリーミング SIMD 拡張機能」の「SSE」とも呼ばれる) があることですが、これらの複数のワードは、16 バイトの倍数であるアドレスで始まるブロックである必要があります。

一般に、コンパイラは特定のオフセットから%esp適切なアドレスが得られるとは想定できません (%esp関数へのエントリ時の の状態は呼び出しコードに依存するため)。しかし、このようにスタック ポインターを意図的に位置合わせすることにより、コンパイラーは、スタック ポインターに 16 バイトの倍数を追加すると、これらの SIMD 命令で安全に使用できる 16 バイトに位置合わせされたアドレスになることを認識します。

于 2010-11-13T23:58:06.470 に答える
19

これはスタック固有ではないように聞こえますが、一般的な配置です。おそらく、整数倍という用語を考えてみてください。

サイズが 1 バイト、単位が 1 のアイテムがメモリ内にある場合、それらはすべて整列されているとだけ言っておきましょう。サイズが 2 バイトの場合、整数の 2 倍は、0、2、4、6、8 などのように整列されます。また、非整数​​の倍数、1、3、5、7 は整列されません。サイズが 4 バイトで、0、4、8、12 などの整数倍のアイテムは整列され、1、2、3、5、6、7 などは整列されません。8、0、8、16、24、16、16、32、48、64 などについても同様です。

これが意味することは、項目のベース アドレスを調べて、整列されているかどうかを判断できるということです。

バイト単位のサイズ、形式のアドレス
1、xxxxxxx
2、xxxxxx0
4、xxxxx00
8、xxxx000
16,xxx0000
32,xx00000
64,x000000
等々

コンパイラが .text セグメント内の命令とデータを混合する場合、必要に応じてデータを整列するのはかなり簡単です (まあ、アーキテクチャによって異なります)。ただし、スタックは実行時のものであり、コンパイラは通常、実行時にスタックがどこにあるかを判断できません。そのため、実行時にアライメントが必要なローカル変数がある場合は、コードでスタックをプログラムで調整する必要があります。

たとえば、スタックに 2 つの 8 バイト項目があり、合計 16 バイトあり、それらを (8 バイト境界で) 揃えたいとします。エントリでは、関数は通常どおりスタック ポインタから 16 を減算して、これら 2 つの項目のためのスペースを確保します。しかし、それらを調整するには、より多くのコードが必要になります。これらの 2 つの 8 バイト項目を 8 バイト境界に整列させ、16 を引いた後のスタック ポインターが 0xFF82 である場合、下位 3 ビットは 0 ではないため、整列されません。下位 3 ビットは 0b010 です。一般的な意味では、0xFF82 から 2 を引いて 0xFF80 を取得します。2 であると判断する方法は、0b111 (0x7) との AND をとり、その量を差し引くことです。これは、演算と and および減算を alu することを意味します。しかし、0x7 の 1 の補数値 (~0x7 = 0xFFFF...

これは、プログラムが行っていたことのようです。-16 との AND は、0xFFFF....FFF0 との AND と同じであり、16 バイト境界にアラインされたアドレスになります。

これをまとめると、メモリを上位アドレスから下位アドレスに移動する典型的なスタック ポインタのようなものがある場合は、次のようにします。

 
sp = sp & (~(n-1))

ここで、n は整列するバイト数です (累乗でなければなりませんが、ほとんどの整列には通常 2 の累乗が含まれます)。malloc (アドレスは低から高に増加) を行ったと言って、何かのアドレスを整列させたい場合 (少なくとも整列サイズだけ必要以上に malloc することを忘れないでください)、

if(ptr&(~(n-)) { ptr = (ptr+n)&(~(n-1)); }

または、ifを取り出して、毎回追加とマスクを実行するだけです。

多く/ほとんどの非 x86 アーキテクチャには、配置規則と要件があります。x86 は、命令セットに関する限り非常に柔軟ですが、実行に関する限り、x86 でアライメントされていないアクセスに対してペナルティを支払うことができます (支払うことになります)。他のアーキテクチャ。おそらくそれが、このコードが行っていたことです。

于 2010-11-14T06:07:11.893 に答える
7

これは、バイト アラインメントに関係しています。特定のアーキテクチャでは、特定の一連の操作に使用されるアドレスを特定のビット境界に合わせる必要があります。

つまり、たとえば、ポインターに 64 ビットのアライメントが必要な場合、概念的には、アドレス指定可能なメモリ全体を 0 から始まる 64 ビットのチャンクに分割できます。アドレスは、これらのチャンクの 1 つに正確に収まる場合は「整列」され、あるチャンクと別のチャンクの一部である場合は整列されません。

バイト アラインメントの重要な機能 (数値が 2 のべき乗であると仮定) は、アドレスの最下位Xビットが常に 0 であることです。これにより、プロセッサは最下位のXビットを使用しないだけで、より少ないビットでより多くのアドレスを表すことができます。

于 2010-11-13T23:46:01.630 に答える
5

この「図」を想像してみてください

住所
 xxx0123456789abcdef01234567 ...
    [-----][------][------] ...
レジスタ

8 の倍数のアドレスの値は、(64 ビット) レジスタに簡単に「スライド」します。

住所
         56789abc ...
    [-----][------][------] ...
レジスタ

もちろん8バイト単位で「歩」を登録

アドレス xxx5 の値をレジスタに入れたい場合は、はるかに困難です :-)


編集 andl -16

-16 は、2 進数で 111111111111111111111111110000 です。

-16 で何かを「AND」すると、最後の 4 ビットが 0 に設定された値が得られます... または 16 の倍数です。

于 2010-11-13T23:47:22.147 に答える
4

プロセッサがメモリからレジスタにデータをロードするとき、ベース アドレスとサイズでアクセスする必要があります。たとえば、アドレス 10100100 から 4 バイトをフェッチします。この例の最後に 2 つのゼロがあることに注意してください。これは、先頭の 101001 ビットが有効になるように 4 バイトが格納されるためです。(プロセッサは、101001XX をフェッチすることにより、「ドント ケア」を介してこれらに実際にアクセスします。)

したがって、メモリ内で何かを整列させるとは、目的のアイテムのアドレスに十分なゼロバイトが含まれるように、データを再配置することを意味します (通常はパディングによって)。上記の例を続けると、最後の 2 ビットがゼロではないため、10100101 から 4 バイトを取得できません。バスエラーの原因になります。そのため、アドレスを 10101000 まで上げなければなりません (そして、その過程で 3 つのアドレス ロケーションが無駄になります)。

コンパイラはこれを自動的に行い、アセンブリ コードで表されます。

これは、C/C++ での最適化として明白であることに注意してください。

struct first {
    char letter1;
    int number;
    char letter2;
};

struct second {
    int number;
    char letter1;
    char letter2;
};

int main ()
{
    cout << "Size of first: " << sizeof(first) << endl;
    cout << "Size of second: " << sizeof(second) << endl;
    return 0;
}

出力は

Size of first: 12
Size of second: 8

2 つcharの を並べ替えるということは、 が適切に配置されることを意味するintため、コンパイラはパディングによってベース アドレスをバンプする必要がありません。そのため、2 番目のサイズが小さくなっています。

于 2010-11-13T23:50:46.253 に答える
3

アクセスするパフォーマンスが低下するため、奇数のアドレスではなく、偶数のアドレスにのみ配置する必要があります。

于 2010-11-13T23:32:23.463 に答える