この記事は、Yahoo! JAPAN 18 新卒 2 Advent Calendar 2018の11日目の記事です。
前回は @walk8243 さんによる「CaaSって高いじゃん!IaaSでしょ!」でした。
次回は @teakun さんによる「Scratchで始める大人のための子ども向けプログラミング入門」です。
はじめに
Linuxの復習していた際に書いたx86_64アセンブリのQuineについて紹介します。Quineはコードと同一の文字列を出力するプログラムですが、テストが容易で言語や環境について理解する目的で活用できるかと思います。
※ 本記事で記載しているアセンブリはGasです。
コード
mov $m, %rsi
mov $280, %rdx
mov $1, %rdi
mov $1, %rax
syscall
push $34
mov %rsp, %rsi
mov $1, %rdx
mov $1, %rax
syscall
mov $m, %rsi
mov $280, %rdx
mov $1, %rax
syscall
push $0x00000a22
mov %rsp, %rsi
mov $2, %rdx
mov $1, %rax
syscall
xor %rdi, %rdi
mov $60, %rax
syscall
m:.ascii"mov $m, %rsi
mov $280, %rdx
mov $1, %rdi
mov $1, %rax
syscall
push $34
mov %rsp, %rsi
mov $1, %rdx
mov $1, %rax
syscall
mov $m, %rsi
mov $280, %rdx
mov $1, %rax
syscall
push $0x00000a22
mov %rsp, %rsi
mov $2, %rdx
mov $1, %rax
syscall
xor %rdi, %rdi
mov $60, %rax
syscall
m:.ascii"
解説
プログラムの概観
疑似コードで書くといたってシンプルな構成です。
定数mを出力する
"を出力する
定数mを出力する
"\nを出力する
終了する
mを宣言する(宣言時にmは""で囲われている)
構成する要素
プログラムの概観が決まれば、あとは構成する要素について理解するだけで自ずと完成します。
レジスタ
使用するレジスタはrax
、rdi
、rsi
、rdx
です。
レジスタ | 使い方 |
---|---|
rax |
システムコール番号を指定した状態でsyscall システムコールを実行することで該当のシステムコール番号に該当する命令が実行されます。 |
rdi |
システムコール命令の第1引数を指定します。 |
rsi |
システムコール命令の第2引数を指定します。 |
rdx |
システムコール命令の第3引数を指定します。 |
syscall
syscall
システムコールの書式について確認します。
$ man 2 syscall # 以下抜粋
SYNOPSIS
#define _GNU_SOURCE /* or _BSD_SOURCE or _SVID_SOURCE */
#include <unistd.h>
#include <sys/syscall.h> /* For SYS_xxx definitions */
int syscall(int number, ...);
引数にシステムコール番号を渡します。システムコールは次の形式で定義されています。
#define SYS__xxx __NR__xxx
つまり、xxxのシステムコール番号を得るためには__NR__xxx
という定数宣言を見ればよいということがわかります。しかし1個ずつ宣言を見ていくのも面倒なので、システムコールの名称と番号のマッピングを出力するausyscall
コマンドを使用して調べていくことにします。
$ man ausyscall # 以下抜粋
DESCRIPTION
ausyscall is a program that prints out the mapping from syscall name to number and reverse for the given arch.
write
syscall
で実行するシステムコール番号を調べます。
$ ausyscall write --exact
1
1
です。また、write
システムコールの書式についても確認します。
$ man 2 write # 以下抜粋
SYNOPSIS
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
第1引数にファイルディスクリプタ、第2引数に文字列バッファ、第3引数にサイズを渡します。ファイルディスクリプタは標準出力を選択するので、1
を入れます。また、文字列はスタック以外にも、.ascii
ディレクティブを指定することで簡単に記述できます。
ここまでくれば文字出力は簡単です。
mov $m, %rsi
mov $13, %rdx
mov $1, %rdi
mov $1, %rax
syscall
m:.ascii "Hello World!\n"
_exit
syscall
で実行するシステムコール番号を調べます。
$ ausyscall exit --exact
60
60
です。また、_exit
システムコールの書式についても確認します。
$ man 2 exit # 以下抜粋
SYNOPSIS
#include <unistd.h>
void _exit(int status);
第1引数にステータスコードを渡します。コマンドが正常終了したことをシェルに伝えるために0
を入れておきます。
_exitシステムコールを呼ぶことでsegmentation fault (core dumped)
が発生しなくなります。
mov $0, %rdi
mov $60, %rax
syscall
ASCII
.asciiディレクティブに書けない"
や\n
は即値として書く必要があります。
$ man ascii # 以下抜粋
Oct Dec Hex Char
---------------------------------------
012 10 0A LF '\n' (new line)
042 34 22 "
実行結果
次の実行環境で動作確認を行いました。
$ uname -a
Linux instance-1 2.6.32-754.6.3.el6.x86_64 #1 SMP Tue Oct 9 17:27:49 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ gcc --version
gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-23)
プログラムとその実行結果に差分がないことを確認します。
$ gcc -nostdlib quine.S -o quine
$ diff quine.S <(./quine)
$ file quine
quine: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
あとがき
初心に立ち返り、なるべくウェブで調べることなくman
を駆使して実装することを心がけてみました。普段アセンブリを書いてシステムコールを呼び出す機会なんてそうそうありませんが、高級言語で記述したときに内部でどのように動作するのかに関心を向けることも時には大切なのではないでしょうか。
参考文献
- https://ja.wikibooks.org/wiki/X86%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%A9/GAS%E3%81%A7%E3%81%AE%E6%96%87%E6%B3%95
- https://www.mztn.org/lxasm64/amd04.html
この記事は クリエイティブ・コモンズ 表示 4.0 国際 ライセンスの下に提供されています。