ロゴ ロゴ

アセンブリのすゝめ


*この記事はあくまで初心者向けに簡単に書いたものです、上級者はお帰り下さい
*この記事に出てくるコードはあくまで疑似コードです。

始めに

皆さんアセンブリ言語というものはご存知でしょうか?
アセンブリ言語とは、プログラミング言語の類型の一つで、コンピュータのCPU(MPU/マイクロプロセッサ)が直接解釈・実行できる機械語(マシン語)と正確に対応する命令語で構成された言語。
わからない人向けに簡単に書くとメモリとレジスタ間での数値のやり取りを行う
CPUが実行できる機械語命令と一対一で対応する低級言語です。

mov eax,dword ptr [b]
add eax,dword ptr [c]
mov dword ptr [a],eax

こんな感じの記述見たことありませんか?

アセンブリ

アセンブリ言語を理解する上で必要な単語は3つあります。
CPU
メモリ
レジスタ

CPUは皆さん知ってる前提として問題はメモリ・レジスタ
この大学の人は大抵C言語をやらされるので「ポインタ」というものの存在は知ってますね。よくポインタの説明でポインタはメモリだとかアドレスだとか言われますよね。例えば

int *p:
*p = 100;
printf(":[%p , %d]");

こういうプログラムがあったらとしたら

:[0x0804F9E6 ,100]

のような出力がされます。この[0x0804F9E6]というのは100が格納されているメモリの場所(アドレス)ですね。
上記の式をアセンブリ言語風に直してみると

LOAD 0x0804F9E6,100         : 0x0804F9E6に100をLOAD(ロード)

つまり0x0804F9E6に100を格納する(代入)という風に表せます。メモリの説明は以上です、ここまで分かればレジスタは(そこそこ)簡単。
変数のa,bを入れ替える時は

tmp = a;
b = a;
a = tmp;

のように書きますよね?
アセンブリ言語風に同じことをするならば

LOAD tmp,a      : tmp に a  を LOAD
LOAD b  ,a      : b   に a   を LOAD
LOAD a  ,tmp    : a   に tmp を LOAD

と書きたくなりますよね?しかしここで問題が発生します。
アセンブリ言語では基本的にメモリからメモリの読み書きはできません、つまり

LOAD tmp[メモリ],a[メモリ]

の様な記法は使えません。
そこで出てくるのがレジスタ、これは端的に言えば超小規模&アクセスが高速なメモリです。
ただしCPUのすぐそばにある為メモリに比べてアクセスが滅茶苦茶早いです。しかしその反面メモリに比べて保存域はゴミカスです。
近年メモリ8GB,16GBなど言ってますがレジスタは精々数十バイト程度です。雲壌月鼈

*少しそれますがGPUなどはこのレジスタを数百キロバイト持ってたりします、これが計算速度に影響するのはなんとなくわかると思います。

このレジスタを使うとさっきの式は

LOAD  A , a : A(レジスタ)にa(メモリ)をロード
LOAD  B , b : B(レジスタ)にb(メモリ)をロード
STR   a , B : a(メモリ)にB(レジスタ)をストア(代入)
STR   b , A : b(メモリ)にA(レジスタ)をストア(代入)


*一般に読み込みはロード、書き込みはストアと言います。

こう表せます。ここからほんの少しややこしいですが自由に使えるレジスタを「汎用レジスタ」と言います。
つまり自由につかえない物もあります。
例えば、プログラムカウンタ、フラグレジスタ、セグメントレジスタなどです。
これについては割愛し先に進みます。

いろんなものをアセンブリで再現しよう

高級言語にはいろんな制御文がありますがアセンブリにそんなものはありません。
しかし、アセンブリに再現出来ないものは何一つありません、試しに色んなものの疑似コードを書いてみましょう。

*javaやCもコンパイルを経てアセンブリ言語->;機械語に変換されます。

if、else文の再現

アセンブリにはif文がありません
代わりに分岐命令が存在します
分岐と言っても実際に行っているのはジャンプ命令です。
ジャンプについてはC言語にも一応goto文があるのでなんとなくわかるかもしれません。
例えば

0:LOAD  A,a
1:STR   b,B
2:JUMP  0

このJUMP 0というのは番目にジャンプするという意味です。つまりこの場合は、
LOAD > STR > JUMP > LOAD > STR > JUMP ….
を永遠に繰り返すということです。
番目というのが突然出てきましたがこれもアドレスです。
*レジスタにも種類があるようにメモリにも種類があります、この場合はプログラム領域とかです。

ここで大事なのが条件(フラグレジスタ)に基づいたジャンプが可能ということです。
x86というCPUの場合は
・ゼロでジャンプ
jzはjump if zeroで、ZFが1の時ジャンプします.対になる命令でjnzがあります.こちらはZFが0の時ジャンプします.
・キャリー発生でジャンプ
jcはjump if carryで、CFが1の時ジャンプします.jzと同様にjncがあり、こちらはCFが0の時ジャンプします.
・オーバフローでジャンプ
joはjump if overflowでOFが1の時ジャンプします.これも、jnoがあります.
・負の計算結果でジャンプ
jsはjump if negative signの略でSFが1の時ジャンプします.
・正の時にジャンプしたい場合はjnsを使います.
などがあります。(他にも沢山ある

0:load      a,5     : a に 5 を入れる
1:cmp       a,5     : a と 5 を比較 (aは5なので ZFがセットされる)
2:jz        4       : ZFがセットされてるので4にジャンプ
3:load      a,100   : 4に飛ぶのでここは実行されない
4:END               : 終了

上記の例の場合 (a==5)なら「4」実行でそれ以外は「3」実行というif文を再現出来ていますね。
else,else ifなどを使いたければ分岐命令を追加すればOKです

for,while文

これもやってることは分岐です

0:load  a,0 : a に 0 を入れる :a=0
1:add   a,1 : a に 1 加算     :a++
2:cmp   a,3 : a と 3 を比較   :if(a == 3)
3:jnz   1   : ZFがセットされてなければ1にジャンプ
4:END       : 終了

一回目はa==1なのでjnzを満たして1にジャンプ、3回目のループでa==3となり無事ループを抜けて終了ということです。
if文との違いを挙げると先にジャンプするか後ろにジャンプするかの違いです。

配列

ここまでの話で変数はアドレスで管理されていることや、分岐命令を用いて様々な制御をしていることが分かりました。

ここで配列について考えてみます。
*ここでは宣言された配列のみ考える(動的確保は無視)

int a[3] = {0,1,2};

という変数があり先頭アドレス(a[0]のアドレス)が0x0804F9E0の場合
a[0] のアドレスは 0x0804F9E0
a[1] のアドレスは 0x0804F9E4
a[2] のアドレスは 0x0804F9E8
となります(int型は4Byte)
つまり、a[1]のアドレスは0x0804F9E0+4 = 0x0804F9E4
これはC言語での*(a+1) と a[1] が同じである理由です。
*+1となっているのはコンパイラが自動でint型(4Byte)の+1を+4として解釈してくれるから

また、これをアセンブリ言語で表すと

LOAD P ,0x0804F9E0  :P <= 0x0804F9E0    :P <= &a
LOAD A ,[P+4]       :A に P+4の中身を代入  :A <= a[1]

と表せます。
ここでは[P+4]という新たな記法が出てきました、これはアドレッシングモードという考えです。
LOAD P ,0x0804F9E0 では P に 0x0804F9E0 を「直接」代入していますが、
LOAD A ,[P+4] では A に [P+4] の「中身」代入しているという違いを表す書き方です。
アドレッシングモードは他にも沢山あるので興味があれば調べてみて下さい。

終わりに

今回の記事はアセンブリ言語のほんの一部分です。他にもシステムコール、文字列、関数、構造体、浮動小数など話せてないことが沢山あります。
調べればより深くプログラムを理解できるようになると思うので損は絶対ないと思います。
次回は関数について書くかもしれませんよ…

コメント入力

関連サイト