コンパイラ作成(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は扱えないしなあ。そもそもファイルポインタとかハードルが高いよ。