ITエンジニアのブログ

IT企業でエンジニアやってる人間の日常について

プログラミング言語を作る。第3回:x86-64アセンブリ

前回は Hello, world! を出力させる x86-64 アセンブリを書いたので、一段階レベルを上げて、数字を出力させてみます。
アセンブリでは、C言語みたく printf("%d", n) のような一発で数値を出力する機構は存在しません。数値を文字列に変換しなければ出力できないので、まずそれを実行します。 unistd.h の write 関数を使い、 write システムコールを呼び出す環境とできるだけ似せています。

#include <stdio.h>
#include <unistd.h>

int getStringLength(char s[]){
    int i = 0;
    while(*s != '\0'){
        s++;
        i++;
    }
    return i;
}

int getDigits(int n){
    int i = 0;

    do{
        n /= 10;
        i++;
    }while(n > 0);

    return i;
}

void numericToString(char s[], int n){
    int digit;
    int i;

    if(n < 0){
        n = -n;
        *s = '-';
        s++;
    }

    digit = getDigits(n);

    for(i = digit - 1; i >= 0; i--){
        s[i] = n % 10 + '0';
        n /= 10;
    }

    s[digit] = '\0';
}

int main(void){
    int n;
    char s[12];
    scanf("%d", &n);
    numericToString(s, n);
    write(1, s, getStringLength(s));
    write(1, "\n", 1);
    return 0;
}

まず桁数を求め、1の位から夫々の桁の数字を取得して配列に代入しています。文字列の長さも求め、 write システムコールを使います。
scanf を用いて標準入力から数値を受け付けているため、適当な数値を入力としてうけ、同じ数値を出力するプログラムになっています。
入力の部分は除いて、 x86-64アセンブリでこれを表現します。

bits 64
section .text
global _start

getStringLength:
    mov ecx, 0
gsl_L2:
    mov bl, byte [rax]
    cmp bl, 0
    jz gsl_L1
    inc rax
    inc ecx
    jmp gsl_L2
gsl_L1:
    mov eax, ecx
    ret

getDigits:
    movsx rax, eax
    mov ecx, 0
gd_L1:
    mov rdx, 0
    mov rbx, 10
    idiv rbx
    inc ecx
    cmp eax, 1
    jns gd_L1
    mov eax, ecx
    ret

numericToString:
    mov rsi, rax
    cmp ebx, 0
    jns nts_L1
    neg ebx
    mov dl, '-'
    mov byte [rsi], dl
    inc rsi 
nts_L1:
    push rbx
    push rsi
    mov eax, ebx
    call getDigits
    mov ecx, eax
    pop rsi
    pop rbx
    push rcx
    sub ecx, 1
nts_L2:
    cmp ecx, 0
    js nts_L3
    mov edi, ebx
    mov eax, ebx
    movsx rax, eax
    mov rdx, 0
    mov ebx, 10
    idiv ebx
    push rsi
    movsx rcx, ecx
    add rsi, rcx
    add dl, '0'
    mov byte [rsi], dl
    pop rsi
    mov ebx, eax
    dec ecx
    jmp nts_L2
nts_L3:
    pop rcx
    add rsi, rcx
    mov byte [rsi], 0
    ret

_start:
    mov ebx, 2016
    sub rsp, 12
    mov rax, rsp
    push rax
    call numericToString
    pop rax
    push rax
    call getStringLength
    mov edx, eax
    mov rax, 1
    mov rdi, 1
    pop rsi
    syscall
    mov rax, 1
    mov rdi, 1
    mov rsi, data_newline
    mov rdx, 1
    add rsp, 12
    syscall
    mov rdi, 0
    mov rax, 60
    syscall
    
section .data
    data_newline db 0x0A

すこし説明します。

rsp はスタックポインタで、メモリの現在位置を示していると思えばわかりやすいです。 push や pop などでデータを蓄えるときに勝手に値が適切に変化しているようです。普通は値をいじったりしないようですが、 char s[12] を宣言するために、値を変えて場所を確保しました。
関数の引数ですが、明確な定義を知らなかったので、第一引数は rax, 第二引数は rbx に格納しておきました。慣習や取り決めと違っていても、プログラム内で統一されていればきちんと動作するはずです。

さて、これを書いてみて、なかなか大変でした。理由は

  • 簡単なマニュアルが無く、一つの記述でも調べるのに苦戦した。
  • 汎用レジスタの数が少ないため、どこにデータを蓄えておくかを吟味しなければならない。
  • Power PC と違って命令に癖がある。(とくに割り算とか)

関数を 3 つも定義しなければならないのも苦労した点です。

第2回で Hello, world! を実行した時と同じコマンドで実行可能です。 2016 という数値が出力されます。アセンブリの内容を参照し、 _start 直下の mov, ebx, 2016 の数値を 32bit の範囲内の数値で置き換えることで、他の数値も出力することができます。

FizzBuzz, 再帰関数のアセンブリも書こうと思ったのですが、このプログラムは条件分岐、繰り返し、配列など様々なものを扱えていますので、既にかなり学習できましたし、それらは特に優先度は高くないと思いましたので、アセンブリのみにとりかかるのではなく、他の事も平行して勉強していこうと思います。