アセンブリ言語入門
注意:以降、名前: ex0
などと名前が付いている例はプロジェクトを新しく作り、指定された名前を付け必ずプログラムを打ち込み実際に実行してください。
提出方法はこのページの下にあります。
名前のついたプロジェクトは後で全て提出します。全部で6つあります。
機械語はCPUが理解できる言葉なので、CPUの種類ごとに異なります。ここではインテルの8086系のCPUについて説明します。
皆さんが使っている殆どのコンピュータは8086系のCPUを採用しています。
機械語がCPUの種類ごとに異なるといっても、基本的な構造は似ているので、ひとつの種類をマスターすると他の種類のCPUの機械語も理解するのは容易です。
レジスタとメモリ
メモリ
機械語やデータなどはすべてメモリに配置されます。メモリは下の図のようなアドレスとその内容が入る場所の2つ
組の配列が並んでいるイメージになります。
アドレス(32ビット) |
内容(8ビット) |
中略 |
00A00000 |
FF |
00A00001 |
10 |
00A00002 |
0A |
中略 |
これは、あくまでも例です。この例では3つの配列ですが、実際にはこのセルは上下にずっと続いています。内容の部分は8ビットです。よって16進数で2桁の数が入ります。アドレスには連続した値が入ります。
レジスタ
CPUにはデータを操作するための変数のようなレジスタと呼ばれるデータを格納する場所がいくつか用意されています。一般的なプログラム言語の変数はプログラマが自由に名前や用途を決めることができ
数にも制限はありませんが、レジスタはCPUの中にあらかじめ決められた個数だけ用意されていて名前とその用途も決まっています。
メモリとレジスタの違いに注意してください。メモリはパソコンの組み立てのときに見たようにCPUの外側にある装置です。レジスタはCPUの内部にありメモリよりもはるかに高速に動作します。また、記憶だけではなくデータを操作するためにも使います。
レジスタには以下のようなものがあります。
64 ビット |
32 ビット |
16 ビット |
8 ビット |
RAX |
EAX |
AX |
AH, AL |
RBX |
EBX |
BX |
BH, BL |
RCX |
ECX |
CX |
CH, CL |
RDX |
EDX |
DX |
DH, DL |
RSI |
ESI |
SI |
|
RDI |
EDI |
DI |
|
RBP |
EBP |
BP |
|
RIP |
EIP |
IP |
|
RSP |
ESP |
SP |
|
他にもありますが、ここでは省略します。
ここでは主に32ビットのレジスタを使います。16ビットや8ビットのレジスタは、昔のCPUと互換性をとるためにあると考えてよいでしょう。昔のCPUは8ビットだったのでレジスタも8ビットでした。
次にレジスタの主な用途を説明します。ただし、ESP以外は汎用としても使えます。
レジスタの名前 |
主な用途 |
EAX |
計算、返り値の格納等、最も活躍するレジスタです。 |
EBX |
計算に使います。 |
ECX |
回数をカウントするのに使います。 |
EDX |
計算に使います。 |
ESI |
メモリのアドレスを覚えておくのに使います。 |
EDI |
メモリのアドレスを覚えておくのに使います。 |
ESP |
スタックポインタのアドレスを覚えておくのに使います。
スタックについてはあとで説明します。 |
EIP |
次に実行するアドレスが入ります。 |
EBP |
スタックフレームのアドレスを覚えておくのに使います。 |
CPUの動き
CPUの動きは意外と単純です。CPUはEIPレジスタが指しているアドレスのメモリの内容にしたがって動作しています。メモリに配置されている機械語は最初が命令で、その後に引数が続きます。どの命令が何個引数をとるかは決まっていますので、次の命令は現在の命令に従ってEIPの内容を加算することでそのアドレスを特定することが出来ます。機械語でのループやジャン部命令はEIPの内容を変更することで実現できます。
レジスタを使って計算する
次にサンプル1の和を計算している部分のアセンブリ言語をみて見ましょう。
[ソースリストA]
ここでは、変数a, b, cはメモリの位置を表しています。
上の図の1行目では、eaxレジスタに変数bの内容を代入しています。
2行目では、eaxレジスタつまりbの内容と変数cの内容を足してその結果をeaxレジスタに上書きしています。
3行目では、eaxレジスタに格納されている計算結果を変数aに代入しています。
それでは更に詳しく見ていきます。
mov
ここで、まず用語の説明をします。
アセンブリ言語の一つ一つの命令をニーモニック(mnemonic)またはアセンブラコードといいます。ニーモニックの先頭の命令
を表す単語をオペコード(オペレーションコード)、オペコードがとる引数のことをオペランドといいます。
例えば
mov a, b
では、movがオペコードで、aが第一オペランド、bが第二オペランドです。
つぎに、オペコードmovの説明に移ります。
movは、基本的に第一オペランドに第二オペランドを代入します。
mov eax, dword ptr [b]
は第一オペランドがeaxレジスタ、第二オペランドがdword ptr [b]です。この第二オペランドは変数bの内容を示しています。ptrはデータの型を指定する演算子でdword
ptrで[b]が4バイトであることを表しています。dwordは4バイトであることを表します
(dword以外のその他の型は下の表を参考にしてください)。[b]だけだとそこから始まる何バイトのデータを使うのかが分からないためにptr演算子を使う必要があります。
この命令で行っていることをまとめると「bが示すメモリ上の4バイトのデータをeaxレジスタに代入する」ということになります。
名前 |
バイト |
ビット |
BYTE |
1 |
8 |
WORD |
2 |
16 |
DWORD |
4 |
32 |
add
addは加算する命令で、第一オペランドに第二オペランドを加算した結果を第一オペランドに代入します。
上の例では
add eax, dword ptr [c]
となっています。これはeaxレジスタの内容とcが示すメモリの4バイトの内容を足してeaxレジスタに代入することを示しています。結果的にb +
cを計算してeaxレジスタに代入していることになります。
ソースリストAの最後の行、
mov dword ptr [a], eax
は、eaxレジスタの内容を変数aが示すメモリ上の4バイトに代入します。
このようにして、C語で記述されたa = b + c;は
ソースリストAの3行のニーモックで実現することが分かりました。
加算のほかに減算、乗算、割り算用の命令もあります。その一部を以下に示します。
命令 |
形式 |
説明 |
ADD |
ADD A B |
A + B の結果をAに入れる。 |
SUB |
SUB A B |
A - B の結果をAに入れる。 |
MUL |
MUL A |
乗算EAX × Aを行い、その結果の33から64ビット(上位ビット)をEDX、1から32ビット(下位ビット)をEAXレジスタに入れる。 |
DIV |
DIV A |
EDXレジスタを上位ビット、EAXレジスタを下位ビットとする64ビットの値を、Aで割って、商をEAX、あまりをEDXレジスタに入れる。 |
C語のプログラムにニーモニック(アセンブラコード)を埋め込む
つぎに、実際にアセンブリ言語を書いてみましょう。
名前: ex0
前の説明でプロジェクトsample1を作成したときと同様にして今度はex0を作成し、main.cppに上のプログラムを入力してください。
プログラムの中の//はコメントです(このエディではコメントは緑色になっています)。よって//a = b + c;は無視されます。
ブレークポイントをb=1;の位置に設定し、実行します。するとこの位置で止まるので、F10を押して1ステップずつ勧めながら変数やレジスタの変化を確認してください。マウスを変数やレジスタの上に置くと現在の値が上の図のように表示されます。
ここで教員のチェックを受けましょう。
インラインアセンブラ命令_asm
上のサンプルプログラムのように_asm{}の括弧の中にアセンブリ言語を挿入することが出来ます。
この例では、ptr演算子が省略されています。コンパイラが変数の定義からptrを賢く補ってくれるため書かなくても良いからです(書いてもかまいません)。表示
を混合モードにしてみると確かに補われています。
C語とアセンブリ言語の区別
この実習では、C語にアセンブリ言語を埋め込む方式を採用しています。MASM等のプログラムを使うことで、アセンブリ言語だけでプログラムを作ることも可能ですが、ここでは触れません。
アセンブリ言語は_asm{}で囲まれた中だけです。プログラム全体としてはあくまでC語のプログラムであること、そしてその中にC語の拡張命令である_asmを使ってアセンブリ言語を埋め込んでいるということに注意してください。
例
以下に掛け算の例を示します。(//で始まる緑の部分はコメントで、実行には関係ありません。)
名前: ex1
ここで教員のチェックを受けましょう。
問題
1. a = b - c;を実現するプログラムをアセンブリ言語で記述して実行してください。名前:problem1
出力例:
注意: 「Press
any key to
continue」は、「デバックなしで開始」でプログラムを実行し終了すると自動で表示される文字です。プログラムで表示する必要はありません。
プログラムが完成したら教員のチェックを受けましょう。
2. a = b / c;を実現するプログラムをアセンブリ言語で記述して実行してください。名前:problem2
出力例:
注意:
「続行するには何かキーを押してください...」は、「デバックなしで開始」でプログラムを実行し終了すると自動で表示される文字です。プログラムで表示する必要はありません。
割り算のヒントが下にあります。
3. 1,2で作成したプログラムをprintfを使って結果が表示されるように変更してください。提出するプログラムは以下の変更した後のものにしてください。
printfというC語の関数を使うと変数の内容が表示できます。
例えばint型の変数aの内容を表示するときは
printf("%d\n", a);
とします。
例えば、int型の変数a, bを表示するには
printf("aの値は%d, bの値は%d\n", a, b);
とします。
printfを使った例を以下に示します。
最初の行の
#include<stdio.h>
はprintfを定義しているファイルを指定しています。printfはstdio.hというファイルに記述されています。
#includeを用いて上記のように記述すると、その位置にこのファイルの内容のテキストを貼り付けたのと同じことになります。stdio.hはコンパイラ(VC++)に予め用意されているファイルで様々な関数が定義されています。
上のファイルをビルドして実行します。ここでは終了時すぐ消えないようにCtrl+F5で実行します。
すると確かに、結果が表示されます。
プログラムが完成したら教員のチェックを受けましょう。
その他のレジスタと命令
ここでは、上で省略したものを簡単に説明しておきます。
セグメントレジスタ
セグメントと言われるメモリの塊のアドレスを指定する。すべて 16 ビット。
セグメントレジスタの値は、セグメントセレクタまたはセレクタと呼ばれる。
CS, DS, ES, SS, FS, GS
実際のメモリの指定はこのセグメントレジスタとEIP, EBP, ESPなどを組み合わせて指定します。
フラグレジスタ
演算の結果によって変化するレジスタです。他のレジスタと大きく異なるのは各ビットごとに意味があることです。
フラグレジスタはEFLAGSのみです。
各ビットの意味は以下の通りです。
- Bit 31〜22. 未使用
- Bit 21. IDフラグ:CPUID命令の使用を制御する。
- Bit 20. 仮想割り込みペンディングフラグ:仮想86モードでの割り込み関連のフラグ
- Bit 19. 仮想割り込みフラグ:仮想86モードでの割り込み関連のフラグ
- Bit 18.
アラインメントチェックフラグ:メモリアクセスのアラインメントチェックを制御するフラグ
- Bit 17. 仮想86モードフラグ:仮想86モードの制御をするフラグ
- Bit 16. レジュームフラグ:デバッグ関連の制御フラグ
- Bit 15. 未使用
- Bit 14. ネストタスクフラグ:タスクがネストしているかどうかを示す。
- Bit 13〜12. 割り込み特権レベルフラグ:動作中タスクの割り込み特権レベルを示す。
- Bit 11. オーバーフローフラグ
- Bit 10. ディレクションフラグ
- Bit 9. インタラプトフラグ
- Bit 8.トラップフラグ
- Bit 7. サインフラグ (SF)
- Bit 6. ゼロフラグ
(ZF)
- Bit 5. 未使用
- Bit 4. 補助キャリーフラグ
- Bit 3. 未使用
- Bit 2. パリティフラグ (PF)
- Bit 1. 未使用
- Bit 0. キャリーフラグ (CF)
たとえば、subで引き算をしてその結果が0の場合6bit目のゼロフラグ(ZF)が1になります。
結果の値がマイナスになるとサインフラグ(SF)が1になり、それ以外はSFが0になります。
よって、引き算をしてこれらのフラグを参照すると引く数と引かれる数の等号や大小関係を調べることが出来ます。
これらのフラグはたとえばIF文に相当する条件分岐などに用いられます。
いろいろな命令
比較 cmp
subで値の大小関係などを調べることが出来ると説明しましたが、単に関係を調べる場合は計算した値は必要ありません。このような時はsubの代わりにcmpを使います。
cmpはsubと同じように引き算を行いますが結果は破棄されフラグレジスタの内容だけが変化します。
使い方
cmp A B
無条件分岐 jmp
BASICやCのgoto文に相当し、処理を指定した場所へと移します。
名前: ex2
上の図はjmpを使ったプログラムの例です。eaxレジスタに1足すことを無限に繰り返します。
ブレークポイントを指定して実行すると以下のようになります。ここではF10を何回か押してレジスタの値が変わる様子をチェックしてください。
終了するときはSHIFT+F5を押すか四角いボタンをクリックします。
label:はラベルと呼ばれるもので自由に名前を付けられます。これは、コンパイル時にメモリのアドレスになります。プログラムを作成中はアドレスは決まらないのでレベルを使ってプログラムの場所を指定します。
使い方
jmp L
プログラムが完成したら教員のチェックを受けましょう。
条件分岐
jmpは無条件に実行する場所を変更しますが、フラグレジスタEFLAGSの内容によって分岐するかしないかを決定する命令があります。アセンブリ言語ではこれを用いてIF文に相当することを実現します。
以下のプログラムが条件分岐命令を使った例です。
名前: ex3
jg, jl, jz が条件分岐命令で、
jg はcmp A, BとしたときA>Bならば指定された位置に分岐します。
jlは cmp A, BとしたときA<Bならば指定された位置に分岐します。
jzはcmp A, BとしたときA=Bならば指定された位置に分岐します。
実際には、cmp A, BでフラグレジスタEFLAGSの内容が変わりそのレジスタの内容に従って条件分岐命令は分岐するかどうかを決めています。
プログラムが完成したら教員のチェックを受けましょう。
他にもいろいろあります
ここで紹介した命令はほんの一部です。より深く学習したい人はインターネットに十分な情報がありますので検索して勉強してください。
ここでは参考になるURLを載せておきます。
最適化の為のアセンブラ入門 http://ray.sakura.ne.jp/asm/index.html
アセンブラ入門 http://www5c.biglobe.ne.jp/~ecb/assembler/assembler00.html
割り算のヒント
割り算は足し算や引き算と少し勝手が違います。
まずは、DIVの説明を注意深く読みます。
DIV |
DIV A |
EDXレジスタを上位ビット、EAXレジスタを下位ビットとする64ビットの値をAで割って、商をEAXあまりをEDXレジスタに入れる。 |
EDXレジスタが上位ビットとなっているので、計算する前にEDXに何か代入する必用があります。
割られる数aをEAXに入れます。EDXはそれより上の桁ですが実質的には必要ないので計算に影響しないようにするにはどんな数を代入すればよいでしょう。
説明の後半は理解できましたか?
答えはEAXに入り、余りはEDXに入ります。割り算は答えと余りがペアで意味を成すので両方の値を取り出して表示しなくてはなりません。
提出するプロジェクト一覧
ex0, ex1, ex2, ex3, problem1, problem2(ただしprintfで結果を表示できるようにしたもの。)
提出の仕方
ex0を例にして説明します。
プロジェクトのフォルダ上手のように右クリックしをzip形式の圧縮フォルダにします。
すると同名の圧縮ファイルができるのでこれをコースサイトにアップロードします。