はじめに
- 書籍「低レベルプログラミング」を読んでアセンブラについて勉強したので、実際に適当な課題を解いて説明しながらアセンブラに入門してみる。
- 同書籍のコードが Docker で実行できる環境の構築についてはこちらを参照のこと。
true コマンド実装
- まずは一番簡単な文法を学習するため、終了コード 0 で exit システムコールを実行するだけの true コマンドを実装してみる。
コード
workdir/true.asm
asm:true.asm
global _start
section .text
_start:
mov rdi, 0
mov rax, 60
syscall
実行方法
- 本家のリポジトリをフォークしたものにコードと実行環境を追加し、(Docker があれば)すぐに試してみることができるようにしてみた。
# フォークしたリポジトリをクローン
git clone https://github.com/nirasan/low-level-programming
cd low-level-programming
# 実行環境の docker マシンをビルドして立ち上げる
docker_build.sh
docker_run.sh
# workdir をマウントしてあるので移動
cd /workdir
# true.asm のコンパイルと実行
nasm -felf64 true.asm -o true.o
ld true.o -o true
./true
echo $? #=> 0
true コマンド解説
1 行目の global
とは
- global は指定したシンボルへの参照を外部に公開するディレクティブ。
1 行目の _start
とは
-
.
や_
などで始まるものをシンボルと呼び 32bit の値を持つ。 - シンボルの中でも
:
で終わるものはラベルと呼び、定義された場所のアドレスを持つ。 -
_start
は 5 行目で定義されたラベルなのでこの場所のアドレスを値としてもつ。
1 行目の意味は
-
global _start
とすることで _start のシンボルと定義位置のアドレスを外部に公開する。 - ld では _start という名前のシンボルから処理を開始するように実行ファイルを作成するので、ここでプログラムのエントリーポイントが定義されている。
3 行目の section
とは
- section ディレクティブは記載されるコードがプログラム中でどのような意味を持つのかを定義する。
-
section .text
とすることで続いて記載されるコードが書き換え不可能なデータであると定義されるので、.text セクションにはプログラム本体や定数などを記載していく。 - 他にも書き換え可能な変数を定義するセクションなどがあるが、実際に利用する際に使用例とともに解説する。
8 行目の syscall
とは
- syscall は OS の機能を呼び出すコマンド。
- どの機能を呼び出すか、どんな引数で呼び出すか、などはあらかじめ準備する必要があり、6, 7 行目でその準備が行われているので先にそちらを見ていく。
6 行目の mov
とは
- mov は値をコピーするコマンドで、
mov A, B
で A に B の値をコピーする。 -
mov rdi, 0
とすると、レジスタ rdi の値を 0 にしている。 - レジスタとはあらかじめ用意されている記憶領域で容量と役割をもつ。
- rdi はデスティネーションインデックスレジスタと呼ばれ 64bit の大きさを持つ。
- rdi の役割として syscall 実行時に値が第一引数として参照される。
7 行目の意味は
-
mov rax, 60
で、レジスタ rax の値を 60 にしている。 - rax はアキュムレータレジスタと呼ばれ 64bit の大きさを持つ。
- rax の役割として syscall 実行時に値がシステムコール番号として参照される。
- exit などのシステムコールはシステムコール番号という番号で参照される。システムコール番号の一覧は Debian だと
ausyscall --dump
で参照できる。-> 参考URL - 60 番のシステムコールは exit
改めて 8 行目の意味は
- syscall コマンドを実行すると rax の値である 60 番のシステムコールの exit が実行される。
- exit は rdi に入っている値 0 を第一引数として実行され、終了コード 0 としてプログラムを終了する。
Hello World の実装
- 定番の "Hello, World" を出力するコードを実装することで、変数定義と write システムコールについて学習する。
workdir/hello.asm
section .data
message: db "Hello, World", 10
section .text
global _start
_start:
mov rax, 1 ; syscall number. 1 is write
mov rdi, 1 ; 1st argument. dest file descriptor number. 1 is stdout
mov rsi, message ; 2nd argument. input data start address
mov rdx, 13 ; 3rd argument. input data length
syscall
mov rax, 60 ; syscall number. 60 is exit
xor rdi, rdi ; 1st argument register
syscall
Hello World の解説
section .data
- .data セクションは書き換え可能なデータの定義セクションで、特に値がセットされた変数の定義を行うための宣言。
- セクションの切り替えはいつでも何度でもできる。
message: db "Hello, World", 10
-
db 1
で 1 を 8bit のデータとして書き込むというコマンド。 -
db 1, 2, 3
で 1, 2, 3 を連続して書き込む。 -
db "string"
で文字列を ASCII コードに変換して数値として連続して書き込む。 -
L: db 1, 2, 3
で db で書き込んだデータの先頭アドレスをラベル L で参照できるようにする。 - つまり
message: db "Hello, World", 10
は-
db 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 10
というデータを書き込んで - そのデータのアドレスを
message
ラベルで参照できるようにするということ - データの末尾の
10
は ASCII コードで改行
-
システムコール write の実行
-
_start
の直後 5 行でシステムコール write の呼び出しを行なっている。
レジストリ | 役割 | 値 | 値の意味 |
---|---|---|---|
rax | syscall 番号の指定 | 60 | write |
rdi | 第一引数. 出力先のファイルディスクリプタを指定する. | 1 | 標準出力 |
rsi | 第二引数. 出力するデータの先頭アドレス | message | 定義済みの "Hello, World", 10 のアドレス |
rdx | 第三引数. 出力するデータのバイト数 | 13 | message のバイト数 |
xor rdi, rdi
とは
- 最後から 3 行は exit システムコール呼び出しだが、第一引数の rdi の指定方法が前回と異なる。
-
xor A, B
とすると A と B で同じビットが 0 になるので、xor, rdi, rdi
と同じレジストリを指定すると全て 0 にクリアされる。mov rdi, 0
とするより実行効率がいいとかなんとか。
Hello World を繰り返し表示する
- Hello World を繰り返し表示するコードの実装で、jump, push, pop について学習する。
- 解説はコメントで記載した。
workdir/hello3.asm
section .data
message: db "Hello, World", 10
section .text
global _start
_start:
; レジスタ rcx をカウンターとして利用する. 初期値 3
mov rcx, 3
.loop:
; カウンターをデクリメント
dec rcx
; write システムコールのパラメータセット
mov rax, 1
mov rdi, 1
mov rsi, message
mov rdx, 13
; push コマンドでカウンターをスタックに一時保存
; rcx は syscall の返り値が挿入されるので上書きされても復帰できるように対比しておく
push rcx
; write の実行
syscall
; pop コマンドでカウンターをスタックから復帰させる
pop rcx
; test コマンドでカウンターが 0 であるかどうかをチェックする.
; もし 0 なら zero flag が立つ
test rcx, rcx
; jnz は jump if not zero の意味. zero flag が立っていなければ対象の .loop ラベルのアドレスへジャンプする.
jnz .loop
; exit の実行
mov rax, 60
xor rdi, rdi
syscall
関数コールと相対アドレスの指定
- 関数の定義と呼び出し方、相対アドレスの指定の仕方二種類について学習するため、ふたつの文字列を交互に出力するコードを実装してみる。
workdir/zip.asm
section .data
numbers: db "0123456789", 10
strings: db "ABCDEFGHIJ", 10
section .text
global _start
; 関数定義は単なるラベルで、終端で ret するだけ
print_numbers:
mov rax, 1
mov rdi, 1
; 出力する文字をカウンター分だけずらして一文字ずつ表示する
; mov を使って相対アドレスを指定する場合は一度 mov でベースをセットしてから add でオフセット分ずらす
mov rsi, numbers
add rsi, rcx
mov rdx, 1
push rcx
syscall
pop rcx
ret
print_strings:
mov rax, 1
mov rdi, 1
; lea A, B とすると A のアドレスを計算して B に入れる.
; lea を使うとひとつのコマンドで相対アドレスを指定することができる
lea rsi, [strings + rcx]
mov rdx, 1
push rcx
syscall
pop rcx
ret
; ついでに exit も関数化
exit:
mov rax, 60
xor rdi, rdi
syscall
_start:
mov rcx, 0
.loop:
; 関数呼び出しは call
call print_numbers
call print_strings
; 数値比較によるジャンプ
; jle は jump if less or equals
inc rcx
cmp rcx, 10
jle .loop
call exit
実行コマンドと実行結果
$ nasm -felf64 zip.asm && ld zip.o && ./a.out
0A1B2C3D4E5F6G7H8I9J
mov と lea の違いは
- mov は値のコピーで lea はアドレスを計算してコピーする
; label ラベルのアドレスを rax にセットする処理として
mov rax, label
; と
lea rax, [label]
; は等価
; label ラベルの起点から rcx の値のバイト数分後ろの位置のアドレスを rax にセットする処理として
mov rax, label
add rax, rcx
; と
lea rax, [label + rcx]
; は等価
FizzBuzz 問題
- ここまでの復習として FizzBuzz 問題を解いてみる。
- ロジックは単純だがレジスタの制御と文字列表示が面倒。
workdir/fizzbuzz.asm
section .data
str_fizz: db "fizz"
str_buzz: db "buzz"
str_numbers: db "0123456789"
str_newline: db 10
section .text
global _start
; fizz の表示
print_fizz:
mov rax, 1
mov rdi, 1
mov rsi, str_fizz
mov rdx, 4
push rcx
syscall
pop rcx
ret
; buzz の表示
print_buzz:
mov rax, 1
mov rdi, 1
mov rsi, str_buzz
mov rdx, 4
push rcx
syscall
pop rcx
ret
; カウンターの数字を3桁の10進数として表示する
print_number:
; カウンターを 100 で割って商を表示
mov rdx, 0
mov rax, rcx
mov rbx, 100
div rbx
; 余りを次で使うので保存
push rdx
call print_rax_number
; 余りを 10 で割って商を表示
pop rax
mov rdx, 0
mov rbx, 10
div rbx
; 余りを次で使うので保存
push rdx
call print_rax_number
; 余りを 1 の位として表示
pop rax
call print_rax_number
ret
; rax に入っている数字を文字列として表示
print_rax_number:
lea rsi, [str_numbers + rax]
mov rax, 1
mov rdi, 1
mov rdx, 1
push rcx
syscall
pop rcx
ret
; 改行の表示
print_newline:
mov rax, 1
mov rdi, 1
mov rsi, str_newline
mov rdx, 1
push rcx
syscall
pop rcx
ret
; fizz を表示するかチェック
; カウンターを 3 で割って 0 と比較だけしておく
; 0 なら je LABEL でラベルにジャンプできる
check_fizz:
mov rdx, 0
mov rax, rcx
mov rbx, 3
div rbx
cmp rdx, 0
ret
; fizz と同じように buzz のチェック
check_buzz:
mov rdx, 0
mov rax, rcx
mov rbx, 5
div rbx
cmp rdx, 0
ret
; 終了
exit:
mov rax, 60
xor rdi, rdi
syscall
_start:
; カウンターの初期化
mov rcx, 1
.loop:
; fizz のチェックとジャンプ
call check_fizz
je .print_fizz
; fizz じゃなければ buzz のチェックとジャンプ
call check_buzz
je .print_buzz
; fizz でも buzz でもなければカウンターを10進数で表示
.print_default:
call print_number
call print_newline
jmp .next
; fizz の表示
.print_fizz:
call print_fizz
; fizzbuzz のケースのためにチェックとジャンプ
call check_buzz
je .print_buzz
call print_newline
jmp .next
; buzz の表示
.print_buzz:
call print_buzz
call print_newline
jmp .next
.next:
; カウンターをインクリメントし 100 以下ならループする
inc rcx
cmp rcx, 100
jle .loop
call exit
実行コマンドと実行結果
$ nasm -felf64 fizzbuzz.asm && ld fizzbuzz.o && ./a.out
001
002
fizz
004
buzz
fizz
007
008
fizz
buzz
011
fizz
013
014
fizzbuzz
016
017
fizz
019
buzz
... 略 ...
ファイルをメモリに展開する
- システムコール open でファイルを開いて mmap でメモリに展開する。
workdir/mmap.asn
%define O_READONLY 0
%define PROT_READ 0x1
%define MAP_PRIVATE 0x2
section .data
fname: db 'test.txt', 0
section .text
global _start
; rdi に入っているアドレスの文字列を表示する
print_string:
push rdi
call string_length
pop rsi
mov rdx, rax
mov rax, 1
mov rdi, 1
syscall
ret
; rdi に入っている文字列の長さを計算する
; 結果は rax に入れて終了する
string_length:
xor rax, rax
.loop:
cmp byte [rdi+rax], 0
je .end
inc rax
jmp .loop
.end:
ret
_start:
; open の呼び出し. 詳細は後述
mov rax, 2
mov rdi, fname
mov rsi, O_READONLY
mov rdx, 0
syscall
; open によって rax に開いたファイルのファイルディスクリプタが入ってくるので保存しておく
mov r8, rax
; mmap の呼び出し. 詳細は後述
mov rax, 9
mov rdi, 0
mov rsi, 4096
mov rdx, PROT_READ
mov r10, MAP_PRIVATE
mov r9, 0
syscall
; mmap によって割り当てたメモリのアドレスが rax に入ってくるので rdi に保存し print_string で出力する
mov rdi, rax
call print_string
; exit
mov rax, 60
xor rdi, rdi
syscall
open システムコール
レジスタ | 値 | 意味 |
---|---|---|
rax | 2 | システムコール open の番号. 成功時にはファイルディスクリプタが入ってくる |
rdi | "test.text", 0 | ファイル名の文字列. 終端を null にする |
rsi | 0 | 読み書き権限のフラグ. 0 で読み込みのみ |
rdx | 0 | ファイル作成時のパーミッション. 読み込みのみなので 0 を指定 |
mmap システムコール
レジスタ | 値 | 意味 |
---|---|---|
rax | 9 | システムコール mmap の番号. 成功時には先頭アドレスが入ってくる |
rdi | 0 | 開くページのアドレス. 0 なら自動で割り当ててくれる. |
rsi | 4096 | 領域のサイズ |
rdx | 1 | メモリの保護属性. 0 は読み出し専用 |
r10 | 2 | 利用属性(shared か private か anonymouse かなど) |
r8 | fd | ファイルディスクリプタ |
r9 | 0 | ファイル内のオフセット |
- flags などの詳細は man mmap (2) を参照
マクロを使う
- すでに値を定義するだけの
%define
マクロは使っているが、マクロで引数をとったり繰り返しをしたり条件分岐をする方法を学習する。 - 以下ではマクロを使って "Hello, World" と "Hi, World" を繰り返し表示するだけのコードを実装する。
workdir/hello3_macro.asm
; %macro NAME ARGS_LANGTH から %endmacro までで引数を取るマクロを定義する
; NAME ARGS で定義後に呼び出すことができる
; 引数は %1, %2 でアクセスできる
; このマクロは文字列の先頭アドレスと文字列長を受け取って write するだけのもの
; @example: print_string message, 14
%macro print_string 2
mov rax, 1
mov rdi, 1
mov rsi, %1
mov rdx, %2
push rcx
syscall
pop rcx
%endmacro
section .data
hello: db 'Hello, World', 10
hi: db 'Hi, World', 10
section .text
global _start
_start:
; %assign A B で B の計算結果を A に入れる
%assign i 0
; %rep NUMBER から %endrep までで指定の回数だけループする
; %exitrep でループから脱出できる
%rep 10
; %if ... [%else ...] %endif で条件分岐できる
%if i % 2 = 0
print_string hi, 11
%else
print_string hello, 13
%endif
; %assign 計算が使えるのでカウンタをインクリメントできる
%assign i i + 1
%endrep
mov rax, 60
xor rdi, rdi
syscall
マクロを使って FizzBuzz 問題をもう一度
- FizzBuzz 問題の関数をマクロに置き換えてもう一度解いてみる。
- マクロにするとループや条件分岐がだいぶ普通の言語っぽく扱えて便利。
workdir/fizzbuzz_macro.asm
; 文字列を表示する
; Usage: print_string 文字列の先頭アドレス, 文字列の長さ
%macro print_string 2
mov rax, 1
mov rdi, 1
mov rsi, %1
mov rdx, %2
push rcx
syscall
pop rcx
%endmacro
; rax に入った数字を一文字表示する
%macro print_rax_number 0
lea rsi, [numbers + rax]
mov rax, 1
mov rdi, 1
mov rdx, 1
push rcx
syscall
pop rcx
%endmacro
; rcx に入った数字を10進数で指定した桁の一文字だけ表示する
; 実行後スタックの先頭に残りの桁の数字が入る
; Usage: print_number 表示する桁数
%macro print_number 1
mov rdx, 0
mov rax, rcx
mov rbx, %1
div rbx
push rdx
print_rax_number
%endmacro
; rcx に入った数字を10進数3桁で表示する
%macro print_number_100 0
print_number 100
pop rax
print_number 10
pop rax
print_rax_number
%endmacro
section .data
fizz: db "fizz"
buzz: db "buzz"
numbers: db "0123456789"
newline: db 10
section .text
global _start
_start:
%assign i 1
%rep 100
%if i % 15 = 0
print_string fizz, 4
print_string buzz, 4
print_string newline, 1
%elif i % 3 = 0
print_string fizz, 4
print_string newline, 1
%elif i % 5 = 0
print_string buzz, 4
print_string newline, 1
%else
mov rcx, i
print_number_100
print_string newline, 1
%endif
%assign i i + 1
%endrep
mov rax, 60
xor rdi, rdi
syscall
おわりに
- 書籍「低レベルプログラミング」の1章から7章について、実際に動作するコードでアセンブラについて解説してもらうことで基本的な部分については理解ができるようになった。一方で解説がそこまで親切ではなかったり OS や CPU についての解説が自分にはまだよく理解できない部分があったので、別の本でもうちょっと勉強してからまた戻ってきたいと思った。
- 以前 Nintendo Switch の「ヒューマン・リソース・マシーン」というゲームをやったが、これがほぼそのままアセンブラだったので「これゼミでやったやつだ!」みたいになった。Switch よりはエディタのほうが操作性がいいので意外と苦でもなく取り組めた。