アセンブリ言語に入門しよう
MMMバックエンドエンジニアの柳沼です。
いよいよ今週はJapan Container Daysですね!!
今回は、Hello Worldを出力してみることで、
アセンブリ言語の基礎を学んでみようと思います。
環境
今回はアセンブラにnasmを採用します。
バージョンは2.0.0以上であることを推奨します。
筆者の環境は以下のとおりです。
$ nasm --version
NASM version 2.13.01 compiled on Feb 28 2018
また、筆者のマシンのCPU情報は以下のとおりです。(4コアなため4行表示されています。)
$ cat /proc/cpuinfo | grep 'model name'
model name : Intel(R) Core(TM) i3-6006U CPU @ 2.00GHz
model name : Intel(R) Core(TM) i3-6006U CPU @ 2.00GHz
model name : Intel(R) Core(TM) i3-6006U CPU @ 2.00GHz
model name : Intel(R) Core(TM) i3-6006U CPU @ 2.00GHz
LinuxディストリビューションはGentooを使用しています。(darwinでもたぶんできると思うので頑張ってください。)
ディストリ及びカーネルのバージョンは以下のとおりです。
$ cat /etc/gentoo-release
Gentoo Base System release 2.4.1
$ uname -r
4.14.8-gentoo-r1
セクション
本エントリではアセンブリの全ての文法には触れません。
とりあえずHello Worldの出力に必要な知識に絞って説明していきます。
今回は、 データセクション
と テキストセクション
について説明します。
データセクション
データセクションは、データを格納するために使用します。以下のような書き方をします。
section .data
msg db "Hello world!"
msg
は変数名、 "Hello world!"
がデータです。
db
は、1バイト分のメモリを確保することを意味します。(dw
は2バイト、dd
は4バイト)
つまり、上記は
char msg[] = "Hello, world!"
と同じようなことです。
テキストセクション
テキストセクションには、実際の処理を記述します。
テキストセクションは、以下のように開始します。
section .text
global _start
global _start
は、プログラムのエントリーポイントになります。
_start
はラベル(関数の開始位置みたいなもの)として使用します。
処理の文法は以下のとおりです。
[label:] instruction [operands] [; comment]
例えば、
mov rax, 1
と書けば、 rax
レジスタに 1
をセットする、という意味になります。
レジスタ
レジスタとは要するに記憶領域です。
メモリのように、データを保存することが可能です。
メモリよりも高速に動作し、CPUの上(物理)にあります。
本エントリで登場するレジスタは、x64環境のものであることに留意してください。(x86とかだと名前が違います。)
レジスタは(物理的に)複数存在しており、それぞれ名前がついています。
今回使用するのは以下のレジスタです。
- rax : システムコール番号を保存する
- rdi : 第1引数を保存する
- rsi : 第2引数(のポインタ)を保存する
- rdx : 第3引数を保存する
とりあえずこれだけわかれば、Hello Worldはできます。コードを見てみましょう。
アセンブリでHello World
以下のようなコードになります。
section .data
msg db "Hello world!"
section .text
global _start
_start:
mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, 12
syscall
mov rax, 60
mov rdi, 0
syscall
データセクションでは文字列を宣言しているだけです。
_start
を読んでいきます。
mov rax, 1
は、raxに1を入れています。
システムコール番号1は sys_write
ですね。
システムコール番号はLinuxカーネルのsyscall_64.tblから参照できます。(便利)
mov rdi, 1
mov rsi, msg
mov rdx, 12
については、前述の通り、引数を詰めています。
sys_write
の引数を知るために、manを見てみましょう。 (man 2 write
)
WRITE(2) Linux Programmer's Manual WRITE(2)
NAME
write - write to a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
引数が3つ必要なことがわかります。
fd
はファイルディスクリプタです。標準出力したいので、1をセットします。
buf
には文字列を指定します。
count
には、データのサイズを指定します。
syscall
上記で、今のレジスタの状態を基にシステムコールを発行します。
つまり、上記3つの引数を基に、raxレジスタに詰めているシステムコール(sys_write)を発行してくれます。
mov rax, 60
mov rdi, 0
syscall
こちらは、
exit(0)
を行っています。
システムコールの60番はexitで、引数は終了コードになります。
実行は、以下のように行います。
$ nasm -f elf64 -o hello.o hello.asm # オブジェクトファイル生成
$ ld -o hello hello.o # 実行バイナリ生成
$ ./hello # 実行
無事 Hello World!
が表示されれば勝ちです。やりましたね。
まとめ
Hello Worldを通して、レジスタの使い方、システムコール発行の流れを追ってみました。
システムコールについては、こういうサイトを使えば
使うべきレジスタも教えてくれるので便利です。
アセンブリがマストなビジネスについてもMMMにご相談ください!