Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール
さて、DJ Bernstein 氏が着目した RDTSC 命令であるが、この暗号のような名前は ReaD Time Stamp Counter に由来している。
余り知られていないが、Pentium 以降の x86 シリーズは、クロックレベルの解像度を持った64ビットカウンターを内部に有している(MSR: Model Specific Registers のひとつ)。この超高精度カウンターは、電源ONでゼロリセットされ、CPU 稼働中は1クロックサイクル毎に1ずつインクリメントされる。RDTSC 命令は本カウンターの現在値を EDX/EAX レジスターペアに格納するための命令である。
これだけではあまり実感が湧かないので、実際に計算してみよう。1GHz のクロック周波数をもった CPU の1クロックサイクルは、1/10^9 秒、すなわち1ナノ秒。光の速度をもってしても、30cm しか進めないほどの短い間隔で、64ビットカウンターは回り続けることになる。
頭の中でイメージすると、あっと言う間にカウンターがオーバーフローしてしまいそうな感じがするが、64ビットのパワーは底知れないものがある。bc を使って計算してみよう。まずは、64ビット長で表現可能な最大の整数値(unsigned)はどれぐらいの大きさになるのだろうか?
$ bc bc 1.06 Copyright 1991-1994, 1997, 1998, 2000 Free Software Foundation, Inc. This is free software with ABSOLUTELY NO WARRANTY. For details type `warranty'. 2^64-1 18446744073709551615
なんと20桁である。見ただけでゲンナリする程だが、この数値に1GHzの場合の1クロック時間を掛け合わせてみる。
(2^64-1)/10^9 18446744073
かなり、小さくなった。1GHz CPU の64ビットカウンターが一杯になるのは、電源が投入されてから184億秒後である。まだ、実感が湧かない。
(2^64-1)/10^9/(60*60*24) 213503 (2^64-1)/10^9/(60*60*24)/365 584
そうですか、584年後ですか。後10回生まれ変わったとしても、心配いらない訳ね。失礼しました。
本題に戻る。これも余り知られていないのだが、x86 GCC は64ビット長整数値に対応しており、long long 宣言を使用することができる。int は32ビットだが、long long を使うことで、一気に64ビットの世界を堪能することが可能になるのだ。
これに併せて、当然のことながら glibc も対応済みである。取り急ぎ、私達が知りたいのは printf での出力制御である。次の ll.c を見てほしい。
#include <stdio.h> // printf() int main() { int i = -1; long long l = -1; printf("%X?n", i); printf("%llX?n", l); printf("%u?n", i); printf("%llu?n", l); }
このプログラムを見るだけで、GCC 上での64ビット整数値の使いこなしのポイントはお分かり頂けるだろう。それでは、実行してみよう。
$ gcc -o ll ll.c $ ./ll FFFFFFFF FFFFFFFFFFFFFFFF 4294967295 18446744073709551615
詳細は省くが、int, long long 変数共に ー1を指定しているのは、このバイナリー値が unsigned では最大値に相当するからである。実際、実行結果は先ほど計算した 2^32-1, 2^64-1 と同じ値になっている。
なお、printf 中で64ビットを表示するためには、このプログラム例のように "ll" を前置する必要があるので注意してほしい。"ll" を指定しなければ、32ビット分の情報しか表示されない。詳細は、man 3 printf 中の "The length modifier" を参照してほしい。
それでは、RDTSC 命令・・と行きたいところだが、その前にどうしても、理解しておかねばならないことがある。それは、関数の戻り値の正体である。
これも世の中の教科書にはほとんど書かれていないのだが、実践では極めて重要。x86 GCC における次の事実は、知っておいて損はない。
しかし、これも所詮は机上の知識に過ぎない。実際に、次に示す 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(); }
このコードは GCC のインラインアセンブリ機能を用いて、simple_func 関数をまるごとアセンブリ言語で記述したものである。この3行のアセンブリソースは、C言語で simple_func() { return 123; } で記述した場合と同じ(正確にはより短縮した)コードに相当する。狐につままれた人も多いかと思うが、ともかく実行してみよう。
$ gcc -o simple_func simple_func.c $ ./simple_func ; echo $? 123
意図通り、親プロセスであるシェルに123が返されていることから、アセンブリ言語版 simple_func 関数は正常に動作していることが分かる。
simple_func.c は、アセンブリ言語を知らない人が見れば、思わず面食らってしまう内容だが、実は機械語こそがC言語の正体なのである。逆に言えば、アセンブリ言語・機械語を知らずして、C言語を制覇することは到底不可能だと、私は信じている。
にもかかわらず、世の中のC言語入門書は、一冊としてアセンブリ言語理解の必要性を説くものはない。それどころか、「1週間で分かる」、「すぐ分かる」、「猫でも分かる」などと甘い言葉で読者を誘い、「皆さん分かりますか?変数というのは箱なんですよ」という決まり文句で、善良なる初心者を迷宮に招き入れるのである。「変数=箱」という概念に囚われている限り、C言語を体得することは不可能。「変数=関数=構造体=メモリー」の境地に達した時、初めてC言語の本質が見えてくるのだが、このためには機械語の知識が必須である。
志を胸に秘めてC言語入門書を手に取ったものの、途中で挫折した若い人達の数は一体どれだけの数に上ることか?しかし、挫折した人はまだいい。中には「自分はC言語を理解できた」と思いこんでいる読者も少なくない。実は、これが一番たちが悪い。そして、「思いこませる」技術に長けた著者のテキストが、ベストセラーになるという悪循環が、この日本で続いている。
日本が置かれた暗い影ばかり見ていても仕方がない。世界には、ワクワクするようなことが満ちあふれている。知的好奇心を満たしてくれるものが、毎日のように登場している。人生、楽しまなければ損である。しかし、心底楽しむためには、それなりの技術と知識・経験が必要。プログラミングも、機械語の知識が加わることで、スクリプト言語やコンパイラーとは全く違う楽しみ方が出来るようになる。やってみよう。
先ほど紹介したように、Pentium CPU 内部のタイマーカウンターは64ビット長である。これに対してレジスターは32ビット長しかない。困った、半分のサイズしかないよ・・。
でも大丈夫。x86 では64ビット長の場合は、EDX および EAX レジスターの2つを組み合わせることで、演算を遂行することができるのである。本当?紙に書かれた文言ではなく、この目で確認することが大切だ。
#include <stdio.h> // printf() long long simple_longlong(void); // Declare simple_longlong as an // function entry address asm("simple_longlong: "); // simple_longlong entry address asm(" movl $0x55667788, %eax "); // return 0x1122334455667788 asm(" movl $0x11223344, %edx "); asm(" ret "); int main() { printf("%llx?n", simple_longlong()); return 0; }
64ビットでデータを扱う場合、上位32ビット(ワード)は EDX レジスター、下位32ビットは EAX レジスターに格納する決まりになっている。このため、simple_longlong 関数では、EDX に 0x11223344、EAX に 0x55667788 を転送した。
$ gcc -o longlong longlong.c $ ./longlong 1122334455667788
ブラボ〜!おっしゃる通りだね。それでは、準備は全て完了した。いよいよ RDTSC 命令を実行してみよう。このためには、インラインアセンブリーを用いて、Cソース中に直接 rdtsc 命令を展開する必要がある。
#include <stdio.h> // printf() long long exec_rdtsc(void); // Declare exec_rdtsc as an // function entry address asm("exec_rdtsc: "); // exec_rdtsc entry address asm(" rdtsc "); // Do RDTSC instruction asm(" ret "); int main() { printf("%lld?n", exec_rdtsc()); return 0; }
rdtsc.c は上のようになった。簡単である。asm() 文中に rdtsc と記載しているだけである。後は、ret (RETurn)命令を使って、呼び出し元へ帰るのみ。おみやげに、64ビットカウンターの値を EDX/EAX レジスターに携えながら。それでは、気合いを入れて実行。
$ gcc -o rdtsc rdtsc.c $ ./rdtsc 3225565264110002
お〜〜、いかにもそれらしい値が現れた。この値を検証してみよう。まず最初に、マシンの CPU およびそのクロック周波数を確認しておく。これには、/proc/ ディレクトリの cpuinfo が便利だ。
$ cat /proc/cpuinfo processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 11 model name : Intel(R) Celeron(TM) CPU 1000MHz stepping : 1 cpu MHz : 1002.306 cache size : 256 KB fdiv_bug : no hlt_bug : no f00f_bug : no coma_bug : no fpu : yes fpu_exception : yes cpuid level : 2 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 mmx fxsr sse bogomips : 1998.84
Celeron 1GHz であることが分かった。uptime からのデータを元に、rdtsc が返した値を検算してみよう。
$ uptime ; ./rdtsc 18:51:06 up 37 days, 6:23, 3 users, load average: 1.02, 1.01, 1.00 3227200233459745 $ bc bc 1.06 Copyright 1991-1994, 1997, 1998, 2000 Free Software Foundation, Inc. This is free software with ABSOLUTELY NO WARRANTY. For details type `warranty'. 3227200233459745/(60*60*24) 37351854553
64ビットカウンターから逆算すると、このサーバーの uptime は 37.35 日であることが分かる。uptime の表示値とは多少ズレがあるが、これこそが今後検討すべき課題である。
「時間道」は、まだ始まったばかりなのだ。