一般的な 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アーキテクチャでは、"$" を冒頭に記述することで即値記述であることを明示します。
即値は値そのものですから、データ転送先として指定することはできません。そのため、以下のプログラムはエラーとなります。
これまでは特に断りなく "movl
"というデータ転送命令の表記を用いてきましたが、この表記は「データ転送」(move)を表す "mov
" と「32 ビット長」を表す "l
"(long)を組み合わせたものです。
データ転送におけるデータ長を16ビット(word)や 8ビット(byte)で行う場合は、データ長を表す "w
" や "b
"を指定します。
データ長を32ビット未満に限定した場合、レジスタ上で対象データ位置に該当しない部分は値が変更されません。
なお、このプログラムに対して、asコマンドは以下のような警告を出します。
これは、レジスタ指定アドレッシングの"%eax
"が「32ビット長」であることを暗に示しているために、データ長指定の"w
"や"b
"と整合が取れていないことに対する警告です。
Intel 80x86アーキテクチャでは、後方互換性の維持の点から、レジスタ表記とデータ長を以下のように定めています。
表1 データ長別レジスタ表記
表記 | データ長 | 意味 |
eax | 32ビット |
ax の32ビット拡張("extended") |
ax | 16ビット |
eaxの下位16ビット |
ah | 8ビット |
axの上位(high) |
al |
8 ビット | ax の下位(low) |
上記はEAXレジスタに対する表記例ですが、16ビット長に関する表記は他の全ての汎用レジスタに対して、8ビット長に関する表記はEBX、ECX、EDXに対して適用することができます。
直接アドレッシング/間接アドレッシング
取り扱い対象を直接指定するのが「直接アドレッシング」、指定したものをアドレス値とみなしてメモリ領域(あるいはそこに格納された値)を取り扱うのが「間接アドレッシング」です。
以下に、レジスタ指定/値指定との組み合わせの例を示します。
上記サンプルプログラムを実行して、データ転送状況を確認してみましょう。
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++言語でのポインタ変数の加減算と同じ感覚で使用すると、想定外の結果となりますので注意してください。
これらのアドレッシング修飾は、複数組み合わせることも可能です。アドレッシング修飾を実際のプログラムで見てみましょう。
上記の例では様々なアドレッシング修飾を用いて、アドレッシングによって算出されたアドレスに対してメモリアクセスを行なう間接アドレッシングを行なっています。
ここでたとえば、アドレッシングによって算出された値そのものを使用したい、つまり直接アドレッシングを行う場合にはどうすればよいのでしょうか?
Intel 80x86アーキテクチャでは、アドレッシングによって算出された値をレジスタに転送する命令として"lea
"(load effective address)命令を提供しています。
たとえば、先のプログラム例で言えば、"mov" 命令と"lea
" 命令は以下のように異なります。
メモリ上のアドレスをレジスタに格納する場合は、"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の並びで格納されます。
32ビットIntel 80x86アーキテクチャはLSB Firstのバイトオーダーを採用しています。
これらのバイトオーダーに注意を要するケースは、ファイルやネットワークを経由したデータ授受などが典型的なのですが、今時はデータの送り手受け手の双方が80x86アーキテクチャ、即ちLSB Firstのバイトオーダーであるため、本来であれば不適切なバイトオーダーであっても、一見正しく稼動しているように見える可能性もあります。
しかし、アセンブラレベルでプログラムを見る場合、特に問題が発生した際のメモリ内容の確認などでは、バイトオーダーの知識が必須となりますので、確実に理解しておきましょう。
境界整合
メモリに対するデータ転送を行う場合、アクセス先メモリのアドレスがデータサイズの倍数になっていないと、「異常検出」扱いでプログラムの実行が中断される場合があります。
たとえば、32ビット=4バイトのデータ転送を行う場合、アクセス先メモリのアドレスが4の倍数になっている必要があります。
前回~今回にかけて掲載しているサンプルプログラムにおいて記述されている.align
指定は、前述した制約を守るために、以降に記述される内容をメモリに配置する際のアドレスが、指定された数値の倍数になるように強制するものです。
このようにアドレスをデータサイズの倍数に揃えることを、「境界整合を取る」あるいは「アラインメントを取る」と言います。
ただし、状況によってはこの制約が緩められる場合もあります。
たとえば、倍精度(64ビット=8バイト)や4倍精度(128ビット=16バイト)の浮動小数点データを、メモリと(特定の)レジスタの間で転送する場合には、境界整合は4バイトで可能とするアーキテクチャもあります。
また、実のところIntel 80x86アーキテクチャの場合、境界整合が取られていなくてもプログラムの実行は継続されます。さらに、従来は性能劣化要因となっていた境界不整合ですが、最新のIntelプロセッサでは性能劣化を防ぐような機構が取り入れられたりもしています。
しかし軽減されているとは言え、境界不整合が性能劣化要因であることには変わりありませんし、CPUアーキテクチャによっては先述したようにプログラムの実行が中断されますから、少なくとも「境界整合」という概念自体は理解しておく必要があります。
メモリの前の平等
プログラム領域からの読み込み
これまでに例示してきたアセンブラソースでは、メモリとレジスタの間でのデータ転送は、常に「この領域はデータを格納するもの」とみなした領域との間で行ってきました。
しかし「メモリに格納されている」という点では、データもプログラムも同じですので、実はプログラムそのものを読み込むことも可能です。
リスト7のプログラムを実行してみましょう。
"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
に続いてデータ形式で記述することもできます。
GDBを使って、entry_point
から始まるデータがプログラムとして、program_as_data
から始まるプログラムがデータとして認識されていることを確認してみましょう。
「プログラム」と「データ」がメモリ上において等価である、という感じが掴めたでしょうか?