intelの流儀(2)
近くのホームセンターで、バランスボールの特売広告が出てた。以前TVで、これを使って、すっきり ウェストになりましょうなんてのを見てた女房から、早速リクエストが有りましたよ。
おいらの認識では、某所のプログラマーも運動不足解消のために利用してるとか知ってたけど、 結構高いもんだと思ってた。一体幾らなんだと、ちらしを眺めてみると、670円とか。楽天とかで 検索すると5000円ぐらいはしてる。ひょっとしてバッタもんかなあ。
見に行ってみると、55cm、耐加重200kg、ノーバーストってなってた。破裂しませんよって事だな。 腰かけていて破裂したら、尾?骨割りで重症になりそうだから、これ重要だな。他の店より20円は 安くしてます。もっと安い所が有ったら、そこより更に20円引きしますだってさ。なにかヤマダ商法を 思い出すな。まあ、安いから買ってみるか。
同じ所に、腕を鍛える、伸びる紐みたいのも売ってた。紐って言ったけど、正確にはシリコンで 出来た直径1cm長さ30cmぐらいのもの。両端が球になってる。そこを掴んで引っ張る訳だ。 現代風のエキスパンダーだな。これなら、バネに挟まれて痛い思いをしないで済みそう。 但し、引っ張り強度は変更出来ないのが残念。まあ、女性用だからな。 フックの法則が成り立つ範囲内で使ってねって事だな。
買ってきたのはバランスボールだと思っていたけど、内部の説明書はジムボールになってた。
バランスボールに座るで、パソコンやったら 落ち着かんなあ。
シリアル出力
前回、BIOSでもシリアル出力するサービスが有る事を知ったけど、横目で眺めてた。喉に骨が 刺さった気分で落ち着かないから、コードを書いてみた。
print: movb $0x00, %ah # set init mode movb $0xe7, %al # 9600, 8N2 xorw %dx, %dx # COM0 int $0x14 # Do init Serial line print_char: lodsb # (%si) -> %al ; %si++ orb %al, %al # Null ? jz print_end # yes, movb $0x01, %ah # set char out mode int $0x14 # Do output %al to COM0 jmp print_char print_end: ret
こんなコードを捻り出すまで、qemuがパニクッて、coreを吐いちゃったってのは秘密だ。実機だと 暴走の一言だけど、仮想だと面白いな。パニクッて、糞石の全てのレジスタをダンプしてくれる んだもの。記念に魚拓を撮っておくべきだったなあ。gdbでは到達出来ないレジスタも表示されて たからね。
上記のコードを書く時、
ポートの初期化 AH = 0x00; AL = BBBPPSCC: BBBの部分(ボーレイト):111=9600bps, 110=4800bps, 101=2400bps, 100=1200bps, 011=600bps, 010=300bps, 001=150bps, 000=110bps PPの部分(パリティ):00=なし, 01=奇数, 10=なし, 11=偶数 Sの部分(ストップビット):0=ストップビット1bit, 1=ストップビット2bit CCの部分(キャラクターサイズ):10=7bit, 11=8bit DX = ポート番号; (たとえば0) 戻り値:なし
こんな説明を参考にしたんだけど、ALに設定する初期値を、いきなりHexに変換出来なかったよ。 そこで、最初は、editor上で、9600ボーで行こう。111だな、パリティは無しだから00か、ストップビットは、 2Bitだから、1、最後は8ビットだから、11か。って具合に、111001111って、ビットパターンを 書き連ねてから、それをHEXに読み直して、変換した。ばかっぽいな。
アセンブラなら、当然低レイヤーなビットパターンを扱うはず。きっとビットパターンをそのまま 書ける書式が有るはず。info asしろってか? 何処に書いてあるんでしょうかね。こいうのは どうやって、調べたらいいんでしょう? みんな(おいら以外)頭がいいから、こういう事は 無いんでしょうな。頭の良いインド人なら、きっとHex同士の乗算とかも頭の中でやっちゃうん だろうな。
しょうがない、ぐぐる先生のお世話になるか。答え一発、 gasって所にありました。 $0b11100111 とか、書けばいいのね。2進、16進数で書けるなら、きっと8進数も受け付けて くれるに違いない。確認してみっか。きっと、$0o347 とかでいいはず。
real.s: Assembler messages: real.s:29: Error: junk `o347' after expression
そんなにガラクタ呼ばわりしなくてもいいと思うぞ。gasの作者さんは、名機のPDP-11を知らない 新世代の人なのね。
で、上記を探してた時、 C vs. Asm のも見つけました。MIPSで、同様な事を解説した本は持ってるけど、糞石対応は貴重な資料ですよ。
mbr
xv6から思いっきり脱線します。OpenBSDのソースツアーをしてたら、/sys/arch/i386/stand/mbr/mbr.S なんてのを発見。シリアルの制御例が載ってた。
#ifdef SERIAL /* Initialize the serial port to 9600 baud, 8N1. */ pushw %dx xorw %ax, %ax movb $0xe3, %ax movw $SERIAL, %dx int $0x14 popw %dx #endif : Lmessage: pushw %ax cld 1: lodsb /* %al = *%si++ */ testb %al, %al jz 1f call Lchr jmp 1b /* * Lchr: write the error message in %ds:%si to console */ Lchr: pushw %ax #ifdef SERIAL pushw %dx movb $0x01, %ah movw $SERIAL, %dx int $0x14 popw %dx #else
真面目にレジスターのセーブ/リストアをしてますなあ。lodsbの所のコメントに要注目かな。 マシン語のくせして複雑な動きをする命令なんで、忘却しないようにコメントを残している とな。罪作りなプログラマ泣かせの石だ事。
プログラマ泣かせと言えば、jz 1fとか、jmp 1bとか謎の記法が出てます。1bは書かれた所から 前に遡って、1:のラベルへ飛んでけって意味。(f)の場合は、先にあるラベルって意味。 適当なラベル名を考えるのを放棄したいプログラマ用です。きっと、名前は命ってあの人は目を 剥いて怒るだろうな。
まあ、schemeなんかでも無名な手続きを定義出来るようになってるから、いいんでないかい。脳負荷 低減に役立つしね。
この他、このソースにはBIOSの使い方を説明したコメントやブートテーブルの説明も書いてあって 有用だな。でも、時代はインテルが主導する GUID とかに移行しつつあるんで、歴史書になちゃうのかな?
そして、悪魔の仕様 Unified Extensible Firmware Interface とか、PAE とか、混沌の時代であります。参考に BIOSとUEFIの歴史とか GPTとMBRはどのように違うのか? をどうぞ。 これもそれも、インテル入ってるの実現のためです。(我ながら、思いっきり、 バイアスしてんな)
cpuid
どんどん脱線していきます。シリアルのコードを書いた時にパニクッて、糞石のレジスタが曝け出さ れた。おいらの予感では、これらの(特殊)レジスタは石の版によって有ったり無かったりするん だろうな。
FreeBSDのブートログを見てると、先頭の方に
CPU: Intel(R) Celeron(R) CPU 900 @ 2.20GHz (2194.50-MHz 686-class CPU) Origin = "GenuineIntel" Id = 0x1067a Family = 6 Model = 17 Stepping = 10 Features=0xfe3fbff<FPU,VME,DE,PSE,TSC,MSR,PAE,MCE,CX8,APIC,SEP,MTRR,PGE,MCA,CMOV,PAT,PSE36,DTS,ACPI,MMX,FXSR,SSE,SSE2,SS> Features2=0x80000201<SSE3,SSSE3,HV> AMD Features=0x100000<NX>
こんなのが出てくる。何処で出してるんだろう? 調べてみたら、/sys/i386/i386/identcpu.cって 所でやってた。非常に長ったらしい。printcpuinfo()の中で、地道に解析してる。ご苦労様です。
ここに書いてあるコードはC言語だけど、アセンブラレベルでは、どうやって特徴を採取してるんだろう? 魔の書を調べてみるか。中款の上にcpuidってのの説明が載ってた。この説明も非常に長ったらしい。 読むのが嫌になるよ。
そんな訳なんで、identcpu.cをcpuidで軽くgrepしてみた。
identcpu.c: do_cpuid(0x80860000, regs); identcpu.c: do_cpuid(0x80860001, regs); identcpu.c: do_cpuid(0x80860002, regs); identcpu.c: do_cpuid(0x80860003, (u_int*) &info[0]); identcpu.c: do_cpuid(0x80860004, (u_int*) &info[16]); identcpu.c: do_cpuid(0x80860005, (u_int*) &info[32]); identcpu.c: do_cpuid(0x80860006, (u_int*) &info[48]);
なんか、8086ってのが出てくるんですけど、これジョークでしょうか? 肝になりそうな、do_cpuidってのを探してみると、 /sys/i386/include/cpufunc.hに
static __inline void do_cpuid(u_int ax, u_int *p) { __asm __volatile("cpuid" : "=a" (p[0]), "=b" (p[1]), "=c" (p[2]), "=d" (p[3]) : "0" (ax)); }
ってのが見つかった。これ、どういう風に解釈したらいいの? アセンブラとCが融合してるっぽいぞ。 ダエモン君マークを付けた方が GCC Inline Assemblerの 説明をされてた。更に、 FreeBSDで学ぶインラインアセンブラの読み方 と言う、ドンピタリな解説も見つかった。有り難い事です。先人に感謝。要約すると
cpuid命令は,eaxレジスタで指定した種類のCPU情報(CPUの種類や利用可能な機能など)を 汎用レジスタeax,ebx,ecx,edxに返す命令. ところで,cpuid命令の説明の See also: の直前には,serialize instruction execution と 書かれている. cpuid命令が入ると命令の実行がシリアル化されるので,out-of-orderな最近のプロセッサで 命令の追い越しがおきなくなる効果あり.(Pentium以降) リング0からリング3のどの特権でも使える命令の中で一番使いやすいこともあって, シリアル化命令の中ではよく利用されていると思う. rdtsc命令と一緒に使うのがよくある使い方だろうか.
おいらの知らないrdtscなんて命令も出てきたな。常識として知っとけやと解説の方がほのめかし ておられるようなんで、後で見ておくか。それにしても、cpuidってのは結果が多値で返って くると言う、Schemeっぽいやつだなあ。CISCの面目を保つってる命令だ事。
おいら的には、シンプルが一番って言うUnix哲学が上から下まで貫通してて欲しかったぞ。まあ、 下流の宴も余り真剣にならずに早く上流へ行けってな。
C + Assembler
上流へこのまま行ってしまってはもったいなさそうなので、ちと実験してみます。
typedef unsigned int u_int; u_int regs[4]; static __inline void do_cpuid(u_int ax, u_int *p) { __asm __volatile("cpuid" : "=a" (p[0]), "=b" (p[1]), "=c" (p[2]), "=d" (p[3]) : "0" (ax)); } main(){ do_cpuid(0x80000000, regs); }
これをtest.cとでもして、コンパイルし、objdump -d でコードを覗いてみます。
08048430 <main>: : 8048441: c7 44 24 04 38 96 04 movl $0x8049638,0x4(%esp) 8048448: 08 8048449: c7 04 24 00 00 00 80 movl $0x80000000,(%esp) 8048450: e8 0b 00 00 00 call 8048460 <do_cpuid> : 08048460 <do_cpuid>: 8048460: 55 push %ebp 8048461: 89 e5 mov %esp,%ebp 8048463: 57 push %edi 8048464: 56 push %esi 8048465: 53 push %ebx 8048466: 83 ec 14 sub $0x14,%esp 8048469: 8b 7d 0c mov 0xc(%ebp),%edi 804846c: 83 c7 04 add $0x4,%edi 804846f: 8b 45 0c mov 0xc(%ebp),%eax 8048472: 83 c0 08 add $0x8,%eax 8048475: 89 45 ec mov %eax,-0x14(%ebp) 8048478: 8b 55 0c mov 0xc(%ebp),%edx 804847b: 83 c2 0c add $0xc,%edx 804847e: 89 55 f0 mov %edx,-0x10(%ebp) 8048481: 8b 4d 08 mov 0x8(%ebp),%ecx 8048484: 89 4d e0 mov %ecx,-0x20(%ebp) 8048487: 8b 45 e0 mov -0x20(%ebp),%eax 804848a: 0f a2 cpuid 804848c: 89 55 e4 mov %edx,-0x1c(%ebp) 804848f: 89 ce mov %ecx,%esi 8048491: 89 45 e8 mov %eax,-0x18(%ebp) 8048494: 8b 45 0c mov 0xc(%ebp),%eax 8048497: 8b 55 e8 mov -0x18(%ebp),%edx 804849a: 89 10 mov %edx,(%eax) 804849c: 89 1f mov %ebx,(%edi) 804849e: 8b 4d ec mov -0x14(%ebp),%ecx 80484a1: 89 31 mov %esi,(%ecx) 80484a3: 8b 55 e4 mov -0x1c(%ebp),%edx 80484a6: 8b 45 f0 mov -0x10(%ebp),%eax 80484a9: 89 10 mov %edx,(%eax) 80484ab: 83 c4 14 add $0x14,%esp 80484ae: 5b pop %ebx 80484af: 5e pop %esi 80484b0: 5f pop %edi 80484b1: 5d pop %ebp 80484b2: c3 ret
インラインで、その場に展開してくれるかと思ったら、そうじゃ無かった。メイン側で、regsの アドレスとeaxに渡すべき引数をスタックに積んでからdo_cpuidをコールしてる。呼ばれた方も ごちゃごちゃやってて読むのに疲れちゃう。(きっとCPUもこんな長ったらしいのを実行したく ないだろうな)
最適化をかければ、もっとスマートになるんちゃう? -O1を指定して、軽く最適化してみます。
08048430 <main>: : 8048447: b8 00 00 00 80 mov $0x80000000,%eax 804844c: 0f a2 cpuid 804844e: a3 f8 95 04 08 mov %eax,0x80495f8 8048453: 89 1d fc 95 04 08 mov %ebx,0x80495fc 8048459: 89 0d 00 96 04 08 mov %ecx,0x8049600 804845f: 89 15 04 96 04 08 mov %edx,0x8049604 :
今度は、ちゃんとその場に展開されました。返値の処理も配列のアドレスが即地として埋め込まれて いて、無駄がありません。
[sakae@secd ~/t]$ cc -g -O1 test.c [sakae@secd ~/t]$ gdb -q a.out (gdb) b main Breakpoint 1 at 0x8048447: file test.c, line 7. (gdb) run Starting program: /usr/home/sakae/t/a.out Breakpoint 1, main () at test.c:7 7 __asm __volatile("cpuid" (gdb) n 14 } (gdb) p/x regs $1 = {0x80000008, 0x0, 0x0, 0x0}
ちゃんとgdbもasmを認識してくれています。今日はここまで。
付録
暇にまかせて、info as した時に目についたもの。最初は、ラベル名考えるの面倒臭い人用の 説明。本物のOSのソースを読むとよく出て来るんで迷わないように。
There is no restriction on how you can use these labels, and you can reuse them too. So that it is possible to repeatedly define the same local label (using the same number `N'), although you can only refer to the most recently defined local label of that number (for a backwards reference) or the next definition of a specific local label for a forward reference. It is also worth noting that the first 10 local labels (`0:'...`9:') are implemented in a slightly more efficient manner than the others. Here is an example: 1: branch 1f 2: branch 1b 1: branch 2f 2: branch 1b Which is the equivalent of: label_1: branch label_3 label_2: branch label_1 label_3: branch label_4 label_4: branch label_3
次は、8進数でもOKよーって説明。なんでオクタルだけ、こんな記法を 許しているんだ。これはきっと歴史の所産に違いない。頭を0で始めるとオクタルと解釈する なんて、罠にはまりそうな仕様だから封印しておこう。
3.6.2.1 Integers ................ A binary integer is `0b' or `0B' followed by zero or more of the binary digits `01'. An octal integer is `0' followed by zero or more of the octal digits (`01234567'). A decimal integer starts with a non-zero digit followed by zero or more digits (`0123456789'). A hexadecimal integer is `0x' or `0X' followed by one or more hexadecimal digits chosen from `0123456789abcdefABCDEF'.