コンパイラ作成(50) 引数のある関数の呼び出しを修正
今回の目標
前回、Segmentation faultになっちゃったのを頑張って修正するよ。
// 引数のある関数の呼び出し int main() { int a = 5, b = 12, c; printf("%d + %d = %d\n",a,b,add(a,b)); }
printfを呼ぶためにセットしたレジスタの値が、addの呼び出しで壊されちゃってた。レジスタを退避して壊れなくするよ。
initialize
管理用の変数を追加。
# コンストラクタ def initialize(fname) @fname = fname # ソースファイルのファイル名 @asmfname = fname.sub(/\.myc$/,'.s') # アセンブリコードのファイル名 =begin @exefname = fname.sub(/\.myc$/,'') # 実行ファイル名 =end @regs32 = ["edi", "esi","edx","ecx","r8d","r9d"] # 32bitレジスタ @regs64 = ["rdi", "rsi","rdx","rcx","r8", "r9" ] # 64bitレジスタ @lex = Lexer.new(@fname) # 字句解析 @funcname = nil # 現在処理している関数名 @labelcnt = nil # 自動生成するラベルの個数(関数単位) @literalcnt = 0 # 文字列リテラルの数 @literaltable = [] # 文字列リテラルのリスト =begin @needprint = false # print.oのリンクが必要か =end @functions = Hash.new # 関数 @lvars = nil # ローカル変数 @lvarsize = nil # スタックに確保する領域のサイズ @breaklabel = nil # breakの飛び先のラベル @codebuffer = [] # コードバッファ @numuseregs = 0 # 関数コールで使用しているレジスタの数 end
numuseregsってやつね。あと、regs32とregs64をここに持ってきたよ。
codegen_func
レジスタの退避、復元の処理を追加。
# 関数コールのコード生成 def codegen_func operand if @numuseregs != 0 then if @numuseregs % 2 == 1 then codegen " sub rsp, 8" end (0...@numuseregs).each do |i| codegen " push #{@regs64[i]}" end end (0...operand.size-2).each do |i| save = @numuseregs @numuseregs = i codegen_el operand[i+2] @numuseregs = save codegen " mov #{@regs32[i]}, eax" end codegen " call " + operand[0].str if @numuseregs != 0 then (0...@numuseregs).reverse_each do |i| codegen " pop #{@regs64[i]}" end if @numuseregs % 2 == 1 then codegen " add rsp, 8" end end end
push/popするよりmovでスタック上に移動する方が、実行サイクル上有利なのかなあ。分かり易いんでこうしちゃったんだけど。それと退避するレジスタが奇数の場合は、rspを調整して16byte境界になるようにしてる。
printfの処理
numuseregsを設定してるよ。
# 標準関数printfの処理 kind, str = @lex.gettoken if kind != TK::SYMBOL || str != "(" then perror end kind, str = @lex.gettoken i = 0 loop do if kind == TK::STRING then label = addliteral str codegen " lea "+@regs64[i]+", "+label kind, str = @lex.gettoken else save = @numuseregs @numuseregs = i kind, str = expr kind, str @numuseregs = save codegen " mov "+@regs32[i]+", eax" end if kind == TK::SYMBOL && str == ")" then break end if kind != TK::SYMBOL || str != "," then perror end kind, str = @lex.gettoken i += 1 end codegen " mov al, 0" codegen " call printf" kind, str = @lex.gettoken if kind != TK::SYMBOL || str != ";" then perror "expected ';' after function" end
早くこの処理を削除したいよ。すべての関数コールをexprで処理できたら削除できるんだけどね。その為にはもうちょっと頑張らないとね。printfの最初の引数の型がchar*なんだけど、これに対応するためにはexprで型を意識しないとダメ。
動作テスト
どうかな。codegen_funcが再帰でcodegen_elを呼んでたりしてるんで、正しくコーディングできてるか心配だよ。考えてるうちに頭の中がこんがらがってくるしね。
~/myc$ myc o4.myc add2.o ~/myc$ ./o4 add: a = 5 b = 12 5 + 12 = 17 ~/myc$
ふう、ちゃんと動いたよ。
.intel_syntax noprefix .global main main: push rbp mov rbp, rsp sub rsp, 16 mov eax, 5 mov dword ptr [rbp - 4], eax mov eax, 12 mov dword ptr [rbp - 8], eax lea rdi, .L.str mov eax, dword ptr [rbp - 4] mov esi, eax mov eax, dword ptr [rbp - 8] mov edx, eax sub rsp, 8 push rdi push rsi push rdx mov eax, dword ptr [rbp - 4] mov edi, eax mov eax, dword ptr [rbp - 8] mov esi, eax call add pop rdx pop rsi pop rdi add rsp, 8 mov ecx, eax mov al, 0 call printf .RET_main: add rsp, 16 pop rbp ret .L.str: .asciz "%d + %d = %d\n"
addのcallの前後でちゃんとpush/popされてる。これで引数のある関数の呼び出しはできるようになったよ。次は呼ばれる方か。これもやっかいだな。レジスターに設定されてる引数を保存しとかないといけないしね。なんだかx86_64のプログラミングって面倒なこと多いな。昔のCPUはもっと扱いが楽だったよね。今日はここまで。