コンパイラ作成(63) void*型・size_t型

今回の目標

いきなりmalloc。いきなりvoid*。いきなりsize_t。

// void*, size_t
extern void *malloc(size_t size);

int main()
{
    char *buffer = malloc(256);
}

mallocなら簡単に呼べるかと思ったんだけど、現実は厳しいよ。void*型はchar*型に置き換えちゃえば良いんだけど、size_t型の方はそうはいかないよ。LP64だとsize_tはunsigned longだからintで済ますのは無理。ビット数が足りないよ。とりあえず今回はmallocが動くのを目標に最低限の修正であちこち手抜きで行くよ。
void*型ってそもそもなんだっけってことでまずはおさらい。C言語ではvoid*は他のポインタ型と暗黙の型変換が可能。c++ではキャストしないとvoid*型から他のポインタ型へ変換できない(逆は可)。確かこういう仕様だったよね。

typeword

修正開始。

    @typeword = [
      "extern","void","int","char","size_t"
    ]

二つの型を追加。

sizeof

ここも修正。

  # pointer型?
  def is_pointer_type?(type)
    return type[-1] == "*"
  end

  # 型のサイズ
  def sizeof(type)
    if is_pointer_type?(type) then return 8 end
    case type
      when "int"
        return 4
      when "size_t"
        return 8
=begin
      when "char*"
        return 8
      when "void*"
        return 8
=end
      else
        perror "unknown type '#{type}'"
      end
  end

分かり易くするためにis_pointer_type?ってのを追加してみたよ。c++だとこういうのにはinlineを付けたくなるんだけどrubyだとinlineとかmacroとかできないのかな。話が逸れたけどこれでポインタ型全部に対応できたよ。

codegen_assign

暗黙の型変換。

  # 代入のコード生成
  def codegen_assign(el)
    if el.size != 3 then perror end
    type_r = codegen_el [el[2]]
    if el[0].kind_of?(Array) then perror end
    if el[0].kind != TK::ID then perror end
    v = @lvars[el[0].str]
    if v == nil then
      perror "undeclared variable \"" + el[0].str + "\""
    end
    type_l = v[0]
    if type_r == "void*" && is_pointer_type?(type_l) then
      type_r = type_l    # 暗黙の型変換
    end
    if type_l != type_r then perror end
    if type_l == "char*" then
      codegen "  mov  qword ptr [rbp - " + v[1].to_s + "], rax"
    else
      codegen "  mov  dword ptr [rbp - " + v[1].to_s + "], eax"
    end
    return type_l
  end

ポインタ同士なんで実際の変換処理はいらないよ。size_tに関しては何もやってないからsize_t型の計算とか代入とか動かないよ。これ全部頑張るのは大変だな。

codegen_func

void*型・size_t型の処理を追加。

  # 関数コールのコード生成
  def codegen_func operand
    rettype = "int"
    f = @functions[operand[0].str]
    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
    if f != nil then
      if operand.size - 2 != f[1].size then
        perror "wrong number of parameters"
      end
    end
    (0...operand.size-2).each do |i|
      save = @numuseregs
      @numuseregs = i
      type = codegen_el operand[i+2]
      @numuseregs = save
      if f != nil then
        if f[1][i] == "size_t" then
          codegen "  movsx rax, eax"
          type = f[1][i]
        elsif f[1][i] == "void*" && is_pointer_type?(type) then
          type = f[1][i]
        end
        if type != f[1][i] then perror "incompatible type parameter" end
      end
      if type == "int"
        codegen "  mov  #{@regs32[i]}, eax"
      elsif type == "size_t"
        codegen "  mov  #{@regs64[i]}, rax"
=begin
      elsif type == "char*"
        codegen "  mov  #{@regs64[i]}, rax"
      elsif type == "void*"
        codegen "  mov  #{@regs64[i]}, rax"
=end
      elsif is_pointer_type?(type) then
        codegen "  mov  #{@regs64[i]}, rax"
      else
        perror
      end
    end
    codegen "  call " + operand[0].str
    if f != nil then
      rettype = f[0]
    end
    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
    return rettype
  end

size_t型の時はmovsxをかますようにしてるけど、間に合わせの論理で問題ありありだよ。これだと32bitより大きい値に対応できないよね。この辺は将来見直すよ。long型がちゃんとサポートできてからの話だな。

動作テスト

さてどうかな。

~/myc$ myc o18.myc
~/myc$ ./o18
~/myc$

コンパイルはできたけど合ってるのかな。

.intel_syntax noprefix
.global main
main:
  push rbp
  mov  rbp, rsp
  sub  rsp, 16
  mov  eax, 256
  movsx rax, eax
  mov  rdi, rax
  call malloc
  mov  qword ptr [rbp - 8], rax
.RET_main:
  add  rsp, 16
  pop  rbp
  ret

アセンブリコード見てみたけど大丈夫そうかな。malloc使ったプログラム組んでみたよ。

// repl
extern void *malloc(size_t size);
extern void free(void *ptr);
extern char *gets(char *s);
extern int puts(char *s);
extern int strcmp(char *string1, char *string2);

int main()
{
    char *buffer = malloc(256);
    for(;;) {
        printf("repl:");
        gets(buffer);
        if(strcmp(buffer,"quit") == 0) break;
        printf("=>");
        puts(buffer);
   }
   free(buffer);
}

どうかな。

~/myc$ myc o19.myc
/tmp/o19-750d92.o: 関数 `main' 内:
(.text+0x37): 警告: the `gets' function is dangerous and should not be used.
~/myc$ ./o19
repl:this is a pen
=>this is a pen
repl:2019
=>2019
repl:12+3*5
=>12+3*5
repl:quit
~/myc$

動いた。気分だけrepl。evalしてないからそのまま表示するだけ。今回の修正はかなり手抜きのいい加減なものだよ。それでも出来上がったものには結構満足してるよ。本当はもうちょっと頑張って電卓っぽくしたかったんだけど、それにはまだまだやらなきゃいけないこと多いな。
getsに関するワーニングはリンカーのldが出してるんだと思う。例のバッファオーバーランの件ね。gets - Wikipediaを見るとgets_sってのが載ってるけど、この関数はLinuxだとサポートされてないね。多分、今後もサポートされないのかな。代わりにfgets使えってことなんだと思うけど、改行の取り扱いが違うから面倒だよね。とりあえず目障りなワーニングだけ消せないかなと思ったんだけど無理っぽい。色々と厄介だな。fgets呼ぶのは大変だよなあ。stdinはグローバル変数だけど、mycは扱えないしなあ。そもそもファイルポインタとかハードルが高いよ。