Code Day's Night

ichikawayのブログ

x86アセンブリで数字を画面表示するだけの実装が大変だった

最近お気に入りの低レイヤーガールというYoutubeチャンネルで、アセンブリでFizzBuzzを書くというのを見て自分でも実践してみました。

Youtube: FizzBuzzをアセンブリ言語で書きたい!

x86アセンブリで画面表示するには、writeのsyscallを呼び出して、標準出力に文字列のアドレスを渡せばできます。 具体的には、次のように第3引数までレジスタにセットしてsyscallを呼び出すだけです。

    mov rax, 0x1   #syscall番号
    mov rdi, 0x1   #第1引数 fd
    lea rsi, [foo]   #第2引数 文字列のアドレス
    mov rdx, 0x03   #第3引数 文字数
    syscall

raxに0x01を入れていますがこれがシステムコール番号です。 システムコールの番号と引数のレジスタの一覧表があるのでこれを見るとわかりやすいです。
LINUX SYSTEM CALL TABLE FOR X86 64

数字が・・出ない

さてFizzBuzzの文字列を出すまでは良かったのですが、試しにループしている箇所のカウンタ(rcxレジスタ)の数字を画面に出力してみるかと、rcxに入っている値を適当なメモリアドレスに入れて同じようにwriteのsyscallを呼んでみました。

結果は・・何も数字が出力されず。

色々とデバッグして辿り着いたのは、カウンタの数字(例えば3)が入ったメモリアドレスをwriteのsyscallに渡すと、charとして扱われて、0x03のAsciiコードとして画面出力に使われてたのでした。Asciiコードの0x00から0x20までは制御文字のためどれも画面に何も出力されません。。

試しに、カウンタの数字に0x30(これは文字列のゼロ)を足して表示させると、ちゃんと数字が表示されました。

長い道のり

さて、0x30を数字に足せばちゃんと数字の文字列が画面にでるところまではいけました。
問題は、2桁以上の数の場合です。例えば、16という整数を画面に表示する場合は1と6の2文字のcharとして画面表示する必要があります。そして、最初に上の桁の1を表示するという順序も重要になります。

ここまでくると、一般的な高級言語で実装されている数字を画面表示するという機能がアセンブリでは果てしなく遠いものに見えてきます。 PHPのecho()にも感謝の気持ちが湧いてくるのです。

itoaを実装するか

整数を文字列に変換する関数としてitoa()がありますが、これと同じようなことをx86アセンブリで実装しました。 やるべきことは次の通り。

  • 1桁目の数字を抜き出してAscii変換して文字にしてスタックにpush
  • 2桁目の数字を抜き出して同じことをする。これを全ての桁が終わるまで繰り返す
  • スタックに積み終わったら、popしながらメモリのアドレスに順にいれていく
  • そのメモリアドレスの先頭アドレスをwriteのsyscall の第2引数に渡す

数字を1桁ずつ抜き出すのは、div命令で割り算をして余りを使うことです。
商は残った桁数の数字のため、商が0になるまで繰り返します。
Asciiコード変換は0x30を足すか、OR命令を使うかです。(or rdx, 0x30)

感想

楽しかった!
趣味の時間なので、GDBでデバッグしたり、ロジック考えたり、期限がないので悩んでる時間も楽しかったです。

今回作ったプログラムがこちらです。
https://github.com/ichikaway/assembler-sample/blob/master/fizzbuzz/fizzbuzz.s#L69

echocount:
    push rbp
    mov rbp, rsp

    mov r13, 0 #数字の桁数
    push [nullbyte]
    mov r11, rcx

    echocount_div_again:
    # カウンタの数字の下位の桁から数字文字列変換してスタックに積む
    inc r13
    mov rdx,0 #割られる上位
    mov rax, r11 #割られる下位
    mov r12, 10 #割る数
    div r12
    mov r11, rax #商 rax
    or rdx, 0x30 #余りrdxの数字をascii文字の数字に変換
    push rdx 
    cmp rax, 0 #商 rax がゼロでなければまだ桁が残っているためループする
    jne echocount_div_again

    # nullbyteがくるまでpopして数字を文字列に変換してstr変数のアドレスにいれていく
    mov r10, 0
    echocount_pop_loop:
    pop r12
    mov [str+r10], r12 # 文字列を格納するアドレス、配列のため1文字ずつアドレスを上げていく
    inc r10
    cmp r12, [nullbyte]
    jne echocount_pop_loop

    #print
    mov rdi, r13 #文字数
    lea rsi, [str]
    call put

    mov rsp, rbp
    pop rbp
    ret