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

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

この記事を読むのに必要な時間:およそ 8.5 分

アドレッシング修飾

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

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

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

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

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

インデックス(index)指定

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

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

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

CPU アーキテクチャによっては,値算出の事前ないし事後に更新を伴うアドレッシングとして,事前減算(pre decrement)や事後加算(post increment)を指定可能なものもあります。

これらのアドレッシングを備えるCPUアーキテクチャに対してCコンパイラは,前置 "--" や後置 "++" のコンパイル結果として,これらのアドレッシングを使用する可能性があります

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

詳細に関しては,各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" 命令を使用するようにします。

アドレッシング結果の算出方法を見ればわかるように,"lea" 命令は複雑な演算を1命令で実施することができますので,演算の高速のために "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アーキテクチャによっては先述したようにプログラムの実行が中断されますから,少なくとも「境界整合」という概念自体は理解しておく必要があります。

著者プロフィール

藤原克則(ふじわらかつのり)

Mercurial三昧の日々が嵩じて, いつの間にやら『入門Mercurial Linux/Windows対応』を上梓。凝り性なのが災いして,年がら年中アップアップな一介の実装屋。最近は仕事の縁が元で,OpenSolarisに入れ込む毎日。