アセンブリ言語に入門しよう

MMMバックエンドエンジニアの柳沼です。
いよいよ今週はJapan Container Daysですね!!

今回は、Hello Worldを出力してみることで、
アセンブリ言語の基礎を学んでみようと思います。

環境

今回はアセンブラにnasmを採用します。
バージョンは2.0.0以上であることを推奨します。

筆者の環境は以下のとおりです。

1
2
$ nasm --version
NASM version 2.13.01 compiled on Feb 28 2018

また、筆者のマシンのCPU情報は以下のとおりです。(4コアなため4行表示されています。)

1
2
3
4
5
$ 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でもたぶんできると思うので頑張ってください。)
ディストリ及びカーネルのバージョンは以下のとおりです。

1
2
3
4
5
$ cat /etc/gentoo-release
Gentoo Base System release 2.4.1

$ uname -r
4.14.8-gentoo-r1

セクション

本エントリではアセンブリの全ての文法には触れません。
とりあえずHello Worldの出力に必要な知識に絞って説明していきます。

今回は、 データセクションテキストセクション について説明します。

データセクション

データセクションは、データを格納するために使用します。以下のような書き方をします。

1
2
section .data
msg db "Hello world!"

msg は変数名、 "Hello world!" がデータです。
db は、1バイト分のメモリを確保することを意味します。(dwは2バイト、ddは4バイト)

つまり、上記は

1
char msg[] = "Hello, world!"

と同じようなことです。

テキストセクション

テキストセクションには、実際の処理を記述します。
テキストセクションは、以下のように開始します。

1
2
section .text
global _start

global _start は、プログラムのエントリーポイントになります。
_start はラベル(関数の開始位置みたいなもの)として使用します。

処理の文法は以下のとおりです。

1
[label:] instruction [operands] [; comment]

例えば、

1
mov     rax, 1

と書けば、 rax レジスタに 1 をセットする、という意味になります。

レジスタ

レジスタとは要するに記憶領域です。
メモリのように、データを保存することが可能です。
メモリよりも高速に動作し、CPUの上(物理)にあります。
本エントリで登場するレジスタは、x64環境のものであることに留意してください。(x86とかだと名前が違います。)

レジスタは(物理的に)複数存在しており、それぞれ名前がついています。
今回使用するのは以下のレジスタです。

  • rax : システムコール番号を保存する
  • rdi : 第1引数を保存する
  • rsi : 第2引数(のポインタ)を保存する
  • rdx : 第3引数を保存する

とりあえずこれだけわかれば、Hello Worldはできます。コードを見てみましょう。

アセンブリでHello World

以下のようなコードになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 を読んでいきます。

1
mov     rax, 1

は、raxに1を入れています。
システムコール番号1は sys_write ですね。
システムコール番号はLinuxカーネルのsyscall_64.tblから参照できます。(便利)

1
2
3
mov     rdi, 1
mov rsi, msg
mov rdx, 12

については、前述の通り、引数を詰めています。
sys_write の引数を知るために、manを見てみましょう。 (man 2 write)

1
2
3
4
5
6
7
8
9
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 には、データのサイズを指定します。

1
syscall

上記で、今のレジスタの状態を基にシステムコールを発行します。
つまり、上記3つの引数を基に、raxレジスタに詰めているシステムコール(sys_write)を発行してくれます。

1
2
3
mov    rax, 60
mov rdi, 0
syscall

こちらは、

1
exit(0)

を行っています。
システムコールの60番はexitで、引数は終了コードになります。

実行は、以下のように行います。

1
2
3
$ nasm -f elf64 -o hello.o hello.asm # オブジェクトファイル生成
$ ld -o hello hello.o # 実行バイナリ生成
$ ./hello # 実行

無事 Hello World! が表示されれば勝ちです。やりましたね。

まとめ

Hello Worldを通して、レジスタの使い方、システムコール発行の流れを追ってみました。
システムコールについては、こういうサイトを使えば
使うべきレジスタも教えてくれるので便利です。

アセンブリがマストなビジネスについてもMMMにご相談ください!

このエントリーをはてなブックマークに追加