少し前に読んだのだけれど面白かった。
まずは大学ぶりにHDLを使って回路を組む。 NAND回路のみを使ってOR/ANDから始まり、マルチプレクサへ行き、クロックが入ってきてメモリ、レジスタ 最終的には機械語命令の仕様に沿ってALUとレジスタを組み合わせる。
プログラミングは1行ずつ実行されるが、このレイヤーだと1クロック時に すべての結線で同時に論理演算が進むので、デバックするのが大変。ここまでで10時間ぐらい使っただろうか。
機械語を理解するCPUを作ったら、次はアセンブラ。 アセンブラといってもほとんど機械語に直で変換できるので軽くシンボルテーブルと定数あたりを作成すればおしまい。
@0 D=M @1 D=D-M @10 D;JGT @1 D=M @12 0;JMP @0 D=M @2 M=D @14 0;JMP
これが
0000000000000000 1111110000010000 0000000000000001 1111010011010000 0000000000001010 1110001100000001 0000000000000001 1111110000010000 0000000000001100 1110101010000111 0000000000000000 1111110000010000 0000000000000010 1110001100001000 0000000000001110 1110101010000111
これになる。なんてわからない。
この後、中間コードをアセンブラにコンパイルするコードを書く。 この辺りは演算や関数呼び出しのスタックなどをアセンブラで自分で書く必要があり、コピペでは全く太刀打ちできない。
例えば以下のような7 + 8 を演算する中間コードがある。
push constant 7 push constant 8 add
これを1行ずつ読み込み、以下のようなアセンブラを出力するプログラムを書くわけだ。
@7 D=A @SP AM=M+1 A=A-1 M=D @8 D=A @SP AM=M+1 A=A-1 M=D @SP AM=M-1 D=M A=A-1 M=M+D
もちろん単純な演算だけでなく、JMP命令につながるものや関数呼び出しも作る。 以下はフィボナッチ数列のコード。アセンブラは400行を超える。
function Main.fibonacci 0 push argument 0 push constant 2 lt // checks if n<2 if-goto IF_TRUE goto IF_FALSE label IF_TRUE // if n<2, return n push argument 0 return label IF_FALSE // if n>=2, returns fib(n-2)+fib(n-1) push argument 0 push constant 2 sub call Main.fibonacci 1 // computes fib(n-2) push argument 0 push constant 1 sub call Main.fibonacci 1 // computes fib(n-1) add // returns fib(n-1) + fib(n-2) return
x86やatomをきちんと学んだわけではないけれど、 特定のレジスタにConstant値を入れて、それを参照しながら別のレジスタやメモリに対してアクセスをかけるアセンブラのコードは 高級言語にはない特有の読みにくさがあり、良い勉強になった。
プログラムコードを中間コードにコンパイルするときには構文解析から始まって一度内部的にXMLを作成したうえで、 これらを中間コードに落とし込む。 このような構文解析と出力の分離はLLVMの階層構造にもつながるところがあり、LLVMというものが流行っているのかなどと考えさせられるきっかけにもなった。
The Architecture of Open Source Applications: LLVM
実装はなかなかつらかったが、低レイヤーを歩き始めるには非常に良い本であった。