Brainf*ck トランスレータ

1
第 4 回 コンパイラ実装会 配布資料
Brainf*ck から入るコンパイラ入門
Brainf*ck は難解言語としてよくネタにされますが、字句解析が不要
Brainf*ck トランスレータ
なためバイナリ関係に集中できて、最初のターゲット言語として向
いていると思います。
1
Brainf*ck から入るコンパイラ入門 ...................... 1
2
はじめに.............................................. 1
3
64bit ............................................ 1
2.2
シンボル名のプレフィックス ....................... 2
1.
インタプリタ
2.3
記法 ............................................. 2
2.
C 言語へのトランスレータ
呼び出し規約.......................................... 2
3.1
3.
アセンブリ言語へのトランスレータ
i386(Win) ........................................ 2
4.
JIT
スタックの効率化............................... 2
5.
実行可能形式でバイナリ出力
3.1.1
4
5
6
次の順番で少しずつ自己完結的にしていくと良いと思います。
2.1
3.2
i386(ELF) ........................................ 2
3.3
i386(Mac) ........................................ 3
最初にインタプリタから作るのは、バイナリやアセンブラの知識を
3.4
x86_64(Win) ...................................... 3
必要とせずに、Brainf*ck 自体に慣れるためです。
3.5
x86_64(ELF) ...................................... 3
3.6
x86_64(Mac) ...................................... 3
いきなりバイナリを出力するのは難しいので、まずは C 言語から出
変数とメモリ.......................................... 3
力して、少しずつバイナリまで自前で出力する方向に持って行きま
4.1
メモリ ........................................... 3
4.2
初期値 ........................................... 3
4.2.1
mov............................................ 4
4.2.2
lea............................................ 4
4.3
退避 ............................................. 4
4.4
増減 ............................................. 4
4.5
メモリの増減 ..................................... 5
す。
2
具体的なコードについて取り上げる前に、私の考えについて簡単に
触れておきます。
ループ................................................ 5
5.1
相対ラベル ....................................... 5
5.2
前・後 ........................................... 5
5.3
ネスト ........................................... 5
2.1
i386(ELF) 非 PIC.................................. 6
6.2
i386(ELF) PIC .................................... 6
6.2.1
GOT............................................ 6
6.2.2
まとめ......................................... 7
6.3
x86_64(ELF) 非 PIC................................ 7
6.4
x86_64(ELF) PIC .................................. 7
6.5
i386(Mac) ........................................ 7
6.6
x86_64(Mac) ...................................... 8
7
おわりに.............................................. 8
8
これから.............................................. 8
8.1
OS 関連の勉強会................................... 8
8.2
その他の構想 ..................................... 8

ハッシュタグは #cmpimpl です。

今回掲載したサンプルコードはすべて CC0(いわゆるパブリッ
ら始める方が良いと考えていました。

64bit(x86_64)は 32bit(i386)の拡張なので、32bit 時代の事情
を知らないと理解できない仕様がある。

同じことは 32bit についても言えるが、16bit はセグメン
トを意識するなど 32bit より難しい点があるため、最初に
16bit をやるべきかは微妙。

Alpha のように最初から 64bit として設計された ISA なら
問題ないと思うが、Alpha はもう製造されていない。

64bit OS では 32bit コードも動くものがあるため、テストのた
めに OS を入れ替えることは少ないかも。

32bit を理解していれば独力で(わざわざ説明しなくても)
64bit 化できるのではないか。
あまりこれを強調すると押し付けになってしまうため、暗黙の了解
として敢えて触れないようにしていました。しかし大半の方が 64bit
で開発されていたため、32bit の説明がそのままでは適用できないケ
クドメイン)で提供します。
ースが続出して、混乱が生じました。
http://sciencecommons.jp/cc0/about

64bit
私自身は以下の理由により、マシン語の勉強は i386(32bit x86)か
位置独立コード........................................ 5
6.1
はじめに
実装例は以下を参照してください。
そもそも上記は一般論で、Brainf*ck のトランスレータを作成するの
https://github.com/mason-/cmpimpl/blob/master/python/bf
に歴史的背景の理解が本当に必要なのか?と考え直しました。ISA
tran.py
が何であれ、アセンブリ言語に慣れることが何よりも大事なはずで
す。そのため今回は 64bit についても説明します。
1
2.2
# putchar('A');
シンボル名のプレフィックス
pushl $65
システムコールを直接呼ぶと完全に OS に依存してしまうため、libc
calll _putchar
を呼び出す方法で説明しました。Win32 を念頭に説明したため C 関数
addl $4, %esp
のシンボル名にプレフィックスのアンダースコアを付けていました
が(例: _purchar)、プレフィックスが不要な ELF 環境では動かない
.intel_syntax noprefix
ため混乱が生じました。
# putchar('A');
push 65
勉強会での Mac 率を考えると 64bit ELF を前提にするのも躊躇する
call _putchar
ため、プレフィックスについては環境ごとに説明しようと思います。
add esp, 4
2.3
記法
3.1.1 スタックの効率化
私が慣れている Intel 記法で説明しました。しかし Mac の as は GNU
関数の呼び出し後に毎回スタックを片付けると冗長です。最初にス
binutils ではなく Intel 記法を受け付けないことが判明したため、
タックを確保して、複数の処理を行ってから、最後に片付ける方法
AT&T 記法が避けられなくなりました。
に書き換えます。
記法自体はアセンブリ言語の本質ではないため、あまり拘っても仕
# スタックを確保
方がありません。説明は AT&T 記法を中心にして、補助的に Intel 記
subl $4, %esp
法も併記する方針に切り替えようと思います。
# putchar('A');
movl $65, (%esp)
3
calll _putchar
呼び出し規約
# putchar('B');
今回のトランスレータでは、OS ごとの差異を吸収するためシステム
movl $66, (%esp)
コールを直接呼ばずに libc を呼びます。以下に呼び出し規約をまと
calll _putchar
めます。
# スタックを解放
addl $4, %esp
アーキテクチャ
引数
接頭辞
i386(Win)
スタック
あり
.intel_syntax noprefix
i386(ELF)
スタック
なし
# スタックを確保
i386(Mac)
スタック(16 バイトアラインメント)
あり
x86_64(Win)
x86_64(ELF)
x86_64(Mac)
スタック 32 バイト予約 + RCX, RDX, R8,
R9, スタック
RDI, RSI, RDX, RCX, R8, R9, スタック
ELF と同じ(スタックは 16 バイトアライ
ンメント)
sub esp, 4
# putchar('A');
なし
mov dword ptr [esp], 65
call _putchar
なし
# putchar('B');
あり
mov dword ptr [esp], 66
call _putchar
# スタックを解放
これらを具体的に見ていきます。
3.1
add esp, 4
i386(Win)
3.2
引数をスタックに積んで call します。スタックは呼び出し元で後片
i386(ELF)
シンボルに接頭辞を付けない以外は Windows と同じです。AT&T 記法
付けをします。シンボルに接頭辞としてアンダースコアを付けます。
のみ示します。
関数の戻り値は eax で返します。
subl $4, %esp
※この規約は cdecl と呼ばれています。スタックの後片付けが異な
movl $65, (%esp)
る stdcall という規約もありますが、ここでは取り上げません。
calll putchar
addl $4, %esp
AT&T 記法と Intel 記法を併記します。AT&T 記法では命令の後にサイ
ズを示す接尾辞 l(long=32bit)が付き、即値の接頭辞として$、レ
ジスタに%が付きます。2 オペランド命令の順番が逆になります。
2
3.3
movl $65, %edi
i386(Mac)
callq putchar
シンボルには接頭辞が付きます。関数呼び出し時の esp は 16 バイト
境界に揃える必要があります。SDK の as は GNU binutils ではなく
スタックの操作が発生しないため、非常に単純です。
AT&T 記法しか受け付けません。
3.6
x86_64(Mac)
※nasm などサードパーティーのアセンブラを使用すれば Intel 記法
も可能ですが、一部文法の違いによりコンパイラが出力したアセン
レジスタの使用方法は x86_64(ELF)と同じです。ただし i386(Mac)と
ブリを流用するときに修正が必要となります。書き方を調べるのに
同じように rsp は 16 バイト境界に揃える必要があります。
コンパイラの出力を利用するのが手軽なため、今回は AT&T 記法を使
用します。
subq $8, %rsp
movl $65, %edi
subl $12, %esp
callq _putchar
movl $65, (%esp)
addq $8, %rsp
calll _putchar
i386 の例と同じように、call で呼ばれた直後と仮定して rsp のアラ
addl $12, %esp
インメントを揃えています。
スタックを 12 バイト確保しているのは、この部分が call で呼ばれ
た直後だと仮定しているためです。call 命令は戻り先をスタックに
4
変数とメモリ
積むため、呼ばれた時点で既に 4 バイトが使用中となります。
Brainf*ck には変数が 1 つあります。トランスレータではこれをレジ
このようにアラインメントを揃える必要がある場合、i386(Win)で最
スタに割り当てます。
初に示した随時 push するやり方では毎回パディングが発生するため、
効率がかなり悪くなります。
ABI により関数呼び出しで破壊されないレジスタが決められていま
す。保存されるレジスタを使えば、値の破壊を気にせずに変数とほ
3.4
x86_64(Win)
ぼ等価に使用できます。今回は 32bit では esi、64bit では r12 を使
用します。
32bit ではレジスタが 8 個でしたが、64bit では倍の 16 個に増えま
した。レジスタに余裕が出来たため、引数も最初の 4 つまではレジ
※64bit で esi を使用すると、関数(putchar など)を呼び出したと
スタ(RCX, RDX, R8, R9)で渡すようになりました。ただしスタッ
きに値が破壊される可能性があります。
クはその 4 つのレジスタの分を確保しておく必要があります。呼び
出し元でスタックに値を入れる必要はありませんが、呼び出し先で
4.1
メモリ
必要に応じて退避するのに使用されます。
Brainf*ck の仕様では 30,000 バイトのメモリを持つと定義されてい
シンボルの接頭辞はありません。32bit と 64bit で接頭辞の扱いが異
ます。変数はメモリのアドレスを指すポインタとして使用します。
なるのは、今回取り上げた中では Windows だけです。
アセンブリでは.comm として bss にメモリを確保します。
subq $32, %rsp
movl $65, %ecx
# char mem[30000];
callq putchar
.comm mem, 30000
addq $32, %rsp
4.2
初期値
ecx を操作すると rcx の上位 32bit はクリアされます。そのため rcx
ではなく ecx に mov してもゴミは残りません。
mem の指すアドレスをレジスタに入れます。単純に mov すると mem
の指すメモリの中身が入ってしまうため、lea 命令を使用します。
3.5
x86_64(ELF)
# i386
Windows とは呼び出し規約が異なります。引数は最初の 6 つまでをレ
.comm mem, 30000
ジスタ(RDI, RSI, RDX, RCX, R8, R9)で渡して、残りをスタック
.text
で渡します。Windows のようにレジスタ渡しの分までスタックを確保
leal mem, %esi
しておく必要はありません。
※Mac OS X ではデフォルトで PIC(位置独立コード)を要求される
3
ため、単純に lea で処理すると警告されます。PIC は 6 位置独立コー
main は CRT から呼び出されます。esi/r12 は呼び出し元に対して値
ドで説明しています。
を保存しなければならないため、main から抜ける際に元の値に戻す
必要があります。そのため main の最初で push して、最後で pop し
ます。main 自体の戻り値は 0 とするため、eax に 0 を mov して ret
4.2.1 mov
します。
lea がどういう命令かを説明する前に、比較対象として mov 命令を見
main は通常の C 関数と同じ扱いのため、呼び出し規約で必要とされ
てみます。
る場合は接頭辞のアンダーバーを付けます。以下では接頭辞が必要
ない ELF の例を示します。
レジスタに即値を代入する場合、mov 命令を使うのが基本的な方法で
す。
# i386(ELF)
# eax = 0x1234
.comm mem, 30000
movl $0x1234, %eax # AT&T
.text
mov
.globl main
eax, 0x1234
# Intel
main:
AT&T 記法では$を外すとメモリの中身が対象となります。Intel 記法
pushl %esi
ではアドレスであることを示すブラケットで囲みます。
leal mem, %esi
# ...
# eax = *(long *)0x1234
popl %esi
movl 0x1234, %eax
movl $0, %eax
mov
# AT&T
eax, [0x1234] # Intel
ret
メモリが以下の内容であれば、eax=0x12345678 となります。
# x86_64(ELF)
.comm mem, 30000
00001230: 00 01 02 03 78 56 34 12 aa bb cc dd 00 00 00 00
.text
.globl main
main:
メモリから 1 バイトだけ取って上位をゼロで埋める場合は movzx 命
令を使用します。AT&T 記法では movzbl となりますが、ニーモニック
pushq %r12
が movz、ソースのサイズが b、デスティネーション(レジスタ)の
leaq mem, %r12
サイズが l となります。
# ...
popq %r12
# eax = *(char *)0x1234
movl $0, %eax
movzbl 0x1234, %eax
ret
movzx
# AT&T
eax, [0x1234] # Intel
4.4
増減
メモリが先ほど示した内容であれば、eax=0x78 となります。
本来、変数の値を増減させる際に、変数の指すアドレスがメモリ空
間からはみ出さないようにチェックする必要があります。今回は説
4.2.2 lea
明を単純化するためチェックは行いません。
指定したアドレスが指すメモリの内容ではなく、アドレスの値を代
Brainf*ck では変数は 1 ずつ増減します。x86 では 1 だけ増やす命令
入する命令が lea です。メモリの内容は一切関係ありません。
が inc、1 だけ減らす命令が dec です。
# eax = 0x1234
leal 0x1234, %eax
lea
# AT&T
# i386
eax, [0x1234] # Intel
incl %esi # esi++
decl %esi # esi—
先ほどの mem の例では、mem がアドレスとして扱われているため、メ
モリの中身ではなく、アドレスを値として取り出すのに lea を使用
# x86_64
しています。
incq %r12 # r12++
decq %r12 # r12—
4.3
退避
4
4.5
か後(b)かをラベルの接尾辞に添えて指定します。相対ラベルは重
メモリの増減
複しても構いません。
Brainf*ck にはメモリの中身を 1 バイト単位で増減させる命令があ
※前・後の方向については次のセクションで取り上げます。
ります。ニーモニックの接尾辞 b でバイト指定にして、オペランド
を括弧で囲みます。
0:
# i386
cmpb $0, (%esi)
incb (%esi) # (*(char *)esi)++
jz 0f # ↓方向に0:を探す
decb (%esi) # (*(char *)esi)—
# ...
jmp 0b # ↑方向に0:を探す
# x86_64
0:
incb (%r12) # (*(char *)r12)++
decb (%r12) # (*(char *)r12)—
前・後
5.2
AT&T 記法での括弧は、Intel 記法でのブラケットに相当します。AT&T
前か後かという表現はパーサの進行方向を基準としています。パー
記法での接尾辞が Intel 記法ではキーワードとして表記されるため、
サはプログラムを先頭から見ていくため、画面での下が前に相当し
見た目は長くなります。
ます。
# i386
後
.intel_syntax noprefix
↓
inc byte ptr [esi] # (*(char *)esi)++
↓
dec byte ptr [esi] # (*(char *)esi)—
↓
前
# x86_64
.intel_syntax noprefix
直感的に判断すると画面での上が前のように思えてしまうため、慣
inc byte ptr [r12] # (*(char *)r12)++
れが必要な表現です。
dec byte ptr [r12] # (*(char *)r12)-ネスト
5.3
5
ループ
ループがネストする場合、ネストレベルを相対ラベルの値に割り当
てます。うまくペアができていることを確認してください。
Brainf*ck では変数の指すメモリの中身が 0 以外の間だけ回るルー
プがあります。
0:
# while (*(char *)esi) { ... }
cmpb $0, (%esi)
start_loop:
jz 0f
1:
cmpb $0, (%esi)
jz end_loop
cmpb $0, (%esi)
# ...
jz 1f
# ...
jmp start_loop
jmp 1b
end_loop:
1:
jmp 0b
cmp 命令で 0 と比較して、一致した場合(jz)は end_loop に飛んで
抜けます。jmp は無条件で start_loop に飛びます。
0:
ループが複数ある場合、ラベル(start_loop, end_loop)に別の名
上記では読みやすいようにインデントしています。Python とは異な
前を付ける必要があります。連番を振れば簡単そうですが、ループ
り、インデントに文法的な意味はありません。
がネストすると厄介です。
6
5.1
位置独立コード
相対ラベル
Mac OS X ではデフォルトで位置独立コード(PIC)が要求されます。
ラベルを局所化するテクニックとして相対ラベルがあります。数字
プログラムがどのアドレスにロードされるか決められないため、グ
のみのラベルが相対ラベルとして扱われます。参照元から見て前(f)
ローバル変数のアドレスを即値ではなく、プログラムカウンタから
5
00000010 R_386_32
の相対で得たテーブルからポインタを取り出す必要があります。
mem
(後略)
非 PIC と PIC を比較するため、まずは ELF を見てみます。
6.2
i386(ELF) PIC
i386(ELF) 非 PIC
6.1
PIC ではコードがどのように変化するか見てみます。リンカによって
PIC ではないコードでは、オブジェクトコードの段階ではゼロが埋め
埋め込まれる値があまりにも多いため、逆アセンブルではなくコン
込まれて、リンク時にアドレスが埋め込まれます。
パイラのアセンブリ出力を見ます。
例として C 言語コードのコンパイルとリンクを追ってみます。テス
$ gcc -m32 -fPIC -S test.c
ト環境は NetBSD/amd64 で、gcc -m32 を指定して 32bit のコードを生
成しています。
mem にアクセスしている部分と、関係する部分を抜粋します。
test.c
call
__i686.get_pc_thunk.cx
int mem[1];
addl
$_GLOBAL_OFFSET_TABLE_, %ecx
movl
mem@GOT(%ecx), %eax
movl
$1, (%eax)
int main(void) {
mem[0] = 1;
return 0;
__i686.get_pc_thunk.cx:
}
movl
(%esp), %ecx
ret
C 言語と対比するため-g オプションでデバッグ情報を埋め込みます。
オブジェクトコードを生成して逆アセンブルします。
i386 ではプログラムカウンタ(eip)を直接取得することができない
ため、__i686.get_pc_thunk.cx で call 命令によってスタックに積ま
$ gcc -m32 -g -c test.c
れた戻り先アドレスを ecx に入れることで、間接的に eip を取得し
$ objdump -S test.o
ています。具体的には次のようなことがやりたかったのだと思われ
ます。
main()でグローバル変数が関係するのは以下の部分です。mem のアド
レスが 0 になっているのが分かります。
%eip, %ecx
addl
$_GLOBAL_OFFSET_TABLE_, %ecx
movl
mem@GOT(%ecx), %eax
movl
$1, (%eax)
mem[0] = 1;
movl
e:
15:
c7 05 00 00 00 00 01
movl
$0x1,0x0
00 00 00
6.2.1 GOT
リンクして逆アセンブルします。
ecx に足されている$_GLOBAL_OFFSET_TABLE_は定数ではなく、リンカ
$ gcc -m32 -o test test.o
がプログラムカウンタを考慮して値を埋め込みます。次の例を見る
$ objdump -S test
と理解しやすいかもしれません。
先ほどの部分に mem のアドレスが埋め込まれているのが分かります。
got.s
addl $_GLOBAL_OFFSET_TABLE_, %ecx
mem[0] = 1;
addl $_GLOBAL_OFFSET_TABLE_, %ecx
80486ea:
c7 05 d0 98 04 08 01
80486f1:
00 00 00
movl
$0x1,0x80498d0
$ gcc -m32 -nostdlib got.s
$ objdump -d a.out
オブジェクトファイルにはリンク時にアドレスを埋め込む場所が記
(中略)
録されています。リンカがこれを見てアドレスを埋め込みます。
8048074:
81 c1 0c 10 00 00
add
$0x100c,%ecx
804807a:
81 c1 06 10 00 00
add
$0x1006,%ecx
$ objdump -x test.o
(中略)
ecx に命令のアドレスが入っていれば、計算結果は常に同じアドレス
RELOCATION RECORDS FOR [.text]:
になります。
OFFSET
TYPE
VALUE
6

0x8048074 + 0x100c = 0x8049080

0x8049080 + 0x1006 = 0x8049080

Global Offset Table(GOT)とは、簡単に言えばグローバル変数や
0x40089e + 1049690 = 0x500cf8
6.4
x86_64(ELF) PIC
即値のポインタが入っているテーブルです。値そのものではなく、
PIC では 32bit と同じように GOT 経由でアクセスします。
ポインタが入っています。更に間接参照してようやく値に到達でき
ます。
$ gcc -fPIC -S test.c

eip → GOT → ポインタ → 目的の値
該当部分を抜粋します。
GOT に値を入れれば間接参照が 1 段階減りますが、巨大な配列を定義
すればすぐに GOT が埋まってしまうため、それを避けるためにこの
movq
mem@GOTPCREL(%rip), %rax
ような仕様になっていると思われます。プログラムカウンタに
movl
$1, (%rax)
$_GLOBAL_OFFSET_TABLE_を足して得られるアドレスは、GOT の先頭で
はなく中央です。相対アドレッシングは符号付きで表現されるため、
たった 1 命令で GOT からポインタを取得しています。GOT の中心から
負の値も活用するための仕様です。
相対計算せずにいきなり算出しています。32bit と比べて非常に単純
になっています。
ちなみに MIPS や Alpha では、GOT の中央を指す専用のレジスタ(GP)
が存在します。それらの CPU ではレジスタが 32 個と多いため、1 つ
6.5
i386(Mac)
くらいレジスタを割り当ててもほとんど問題にはなりません。
さて、いよいよ Mac OS X です。デフォルトで PIC となります。
6.2.2 まとめ
$ gcc -m32 -S test.c
まとめると、プログラムカウンタ相対で GOT(ポインタが集まってい
るテーブル)のアドレスを求めて、そこからポインタを取り出して
該当部分を抜粋します。
目的のアドレスにアクセスするわけです。
.comm
x86_64(ELF) 非 PIC
6.3
_mem,4
(中略)
call
64bit ではプログラムカウンタ(rip)が直接取得できるようになっ
___i686.get_pc_thunk.cx
L00000000001$pb:
たため、即値は埋め込まずにプログラムカウンタ相対でアドレスを
取得します。
leal
L_mem$non_lazy_ptr-L00000000001$pb(%ecx), %e
movl
(%eax), %eax
movl
$1, (%eax)
ax
$ gcc -S test.c
(中略)
.section __TEXT,__textcoal_nt,coalesced,pure_instruc
該当部分を抜粋します。
tions
movl
$1, mem(%rip)
.weak_definition
___i686.get_pc_thunk.cx
.private_extern ___i686.get_pc_thunk.cx
逆アセンブルすると以下の通りです。
___i686.get_pc_thunk.cx:
movl
(%esp), %ecx
$ gcc -g test.c
ret
$ objdump -S a.out
.section __IMPORT,__pointers,non_lazy_symbol_pointer
(中略)
s
mem[0] = 1;
400894: c7 05 5a 04 10 00 01
L_mem$non_lazy_ptr:
movl $0x1,1049690(%rip)
.indirect_symbol _mem
# 500cf8<mem>
.long
0
40089b: 00 00 00
.subsections_via_symbols
(後略)
ELF での$_GLOBAL_OFFSET_TABLE_に相当する部分が自動ではなく泥
臭い表現になっていますが、やっていることはほぼ同じです。
プログラムカウンタは次の命令を指しているため、相対アドレスを
計算すると以下の通りとなります。これがコメントの意味です。
7
6.6
のネイティブ呼び出しに挑戦
x86_64(Mac)

PE 勉強会 CIL 編
.NET のバイナリを分析。CIL はいわゆるバイトコード。
シンボルにプレフィックスが付いている以外は ELF の PIC と同じで

す。
PE32+勉強会
64bit 用の PE。32bit との比較を中心に。

$ gcc -S test.c
ユーザーモードインタプリタ実装会
x86 以外の CPU を実装しながら勉強

該当部分を抜粋します。
デバイスエミュレータ実装会(NetBSD 編)
ドライバのソースを見ながら動かす
movq
_mem@GOTPCREL(%rip), %rax
movl
$1, (%rax)
7

Alpha や PowerPC の NT を動かすことを目指す
おわりに
以上で Mac/Win/ELF で Brainf*ck のアセンブリ言語トランスレータ
の実装に必要な知識の説明は終了です。複数のアーキテクチャを比
較したため複雑になりました。しかし自分の使っているアーキテク
チャで実装したいというのが人情ですから、ある程度は網羅的な説
明になるのは避けられません。
※こうなると収集が付かなくなる恐れがあるので、入り口として
i386-pe に的を絞ったのが PE 勉強会だったというわけです。
8
これから
Brainf*ck で実行可能形式バイナリが出力できるようになれば、次は
Forth のようなスタック式の逆ポーランド表記言語に進むのが良い
と思います。
8.1
OS 関連の勉強会
コンパイラがある程度動くようになれば、それを使って OS を作って
みるのが面白いと考えています。

DOS COM 勉強会
COM(16bit x86 コード)を出力するコンパイラを作る

ブートローダ実装会
DOS COM 勉強会で作った 16bit コンパイラを使って実装

プロテクトモード勉強会
16bit の世界から 32bit の世界へ

OS 実装会
プロテクトモードに入れば PE 用コンパイラが使えるよ!
8.2
その他の構想
こんなことができたら楽しいなと思っているもの。

COM 勉強会
Windows の Component Object Model をメモリレイアウトや PE
の観点から探る

デバイスエミュレータ実装会(NT 編)
WP7 ネイティブ DLL 勉強会
ARM バイナリと COM の知識を組み合わせて、Windows Phone 7
8