背景
最近になって,x64をちゃんと勉強したので,少しだけひねったHello WorldをC言語で書いてみることにした. (※x64のLinux環境でしか動作しない)
const char main[] = "\x48\xc7\xc0\x01\x00\x00\x00\xba\x0d\x00\x00\x00\xbf\x01\x00\x00\x00\xe8\x0c\x00\x00\x00\x48\x81\xc6\x11\x00\x00\x00\x0f\x05\x31\xc0\xc3\x48\x8b\x34\x24\xc3Hello World!\n";
何とワンライナーである. ヘッダのインクルードが無く,main関数ではなく,main配列となっている点が通常のHello Worldと比較すると異質な点であろう.
ワンライナーではわかりづらいので,コメントを入れてみる. 要は機械語である.
const char main[] = // mov $0x01,%rax "\x48\xc7\xc0\x01\x00\x00\x00" // mov $0x0d,%edx "\xba\x0d\x00\x00\x00" // mov $0x01,%edi "\xbf\x01\x00\x00\x00" // callq 0x0c "\xe8\x0c\x00\x00\x00" // add $0x11,%rsi "\x48\x81\xc6\x11\x00\x00\x00" // syscall "\x0f\x05" // xor %eax,%eax "\x31\xc0" // ret "\xc3" // mov (%rsp),%rsi "\x48\x8b\x34\x24" // ret "\xc3" // String data "Hello World!\n";
先にタネ明かしをすると,以下を行っているだけにすぎない.
// 第1引数:ファイルディスクリプタ (1: stdout) // 第2引数:文字列領域へのポインタ // 第3引数:出力文字数 write(1, "Hello World!\n", 13);
このことを念頭に置き,解説を行っていく.
解説
全体像
mainというのは所詮 crt*.o
中から呼び出しされているに過ぎない.
従って,main
という名前の指し示すアドレスの先に関数と同様のデータがあれば問題はない.
基本的に1つの配列に納めたかったので,コードとデータを同一の領域に置いてある. 別々にした場合,データのアドレスを取得するのが面倒になるためだ. (引数でデータのポインタを渡す等の処理が必要になるが,今回はmainを作っているため難しい)
配列 main
中の "Hello World!\n"
より前は実行するコードであり,"Hello World!\n"
は当然データである.
後は,コード中のいずれかの位置で,その地点のアドレスを取得し, "Hello World!\n"
領域のアドレスを取得すればよい.
システムコール
x64におけるシステムコールでは各レジスタが以下のように役割を果たす. 今回利用するのは第三引数まで.
レジスタ | 役割 |
---|---|
rax | システムコール番号 |
rdi | システムコールの第1引数 |
rsi | システムコールの第2引数 |
rdx | システムコールの第3引数 |
今回利用する write
システムコールのC言語APIは以下の通り.
size_t write(int fd, const void *buf, size_t count)
引数 | 役割 |
---|---|
fd |
ファイルディスクリプタ |
buf |
出力データのサイズ |
count |
出力文字数 |
以上より,レジスタを以下の状態にすることを目指す.
レジスタ | 中身 |
---|---|
rax | 1 (writeシステムコールの番号は1) |
rdi | 1 (stdoutは1) |
rsi | Hello Worldへのアドレス |
rdx | 13 (“Hello World!\n” の文字数は13) |
現在位置の取得
Hello World!
文字列アドレスの取得のために,実行しているコードの位置を取得する必要がある.
現在位置はレジスタripに入っているが,直接値を取り出すことはできない.
したがって,
という手段で取得する.
コードまとめ
ここまでの話をまとめると,配列の中身は以下の通りになる.
命令 | 動作 |
---|---|
mov $0x01,%rax |
raxに1を格納 |
mov $0x0d,%edx |
rdxの下位32bitに0x0d (0x14 )をセット |
mov $0x01,%edi |
rdiの下位32bitに0x01 をセット |
callq 0x0c |
12bbyte先を関数として呼び出し |
add $0x11,%rsi |
0x0fをrsiに加算.この命令位置から17byte先が "Hello World!\n" |
syscall |
システムコール |
xor %eax,%eax |
mainの返り値を0に設定(eaxは関数の返り値) |
ret |
main関数のreturn |
(%rsp),%rsi |
呼出元のアドレスをrsiに格納 |
ret |
呼出位置アドレス取得関数のreturn(add $0x0f,%rsi に復帰) |
"Hwllo World!\n" |
文字列データ |
その他いろいろ
通常,関数の返り値はraxに格納するものだが,面倒なので今回は直接rsiに放り込むという手段を取っている. 自分でコードを書く分には,x64の呼出規約を無視しても構わないだろう.
まとめ
この記事では,少し奇妙な形のC言語のHello Worldを紹介した. 一見,難解に見えるHello Worldのコードであるが,わかってしまえば大したことはない.
今回はx64用のコードを紹介した. なので,Wandboxに貼り付ければ実行できる.
x86については読者の課題としよう(笑)