配列へのアクセス
数値や文字型といったプリミティブな変数は、インラインアセンブラの場合には、C言語の識別子から直接アクセスすることができることを説明してきましたが、C言語では配列や構造体、共用体、列挙体など、より複雑なデータ構造を持つ変数を作成することができます。実践でインラインアセンブラを利用するような場合は、こうした複雑なデータ構造を持つメモリを操作する必要があります。
配列型の変数の各要素にアクセスする方法は、C言語の入れ子の操作方法と同じです。結局は、先頭要素からのアドレスの計算でどのようにでも操作することができます。幸いなことにインラインアセンブラであれば[ ]
を使ったC言語の添字による配列要素の指定をそのまま使うことができます。
#include <stdio.h> int main() { char text[6]; __asm { mov text[0], 'K'; mov text[1], 'i'; mov text[2], 't'; mov text[3], 't'; mov text[4], 'y'; mov text[5], 0; } printf("%s\n", text); return 0; }
Kitty
サンプル6では、char
型の配列text
の各要素に、アセンブリ言語を使って文字をストアしています。C言語では、文字配列の末尾は0で終わらなければならないので、最後の要素である5番には0をストアします。実行結果を見ると、適切に配列を初期化できたことを確認できます。
ただし、インラインアセンブラで指定する添字は、配列の先頭から純粋にアドレスを加算する値となります。C言語では、配列型によって適切にアドレス計算が行われていましたが、アセンブリではtext[1]
は配列の先頭であるtext
からアドレスを1加算したメモリアドレスとなります。よって、配列が1バイト以上である場合には、配列型のサイズにアクセスしたい要素番号を乗算しなければなりません。
#include <stdio.h> int main() { int ary[2] = { 0, 0 }; __asm { mov ary[1], 0xFF } printf("ary[0]=%X\n", ary[0]); printf("ary[1]=%X\n", ary[1]); return 0; }
ary[0]=FF00 ary[1]=0
サンプル7を実行すると、インラインアセンブラの場合は、マルチバイト型の配列に対する操作でも添字に指定した値がバイト単位で解釈されていることが確認できます。インラインアセンブラでは、ary[1]
は、メモリアドレスary
から1バイト目のアドレスを表し、C言語のように論理的な次の要素を表すわけではありません。
こうした問題は、Microsoftのマクロアセンブラで使われている特殊な演算子を用いることで解決できます。より純粋なアセンブリ言語では構造的なメモリ領域の管理もすべてプログラマがアドレスを計算して行う必要がありますが、一部の計算を自動化してくれるマクロアセンブラを使うことで、こうした作業を軽減することができます。
C言語におけるsizeof
演算子に該当するマクロアセンブラの演算子として、LENGTH
、SIZE
、TYPE
の3つの演算子があります。これらの演算子は、与えられた引数のサイズをバイト単位で返します。
LENGTH
は、与えられた配列の要素の数を返します。SIZE
は指定された変数の純粋なサイズを返し、TYPE
も、指定された変数のサイズを返しますが、配列型の変数を与えた場合はその変数の型のサイズを返します。
C言語 | インラインアセンブラ |
sizeof(ary) / sizeof(ary[0]) | LENGTH ary |
sizeof(ary) | SIZE ary |
sizeof(ary[0]) | TYPE ary |
TYPE
演算子を使うことによって、配列の要素に適切にアクセスすることができるようになります。例えば、次のような数値型の配列があったとします。
int ary[5];
このうち、C言語上の表記でary[2]
とする3番目の要素にアクセスしたい場合、インラインアセンブラではTYPE
演算子を使って次のように記述します。
ary[type ary * 2]
TYPE
演算子に配列型の変数を指定すれば、その変数の型のサイズを返すので、上記の場合はsizeof(int)
と同じです。int
型のサイズにアクセスしたい要素の番号を乗算すれば、適切なメモリアドレスを算出することができます。
#include <stdio.h> int main() { int _length, _size, _type; int ary[6] = { 0x10, 0x20, 0x30, 0x40, 0x50 , 0x60 }; __asm { mov _length, length ary mov _size, size ary mov _type, type ary mov ary[0], 0x100 mov ary[type ary * 1], 0x200 mov ary[type ary * 2], 0x300 mov ary[type ary * 3], 0x400 mov ary[type ary * 4], 0x500 mov ary[type ary * 5], 0x600 } printf("length=%d\n", _length); printf("size=%d\n", _size); printf("type=%d\n", _type); printf("ary[0]=%X\n", ary[0]); printf("ary[1]=%X\n", ary[1]); printf("ary[2]=%X\n", ary[2]); printf("ary[3]=%X\n", ary[3]); printf("ary[4]=%X\n", ary[4]); printf("ary[5]=%X\n", ary[5]); return 0; }
length=6 size=24 type=4 ary[0]=100 ary[1]=200 ary[2]=300 ary[3]=400 ary[4]=500 ary[5]=600
サンプル8は、LENGTH
、SIZE
、TYPE
演算子のそれぞれの結果を表示すると同時に、宣言したint
型の配列ary
のすべての要素を、インラインアセンブラから上書きしています。TYPE
演算子を用いて目的の要素に正しくアクセスできていることが確認できます。
構造体、共用体
構造体や共用体をインラインアセンブラ内で使う方法は難しくありません。これまでの C言語と同様に、メンバアクセス演算子.
を用いてメンバを指定することができます。問題は、ポインタから構造体や共用体のメンバに間接参照する場合です。間接参照を行うには、前述したようにEBXレジスタにアドレスを設定してMOV
命令からアクセスしなければならず、C言語の ->
演算子を使ってアクセスすることはできません。
ポインタから構造体にアクセスするには、まずポインタの有効なメモリアドレスをEBXレジスタにロードします。EBXレジスタから目的のメンバにアクセスするには、EBXレジスタを[ ]
で括って、その後にメンバを指定します。例えば、次のMOV
命令は構文としては問題ありません。
MOV [EBX].member, 100
上記のアセンブリ文は、EBXレジスタのアドレスから間接参照を行い、対象の構造体のmember
メンバに100をストアすることを表しています。ただし、コンパイラはメンバ名から構造体型を判断しているため、メンバ名が一意ではない場合はコンパイルエラーとなります。複数の構造体で同じ名前のメンバ名が宣言されている場合、構造体型の変数名を指定しなければなりません。member
メンバを保有する構造体型の変数obj
が宣言されているものとして、次のように記述することができます。
MOV [EBX]obj.member, 100
上記の場合、コンパイラはobj
変数から構造体型を判別して適切なアドレスの計算を行うことができます。
#include <stdio.h> struct A { int member1; int member2; int member3; }; struct B { int member1; }; int main() { struct A obj; struct A* pt = &obj; __asm { mov ebx, pt; mov [ebx]pt.member1, 10 mov [ebx].member2, 20 mov obj.member3, 30 } printf("member1=%d\n", obj.member1); printf("member2=%d\n", obj.member2); printf("member3=%d\n", obj.member3); return 0; }
member1=10 member2=20 member3=30
サンプル9は、A構造体型の変数obj
と、obj
へのポインタpt
を作成し、これらの変数からインラインアセンブラを使って構造体のメンバを初期化しています。A構造体型のポインタpt
から間接的に参照してobj
変数を操作するには、最初にEBXレジスタにpt
ポインタが保存しているメモリアドレスをロードします。そして、EBXレジスタのアドレスから構造体のメンバに即値10と20をストアしています。重要な部分だけを見てみましょう。
mov [ebx]pt.member1, 10 mov [ebx].member2, 20
上記の部分が、構造体へのポインタからメンバにアクセスするのコードです。member1
へのアクセスには[ebx]
に続いてpt
変数を指定していますが、これはB構造体で同じ名前のメンバmember1
が宣言されているためです。特別な理由がない限り、元となっている変数名を明示的に記述した方が可読性の点から考えても推奨されます。しかし、メンバが一意であるのならば、メンバ名だけでアクセスすることも可能です。
mov obj.member3, 30
最後のmember3
メンバの初期化は、A構造体型のローカル変数obj
から直接メンバを指定しています。ポインタでなければ、このようにメンバへのアクセス方法はC言語と同じです。
最後に
本稿では、MOV
命令を使ったデータの読み込みや書き込みを集中的に解説しましたが、アセンブリ言語による命令はまだまだ沢山あります。詳細はIntelが発行している「IA-32 インテル・アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル」を参照してください。また、Microsoftのインラインアセンブラは、一般的なアセンブリ言語に加えて独自の拡張が加えられていたり、C言語との組み合わせのための独自の仕様が存在しています。Microsoftのインラインアセンブラの詳細についてはMSDNを参照してください。
アセンブリ言語の経験がない開発者にとって、アセンブリ言語を使った開発というのは敷居が高いものだと思います。インラインアセンブラは、使い慣れたVisual C++環境をそのままに、アセンブリ言語の基礎を学習することができるよい機会を与えてくれると思います。高水準言語に慣れ親しんでしまっている開発者にとってメモリアドレスの計算は敬遠してしまいがちな存在ですが、インラインアセンブラから徐々に、より機械語に近い本物のアセンブリ言語の世界に足を運んでみてはいかがでしょうか。
- 第2回 『アセンブリ言語で算術演算/論理演算を行う』
参考資料
- インテル 『IA-32 インテル・アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル 上巻・中巻』
- MSDN ライブラリ 『C++ Language Reference Inline Assembler』
- 『はじめて読む8086』 蒲池輝尚 著、アスキー刊、1987年