アンティーク・アセンブラ~Antique Assembler

第2回メモリに始まりメモリに終わる

一般的な CPU アーキテクチャでは、演算や比較といったデータの加工は、レジスタに読み込んだデータに対して実施するのが通例です。その一方で加工対象となるデータは、プログラム作成時には定まっておらず、通常はメモリ上に格納されています。

そのためアセンブラプログラムの多くは、メモリ/レジスタの間でのデータ転送命令によって占められています。

今回は、メモリアクセスの基本となるデータ転送命令について説明します。

アドレッシング

アセンブラでは、処理対象となるものを特定するための形式のことを アドレッシングモード⁠addressing mode⁠⁠、あるいは単に「アドレッシング」と呼びます。

冒頭で「(メモリ間との)データ転送について説明する」と述べた上で「アドレス」⁠address)という言葉が含まれていることから、⁠アドレッシングはメモリを指定するもの」といった誤解を招くかもしれませんが、先述したように「アドレッシング」とは「処理対象となるものを特定するための形式」を指すものですので注意してください。

各CPUアーキテクチャごとにさまざまな種類のアドレッシングが提供されていますが、概ね以下に述べるような分類が可能です。

レジスタ指定/値指定

処理対象として、レジスタ自身(あるいはレジスタが保持している値)を指定するのか、あるいは値そのものを指定するかで、アドレッシングは大きく2種類に分類できます。

Intel 80x86アーキテクチャでレジスタを指定する場合、"%" に続けてレジスタ名を記述します。

32ビットIntel 80x86アーキテクチャでは、32 ビット幅の値を扱うことができる汎用レジスタとして、EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESPの8つのレジスタが利用可能です。

ただし、汎用レジスタと銘打ってはいるものの、歴史的経緯/アーキテクチャ設計上の理由から、特定用途での使用を前提としているレジスタがあります。上記のレジスタで言うなら、EBPおよびESPはスタック管理(詳細は第5回以降で説明予定)専用と考えた方が良いでしょう。

値そのものが指定される場合、その値を即値⁠immediate value)と呼びます。Intel 80x86アーキテクチャでは、"$" を冒頭に記述することで即値記述であることを明示します。

リスト1 レジスタ指定/値指定
    .text
    .align  4

    .global entry_point
entry_point:
    int3        # プログラム実行の一時停止

    # 即値 ⇒ レジスタ eax
    movl    $0x12345678, %eax

    # レジスタ eax ⇒ レジスタ ebx
    movl    %eax, %ebx

    .global end_of_program
end_of_program:
    int3        # プログラム実行の一時停止
    nop

即値は値そのものですから、データ転送先として指定することはできません。そのため、以下のプログラムはエラーとなります。

リスト2 アドレッシングエラー
    # レジスタ ⇒ 即値
    movl    %eax, $0x12345678

    # 即値 ⇒ 即値
    movl    $0xFFFFFFFF, $0x12345678

これまでは特に断りなく "movl"というデータ転送命令の表記を用いてきましたが、この表記は「データ転送」⁠move)を表す "mov" と「32 ビット長」を表す "l"(long)を組み合わせたものです。

データ転送におけるデータ長を16ビット(word)や 8ビット(byte)で行う場合は、データ長を表す "w" や "b"を指定します。

リスト3 データ長指定
    .text
    .align  4

    .global entry_point
entry_point:
    int3        # プログラム実行の一時停止

    movl    $0x12345678, %eax
    movl    $0xFFFFFFFF, %ebx
    movl    $0xFFFFFFFF, %ecx

    # 16 ビット長の転送
    movw    %eax, %ebx
    # 8 ビット長の転送
    movb    %eax, %ecx

    .global end_of_program
end_of_program:
    int3        # プログラム実行の一時停止
    nop

データ長を32ビット未満に限定した場合、レジスタ上で対象データ位置に該当しない部分は値が変更されません。

図1 データ長指定の実行例
(gdb) run
....
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00401001 in entry_point ()
(gdb) continue
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
0x00401016 in end_of_program ()
(gdb) info register eax ebx ecx
eax      0x12345678    305419896
ebx      0xffff5678    -43400
ecx      0xffffff78    -136
(gdb) 

なお、このプログラムに対して、asコマンドは以下のような警告を出します。

図2 データ長指定による警告
Warning: using `%bx' instead of `%ebx' due to `w' suffix
Warning: using `%ax' instead of `%eax' due to `w' suffix
Warning: using `%cl' instead of `%ecx' due to `b' suffix
Warning: using `%al' instead of `%eax' due to `b' suffix

これは、レジスタ指定アドレッシングの"%eax"が「32ビット長」であることを暗に示しているために、データ長指定の"w"や"b"と整合が取れていないことに対する警告です。

Intel 80x86アーキテクチャでは、後方互換性の維持の点から、レジスタ表記とデータ長を以下のように定めています。

表1 データ長別レジスタ表記
表記データ長意味
eax32ビット axの32ビット拡張("extended")
ax16ビット eaxの下位16ビット
ah8ビット axの上位(high)
al 8 ビットax の下位(low)

上記はEAXレジスタに対する表記例ですが、16ビット長に関する表記は他の全ての汎用レジスタに対して、8ビット長に関する表記はEBX、ECX、EDXに対して適用することができます。

直接アドレッシング/間接アドレッシング

取り扱い対象を直接指定するのが直接アドレッシング⁠⁠、指定したものをアドレス値とみなしてメモリ領域(あるいはそこに格納された値)を取り扱うのが間接アドレッシング」です。

以下に、レジスタ指定/値指定との組み合わせの例を示します。

リスト4 直接/間接アドレッシング
    .data
    .align  4

    .global value
value:
    .long   1

    .text
    .align  4

    .global entry_point
entry_point:
    int3        # プログラム実行の一時停止

    # 転送元: value 領域の格納値(間接)
    # 転送先: レジスタ eax(直接)
    movl    value, %eax

    # 転送元: value 領域のアドレス(直接)
    # 転送先: レジスタ eax(直接)
    movl    $value, %eax

    # 転送元: レジスタ eax の参照先領域の格納値(間接)
    # 転送先: レジスタ ebx(直接)
    movl    (%eax), %ebx

    # 転送元: 即値(直接)
    # 転送先: レジスタ eax の参照先領域の格納値(間接)
    movl    $0xFFFFFFFF, (%eax)

    # 転送元: 即値(直接)
    # 転送先: value 領域(間接)
    movl    $0x12345678, value

    # 転送元: レジスタ eax(直接)
    # 転送先: value 領域(間接)
    movl    %eax, value

    .global end_of_program
end_of_program:
    int3        # プログラム実行の一時停止
    nop

上記サンプルプログラムを実行して、データ転送状況を確認してみましょう。

図3 直接/間接アドレッシングの実行例
(gdb) disassemble entry_point end_of_program
    ※ 逆アセンブルで命令位置を確認
Dump of assembler code from 0x401000 to 0x401022:
0x00401000 :    int3   
0x00401001 :    mov    0x402000,%eax
0x00401006 :    mov    $0x402000,%eax
0x0040100b :    mov    (%eax),%ebx
0x0040100d :    movl   $0xffffffff,(%eax)
0x00401013 :    movl   $0x12345678,0x402000
0x0040101d :    mov    %eax,0x402000
End of assembler dump.
(gdb) run
....
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00401001 in entry_point ()
(gdb) info register eax ebx
eax            0x0    0
ebx            0x7ffdf000    2147348480
(gdb) print/x value
$1 = 0x1
    ※ レジスタ/メモリ内容の初期値を確認
(gdb) print/x &value
$2 = 0x402000
    ※ value のアドレスを確認
(gdb) stepi
0x00401006 in entry_point ()
    ※ "mov    0x402000,%eax" の実行
(gdb) info register eax
eax            0x1    1
    ※ レジスタ値の確認(= value 位置の内容)
(gdb) stepi
0x0040100b in entry_point ()
    ※ "mov    $0x402000,%eax" の実行
(gdb) info register eax
eax            0x402000    4202496
    ※ レジスタ値の確認(= value のアドレス)
(gdb) stepi
0x0040100d in entry_point ()
    ※ "mov    (%eax),%ebx" の実行
(gdb) info register ebx
ebx            0x1    1
    ※ レジスタ値の確認(= value 位置の内容)
(gdb) stepi
0x00401013 in entry_point ()
    ※ "movl   $0xffffffff,(%eax)" の実行
(gdb) print/x value
$3 = 0xffffffff
    ※ value 位置の内容確認
(gdb) stepi
0x0040101d in entry_point ()
    ※ "movl   $0x12345678,0x402000" の実行
(gdb) print/x value
$4 = 0x12345678
    ※ value 位置の内容確認
(gdb) stepi
0x00401022 in end_of_program ()
    ※ "mov    %eax,0x402000" の実行
(gdb) print/x value
$5 = 0x402000
    ※ value 位置の内容確認
(gdb) 

GDBのstepiコマンドは、CPU命令単位で1命令毎の実行を行います(= STEP Instruction⁠⁠。実行後に表示されるのは次に実行される命令の位置ですので、読み間違えないように注意してください。

あまり一般的ではありませんが、指定されたメモリ領域に格納された値をアドレスとみなし、さらに別のメモリ領域へとアクセスする、といった多段間接アドレッシングを許すCPUアーキテクチャもあります。

アドレッシング修飾

アドレッシングを理解する上での大きな分類としては、これまでに説明した2種類×2種類の4分類で概ね十分なのですが、実際のプログラミングで使用されるアドレッシングでは、さまざまな修飾指定を伴って用いられるのが一般的です。

絶対(absolute)/相対(relative)
たとえば、処理対象を単独(=絶対値とみなす)で利用する以外に、他の値と組み合わせて(=相対値とみなす)使用することも可能です。
ディスプレースメント(displacement)指定

処理対象に対して、即値を加えるアドレッシングは、一般にディスプレースメント指定と呼ばれます(相対アドレッシングの一種と言えます⁠⁠。

通常、指定された即値は符号拡張されますので、⁠加える」と表現してはいますが、実際には加算対象を中心とする一定範囲を指す用途に使用されます。

この指定は、C言語で言う構造体のフィールドアクセスや、スタック領域の参照に使用されます。

インデックス(index)指定

即値の加算を「ディスプレースメント指定」と呼ぶのに対して、レジスタ値(あるいはそれを元にした値)の加算はインデックス指定と呼ばれます(これも相対アドレッシングの一種と言えます⁠⁠。

ディスプレースメント指定の場合と同様に、加算対象を中心とする一定範囲を指す用途に使用されます。

この指定は、配列要素のアクセスに使用されます。

なお、必ずしも全てのアドレッシングが常に使用できるわけではありません。命令種別や、利用するレジスタによっては、使用可能なアドレッシングが制約を受ける場合もあります。

詳細に関しては、各CPUアーキテクチャのリファレンスマニュアル等を参照してください。32ビットIntel 80x86アーキテクチャに関しては、Intelの日本語資料ダウンロードページから、IA-32アーキテクチャ向けの「ソフトウェア・デベロッパーズ・マニュアル」⁠4巻組)等が入手可能です。

Intel 80x86アーキテクチャでは、これらのアドレッシング修飾を伴う際の記述を以下のように定めています(厳密にはここで示した以外にも省略バリエーションがあるのですが、ここでは割愛します⁠⁠。

[ディスプレースメント](レジスタ[, インデックスレジスタ[, 倍数]])

括弧("[ ]")で囲まれた部位は省略可能です。

ディスプレースメント値自体はいわゆる即値ですが、アドレッシング修飾における記述の場合は、冒頭の "$" 記述は必要ありません。

倍数⁠scaling factor)とは、インデックスとして使用するレジスタの値を何倍するかを指定するもので、1(省略時の値⁠⁠、2、4、8の中から指定することが可能です。倍数はデータ転送におけるデータ長とは無関係に決定されるため、データ長情報が加味されるC/C++言語でのポインタ変数の加減算と同じ感覚で使用すると、想定外の結果となりますので注意してください。

これらのアドレッシング修飾は、複数組み合わせることも可能です。アドレッシング修飾を実際のプログラムで見てみましょう。

リスト5 アドレッシング修飾
    .data
    .align  4

    .global value
value:
    .long   1
    .long   2
    .long   3
    .long   4

    .text
    .align  4

    .global entry_point
entry_point:
    int3        # プログラム実行の一時停止

    movl    $value, %eax

    # 修飾無し(絶対)
    movl    (%eax), %ebx

    # ディスプレースメント指定(相対)
    movl    4(%eax), %ebx
    # アクセス先アドレスは eax + 4 となり
    # ebx には 2 が格納される

    # インデックス指定(相対)
    movl    $2, %ecx
    movl    (%eax, %ecx, 4), %ebx
    # アクセス先アドレスは eax + ecx(2) x 4 となり
    # ebx には 3 が格納される

    # ディスプレースメント・インデックス指定(相対)
    movl    $4, %ecx
    movl    -4(%eax, %ecx, 4), %ebx
    # アクセス先アドレスは eax + ecx(4) x 4 - 4 となり
    # ebx には 4 が格納される

    .global end_of_program
end_of_program:
    int3        # プログラム実行の一時停止
    nop

上記の例では様々なアドレッシング修飾を用いて、アドレッシングによって算出されたアドレスに対してメモリアクセスを行なう間接アドレッシングを行なっています。

ここでたとえば、アドレッシングによって算出された値そのものを使用したい、つまり直接アドレッシングを行う場合にはどうすればよいのでしょうか?

Intel 80x86アーキテクチャでは、アドレッシングによって算出された値をレジスタに転送する命令として"lea"(load effective address)命令を提供しています。

たとえば、先のプログラム例で言えば、"mov" 命令と"lea" 命令は以下のように異なります。

リスト6 実効アドレス設定
movl    -4(%eax, %ecx, 4), %ebx
# アクセス先アドレスは eax + ecx(4) x 4 - 4 となり
# ebx には 4 が格納される

leal    -4(%eax, %ecx, 4), %ebx
# アドレッシング結果は eax + ecx(4) x 4 - 4 となり
# ebx には eax + 12 が格納される

メモリ上のアドレスをレジスタに格納する場合は、"mov" 命令よりも"lea" 命令を用いるのが一般的ですので、本連載でも今後のサンプルコードでは"lea" 命令を使用するようにします。

データ格納に関するあれこれ

バイトオーダー

Nビットの値において、最下位ビットは0ビット、最上位ビットはN-1ビットと呼びます。たとえば32ビットの値の場合、最上位ビットは31ビットとなります。

最上位ビットを含むバイト(N-1ビットからN-8ビットを含むバイト)がアドレス低位に格納される形式をMost Significant Byte First⁠MSB First)あるいは「ビッグエンディアン」(big endian)、 最下位ビットを含むバイト(7ビットから0ビットを含むバイト)がアドレス低位に格納される形式を、Least Significant Byte First⁠LSB First)あるいは ⁠リトルエンディアン」⁠little endian)と呼称します。また、これら格納順序のことをバイトオーダー⁠byte order)と呼びます。

0x12345678という32ビット値の例で言うと、MSB First格納形式なら、アドレス低位から順に0x12 0x34 0x56 0x78の並びで、LSB First格納形式なら、0x78 0x56 0x34 0x12の並びで格納されます。

図4 バイトオーダー
図4 バイトオーダー

32ビットIntel 80x86アーキテクチャはLSB Firstのバイトオーダーを採用しています。

これらのバイトオーダーに注意を要するケースは、ファイルやネットワークを経由したデータ授受などが典型的なのですが、今時はデータの送り手受け手の双方が80x86アーキテクチャ、即ちLSB Firstのバイトオーダーであるため、本来であれば不適切なバイトオーダーであっても、一見正しく稼動しているように見える可能性もあります。

しかし、アセンブラレベルでプログラムを見る場合、特に問題が発生した際のメモリ内容の確認などでは、バイトオーダーの知識が必須となりますので、確実に理解しておきましょう。

境界整合

メモリに対するデータ転送を行う場合、アクセス先メモリのアドレスがデータサイズの倍数になっていないと、⁠異常検出」扱いでプログラムの実行が中断される場合があります。

たとえば、32ビット=4バイトのデータ転送を行う場合、アクセス先メモリのアドレスが4の倍数になっている必要があります。

前回~今回にかけて掲載しているサンプルプログラムにおいて記述されている.align指定は、前述した制約を守るために、以降に記述される内容をメモリに配置する際のアドレスが、指定された数値の倍数になるように強制するものです。

このようにアドレスをデータサイズの倍数に揃えることを、境界整合を取る」あるいはアラインメントを取る」と言います。

ただし、状況によってはこの制約が緩められる場合もあります。

たとえば、倍精度(64ビット=8バイト)や4倍精度(128ビット=16バイト)の浮動小数点データを、メモリと(特定の)レジスタの間で転送する場合には、境界整合は4バイトで可能とするアーキテクチャもあります。

また、実のところIntel 80x86アーキテクチャの場合、境界整合が取られていなくてもプログラムの実行は継続されます。さらに、従来は性能劣化要因となっていた境界不整合ですが、最新のIntelプロセッサでは性能劣化を防ぐような機構が取り入れられたりもしています。

しかし軽減されているとは言え、境界不整合が性能劣化要因であることには変わりありませんし、CPUアーキテクチャによっては先述したようにプログラムの実行が中断されますから、少なくとも「境界整合」という概念自体は理解しておく必要があります。

メモリの前の平等

プログラム領域からの読み込み

これまでに例示してきたアセンブラソースでは、メモリとレジスタの間でのデータ転送は、常に「この領域はデータを格納するもの」とみなした領域との間で行ってきました。

しかし「メモリに格納されている」という点では、データもプログラムも同じですので、実はプログラムそのものを読み込むことも可能です。

リスト7 プログラム領域からの読み込み
    .text
    .align  4

    .global entry_point
entry_point:
    int3        # プログラム実行の一時停止

    # entry_point から 4 バイトの読み込み
    movl    entry_point, %eax

    .global end_of_program
end_of_program:
    int3        # プログラム実行の一時停止
    nop

リスト7のプログラムを実行してみましょう。

図5 プログラム領域からの読み込み実行例
(gdb) run
....
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00401001 in entry_point ()
(gdb) x/8bx entry_point
    ※ entry_point からの 8 バイトをデータとみなして16進数表示
0x401000 <entry_point>:    0xcc 0xa1 0x00 0x10 0x40 0x00 0xcc 0x90
(gdb) info register eax
eax        0x0    0
(gdb) continue
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
0x00401007 in end_of_program ()
(gdb) info register eax
eax        0x1000a1cc    268476876
(gdb) 

"movl entry_point, %eax" の実行により、レジスタeaxにはentry_pointが指す領域に格納された0x1000a1ccが格納されました(先述したようにIntel 80x86アーキテクチャのバイトオーダーはLSB Firstですから、メモリ内容の表示とレジスタ値の表示を比較する際には順序の読み替えが必要です⁠⁠。

なお、⁠プログラムそのものを読み込む」とは言っても、レジスタ上に読み込んだ「プログラム」が実行できるわけではありません。単に「プログラムをデータとみなして読み込む」だけですのでご注意ください。

ちなみに、上記のプログラムを改変して読み込み位置を変化させてみると、想定していたものとは違うデータがレジスタeaxに読み込まれた、といった状況が発生するかもしれません。

これは、ブレークポイントを設定したり、step/stepi等による実行制御を行う際に、実行を中断するための命令int3をデバッガが埋め込むためです(この命令は16進数表記で0xccとなる長さ1バイトの命令です⁠⁠。

デバッガがint3を埋め込んだ位置に対して、メモリ内容表示や逆アセンブル表示が要求された場合には、埋め込み前の内容を元にした処理を行うので、一見想定外のデータが読み込まれたように見えてしまうのです。

「プログラム 」と「データ」

これまでは特に説明せずに.text.dataなどとアセンブラソース中に記述してきましたが、これらには重要な意味があります。

.textは、それ以後に記述された内容がプログラムとして実行可能なものであることを、.dataは、それ以後に記述された内容がプログラムとして実行されないもの、すなわちデータであることを意味します。

一般に、前者をテキストセグメント⁠text segment⁠⁠、後者をデータセグメント⁠data segment)と呼びます。

最終的な所属セグメントが異なる以外は、アセンブラにとってプログラムとデータの間に差異はありませんから、.data に続いてプログラム形式で記述することも、.text に続いてデータ形式で記述することもできます。

リスト8 記述形式の入れ替え(0002-08)
    .data
    .align  4

    .global program_as_data
program_as_data:
    # データセグメントでプログラム形式の記述
    movl    $0x12345678, %eax
    movl    %eax, %ebx
    .long   0xFFFFFFFF    # ダンプ時の目印

    .text
    .align  4

    .global entry_point
entry_point:
    int3        # プログラム実行の一時停止

    # テキストセグメントでデータ形式の記述
    .byte   0xB8, 0x78, 0x56, 0x34, 0x12
    .byte   0x89, 0xC3

    .global end_of_program
end_of_program:
    int3        # プログラム実行の一時停止
    nop

GDBを使って、entry_pointから始まるデータがプログラムとして、program_as_dataから始まるプログラムがデータとして認識されていることを確認してみましょう。

図6 記述形式の入れ替え確認
(gdb) disassemble entry_point end_of_program
Dump of assembler code from 0x401000 to 0x401008:
0x00401000 <entry_point+0>:    int3   
0x00401001 <entry_point+1>:    mov    $0x12345678,%eax
0x00401006 <entry_point+6>:    mov    %eax,%ebx
End of assembler dump.
(gdb) x/12bx &program_as_data
0x402000 <program_as_data>: 0xb8 0x78 0x56 0x34 0x12 0x89 0xc3 0xff
0x402008 <program_as_data+8>: 0xff 0xff 0xff 0x00
(gdb)

「プログラム」「データ」がメモリ上において等価である、という感じが掴めたでしょうか?

おすすめ記事

記事・ニュース一覧