コンパイラ作成(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先輩だ。
この件はもうちょっと考えることにして今回はここまでにするよ。なんか中途半端だけどさ。