はじめに
最近流行っている Javaで湯婆婆を実装してみる - Qiita を、誰もやらないと思ったので「アセンブリ言語」でやってみました。
軽い気持ちではじめましたが、アセンブリは何をやるにも普通の言語で簡単にできるようなことができません‥。
たぶん、 ソースコードは湯婆婆史上一番長い と思います。
動画で確認したい方はこちらをどうぞ!
軽い気持ちでアセンブリで湯婆婆したら大変だった件
コードはGitHubに公開しています。
https://github.com/yassun-youtube/yubaba-assembly
湯婆婆 史上一番長いソースを先に見たい方はこちら
default rel
%define SYSCALL_EXIT 0x2000001
%define SYSCALL_SYSREAD 0x2000003
%define SYSCALL_SYSWRITE 0x2000004
%define STDIN 0
%define STDOUT 1
%define BUFFER_SIZE 64
section .data
keiyaku: db "契約書だよ。そこに名前を書きな。", 0x0a
keiyaku_len: equ $-keiyaku
fun: db "フン。"
fun_len: equ $-fun
zeitaku: db "というのかい。贅沢な名だねぇ。", 0x0a
zeitaku_len: equ $-zeitaku
imakara: db "今からお前の名前は"
imakara_len: equ $-imakara
iikai: db "だ。いいかい、"
iikai_len: equ $-iikai
henji: db "だよ。分かったら返事をするんだ、"
henji_len: equ $-henji
bikkuri: db "!!", 0x0a
bikkuri_len: equ $-bikkuri
section .bss
buffer: resb BUFFER_SIZE
section .text
global start
start:
mov rsi, keiyaku
mov rdx, keiyaku_len
call .syswrite
call .get_random_one_word
mov r10, rax ; save new name to r10
mov r9, rcx ; save given name byte length to r9
sub r9, 1 ; remove new line
mov rsi, fun
mov rdx, fun_len
call .syswrite
mov rsi, buffer
mov rdx, r9
call .syswrite
mov rsi, zeitaku
mov rdx, zeitaku_len
call .syswrite
mov rsi, imakara
mov rdx, imakara_len
call .syswrite
call .print_new_name
mov rsi, iikai
mov rdx, iikai_len
call .syswrite
call .print_new_name
mov rsi, henji
mov rdx, henji_len
call .syswrite
call .print_new_name
mov rsi, bikkuri
mov rdx, bikkuri_len
call .syswrite
call .exit
.print_new_name:
mov rsi, r10
mov rdx, 3
call .syswrite
ret
; return
; rax: address of new name char
; rcx: number of given name characters bytes
.get_random_one_word:
call .sysread ; set values into buffer and rax has input length
mov rcx, rax
xor rdx, rdx
mov rbx, 3
idiv rbx ; rax has length of japanese word
mov r8, rax ; store in r8
rdtsc ; set random value to rax(this return just clock...)
xor rdx, rdx
mov rbx, r8
idiv rbx ; get random number from 0 ~ r8-1
mov r9, rdx
imul r9, 3 ; set number of
mov rax, buffer
add rax, r9 ; set char address to rax
ret
.sysread:
mov rax, SYSCALL_SYSREAD
mov rdi, STDIN
mov rsi, buffer
mov rdx, BUFFER_SIZE
syscall
ret
.syswrite:
mov rax, SYSCALL_SYSWRITE
mov rdi, STDOUT
syscall
ret
.exit:
mov rax, SYSCALL_EXIT
xor rdi, rdi
syscall
.exit_without_rdi:
mov rax, SYSCALL_EXIT
syscall
始めるにあたって
アセンブリ言語をmacで使うために、 nasm
をインストールしました。
brew install nasm
実行方法
下記でアセンブルして実行可能です。
nasm -f macho64 test.asm
ld test.o -static -o test
./test
苦戦1: アセンブリで標準入力を受けるには
みなさんも一度 「mac アセンブリ」で検索してみてもらいたいのですが、ほぼみんな同じコードを利用しています。
つまり、 「Hello, World!」のコードはあるのですが、それ以外のコードがなかなか見つかりません…。
まず,「Hello, World」のコードは下記です。
section .data
hello_world: db "Hello, World!", 0x0a
hello_world_len: equ $-hello_world
section .text
global start
start:
mov rax, 0x2000004 ; システムコールの番号を指定 (syswrite)
mov rdi, 1 ; 1(STDOUT)を指定
mov rsi, hello_world ; Hello, World! が格納されているアドレスを指定
mov rdx, hello_world_len ; 何文字読むかを指定
syscall
mov rax, 0x2000001 ; システムコールの番号を指定(exit)
xor rdi, rdi ; 0 で exit
syscall
システムコールの値を推測
Writeが4なら3じゃね?と推測して実行してみます。
section .text
global start
start:
mov rax, 0x2000003 ; システムコールの番号を指定 (sysread)
syscall
mov rax, 0x2000001 ; システムコールの番号を指定(exit)
xor rdi, rdi ; 0 で exit
syscall
実行しても何も起きず…。
他の入力を推測
入力が足りないのじゃないかと思い、 Hello, World で rdi に 標準出力(1)を設定していたことから、rdiに標準入力を入れれば良いいのではと思い、
rdi に 0 を入れてみました。(1が標準出力なら0が標準入力のはず)
section .text
global start
start:
mov rax, 0x2000003 ; システムコールの番号を指定 (sysread)
mov rdi, 0
syscall
mov rax, 0x2000001 ; システムコールの番号を指定(exit)
xor rdi, rdi ; 0 で exit
syscall
しかし動かず…。
さらに他の入力を推測
ここからさらに推測して、おそらく標準入力が入るためのアドレスが足りないのだろうと思ったので (Hello, World のコードで rsi, rdx にあたる部分)、そこを入れようとしました。
ここで参考になりそうなサイトを発見!! 予想はだいたい当たっていたのですが、下記で標準入力できました。
%define BUFFER_SIZE 64
section .bss
buffer: resb BUFFER_SIZE
section .text
global start
start:
mov rax, 0x2000003 ; システムコールの番号を指定 (sysread)
mov rdi, 0
mov rsi, buffer
mov rdx, BUFFER_SIZE
syscall
mov rax, 0x2000001 ; システムコールの番号を指定(exit)
xor rdi, rdi ; 0 で exit
syscall
$ ./test
あああ
$
とりあえず入力できるところまでいきました。
入力された長さをどう取得するか
これで安心と思いきや、入力された文字の長さを取得する方法がわかりません。
レジスタリストを見ると、syscallの返り値は rax に入ってくるようだということがわかったのですが、
そもそもレジスタの値を確かめる方法がわからない ということに気づきます…。
苦戦2: 値の確認…
検索し、 Mac でアセンブリを書いてみる を参考にしようとしました。
vmmap
lldp
というツールを使うと確認できるらしいということがわかりました。
ただ、めっちゃ難しそう…。
ということでいったん最終手段にして今わかっている方法だけでどうにかできないかと思いました。
Hello, World! のように標準出力で出力してみよう!
さて、デバッグといえばまずは print
で出力してみるというのが我らエンジニアの常識です!
ということで出力させるぞー!!
と思ったら、そもそもレジスタに入っている値をどうやって出力するかさえわからない…。
動くかなと思ってとりあえず Hello, World! とおなじようにやってみました!!
; raxの値をhello_worldと同じ様に標準出力しようとしても失敗する
%define BUFFER_SIZE 64
section .bss
buffer: resb BUFFER_SIZE
section .text
global start
start:
mov rax, 0x2000003 ; システムコールの番号を指定 (sysread)
mov rdi, 0
mov rsi, buffer
mov rdx, BUFFER_SIZE
syscall
mov rsi, rax ; ここでrsiにraxの値を入れる <-- ここ
mov rax, 0x2000004 ; システムコールの番号を指定 (syswrite)
mov rdi, 1 ; 1(STDOUT)を指定
mov rdx, 1
syscall
mov rax, 0x2000001 ; システムコールの番号を指定(exit)
xor rdi, rdi
syscall
しかし動かず…。
というのも当たり前で、 Hello, World! で標準出力しているときに指定しているのは、
Hello, World!の文字列が入っているメモリの最初のアドレス
と 文字列の長さ
でした。
レジスタはメモリに入っているわけではないので、普通にレジスタの値を入れても、そのレジスタに入っている値のアドレスに入っている文字列を出力しようとします。
そりゃ動きません。
これ、アセンブリやC言語などに詳しくない方には何を言っているかわからないかもしれないですね…。
Hello, World!は断念…。ではどうする…?
ということで値を標準出力に出力する方法はわかりませんでした…。
print
できない、とめることもできない、どうやって値確認するの?
と思って絶望しかけていたところ…。
ある考えを思いつきました。
Hello, World! の一番最後の部分で、プログラムを exit
しています。
exit
は、引数を設定できます。コマンドの成功を0で表しますね。
ということは、 exit
でレジスタの値を返せば echo $?
で値を確認できるのではないか? とひらめきました。
; 標準入力に入れられた長さをexitで返す
%define BUFFER_SIZE 64
section .bss
buffer: resb BUFFER_SIZE
section .text
global start
start:
mov rax, 0x2000003 ; システムコールの番号を指定 (sysread)
mov rdi, 0
mov rsi, buffer
mov rdx, BUFFER_SIZE
syscall
mov rdi, rax
mov rax, 0x2000001 ; システムコールの番号を指定(exit)
syscall
これを実行してみます。
$ ./test
あ
$ echo $?
4
$ ./test
あい
$ echo $?
7
取得できました!! 日本語が3byteで、最後の改行を含めた数値が帰ってきています!!
これで標準入力で受け取ったものを確認できました!
標準入力に入力された内容を標準出力に出力してみる
やはり rax に標準入力で入力された文字列の長さが入っていたので、それを利用して標準入力に入れられた文字列を標準出力に出力してみます。
; 入力された内容をそのまま標準出力する
%define BUFFER_SIZE 64
section .bss
buffer: resb BUFFER_SIZE
section .text
global start
start:
mov rax, 0x2000003 ; システムコールの番号を指定 (sysread)
mov rdi, 0
mov rsi, buffer
mov rdx, BUFFER_SIZE
syscall
mov rdx, rax
mov rax, 0x2000004 ; システムコールの番号を指定 (syswrite)
mov rdi, 1 ; 1(STDOUT)を指定
mov rsi, buffer
syscall
mov rdi, rax
mov rax, 0x2000001 ; システムコールの番号を指定(exit)
syscall
$ ./test
あい
あい
$
成功していますね!
苦戦3: ランダムな値を取得する
アセンブラには、ランダム関数などは存在しないため、擬似乱数を作る必要がある。
今回、乱数を作るのがとても大変なようだったので、 rdtsc
というプロセッサのタイムスタンプを取得できる命令を利用しました。
1行で楽勝じゃん、と思うかもしれませんが、これを調べ出すのがまず大変なんですね。
エンジニアの皆さんなら気持ちわかると思います…。
rdtsc は Read Time Stamp Counter のことらしいです。
rdtsc ; これで rax に値が入る
; 割り算してあまりを撮る
xor rdx, rdx
mov rbx, r8
idiv rbx ; get random number from 0 ~ r8-1
; rdx にランダムな値が入る
これでやっとゴールが見えてきました。
苦戦4( 未解決問題 => コメントいただき解決しました ): 一文字だけ取り出すには?
bufferのアドレスに、ランダムな数値に3をかけたものを足したアドレスが、今回利用する改変後の名前になります。
ここで、レジスタに値を入れておくのが嫌だったので、アドレスに値を格納しようとしました。
section .bss
new_name resb 64
section .text
global start
start:
mov rax, 10
mov [new_name], rax
mov rdi, rax
mov rax, 0x2000001 ; システムコールの番号を指定(exit)
syscall
$ nasm -f macho64 test.asm
register_into_address.asm:9: error: Mach-O 64-bit format does not support 32-bit absolute addresses
64bitだと []
で値を格納することができませんでした…。
これ、解決できませんでした…。誰か知っている方がいたら教えて下さい。
コメントいただき解決しました。
[rel new_name]
で 相対アドレスになるようです!!(RIP相対? よくわからぬ!)
section .bss
new_name resb 64
section .text
global start
start:
mov [rel new_name], 0x41 ; 'A' のascii
mov rax, 0x2000004
mov rdi, 1
mov rsi, new_name
mov rdx, 1
syscall
mov rdi, rax
mov rax, 0x2000001 ; システムコールの番号を指定(exit)
syscall
$ ./test
A
値が格納されました!
小さな苦戦, 割り算
割り算も意外と難しい
xor rdx, rdx
mov rbx, 3
idiv rbx
; これで rax に割り算した結果, rdxにあまりが入る
ソースコード
色々と苦戦したものの完成しました。
default rel
%define SYSCALL_EXIT 0x2000001
%define SYSCALL_SYSREAD 0x2000003
%define SYSCALL_SYSWRITE 0x2000004
%define STDIN 0
%define STDOUT 1
%define BUFFER_SIZE 64
section .data
keiyaku: db "契約書だよ。そこに名前を書きな。", 0x0a
keiyaku_len: equ $-keiyaku
fun: db "フン。"
fun_len: equ $-fun
zeitaku: db "というのかい。贅沢な名だねぇ。", 0x0a
zeitaku_len: equ $-zeitaku
imakara: db "今からお前の名前は"
imakara_len: equ $-imakara
iikai: db "だ。いいかい、"
iikai_len: equ $-iikai
henji: db "だよ。分かったら返事をするんだ、"
henji_len: equ $-henji
bikkuri: db "!!", 0x0a
bikkuri_len: equ $-bikkuri
section .bss
buffer: resb BUFFER_SIZE
section .text
global start
start:
mov rsi, keiyaku
mov rdx, keiyaku_len
call .syswrite
call .get_random_one_word
mov r10, rax ; save new name to r10
mov r9, rcx ; save given name byte length to r9
sub r9, 1 ; remove new line
mov rsi, fun
mov rdx, fun_len
call .syswrite
mov rsi, buffer
mov rdx, r9
call .syswrite
mov rsi, zeitaku
mov rdx, zeitaku_len
call .syswrite
mov rsi, imakara
mov rdx, imakara_len
call .syswrite
call .print_new_name
mov rsi, iikai
mov rdx, iikai_len
call .syswrite
call .print_new_name
mov rsi, henji
mov rdx, henji_len
call .syswrite
call .print_new_name
mov rsi, bikkuri
mov rdx, bikkuri_len
call .syswrite
call .exit
.print_new_name:
mov rsi, r10
mov rdx, 3
call .syswrite
ret
; return
; rax: address of new name char
; rcx: number of given name characters bytes
.get_random_one_word:
call .sysread ; set values into buffer and rax has input length
mov rcx, rax
xor rdx, rdx
mov rbx, 3
idiv rbx ; rax has length of japanese word
mov r8, rax ; store in r8
rdtsc ; set random value to rax(this return just clock...)
xor rdx, rdx
mov rbx, r8
idiv rbx ; get random number from 0 ~ r8-1
mov r9, rdx
imul r9, 3 ; set number of
mov rax, buffer
add rax, r9 ; set char address to rax
ret
.sysread:
mov rax, SYSCALL_SYSREAD
mov rdi, STDIN
mov rsi, buffer
mov rdx, BUFFER_SIZE
syscall
ret
.syswrite:
mov rax, SYSCALL_SYSWRITE
mov rdi, STDOUT
syscall
ret
.exit:
mov rax, SYSCALL_EXIT
xor rdi, rdi
syscall
.exit_without_rdi:
mov rax, SYSCALL_EXIT
syscall
実行例
$ ./test
契約書だよ。そこに名前を書きな。
山田太郎
フン。山田太郎というのかい。贅沢な名だねぇ。
今からお前の名前は郎だ。いいかい、郎だよ。分かったら返事をするんだ、郎!!
成功している!!
軽い気持ちで始めたアセンブリ…。ここまで長かった…。
湯婆婆エラー
文字列を何も入れずにEnterを押すと、もちろんエラーが起きます。
$ ./test
契約書だよ。そこに名前を書きな。
[1] 16882 floating point exception ./test
アセンブリっぽいエラーが出ましたね!!!!!
終わりに
流行っているからと思ってやってみたアセンブリですが、とても大変でした。
何から何まで他の言語では味わえない大変さがあります。そこが楽しいというのもありますが。
高級言語のありがたみが本当に分かる一日でした。
正直、もうアセンブリはやりたくない…。
ではまた!!