Linux/gccで記号プログラミング

hotpepsi
2010-12-21

こんにちは。hotpepsiです。
以下はx86のLinuxで動作するhello.cのソースコードです。

__[]={
(((((('&'-' ')<<('$'-' '))|(','-' '))<<('('-' '))|
((('&'-' ')<<('$'-' '))|(','-' ')))<<('='-'-'))|
((((('&'-' ')<<('$'-' '))|('%'-' '))<<('('-' '))|
((('$'-' ')<<('$'-' '))|('('-' '))),
(((((('('-'!')<<('$'-' '))|('('-'!'))<<('('-' '))|
((('#'-'!')<<('$'-' '))|(' '-' ')))<<('='-'-'))|
((((('#'-'!')<<('$'-' '))|(','-' '))<<('('-' '))|
((('&'-' ')<<('$'-' '))|('/'-' '))),
(((((('&'-' ')<<('$'-' '))|('$'-' '))<<('('-' '))|
((('&'-' ')<<('$'-' '))|(','-' ')))<<('='-'-'))|
((((('('-'!')<<('$'-' '))|('#'-'!'))<<('('-' '))|
((('&'-' ')<<('$'-' '))|('/'-' '))),
((((((' '-' ')<<('$'-' '))|(' '-' '))<<('('-' '))|
(((' '-' ')<<('$'-' '))|(' '-' ')))<<('='-'-'))|
(((((' '-' ')<<('$'-' '))|('*'-' '))<<('('-' '))|
((('#'-'!')<<('$'-' '))|('!'-' '))),
};
_[]={
((((((' '-' ')<<('$'-' '))|(' '-' '))<<('('-' '))|
(((' '-' ')<<('$'-' '))|(' '-' ')))<<('='-'-'))|
(((((' '-' ')<<('$'-' '))|('$'-' '))<<('('-' '))|
((('+'-' ')<<('$'-' '))|('('-' '))),
((((((' '-' ')<<('$'-' '))|(' '-' '))<<('('-' '))|
(((' '-' ')<<('$'-' '))|('.'-' ')))<<('='-'-'))|
((((('+'-' ')<<('$'-' '))|('*'-' '))<<('('-' '))|
(((' '-' ')<<('$'-' '))|(' '-' '))),
((((((' '-' ')<<('$'-' '))|('!'-' '))<<('('-' '))|
((('+'-' ')<<('$'-' '))|('+'-' ')))<<('='-'-'))|
(((((' '-' ')<<('$'-' '))|(' '-' '))<<('('-' '))|
(((' '-' ')<<('$'-' '))|(' '-' '))),
(((((('+'-' ')<<('$'-' '))|(')'-' '))<<('('-' '))|
(((' '-' ')<<('$'-' '))|(' '-' ')))<<('='-'-'))|
(((((' '-' ')<<('$'-' '))|(' '-' '))<<('('-' '))|
(((' '-' ')<<('$'-' '))|(' '-' '))),
__,
((((((','-' ')<<('$'-' '))|(' '-' '))<<('('-' '))|
((('#'-' ')<<('$'-' '))|('!'-' ')))<<('='-'-'))|
((((('('-' ')<<('$'-' '))|(' '-' '))<<('('-' '))|
(((','-' ')<<('$'-' '))|('-'-' '))),
((((((')'-' ')<<('$'-' '))|(' '-' '))<<('('-' '))|
((('('-' ')<<('$'-' '))|(' '-' ')))<<('='-'-'))|
(((((','-' ')<<('$'-' '))|('-'-' '))<<('('-' '))|
((('$'-' ')<<('$'-' '))|(' '-' '))),
};

実行ファイルは

# gcc -c hello.c
# ld hello.o -o hello -e _

で作成することができます。

前回はついdefineを使ってしまいましたが、-Dオプションは本文に入れるべきdefine文をコマンドラインに出しただけ、と考えると反則でした。
なので今回はdefineなしでやってみました。

元ネタは

char main[] = "...";

という形でした。ここからcharとmainをなくしてみます。
まず型宣言のcharですが、これを省略するとint型になるので

main[] = { ... };

のようにint型の配列として初期化することにします。
次にmainですが、さて、世の中にはmain関数を使わないC言語のプログラムもたくさん存在します。
別にmainでなくてもよいのです。とりあえず _ という名前にしてみます。すると

_[] = { ... };

という形になりました。元ネタと同様、変数としてバイナリを定義してコードとして実行させます。
これなら記号だけでいけそうです。
エントリポイントを _ にしてリンクすれば、mainではなく _ からプログラムを開始できます。
(Cのランタイムライブラリが使えませんが、どのみちアルファベットを使用する関数は呼べないので問題ありません)

初期化要素として使うint型の値ですが、記号だけでint型の変数、といえば記号プログラミングではおなじみの '_' などが丁度よくint型ですのでこれを使います。
0と1さえ表せればあとは演算子でどうにでもなりますが、ASCII文字コード表を眺めていたら0から0x10まで作れそうだったので用意してみました。

0 ' '-' '
1 '!'-' '
2 '#'-'!'
3 '#'-' '
4 '$'-' '
5 '%'-' '
6 '&'-' '
7 '('-'!'
8 '('-' '
9 ')'-' '
A '*'-' '
B '+'-' '
C ','-' '
D '-'-' '
E '.'-' '
F '/'-' '
10 '='-'-'

この0~0x10までを使って、「Hello, world!」という文字列と、それを表示するコードを作ります。
上の記号を手で書くのは不便なので、数値を記号に変換することを考えてみます。
変換、そうdefineです。

#define _0 ' '-' '

数値そのままではシンボルにならないので_をつけましたが、これでなんとなく可読性のある数値によりプログラミングできそうです。より具体的には以下のようにします。

#define _0 ' '-' '
#define _1 '!'-' '
#define _2 '#'-'!'
#define _3 '#'-' '
#define _4 '$'-' '
#define _5 '%'-' '
#define _6 '&'-' '
#define _7 '('-'!'
#define _8 '('-' '
#define _9 ')'-' '
#define _A '*'-' '
#define _B '+'-' '
#define _C ','-' '
#define _D '-'-' '
#define _E '.'-' '
#define _F '/'-' '
#define _10 '='-'-'

#define MAKE8(a,b) ((a)<<(_4))|(b)
#define MAKE16(a,b,c,d) ((MAKE8(c,d))<<(_8))|(MAKE8(a,b))
#define MAKE32(a,b,c,d,e,f,g,h) ((MAKE16(e,f,g,h))<<(_10))|(MAKE16(a,b,c,d))

defineは使わないんじゃなかったのかよ、とお思いかもしれませんが、これは記号のコードを生成するだけのために利用します。
実は先頭のソースコードの元データはこのようになっています。

hello[]={
MAKE32(_4,_8,_6,_5,_6,_C,_6,_C),  // Hell
MAKE32(_6,_F,_2,_C,_2,_0,_7,_7),  // o, w
MAKE32(_6,_F,_7,_2,_6,_C,_6,_4),  // orld
MAKE32(_2,_1,_0,_A,_0,_0,_0,_0),  // !\n
};
main[]={
MAKE32(_B,_8,_0,_4,_0,_0,_0,_0),  // movl $4, %eax
MAKE32(_0,_0,_B,_A,_0,_E,_0,_0),  // movl $0xE, %edx
MAKE32(_0,_0,_0,_0,_B,_B,_0,_1),  // movl $1, %ebx
MAKE32(_0,_0,_0,_0,_0,_0,_B,_9),  // movl hello, %ecx
hello,  // なんとintとポインタのサイズが同じ!
MAKE32(_C,_D,_8,_0,_3,_1,_C,_0),  // int $0x80 / xorl %eax, %eax
MAKE32(_4,_0,_C,_D,_8,_0,_9,_0),  // incl %eax / int $0x80
};

MAKE32マクロを利用してこれを

# gcc -E hello.c

として展開すると、プリプロセッサが記号に変換してくれるのがわかると思います。