はじめに
この記事は低レイヤーについてほとんど知識がない人間がアセンブリ言語を学んでいく際のメモ書きです。学習の際には「低レベルプログラミング」という本を参考書にしています。今回は前回に学習したレジスタの理解を前提として、実際にアセンブリ言語を書いていきたいと思います。
目次
- hello, world
- プログラムの中身
- コメント
- セクション
- ラベル
- ディレクティブ
- ディスクリプタ
- 基本的な命令
-
mov
命令 -
xor
命令
- システムコール
-
write
システムコール -
exit
システムコール
-
環境
- os: Ubuntu 22.04 LTS (WSL2)
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.1 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.1 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
- アセンブラ
$ nasm --version
NASM version 2.15.05
hello, world
とりあえず、はじめてのアセンブリプログラムということで、hello worldを出力してみる。
section .data
message: db 'hello, world!', 10
section .text
global _start
_start:
mov rax, 1 ; システムコールの番号をraxに入れる
mov rdi, 1 ; 1は書き込み先(descriptor)
mov rsi, message ; messageは文字の先頭
mov rdx, 14 ; 14は書き込むバイト数
syscall ; システムコールの呼び出し
mov rax, 60 ; 60は'exit'のsyscall番号
xor rdi, rdi
syscall
実行するには、
-
nasm
によりアセンブリして -
ld
でリンク - 適切な実行権限を与える
という手順で実行できます。
$ nasm -felf64 hello.asm -o hello.o
$ ld -o hello hello.o
$ chmod u+x hello
実行結果は以下のようになるはずです。
$ ./hello
hello, world
プログラムの中身
コメント
アセンブリ言語ではセミコロンからその行の最後までがコメントとして扱われる。
セクション
ノイマン型コンピュータでは1つのメモリにコードとデータが書き込まれる。プログラムを作成する際には、この2つを別々に扱うためにプログラムを複数のセクションに分ける。
.text
セクションには命令、.data
セクションにはグローバル変数(global variable)が記述される。
ラベル
可読性を上げるためにプログラマはラベルを使用することができる。ラベルによってアドレスに読みやすい名前を付けることができる。
プログラムでは
_start:
がそれにあたるが、この_start
というラベルはアセンブリプログラムのエントリーポイントであり、1つのアセンブリプログラムに必ず設けられるラベルである(1つのアセンブリプログラムを複数のファイルに分けて場合には、その中の1つに_start
ラベルが設けられる)。
また、_start
ラベルはglobal宣言が必要である。プログラム内でも
global _start
として宣言されている。
ディレクティブ
global宣言、セクションなどの変数処理を制御するコマンドをディレクティブ(directive)と呼ぶ。
プログラムでは、先程のglobal
やsection
の他にdb
というディレクティブも使用されていて、これはバイトデータを作成する際に使用される。
message: db 'hello, world!', 10
この, 10
でhello, world
にASCIIコードでの10の文字(改行\n
)を足している。
データを作成する際は
-
db
: バイト(1byte) -
dw
: ワード(2byte) -
dd
: ダブルワード(4byte) -
dq
: クアッドワード(8byte)
の4つのディレクティブが使用される。
ディスクリプタ
write
システムコールの出力先を決めるのがディスクリプタ(descripter, 記述子)である。これはファイルを識別する番号で、中身は単なる整数である。基本的にはディスクリプタによって指定されたファイルはopen
システムコールを呼び出し明示的にファイルをオープンしなければならないが、stdin
とstdout
、stderr
の3つはプログラムが開始されると即座にオープンされ、プログラマ側が管理することはできない。それぞれのディスクリプタの値は以下の通りである。
-
stdin
: 0 -
stdout
: 1 -
stderr
: 2
また、デフォルトではキーボード入力はstdin
に、ターミナル出力はstdout
にリンクされている。
今回の場合、hello, world
という文字列は標準出力に書き込まないといけないので、ディスクリプタの値は1とする。
プログラムでいうと以下がその部分に該当する。
mov rdi, 1
基本的な命令
mov
命令
mov
命令はある値をレジスタやメモリに書き込むことができる。書き込む値は直接指定する以外に、他のレジスタやメモリから取ることもできる。
プログラムでは
mov rax, 1
などがそれにあたる。この場合、レジスタrax
に直接1を書き込んでいる。
xor
命令
xor
命令は第一引数に対して第一引数と第二引数の排他的論理和をとって書き込む。
プログラムでは
xor rdi, rdi
がそれにあたる。rdi
=xor(rdi
, rdi
)となっている。
システムコール
システムコール(system call)はsyscall
により実行することができる。その際はrax
に入っているシステムコール番号のシステムコールが実行される。
システムコールの引数を設定するには順にrdi
、rsi
、rdx
、r10
、r9
に値を格納する。
今回のプログラムでは
mov rax, 1
mov rdi, 1
mov rsi, message
mov rdx, 14
syscall
という部分と
mov rax, 60
xor rdi, rdi
syscall
という部分がそれにあたる。
write
システムコール
hello, world
という文字列を出力するにはwrite
システムコールを利用する。これは出力先が標準出力だろうが、標準エラー出力だろうが、ファイルだろうが同じである。そしてwrite
システムコールの番号は1であるためレジスタrax
に格納しておく。
プログラムでいうと以下がその部分にあたる。
mov rax, 1
exit
システムコール
プログラムを正常に終了するためにexit
システムコールを実行する。
プログラムでは
mov rax, 60
がそれにあたり、rax
にexit
システムコール番号である60を代入することで実行している。
そして、exit
システムコールの引数は終了ステータスコードを指している。正常終了は0、一般的なエラーは1などである。
プログラムでは正常終了のステータスコードをrdi
に格納するために
xor rdi, rdi
としている。排他的論理和は2つの値が同じ時、0になるので必然的にrdi
には0が格納される。
つまり、
mov rdi, 0
とやっていることは同じになる。ただ、xor
を使用することで命令が小さくなるため、xor
を使用した書き方が一般的である。
ちなみにこのexit
システムコールが実行されないとプログラムは次の命令(write
システムコールが呼び出された後にメモリセルに入っているランダムな値を読み込んで実行される命令)が実行され、意図していない命令が実行され、クラッシュする。
exit
システムコールを呼び出さないコードは以下のようになる。
section .data
message: db 'hello, world!', 10
section .text
global _start
_start:
mov rax, 1 ; システムコールの番号をraxに入れる
mov rdi, 1 ; 1は書き込み先(descriptor)
mov rsi, message ; messageは文字の先頭
mov rdx, 14 ; 14は書き込むバイト数
syscall ; システムコールの呼び出し
これを以下のように実行する。
$ nasm -felf64 hello_without_exit.asm -o hello_without_exit.o
$ ld -o hello_without_exit hello_without_exit.o
$ chmod u+x hello_without_exit
$ ./hello_without_exit
すると、実行結果は
$ ./hello_without_exit
hello, world
zsh: segmentation fault ./hello_without_exit
となって、セグフォしていることがわかる。