LoginSignup
9
1

More than 5 years have passed since last update.

アセンブリでQuine

Last updated at Posted at 2018-12-10

この記事は、Yahoo! JAPAN 18 新卒 2 Advent Calendar 2018の11日目の記事です。
前回は @walk8243 さんによる「CaaSって高いじゃん!IaaSでしょ!」でした。
次回は @teakun さんによる「Scratchで始める大人のための子ども向けプログラミング入門」です。

はじめに

Linuxの復習していた際に書いたx86_64アセンブリのQuineについて紹介します。Quineはコードと同一の文字列を出力するプログラムですが、テストが容易で言語や環境について理解する目的で活用できるかと思います。

※ 本記事で記載しているアセンブリはGasです。

コード

quine.S
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は""で囲われている)

構成する要素

プログラムの概観が決まれば、あとは構成する要素について理解するだけで自ずと完成します。

レジスタ

使用するレジスタはraxrdirsirdxです。

レジスタ 使い方
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, ...);

引数にシステムコール番号を渡します。システムコールは次の形式で定義されています。

sys/syscall.h
#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ディレクティブを指定することで簡単に記述できます。

ここまでくれば文字出力は簡単です。

hello.S
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)が発生しなくなります。

exit.S
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を駆使して実装することを心がけてみました。普段アセンブリを書いてシステムコールを呼び出す機会なんてそうそうありませんが、高級言語で記述したときに内部でどのように動作するのかに関心を向けることも時には大切なのではないでしょうか。

参考文献

クリエイティブ・コモンズ・ライセンス
この記事は クリエイティブ・コモンズ 表示 4.0 国際 ライセンスの下に提供されています。

9
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
1