はじめに
TECH::CAMP Advent Calendar 12/12担当だった井上です。
クリスマスが近いということもあるので本日は現代人の99.9999%以上は触れる必要がないと思うプログラミング言語界におけるモーゼのような存在であるアセンブリ言語に触れてプログラムが動く仕組みやCPUについてあっさりとした切り口で触れたいと思います。
おことわり
この記事は以下のような人でも分かるように記述しようと意図があるため、重要な概念・用語の省略、(意図して書くものではないですが)解釈によっては嘘に近くなる内容が記述されている可能性がありますので、これをご了承の上、用法用量を守ってお読みください。
- CPUってパーツがパソコンの中に入ってるのは知ってるけど、実際のところ何をどうしてるのかは知らない
- パソコンとかOSがどうやってプログラムを動かしてるのか知らない
- アセンブリ言語?システムコール?何それ?
##環境
以下の環境で動作を確認しています。
macOS 10.13.1
Intel Core i5 (Intelの64bit CPUなら動くと思います)(多分)
#とりあえずインストール
何は無くともまずは環境を用意しましょう。
##アセンブラ(NASM)のインストール
アセンブリ言語で記述された言語を機械語に変換するプログラムをアセンブラといいます。
MacではNASMと呼ばれるアセンブラが標準でインストールされていますが、バージョンがとても古い(確か0.9.8とか)ので最新版の2.1.3にアップデートしましょう。
HomeBrewで簡単に最新版を利用できるようになります
$ brew install nasm
(中略)
$ nasm -v
NASM version 2.13.** ~~~ のような出力がされれば成功
##ちょっと説明
###そもそも機械語って何?
機械語とは0,1の羅列であり、CPUが直接解釈できる唯一の言語です。
2つのスイッチと動作実行ボタンがある(=2ビットの入力を受け取る)機械をイメージしてください。
これを「2ビットくん」と呼びましょう。
2ビットくんは、以下の表のように2つのスイッチの状態に応じて異なる動作を行うように作られています。
スイッチB | スイッチA | 行う動作 |
---|---|---|
OFF | OFF | 動作A |
OFF | ON | 動作B |
ON | OFF | 動作C |
ON | ON | 動作D |
2ビットくんを作ったことで、人類は少し幸せになれたと思います。
しかし、もっと幸せになりたい人類は逐一スイッチを操作し、ボタンを押して実行する代わりに
数字の0,1をスイッチのOFF,ONに対応させ、命令をあらかじめ別のところに書いておき、1つずつ実行することで処理を自動化することができました。
例えば、以下のようなファイルを読み込ませると自動で動作を行うようになった「2ビットくん3世」が作られたりしました。
// はコメント
00 // 動作Aを行う
01 // 動作Bを行う
10 // 動作Cを行う
11 // 動作Dを行う
.
.
.
実はCPUもこの2ビットくん3世と同じような仕組みで動いています。
与えられた0,1の羅列によって、自らが行う動作が決められているのです。
故に、この0,1の羅列が後に機械語と呼ばれるようになったのでした。
###じゃあ、アセンブリ言語って何なの?
機械語を人間に分かりやすくした言語です。
例えば、先ほど記載した「2ビットくん3世がやること」ファイルにコメントが無いと、2ビットくん3世の仕様を理解しない人にはこのファイルを読み込ませた2ビットくん3世が何を行うのかさっぱり予測できないでしょう。
// 何をするのかさっぱり分からないと思うんだ
00
01
10
11
.
.
.
また、2ビットくん3世がCPUへ進化し、行う動作の数が何十にもなると、人類は0,1の羅列と動作の対応表が欲しくなりました。
その際、誰かがその対応それぞれにも名前をつけました。
与えるビット列 | 行う動作 | 対応の名前 |
---|---|---|
0000 | 加算 | add |
0001 | 減算 | sub |
0010 | 積算 | mlt |
0011 | 除算 | div |
... | ... | ... |
ここで人類は気づきます。
「じゃあ、この対応の名前からビット列に変換するヤツを作ったら、0,1の羅列覚えずに済むし、すんごい便利じゃない??」
これがアセンブリ言語の始まりです。そして、「対応の名前からビット列に変換するヤツ」こそが先ほどインストールしたアセンブラです。
アセンブラの登場以降、人類はCPUに行わせる動作(=プログラム)を以下のような形で記述することが可能になりました。
// ビット列の直書きよりは何をするのかまだ分かると思うの
add
sub
mlt
div
.
.
.
また副次的な効果として、動作に対してビット列の対応が違うCPUでも、対象のCPU用のアセンブラで再アセンブルすれば同じソースコードで動くようにできました。
例えば、あるCPUメーカー「bar」は以下の表のようにビット列に対しての動作を決めていたとします。
(先述の対応表を作ったのはCPUメーカー「foo」とします。)
与えるビット列 | 行う動作 | 対応の名前 |
---|---|---|
0000 | 加算 | add |
0011 | 減算 | sub |
0001 | 積算 | mlt |
0010 | 除算 | div |
... | ... | ... |
アセンブラ登場以前は、メーカーfooのCPU向けに書かれたプログラムをメーカーbarのCPUで動かすには、少なくとも「sub,mlt,div」の命令を、メーカーbarが定めているビット列に書き換える必要がありました。
メーカーfooのCPUには「0001」を与えれば減算が行われましたが、メーカーbarのCPUに「0001」を与えると積算が行われるからです。
ですがアセンブラが登場してからは、メーカーbarが自社CPU用のアセンブラを用意すれば、アセンブリ言語で記述されたソースコードを書き換える必要なくメーカーbarで動くプログラムが作れるようになったのです。
アセンブリを見てみる
アセンブリ言語の偉大さが少しわかったところで、HelloWorldのソースコードをご覧ください。(引用元)
; helloworld.asm
GLOBAL start
SECTION .data
str_hello db "Hello World", 0x0a ; Output string and \n
SECTION .text
start:
mov rax, 0x2000004 ; Set system call to write=4.
mov rdi, 1 ; Set output to stdout.
mov rsi, str_hello ; Set output data.
mov rdx, 13 ; Set output data size.
syscall ; Call system call.
mov rax, 0x2000001 ; Set system call to exit=1.
mov rdi, 0 ; Set success value of exit.
syscall ; Call system call.
###実行
$ nasm -f macho64 helloworld.asm
$ ld -o helloworld helloworld.o
$ ./helloworld
Hello World!
###コードの解説
; helloworld.asm
行頭に;をつけるとその行はコメントになります。
GLOBAL start
startというラベルを外部ファイルから参照するためにGLOBAL命令を記述します。
SECTION .data
メモリ上にプログラムを展開する際の単位となるセクションを設定しています。
特に.dataセクションはC言語において「0以外の初期値を持つグローバル変数」「0以外の初期値を持つ静的局所変数」がここに置かれます。
str_hello db "Hello World", 0x0a ; Output string and \n
コメントの通りHello World\n
という文字列のバイト列を生成し、また、それにアクセスできるようにstr_helloという名前をつけています。
SECTION .text
.textセクション以下に実際のプログラムコードを記述していきます。
start:
startというラベルを定義しています。
ラベルを定義すると、プログラム上でそのラベルへジャンプできます。
mov rax, 0x2000004 ; Set system call to write=4.
mov rdi, 1 ; Set output to stdout.
mov rsi, str_hello ; Set output data.
mov rdx, 13 ; Set output data size.
syscall ; Call system call.
Linuxシステムコールの仕様で、RAXレジスタに入れた値がシステムコールを指し、RDI,RSI,RDXレジスタに入れた値がそれぞれそのシステムコールに渡す第1、第2、第3引数になります。
最後にsyscall命令を実行することでシステムコールが実行されます。
C言語っぽく記述すると以下のようになります。
write(1,str_hello,13) //の実行
mov rax, 0x2000001 ; Set system call to exit=1.
mov rdi, 0 ; Set success value of exit.
syscall ; Call system call.
ここではexitシステムコールを呼び出す処理を行っています。
##用語の解説
いきなり「レジスタ」や「システムコール」などの謎すぎるであろうワードが飛び交ったので時間の許す限り解説したいと思います。
###レジスタ
CPUの中にある記憶領域です。メモリ等の記憶領域からCPUに持ってきた電気的な意味で計算される直前、直後の値や、実行する命令のアドレス値などがここに入っています。
###システムコール
平たく言うとOSの機能の呼び出し。
長いのでWikipediaを見よう。 システムコール
時間切れ
ここで〆切のタイムリミットが来てしまいましたので、続きは今度ということでお願いいたします。(ジャンピング土下座)
こうやって歴史を振り返るとC言語のCはChristのCと言われてもなんとなく理解できるのでは無いでしょうか。
それでは皆様、楽しいクリスマスを…