コンパイラ作成(49) 引数のある関数の呼び出し
今回の目標
以前、引数のない関数の呼び出しはサポートしたんだけど、引数がある場合は未サポートのままになってたよ。
// 引数のある関数の呼び出し int main() { int a = 5, b = 12, c; c = add(a,b); printf("%d + %d = %d\n",a,b,c); }
今回はこれを頑張ってコンパイルできるようにするよ。
read_el
引数を含めて読み込むようにする。
# 式の最後までのトークンを読み込む # 返り値 # el : expression's token list # kind : 処理しなかったトークン(セミコロンもしくは閉じ括弧) # str : 処理しなかったトークン(セミコロンもしくは閉じ括弧) def read_el(fkind,fstr,skind,sstr) el = [] if fkind == TK::ID && sstr == "(" then # 関数呼出の処理 sel = [] sel << Token.new(fkind,fstr) sel << Token.new(skind,"()") skind, sstr = @lex.gettoken loop do if skind == TK::SYMBOL && sstr == ")" then break end fkind, fstr = skind, sstr skind, sstr = @lex.gettoken pel, skind, sstr = read_el fkind, fstr, skind, sstr sel << pel if skind == TK::SYMBOL && sstr == "," then skind, sstr = @lex.gettoken elsif skind != TK::SYMBOL || sstr != ")" then perror end end el << sel if skind != TK::SYMBOL || sstr != ")" then perror end skind, sstr = @lex.gettoken elsif fstr == "(" then # 括弧の処理 fkind, fstr = skind, sstr skind, sstr = @lex.gettoken sel, skind, sstr = read_el fkind, fstr, skind, sstr el << sel skind, sstr = @lex.gettoken else el << Token.new(fkind,fstr) end loop do if skind == TK::EOF then break end if sstr == ";" then break end if sstr == ")" then break end if sstr == "," then break end if sstr == "}" then break end fkind, fstr = skind, sstr skind, sstr = @lex.gettoken if fkind == TK::ID && sstr == "(" then # 関数呼出の処理 sel = [] sel << Token.new(fkind,fstr) sel << Token.new(skind,"()") skind, sstr = @lex.gettoken loop do if skind == TK::SYMBOL && sstr == ")" then break end fkind, fstr = skind, sstr skind, sstr = @lex.gettoken pel, skind, sstr = read_el fkind, fstr, skind, sstr sel << pel if skind == TK::SYMBOL && sstr == "," then skind, sstr = @lex.gettoken elsif skind != TK::SYMBOL || sstr != ")" then perror end end el << sel if skind != TK::SYMBOL || sstr != ")" then perror end skind, sstr = @lex.gettoken elsif fstr == "(" then # 括弧の処理 fkind, fstr = skind, sstr skind, sstr = @lex.gettoken sel, skind, sstr = read_el fkind, fstr, skind, sstr el << sel skind, sstr = @lex.gettoken else el << Token.new(fkind,fstr) end end return el, skind, sstr end
このルーチン同じようなことを二箇所でやっててイマイチだよなあ。どうにかしてすっきり書けないかなあ。
codegen_func
新しいメソッドを追加。
# 関数コールのコード生成 def codegen_func operand regs32 = ["edi", "esi","edx","ecx","r8d","r9d"] regs64 = ["rdi", "rsi","rdx","rcx","r8", "r9" ] (0...operand.size-2).each do |i| codegen_el operand[i+2] codegen " mov "+regs32[i]+", eax" end codegen " call " + operand[0].str end
今まではcall命令を一個生成する単純な処理だったけど、引数を処理しないといけないんで新設したよ。今のところint型以外の引数は扱えないからregs64はいらないんだけど、将来のことを考えて入れてみたよ。regs32とreges64はここじゃなく、initializeに持ってった方が良いかもなあ。定数だもんなあ。ここに置いておくと毎回Arrayの作成が起こるしなあ。
コード生成部
最後にここを修正。
# 式のコード生成(二項演算の左側被演算子) def codegen_elf(operand) if operand.kind_of?(Array) then if !operand[0].kind_of?(Array) && operand[0].kind == TK::ID && operand[1].str == "()" then codegen_func operand else codegen_el operand end elsif operand.kind == TK::NUMBER then codegen " mov eax, " + operand.str elsif operand.kind == TK::ID then v = @lvars[operand.str] if v == nil then perror "undeclared variable \"" + operand.str + "\"" end codegen " mov eax, dword ptr [rbp - " + v[1].to_s + "]" end end # 式のコード生成(二項演算の右側被演算子) def codegen_els(op, operand) if op.str == "+" then ostr = "add " elsif op.str == "-" then ostr = "sub " elsif op.str == "*" then ostr = "mul " elsif op.str == "/" then ostr = "div " elsif op.str == "==" then ostr = "cmp " elsif op.str == "!=" then ostr = "cmp " elsif op.str == "<" || op.str == "<" || op.str == ">" || op.str == "<=" || op.str == ">=" then ostr = "cmp " else perror "unknown operator \"" + op.str + "\"" end if operand.kind_of?(Array) then if operand[0].size == 2 && operand[0].kind == TK::ID && operand[1].str == "()" then codegen " sub rsp, 8" codegen " push rax" codegen_func operand codegen " mov r10d, eax" codegen " pop rax" codegen " add rsp, 8" else codegen " sub rsp, 8" codegen " push rax" codegen_el operand codegen " mov r10d, eax" codegen " pop rax" codegen " add rsp, 8" end str = "r10d" elsif operand.kind == TK::ID then v = @lvars[operand.str] if v == nil then perror "undeclared variable \"" + operand.str + "\"" end str = "dword ptr [rbp - " + v[1].to_s + "]" elsif operand.kind == TK::NUMBER then str = operand.str end if op.str == "==" then codegen " " + ostr + " eax, " + str codegen " sete al" codegen " and eax, 1" elsif op.str == "!=" then codegen " " + ostr + " eax, " + str codegen " setne al" codegen " and eax, 1" elsif op.str == "<" then codegen " " + ostr + " eax, " + str codegen " setl al" codegen " and eax, 1" elsif op.str == ">" then codegen " " + ostr + " eax, " + str codegen " setg al" codegen " and eax, 1" elsif op.str == "<=" then codegen " " + ostr + " eax, " + str codegen " setle al" codegen " and eax, 1" elsif op.str == ">=" then codegen " " + ostr + " eax, " + str codegen " setge al" codegen " and eax, 1" elsif op.str == "*" || op.str == "/" then if str != "r10d" then codegen " mov r10d, " + str end codegen " mov r11, rdx" if op.str == "/" then codegen " xor edx, edx" end codegen " " + ostr + " r10d" codegen " mov rdx, r11" else codegen " " + ostr + " eax, " + str end end
新設したcodegen_funcを二箇所で呼んでる。
動作テスト
さてさてどうかな。あ、そうだ。関数呼出の処理は作ったけど、呼び出される関数はまだ駄目だったよ。てことでclang先輩にお願いすることにした。
int add(int a, int b) { return a + b; }
これをコンパイルしてadd.oを作っといたよ。それではテスト。
~/myc$ myc o.myc add.o ~/myc$ ./o 5 + 12 = 17 ~/myc$
動いたみたい。でもちゃんと引数が渡ってるか分かり辛いな。
#include <stdio.h> int add(int a, int b) { printf("add: a = %d b = %d\n", a, b); return a + b; }
add.cをちょっと弄ったadd2.cを作成。
~/myc$ myc o.myc add2.o ~/myc$ ./o add: a = 5 b = 12 5 + 12 = 17 ~/myc$
うん、ちゃんと引数渡ってる。引数付きの関数コールできたっぽい。もうちょっとテストしてみるよ。
// 引数のある関数の呼び出し int main() { int a = 5, b = 12, c; printf("%d + %d = %d\n",a,b,add(a,b)); }
どうかな。
~/myc$ myc o4.myc add2.o ~/myc$ ./o4 add: a = 5 b = 12 Segmentation fault (コアダンプ) ~/myc$
ありゃ。駄目じゃん。えーとなんでだろ。あ、そっかprintfの引数をレジスタに格納したのに、addの呼び出しで同じレジスタに引数を格納しちゃってるからか。関数呼出が入子になってるときはレジスタを保存しなきゃいけないんじゃんか。うーん、毎回がーっとレジスタを退避するのは簡単だけど、それだと無駄になる場合もあるのか。もうちょっと賢くしたいなあ。簡単にできるかなあ。
.text .intel_syntax noprefix .file "../o4.myc" .globl main # -- Begin function main .p2align 4, 0x90 .type main,@function main: # @main # BB#0: push rbp mov rbp, rsp sub rsp, 32 mov dword ptr [rbp - 4], 5 mov dword ptr [rbp - 8], 12 mov esi, dword ptr [rbp - 4] mov edx, dword ptr [rbp - 8] mov edi, dword ptr [rbp - 4] mov eax, dword ptr [rbp - 8] mov dword ptr [rbp - 16], esi # 4-byte Spill mov esi, eax mov al, 0 mov dword ptr [rbp - 20], edx # 4-byte Spill call add movabs rdi, .L.str mov esi, dword ptr [rbp - 16] # 4-byte Reload mov edx, dword ptr [rbp - 20] # 4-byte Reload mov ecx, eax mov al, 0 call printf xor ecx, ecx mov dword ptr [rbp - 24], eax # 4-byte Spill mov eax, ecx add rsp, 32 pop rbp ret .Lfunc_end0: .size main, .Lfunc_end0-main # -- End function .type .L.str,@object # @.str .section .rodata.str1.1,"aMS",@progbits,1 .L.str: .asciz "%d + %d + 10 = %d\n" .size .L.str, 19 .ident "clang version 5.0.1-4 (tags/RELEASE_501/final)" .section ".note.GNU-stack","",@progbits
同じソースをclangでコンパイルしてみたら、ちゃんとレジスタを退避してる。さすがclang先輩だ。
この件はもうちょっと考えることにして今回はここまでにするよ。なんか中途半端だけどさ。