LoginSignup
170
79

More than 3 years have passed since last update.

軽い気持ちでアセンブリで湯婆婆したらかなり大変だった件

Last updated at Posted at 2020-11-11

はじめに

最近流行っている Javaで湯婆婆を実装してみる - Qiita を、誰もやらないと思ったので「アセンブリ言語」でやってみました。
軽い気持ちではじめましたが、アセンブリは何をやるにも普通の言語で簡単にできるようなことができません‥。
たぶん、 ソースコードは湯婆婆史上一番長い と思います。

動画で確認したい方はこちらをどうぞ!
軽い気持ちでアセンブリで湯婆婆したら大変だった件
image

コードは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

アセンブリっぽいエラーが出ましたね!!!!!

終わりに

流行っているからと思ってやってみたアセンブリですが、とても大変でした。
何から何まで他の言語では味わえない大変さがあります。そこが楽しいというのもありますが。

高級言語のありがたみが本当に分かる一日でした。
正直、もうアセンブリはやりたくない…。

ではまた!!

170
79
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
170
79