GAS(AT&Tアセンブリ)でfizzbuzzを実装する

GAS(AT&Tアセンブリ)でfizzbuzzを実装するために、実際に書いたコードとつまづいたポインタを解説していく。

開発環境

  • gcc (Alpine 5.3.0) 5.3.0
  • alpine 3.4 (Linux 64bit)

上記の環境を構築するためにdockerを使用している。

実際に書いたコード

まずは実際に書いたコードを下記に示す。(長いけど)

gcc -o fizzbuzz fizzbuzz.sでコンパイラして./fizzbuzzで実行すれば、1から60までの整数でfizzbuzzを実行してくれる。

.set LOOP_END, 60
.set LOOP_START, 1
.set FIZZBUZZ_NUM, 15
.set FIZZ_NUM, 3
.set BUZZ_NUM, 5
.set EXIT, 60
.set EXIT_CODE, 1

.data
fizz:
    .string "Fizz"

buzz:
    .string "Buzz"

fizzbuzz:
    .string "FizzBuzz"

num_str:
    .string "%d\n"

end_string:
    .string "\nComplete FizzBuzz!!"

.text
.globl main

fizz_write:
    leaq fizz(%rip), %rdi
    call puts@PLT
    jmp loop_inc

buzz_write:
    leaq buzz(%rip), %rdi
    call puts@PLT
    jmp loop_inc

fizzbuzz_write:
    leaq fizzbuzz(%rip), %rdi
    call puts@PLT
    jmp loop_inc

num_write:
    leaq num_str(%rip), %rdi
    movq %r13, %rsi
    xor %rax, %rax
    call printf@PLT
    jmp loop_inc

loop_process:
    cmpq %r13, %r12
    jl end_process

    movq  $0, %rdx
    movq %r13, %rax
    movq $FIZZBUZZ_NUM, %r14
    divq %r14
    cmpq $0, %rdx
    je fizzbuzz_write

    movq  $0, %rdx
    movq %r13, %rax
    movq $FIZZ_NUM, %r14
    divq %r14
    cmpq $0, %rdx
    je fizz_write

    movq  $0, %rdx
    movq %r13, %rax
    movq $BUZZ_NUM, %r14
    divq %r14
    cmpq $0, %rdx
    je buzz_write

    movq  $0, %rdx
    movq $0, %rax
    jmp num_write

loop_inc:
    inc %r13
    jmp loop_process

main:
    movq $LOOP_START, %r13    /* r13をloopカウントにする */
    movq $LOOP_END, %r12
    jmp loop_process

end_process:
    leaq end_string(%rip), %rdi
    call puts@PLT
    movq $EXIT, %rax  /* exit */
    movq $EXIT_CODE, %rdi
    syscall
    ret

アセンブリでprintfを使う時のポイント

C言語では頻繁に使うprintfだが、アセンブリで使う場合はC言語よりも注意する点が多くある。

  • printfを呼び出す時はcall printfではなく、call printf@PLTとする
  • %r11レジスタに値を格納しない

アセンブリでC言語の標準ライブラリ(printfなど)を使う場合は、call printf@PLTという風に末尾に@PLTをつけること。

PLTとは、procedure linkage tableの略で動的リンクを実現するためのもの。

通常、アセンブリでcall printfの命令を書いた時は、最終的に「メモリ番号100にジャンプしてね」という風に具体的なメモリ番号を示した機械語に直される。そのためコードを変更した時にメモリ位置がずれるため、全てのコードをコンパイルし直す必要がある。

一方、PLTを使った動的リンクでは、コンパイル時に「どのメモリ番号にジャンプするか」を決めるのではなく、実行時に linkage tableを調べて、「call printfの時はメモリ番号何番目に飛べば良い」というのをチェックするため、仮にコードを書き直してもコンパイルし直す必要がなくなるメリットがある。

参考:assembly - What does @plt mean here? - Stack Overflow

また、64bitのCPUには「システム関数を読んだ時にr11レジスタの値が保持されない性質がある。printfputsなどのC言語の標準ライブラリ関数は内部でシステム関数を呼んでいるので、r11レジスタの扱いには注意すること。