〜低級(アセンブリ言語)への誘い〜その1

JavaやC#、その他スクリプト言語等を使ってみて思うことは、機能としては高機能で、やりたいことは大抵ライブラリで用意されていてすぐに実現できる。しかし、やっていることはライブラリの使い方を調べることが非常に多いとも感じる。

プログラミングってこんなに面白くない作業だっただろうか…
プログラミングは手段なので、やりたいことを簡単に実現できた方が効率がいい。それは確かに事実だが、面白くない。私はそう感じてしまう。

ある日、ファミコンのプログラミングをやってみて思ったのは、単純に面白い!少ないリソースをいかに効率よく使うか。プログラミングそのものがパズルゲームのように面白く感じる。この面白さを、もっと沢山の人に知ってもらいたい!
しかしながらファミコンは、組込みシステムなので若干のハードウェアの知識も必要で、アセンブリ言語も事実上必須。高級言語を学んだだけの状態から始めるには、少しハードルが高めかもしれない。

私がアセンブリ言語を学んだのはx86系CPUが32bitの時代だったのもあり、
良い機会なので64bit時代のアセンブリ言語も学んでおこうかと。
ついでにアセンブリ言語を学んだことがないという方にもわかるように書ければ一石二鳥。
アセンブリ言語初学者の一助になれれば幸いである。

まず、私の環境は
OS:FreeBSD
コンパイラ:clang 18.1.5
CPU:x86-64bitアーキテクチャ

FreeBSDユーザなのでclangを使用しているが、gccでも同様のオプションが使えるはず。
取り敢えず私は上記の環境を使用するので、アセンブリソースの説明はAT&T記法の説明になる。
IntelやMicrosoftのアセンブラを使用するとIntel記法になるので、違いには注意が必要。
基本的には以下のコマンドでC言語のソースをコンパイルして、アセンブリソースを出力する。

clang -S -fno-asynchronous-unwind-tables c_src1.c

-Sオプションは、C言語のソースからアセンブリ言語のソースを出力するオプション。*.sという拡張子でファイルが出力される。
-fno-asynchronous-unwind-tablesオプションは、コンパイル後のアセンブリソースにデバッグのための情報を出力しないようにする。アセンブリソースにデバッグ情報が入っていると読みにくくなるので、このオプションを付けている。
まず初めに以下のC言語のソースをコンパイル。

int result;

int main()
{
	result = 100;
	return result;
}

10進数の数値100をグローバル変数のresultに格納し、main関数の戻り値とするだけ。これを前出のコマンドでコンパイルすると、以下のアセンブリソースが出力される。

	.text
	.file	"c_src1.c"
	.globl	main                            # -- Begin function main
	.p2align	4, 0x90
	.type	main,@function
main:                                   # @main
# %bb.0:
	pushq	%rbp
	movq	%rsp, %rbp
	movl	$0, -4(%rbp)
	movl	$100, result
	movl	result, %eax
	popq	%rbp
	retq
.Lfunc_end0:
	.size	main, .Lfunc_end0-main
                                        # -- End function
	.type	result,@object                  # @result
	.bss
	.globl	result
	.p2align	2, 0x0
result:
	.long	0                               # 0x0
	.size	result, 4

	.ident	"FreeBSD clang version 18.1.5 (https://github.com/llvm/llvm-project.git llvmorg-18.1.5-0-g617a15a9eac9)"
	.section	".note.GNU-stack","",@progbits
	.addrsig
	.addrsig_sym result

ゴチャゴチャと書かれていてわかりにくいが、主要な処理は以下の部分。

	movl	$100, result
	movl	result, %eax

アセンブリ言語は、基本的に1行につきCPUの1命令を記述する。

  オペコード オペランド1, オペランド2

のような書き方をするので

  movl…オペコード
  $100…オペランド1
  result…オペランド2

ということになる。
オペランドの数はオペコードによって異なる。movl命令の場合はオペランドは2つ。オペランド1をオペランド2にコピーする命令。
movlの'l'は32ビットをコピーするという意味。他に
movb…8ビットコピー
movw…16ビットコピー
movl…32ビットコピー
movq…64ビットコピー
とmov命令にはサイズによってバリエーションがある。

オペランド1の$100は、即値で10進数の100。
ではオペランド2のresultとは何か。
アセンブリソースを見ると、

result:
	.long	0                               # 0x0
	.size	result, 4

とある。結論から言うと、メモリ上のどこかに4バイトの領域を確保し、その領域の名前をresultと名付ける。
result:とすることでメモリ領域に「ラベル」を付けることができる。
実際にメモリ上のどこのアドレスになるかはリンク時に確定するので、アセンブリコード上はラベル名を使用して領域にアクセスする。
ということで、

	movl	$100, result

は、メモリ上のresultという32ビットの領域に100をコピーするという命令になる。C言語のint型が32ビットとして扱われているということがわかる。
同様に、

	movl	result, %eax

は、メモリ上のresultという32ビット領域の内容を%eaxにコピーするという命令になる。
では%eaxとは何か。レジスタである。
レジスタは、CPU内部の小さな記憶装置で、CPUが演算などに使用する。
このアセンブラではレジスタ名の前に'%'を付加する。
%eaxは、64ビットある%raxレジスタの下位32ビットを表す名前。
x86アーキテクチャは、16ビットCPUから拡張されてきた歴史がある。
64ビットレジスタでも、名前によって使用する場所を指定することができる。


RAXレジスタの内訳

上記のような関係性のため、%eaxと指定すると、%raxレジスタの下位32ビットを使用することになる。しかし、C言語の

return result;

がなぜアドレスresultにある値を%eaxにコピーする処理になるんだ?と疑問を持たれるかもしれない。しかし、整数型の戻り値は%raxレジスタに格納して返すというのがABI(Application Binary Interface)によって決まっているのでその決まりに従わないと、戻り値の受け側はどこに戻り値があるのかわからなくなってしまう。
なので、32ビットの整数値を返す場合は%eaxレジスタに格納して返す。

以上が主要な処理。
初回なので簡単なサンプルプログラムだったが、次回があればもう少し複雑なものを見ていきたい。