search
LoginSignup
1

posted at

updated at

自作言語のコンパイラにオレオレアセンブリではなくx86_64アセンブリを生成させる(関数呼び出しと足し算だけ)

自作言語とそのコンパイラを Ruby で作って(一応 x86風のつもりの)オレオレVM向けにオレオレアセンブリ・オレオレ機械語を出力するということをこれまでやってきたのですが、今回はこれを改造して本物の x86_64 アセンブリを出力させてみます。

x86_64 アセンブリを触るのは今回が初めてで、ちゃんとやろうとすると大変そうなのでハードルを下げます。あんまりがんばらなくて済むようにしたい。

  • 関数呼び出しと足し算だけ
  • 正常系1パターンだけ動けばOK
  • 「x86_64アセンブリとツールまわりを軽く触ってみた」の実績解除ができればOK
  • 細かいところまで理解してなくてもOK
    • 細かいところに深入りしない。深入りしないぞ!
  • なんとなく雰囲気が分かればOK

今回のスコープはここまで。

これを改造します

<自作言語処理系(Ruby版)の説明用テンプレ>
自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。簡単に概要を書くと下記のような感じ。

  • 製作過程を製作メモに全部書いています。凝ったことはしていないので Ruby 知らない人でも雰囲気くらいは分かるかも。

<説明用テンプレおわり>

機能は最低限のものに留めており、凝ったことをしていない素朴な実装なので、今回のように改造・実験したくなったときにサッといじって遊べます。

できたもの

入力となるプログラムと期待する出力

先に最終形を貼っておきます。

func add(a, b) {
  return a + b;
}

func main() {
  var x;
  call_set x = add(19, 23);
  return x;
}

↑これをコンパイルしたら↓これを出力する。AT&T形式にしてみました。

  .globl main

main:
  call _main
  mov %rax, %rdi
  mov $60, %rax
  syscall

add:
  push %rbp
  mov %rsp, %rbp

  # 関数の処理本体
  # return a + b;
  mov 16(%rbp), %rax
  push %rax
  mov 24(%rbp), %rax
  push %rax
  pop %rcx
  pop %rax
  add %rcx, %rax

  mov %rbp, %rsp
  pop %rbp
  ret

_main:
  push %rbp
  mov %rsp, %rbp

  # 関数の処理本体
  # var x;
  sub $8, %rsp
  # call_set x = add(23, 19);
  mov $23, %rax
  push %rax
  mov $19, %rax
  push %rax
  call add  add $16, %rsp
  mov %rax, -8(%rbp)
  # return x;
  mov -8(%rbp), %rax

  mov %rbp, %rsp
  pop %rbp
  ret

ちなみに、改造前の状態で出力されるオレオレアセンブリは以下です。作者の目からはだいたい似た感じに見えます。

  call main
  exit

label add
  push bp
  cp sp bp

  # 関数の処理本体
  cp [bp:2] reg_a
  push reg_a
  cp [bp:3] reg_a
  push reg_a
  pop reg_b
  pop reg_a
  add_ab

  cp bp sp
  pop bp
  ret

label main
  push bp
  cp sp bp

  # 関数の処理本体
  add_sp -1
  cp 23 reg_a
  push reg_a
  cp 19 reg_a
  push reg_a
  call add
  add_sp 2
  cp reg_a [bp:-1]
  cp [bp:-1] reg_a

  cp bp sp
  pop bp
  ret

# ... 略 ...

x86_64アセンブリを触ってみる

準備

# 010_base.s

    .globl main

main:
    # ここに main 関数の処理を追加していく

    # exit N で終了
    mov $60, %rax
    mov $42, %rdi
    syscall

終了ステータス=42 で終了するだけのプログラムです。これを雛形とします。

このアセンブリコードをアセンブル・リンクして、実行して、結果(終了ステータス)を表示する Bashスクリプト run.sh がこちら。

#!/bin/bash

bname=$(basename $1 .s)

as -o ${bname}.o ${bname}.s  # アセンブル
gcc -o ${bname} ${bname}.o   # リンク

./${bname}      # 実行
echo status=$?  # 終了ステータスを表示

次のように実行します。

./run.sh 010_base.s

単に実行するだけでなく、 gdb で実行するための run_gdb.sh も用意しました。

#!/bin/bash

bname=$(basename $1 .s)

as -o ${bname}.o ${bname}.s
gcc -o ${bname} ${bname}.o

gdb -tui ${bname}

gdb を使うのも今回始めてで、とりあえず下記の操作だけ覚えてそれでなんとかしました。短いプログラムなので今回はブレークポイントは使わず。

start
quit / q
stepi / si
info registers / i r
display
disassemble / disas
(TUI モード用)
C-x 2
C-x 2
→ 上にレジスタ、下にアセンブリが表示される

あと、 si Enter, si Enter, si Enter, …… のように毎回同じコマンドを入力しなくても、 Enter だけで同じコマンドを実行してくれます(最初これを知らなくてめんどくさいなーと思ってた)。

準備ができました。これを使って試していきます。

rax に即値をセットしてみる

はじめの1歩ということで。

    .globl main

main:
    mov $42, %rax  # rax に 42 をセット

    mov $1,  %rdi
    mov $60, %rax
    syscall

gdb の TUIモードでステップ実行して rax に 42 がセットされたことを確認している様子:
image.png

rax にセットした値で exit してみる

mov $60, %rax で rax が上書きされるので、その前に rax の値を rdi にコピーしておく。

    .globl main

main:
    mov $3, %rax

    mov %rax, %rdi  # rax の値を終了ステータスとして rdi にセット
    mov $60, %rax
    syscall

add 命令を使ってみる

足し算してみます。

    .globl main

main:
    mov $3, %rax
    mov $1, %rcx
    add %rcx, %rax  # rax + rcx の結果を rax にセット

    mov %rax,  %rdi
    mov $60, %rax
    syscall

rbx は callee-save らしいので、rbx は避けて rcx を使ってみました。

call/ret 命令を使ってみる

call でラベル f に移動して、何もせず ret で main に戻る。以後はこれを関数の呼び出しに見立てます。

    .globl main

f:
    ret  # 呼び出し元に戻る
    
main:
    call f  # 関数 f の呼び出し

    mov $6,  %rdi
    mov $60, %rax
    syscall

関数から値を返してみる

関数 f からの返り値を rax にセットして、 main 関数でその返り値を利用する。

    .globl main

f:
    mov $7, %rax
    ret
    
main:
    call f
    # f から戻った時点で rax == 7 になっている

    mov %rax,  %rdi  # 関数の返り値を利用
    mov $60, %rax
    syscall

足し算して結果を返却

呼び出した先の関数で足し算して、結果を返します。上でやったことを組み合わせてみたもの。

    .globl main

f:
    mov $3, %rax
    mov $4, %rcx
    add %rcx, %rax  # rax == 7 になる
    ret
    
main:
    call f
    # 関数の返り値 == 7

    mov %rax,  %rdi
    mov $60, %rax
    syscall

関数の準備・後片付け

スタックを利用して関数に引数を渡すための下ごしらえ。

    .globl main

f:
    # ★準備
    push %rbp       # ベースポインタをスタックに退避
    mov %rsp, %rbp  # ベースポインタをスタックポインタの位置まで持ってくる

    # 関数の処理本体
    mov $6, %rax
    mov $4, %rcx
    add %rcx, %rax

    # ★後片付け
    mov %rbp, %rsp  # スタックポインタをベースポインタの位置に戻す
    pop %rbp        # ベースポインタをスタックから復元

    ret
    
main:
    call f

    mov %rax,  %rdi
    mov $60, %rax
    syscall

↓このあたりでやったやつ。懐かしい。

関数に引数を1個渡す(渡すだけ)

(x86_64 では)普通は関数の引数はレジスタで渡すらしいのですが、スタックで渡す方式にします。コンパイラの方がスタックで渡す作りになっているので、そっちに合わせました。

この時点ではまだ引数を渡しているだけで関数の側では使っていない状態。

    .globl main

f:
    # 準備
    push %rbp
    mov %rsp, %rbp

    # 関数の処理本体
    mov $5, %rax
    mov $6, %rcx
    add %rcx, %rax

    # 後片付け
    mov %rbp, %rsp
    pop %rbp

    ret
    
main:
    # ★引数をスタックに積む
    push $7  # rsp は +8

    # 関数を呼び出し
    call f

    # ★スタックに積んだ引数の分だけ戻す
    add $8, %rsp

    mov %rax,  %rdi
    mov $60, %rax
    syscall

関数に渡した引数を使う

16(%rbp) でスタックに置いた1つめの引数を参照できます。

    .globl main

f:
    # 準備
    push %rbp
    mov %rsp, %rbp

    # 関数の処理本体
    mov 16(%rbp), %rax  # ★ bp+16 で引数1を参照
    mov $5, %rcx
    add %rcx, %rax

    # 後片付け
    mov %rbp, %rsp
    pop %rbp

    ret
    
main:
    # 引数をスタックに積む
    push $7

    # 関数を呼び出し
    call f

    # スタックに積んだ引数の分だけ戻す
    add $8, %rsp

    mov %rax,  %rdi
    mov $60, %rax
    syscall

2つ目の引数を渡して使う

2つ目の引数も同じ要領で。

    .globl main

f:
    # 準備
    push %rbp
    mov %rsp, %rbp

    # 関数の処理本体
    mov 16(%rbp), %rax  # ★ 引数1 を参照
    mov 24(%rbp), %rcx  # ★ 引数2 を参照
    add %rcx, %rax

    # 後片付け
    mov %rbp, %rsp
    pop %rbp

    ret
    
main:
    # 引数をスタックに積む
    push $5  # ★ 引数2
    push $8  # ★ 引数1

    # 関数を呼び出し
    call f

    # スタックに積んだ引数の分だけ戻す
    add $16, %rsp

    mov %rax,  %rdi
    mov $60, %rax
    syscall

関数 f の「処理本体」の部分ではスタックが以下のようになっているはず。たぶん。

bp+0
bp+8   戻りアドレス
bp+16  引数1
bp+24  引数2

ローカル変数もやってみる

ここまでできれば必要なものが揃います。

    .globl main

f:
    # 準備
    push %rbp
    mov %rsp, %rbp

    # 関数の処理本体
    mov 16(%rbp), %rax  # 引数1
    mov 24(%rbp), %rcx  # 引数2
    add %rcx, %rax

    # 後片付け
    mov %rbp, %rsp
    pop %rbp

    ret
    
main:
    # 準備    push %rbp
    mov %rsp, %rbp

    # ★ローカル変数を宣言
    sub $8, %rsp

    # 引数をスタックに積む
    push $2  # 引数2
    push $1  # 引数1

    # 関数を呼び出し
    call f

    # スタックに積んだ引数の分だけ戻す
    add $16, %rsp

    # ★関数からの返り値をローカル変数にセット
    mov %rax, -8(%rbp)  # bp-8 でローカル変数を利用

    # ★ローカル変数から返り値にセット
    mov -8(%rbp), %rax

    # 後片付け
    mov %rbp, %rsp
    pop %rbp

    mov %rax, %rdi
    mov $60, %rax
    syscall

これで必要なものが揃いました。後はこんな感じのを出力するようにコード生成処理を改造すればOK。

コード生成処理の改造

システムコールの呼び出しを main の外側に追い出したかったため、コード生成時に main 関数を_main にリネームし、 main から呼び出す形にしてみました。

    .globl main

main:
    _main を呼び出し
    mov %rax, %rdi  # _main の返り値で exit
    mov $60, %rax
    syscall

add:
    # ...

_main:
    # ...

後は割と機械的な書き換えでOK。

--- a/vgcodegen.rb
+++ b/vgcodegen.rb
@@ -5,13 +5,13 @@ require_relative "./common"
 $label_id = 0
 
 def asm_prologue
-  puts "  push bp"
-  puts "  cp sp bp"
+  puts "  push %rbp"
+  puts "  mov %rsp, %rbp"
 end
 
 def asm_epilogue
-  puts "  cp bp sp"
-  puts "  pop bp"
+  puts "  mov %rbp, %rsp"
+  puts "  pop %rbp"
 end
 
 def to_fn_arg_disp(fn_arg_names, fn_arg_name)
@@ -27,10 +27,11 @@ end
 # --------------------------------
 
 def _gen_expr_add
-  puts "  pop reg_b"
-  puts "  pop reg_a"
+  # rbx は callee-save なので避ける
+  puts "  pop %rcx"
+  puts "  pop %rax"
 
-  puts "  add_ab"
+  puts "  add %rcx, %rax"
 end
 
 def _gen_expr_mult
@@ -92,9 +93,9 @@ def _gen_expr_binary(fn_arg_names, lvar_names, expr)
   operator, arg_l, arg_r = expr
 
   gen_expr(fn_arg_names, lvar_names, arg_l)
-  puts "  push reg_a"
+  puts "  push %rax"
   gen_expr(fn_arg_names, lvar_names, arg_r)
-  puts "  push reg_a"
+  puts "  push %rax"
 
   case operator
   when "+"  then _gen_expr_add()
@@ -109,15 +110,15 @@ end
 def gen_expr(fn_arg_names, lvar_names, expr)
   case expr
   when Integer
-    puts "  cp #{expr} reg_a"
+    puts "  mov $#{expr}, %rax"
   when String
     case
     when fn_arg_names.include?(expr)
       disp = to_fn_arg_disp(fn_arg_names, expr)
-      puts "  cp [bp:#{disp}] reg_a"
+      puts "  mov #{disp * 8}(%rbp), %rax"
     when lvar_names.include?(expr)
       disp = to_lvar_disp(lvar_names, expr)
-      puts "  cp [bp:#{disp}] reg_a"
+      puts "  mov #{disp * 8}(%rbp), %rax"
     else
       raise panic("expr", expr)
     end
@@ -133,12 +134,12 @@ def _gen_funcall(fn_arg_names, lvar_names, funcall)
 
   fn_args.reverse.each do |fn_arg|
     gen_expr(fn_arg_names, lvar_names, fn_arg)
-    puts "  push reg_a"
+    puts "  push %rax"
   end
 
   gen_vm_comment("call  #{fn_name}")
   puts "  call #{fn_name}"
-  puts "  add_sp #{fn_args.size}"
+  puts "  add $#{fn_args.size * 8}, %rsp"
 end
 
 def gen_call(fn_arg_names, lvar_names, stmt)
@@ -152,17 +153,17 @@ def gen_call_set(fn_arg_names, lvar_names, stmt)
   _gen_funcall(fn_arg_names, lvar_names, funcall)
 
   disp = to_lvar_disp(lvar_names, lvar_name)
-  puts "  cp reg_a [bp:#{disp}]"
+  puts "  mov %rax, #{disp * 8}(%rbp)"
 end
 
 def _gen_set(fn_arg_names, lvar_names, dest, expr)
   gen_expr(fn_arg_names, lvar_names, expr)
-  src_val = "reg_a"
+  src_val = "%rax"
 
   case
   when lvar_names.include?(dest)
     disp = to_lvar_disp(lvar_names, dest)
-    puts "  cp #{src_val} [bp:#{disp}]"
+    puts "  mov #{src_val}, #{disp * 8}(%rbp)"
   else
     raise panic("dest", dest)
   end
@@ -290,7 +291,7 @@ def gen_stmts(fn_arg_names, lvar_names, stmts) end
 
 def gen_var(fn_arg_names, lvar_names, stmt)
-  puts "  add_sp -1"
+  puts "  sub $8, %rsp"
 
   if stmt.size == 3
     _, dest, expr = stmt

(他の部分もいくつか修正していますが、主な部分はこのあたり)

コンパイルして実行

コンパイルして実行まで行うスクリプトを用意して、

#!/bin/bash

mkdir -p ./tmp/

file="$1"
bname=$(basename $file .vg.txt)

tokensfile=tmp/${bname}.tokens.txt
treefile=tmp/${bname}.ast.json
asmfile=tmp/${bname}.s
objfile=tmp/${bname}.o

# 字句解析
ruby vglexer.rb $file > $tokensfile
# 構文解析
ruby vgparser.rb $tokensfile > $treefile
# コード生成(アセンブリコードをせいせい)
ruby vgcodegen.rb $treefile > $asmfile

# アセンブル+リンクして実行ファイルを生成
as -o $objfile $asmfile
gcc -o $bname $objfile

# 実行
./${bname}
status=$?

echo status=${status}

実行。

   $ cat sample_x86_64.vg.txt
func add(a, b) {
  return a + b;
}

func main() {
  var x;
  call_set x = add(19, 23);
  return x;
}

   $ ./run_x86_64.sh sample_x86_64.vg.txt
status=42

動きました! :clap:

というわけで今回はこれでOKです。満足。

せっかくなので objdump -d

せっかくなので実行ファイルを逆アセンブルしてみます。以下、 main, add, _main だけを抜粋したもの。

  $ objdump -d sample_x86_64

... snip ...

00000000000005fa <main>:
 5fa:   e8 24 00 00 00          callq  623 <_main>
 5ff:   48 89 c7                mov    %rax,%rdi
 602:   48 c7 c0 3c 00 00 00    mov    $0x3c,%rax
 609:   0f 05                   syscall 

000000000000060b <add>:
 60b:   55                      push   %rbp
 60c:   48 89 e5                mov    %rsp,%rbp
 60f:   48 8b 45 10             mov    0x10(%rbp),%rax
 613:   50                      push   %rax
 614:   48 8b 45 18             mov    0x18(%rbp),%rax
 618:   50                      push   %rax
 619:   59                      pop    %rcx
 61a:   58                      pop    %rax
 61b:   48 01 c8                add    %rcx,%rax
 61e:   48 89 ec                mov    %rbp,%rsp
 621:   5d                      pop    %rbp
 622:   c3                      retq   

0000000000000623 <_main>:
 623:   55                      push   %rbp
 624:   48 89 e5                mov    %rsp,%rbp
 627:   48 83 ec 08             sub    $0x8,%rsp
 62b:   48 c7 c0 17 00 00 00    mov    $0x17,%rax
 632:   50                      push   %rax
 633:   48 c7 c0 13 00 00 00    mov    $0x13,%rax
 63a:   50                      push   %rax
 63b:   e8 cb ff ff ff          callq  60b <add>
 640:   48 83 c4 10             add    $0x10,%rsp
 644:   48 89 45 f8             mov    %rax,-0x8(%rbp)
 648:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 64c:   48 89 ec                mov    %rbp,%rsp
 64f:   5d                      pop    %rbp
 650:   c3                      retq   
 651:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
 658:   00 00 00 
 65b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

... snip ...

call, ret が callq, retq になってるな、とか、retq の後に nop が加えられてるな、といったあたりをなんとなく眺めます。今回はなんとなく眺めるだけ。

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1