2017.7.22
着手。ひとまず方針を探った。 最初はRaspberry Pi向けのOSを自作しようとしたが、CPUがARM64で、簡単に環境を準備できなかったのと、OS自作入門の知識が使えないのが痛いので、x86CPU向けにOSを作製することにした。 この踏ん切りにあたっては、次の資料が役に立った。(これならばやれそうという直感が動いた)
アセンブラはNASM
を選択。NASK
はOS自作筆者が作成したもので可搬性がないため。NASK
とNASM
の文法は大体似ているので問題ないと思っている。NASM
の参考となるページは次辺りだろうか(まだちゃんと読んでない):
img
ファイルの作り方。。。あんまりはっきりしないが、アセンブラを通した結果が既に実行形式バイナリになっているらしい:
nasm -f bin -o os-image.img source.S -l os-image.lst
-l
オプションでlst
ファイル(ソースとアセンブリの対応結果)が得られる。
エミュレータはQEMU
が使える。i386
環境ならば、qemu-system-i386
を使用すれば良い。OS自作入門と同じく、フロッピーディスクからの起動を試みるので、エミュレート時には次のコマンドを打つ:
qemu-system-i386 -fda os-image.img
実際のソースを見ていく。電源が投下されると、BIOSはアドレス0x7c00
(Assembler/なぜx86ではMBRが"0x7C00"にロードされるのか?(完全版))から始まる512バイトサイズのブートセクタをロードし実行を開始する。ブートセクタの末尾はマジックナンバー0x55AA
(ブートシグネチャ)を入れないと正しいブートセクタとして認識してくれない(ハマった)
BITS 16 ; 16ビット(リアル)モード ORG 0x7c00 ; このプログラムの読み込み位置 JMP entry ; goto entry; DB 0x90 ; ;; FAT12フォーマットフロッピーディスクの為の記述 DB "HELLO-OS" ; ブートセクタ名(8バイト) DW 512 ; 1セクタの大きさ = 512バイト DB 1 ; クラスタの大きさ = 1セクタ DW 1 ; FATがどこから始まるか = 1セクタ目から DB 2 ; FATの個数 = 2個 DW 224 ; ルートディレクトリ領域の大きさ = 224エントリ DW 2880 ; このドライブの大きさ = 2880セクタ DB 0xf0 ; メディアのタイプ = 0xf0 DW 9 ; FAT領域の長さ = 9セクタ DW 18 ; 1トラックのセクタ数 = 18 DW 2 ; ヘッドの数 = 2 DD 0 ; パーティションを使用しない DD 2880 ; このドライブの大きさ= 2880セクタ(もう一度書く) DB 0,0,0x29 ; 謎. 取り敢えずこの値にする DD 0xffffffff ; おそらくボリュームシリアル番号 DB "HELLO-OS " ; ディスク名(11バイト) DB "FAT12 " ; フォーマット名(8バイト) RESB 12 ; ;; プログラム本体 entry: MOV AX,0 ; AX <- 0 AX:アキュムレータ MOV SS,AX ; SS <- 0 SS:スタックセグメント MOV SP,0x7c00 ; SP <- 0x7c00 SP:スタックポインタ MOV DS,AX ; DS <- 0 DS:データセグメント MOV ES,AX ; ES <- 0 ES:エクストラセグメント MOV SI,msg ; SI <- msgのアドレス putloop: MOV AL,[SI] ; AL <- *SI (SIが指す内容の中身), 表示する文字を指定 ADD SI,1 ; SI <- SI + 1 (次の文字のアドレスへ) CMP AL,0 ; ALが0と等しいかチェック JE fin ; if (AL == 0) goto fin; MOV AH,0x0e ; 一文字表示関数 MOV BX,15 ; カラーコード指定 INT 0x10 ; ビデオBIOS呼び出し JMP putloop ; goto putloop fin: HLT ; 割込みが入るまでCPUを停止 JMP fin ; 無限ループ msg: DB 0x0a, 0x0a ; 改行2つ DB "hello, world" ; メッセージ DB 0x0a ; 改行 DB 0 ; 終端記号 TIMES 510-($-$$) DB 0 ; ここから510バイト目まで0埋め. $:現在の命令, $$:このプログラムの先頭の命令 ;; RESB 0x7dfe-($-$$) ; ここから0x7dfeバイト目まで0埋め. DB 0x55, 0xaa ; ブートシグネチャ(マジックナンバー) リトルエンディアンの時に, 511:55, 512:AA
こちらをimg
化してQEMU
で実行するとHello, worldが表示される。
OS自作入門では、FAT12ファイルシステムの記載がある。これはWindowsにフロッピーを突っ込んだ時にもフロッピーとして認識してもらえるため存在している。OS起動のためには必須ではないかも?
この後はこいつをIPL(Initial Program Loader)に拡張していく予定。IPLではディスクからRAM上にOSの中身を展開する処理が行われる。 また、最終的にOS側に処理を渡す処理も追加しなければならない。ここらへんはOS自作入門を頼りにやっていこうと思うが、おそらくメモリ配置の所でリンカスクリプトを使用する必要が出でくるかも。
TODO:
2017.7.23
OS自作入門ではedimg
を使用してイメージファイルを編集していたが、それも独自ツールなので可搬性がない。そこで一般的なツールを探していたら
に、mtools
内にmformat
とmcopy
コマンドがあり、これを使えばedimg
とほぼ同様の事が出来るみたい。
→軽く試してみたが、うまくいかない...もうチョットURLを熟読しないと。
リンカスクリプトも記述必須なのか? 以下のサイトを見ている途中で時間がやばいので寝る。
2017.7.26
TinyLinuxという偉大な先行者が。これをもっと優しくかつコンパクトに実装できれば。
2017.7.29
なぜブートセクタのファイルシステムにFAT12
を使用するのか。やっぱりフロッピーディスクとして認識してほしいから?
C言語を使える環境に持っていく為には32bitモード移行が必須。でも、32bit命令を使用しているとブートセクタの512バイトが速攻で枯れてしまう。そのため、メモリの空き領域を使用してそちらにジャンプし、32bitモードに移行してから、OS本体のロードを行う。(いわゆる多段ブート)
BIOSから制御が渡ってきた直後のメモリマップは以下(上のページから引用)
0x00000500 - 0x00007C00 と 0x00007E00 - 0x000A0000 が空いているので、そこの何処かにOS本体のローダを入れる事ができる。OS自作入門だと0xC200以降に配置していた。上のページだと0x00000500以降に配置していた。
また、読み込んだOSは慣例的に0x00100000以降に読み込むのが慣習のようである。はりぼてOSでは、
- 0x00000000 - 0x000FFFFF : ブートロードに使用。その後は未使用。(VRAM等は使用する)
- 0x00000FF0 - 0x00000FF8 : 画面モード等の情報
- 0x0000C200 - 0x???????? : カーネルローダ(2nd ブートローダ)
- 0x00007C00 - 0x00007DFF : ブートセクタ(1st ブートローダ)
- 0x00008000 - 0x000081FF : ブートセクタの内容
- 0x00008200 - 0x00034FFF : フロッピーディスクの内容
- 0x00100000 - 0x00267FFF : フロッピーディスクの内容記憶(1440KB)
- 0x00268000 - 0x0026F7FF : 空き(30KB)
- 0x0026F800 - 0x0026FFFF : IDT(2KB)
- 0x00270000 - 0x0027FFFF : GDT(64KB)
- 0x00280000 - 0x002FFFFF : bootpack.hrb (カーネルエントリ)
- 0x00300000 - 0x003FFFFF : スタック等(1MB)
- 0x00400000 - : 空き
となっている。2nd ブートローダの位置決めはimg
をひとまず作ってから調べるやり方をしていたので、そうではなく、自分で勝手に決めた位置でやりたい。→と思ったら、NASMのソースのORGで勝手に決められる模様。試してみる。
32bitモード移行へ
PICを無効にする
PIC(Programable Interrupt Controller, 割り込みコントローラ)を全て禁止するコードが有ったが、PIC0,PIC1のマスク設定については
が参考になった。
A20ゲートを無効にする
A20ゲートが有効になっていると、16bitアドレス空間(0x0000 - 0xFFFF)までしか使用する事が出来ず、0xFFFFから更に上位のアドレスはオーバーフローしてしまう(昔はこの動作を逆手に取っていたらしい。初めて読む486より)
その際にキーボードを介してA20ゲートを無効にする。BIOSへのIN,OUT
命令を使用するが、参考となるのが以下のサイト:
実際のコードは以下:
;; CPUから1MB以上のメモリにアクセスできるように, A20GATEを設定 CALL waitkbdout ; キーボードが送信可能になるのを待機 MOV AL,0xd1 ; 0xd1:キーボードコントローラは次の0x60の書き込みを受けた値をアウトプットポートに書き込む OUT 0x64,AL ; 0xd1を0x64(キーボードコントローラ)に書き込み CALL waitkbdout ; キーボードが送信可能になるのを待機 MOV AL,0xdf ; 0xdf:A20GATE信号線をONに OUT 0x60,AL ; 0xdfを0x60(ライトバッファレジスタ)に書き込み CALL waitkbdout ; A20GATEの処理完了待ち waitkbdout: ;; キーボードが送信可能になるのを待つ IN AL, 0x64 ; 0x64に対応するデバイス(キーボード)からALに読み込み AND AL, 0x02 ; 0x02(キーボード送信可能状態)と比較 JNZ waitkbdout ; 送信可能でなければ、ループ RET
仮のGDTを作っておく
32bitモード移行直後、すぐさまGDT(とGTDR)を使用したメモリアクセスが始まる。そのため32bitモード移行の前にGDTを作っておかなければならない。しかしGDT(IDT)OS側が自由に編集したいため、ブートロードの間は仮のGDTを使用する。
実際のGDTの定義は以下(アセンブリってデータも記述できるのがすごいんだよなあ):
;; プロテクトモード移行まで使用する仮のGDT, GDTRの定義 ;; OSカーネルに制御を渡したあとは別のアドレスでGTR, GDTRが再構築 ;; されるため、使われなくなる ;; 注意)OSカーネルが立ち上がるまではIDTも作られていないので割込みを ;; 処理することができない ALIGNB 16 ; MOVの高速化のため、16バイトアライン GDT0: RESB 8 ; ヌルセレクタ. 0番目のセレクタのGDTは定義できない ;; 1番目のGDT: データセグメント ;; リミット: 0xFFFFF, ベースアドレス: 0x00000000 (32bit空間全体) ;; 属性: 0x92(システム専用, 読み/書き可能, 実行不可能) DW 0xFFFF ; リミット0-15bit: 0xFFFF DW 0x0000 ; ベースアドレス0-15bit: DB 0x00 ; ベースアドレス16-23bit DB 0x92 ; 7bit:P(セグメント存在): 1 ; 6-5bit:DPL(特権レベル): 0 ; 4bit:S(セグメントか): 1 ; 3-1bit:TYPE(属性) ; 0bit:A(セグメントアクセス): 0 DB 0xCF ; 7bit:G(粒度): 1 ; 6bit:D(16bit:0, 32bit:1): 1 ; 5bit:0固定 ; 4bit:AVL(?) ; 3-0bit:リミット16-19bit: 0xF DB 0x00 ; ベースアドレス24-31bit ;; 2番目のGDT: コードセグメント ;; リミット: 0x7FFFF, ベースアドレス: 0x00280000(OSカーネル先頭) ;; 属性: 0x9a(システム専用, 実行/読み可能, 書き込み不可能) DW 0xFFFF DW 0x0000 DB 0x28 DB 0x9a DB 0x47 ; D:1, DB 0x00 GDTR0: ;; GDTR DW 8*3-1 ; GDTサイズ-1 = 8byte * 3 - 1 DD GDT0 ; GDT先頭アドレス
CR0を書き換える
CR0(Control Register 0)の0ビット目を立てる事で即座にプロテクト(32bit)モードに移行する。移行直後、命令パイプラインに16bit命令列が残っているので、JMP
命令を打つことで(分岐を発生させ?)パイプラインのフラッシュを行う。
;; プロテクト(32bit)モード移行 LGDT [GDTR0] ; 暫定のGDTを設定 MOV EAX,CR0 ; CR0は直接演算出来ないので、一旦EAXにコピー AND EAX,0x7fffffff ; bit31を0に(ページング禁止) OR EAX,0x00000001 ; bit0を1に(プロテクトモードへ) MOV CR0,EAX ; CR0にセット -> 即座に32bitモードへ JMP pipelineflush ; JMPにより16bit命令が詰まっているパイプラインをフラッシュ pipelineflush: ;; CS以外のセグメントレジスタを0x0008(現時点のGDT1番目)にセット ;; →OSをロードするまではCSはそのまま MOV AX,1*8 MOV DS,AX MOV ES,AX MOV FS,AX MOV GS,AX MOV SS,AX
2017.8.1
Macデフォルトのリンカーld
が普通じゃない(使えない)ため、x86 Linux向けのツールチェインを纏めてダウンロードした:
アセンブラはなるべくNASM
で統一したい。んで、NASM
でelf
の実行形式を得るには、
nasm -f elf input.asm -o input.o
とやる。elf
で吐ければあとはリンクだ...
2017.8.5
C言語を使用するにはリンカ必須。そして、C言語にエントリを作る方法は以下に簡単に書いてある。これを試してみたい。
デバッグには自前のprintf
が必須やなあ…BIOSを直接叩いていければ問題ないように思えるが。プロテクトモードではBIOSを叩けないからどうする...。
2017.8.14
コミケを通り過ぎる。初日には大神祐真さんからOS製作の同人を購入。自作OSの流れを汲んでいるので、参考になるはず。
色んな書籍で『パソコンのレガシィI/O活用大全』という本を参照しているが、入手困難。この本はIA-32に共通するメモリマップと割込みについて纏めてある日本語では大変珍しい書。図書館で写すしかないか…
C言語で書いた何もしないOSを作る
C言語に制御を渡す所まで書いてみたい。大まかには、
- ブートローダとカーネルローダは
NASM
から直接バイナリイメージにコンパイルする. - C言語でカーネルエントリを書く。
HLT
するだけだけど、C言語レベルではHLT
を呼び出せない(インラインアセンブラはナシにして…可読性が落ちる)ため、アセンブリで書いたHLT
するだけの関数を呼ぶ。- C言語で書いた部分はリンカで配置を決め、バイナリイメージを吐く。カーネルエントリは0x00280000以降に配置するよう決めているので、それに従えば良い。
- 全てのバイナリイメージは
cat
で連結し全体のイメージをまとめる。
となる。1. はこれまでで作ったので、2, 3について見ていく。
C言語で実装を書く
非常に簡単なソースにできる。
/* アセンブリで宣言されている_io_hltを期待 */ /* C言語で宣言した関数には_が付くが、アセンブリの場合は付かず、そのままのシンボル名が使用されることに注意. */ extern void _io_hlt(void); /* カーネルエントリ */ void kernel_entry(void) { for (;;) { _io_hlt(); } }
void _io_hlt(void)
についてはアセンブリで書く。
[BITS 32] ;; 32bitモードの機械語を吐かせる ;; アセンブリで書かれた関数を定義する. GLOBAL _io_hlt [SECTION .text] ; テキストセクション _io_hlt: ; void _io_hlt(void) HLT RET
ハマったのが、C言語でio_hlt()
で呼び出してもリンク時にundefined referenceとなってしまうところ。C言語で定義した関数シンボルには頭に_
が付くが、アセンブリで定義したシンボルはC言語ではそのまま参照しなければならない。
リンカスクリプト
最初に配置する位置(ロケーションカウンタ)を変えるだけ。
SECTIONS { /* 0x00280000から配置開始 */ . = 0x00280000; /* テキストセクション */ .text : { _text_start = .; *(.text) _text_end = .; } /* 読み取り専用データセクション */ .rodata : { _rodata_start = .; *(.rodata) _rodata_end = .; } /* データセクション */ .data : { _data_start = .; *(.data) _data_end = .; } . = ALIGN(4096); /* BSS(ロード時に初期化するデータ)セクション */ .bss : { _bss_start = .; *(.bss) _bss_end = .; } }
コンパイルとリンク
Makefile
を抜粋すると、
# カーネルイメージの作成 first-os-kernel.img: $(KERNEL_OBJS) $(LD) $(LDFLAGS) -T kernel.ld -Map kernel.map -o $@ $+
2017.8.15
今日は地固めとして、printk
、もしくはそれに準じる機能を実装したい。文字表示の時に即刻に問題になるのがフォント。
- BIOSの画面設定でテキストモードを使用すれば、VRAMにASCIIコードを書き込むだけで(BIOS内臓のフォントを使って)文字表示ができる。
- 作りながら学ぶOSカーネル、大神さんのOS5はこれ
- BIOSの画面設定でグラフィックモードを使用した場合は自前のフォント必須。
- OS自作入門はこれ
1.の場合、ネイティブなOSはこれで良いが、今作っているOSの目的地をゲームOSとしたいと考えた時、テキストモードではつらそうなので、2.を選ぶ。
OS自作入門ではhankaku.txtなるフォントファイルをobjに変換して組み込んでいたが、それだと専用ツールが必要で大変面倒。そこで、エキスパートCプログラミングで紹介されていたAAでビットパターンを記述するテクニックを使用してhankaku.txtを改ざん、転用した(ライセンス的にアウトかも…)。
/* AAで文字のビットパターンを表現する */ /* 参考: エキスパートCプログラミング 知られざるCの深層 */ #define X )*2+1 /* 1を表現 */ #define _ )*2 /* 0を表現 */ #define s ((((((((0 /* 一行の始まり */ char hankaku[4096] = { /* ... */ /* char 0x41 */ s _ _ _ _ _ _ _ _ , s _ _ _ X X _ _ _ , s _ _ _ X X _ _ _ , s _ _ _ X X _ _ _ , s _ _ _ X X _ _ _ , s _ _ X _ _ X _ _ , s _ _ X _ _ X _ _ , s _ _ X _ _ X _ _ , s _ _ X _ _ X _ _ , s _ X X X X X X _ , s _ X _ _ _ _ X _ , s _ X _ _ _ _ X _ , s _ X _ _ _ _ X _ , s X X X _ _ X X X , s _ _ _ _ _ _ _ _ , s _ _ _ _ _ _ _ _ , /* ... */ };
エキスパートCに助けられるのはこれで何回目か分からない。
…よし、これで画面表示に集中できる。VGAへの画面表示を復習して、フォント表示までやってやろう。
画面表示をやろうとしてとりあえずVRAM
をいじるのをやっていたら、動作が変わらない!なぜだ…と思ってmapファイルを見てみたら、エントリが0x00280000
に配置されていないことに気づく。テキストセクションの先頭にio_hlt
等のアセンブリ関数が配置されていて上手くエントリにジャンプ出来ていなかった。リンカに入力する順番を変え、エントリのオブジェクトファイルを先頭に入れることで一旦は解決したが、これは良いのか…?
ブート時の情報取得
カーネルロードを行っている時に、ついでに画面モード等の情報を特定のアドレスに保存している:
CYLS EQU 0x0ff0 ; 読み込んだシリンダ数(ブートセクタで設定される) LEDS EQU 0x0ff1 ; キーボードのキーロックとシフト状態 ;; bit0:右シフト, bit1:左シフト, bit2:Ctrl, bit3:Alt, bit4:Scrollロック, bit5:Numロック, bit6:Capsロック, bit7:Insertモード VMODE EQU 0x0ff2 ; 色数に関する情報。何ビットカラーか? SCRNX EQU 0x0ff4 ; Xの解像度 SCRNY EQU 0x0ff6 ; Yの解像度 VRAM EQU 0x0ff8 ; グラフィックバッファの開始アドレス
これを取得する構造体を定義する:
/* ブートセクタから得られる情報 */ struct BootInfo { char cyls; /* 0x0ff0: フロッピーから読み込んだシリンダ数 */ char leds; /* 0x0ff1: キーボードのLED情報 */ char vmode; /* 0x0ff2: 画面モード */ char reserve; /* 0x0ff3: パディング(2byte境界に揃える) */ short scrnx; /* 0x0ff4: Xの解像度 */ short scrny; /* 0x0ff6: Yの解像度 */ char *vram; /* 0x0ff8: VRAM(グラフィックバッファ)の開始アドレス */ };
所詮構造体もバイトフィールドの連続である。(short
のバイト境界に合わせるため、reserve
の1バイトパディング必須)カーネルローダ通過後、先頭さえ合っていれば一気に情報が取得できる:
#define BOOTINFO_PTR 0x0ff0 /* ブートセクタから得られる情報の配置開始位置 */ /* カーネルエントリ */ void kernel_main(void) { struct BootInfo *binfo; /* 指定したアドレスから情報を取得 */ binfo = (struct BootInfo *)BOOTINFO_PTR; /* ... */ }
文字の表示まで
...急ぎ足で文字の表示までやり抜く。
パレットの設定
画面に色を設定するため、ビデオDAコンバータのパレットなるものに色を設定する。パレットは1つの数値(パレット番号)とRGB値を結びつけるもの。一旦パレット番号とその色を設定しておけば、後は番号を指定するだけで欲しい色が得られると言う仕組み。
パレットの設定はset_pallete
関数でやっている。start
からend
までのパレット番号の色を設定する。
/* ビデオDAコンバータのパレット設定 */ void set_palette(int start, int end, unsigned char *rgb) { int i, eflags; /* ビデオDAコンバータのパレット設定関連のポート */ #define VIDEODACONV_PALETTE_NO_PORT 0x03c8 #define VIDEODACONV_PALETTE_COLOR_PORT 0x03c9 eflags = io_load_eflags(); /* 割込み許可フラグを退避 */ io_cli(); /* 割込み禁止 */ /* 設定を開始するパレット番号を指定 */ io_out8(VIDEODACONV_PALETTE_NO_PORT, start); for (i = start; i <= end; i++) { /* (連続で設定する時はパレット番号の指定を省略可能) */ io_out8(VIDEODACONV_PALETTE_COLOR_PORT, rgb[0] / 4); /* R */ io_out8(VIDEODACONV_PALETTE_COLOR_PORT, rgb[1] / 4); /* G */ io_out8(VIDEODACONV_PALETTE_COLOR_PORT, rgb[2] / 4); /* B */ rgb += 3; } io_store_eflags(eflags); /* 割込み許可フラグを復帰 */ return; }
ここは仕様通りに設定しているという他ない。色を設定する時に4で割っているのは、1バイトで256階調表現できるのに対して、パレットで設定できる色の階調が64しかないからみたいだ。色の組み合わせについてもOS自作入門からの16色をパクった。
文字表示
まずは1文字表示から。といっても半角フォントデータを上手くVRAM
のピクセルに展開してあげれば良い。丁度1行が1バイトデータになっているので、簡単なビット比較によりどのビットが立っているのか/立っていないのかが分かる。
/* 一文字表示 */ void putfont8(char *vram, int xsize, int x, int y, char c, char *font) { int i; char *p; /* VRAM書き込み位置 */ char row_bits; /* 1行のビットパターン */ for (i = 0; i < HANKAKU_HEIGHT; i++) { /* 書き出し位置の決定 */ p = vram + (y + i) * xsize + x; /* 1行分の1バイトデータを取得 */ row_bits = font[i]; /* 各ビットが立っているか検査し、立っていれば * 色を塗る */ if ((row_bits & 0x80) != 0){ p[0] = c; } if ((row_bits & 0x40) != 0){ p[1] = c; } if ((row_bits & 0x20) != 0){ p[2] = c; } if ((row_bits & 0x10) != 0){ p[3] = c; } if ((row_bits & 0x08) != 0){ p[4] = c; } if ((row_bits & 0x04) != 0){ p[5] = c; } if ((row_bits & 0x02) != 0){ p[6] = c; } if ((row_bits & 0x01) != 0){ p[7] = c; } } return; }
一文字表示関数が出来てしまえば、文字列表示も簡単。文字列終端(\0
)に到達するまでX方向にずらしながら文字を表示すれば良い。
/* 文字列表示 */ void putfonts8(char *vram, int xsize, int x, int y, char c, char *str) { /* どこかで定義されている半角フォントデータを参照 */ extern char hankaku[4096]; /* 文字列終端に達するまで印字 */ for (; *str != 0x00; str++) { putfont8(vram, xsize, x, y, c, hankaku + *str * HANKAKU_BYTES_PAR_WORD); /* X方向には1文字分ずらす */ x += 8; } }
これで擬似ターミナルが実装できる所まで来ていると思う。次にやりたいのは…マウス/キーボード(割込み)とタイマ。
2017.8.16
今日は割込み関連をこなして、マウス/キーボード/タイマを使えるようにしたい。すなわち、GDT、IDT、並びにPICを設定したい。
進捗出ず
...と思ったらさしたる進捗なし。起きた瞬間から頭にもやがかかっていた。1時間ほど作業したらニコ動開いており、気付いたら夜の20:00...。8.15に飛ばしすぎて集中力が戻っていないのかも。もしくは気温の急降下が原因か?(最大気温24℃)仕方なく、アイカツしてピアノやって寝る。
...と思ったけど、有効な進捗を出せる日が今日が最後なので、夜更かししてやる。
セグメントディスクリプタ
セグメントディスクリプタ(セグメント記述子)はメモリを区分けにしたもの(セグメント)を記述するIntel x86 CPU特有の概念。(実行形式ELFをロードする時のセグメントとは異なる。注意。)各セグメントは、そのセグメントが指すアドレス(ベースアドレス)と、その大きさ(リミット)、そして属性(アクセス権等)の情報を持つ。
構造体にすると、以下のビットフィールドの順に並んでいる。
/* セグメントディスクリプタ */ struct SegmentDescriptor { short limit_low; /* リミット0-15bit */ short base_low; /* ベースアドレス0-15bit */ char base_mid; /* ベースアドレス16-23bit */ char access_right; /* 属性0-7bit(本来は12bit幅だが、使わない) */ char limit_high; /* リミット16-19bit */ char base_high; /* ベースアドレス24-31bit */ };
属性についての詳細は省略(初めて読む486が死ぬほど丁寧)。GDT(Global Descriptor Table)はセグメントの設定を並べたテーブルであり、そのテーブル自体の情報(テーブルの先頭番地と設定個数)をGDTR(Global Descriptor Table Register)というレジスタに登録する。
セグメントディスクリプタを設定する関数は次の様に書ける:
void set_segment_descriptor(struct SegmentDescriptor *sd, unsigned int limit, int base_address, int access_right) { /* リミット値が0x100000(1M)を超えている時は、Gビットを * 立て、セグメントの粒度を4Kにする */ if (limit > 0xFFFFF) { access_right |= 0x8000; /* Gbitを立てる:リトルエンディアンで並んでいるため、上位バイトの最上位ビットを立てる. */ limit /= 0x1000; /* リミット値を4K(4096)の倍数に直す */ } /* 引数をビット演算/マスクにより構造体フィールドに設定 */ sd->limit_low = limit & 0xFFFF; sd->base_low = base & 0xFFFF; sd->base_mid = (base >> 16) & 0xFF; sd->access_right = access_right & 0xFF; sd->limit_high = ((limit >> 16) & 0x0F) | ((access_right >> 8) & 0xF0); sd->base_high = (base_address >> 24) & 0xFF; return; }
現段階でのGDTの初期化処理はこんな感じ:
void init_gdt(void) { struct SegmentDescriptor *gdt = (struct SegmentDescriptor *) GDT_ADDR; int i; /* 全GDTを0クリアにより初期化 */ /* -> セグメントレジスタは2byteのセレクタ値で選択できるが、下位3bitが使用できないため、最大13bit=8192個となる */ for (i = 0; i < 8192; i++) { set_segment_descriptor(&gdt[i], 0, 0, 0); } /* 2つのGDTを設定 セレクタ0はヌルセレクタで設定不可能 */ /* セレクタ1 * ベースアドレス 0x00000000 * リミット 0xFFFFFFFF (32bit空間全体) * 属性 0x40:Dビット1(32bitセグメント) 0x92:システム専用の読み書き可能なセグメント.実行は不可. */ set_segment_descriptor(&gdt[1], 0xFFFFFFFF, 0x00000000, 0x4092); /* セレクタ2 * ベースアドレス 0x00280000 * リミット 0x7FFFF * 属性 0x40:Dビット1(32bitセグメント) 0x9a:システム専用の実行/読み出し可能なセグメント.書き込みは不可. */ set_segment_descriptor(&gdt[2], 0x0007FFFF, 0x00280000, 0x409a); /* GDT_ADDR - GDT_ADDR + 0xFFFF をGDTとする */ /* 1GDTで8byte * 8192個 = 65536byte(64KB) = 0x10000 * リミット値としては0x10000 - 1 = 0xFFFF */ load_gdtr(0xFFFF, GDT_ADDR); return; }
ゲートディスクリプタ
ゲートディスクリプタは特権レベルの異なるセグメント間で関数の呼び出しが発生した時の出入り口(ゲート)となる。
ゲートディスクリプタを表す構造体は以下:
/* ゲートディスクリプタ */ struct GateDescriptor { short offset_low; /* オフセットアドレス0-15bit */ short selector; /* セレクタ0-15bit */ char dw_count; /* コピーカウント0-7bit */ char access_right; /* 属性0-7bit */ short offset_high; /* オフセットアドレス16-31bit */ };
セレクタは対象のセグメント番号を表し、オフセットはセグメント内のアドレスを表す。また、コピーカウントは呼び出し時の引数をコピーする個数(386, 486ではカウント×4バイトのコピーが発生する)を表す。
ゲートディスクリプタを設定する関数は次の様に書ける:
void set_gate_descriptor(struct GateDescriptor *gd, int offset, int selector, int access_right) { gd->offset_low = offset & 0xFFFF; gd->selector = selector; gd->dw_count = (access_right >> 8) & 0xFF; gd->access_right = access_right & 0xFF; gd->offset_high = (offset >> 16) & 0xFFFF; return; }
IDT - 割込みに使用するゲートディスクリプタ
リアルモードではメモリ下位アドレスに存在する割込みベクタテーブルを使用して割込み番号と割込みハンドラ(処理ルーチン)を対応付けるが、プロテクトモードではIDT(Intrrupt Descriptor Table)を用いて対応付けを行う。割込み番号はモードを問わず全部で256個。
IDT自体はゲートディスクリプタによって記述し、そしてIDTを並べたテーブルをGDTRと同様の構造を持つIDTR(Inturrupt Descriptor Table Register)に登録する事で割込みを処理できるようになる。
void init_idt(void) { struct GateDescriptor *idt = (struct GateDescriptor *) IDT_ADDR; int i; /* 全IDTを0クリアにより初期化 */ for (i = 0; i < 256; i++) { set_gate_descriptor(&idt[i], 0, 0, 0); } /* IDT_ADDR - IDT_ADDR + 0x7FF をIDTとする */ /* 1IDTで8byte * 割込み番号は全部で256個 = 2048byte(2KB) = 0x800 * リミット値としては0x800 - 1 = 0x7FF */ load_idtr(0x7FF, IDR_ADDR); }
GDT,IDTはビットフィールドの図が有ったほうが理解が進む。いつか書いておくべきだ。
...ここでAM 4:00。寝よう。PICは次に回す。
2017.8.19
PICを設定
割込みの実現は電気回路の割込み線IRQの電位が高くなること(電気回路的な事は省略!)でCPUがその電位変化を読み取ることで実現される。その割込み線を扱うマイコン(PIC, Programmable Interrupt Controller)を適切に設定することになる。と言ってもIA-32相当のPCではどうあがいてもほぼ同様な設定になるので、解説をメインとする。
Webにも情報があった:
Intel系CPUにはPICとして8bitマイコン8259A相当(エミュレーション回路で実現している場合がある)が2つ組み込まれている。 1つのPICはマスタPIC(PIC1)、もう一方のPICはスレーブPIC(PIC2)と呼ばれ、スレーブのIRはそのままマスタの割込み線に入力されている。その為、スレーブのIRが高電位になってもマスタの電位も高くならなければCPU側に割込みが通知されない。
PICの初期化
PICの初期化について見ていく。IA-32系では特定の8bitポートに書き込む事で初期化ができる。
以下、PC向けに使いそうな情報だけ列挙。完備な情報はパソコンのレガシィI/O活用大全という失われた書物にある。
マスタPICアドレス | スレーブPICアドレス | R/W | レジスタ | ビットフィールド |
---|---|---|---|---|
0x0020 | 0x00A0 | W | ICW1 | 4:1にすることで初期化開始 3:0にする(エッジトリガモード) 1:0にする(8259Aが複数) 0:0でICW4無し, 1でICW4有り |
0x0021 | 0x00A1 | W | OCW1(IMR) | [7:0]: 割込みマスク. bit7がIR7-bit0がIR0に対応し, 1で割込み禁止, 0で割込み許可. |
0x0021 | 0x00A1 | W | ICW2 | [7:3]: 割込みベクタのベースアドレス [2:0]: 未使用 |
0x0021 | 0x00A1 | W | ICW3 | マスタPICでは[7:0]のビットでスレーブと接続している割込み線を表す. IA-32ではIR2のため4. スレーブPICでは自身がどの割込み線に接続しているかを設定する. IA-32では2. |
0x0021 | 0x00A1 | W | ICW4 | [3:2]: バッファモード. IA-32では00もしくは01を指定しノンバッファモードとする. 0: μPM(?)1にすることで8086モードになる. IA-32では1にする. |
アドレスが重複しているが誤植ではない。初期化時は次のようにしてICW1-4に書き込む:
- OCW1に0xFFを書き込み割込みを禁止する。
- ICW1に書き込む際にbit4を立てる事で初期化を通知する。
- bit0を立てるとICW4まで書き込める。
- ICW2, ICW3, ICW4の順番で書き込む。
- OCW1に割込みを許可するビットを立てる。
また、OSに関連する部分としてはICW2のベクタのベースアドレス設定が重要に見える。0x00 - 0x1FはCPU側で予約されているため、0x20以降を使用しなければならない。ハードウェア的には、IRの入力はそれぞれ次の機器に接続されている。重要そうなのは太字にした。
入力 | マスタPIC用途 | スレーブPIC用途 |
---|---|---|
IR0 | システムタイマ | リアルタイムクロック(RTC) |
IR1 | PS/2 キーボード | --- |
IR2 | スレーブPICと接続 | --- |
IR3 | COM2(?) | --- |
IR4 | COM1(?) | PS/2 マウス |
IR5 | パラレルポート(?) | コプロセッサ |
IR6 | FDC(Floppy Disc Controller) | IDE(プライマリ?) |
IR7 | パラレルポート(?) | IDE(セカンダリ?) |
実際の初期化コードは以下:
/* PICの初期化 */ void init_pic(void) { io_out8(PIC1_IMR, 0xFF); /* PIC1の全ての割込みをマスク(禁止) */ io_out8(PIC2_IMR, 0xFF); /* PIC2の全ての割込みをマスク */ /* ICW1のbit4を立てることでPICに初期化を通知する. * その後はICW2-4の順にデータを書き込む */ /* PIC1(マスタ)の割込み設定 */ io_out8(PIC1_ICW1, 0x11); /* bit4に1を立てる事で初期化を通知, ICW4の入力を通知, エッジトリガモード */ io_out8(PIC1_ICW2, 0x20); /* 割り込みベクタのベースアドレスを指定:IRQ0-7は, INT20-27の割込みに対応する */ io_out8(PIC1_ICW3, 1 << 2); /* スレーブとIRQ2で接続している */ io_out8(PIC1_ICW4, 0x01); /* ノンバッファモード, 8086モード */ /* PIC2(スレーブ)の割込み設定 */ io_out8(PIC2_ICW1, 0x11); /* bit4に1を立てる事で初期化を通知, ICW4の入力を通知, エッジトリガモード */ io_out8(PIC2_ICW2, 0x28); /* IRQ8-15は, INT28-2Fの割込みに対応 */ io_out8(PIC2_ICW3, 2); /* マスタのIR2に接続している */ io_out8(PIC2_ICW4, 0x01); /* ノンバッファ/8086モード */ io_out8(PIC1_IMR, 0xFB); /* 11111011 PIC1はIRQ2以外は割込み禁止 */ io_out8(PIC2_IMR, 0xFF); /* PIC2は全ての割込み禁止 */ }
割込みハンドラ登録
PICの設定まで済めば、後はIDTのコールゲートに割込みハンドラを登録するだけ。
余談:C言語の関数呼び出し時はES, DS, SS
を揃える必要がある
... アセンブリからC言語を呼び出す時に、なぜES, DS, SS
を揃える必要があるのか? 答えが出ない:
asm_inthandler21: PUSH ES ; ES エクストラセグメントレジスタの退避 PUSH DS ; DS データセグメントレジスタの退避 PUSHAD ; PUSH E{A,C,D,B}X, ESP, EBP, ESI, EDI データレジスタの退避 MOV EAX,ESP ; EAX <- ESP PUSH EAX ; スタックポインタの退避 MOV AX,SS ; こ↑こ↓ MOV DS,AX ; こ↑こ↓ MOV ES,AX ; こ↑こ↓ CALL _inthandler21 ; C言語関数呼び出し POP EAX ; スタックポインタの復帰 POPAD ; PUSHADしたレジスタを復帰 POP DS ; DSを復帰 POP ES ; ESを復帰 IRETD ; 割込みから復帰
この問題、根が深くないか?
結論から言うとx86のメモリモデルに起因していると考えられる。Cコンパイラはデフォルトでスモールコードモデルを仮定して動作する為、異なるセグメント間でのアクセスが出来ず、SSとDS(ついでにESも。ESはよく使用される)を同一のセグメントに設定する必要がある。
によると、スモールメモリモデルでは、プログラムはDS=SSと仮定して動作する。
また下の情報によると、各モデルは次のポインタで構成される。nearは2バイトポインタで64KBまでの領域を指せ、farは4バイトポインタで、上位2バイトでセグメントセレクタを、下位2バイトでオフセットアドレスを指定する。
モデル名 | ポインタ |
---|---|
スモール・モデル | near |
ミディアム・モデル | コード・ポインタは far、データポインタは near |
ラージ・モデル | far ポインタ |
gccではデフォルトでスモールコード(notメモリ)モデルが選択される。ソースはGCC公式:
コードモデルについては(64bitだけど)次の記事が詳しそう:
2017.8.21
プリパラライブ大阪完了(一言感想:楽しいに尽きる)。とんでもない疲労のなかのぞみに乗っている。このタイミングでOSをどこまで作るか少し纏めておきたい。欲しい機能は次の様に纏めた:
- マウス割込み/キーボード割込みを完成させる
- 現状の延長上で問題なく出来る見込み。
- サブタスクとして、割込み等のメッセージを受けるFIFOをどこかに作製する必要がある。
- メモリマネージャ
- 仮想メモリは使用する予定なし。ブロック単位で切ってメモリを管理できるようにする。
- 文字列を扱う標準関数(
sprintf
さえあれば)がひとまず欲しい。 - タイマ
- タイムアウトをFIFOによって管理する実体。
- 以降の機能実現に必須。
- マルチタスク
- TSSを扱ってタスク構造体に抽象化。
- スケジューリングは王道中の王道、優先度ありのラウンドロビンでやるつもり。
- コンソール
- 今の所GUIはほぼなし、コンソールがメインとなるOSの作成を考えている。文字列入力/出力を頑張ってやる。
- ファイルシステム
- →作業が重そう。自作OSに例がないため。
- CP/Mファイルシステムライクな単純かつフラットなファイルシステムを実装したい。
- 構造化ファイルシステム構造はまたあとで。
- API(システムコール)
- 割込み番号のいずれかの番号をOSのシステムコール用に割り当てる。
- APIを呼び出すことでアプリを作りやすくする。サポートするのはメモリ管理、文字列表示、ファイルオープン等(自作OSを参考に、API仕様の明文化もしたい)
- ELF対応
- ELFの実行形式に対応したローダを作成して、32bitマシン向けにビルドしたバイナリを動かせるようにする。
- 優先度高め。(はりぼてOSではhrbフォーマットを持っていたが、自分が作るものでは使えないから。)
- →組み込みOS自作入門でやってた… 組み込み自作入門ではOS自体をロードして実行する所で使っていたが、ここではアプリをELFとしたい。
- ウィンドウ対応
ゲームを作るための基盤。ウィンドウ作成、矩形/線/円描写APIを追加。
音声出力対応
- 最初はビープ音(ビープ音の和音対応はぜひとも)、次はサウンドドライバ…(出来るのか?)
昨年やった組み込みOSの実績が活かせそう。lib.c
では標準ライブラリを使わないユーティリティ関数の実装があるし、defines.h
で型を固めてるし…。型はstdint.h相当の型でtypedef
しておきたいなあ。
2017.8.26
割込みではまる
キーボード割込みの受付処理を作成中。シンボル解決が通らない問題があったが、何故かC言語関数のシンボルをアンダースコア無しで参照するとリンクが通る…
割込みが出来ねえ...asm_inthandler21, inthandler21
の単純な呼び出しは正常動作している(なのでNASM
でのC言語関数呼び出しはOK)のに、割込みを受け付けてくれない。しかも0x555f3b8bを実行しようとして落ちる(キーボード入力しないと落ちない。なので割込み発生で何意図したアドレスに飛んでいない。IDTのオフセットに強引にasm_inthandler21
のアドレスを指定してもだめ)。IDTかPICの設定がおかしいのにあたりを付けているが、どうもおかしくなさそう。
→原因判明。セグメント内のオフセットが指定できておらず、直で物理メモリを指定していた。
set_gate_descriptor(&idt[0x21], (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
としていたが、asm_inthandler21
で参照してしまうと物理メモリが指定されてしまう。コードセグメント内のオフセットで指定しなければならない。
次の様に、実際に配置されたアドレスを直接指定すれば割込みができる。
set_gate_descriptor(&idt[0x21], (int) 0x48c, 2 * 8, AR_INTGATE32);
これをどう回避すればいいんだ…? すげえ悩んでいたら、やっぱり同様の原因で詰まっていた人がいた。
解決策としては、32bit空間全体を指すコード用のGDTを作ってしまうこと。リンカスクリプトでどうやっても再配置できないから、IDTのコールゲート用に任意の物理メモリが見えるようにすればよい。設定は以下:
/* セレクタ3 * 32bit空間全体. IDTを物理メモリ上に配置できるようにしている */ set_segment_descriptor(&gdt[3], 0xFFFFFFFF, 0x00000000, AR_CODE32_ER);
進捗
キーボード割込みはOK。次はマウス割込みのついでにFIFO作り。
sprintf
がないとprintf
デバッグする権利さえ与えられん。早急な実装が必要。→va_start, va_end, va_arg
のマクロの実体もおそらく関数だから標準ライブラリをリンクしないといけない = 可変長引数がとれない...アセンブリで頑張れば(スタックポインタをずらして)やれるが、つらい。ひとまずは固定引数でやろう。
2017.9.20
仕事が狂気じみてて絵もピアノも全く手のつかない毎日。もしこれが長く続くようなら仕事の進退を考えなければならない。
さておき、OSではsprintf
の実装に入っているけど、試しにtsprintf
の実装をOSに追加したら突然ブラックアウトするようになってしまった...
標準ヘッダも標準ライブラリも使用していないので、va_arg
系が使えず、たいへん辛い。x86のスタックフレームを悪用した例があった:
ブラックアウトする原因を追う。
- 読み込んだシリンダ数が足りない?
- NO.10から30に変えたけど変化なし。
init_palette()
を抜いたら正常動作するようになったぞ?table_rgb
のstatic
記憶クラス指定子を外すか、const
を付けると正常に動く(??)- →リンカの配置によっては上手く動かない?要調査。取り敢えず読み専データだから
const
を付与(ついでに半角フォントもconst
を付与)
2017.9.21
TGSの合間、宿泊先の千葉カプセルホテル(ノーブル)にて作業。my_sprintf
は何となく動くようになってきたけど、振る舞いが非常に不安定。
- switch文の分岐を増やすとブラックアウトする
- if文に置き換えたら上手く動いた
tsprintf_decimal
関数にどの数値を入れてもマイナス符号が付いてしまう...
コンパイラ、リンカの設定不良を疑っている。
問題のswitch文周辺を逆アセンブルした。
if (*format == '%') { /* % に関する処理 */ 468: 8b 45 0c mov 0xc(%ebp),%eax 46b: 8a 00 mov (%eax),%al 46d: 3c 25 cmp $0x25,%al 46f: 0f 85 ae 01 00 00 jne 623 <my_sprintf+0x1e4> zeroflag = width = 0; 475: c7 45 f0 00 00 00 00 movl $0x0,-0x10(%ebp) 47c: 8b 45 f0 mov -0x10(%ebp),%eax 47f: 89 45 f4 mov %eax,-0xc(%ebp) format++; 482: 8b 45 0c mov 0xc(%ebp),%eax 485: 40 inc %eax 486: 89 45 0c mov %eax,0xc(%ebp) if (*format == '0') { 489: 8b 45 0c mov 0xc(%ebp),%eax 48c: 8a 00 mov (%eax),%al 48e: 3c 30 cmp $0x30,%al 490: 75 0e jne 4a0 <my_sprintf+0x61> format++; 492: 8b 45 0c mov 0xc(%ebp),%eax 495: 40 inc %eax 496: 89 45 0c mov %eax,0xc(%ebp) zeroflag = 1; 499: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%ebp) } if ((*format >= '0') && (*format <= '9')) { 4a0: 8b 45 0c mov 0xc(%ebp),%eax 4a3: 8a 00 mov (%eax),%al 4a5: 3c 2f cmp $0x2f,%al 4a7: 7e 1d jle 4c6 <my_sprintf+0x87> 4a9: 8b 45 0c mov 0xc(%ebp),%eax 4ac: 8a 00 mov (%eax),%al 4ae: 3c 39 cmp $0x39,%al 4b0: 7f 14 jg 4c6 <my_sprintf+0x87> width = *(format++) - '0'; 4b2: 8b 45 0c mov 0xc(%ebp),%eax 4b5: 8d 50 01 lea 0x1(%eax),%edx 4b8: 89 55 0c mov %edx,0xc(%ebp) 4bb: 8a 00 mov (%eax),%al 4bd: 0f be c0 movsbl %al,%eax 4c0: 83 e8 30 sub $0x30,%eax 4c3: 89 45 f0 mov %eax,-0x10(%ebp) } switch (*format) { 4c6: 8b 45 0c mov 0xc(%ebp),%eax 4c9: 8a 00 mov (%eax),%al 4cb: 0f be c0 movsbl %al,%eax 4ce: 83 e8 58 sub $0x58,%eax 4d1: 83 f8 20 cmp $0x20,%eax 4d4: 0f 87 20 01 00 00 ja 5fa <my_sprintf+0x1bb> 4da: 8b 04 85 00 00 00 00 mov 0x0(,%eax,4),%eax 4e1: ff e0 jmp *%eax case 'd': /* 10進数 */ size = tsprintf_decimal(vargs[argc++], str_buf, zeroflag, width); 4e3: 8b 45 ec mov -0x14(%ebp),%eax 4e6: 8d 50 01 lea 0x1(%eax),%edx 4e9: 89 55 ec mov %edx,-0x14(%ebp) 4ec: 8d 14 85 00 00 00 00 lea 0x0(,%eax,4),%edx 4f3: 8b 45 e8 mov -0x18(%ebp),%eax 4f6: 01 d0 add %edx,%eax 4f8: 8b 00 mov (%eax),%eax 4fa: 8b 55 f0 mov -0x10(%ebp),%edx 4fd: 89 54 24 0c mov %edx,0xc(%esp) 501: 8b 55 f4 mov -0xc(%ebp),%edx 504: 89 54 24 08 mov %edx,0x8(%esp) 508: 8b 55 08 mov 0x8(%ebp),%edx 50b: 89 54 24 04 mov %edx,0x4(%esp) 50f: 89 04 24 mov %eax,(%esp) 512: e8 c7 fc ff ff call 1de <tsprintf_decimal> 517: 89 45 f8 mov %eax,-0x8(%ebp) break; 51a: e9 ef 00 00 00 jmp 60e <my_sprintf+0x1cf> case 'x': /* 16進数 0-f */ size = tsprintf_hexadecimal(vargs[argc++], str_buf, 0, zeroflag, width); 51f: 8b 45 ec mov -0x14(%ebp),%eax 522: 8d 50 01 lea 0x1(%eax),%edx 525: 89 55 ec mov %edx,-0x14(%ebp) 528: 8d 14 85 00 00 00 00 lea 0x0(,%eax,4),%edx 52f: 8b 45 e8 mov -0x18(%ebp),%eax 532: 01 d0 add %edx,%eax 534: 8b 00 mov (%eax),%eax 536: 8b 55 f0 mov -0x10(%ebp),%edx 539: 89 54 24 10 mov %edx,0x10(%esp) 53d: 8b 55 f4 mov -0xc(%ebp),%edx 540: 89 54 24 0c mov %edx,0xc(%esp) 544: c7 44 24 08 00 00 00 movl $0x0,0x8(%esp) 54b: 00 54c: 8b 55 08 mov 0x8(%ebp),%edx 54f: 89 54 24 04 mov %edx,0x4(%esp) 553: 89 04 24 mov %eax,(%esp) 556: e8 b5 fd ff ff call 310 <tsprintf_hexadecimal> 55b: 89 45 f8 mov %eax,-0x8(%ebp) break; 55e: e9 ab 00 00 00 jmp 60e <my_sprintf+0x1cf> case 'X': /* 16進数 0-F */ size = tsprintf_hexadecimal(vargs[argc++], str_buf, 1, zeroflag, width); 563: 8b 45 ec mov -0x14(%ebp),%eax 566: 8d 50 01 lea 0x1(%eax),%edx 569: 89 55 ec mov %edx,-0x14(%ebp) 56c: 8d 14 85 00 00 00 00 lea 0x0(,%eax,4),%edx 573: 8b 45 e8 mov -0x18(%ebp),%eax 576: 01 d0 add %edx,%eax 578: 8b 00 mov (%eax),%eax 57a: 8b 55 f0 mov -0x10(%ebp),%edx 57d: 89 54 24 10 mov %edx,0x10(%esp) 581: 8b 55 f4 mov -0xc(%ebp),%edx 584: 89 54 24 0c mov %edx,0xc(%esp) 588: c7 44 24 08 01 00 00 movl $0x1,0x8(%esp) 58f: 00 590: 8b 55 08 mov 0x8(%ebp),%edx 593: 89 54 24 04 mov %edx,0x4(%esp) 597: 89 04 24 mov %eax,(%esp) 59a: e8 71 fd ff ff call 310 <tsprintf_hexadecimal> 59f: 89 45 f8 mov %eax,-0x8(%ebp) break; 5a2: eb 6a jmp 60e <my_sprintf+0x1cf> case 'c': /* キャラクター */ size = tsprintf_char(vargs[argc++], str_buf); 5a4: 8b 45 ec mov -0x14(%ebp),%eax 5a7: 8d 50 01 lea 0x1(%eax),%edx 5aa: 89 55 ec mov %edx,-0x14(%ebp) 5ad: 8d 14 85 00 00 00 00 lea 0x0(,%eax,4),%edx 5b4: 8b 45 e8 mov -0x18(%ebp),%eax 5b7: 01 d0 add %edx,%eax 5b9: 8b 00 mov (%eax),%eax 5bb: 8b 55 08 mov 0x8(%ebp),%edx 5be: 89 54 24 04 mov %edx,0x4(%esp) 5c2: 89 04 24 mov %eax,(%esp) 5c5: e8 2e fe ff ff call 3f8 <tsprintf_char> 5ca: 89 45 f8 mov %eax,-0x8(%ebp) break; 5cd: eb 3f jmp 60e <my_sprintf+0x1cf> case 's': /* ASCIIZ文字列 */ size = tsprintf_string((char *)vargs[argc++], str_buf); 5cf: 8b 45 ec mov -0x14(%ebp),%eax 5d2: 8d 50 01 lea 0x1(%eax),%edx 5d5: 89 55 ec mov %edx,-0x14(%ebp) 5d8: 8d 14 85 00 00 00 00 lea 0x0(,%eax,4),%edx 5df: 8b 45 e8 mov -0x18(%ebp),%eax 5e2: 01 d0 add %edx,%eax 5e4: 8b 00 mov (%eax),%eax 5e6: 8b 55 08 mov 0x8(%ebp),%edx 5e9: 89 54 24 04 mov %edx,0x4(%esp) 5ed: 89 04 24 mov %eax,(%esp) 5f0: e8 17 fe ff ff call 40c <tsprintf_string> 5f5: 89 45 f8 mov %eax,-0x8(%ebp) break; 5f8: eb 14 jmp 60e <my_sprintf+0x1cf> default: /* コントロールコード以外の文字 */ /* %%(%に対応)はここで対応される */ len++; 5fa: ff 45 fc incl -0x4(%ebp) *(str_buf++) = *format; 5fd: 8b 45 08 mov 0x8(%ebp),%eax 600: 8d 50 01 lea 0x1(%eax),%edx 603: 89 55 08 mov %edx,0x8(%ebp) 606: 8b 55 0c mov 0xc(%ebp),%edx 609: 8a 12 mov (%edx),%dl 60b: 88 10 mov %dl,(%eax) break; 60d: 90 nop }
各case文内で正しく処理は行っていそう。ただ、分岐で明らかに最適化が入っている:
switch (*format) { 4c6: 8b 45 0c mov 0xc(%ebp),%eax 4c9: 8a 00 mov (%eax),%al 4cb: 0f be c0 movsbl %al,%eax 4ce: 83 e8 58 sub $0x58,%eax 4d1: 83 f8 20 cmp $0x20,%eax 4d4: 0f 87 20 01 00 00 ja 5fa <my_sprintf+0x1bb> 4da: 8b 04 85 00 00 00 00 mov 0x0(,%eax,4),%eax 4e1: ff e0 jmp *%eax
mov 0xc(%ebp),%eax ; %eax <- (%ebp + 0xcの内容) mov (%eax),%al ; %al <- (%eaxの内容) (%eaxはformatか?%alはcharそのもの?) movsbl %al,%eax ; %eax <- %alの内容を32bit拡張 sub $0x58,%eax ; %eax <- %eax - 0x58('X') cmp $0x20,%eax ; if (%eax > 0x20) ja 5fa ; 0x5fa(default節)に飛ぶ。 mov 0x0(,%eax,4),%eax ; %eax <- (4 * %eaxの内容) jmp *%eax ; %eaxの内容にジャンプ
GAS(GNU Assembler)は全く読んだことがない。以下のサイトを参照した:
switch文のcaseを少なくすると、各case文に直接jmpするコードが生成される。。。
switch (*format) { 4c6: 8b 45 0c mov 0xc(%ebp),%eax 4c9: 8a 00 mov (%eax),%al 4cb: 0f be c0 movsbl %al,%eax 4ce: 83 f8 63 cmp $0x63,%eax 4d1: 0f 84 a6 00 00 00 je 57d <my_sprintf+0x13e> ; -> 'c'節へジャンプ 4d7: 83 f8 63 cmp $0x63,%eax 4da: 7f 0a jg 4e6 <my_sprintf+0xa7> 4dc: 83 f8 58 cmp $0x58,%eax 4df: 74 5b je 53c <my_sprintf+0xfd> ; -> 'X'節へジャンプ 4e1: e9 ed 00 00 00 jmp 5d3 <my_sprintf+0x194> ; -> default節へジャンプ 4e6: 83 f8 73 cmp $0x73,%eax 4e9: 0f 84 b9 00 00 00 je 5a8 <my_sprintf+0x169> ; -> 's'節へジャンプ 4ef: 83 f8 78 cmp $0x78,%eax 4f2: 0f 85 db 00 00 00 jne 5d3 <my_sprintf+0x194> ; すり抜けると'x'節に行く。
以下、関連しそうな記事:
- gccのswitch文最適化
- Bug 11823 - Optimizing large jump tables for switch statements
- Switch case assembly level code
- From Switch Statement Down to Machine Code
- Bug 57583 - large switches with jump tables are horribly broken on m68k
- ジャンプテーブルについて
そもそもi586向けのコンパイラを使っているのが不健全だ。i386向けのクロスコンパイラを用意しよう。
i386向けのコンパイラを入手したが、動作変わらず。。。逆アセンブル結果もほぼ同じ。
2017.9.24
海沿いのベローチェに作業しに行く。
観察から、switch文のcase節が5つ以上になるとジャンプテーブルが生成され、ジャンプテーブルによって分岐に飛ぶとブラックアウトになっているようだ。
ちょっと視点を変えて、ブラックアウトした後どうなってるか観察する。
- switch文突入までは正常動作だが、switch文内に入る前にブラックアウト
- case 節内に入らない
- default 節にも飛ばない
- ブラックアウトした後でも割込みは機能する
- IDTの設定は生きているらしい
簡単なコードでも再現した。もうこれ以上の簡略化は望めない。
/* カーネルエントリ */ void kernel_main(void) { char str_buf[200]; struct BootInfo *binfo = get_bootinfo(); int sw; /* GDT/IDTを再構築 */ init_gdt(); sw = 2; switch (sw) { case 1: strcpy(str_buf, "Test OS... 1"); break; case 2: strcpy(str_buf, "Test OS... 2 "); break; case 3: strcpy(str_buf, "Test OS... 3 "); break; case 4: strcpy(str_buf, "Test OS... 4 "); break; case 5: strcpy(str_buf, "Test OS... 5 "); break; default: strcpy(str_buf, "Test OS... default "); break; } putfonts8(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, str_buf); /* HLTして停止 */ for (;;) { io_hlt(); } }
うーん、アセンブリコードもどうも悪くなさそう...詰まるところ
movl -4(%ebp), %eax ; switch判定値を%eaxに代入 sall $2, %eax ; アドレス値(4バイト)のため2**2=4倍 movl .L8(%eax), %eax ; %eax <- .L8 + %eax jmp *%eax ; %eaxのアドレスにジャンプ(*は間接参照ではない!jmpのオペランドに必ず付ける) .section .rodata .align 4 .align 4 .L8: ; ジャンプテーブル .long .L2 ; .long .L3 .long .L4 .long .L5 .long .L6 .long .L7
のjmp *%eax
で落ちている。(そもそも落ちていると何が起こるのか分からない)
真に何処に飛んでいるのか調べるため、first-os-kernel.imgを逆アセンブルした。
$ /usr/local/i386-linux-4.1.1/bin/i386-linux-ld -m elf_i386 -nostdlib -e kernel_main -T kernel.ld -Map kernel.map -o first-os-kernel.img kernel_main.o asmfunc.o english_fonts.o bootinfo.o graphics.o segment_descriptor.o interrupt.o mouse.o keyboard.o utility.o kernel.ld $ /usr/local/i386-linux-4.1.1/bin/i386-linux-objdump -S first-os-kernel.img -d > kernel_dump.txt
簡単な再現例コードは次の用に逆アセンブルされた:
first-os-kernel.img: file format elf32-i386 Disassembly of section .text: 00280000 <kernel_main>: 280000: 55 push %ebp 280001: 89 e5 mov %esp,%ebp 280003: 81 ec d8 00 00 00 sub $0xd8,%esp 280009: e8 9e 01 00 00 call 2801ac <get_bootinfo> 28000e: 89 45 f8 mov %eax,0xfffffff8(%ebp) 280011: e8 9e 03 00 00 call 2803b4 <init_gdt> 280016: e8 14 04 00 00 call 28042f <init_idt> 28001b: e8 37 06 00 00 call 280657 <init_pic> 280020: e8 ef 00 00 00 call 280114 <io_sti> 280025: e8 33 02 00 00 call 28025d <init_palette> 28002a: c7 45 fc 02 00 00 00 movl $0x2,0xfffffffc(%ebp) 280031: 83 7d fc 05 cmpl $0x5,0xfffffffc(%ebp) 280035: 0f 87 8b 00 00 00 ja 2800c6 <kernel_main+0xc6> 28003b: 8b 45 fc mov 0xfffffffc(%ebp),%eax 28003e: c1 e0 02 shl $0x2,%eax 280041: 8b 80 1c 0e 28 00 mov 0x280e1c(%eax),%eax 280047: ff e0 jmp *%eax 280049: 83 ec 08 sub $0x8,%esp 28004c: 68 c0 0d 28 00 push $0x280dc0 280051: 8d 85 30 ff ff ff lea 0xffffff30(%ebp),%eax 280057: 50 push %eax
実際に飛び先を決める時のコード
280041: 8b 80 1c 0e 28 00 mov 0x280e1c(%eax),%eax
0x280e1c
が相当怪しい。そういえばジャンプテーブルのラベルは全てrodataに配置していた気がする。リンカによってrodataに置かれ、jmp
もそこに飛んでしまった可能性が...?
→正解。リンカのマップファイルを見ると、上のラベルは完全にrodata内にある。
.rodata 0x00280dc0 0x116c 0x00280dc0 _rodata_start = . *(.rodata) .rodata 0x00280dc0 0x74 kernel_main.o *fill* 0x00280e34 0xc 00 .rodata 0x00280e40 0x1000 english_fonts.o 0x00280e40 hankaku .rodata 0x00281e40 0x30 graphics.o .rodata 0x00281e70 0x1a mouse.o .rodata 0x00281e8a 0x1d keyboard.o *fill* 0x00281ea7 0x1 00 .rodata 0x00281ea8 0x84 utility.o 0x00281f2c _rodata_end = . 0x00281f2c _rodata_start = . *(.rodata) 0x00281f2c _rodata_end = .
ジャンプテーブルの生成によってRAMが消費される報告がある:
上の記事で、対処法はif
文に置き換えることだったが、それでは安心してswitch文が使えないし辛すぎる。リンカスクリプトで頑張って再配置できないか。
セキュリティに絡む問題でもあるらしい。
問題を整理すると:
- 5つ以上のcase節が存在するswitch文をgccでコンパイルすると、
-O0
でも最適化によりジャンプテーブルが.rodata
に生成される。 - リンク時に
.text
に埋め込まれた.rodata
が分かたれる(Linux付属のリンカスクリプトは.rodata
を.text
セクションに混ぜるため、gccは埋め込んでいるっぽい) - 結果、ジャンプテーブルの飛び先が
.rodata
内になる - 実際にジャンプすると帰ってこれない
今回はジャンプテーブルに絡む問題だったため、gccの-fno-jump-tables
を使用することで解決出来るみたい...
ジャンプテーブルに関する問題はOKだけど、他に.text
内に埋め込みで.rodata
が入ってくるとつらい。これを抑制するオプションはないのか。
諸々細かいバグを潰し、my_sprintf
完成。ようやく、人権を手に入れた。
2017.9.26
仕事の締切が絶望的。だが作業はする。
どうやらC言語の割込みハンドラではなく、アセンブリでIRETD
を打った時に何処かに飛んでしまったようだ。(C言語関数からの復帰は確認できた)
2017.10.1
Oさんの留数定理からのz変換の基礎づけには感嘆したので、複素解析を勉強しようと思う。(というか、ディジタル信号処理全体を纏めたい...)
話は戻ってまだキーボード割込みで再起動(例外発生?)している。
どうしても直らないため、サイトを参照してセグメントを分けた所正常動作。なぜだ…
/* セレクタ1 * ベースアドレス 0x00000000 * リミット 0xFFFFFFFF (32bit空間全体) * 属性 0x40:Dビット1(32bitセグメント) 0x92:システム専用の読み書き可能なセグメント.実行は不可. */ set_segment_descriptor(&gdt[1], 0xFFFFFFFF, 0x00000000, AR_DATA32_RW); /* セレクタ2 * ベースアドレス 0x00000000 * リミット 0x0007FFFF * 属性 0x40:Dビット1(32bitセグメント) 0x9a:システム専用の実行/読み出し可能なセグメント.書き込みは不可. */ set_segment_descriptor(&gdt[2], 0x0007FFFF, 0x00280000, AR_CODE32_ER); /* セレクタ3 * 32bit空間全体をを指すシステム専用実行/読み出し可能なセグメント */ set_segment_descriptor(&gdt[3], 0xFFFFFFFF, 0x00000000, AR_CODE32_ER);
セレクタ3番に32bit全体を指すセグメントを新しく定義して
/* IDTの設定 */ /* * 割込みベクタ0x21 - PS/2 キーボード * セグメント: セレクタ3(32bit空間全体を指せる必要がある.) */ set_gate_descriptor(&idt[0x21], (int) asm_inthandler21, 3 * 8, AR_INTGATE32); /* 割り込みベクタ0x2c - PS/2 マウス */ set_gate_descriptor(&idt[0x2c], (int) asm_inthandler2c, 3 * 8, AR_INTGATE32);
それに対して割込みハンドラを登録。
疑問点が2つ。
- 結局再起動してしまう原因がわからない
- この修正で正しく動いている(ように見える)ので、セグメント絡みの問題であることは間違いない。セグメントの領域外アクセスになっているのだろうか。
- 最初から32bit全体を指すセグメントだけ用意しておけば良いのでは?
- カーネルローダからカーネルエントリに飛ぶ時は、飛び先はセグメント指定ではなく直接
JMP 2*8:0x00280000
とする
2.番の修正を取り込み、上手く動いているようならば良しとするか... →OK.
よし、今日はFIFO作成まで行こうか。直近では、FIFO作成→マウス割込み作成→メモリマネージャ作成になりそう。
一応ゴリゴリ書いて、32bit向けのFIFOを作成。でもすげえsplint先生に怒られる...
2017.10.8
ここ最近帰りが遅過ぎで何も出来ていない。絵を書きたい…
FIFOモジュールを作成してそれをキーボードに組み込んだらブラックアウトするようになった... しばらく苦闘していると、次の2点でブラックアウトが起こるようだった。
1.か2.それぞれ1つだけだと割込みを受け付けなくなる(キーボードを押しても反応がなくなる)だけだが、両方含めると完全にブラックアウトする。遠回りになるかもだけど、原因を理解するためにもそれぞれのアセンブリコードを見てみる。
グローバル変数とアセンブリ
/* PS/2キーボード構造体 */ struct PS2KeyBoard { FIFO32Hn fifo; /* キーボードデータのFIFO */ uint8_t flag; /* 内部状態フラグ */ };
この構造体PS2KeyBoard
の記憶指定子を変えて定義した時、どの様なアセンブリ出力を得るのか見てみる:
1. 普通のグローバル
struct PS2KeyBoard ps2keyboard = {0, 0};
.globl ps2keyboard .bss .align 4 .type ps2keyboard, @object .size ps2keyboard, 8 ps2keyboard: .zero 8 .section .rodata
ps2keyboard
というシンボルにobject
というタイプが付けられ、かつそのサイズが8に設定されている。(.size
はデバッグ情報に過ぎないらしい)
初期化が入っているのでps2keyboard
のセクションにはゼロパディングが入っている。
struct PS2KeyBoard ps2keyboard;
.comm ps2keyboard,8,4
.comm
ディレクティブでps2keyboard
シンボルが定義されている。.comm
ディレクティブはリンカ(ld
)がシンボルを纏めてデータセクション(.data
)に初期化せず配置してくれる。この時、シンボル名が他のオブジェクトファイルと重複していると、一番サイズの大きいもののサイズに合わせて領域を確保する:
If ld sees multiple common symbols with the same name, and they do not all have the same size, it will allocate space using the largest size.
.comm
ディレクティブの引数は以下:
- 第一引数:シンボル名
- 第二引数:サイズ
- 第三引数:(ELFのみ任意で)アラインメント
つまり、ps2keyboard
シンボルが
.type
, .size
, .comm
等のgasのディレクティブについては次を見た:
2. グローバルconst
const struct PS2KeyBoard ps2keyboard = {0, 0};
.globl ps2keyboard .section .rodata .align 4 .type ps2keyboard, @object .size ps2keyboard, 8 ps2keyboard: .zero 8
.rodata
セクションに置かれる以外は新しいことはない。
const struct PS2KeyBoard ps2keyboard;
.comm ps2keyboard,8,4
なんと.rodata
も無しでグローバルと同様に確保される。コンパイラレベルでしかエラーが検知できなそうだ。
3. static(ファイルグローバル)
static struct PS2KeyBoard ps2keyboard = {0, 0};
.local ps2keyboard .comm ps2keyboard,8,4
static struct PS2KeyBoard ps2keyboard;
.local ps2keyboard .comm ps2keyboard,8,4
なんと全く変わらない。しかしアセンブラに書かれる位置が違う。初期化ありの場合はファイルの先頭でシンボルが定義されていたが、無しの場合はファイルの末尾だった。これによって何かの影響があるのか?
つうかオラクルのx86 Assembly Language Reference Manualがすごい:
…リンカのmapファイルkernel.map
を見ても、ちゃんと出力されている感触がある。記憶域指定子は悪くなさそう。
...分からん。アセンブリコードが少し変わっている程度で壊れる意味が分からない。
ps2keyboard
変数を1回アクセス: 正常動作ps2keyboard
変数を2回以上アクセスする: 割込み表示がなくなるps2keyboard
変数を11回アクセス: ブラックアウト
更に見ていくと、構造体じゃなくてint
型の変数をアクセスしていても同様の現象が起こることが確認できた...でも既にGDT/IDTをstatic
に置いているので、ここでいきなり正常動作しなくなるのはおかしい...
リンカスクリプトの設定を疑いつつ...気力が付きた。原因となるのはもはや次の条件だけ。ソース関係なし!:
static
変数を定義して代入を2回以上行う。
2017.10.9
気晴らしに荻窪駅から杉並アニメセンターに行く。アイカツ旧筐体をプレイ。
OSの方では、もうちょっと原因を追うと、fifo.o
をリンクしてから上記の代入制限が出るようになっていた。(fifo.o
をリンクしなければいくら代入してもOK)
fifo.o
をリンクすることがいかんのか...?
なんだかテキストセグメントのサイズが0x1000を越えてからだめになっている様に見える。
fifo.o
のリンクを切った状態でも再現確認。kernel_main.c
にグローバル変数foo
を定義し、そのfoo
に代入する文を増やして挙動の変化を観察。
- グローバル変数(
foo
)が0x2820f0
に置かれているまではOK。(.text
セクションサイズ:0x1071
) - グローバル変数(
foo
)が0x282110
に置かれると割込みが効かなくなる。(.text
セクションサイズ:0x1081
) - グローバル変数(
foo
)が0x282130
に置かれるとブラックアウト(kernel.map
で確認、.text
セクションサイズ:0x10a1
)
謎だ。テキストセクションのサイズが0x1080
を超えると駄目っぽいので、探ってみると次のような記事が...
OS Dev.orgなるものがあったぞ…
結局、.data
は関係なさそうで完全にコードサイズに依存しているみたい。コードサイズが0x1080
を超えると真っ暗になる。
スタックの位置が怪しいかも。そういえば今まで設定したことがなかった。→色々動かしたけど挙動に変化なし...
もしかしたらリアルモードになっててアドレスの限界に来た可能性も?(OSdevのフォーラム)
2017.10.13
ようやく製品リリース。これで楽になるのかな…
OSの方はどん詰まり。Bochsを導入して打開を計る(なんでもデバッグ情報が取れるらしい)...とおもったらMacではコンパイルするしかなくてしかも難しそう。 VirtualBoxを試すが、フロッピーimgからのブートが一筋縄ではいかない… qemuでデバッグする手段もあるが...一応試してダメそうなら、寝る。
下の記事がすばらしい。つうか今簡単にやれるのは最早これしかない。
2017.10.15
今日もブラックアウト現象を追う。gdb
を駆使しながらなので、ちょっと状況が見えてくる。
- 割込みは正常動作している。(キーボード割込みハンドラにブレークかけたら止まってくれた)
- パレットの色設定は大丈夫みたい(パレットからの読み出しを行ったが設定した値と一致)
なので、どうも描画が出来ていないように見える。VRAM
操作関連を見てみる。...が分からず。詰み?
gccのコンパイラオプションを疑ってみる...が、違うのは正味コードサイズだけ。コードサイズが増えたことによる妙な挙動も見られない。
かくなる上はリンカだろうか。
OS開発に関する包括的な纏めがあった。
2017.10.16
ブレークかけながら観察。
初回
kernel_main
でputfonts8
を呼び出す時はputfont8
が呼ばれていたが、キーボード割り込みハンドラ内でのputfonts8_withback
ではputfont8
が一回も呼ばれていないのを確認した。putfonts8
では文字列終端になるまでputfont8
を繰り返すようになっていたが、初回で文字列終端に至って処理が終了していた。なぜ?
シンボル情報が無くてデバッグが辛いので、デバッグ情報を残したカーネルをビルドする。
シンボル情報をつかむにはカーネル全体をELFにしないと駄目みたい...(今、ブートローダが生バイナリになってる...)
デバッグの途中で、asmfunc
のシンボルにタイプが付いていなかったのでfunction
タイプを付与した。
更に挙動を追っていたところ、表示直後に無限ループで止めれば無事に表示+割込みが出来ていることを確認。じゃあ、何が原因なんだ?
動かないバイナリと動くバイナリを比較しても、本当に差がないぞ?(関数呼び出しアドレスオフセットの違いくらい)
2017.10.19
graphics.o
をリンカに渡す順番を変えると、シンボルの配置が変わるのはとうぜんとして、動作が変わるぞ?
2017.10.22
台風の影響でピアノ中止。OS不具合調査やってく。
昨日まででgcc
を真性のクロスコンパイラに置き換えたが、動作は変わらず、コードサイズを増やすとブラックアウトする。。
コンパイラで疑わしいのは最早オプションくらいしかない。オプション、特にメモリモデルを疑ってみる。
...が、効果なし!
小さなコツ:ファイル毎にアラインメントを揃える場合はSUBALIGN
を使う
.text : SUBALIGN(32) { _text_start = .; *(.text*) _text_end = .; }
アセンブラのlea
の意味がわからなかったので、以下ページを見た:
アドレスを計算する命令のようです。
ブラックアウト時はputfont8
すら呼ばれていない状態で、表示すべき文字列の先頭が0x00
になってしまっている。my_sprintf
に渡されている時点で既に先頭が0x00
になっているので、フォーマット文字列のrodata
がぶっ壊されている(もしくは、ローダによって読み込まれていない)可能性がある。
→ kernel_main.c
の文字列は破壊されていなかったが、ps2keyboard.c
の文字列領域がコードサイズが大きくなると0x00
で埋まる。(ps2mouse.c
も消えてた...)
文字列領域が0x00
になっててputfont8
自体が呼ばれずブラックアウトしている。どうやら、これが原因のようだ。
割込みが処理自体はうまく出来ているが、現象を見ているとロードが途中で止まっているっぽいので、後々のためにも早めに解決しておきたい。
というわけでメモリダンプをgdb
でやる。前もやったけどやり方が消えていたので記載。
簡単にはgdb
実行中にx /<サイズ><フォーマット> 開始アドレス
で見れる。フォーマットはx
で16進数。
gdb
のメモリアクセスブレークのコマンドは次の通り:
watch アドレス
: 書きブレークrwatch アドレス
: 読みブレークawatch アドレス
: 読み/書きブレーク
例:watch *0x280000
どうやら、0x2820ef
までしか読み込まれていないみたいだ。(table_rgb
を0xff
で埋めて確認。)
この時、カーネルローダではソース(ESI
)は0xa400
になっていた。
ローダあたりに注目しつつデバッグするべし。
2017.10.24
最早ブートローダにバグが有るとしか...カーネルローダに入った瞬間で0xa400
までしかロードされてない
2017.10.26
ブートローダ(ipl)でのディスクロード時、データブレークで止めることが出来ない... 気合でループを止めながら見る。 したら、どうやら一番内側のループ(セクタについてのループ)が17回回った後、読み込みが出来ていないというのが見えた。 17回: 512byte * 17 + 0x8200 = 0xa400。 (一番内側のループは18セクタぶん回すが、初回ループ時のみブートセクタを除くので1回分ループが少ない)
トラックをまたいだ読み込みができていない。imgの作り方が悪いのか、それとも、読み出しであるINT 13
が悪いのか…
FAT12の記述が怪しかったので、はりぼてOSとの比較を行った。file
コマンドでharibote.img
を調べたら、次の出力:
haribote.img: DOS/MBR boot sector, code offset 0x4e+2, OEM-ID "HARIBOTE", root entries 224, sectors 2880 (volumes <=32 MB) , sectors/FAT 9, sectors/track 18, sectors 2880 (volumes > 32 MB) , serial number 0xffffffff, label: "HARIBOTEOS ", FAT (12 bit), followed by FAT
一方で自分が作ったfirst-os.img
は次の出力:
first-os.img: DOS/MBR boot sector, code offset 0x4e+2, OEM-ID "KIRIAOS ", root entries 224, sectors 2880 (volumes <=32 MB) , sectors/FAT 9, sectors/track 18, sectors 2880 (volumes > 32 MB) , serial number 0xffffffff, label: "KIRIAOS ", FAT (12 bit)
末尾のfollowed by FATがない。でも文字列(KIRIAOS)以外は一致しているから、後違いはimg
のサイズくらいか…と思って、dd
コマンドで無理やりFAT通りの1440KBイメージを作ったら、メモリにimg
の内容がロードされ動くようになった。(依然としてfollowed by FATは表示されないが...一時的にはりぼてOSとヘッダを揃えたけど効果なし。また、img
を大きくしなくてもトラック当たりのセクタ数を19に増やしても動いていた)
を参考に、次のコマンドでイメージを作成。
dd if=/dev/zero of=test.img bs=1024 count=1440 dd if=first-os.img of=test.img conv=notrunc
ついでに、ブートローダの警告を消すためにnask
からの書き換えを行った:
かなり長い戦いだったが、これでしばらく自由にプログラミングができそうだ…。
今のカーネルのブートプロセスを載せとく。(読み込みシリンダ数CLYS
は10でフロッピーは1440KB、カーネルの中身は512KBまでロードするものとする。)
2017.11.3
まだ少し忙しい…。ピアノは少しずつ再開しているけど絵が…。
最近は圧縮(と複素数)にお熱。前者はOSで使う、後者はz変換の基礎付けとして。圧縮ではLZSS、ハフマン符号化あたりまでを実装していこうと思う。最初はRubyで作って、C言語に移植する。そこでマストになるのがビットストリームモジュール。
PS/2マウス対応
PS/2マウスを有効化するには、割り込みハンドラだけでなく、キーボード制御回路とマウス自身の2つを有効にする必要がある。
キーボード制御回路(Key board controller)の初期化
実はマウスを制御する回路はキーボード制御回路の中に組み込まれているらしい。そのため、キーボード制御回路を初期化を行う。
/* KBC(キーボードコントローラ)に出力する命令 */ #define KEYBOARD_STATUS_SEND_NOTREADY 0x02 #define KEYBOARD_COMMAND_WRITE_MODE 0x60 #define KEYBOARD_CONTROLLER_MODE 0x47 #define KEYBOARD_COMMAND_SENDTO_MOUSE 0xd4 #define KEYBOARD_DATA_MOUSE_ENABLE 0xf4 /* キーボードデータが送信可能になるのを待つ */ static void KBC_WaitSendready(void) { /* CPUに比べキーボードコントローラの動作は遅いため、 * 送信可能になるのをビジーループで待機 */ for (;;) { /* ポート0x64の下2bitが0になるのを待機 */ if ((io_in8(PORT_KEYBOARD_STATUS) & KEYBOARD_STATUS_SEND_NOTREADY) == 0) { break; } } } /* キーボードコントローラの初期化 */ static void KBC_Initialize(void) { /* モード設定を行う */ KBC_WaitSendready(); io_out8(PORT_KEYBOARD_COMMAND, KEYBOARD_COMMAND_WRITE_MODE); /* マウス利用を通知 */ KBC_WaitSendready(); io_out8(PORT_KEYBOARD_DATA, KEYBOARD_CONTROLLER_MODE); }
マウスの有効化
マウス自身の有効化も行う必要がある。
#define KEYBOARD_COMMAND_SENDTO_MOUSE 0xd4 #define KEYBOARD_DATA_MOUSE_ENABLE 0xf4 /* マウス有効化 */ static void KBC_MouseEnable(void) { /* キーボードコントローラに次に書き込んだデータをマウスに送信する事を指示 */ KBC_WaitSendready(); io_out8(PORT_KEYBOARD_COMMAND, KEYBOARD_COMMAND_SENDTO_MOUSE); /* マウス有効化命令を送信 */ KBC_WaitSendready(); io_out8(PORT_KEYBOARD_DATA, KEYBOARD_DATA_MOUSE_ENABLE); }
マウスの割り込みハンドラ
割り込みハンドラに関しては問題ないと思う。
/* PS/2マウスからの割込みハンドラ */ void ps2mouse_inthandler(int *esp) { unsigned char data; /* マウスの割込み完了を通知 */ pic_ps2mouse_eoi(); /* マウスデータ取得(キーボードから送信されてくる!) */ data = io_in8(PORT_KEYBOARD_DATA); /* データバッファにデータをPut */ FIFO32_PutData(ps2mouse.fifo, (uint32_t)data); }
割り込み完了通知(EOI)はPIC2とPIC1の両方に送る必要がある:
/* マウスの割込み完了通知 */ void pic_ps2mouse_eoi(void) { pic2_eoi(4); pic1_eoi(2); }
(説明はほとんど自作OS入門のパクリ…レガシィI/Oで補足したい...)