アセンブラとかマシン語とか

この3週間くらい アセンブラとかマシン語とかを使った修行を行なっています.
このまま復習せずにいたら修行が無駄になりそうなので どんなことがしたのか どんなことができるようになったのか まとめておきたいです.

ただ 勉強して日が浅いので間違いを指摘していただけるとありがたいです.

それでははじめます.
長文ですが お願いします.

前提

Mac OSXの10.8を使います.
OSは64ビットですが わからないので32ビットでGnu Assemblerを利用します.
32bitなのでアセンブラの命令に"l"が付きます.
なお パッケージ管理にはHomebrewを利用しています.

WindowsやLinuxでもgccが入っていれば同じことができます.
Linuxは未確認ですが WindowsではMinGWを入れて確認済みです.)

簡単な関数をAssembly

整数の引数をそのまま返す簡単な関数fを考えます.
この関数fをf.cというファイルに保存します.
f.cの中身は次のようになります.

int f(int x)
{
	return x;
}

gccを使ってAssemblyします.
Assemblyするには

$ gcc -m32 -S f.c

というコマンドを打ち込みます.
このコマンドを実行するとf.sというファイルができます.
このf.sというファイルの中身が関数fをAssemblyした結果です.
私の環境では以下のようになりました.

	.section	__TEXT,__text,regular,pure_instructions
	.globl	_f
	.align	4, 0x90
_f:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$12, %esp
	movl	8(%ebp), %eax
	movl	%eax, -4(%ebp)
	movl	-4(%ebp), %eax
	movl	%eax, -12(%ebp)
	movl	-12(%ebp), %eax
	movl	%eax, -8(%ebp)
	movl	-8(%ebp), %eax
	addl	$12, %esp
	popl	%ebp
	ret


.subsections_via_symbols

多分 初見ではさっぱりだと思うので簡単に説明を付けます.
詳しいことはこのページマニュアルを参照してください.

レジスタとメモリ

Assemblyした結果の説明をレジスタとメモリからはじめます.
アセンブラではレジスタというCPU上のメモリと主記憶装置のメモリ(これから先主記憶装置のメモリを単にメモリと呼びます.)を使います.
アセンブラで処理ができるのはレジスタ上にあるデータだけです.
なので メモリ上にあるデータを処理するためには”レジスタに移す”という命令を書く必要があります.

汎用レジスタ

使えるレジスタは数が少ないのでうまくメモリを使いながら命令を書きます.
具体的に使えるレジスタ(汎用レジスタと言われています.)は8種類あります.
ほとんどのレジスタには役割が決まっているので実際に使えるのは三種類です.
この三種類は%eax,%ecx,%edxです.
注意してほしいのは%eaxはAccumulatorと言われ 関数の戻り値として用意されているレジスタです.
つまり最終的に%eaxに入ってる値が関数の戻り値と返されるわけです.

ベースポインタとスタックポインタ

汎用レジスタの中で役割が決まっているうちで知って置かなければならないのが %espと%ebpです.
それぞれスタックポインタとベースポインタと呼ばれています.
この二つが何をしているかというと関数の使う領域をメモリ上に確保する という役割を担っています.
上のアセンブラでは

	pushl	%ebp
	movl	%esp, %ebp
	subl	$12, %esp

という命令が一番最初あります.
この3行の命令で関数で使う領域を確保します.

	pushl	%ebp

で%ebpレジスタをスタック上に用意します.
スタックというのはメモリ上でのデータの置き方です.
スタックでデータを保存するとデータは本を山積みするように保存されます.
つまり 一番最後に保存されたものが最初に取り出されます.
"pushl"というのはスタックの一番上に積むという命令です.

	movl	%esp, %ebp

でスタック上に”基準”を決めます.
"movl"は値を移す(代入と言ったほうがわかりやすいかもしれない.)という命令です.
%espはスタックの空いてる部分の下限を指しています.
"movl"して%ebpを空いてるスタックの基準にしているわけです.

領域準備の最後としてローカル変数を確保します.

	subl	$12, %esp

この命令は%espから12引くという意味です.
この命令でローカル変数3つ分が確保されます.
4がローカル変数1つ分です.

間を飛ばしますが 最後の

	addl	$12, %esp
	popl	%ebp
	ret

という命令で確保した領域をcleanしています.

引き数の利用

引数は%ebpから遡って参照します.
上のアセンブラで引数を参照しているのが

	movl	8(%ebp), %eax

という部分です.
引数は確保した領域の外側にあります.
つまり %ebpより下側にあります.
先ほどローカル変数の領域を確保するのに%ebpから12を引いて上側に領域を確保しました.
だから下側は%ebpに足して参照します.
具体的には"8(%ebp)"で一番目の引数を参照できます.
"4(%ebp)"は関数のreturnのために用意された領域で引数は"8(%ebp)"から始まります.
"4"ずつ間をとって並んでいるので二番目の引数は"12(%ebp)"となります.

ローカル変数

ローカル変数は"-4(%ebp)"のようにして参照します.

"pushl"でスタックに積んだ変数もローカル変数と同じく参照できます.
"pushl"で積む場合は予めローカル変数の領域をする必要はありません.
ただ"pushl"で積んだデータを参照するとき"8(%ebp)”というふうに参照することはありません.
大概 "pushl"の逆 "popl"を使ってデータを取り出します.
"popl"は"pushl"の逆なので"pushl"で一番最後に保存した値が一番最初に取り出せます.

より人間らしく

以上でf.sを理解できるかと思います.
じっくり見ると機械的に生成したアセンブラは無駄な部分が多いと思いませんか?
ただ単に引数をreturnするだけなのにローカル変数を用意したり 意味のない代入をしていたり.
スマートに人間らしくアセンブラを書いてみます.
ファイル名をg.Sとして書いてみたアセンブラ

	.text
	.globl _g
_g:
	pushl	%ebp
	movl	%esp,		%ebp
	movl	8(%ebp),	%eax
	leave
	ret

です.

では実際に引数をそのまま返してくれるのか さきほどのf.sも合わせて試します.
main.cを用意します.

#include <stdio.h>

int main(int argc, char const *argv[])
{
	fprintf(stderr, "%d\n", f(3));
	fprintf(stderr, "%d\n", g(3));
	return 0;
}

コンパイルするときは"-m32"という32ビットだということを表すオプションを忘れずにつけて

$ gcc -m32 main.c f.s g.S

とします.
実行すると

$ ./a.out
3
3

となって引数をそのまま返してくれることがわかります.

足し算をする

先ほどの関数gのアセンブラを書き換えます.
引数に3を足した値を返すよう変更します.
g.Sは次のようになります.

	.text
	.globl _g
_g:
	pushl	%ebp
	movl	%esp,		%ebp
	movl	8(%ebp),	%eax
	addl	$3,			%eax
	leave
	ret

"addl"という命令を加えました.
これは足し算の命令です.
main.cは変更ありません.
同様に

$ gcc -m32 main.c f.s g.S

でコンパイルして実行します.
すると

3
6

という結果が得られます.

なお ここでは足し算を使いましたが 足し算以外の四則演算もできます.
かけ算・割り算は少し厄介ですが…
詳しくはマニュアルを参照してください.

disassembly

実行ファイルをdisassemblyしてマシン語に変換します.
disassemblyには"objdump"というコマンドを使います.
gccがあれば大抵は使えるはずですが Mac OSXでは使えませんでした.
なのでHomebrewを使ってインストールします.
objdumpはパッケージbinutilsに含まれています.

$ brew install binutils

Macでは"objdump"ではなく"gobjdump"と名前が変わってます.

$ gobjdump -d ./a.out > disassembly.txt

というコマンドでdisassemblyします.
だーっと吐き出されるのですが 注目したいのは次の部分です.

…
00001f70 <_g>:
    1f70:       55                      push   %ebp
    1f71:       89 e5                   mov    %esp,%ebp
    1f73:       8b 45 08                mov    0x8(%ebp),%eax
    1f76:       83 c0 03                add    $0x3,%eax
    1f79:       c9                      leave
    1f7a:       c3                      ret
…

一番右にアセンブラが表示されています.
アセンブルでは10進数が16進数に変換されています.
真ん中の部分が関数gのマシン語です.
環境によってマシン語が異なるかもしてないので自信の環境で実行した結果を使ってください.

マシン語で関数を呼ぶ

マシン語を使って関数gを呼び出します.
マシン語を使って関数gを呼び出すには次のように書きます.
ファイル名はmachine_code.cとします.

#include <stdio.h>
#include <mach/mach.h>

typedef int (*g_t)(int);

int main(int argc, char const *argv[])
{
	unsigned char g[256];

	vm_protect(
		mach_task_self(),
		(vm_address_t)g, 256 * sizeof(char),
		FALSE,
			VM_PROT_READ | 
			VM_PROT_WRITE | 
			VM_PROT_EXECUTE
		);

	g[0] = 0x55; // pushl %ebp
	g[1] = 0x89; // movl  %esp,       %ebp
	g[2] = 0xe5;
	g[3] = 0x8b; // movl  0x08(%ebp), %eax
	g[4] = 0x45;
	g[5] = 0x08;
	g[6] = 0x83; // addl  $0x03,      %eax
	g[7] = 0xc0;
	g[8] = 0x03;
	g[9] = 0xc9; // leave
	g[10] = 0xc3; // ret

	fprintf(stderr, "%d\n", ((g_t)g)(6));

	return 0;
}

いくつか説明の必要な点があるので簡単に説明していきます.

関数ポインタ

typedef int (*g_t)(int); 

は関数ポインタを定義しています.
この関数ポインタでキャストしてマシン語を呼び出します.

実行フラグ

	vm_protect(
		mach_task_self(),
		(vm_address_t)g, 256 * sizeof(char),
		FALSE,
			VM_PROT_READ | 
			VM_PROT_WRITE | 
			VM_PROT_EXECUTE
		);

この命令で配列gに実行フラグを立てます.
フラグを立てるためには

#include <mach/mach.h>

としてmach.hをインクルードしてください.

コンパイルと実行

32bitということを忘れないで実行します.

$ gcc -m32 machine_code.c
9

となって予想通りの結果が得られます.