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