【脱糖】命令をいくつか禁止して学ぶアセンブリの関数

様々な事柄について学ぶ際、その本質を学ぶためには、あえて縛りを設け、強制的に代替手法を用いなければならない状況を作り出すことは有効な方法だと思います。
そこで今回は単純な関数呼び出しを行うアセンブリプログラムをアセンブリへ翻訳し、そのアセンブリからいくつかの命令(ret,pop,push)を簡単な命令で書き直し、アセンブラの基礎的な命令の流れをあらわにしました。
※今回はこのいくつかの命令を禁止して翻訳することを便宜上脱糖と表現します。脱糖という言葉の正確性に関して少し懸念が残るかもしれませんがご了承ください。
この記事内で登場するコードはhttps://github.com/yutadd/begin_IA-32_assemblyへプッシュしました
前提知識として、今回縛るそれぞれの命令は以下のような対応表により、簡単な命令へ変換できます。
甘いコード苦いコード
push EBPsub ESP,4
mov dword
call calcsub ESP,4
mov dword [ESP],_return_address
jmp calc
popmov EBP, [ESP]
add ESP,4
ret 8mov EBX, [ESP]
add ESP,12
jmp EBX
この対応表のそれぞれの命令とその対応については後半で説明したいと思います。

それでは翻訳と脱糖を行っていきます。
1.翻訳前
まずは単純な関数呼び出しを行うpythonプログラム
def calc(x:int,y:int)->int:
return x+y
if __name__=="__main__":
           calc(0x42,0x2f)

言うまでもないですが、実行結果は下図のように、113(10)が関数の結果として返されました。
※値をデバッガで参照するためにeaxという変数に結果を代入するようにしています
2.翻訳後(未脱糖)
まだ脱糖はせず、先ほどのpythonコードを単純に変換しました。
section .data
section .text
    global _start
calc:
push EBP
mov EBP, ESP
mov EAX,[EBP+8]
add EAX,[EBP+12]
pop EBP
ret 8

_start:
push 42h
push 2fh
call calc
mov EAX,1
mov EBX,0
int 0x80

Pythonのコードよりは長くなりましたが、未だかなりシンプルにできています。
これはPushやRet、pop、callなどの命令を多く使うことで、命令が圧縮されていることで実現されています。
※なお、最後の3行はexitのシステムコールです。
出力が正しいことをGDBデバッガを使って確認してみました。
_startのcallあとにブレークポイントを設置した結果、きちんと113が計算結果として出力されていることがわかります。

3.翻訳+脱糖後のコード
いくつかの命令を禁止し、置き換えた最終的なコード。
 
section .data
section .text
    global _start
calc:
    sub ESP,4
    mov [ESP],EBP
    mov EBP, ESP
    mov EAX,[EBP+8]
    add EAX,[EBP+12]
    mov EBP, [ES]
    add ESP,4
    mov EBX, [ESP]
    add ESP,12
    jmp EBX
 
_start:
    sub ESP,4
    mov dword [ESP],42h
    sub ESP,4
    mov dword [ESP],2fh
    sub ESP,4
    mov dword [ESP],_return_address
    jmp calc
 
_return_address:
    mov EAX,1
    mov EBX,0
    int 0x80

コード自体が長くなり、関数(ラベル)も増えましたね。
実行結果も正常でした。
肝心の変更点はおおむね文頭にある対応表に基づいています。対応表再掲↓
甘いコード苦いコード
push EBPsub ESP,4
mov dword
call calcsub ESP,4
mov dword [ESP],_return_address
jmp calc
pop EBPmov EBP, [ESP]
add ESP,4
ret 8mov EBX, [ESP]
add ESP,12
jmp EBX
push命令は、スタックポインタレジスタの値を減算したうえで空いたスペースにデータを格納することで実装しました。popはその逆で、初めにデータを取り出して、そのあとでスタックポインタレジスタの値を増加させます。
次にretはその関数が使用したヒープ容量分スタックを縮小したうえで、jmpを行うことで実現しました。
callはリターンアドレスをスタックに格納してすぐにjmpを行うように実装しました。
つまり、関数の呼び出しと返却は、次の流れで行われるようにしました。
1.リターンアドレスをスタックへ積む
2.関数先頭へジャンプ
3.必要な時に必要なだけスタック領域を確保する
4.使った分のスタックを開放する
5.リターンアドレスへジャンプする
ちなみにわざわざ_return_address:関数(ラベル)を作成した理由は、EIP(命令ポインタレジスタ)に直接アクセスできないためです。EIPに直接アクセスできないと、リターンアドレスを簡単に設定できません。そのため今回は、EIPの代わりに関数を新しく作り、その関数のアドレスをリターンアドレスとして使用したのです。
このようにして脱糖してみたことで、コンピュータで動作しているプログラムが、非常に単純な命令の塊でできていることがわかります。

まとめ
いくつかの命令を禁止した状態で単純な関数の呼び出しを行うコードを書いてみました。これにより、callやretなどの、高度な命令も、単純な命令を組み合わせて実現されていることを実感することができました。また、関数の呼び出しが行われる様子を改造度高く記述することで、アセンブリ及びコンピュータの仕組みに迫ることができました。

ささいな気づき:「だっとう」って打つと予測変換で「脱党」になるんだけど、「だつとう」って打つときちんと「脱糖」になるんですね。