「アセンブリ言語の教科書」の原稿

Last modified: 2006/08/05 09:26:36

このテキストは、2005年7月にデータハウスから出版された「アセンブリ言語の教科書」の原稿をWEB用に修正したものです。WEB用に修正したといっても、誤植を直した程度であり、ほぼそのままの状態で公開しています。

現在でも「アセンブリ言語の教科書」は書店で売られており、一般に流通しているため、本来ならば、出版社との契約上、このようにフリーでWEB上に公開することはできません。しかし、「アセンブリ言語の教科書」は、発売後すでに一年を過ぎようとしているにも関わらず、現在でも安定した売り上げを伸ばしており、当初の予想を超えて多くの方々に読んでいただけました。

よって、出版社に「本書の値段が高くて、読みたくても買えない学生の方々や、まだ本書の存在を知らない人たちのために、原稿の一部をWEB上にも公開できないだろうか」と、相談を持ちかけたところ、本書に関わった編集者からも「原稿のすべては無理だが一部分といことなら問題ない」という承諾をいただきました。

出版社も、著者である私も、本書がもっと売れて欲しいという気持ちはもちろんあります。しかし、同時に、少しでもアセンブラに興味を持ってくれる多くの方々に読んでもらいたいとも思っており、そのような考えから、今回、原稿をWEB上に公開するということになりました。公開されているのは、原稿のほんの10パーセント程度ではありますが、著者が気に入った部分や、これからアセンブラを学ぼうとする方々に有用だと思える部分をピックアップしたつもりです。これらの情報が、少しでも読者の方々の参考になれば幸いです。(2006年08月 Kenji Aiko)


8086アセンブラプログラミング

8086とは、1978年にIntel社が発売した16ビットマイクロプロセッサ(MPU)のことです。マイクロプロセッサ(超小型処理装置)とは、コンピュータ内で基本的な演算処理を行う、いわばコンピュータの心臓部に当たる半導体チップのことですが、これは、CPUと同じと捉えて問題ありません。厳密に言えば、コンピュータの演算処理は複数の半導体チップが連携して行っており、この半導体チップ群を「中央処理装置(CPU)」と呼んでいたようですが、現在ではマイクロプロセッサが全ての演算を担当することが当然となっているため、CPUという言葉もMPUと同じ意味として使われているようです。

現在のクライアントPC(サーバとして利用されない一般的なPC)には大抵32ビットCPUが搭載されています。最近では、32ビットCPUから64ビットCPUへの移行について議論されている状態であり、あと5年後にはクライアントPCでも64ビットCPUが搭載されるようになっているかもしれません。

8086アセンブラとは、16ビットCPUである8086上で動作するアセンブラのことです。しかし現在の主流は32ビットCPUであり、Windows2000やWindowsXPが動いている環境ならば、まず間違いなく32ビットCPUが搭載されているコンピュータでしょう。そのような環境で8086アセンブラを学ぶことができるのか、と思われるかもしれませんが、現在もっとも利用されているOSであるWindowsXPにも、16ビットCPUとの互換性は、完全にとはいえませんが保たれています。

ようこそ16ビットの世界へ

現在のWindowsプログラミングは、GUIが当たり前で、膨大なライブラリを駆使してあらゆることが実現できます。それに比べると、アセンブラプログラミングとはなんとも地味で魅力を感じにくいかもしれません。しかし、アセンブラとはコンピュータが理解できるマシン語にもっとも近いプログラミング言語であることは間違いありません。そういう意味でいうなれば、アセンブラの理解が大いにスキルアップに繋がることは間違いないでしょう。たとえ、直接アセンブラを使用してツールを作ろうと思っていなくとも、32ビット、さらには64ビットとコンピュータの進化が進んでいこうとも、コンピュータの根底で処理されているのはマシン語でありアセンブリ言語なのです。そして、もちろんWindowsでもアセンブラを学ぶことは可能です。アセンブラにはいろいろな種類がありますが、最初に学ぶにはコマンドプロンプトに付属しているdebugコマンドを利用するのが良いです。

では、いよいよアセンブラの世界へ足を踏み入れてみます。コマンドプロンプトを起動し、「debug」と入力してください。

debugコマンドの入力

コマンドプロンプトに「debug」と入力すると、コマンドプロンプトの表示エリアがクリアされ、以下のように表示されます。

debugコマンドの起動

まずは、ここまで表示されます。ちなみにこれはWindowsXPで行った結果なので、他の環境では少し違うかもしれませんが、似たようなものが表示されます。では、さっそくプログラムを書いていくことにします。最初に「A 0100」と入力してください。「A」の後には、プログラムを書き始めるメモリアドレスを指定します。通常は「0100」を指定します。

debug1

するとこのように表示されます。「34F2」の部分は毎回変わりますが、これはセグメントと呼ばれておりメモリアドレスを示しています。ちなみに「0100」もメモリアドレスを示しており、これはオフセットと呼ばれています。とりあえず、いまはこの部分は気にしないでください。では、ここから実際にアセンブラのプログラムを書いていきます。以下のように入力してください。

debug2

ここまでがアセンブラのプログラミングです。「34F2:0103」は改行のみ入力してください。次にこのプログラムを実行させたいので、以下のように入力します。

debug3

「G」には実行を開始するアドレスと、終了するアドレスを渡します。よって、上記の例では「0100」番地から「0103」番地までのプログラムを実行するという意味になります。プログラムの実行が終了するとレジスタの値が表示されます。注目すべき部分は最初の「AX=0050」です。これはAXという箱(レジスタ)に0050という値が代入されていることを示しています。つまり、「mov ax, 50」は、AXレジスタに50を代入するという命令だったわけです。

まず、「mov」は命令です。これは処理の内容を意味します。次の「ax」と「50」はオペランドと呼ばれます。左のオペランドを「デスティネーションオペランド」、右のオペランドを「ソースオペランド」と呼びます。つまりこの場合は「50」という値がソースオペランドとなります。

mov命令は「デスティネーションオペランドへソースオペランドを代入しろ」という命令です。よってこの1行の意味は「axへ50という値をmov(代入)する」ということになります。そして現に「AX=0050」というようにAXレジスタに50という値が代入されているのが分かります。

さて、アセンブラを扱うためにはコマンドプロンプトに「debug」と入力すればよかったわけですが、終了するためには「q」を入力してください。

debug4

これで無事debugコマンドを終了しました。これが一連のプログラミングの流れとなります。まず、「A 開始アドレス」と入力し、プログラムを書きます。書き終えたら「G=実行アドレス 終了アドレス」と入力してプログラムを実行し、最後に「q」を入力してdebugコマンドを終了します。

汎用レジスタ

レジスタとは、一言で言うならば「CPUの中にあるメモリ」です。一般的にメモリと呼ばれているものは正確にはメインメモリ(主記憶装置)と言い、コンピュータ内でデータやプログラムを記憶する装置のことを指します。それに対し、レジスタとは、CPU(Central Processing Unit)の中に組み込まれているメモリのことを指します。最近のメインメモリは、128MBや256MB、さらには512MBといった大量の記憶領域を持っています。それに比べて、CPUの中に搭載されているメモリであるレジスタは、ほんのわずかな記憶領域しか持っていません。しかし、レジスタはメインメモリよりもはるかに高速にアクセスできます。そしてアセンブラを学ぶこととは、まさに、レジスタの利用法を学ぶことに他なりません。

レジスタには、大きく分けて「汎用レジスタ」「フラグレジスタ」「命令ポインタ」「セグメントレジスタ」という4つの種類があります。まずはその中の1つの汎用レジスタと呼ばれるものをみてみます。

汎用レジスタ

汎用レジスタは6つあります。これらはすべて16ビット(2バイト)の領域を持っています。以下のプログラムをみてください。

-----  コマンドプロンプト
C:\DOCUME~1\kenji>debug
-A 0100
34D8:0100 mov ax, 1
34D8:0103 mov bx, 2
34D8:0106 mov cx, 3
34D8:0109 mov dx, 4
34D8:010C mov si, 5
34D8:010F mov di, 6
34D8:0112
-G=0100 0112

AX=0001  BX=0002  CX=0003  DX=0004  SP=FFEE  BP=0000  SI=0005  DI=0006
DS=34D8  ES=34D8  SS=34D8  CS=34D8  IP=0112   NV UP EI PL NZ NA PO NC
34D8:0112 0000          ADD     [BX+SI],AL                  DS:0007=FE
-----

最後に出力されたレジスタの情報をみてください。それぞれの汎用レジスタの値が表示されています。「AX=0001」「BX=0002」「CX=0003」「DX=0004」となっています。さらに「SI=0005」「DI=0006」も代入した値になっています。

汎用レジスタはプログラマが自由に利用してよいレジスタです。基本的にどんなことに利用しても構わないのですが、一応大まかな使用方法が定義されています。一般的に、BXレジスタはメモリのアドレスを入れるために使用するべきであり、CXレジスタはカウンタなどに利用するようです。AXレジスタとDXレジスタは共にデータを入れるために利用されます。SIレジスタとDIレジスタは、特に決められていないようですが、BXレジスタと同じような使い方をされることが多いです。

汎用レジスタはすべて16ビット(2バイト)です。そして、AX、BX、CX、DXの4つに限り、それぞれさらに2つに分けられます。どういうことかというと、以下のプログラムをみてください。

-----  コマンドプロンプト
C:\DOCUME~1\kenji>debug
-A 0100
34D8:0100 mov al, FF
34D8:0102 mov ah, 44
34D8:0104 mov bl, 12
34D8:0106 mov bh, 34
34D8:0108
-G=0100 0108

AX=44FF  BX=3412  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=34D8  ES=34D8  SS=34D8  CS=34D8  IP=0108   NV UP EI PL NZ NA PO NC
34D8:0108 800000        ADD     BYTE PTR [BX+SI],00         DS:3412=00
-----

AXレジスタに44FFという値が入っています。これはAXレジスタの上位8ビット(1バイト)はAHレジスタとして扱うことができるというわけです。同じようにALレジスタはAXレジスタの下位8ビット(1バイト)となります。これはAX、BX、CX、DXの4つのレジスタで同じことが言えます。図示すると以下のようになります。

汎用レジスタの関係

AX、BX、CX、DXはそれぞれサイズが16ビットですが、AH、AL、BH、BLなどといったものは8ビット(1バイト)です。汎用レジスタにはこのような特徴があります。ただしSI、DIといったレジスタには、8ビット単位のレジスタは存在しません。あくまでAX、BX、CX、DXの4つだけです。

加算命令

コンピュータとは「電子計算機」のことを意味します。そして、その名のとおりコンピュータは電子計算機です。よって加算や減算といった計算を行うことができます。では、汎用レジスタを使って加算を行うプログラムを作ってみます。

データの転送にはmov命令を使いました。この命令を使うことによってレジスタへ定数を代入したり、またメモリにレジスタの値を代入することができました。これと同じようにデータを加算する命令があります。それは、add命令です。まずは実際に加算を行ってみますので以下のプログラムを入力してください。

-----  コマンドプロンプト
C:\DOCUME~1\kenji>debug
-A 0100
34D8:0100 mov ax, 1
34D8:0103 mov bx, 3
34D8:0106 add ax, bx
34D8:0108
-G=0100 0108

AX=0004  BX=0003  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=34D8  ES=34D8  SS=34D8  CS=34D8  IP=0108   NV UP EI PL NZ NA PO NC
34D8:0108 0000          ADD     [BX+SI],AL                  DS:0003=9F
-----

add命令を使うと、最初に渡されたレジスタ(デスティネーションオペランド)に2番目に渡されたレジスタ(ソースオペランド)の値を加算します。つまりこの例でいうならば、AXレジスタの値に、BXレジスタの値が加算されることになります。axには1を代入し、bxには3を代入していますので、最終的にAXレジスタには4が入れられることになります。add命令が実行されてもBXレジスタの値は変化しませんので、BXレジスタの値は3のままです。

減算命令

同じようにして減算処理も行うことができます。減算はsub命令を使います。

-----  コマンドプロンプト
C:\DOCUME~1\kenji>debug
-A 0100
34D8:0100 mov ax, 5
34D8:0103 mov bx, 3
34D8:0106 sub ax, bx
34D8:0108
-G=0100 0108

AX=0002  BX=0003  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=34D8  ES=34D8  SS=34D8  CS=34D8  IP=0108   NV UP EI PL NZ NA PO NC
34D8:0108 0000          ADD     [BX+SI],AL                  DS:0003=9F
-----

axには5を代入し、bxには3が代入されています。sub命令を使って、AXレジスタの値からBXレジスタの値が減算されることになります。よってAXレジスタには2が入ることになります。

ここまでのテキストは「アセンブリ言語の教科書」の40ページ以降の原稿をWEB用にリライトしたものです。


16ビットNASMプログラミング

本格的にアセンブリ言語を学ぶためには、コンパイラが必要となります。世の中には様々なアセンブリ言語のコンパイラが存在しますが、本章では、現在もっとも多く利用されているアセンブラのひとつであるNASM(The Netwide Assembler)を使ってプログラミングを学んでいくことにします。

NASMとは、様々なファイルフォーマットに対応しているアセンブラであり、もちろんマイクロソフトの16ビットOBJファイル、およびWin32もサポートしています。さらに、シンプルなバイナリも出力することができ、その高い汎用性により、現在もっとも多く利用されているアセンブラのひとつです。

インストール

NASMは、以前までは「The Netwide Assembler : Home Page」からダウンロードできたはずなのですが、今はなぜかページを開いていも何も表示されないので、直接ダウンロードページへ進んでください。

NASMダウンロード1

NASMダウンロード2

NASMダウンロード3

NASMはオープンソースであり、Windows、Linuxを始めとしていくつものOSに対応しています。ここでは「Win32 binariess」をダウンロードしてください。その名の通りWindows用のNASMです。そして「nasm-0.98.39-win32.zip」をダウンロードします。

NASMダウンロード4

NASMダウンロード5

どこからダウンロードするのか、ということですが、特にどこでもよいです。適当なサーバを選んでダウンロードしてください。「Download」以下のリンクをクリックすると実際のダウンロードページへ進みます。そのまま待っていればダウンロードが始まりますが、もしいつまでたってもダウンロード確認のダイアログが表示されない場合は、リンクをクリックしてください。

ダウンロードが完了すると、nasm-0.98.38-win32.zipというファイルが作成されますので、解凍(展開)してフォルダを開いてください。そして、nasmw.exe、ndisasmw.exe、copyingという3つのファイルがあることを確認してください。これで無事インストール完了となります。

NASMファイル確認

解凍されたフォルダは特にどこに置いてもらってもかまいませんが、便宜上本書ではnasmw.exe、ndisasmw.exe、copyingの3つのファイルが入っているフォルダの名前を「nasm」とし、C:\以下に置いていると仮定して話を進めていきます。

コンパイル

インストールが完了したので、次は正確にコンパイルできるかどうかを試します。プログラムの内容はシンプルに「A」という文字を標準出力に表示するだけのプログラムとします。

-----  putchar.asm
    org 100H      (おまじない的なもの、詳細な説明は後ほど)
section .text     (おまじない的なもの、詳細な説明は後ほど)
start:            (ラベル、ここからプログラムがスタートする)
    mov ah, 02H   (AHレジスタに0x02を転送)
    mov dl, 41H   (DLレジスタへ0x41を転送)
    int 21H       (MS-DOSシステムコール呼び出し)
    mov ah, 4CH   (AHレジスタへ0x4Cを転送)
    int 21H       (MS-DOSシステムコール呼び出し)
-----
-----  コマンドプロンプト
C:\nasm>debug
-q
C:\nasm>nasmw putchar.asm -fbin -o putchar.com
C:\nasm>
-----

最初にdebugコマンドを起動してすぐに終了しているのは、コマンドプロンプトを16ビットモードに切り替えるためです。環境がWindows2000/XPの場合はこれが必要です。Windows9x系でのMS-DOSプロンプトでは必要ありません。Windows2000/XPに付属しているコマンドプロンプトは、通常32ビットモードで起動されます。しかし、私たちが作成しているアセンブラプログラムは16ビットCPU用に書かれてあるため正常にプログラムが動作しない場合があります。よって、あらかじめコマンドプロンプトを16ビットモードに切り替えてプログラムをコンパイルし実行しているわけです。最初にdebugコマンドを実行したら、あとはコマンドプロンプトを終了するまでは16ビットモードですので、コンパイル、実行を繰り返して構いません。以後、本書ではdebugコマンドの部分は省略します。

コンパイルには、まず「C:\nasm>nasmw <asmファイル>」と指定して「-fbin」オプションをつけてください。このオプションはバイナリファイルを作成するという意味ですが、いまは決まり事と考えてください。さらに「-o」オプションをつけた後、作成する実行ファイル名を指定します。作成する実行ファイル名の拡張子は必ず「.com」としてください。

putchar.com実行例

実行すると、標準出力に「A」という文字が表示されます。

今回使用したシステムコール

1文字出力       AH = 02H, 06H
                  02H Ctrl+Cを承諾
                  06H Ctrl+Cを拒否
                DL = 出力する文字
                戻り値:なし
プログラム終了  AH = 4CH
                AL = リターンコード
                戻り値:なし

マシン語とアセンブリ言語

マシン語(機械語)とは、CPUが直接解釈し実行できる言語です。数字の羅列で表現され、残念ながら人間が容易に理解できるような形式にはなっていません。しかしアセンブリ言語を学ぶのならば、マシン語に対してもある程度の知識が必要となります。よって、これから少しマシン語について見ていきたいと思います。では以下のプログラムを見てください。

-----  putchar.asm
    org 100H      (メモリアドレス100番地から始める)
section .text     (以降はテキストセクションであるという通知)
start:            (ラベル、ここからプログラムがスタートする)
    mov ah, 02H   (AHレジスタに0x02を転送)
    mov dl, 41H   (DLレジスタへ0x41を転送)
    int 21H       (MS-DOSシステムコール呼び出し)
    mov ah, 4CH   (AHレジスタへ0x4Cを転送)
    int 21H       (MS-DOSシステムコール呼び出し)
-----

上記のプログラムは、標準出力に「A」と出力するプログラムでした。まず最初の1行はこのプログラムをメモリアドレス0100番に書き込むということを示しています。COMファイルを作成する場合は、必ず0100番地から始めることになりますので、決まりごとと考えてもらっても構いません。ちなみにメモリの0000番地から00FF番地までには、OSによってプログラムに関する様々なデータが格納されます。NASMでは、数値を表す方法として10進数で表記することもできるので、10進数と区別するために、16進数の数値の場合は最後に「H」をつけることになっています。小文字の「h」でも良いのですが、本章では大文字の「H」を使うことにします。

AHレジスタに02Hを、DLレジスタに文字「A」を意味する41Hを格納して、システムコールを呼び出しているので、これは標準出力への文字の表示です。最後の2行は、プログラムを終了し、使用していたメモリを解放するシステムコールです。以後、NASMのプログラムでは必ず書くので、覚えておいてください。

では、この実行ファイル(putchar.com)をバイナリエディタで開いてください。

putchar.comのバイナリデータ

-----  putchar.com
B4 02 B2 41 CD 21 B4 4C CD 21
-----

このような数値の羅列が表示されます。マシン語は人間には容易に理解できないものですが、アセンブリ言語ならばアルファベットですので、簡単に理解することができます。そしてマシン語とアセンブリ言語は、命令が1対1に対応しています。例えば、putchar.asmの4行目「mov ah, 02H」は、マシン語では「B4 02」に対応しています。さらに厳密にいえば「mov ah」が「B4」に対応しているわけです。同じようにputchar.asmの5行目「mov dl, 41H」は、マシン語では「B2 41」に対応しています。つまり「mov dl」が「B2」に対応していることになります。さらに6行目「int 21H」は「CD 21」に対応しています。ここまで分かれば、7行目と8行目も理解できるでしょう。以下にプログラムとの対応を示します。

マシン語との対応表

さて、アセンブリ言語との対応表が分かれば、マシン語がどのようなものか、ほんの少し見えてくるのではないでしょうか。以下のようなプログラムを書いてみます。

-----  test01.asm
    org 100H
section .text
start:
    mov al, 01H    (ALレジスタに0x01を転送)
    mov cl, 01H    (CLレジスタに0x01を転送)
    mov dl, 01H    (DLレジスタに0x01を転送)
    mov bl, 01H    (BLレジスタに0x01を転送)
    mov ah, 02H    (AHレジスタに0x02を転送)
    mov ch, 02H    (CHレジスタに0x02を転送)
    mov dh, 02H    (DHレジスタに0x02を転送)
    mov bh, 02H    (BHレジスタに0x02を転送)
    mov ax, 03H    (AXレジスタに0x03を転送)
    mov cx, 03H    (CXレジスタに0x03を転送)
    mov dx, 03H    (DXレジスタに0x03を転送)
    mov bx, 03H    (BXレジスタに0x03を転送)
-----

プログラムの内容はただmov命令を使って、値を代入しているだけです。実行するプログラムではないので、最後の終了システムコールは省略しました。では、このプログラムをコンパイルして、バイナリエディタで開いてください。くれぐれも実行はしないでください。

-----  コマンドプロンプト
C:\nasm>nasmw test01.asm -fbin -o test01.com
C:\nasm>
-----

test01.comのバイナリデータ

test01.comをバイナリエディタで開いてみると、このようなマシン語が生成されます。さて、最初はALレジスタの操作ですが、これは「B0」となっています。次のCLレジスタの操作では「B1」です。さらに次のDLレジスタは「B2」であり、BLレジスタは「B3」と対応しています。このように見ていくと「B0」から「B3」まではALレジスタ、CLレジスタ、DLレジスタ、BLレジスタまでが対応しており「B4」から「B7」まではAHレジスタ、CHレジスタ、DHレジスタ、BHレジスタまでが対応していることになります。そして「B8」から「BB」までは、AXレジスタ、CXレジスタ、DXレジスタ、BXレジスタまでが対応しています。以下に、命令に対応するマシン語を示します。

マシン語との対応表2

マシン語は確かに容易には理解できません。しかし、決して暗号化されているわけではなく、読もうと思えば読めないことはないのです。そして、その気になれば、マシン語でプログラミングを行うことも可能です。

マシン語でプログラミング

では、実際にマシン語でプログラミングをやってみることにします。最初のマシン語プログラムとして、標準入力から文字を受け取るだけのプログラムを作成してみます。標準入力から文字を受け取るシステムコールを利用するには、AHレジスタを01Hとしてint命令を呼びだします。入力された文字はALレジスタに格納されます。

では、まずはバイナリエディタを起動してください。最初にやるべきことはAHレジスタに01Hを格納することです。よって「B4 01」と入力します。次にint命令を呼び出します。「int 21H」は「CD 21」でした。これで標準入力から文字を受け取るシステムコールを呼び出しました。終了プログラムも書かなければなりませんので、AHレジスタに4CHを格納します。「B4 4C」そしておなじみの「CD 21」を書いて終了です。よってプログラムは以下になります。

-----  test02.com
B4 01 CD 21 B4 4C CD 21
-----

入力を受け付けるプログラム(test02.com)

では、このプログラムをtest02.comというファイル名で保存し、実行してください。直接マシン語で記述しているので、コンパイルやリンクといったことは必要ありません。まさにコンピュータが解釈し実行できる言語です。

test02.comの実行結果

見事にマシン語でプログラミングを行うことができました。ちなみに上記のマシン語は以下のアセンブラ命令に対応しています。

test02.comのマシン語の対応表

つまり、test02.comをNASMで書くとしたら、以下のようになるということです。

-----  test02.asm
    org 100H
section .text
start:
    mov ah, 01H    (B4 01)
    int 21H        (CD 21)
    mov ah, 4CH    (B4 4C)
    int 21H        (CD 21)
-----

さて、マシン語で最初のプログラムを書きましたが、このプログラムでは、コンピュータからのレスポンスがなく何か物足りません。よって、次はユーザが入力した文字を受け取り、その文字の次の文字を出力するように改良してみます。例えばユーザが「a」と入力したら「b」と出力し、「4」と入力したら「5」と出力するようなプログラムを作ります。

まずマシン語の最初の4バイトですが、標準入力から文字を受け取らなければならないので「B4 01 CD 21」となります。このシステムコールにより、入力された文字はALレジスタに格納されているので、その文字を次の文字にするためにインクリメントします。しかし、ALレジスタをインクリメントする命令が分かりません。もちろん実際にNASMで書いてコンパイルし、バイナリエディタで実行ファイルを閲覧すれば良いのですが、それは少々面倒くさいので、ここではdebugコマンドを使います。

-----  コマンドプロンプト
C:\nasm>debug
-A 0100
34D6:0100 inc al
34D6:0102
-U 0100 0100
34D6:0100 FEC0          INC     AL
-----

「-U」コマンドは、逆アセンブルを行うコマンドです。最初に「-A」コマンドでプログラムを書いて、次に「-U」コマンドで逆アセンブルすることにより、マシン語とそれに対応するアセンブリ命令が出力されます。「-U」コマンドは以下のような定義になっています。

-----
-U [開始アドレス] [終了アドレス]
-----

逆アセンブルを開始するアドレスと、逆アセンブルを終了するアドレスを引数として指定してやれば、そのアドレス間の命令が逆アセンブルされます。上記に例では、inc命令に対応するマシン語が知りたかったので「-U 0100 0100」というように開始アドレスと終了アドレスを同じにしています。

さて、これにより「inc al」は「FF C0」に対応することが分かりました。よって標準入力から文字を受け取るマシン語命令と合わせて、プログラムは「B4 01 CD 21 FF C0」となりました。次は文字を出力したいので、AHレジスタに02Hを格納し、ALレジスタの値をDLレジスタに格納します。まずAHレジスタに02Hを格納するマシン語は、「B4 02」です。ALレジスタの値をDLレジスタに格納するマシン語は分からないので、再びdebugコマンドを使って調べてみます。

-----  コマンドプロンプト
C:\nasm>debug
-A 0100
34D6:0100 mov dl, al
34D6:0102
-U 0100 0100
34D6:0100 88C2          MOV     DL,AL
-----

debugコマンドの逆アセンブル結果から「mov dl, al」は「88 C2」ということが分かりました。この命令を実行した後、システムコールを呼び出す「CD 21」を追加し、文字の出力が完了します。

最後にプログラムの終了を意味する「B4 4C CD 21」を追加してマシン語入力は終了です。以下が、キー入力を受け付け、入力された次の文字を出力するマシン語プログラムです。

-----  test03.com
B4 01 CD 21 FF C0 B4 02 88 C2 CD 21 B4 4C CD 21
-----

test03.comのマシン語プログラム

test03.comの実行結果

バイナリエディタで上記のプログラムを入力して、test03.comというファイル名で保存し、実行してください。実行後、キー入力待ちとなりますので、何かのキーを入力してください。「e」と入力すると「f」と出力され、「6」と入力すると「7」と出力されます。このように入力した文字の次の文字が出力されます。ちなみにtest03.comの対応アセンブラ命令は以下です。

test03.comのマシン語の対応表

これは、debugコマンドでも確認することができます。

-----  コマンドプロンプト
C:\nasm>debug test03.com
-U 0100 010E
3528:0100 B401          MOV     AH,01
3528:0102 CD21          INT     21
3528:0104 FFC0          INC     AX
3528:0106 B402          MOV     AH,02
3528:0108 88C2          MOV     DL,AL
3528:010A CD21          INT     21
3528:010C B44C          MOV     AH,4C
3528:010E CD21          INT     21
-----

このように、アセンブラ命令とマシン語は1対1で対応しています。マシン語は確かに容易には解読できませんが、やろうと思えば書けないことはありません。現にコンパイラなどがなかった時代では、ハンドアセンブルといって、プログラマが手動でアセンブラ命令をマシン語へ変換していたこともあったようです。

しかし、何かのツールを作成しようとしたときに、直接マシン語で書くよりもアセンブリ言語で記述する方が簡単であることはいうまでもありませんし、さらにはCやJavaといった高級言語で書いたほうが、より実用的であることは間違いありません。しかし、高級言語で書いたほうが実用的であるからといって、アセンブリ言語を学ばなくて良いとわけではありません。プログラミング言語にも向き不向きがあります。アセンブラを使わなければ実現できない処理もありますし、Cを使ったほうが効率的に組めることも多々あります。適材適所という言葉が言い表しているように、どういったことをやりたいか、という要望に合わせてプログラミング言語を選べることに越したことはないでしょう。もしかしたら、マシン語の知識が必要になることもあるかもしれません。

今回使用したシステムコール

1文字入力    AH = 01H, 06H, 07H, 08H
               01H 入力を待ち入力された文字をディスプレイに表示する。Ctrl+C承諾
               06H 入力を待たず入力された文字をディスプレイに表示しない。Ctrl+C拒否
               07H 入力を待ち入力された文字をディスプレイに表示しない。Ctrl+C拒否
               08H 入力を待ち入力された文字をディスプレイに表示しない。Ctrl+C承諾
             DL = AHレジスタが06Hの場合のみFFHを指定
             戻り値:AL 入力文字(入力がなかった場合は00Hが設定される)

文字列の出力

いよいよ、プログラミングを学ぶ際のお約束である「Hello World!」の出力を行うプログラムを作成します。通常のプログラミング言語だと一番最初の例題として紹介されるような内容のプログラムですが、アセンブラだとやっとここまできたかという感じがします。では以下のプログラムをみてください。

----- hello.asm
    org 100H
section .text
start:
    mov ax, 0200H
    mov bx, HELLO
PRINT:
    mov dl, [ds:bx]
    cmp dl, 00H
    jz PRINTEND
    int 21H
    inc bx
    jmp PRINT
PRINTEND:
    mov ah, 4CH
    int 21H

section .data
HELLO db 'Hello World!', 00H
-----

まず最初のAXレジスタに0200Hを入れているのは、AHレジスタに02Hを格納するためです。別に「mov ah, 02H」と書いても良いのですが、下の1行も2バイト移動なので読みやすくするために合わせただけです。BXレジスタには「Hello World!」文字列のアドレスを格納します。

「PRINT:」はラベルです。debugコマンドの8086アセンブラでは、jmp命令やloop命令などには直接アドレスを指定していましたが、NASMではラベルを定義して、そのラベルをアドレスのように扱います。とりあえずこの行では定義しているだけですので、処理としては何もしません。PRINTラベル以下に進みます。まずDLレジスタには出力したい文字を指定します。BXレジスタにはHELLOのアドレスが格納されているので、その値を表示します。よって、最初は「H」が表示されることになります。dsはデータセグメントですが、comファイルだと、DSレジスタ、SSレジスタ、CSレジスタはすべて同じ値なので、どのセグメントでも関係ありません。結局「ds:bx」は「Hello World!」のアドレスを示すことになります。

AHレジスタには02Hが入っているので、システムコールが呼ばれたら1文字出力されます。最初は「H」が出力されることになります。そしてbxをインクリメントし「ds:bx」が「H」の次の「e」を示すようにして、PRINTラベルへジャンプします。「ds:bx」は「e」を示しているので今度は「e」が出力され、以後、同じ処理がDLレジスタが00Hになるまで続きます。DLレジスタが00HになるとPRINTENDへジャンプし、プログラムを終了します。では、コンパイルしてください。実行すると「Hello World!」という文字列が出力されます。

hello.comの実行例

ではこのプログラムをバイナリエディタで開いてください。「Hello World!」という文字列は最後の00Hも合わせると、全部で13バイトです。hello.comのバイナリであるマシン語は、もちろんそれぞれのアセンブリ命令に対応しているわけですが、最後の13バイトをみてください。ASCIIコードと対応させると分かりますが、それぞれが「Hello World!」という文字列を表しています。

hello.comのマシン語

-----  hello.comの最後の13バイト
48 65 6C 6C 6F 20 57 6F 72 6C 64 21 00
 H  e  l  l  o     W  o  r  l  d  !   
-----

つまり、NASMでは「HELLO db 'Hello World!', 00H」というように変数を定義すると、実行ファイルの最後にその文字列が追加されます。ポイントは「section .data」であり、これは以下のプログラムはデータセクションであるということを意味しています。この部分に書かれるプログラムは、実行すべきマシン語ではなく、データとして扱われるマシン語であるということです。そして、データであるその文字列をプログラムが参照することになるわけです。プログラム中では「HELLO」は文字列のアドレスを表します。次の「db」は1バイトの羅列であることを表します。つまり確保されるメモリ領域の単位が1バイトであるということです。そしてそのメモリへ「'Hello World!', 00H」が連続して格納されることになります。これはHELLOという名前のchar型(1バイト)配列を用意したと考えれば分かりやすいと思います。

ここまでのテキストは「アセンブリ言語の教科書」の90ページ以降の原稿をWEB用にリライトしたものです。


GNU(Linux)アセンブラプログラミング

GNUとは「Gnu's Not Unix」の略であり、Richard Stallman氏を中心としたFSF(Free Software Foundation)によって作られているUNIX互換ソフトウェアシステムの名称です。そして、Stallman氏の「優れたソフトウェアはフリーであるべき」という思想に基づき、GNUに含まれるソフトウェアはすべてフリーソフトとなっています。

この思想に基づいて作られたアセンブラが「The GNU Assembler」であり、通称GASと呼ばれています。GASはDean Elsner氏、Jay Fenlason氏を中心に開発されたアセンブラであり、AT&T形式に従うアセンブラです。NASMやMASM、TASMといったアセンブラはすべてIntel形式の記述方法であるのに対し、GASはAT&T形式の記述法であるため、最初はすこし戸惑うかもしれませんが、それぞれの記述法に慣れればどちらの記述法になっても気にはなりません。

Linux環境でもNASMを使用してプログラミングを行うことは可能です。しかし、gccがLinuxの主流コンパイラであるので、GASを覚えておいた方が何かと便利です。よって、この章では、GASを利用したプログラミングを学んでいくことにします。

GDBの使い方

Linuxでアセンブラプログラミングを行う前に、まずGDBの使い方を解説します。

GDBとは

GDBは「The GNU Source-Level Debugger」の略でありGNUが提供する有名なデバッガです。GDBはフリーソフトであり、著作権はGCCと同様にGNU GPLにより規定されています。世の中にはプログラム開発を手助けする様々なデバッガがありますが、GDBはUNIX系OSではもっともよく使われているデバッガです。しかし、GDBはCUI環境でのコマンド形式のデバッガであるため、GUI環境に慣れている人にとっては少々使いにくいものになっています。よって、そのGDBの簡単な解説を行います。

デバッグの流れ

まずは以下のプログラムをみてください。

-----  ex1.s
.text
.globl main

main:
    movl $1, %eax
    movw $0xFFFF, %ax
    movb $0b01010101, %ah
    ret
-----

eaxレジスタに1を転送し、そのあとaxレジスタに0xFFFFを転送し、そしてahレジスタに0b01010101を転送するだけのプログラムです。「$」から始まるのは既値であり、最初に「0x」がついたら16進数、「0b」がついたら2進数を意味します。では、この処理の1命令ずつみていくためにgdbを使ってex1を実行することにします。

-----  terminal
$ gcc -Wall ex1.s -o ex1
$ gdb ex1
GNU gdb 5.0
Copyright 2000 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License,
and you are welcome to change it and/or distribute copies of it 
under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for 
details. This GDB was configured as "i386-redhat-linux"...
(gdb)
-----

まずはex1.sをコンパイルします。そしてgdbの引数にそのプログラムを渡してgdbを実行します。すると上記のような文字列が表示されます。gdbはフリーソフトウェアであり、GNU GPLライセンスに基づくといったことが書かれてあります。

では、さっそくgdbを使ってプログラムex1を解析していきます。まずはmain関数にブレイクポイントを仕掛けます。

-----  terminal
(gdb) break main
Breakpoint 1 at 0x80483c0
-----

「break main」と入力することでブレイクポイントを仕掛けることができます。するとアドレス0x80483c0にブレイクポイントを設置したむねが表示されます。では、プログラムを実行します。gdb上でプログラムを実行するためには「run」と入力します。

-----  terminal
(gdb) run
Starting program: /home/kenji/book2/li_gcc/ex1 

Breakpoint 1, 0x080483c0 in main ()
-----

「run」と入力すると実行されたプログラムのパスが表示されます。そして、あらかじめ仕掛けておいたブレイクポイントで処理が停止したことが分かります。では、main関数から1命令だけ進めてみます。つまり「movl $1, %eax」を実行させます。1命令だけ実行させるためには「stepi」を使います。

-----  terminal
(gdb) stepi
0x080483c5 in main ()
(gdb) display/x $eax
1: /x $eax = 0x1
-----

「stepi」と入力すると、どの部分が実行されたのかを示すアドレスが表示されます。上記の例では、0x080483c5のアドレスにある命令が実行されたようです。eaxレジスタの値を参照するためには、「display/x $eax」と入力します。すると、eaxレジスタの値が0x1であることが分かります。つまり、「movl $1, %eax」が実行されていることが確認できます。では、次の命令も実行させます。さらに「stepi」と入力してください。

-----  terminal
(gdb) stepi
0x080483c9 in main ()
1: /x $eax = 0xffff
-----

次は「display/x $eax」を入力しなくてもレジスタの値が表示されます。みての通り、「movw $0xFFFF, %ax」が実行され、eaxレジスタの値が0xffffになっています。では、さらに処理を進めて下さい。

-----  terminal
(gdb) stepi
0x080483cb in main ()
1: /x $eax = 0x55ff
-----

同じく次の命令の「movb $0b01010101, %ah」が実行され、eaxレジスタの値が0x55ffとなっています。2進数の01010101は16進数では0x55です。そして、ahレジスタはaxレジスタの上位8ビットなので、結果的にeaxは0x55ffとなるわけです。当たり前ですが、正確にプログラムの処理が実行されているのが分かります。最後にgdbを終了するためには「quit」もしくは「q」と入力します。

-----  terminal
(gdb) quit
The program is running.  Exit anyway? (y or n) y
$
-----

「現在プログラムが実行されていますが、本当に終了しますか?」といった確認メッセージが表示されますが、終了するので、「y」と入力してください。これでデバッグは終了です。

コマンドの説明

一連のデバッグの流れをみていきましたが、その中で使ったコマンドがいくつかありました。それを以下に列挙します。

GDBコマンド1

breakはブレイクポイントを設置します。フォーマットは「break ラベル名」となります。runはプログラムを始めから実行するコマンドです。ブレイクポイントを設置していなければ、プログラムの終了、もしくはセグメント違反などの何かしらのエラーまで処理を進めます。displayコマンドは現在のレジスタの値を表示します。フォーマットは「display/FMT DATA」です。FMTには「x」「t」「o」などを入れることができ、それぞれレジスタの値を16進数で表示させるか、2進数で表示させるか、8進数で表示させるかを決めることができます。FMTを指定しなかったら、10進数で表示されます。stepiコマンドは、1命令のみ実行させるコマンドです。quitはgdb自体を終了します。

以上が、ex1のデバッグ時に使用したコマンドです。ただし他にも重要なコマンドがいくつかありますので、それを説明します。

GDBコマンド2

GDBコマンド3

まず、breakコマンドによって設置したブレイクポイントを解除するclearコマンドです。フォーマットはbreakコマンドと同じで「clear LABEL」となります。nextiコマンドは、ネクスト実行となります。stepiと同じで1命令を実行するコマンドですが、nextiはcall命令もひとつの命令として実行します。しかし、stepiはcall命令により呼ばれた先のアドレスまで進みます。そこがstepiとnextiの違いです。

stepiとnexti

つまり、stepiは別の関数にある命令にも進みますが、nextiは現在の関数にある命令しか実行しません。よって、call命令がひとつの命令として処理されるわけです。

continueは現在位置から次のブレイクポイントまで一気に実行します。もしブレイクポイントがなければプログラムは終了します。xはADDRで指定されたアドレスの値を表示します。USIZEは何バイト分表示させるかを指定します。そしてprint命令は、レジスタ、変数、配列の内容を表示します。FMTオプション、DATAオプションはそれぞれdisplayコマンドと同じように使用することができます。

Hello World!

では、いよいよGASを使ってのアセンブラプログラミングを行っていきます。まずは、お馴染の「Hello World!」を表示するプログラムを作成します。以下のプログラムをみてください。

-----  ex2.s
.data
hello:   .string "Hello World!\n"

.text
.globl main
main:
    pushl %ebp
    movl %esp, %ebp
    pushl $hello
    call printf
    leave
    ret
-----
-----  terminal
$ gcc -Wall ex2.s -o ex2
$ ./ex2
Hello World!
$ 
-----

みての通り「Hello World!」と表示するプログラムです。といってもやっていることはC言語のプログラムとほとんど同じです。スタックに「Hello World!」という文字列のアドレスをpushしてprintf関数をcallすることで文字列が出力されます。.data部分はデータセクションです。そして.text部分がコードセクションです。データセクションではhelloというラベルを作り、そこから文字列(.string)として「Hello World!」というデータを作っています。よって、helloというラベルが文字列のアドレスとして機能することになります。それをスタックにpushすることによってprintf関数に渡し、文字列を出力しています。そして、最後のleave命令とret命令ですが、leave命令は大抵ret命令の前に置かれます。leave命令が行うことは以下のプログラムと同じです。

-----
movl %ebp, %esp
popl %ebp
-----

つまり、mainラベル以降の最初の2行である「pushl %ebp」「movl %esp, %ebp」と同じことをやっているわけです。そして、espレジスタが指しているアドレス、つまりスタックのトップには呼び出し元のアドレスがあるので、そこにret命令によってジャンプしているということです。

数値カウントプログラム

今度は数値をカウントするプログラムを作成します。ここでは、実際にC言語でいうローカル変数のためのメモリ確保をアセンブリ言語でやってみます。

-----  ex3.s
.data
count:   .string "count: %d\n"

.text
.globl main
main:
    pushl %ebp
    movl %esp, %ebp
    subl $4, %esp

    xor %eax, %eax
count_loop:
    movl %eax, -4(%ebp)
    pushl %eax
    pushl $count
    call printf
    movl -4(%ebp), %eax
    inc %eax
    cmpb $5, %al
    jnz count_loop

    leave
    ret
-----

まず最初にお決まりのebpレジスタをスタックにpushして、espレジスタの値をebpに転送しています。そして、espを4減算しています。この状態のメモリ領域は以下のように変化します。

espとebpの関係1

まずは上記のような状態です。%espにある08a00001は呼び出し元のアドレスです。call命令はスタックに呼び出し元のアドレスをpushして関数にジャンプするため、関数実行の直前は必ずスタックトップに呼び出し元のアドレスが格納されていることになります。そして、ebpレジスタをスタックにpushして、espレジスタの値をebpに転送すると以下のように変化します。

espとebpの関係2

ffffc000は前の関数が使っていたebpレジスタの値です。これをスタックに保存した状態で、espレジスタとebpレジスタが同じ値となります。この状態で、今度はespレジスタから4減算します。すると以下のように変化します。

espとebpの関係3

さて、espレジスタはスタックのトップを示します。つまり、push命令が実行されたら、espレジスタが指しているアドレスの次(低位)のアドレスにデータが格納されることになり、pop命令が実行されたら、espレジスタが指しているアドレスのデータがpopされるわけです。もちろんそれぞれの命令によりespレジスタの値も変化します。ここで、もしpush命令が実行されたら、espレジスタの次(低位)のアドレスであるfffffb0cにデータが格納されることになります。つまり以下のようになります。

espとebpの関係4

仮にpushされたデータを10としていますが、何でも構いません。問題はスタックの状態です。以後スタックはfffffb08、fffffb04というようにアドレス低位の方向に伸びていくわけですが、ここで、espを4減算したためにアドレスfffffb10にフリーのメモリ領域ができました。

さて、ex3.sのcount_loopラベル以降をみてください。「movl %eax, -4(%ebp)」という命令があります。これはeaxレジスタの値をebpレジスタの値から4バイト減算したメモリアドレスへ転送することを意味しています。つまりfffffb10にeaxのデータを格納するということです。当然プログラムex3.sの最初のeaxは0ですので、0を格納すると以下のようになります。

espとebpの関係5

スタックはメモリ低位の方向へ伸びていくため、アドレスfffffb10の値がスタックに影響されることはありません。つまり自由に使用できるメモリ領域ができたことになります。さらに、この関数が終了するとleave命令により、現在のebpの値がespとなり、あらかじめスタックにpushしていたffffc000という値が%ebpへpopされるため、自動的に呼び出し前の状態に戻ります。

espとebpの関係6

espとebpの関係7

この時点でespレジスタが指しているアドレスはfffffb18となり、ret命令が実行されます。ret命令が実行されたら、スタックトップの値である呼び出し先アドレス08a00001へ処理がジャンプして、esp、ebpレジスタが共に元の状態に戻ってmain関数終了となります。

さて、プログラムの解説に戻ります。count_loopラベル以降の処理は以下のようになっています。

-----
count_loop:
    movl %eax, -4(%ebp)
    pushl %eax
    pushl $count
    call printf
    movl -4(%ebp), %eax
    inc %eax
    cmpb $5, %al
    jnz count_loop
-----

このプログラムは、ebxレジスタやecxレジスタなどの他のレジスタを使っても実現できるので、わざわざメモリを使う必要はないのですが、メモリ領域のアクセスの仕方を学ぶためにも、確保したメモリを参照しています。まず最初に、メモリ領域に0を格納して、printf関数を呼び出すために、引数を逆順にスタックへpushしています。関数をcallするとその戻り値がeaxレジスタに格納されるため、eaxレジスタの値は変更されています。よって、メモリからeaxレジスタにデータを転送して、現在のカウント値を復元します。そしてそのデータをインクリメントし、cmp命令によって5であるかどうかを判別します。もし5ならば通過し、5じゃないならばjnz命令によりcount_loopへとジャンプします。このようにして0から4までの数値がカウントされます。では、プログラムを実行してください。

-----  terminal
$ gcc -Wall ex3.s -o ex3
$ ./ex3
count: 0
count: 1
count: 2
count: 3
count: 4
$ 
-----

このように表示されます。数値をカウントしながら、標準出力へ表示しています。

ここまでのテキストは「アセンブリ言語の教科書」の222ページ以降の原稿をWEB用にリライトしたものです。


Windowsアセンブラプログラミング

GUI(Graphical User Interface)とは、ユーザに対する情報の表示にグラフィックを多用し、大半の基礎的な操作をマウスなどのポインティングデバイスによって行なうことができるユーザインターフェースのことです。現在では、GUIを利用するための基本的なプログラムをOSが提供することにより、開発負担の軽減などが図られています。Microsoft社のWindowsももちろんGUIを利用するためのAPI(Application Program Interface)が提供されています。

さて、これまではNASMでCUI(Character-based User Interface)プログラムを作成してきました。しかし、せっかくWindowsでプログラミングを行っているので、ここからはGUIプログラムの作成に挑戦することにします。C/C++言語をはじめとした多くの言語で書かれたプログラムも、最終的にはマシン語に変換されているわけですので、アセンブラ(NASM)でGUIプログラムを作成することももちろん可能です。ただし、Windowsプログラミングは一般的にWindowsAPIと呼ばれるインターフェースを利用して行っていきます。よって、アセンブリ言語の知識ももちろんですが、それよりもWindowsAPIの知識が必要となります。WindowsAPIの解説ももちろん行っていきますが、それを詳細に説明していくと本来のアセンブラという内容から大きく外れてしまうので、この章では、とりあえずある程度のWindowsプログラミングの経験がある方を対象とさせていただきます。

リンカ

Windowsプログラミングを行うためにはNASMの他にリンカが必要になります。「リンカとは?」と思われるかもしれませんが、その前にまずコンパイルの定義を示します。コンパイルとは、アセンブリ言語やC言語によって記述された複数のソースファイルをオブジェクトファイルに変換することです。そしてこれを行うソフトをコンパイラと呼びます。コンパイルすることによって作成されたオブジェクトファイルは、もちろん実行ファイルではないため実行できません。このオブジェクトファイルを実行可能なプログラムの形式に変換することをリンクと呼びます。そして、リンクを行うソフトをリンカと呼びます。

オブジェクトファイルは大きく分けて2種類あります。ひとつは単独のソースファイルに対応する形式でオブジェクトファイルと呼ばれます。このファイルの拡張子は、標準で.objです。もうひとつは、複数のオブジェクトファイルをまとめて一つのファイルにしたもので、ライブラリファイルと呼ばれます。このファイルの拡張子は標準で.libです。大規模なプログラムを作成する場合、複数のソースファイルを作成して、ひとつのプログラムを作ります。そのため、ソフトウェアの開発時は複数のソースファイルをそれぞれがコンパイルしておき、最後にリンカを使って、すべてのオブジェクトファイル、もしくはライブラリファイルを結合して実行ファイルを作成することになります。つまり、プログラム(実行ファイル)を作成する全体的な流れは以下のようになります。

コンパイルとリンク

コンパイルは、これまで通りnasmw.exeが行ってくれますが、リンクは残念ながらできません。そのため新しくリンカをダウンロードしなければなりません。NASMと相性のよいリンカにalinkがあります。本書ではこれを使うことにします。以下のページからダウンロードすることができます。

http://alink.sourceforge.net/

「Downloads」というリンクをクリックするとダウンロードページへ進めます。

alinkダウンロードページ

いくつかリンクがありますが、ダウンロードすべきなのは2つです。まず「Download ALINK (Win32 version)」というリンクをクリックしてください。これがWindows用のリンカです。「al_al.zip」というファイルを解凍したら、フォルダの中に「ALINK.EXE」というファイルが存在することを確認してください。他にもいろいろとファイルがありますが、これらはサンプルプログラムですので気にしないでください。重要なのは「ALINK.EXE」だけですので、このファイルをnasmw.exeと同じフォルダにコピーしてください。

次に同じページから「Download my Win32 Import library (win32.lib)」をクリックしてください。ダウンロードすると「win32lib.zip」というファイルが作成されますので、これを解凍してください。すると、「WIN32.LIB」というファイルが出来上がります。これも、nasmw.exeやALINK.EXEと同じフォルダへコピーしてください。これで準備完了です。

Hello World!

やはり最初はお約束の「Hello World!」を出力するプログラムを作成することにします。もちろん、出力といってもGUIプログラムなので、コマンドプロンプトの標準出力へ表示するわけではなく、メッセージボックスを使って表示させることにします。では、以下にプログラムを示します。

-----  hello32.asm
extern MessageBoxA

section .text
global main

main:
    push dword 0
    push dword title
    push dword text
    push dword 0
    call MessageBoxA
    ret

section .data
title: db 'Message', 0
text:  db 'Hello World!', 0
-----

まずはコンパイルしてください。コンパイルするにはnasmw.exeとalink.exeを以下のように実行します。

-----  コマンドプロンプト
C:\nasm>nasmw -fwin32 hello32.asm
C:\nasm>alink -oPE hello32 win32.lib -entry main
ALINK v1.6 (C) Copyright 1998-9 Anthony A.J. Williams.
All Rights Reserved

Loading file hello32.obj
Loading file win32.lib
matched Externs
matched ComDefs
Generating PE file hello32.exe

C:\nasm>
-----

hello32.exe実行例

実行すると「Hello World!」という文字列がメッセージボックスに表示されます。コマンドプロンプト上で作成していたプログラムとは性質が違っています。まず、MessageBoxAはWindowsのAPI(Application Programming Interface)のひとつであり、これを呼び出すことによってメッセージボックスを表示しています。

Windowsプログラミングにおいて、APIを呼び出すためには、そのAPIの引数をスタックに逆順に詰め込んでからAPI自身をcallすることによって実行させます。つまりhello32.asmの場合は、push命令によってスタックにデータを格納し、MessageBoxAを呼び出しています。MSDN(http://msdn.microsoft.com/)では、MessageBox関数は次のように定義されています。ちなみにMessageBox関数はWindows内部でASCIIコード用のMessageBoxAと、UNICODE用のMessageBoxWに分けられています。

-----  MessageBox
int MessageBox(HWND hWnd,         // ウィンドウハンドル
               LPCTSTR lpText,    // 本文として表示するテキスト
               LPCTSTR lpCaption, // タイトルに表示するテキスト
               UINT uType         // メッセージボックスのタイプ
              );
-----

MessageBox関数の定義とhello32.asmのスタックへ格納した順番を比べてください。

-----
push dword 0     // メッセージボックスのタイプ
push dword title // タイトルに表示するテキスト
push dword text  // 本文として表示するテキスト
push dword 0     // ウィンドウハンドル
-----

タイトルと本文をみることによって定義されている順番とは逆にスタックに詰め込まれているのが分かります。Windows上でのアセンブラプログラミングはこのようにして作成していくことになります。

ここまでのテキストは「アセンブリ言語の教科書」の320ページ以降の原稿をWEB用にリライトしたものです。


Copyright (C) 2003-2006 Kenji Aiko All Rights Reserved