私はハイレベルのコーダーであり、アーキテクチャーは私にとってかなり新しいので、ここでアセンブリーに関するチュートリアルを読むことにしました。
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
いずれかの行について、
andl $-16, %esp
説明は:
このコードは「および」のESP with 0xFFFFFFF0、スタックを次に低い16バイト境界に揃えます。Mingwのソースコードを調べると、これは "_main "ルーチンは、アラインされたアドレスでのみ動作します。このルーチンにはSIMD命令が含まれていないため、この行は不要です。
この点はわかりません。スタックを次の16バイト境界に揃えることが何を意味するのか、なぜそれが必要なのかについて誰かが私に説明してもらえますか?そして、andl
はこれをどのように達成していますか?
_main
へのエントリでスタックが次のようになっていると想定します(スタックポインターのアドレスは単なる例です)。
| existing |
| stack content |
+-----------------+ <--- 0xbfff1230
%ebp
をプッシュし、%esp
から8を減算して、ローカル変数用にスペースを予約します。
| existing |
| stack content |
+-----------------+ <--- 0xbfff1230
| %ebp |
+-----------------+ <--- 0xbfff122c
: reserved :
: space :
+-----------------+ <--- 0xbfff1224
ここで、andl
命令は%esp
の下位4ビットをゼロにします。これにより、mayが減少します。この特定の例では、追加の4バイトを予約する効果があります。
| existing |
| stack content |
+-----------------+ <--- 0xbfff1230
| %ebp |
+-----------------+ <--- 0xbfff122c
: reserved :
: space :
+ - - - - - - - - + <--- 0xbfff1224
: extra space :
+-----------------+ <--- 0xbfff1220
この点は、メモリ内の複数のワードで並列処理を実行できる「SIMD」(単一命令、複数データ)命令(x86ランドでは「ストリーミングSIMD拡張機能」の「SSE」とも呼ばれます)があることですが、これらの複数のワードは、16バイトの倍数であるアドレスで始まるブロックである必要があります。
一般に、コンパイラーは、%esp
からの特定のオフセットが適切なアドレスになるとは想定できません(関数への入り口での%esp
の状態は呼び出しコードに依存するため)。ただし、このようにスタックポインターを意図的に調整することで、16バイトの倍数をスタックポインターに追加するとアドレスが16バイトに調整され、これらのSIMD命令で安全に使用できることがコンパイラーにわかります。
これはスタック固有であるように聞こえませんが、一般的にはアラインメントです。おそらく整数倍という用語を考えてください。
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セグメント内の命令とデータを混合する場合、必要に応じてデータを整列させることはかなり簡単です(まあ、アーキテクチャによって異なります)。しかし、スタックは実行時のものであり、コンパイラは通常、実行時にスタックがどこにあるかを決定できません。したがって、実行時に、位置合わせする必要のあるローカル変数がある場合、コードでスタックをプログラムで調整する必要があります。
たとえば、スタックに8バイトの項目が2つあり、合計16バイトで、それらを実際に整列させたいとします(8バイトの境界で)。エントリ時には、関数は通常どおりスタックポインタから16を減算して、これら2つの項目のためのスペースを作ります。しかし、それらを整合させるには、より多くのコードが必要になります。これらの2つの8バイト項目を8バイト境界で整列させ、16を減算した後のスタックポインターが0xFF82である場合、下位3ビットは0ではないため、整列されません。下位3ビットは0b010です。一般的な意味では、0xFF82から2を減算して0xFF80を取得します。 2であると判断する方法は、0b111(0x7)でANDしてその量を引くことです。これは、ALU演算のandおよびand減算を意味します。しかし、1の補数の値が0x7(〜0x7 = 0xFFFF ... FFF8)の場合、1つのALU演算を使用して0xFF80を取得する場合(コンパイラとプロセッサがそれを行う単一のオペコード方法を持っている限り)、ショートカットを取ることができます。そうでない場合は、andおよびsubtractよりも費用がかかる可能性があります)。
これは、プログラムが実行していたことのようです。 -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での非境界整列アクセスに対してペナルティを支払うことができます/支払うことになるので、たとえそれが可能であっても、他のすべてと同じように境界整列を維持するよう努める必要があります他のアーキテクチャ。おそらく、それがこのコードが行っていたことです。
これは バイトアライメント に関係しています。特定のアーキテクチャでは、特定の操作セットに使用されるアドレスを特定のビット境界に合わせる必要があります。
つまり、たとえば、ポインターに64ビットの境界整列が必要な場合、アドレス可能なメモリ全体を概念的にゼロから始まる64ビットのチャンクに分割できます。アドレスは、これらのチャンクの1つにぴったり収まる場合は「整列」され、1つのチャンクと別のチャンクの一部を占める場合は整列されません。
バイトアライメントの重要な機能(数値が2の累乗であると仮定)は、最下位[〜#〜] x [〜#〜]アドレスのビットは常にゼロです。これにより、プロセッサーは、最下位[〜#〜] x [〜#〜]ビットを使用しないだけで、より少ないビットでより多くのアドレスを表すことができます。
この「絵」を想像してください
アドレス xxx0123456789abcdef01234567 ... [------] [------] [------] ... 登録
8の倍数のアドレスの値を(64ビット)レジスタに簡単に「スライド」
アドレス 56789abc ... [------] [------] [------] ... 登録
もちろん、8バイト単位で「ウォーク」を登録します
ここで、アドレスxxx5の値をレジスターに入れたい場合は、はるかに困難です:-)
Andl -16を編集
-16は2進数で11111111111111111111111111110000です
-16の「および」何かを使用すると、最後の4ビットが0に設定された値、または16の倍数が得られます。
プロセッサがメモリからレジスタにデータをロードするとき、ベースアドレスとサイズでアクセスする必要があります。たとえば、アドレス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
が適切に配置されることを意味します。そのため、コンパイラーはパディングを介してベースアドレスをバンプする必要がありません。そのため、秒のサイズは小さくなります。
これらのアドレスにアクセスするとパフォーマンスが低下するため、奇数アドレスではなく偶数アドレスにのみ配置する必要があります。