CodeZine(コードジン)

特集ページ一覧

インラインアセンブラで学ぶアセンブリ言語 第1回

Visual C++を用いたアセンブラプログラミング体験

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2006/06/05 00:00
目次

配列へのアクセス

 数値や文字型といったプリミティブな変数は、インラインアセンブラの場合には、C言語の識別子から直接アクセスすることができることを説明してきましたが、C言語では配列や構造体、共用体、列挙体など、より複雑なデータ構造を持つ変数を作成することができます。実践でインラインアセンブラを利用するような場合は、こうした複雑なデータ構造を持つメモリを操作する必要があります。

 配列型の変数の各要素にアクセスする方法は、C言語の入れ子の操作方法と同じです。結局は、先頭要素からのアドレスの計算でどのようにでも操作することができます。幸いなことにインラインアセンブラであれば[ ]を使ったC言語の添字による配列要素の指定をそのまま使うことができます。

サンプル6 配列へのアクセス
#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;
}
サンプル6 実行結果
Kitty

 サンプル6では、char型の配列textの各要素に、アセンブリ言語を使って文字をストアしています。C言語では、文字配列の末尾は0で終わらなければならないので、最後の要素である5番には0をストアします。実行結果を見ると、適切に配列を初期化できたことを確認できます。

 ただし、インラインアセンブラで指定する添字は、配列の先頭から純粋にアドレスを加算する値となります。C言語では、配列型によって適切にアドレス計算が行われていましたが、アセンブリではtext[1]は配列の先頭であるtextからアドレスを1加算したメモリアドレスとなります。よって、配列が1バイト以上である場合には、配列型のサイズにアクセスしたい要素番号を乗算しなければなりません。

サンプル7 期待とは異なる結果
#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演算子に該当するマクロアセンブラの演算子として、LENGTHSIZETYPEの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型のサイズにアクセスしたい要素の番号を乗算すれば、適切なメモリアドレスを算出することができます。

サンプル8 LENGTH、SIZE、TYPE 演算子
#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は、LENGTHSIZETYPE演算子のそれぞれの結果を表示すると同時に、宣言した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変数から構造体型を判別して適切なアドレスの計算を行うことができます。

サンプル9 構造体のメンバへのアクセス
#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++環境をそのままに、アセンブリ言語の基礎を学習することができるよい機会を与えてくれると思います。高水準言語に慣れ親しんでしまっている開発者にとってメモリアドレスの計算は敬遠してしまいがちな存在ですが、インラインアセンブラから徐々に、より機械語に近い本物のアセンブリ言語の世界に足を運んでみてはいかがでしょうか。

参考資料

  1. インテル 『IA-32 インテル・アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル 上巻・中巻
  2. MSDN ライブラリ 『C++ Language Reference Inline Assembler
  3. はじめて読む8086』 蒲池輝尚 著、アスキー刊、1987年


  • LINEで送る
  • このエントリーをはてなブックマークに追加

バックナンバー

連載:インラインアセンブラで学ぶアセンブリ言語

著者プロフィール

  • 赤坂 玲音(アカサカ レオン)

    平成13年度「全国高校生・専門学校生プログラミングコンテスト 高校生プログラミングの部」にて最優秀賞を受賞。 2005 年度~ Microsoft Most Variable Professional Visual Developer - Visual C++。 プログラミング入門サイト Wis...

あなたにオススメ

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5