/ «2003-10-06 (Mon) ^ 2003-10-08 (Wed)» ?
   西田 亙の本:GNU 開発ツール -- hello.c から a.out が誕生するまで --

Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール


2003-10-07 (Tue)

[Thoughts] 関数=変数=メモリー

先日も書いた通り、私は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言語は不滅です」。