Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール
先日も書いた通り、私はC言語を体得するためには、まず最初に機械語を理解する必要があると考えている。現在、具体的にどのようなアプローチでアセンブリ言語からC言語への橋渡しを実現すれば良いのか、その方法を模索している最中だが、今日は先日作成した Toy code を紹介しよう。以下は、RDTSC 命令を紹介した際に使用したコードを若干改変した、simple_func.c である。
int simple_func(void); // Declare simple_func as an // function entry address asm("simple_func: "); // simple_func entry address asm(" movl $123, %eax "); // return 123 asm(" ret "); int main() { return simple_func(); }
お馴染みのインラインアセンブラーを用いて、123 を返すだけの単純な simple_func 関数を定義している。その実体は、mov 命令と ret 命令の2つだ。
$ gcc -o simple_func simple_func.c $ ./simple_func ; echo $? 123
私達の意図通り、コードは正常に動作している。さて、ここからが面白いところ。simple_func.c に手を加え dump_func.c を用意した。
1 #include <stdio.h> // printf() 2 3 int simple_func(void); // Declare simple_func as an 4 // function entry address 5 6 asm("simple_func: "); // simple_func entry address 7 asm(" movl $123, %eax "); // return 123 8 asm(" ret "); 9 asm(" .byte 0x55, 0xAA "); // Mark the end of function 10 11 int main() { 12 unsigned char* p; 13 14 p = (unsigned char*) simple_func; 15 while ((*p != 0x55) || (*(p + 1) != 0xAA)) 16 printf("%02X ", *p++); 17 printf("?n"); 18 }
simple_func 関数の最終尾に .byte 疑似命令を用いて、0x55 0xAA の2バイトをマーキング代わりに配置した。ret 命令の後ろなので、関数コード自体に影響はない。次に、main 関数内部で simple_func アドレスからのメモリー内容を 0x55 0xAA が見つかるまで、ダンプさせている。
この時、14行目のキャスト演算子 (unsigned char*) に注目してほしい。困ったことに、世の中で売られているほとんどのCテキストは、キャスト演算子を説明する際に、int <--> float 間の型変換にしか言及していない。しかし、システムプログラミング上でキャスト演算子が活躍するのは、上記例のようにコンパイラーに対して、ポインタ変数が指すオブジェクトの型を伝えるケースがほとんどなのである。simple_func シンボルは、関数のエントリーアドレスであるが、(unsigned char*) キャストを用いることで、Cコンパイラーは simple_func を unsigned char 配列の先頭アドレスとみなすようになるのである。すなわち、「キャスト演算子の真の意義は、馬鹿正直なCコンパイラーを騙すことにある」と言っても過言ではない。これは、C言語を縦横無尽に使いこなす上で、とても大切な認識だ。
にもかかわらず、時としてこの型変換を「まぁ、お行儀が悪いこと!」と、顔をしかめる先生方がおられるらしい。困ったことである。この状況は大学で行われる英語の授業に酷似している。英語の楽しさや素晴らしさを伝えることなく、やたらと文法だけにはこだわる教官。しかも目線の先は生徒ではなく、つまらないテキストだけに向いている。最悪だ。
ついでながら、業界的には、C言語の熟達度を知るためには「ポインタ変数を理解できているかどうか」がバロメーターになるらしい。が、私の見方は少々違う。私がCプログラマーの力量をテストするのであれば、まず「ポインタ変数とキャスト演算子の組み合わせをマスターしているかどうか」をチェックするだろう。実際、ポインタ変数とキャスト演算子の組み合わせは、カーネル・ライブラリーソース中では山ほど登場する。世の中の教科書が教える int <--> float 変換は、システムプログラミング上ではほとんど登場しないのである。
話を元に戻そう。dump_func.c をビルドし、実行すると次のようになる。
$ gcc -o dump_func dump_func.c $ ./dump_func B8 7B 00 00 00 C3
この結果から、simple_func 関数の実体は 0xB8, 0x7B, 0x00, 0x00, 0x00, 0xC3 の計6バイトであることが分かる。先頭5バイトが movl $123, %eax 命令、最後の1バイトが ret 命令の機械語である。このようにポインタ変数とキャスト演算子を組み合わせることで、神聖な実行コードと言えども、ただの配列データとして取り扱うことが可能になるのである。この調子で、もう一段踏み込んでみよう。
1 #include <stdio.h> // printf() 2 3 int simple_func(void); // Declare simple_func as an 4 // function entry address 5 6 asm("simple_func: "); // simple_func entry address 7 asm(" movl $123, %eax "); // return 123 8 asm(" ret "); 9 asm(" .byte 0x55, 0xAA "); // Mark the end of function 10 11 unsigned char text[ 256 ]; // .text section holder 12 13 int main() { 14 unsigned char *src, *dst; 15 int (*fn)(void); 16 17 src = (unsigned char*) simple_func; 18 dst = text; 19 while ((*src != 0x55) || (*(src + 1) != 0xAA)) { 20 printf("%02X ", *src); 21 *dst++ = *src++; 22 } 23 text[ 1 ] = 88; 24 printf("?n"); 25 26 fn = (int (*)(void)) text; 27 return (*fn)(); 28 }
今度の create_func.c は本邦初公開であるが、なかなか面白いコードである。simple_func アドレスからのバイトデータを、text 配列に転送している(17〜22行)。ここまでは、dump_func.c の延長線だが、面白いのは26行。なんと、text 配列の先頭アドレスをまたもやキャスト演算子を用いて、関数のエントリーアドレスへ読み替え、関数ポインター変数である fn にコピーしている。そして極めつけは、27行で text 配列中のデータを実行コードとみなして実行しているのである!
神をも恐れぬ不届き者とは、まさにこのことであるが、こんな暴挙が許されて良いのであろうか?良いのである。
$ gcc -o create_func create_func.c $ ./create_func ; echo $? B8 7B 00 00 00 C3 88
オ〜マイガァ〜〜!実行できてしまったよ。あれ、何か変でないかえ?create_func プロセスの終了ステータスが123ではなく、88になっているよ。
賢明な読者諸氏は既にお気づきのように、これは23行で123を88に書き換えているからである。このことは、C言語プログラムであっても、自分自身でプログラムを創造できることを意味している。
素晴らしきかな、ポインタ変数&キャスト演算子。この2つがある限り「C言語は不滅です」。