【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

Brainf*ckトランスレータ (2) 呼び出し規約

前回に引き続きBrainf*ckのトランスレータを見ていきます。

今回のトランスレータでは、OSごとの差異を吸収するためシステムコールを直接呼ばずにlibcを呼びます。以下に呼び出し規約をまとめます。

arch 引数 接頭辞
i386(Win) スタック あり
i386(ELF) スタック なし
i386(Mac) スタック(16バイトアラインメント) あり
x86_64(Win) スタック32バイト予約 + RCX, RDX, R8, R9, スタック なし
x86_64(ELF) RDI, RSI, RDX, RCX, R8, R9, スタック なし
x86_64(Mac) ELFと同じ(スタックは16バイトアラインメント) あり

これらを具体的に見ていきます。

i386(Win)

引数をスタックに積んでcallします。スタックは呼び出し元で後片付けをします。シンボルに接頭辞としてアンダースコアを付けます。関数の戻り値はeaxで返します。

※この規約はcdeclと呼ばれています。スタックの後片付けが異なるstdcallという規約もありますが、ここでは取り上げません。

AT&T記法とIntel記法を併記します。AT&T記法では命令の後にサイズを示す接尾辞l(long=32bit)が付き、即値の接頭辞として$、レジスタに%が付きます。2オペランド命令の順番が逆になります。

# putchar('A');
pushl $65
calll _putchar
addl $4, %esp
.intel_syntax noprefix
# putchar('A');
push 65
call _putchar
add esp, 4
スタックの効率化

関数の呼び出し後に毎回スタックを片付けると冗長です。最初にスタックを確保して、複数の処理を行ってから、最後に片付ける方法に書き換えます。

# スタックを確保
subl $4, %esp
# putchar('A');
movl $65, (%esp)
calll _putchar
# putchar('B');
movl $66, (%esp)
calll _putchar
# スタックを解放
addl $4, %esp
.intel_syntax noprefix
# スタックを確保
sub esp, 4
# putchar('A');
mov dword ptr [esp], 65
call _putchar
# putchar('B');
mov dword ptr [esp], 66
call _putchar
# スタックを解放
add esp, 4

i386(ELF)

シンボルに接頭辞を付けない以外はWindowsと同じです。AT&T記法のみ示します。

subl $4, %esp
movl $65, (%esp)
calll putchar
addl $4, %esp

i386(Mac)

シンボルには接頭辞が付きます。関数呼び出し時のespは16バイト境界に揃える必要があります。SDKのasはGNU binutilsではなくAT&T記法しか受け付けません。

※nasmなどサードパーティーのアセンブラを使用すればIntel記法も可能ですが、一部文法の違いによりコンパイラが出力したアセンブリを流用するときに修正が必要となります。書き方を調べるのにコンパイラの出力を利用するのが手軽なため、今回はAT&T記法を使用します。

subl $12, %esp
movl $65, (%esp)
calll _putchar
addl $12, %esp

スタックを12バイト確保しているのは、この部分がcallで呼ばれた直後だと仮定しているためです。call命令は戻り先をスタックに積むため、呼ばれた時点で既に4バイトが使用中となります。

このようにアラインメントを揃える必要がある場合、i386(Win)で最初に示した随時pushするやり方では毎回パディングが発生するため、効率がかなり悪くなります。

x86_64(Win)

32bitではレジスタが8個でしたが、64bitでは倍の16個に増えました。レジスタに余裕が出来たため、引数も最初の4つまではレジスタ(RCX, RDX, R8, R9)で渡すようになりました。ただしスタックはその4つのレジスタの分を確保しておく必要があります。呼び出し元でスタックに値を入れる必要はありませんが、呼び出し先で必要に応じて退避するのに使用されます。

シンボルの接頭辞はありません。32bitと64bitで接頭辞の扱いが異なるのは、今回取り上げた中ではWindowsだけです。

subq $32, %rsp
movl $65, %ecx
callq putchar
addq $32, %rsp

ecxを操作するとrcxの上位32bitはクリアされます。そのためrcxではなくecxにmovしてもゴミは残りません。

x86_64(ELF)

Windowsとは呼び出し規約が異なります。引数は最初の6つまでをレジスタ(RDI, RSI, RDX, RCX, R8, R9)で渡して、残りをスタックで渡します。Windowsのようにレジスタ渡しの分までスタックを確保しておく必要はありません。

movl $65, %edi
callq putchar

スタックの操作が発生しないため、非常に単純です。

x86_64(Mac)

レジスタの使用方法はx86_64(ELF)と同じです。ただしi386(Mac)と同じようにrspは16バイト境界に揃える必要があります。

subq $8, %rsp
movl $65, %edi
callq _putchar
addq $8, %rsp

i386の例と同じように、callで呼ばれた直後と仮定してrspのアラインメントを揃えています。