な毎か

なんて事ない毎日がかけがえないんだよなぁ…

x86(i386)向けのOS作成日記 ローカル保存分

2017.7.22

着手。ひとまず方針を探った。 最初はRaspberry Pi向けのOSを自作しようとしたが、CPUがARM64で、簡単に環境を準備できなかったのと、OS自作入門の知識が使えないのが痛いので、x86CPU向けにOSを作製することにした。 この踏ん切りにあたっては、次の資料が役に立った。(これならばやれそうという直感が動いた)

アセンブラNASMを選択。NASKはOS自作筆者が作成したもので可搬性がないため。NASKNASMの文法は大体似ているので問題ないと思っている。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はアドレス0x7c00Assembler/なぜ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:

  1. IPLへの拡張
  2. OS側に処理を渡す
    • 32bit(プロテクト)モードへの移行。
    • C言語ソースを使う。
    • 柔軟なメモリ配置を行うため、リンカスクリプトを使う?
  3. (何となくで良いので)着地点を決める

2017.7.23

OS自作入門ではedimgを使用してイメージファイルを編集していたが、それも独自ツールなので可搬性がない。そこで一般的なツールを探していたら

に、mtools内にmformatmcopyコマンドがあり、これを使えば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で統一したい。んで、NASMelfの実行形式を得るには、

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言語に制御を渡す所まで書いてみたい。大まかには、

  1. ブートローダカーネルローダはNASMから直接バイナリイメージにコンパイルする.
  2. C言語カーネルエントリを書く。
  3. HLTするだけだけど、C言語レベルではHLTを呼び出せない(インラインアセンブラはナシにして…可読性が落ちる)ため、アセンブリで書いたHLTするだけの関数を呼ぶ。
  4. C言語で書いた部分はリンカで配置を決め、バイナリイメージを吐く。カーネルエントリは0x00280000以降に配置するよう決めているので、それに従えば良い。
  5. 全てのバイナリイメージは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、もしくはそれに準じる機能を実装したい。文字表示の時に即刻に問題になるのがフォント。

  1. BIOSの画面設定でテキストモードを使用すれば、VRAMにASCIIコードを書き込むだけで(BIOS内臓のフォントを使って)文字表示ができる。
  2. 作りながら学ぶOSカーネル、大神さんのOS5はこれ
  3. BIOSの画面設定でグラフィックモードを使用した場合は自前のフォント必須。
  4. 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に書き込む:

  1. OCW1に0xFFを書き込み割込みを禁止する。
  2. ICW1に書き込む際にbit4を立てる事で初期化を通知する。
  3. bit0を立てるとICW4まで書き込める。
  4. ICW2, ICW3, ICW4の順番で書き込む。
  5. 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をどこまで作るか少し纏めておきたい。欲しい機能は次の様に纏めた:

  1. マウス割込み/キーボード割込みを完成させる
  2. 現状の延長上で問題なく出来る見込み。
  3. サブタスクとして、割込み等のメッセージを受けるFIFOをどこかに作製する必要がある。
  4. メモリマネージャ
  5. 仮想メモリは使用する予定なし。ブロック単位で切ってメモリを管理できるようにする。
  6. 文字列を扱う標準関数(sprintfさえあれば)がひとまず欲しい。
  7. タイマ
  8. タイムアウトFIFOによって管理する実体。
    1. 以降の機能実現に必須。
  9. マルチタスク
  10. TSSを扱ってタスク構造体に抽象化。
  11. スケジューリングは王道中の王道、優先度ありのラウンドロビンでやるつもり。
  12. コンソール
  13. 今の所GUIはほぼなし、コンソールがメインとなるOSの作成を考えている。文字列入力/出力を頑張ってやる。
  14. ファイルシステム
  15. →作業が重そう。自作OSに例がないため。
  16. CP/Mファイルシステムライクな単純かつフラットなファイルシステムを実装したい。
  17. 構造化ファイルシステム構造はまたあとで。
  18. APIシステムコール
  19. 割込み番号のいずれかの番号をOSのシステムコール用に割り当てる。
  20. APIを呼び出すことでアプリを作りやすくする。サポートするのはメモリ管理、文字列表示、ファイルオープン等(自作OSを参考に、API仕様の明文化もしたい)
  21. ELF対応
  22. ELFの実行形式に対応したローダを作成して、32bitマシン向けにビルドしたバイナリを動かせるようにする。
  23. 優先度高め。(はりぼてOSではhrbフォーマットを持っていたが、自分が作るものでは使えないから。)
  24. →組み込みOS自作入門でやってた… 組み込み自作入門ではOS自体をロードして実行する所で使っていたが、ここではアプリをELFとしたい。
  25. ウィンドウ対応
  26. ゲームを作るための基盤。ウィンドウ作成、矩形/線/円描写APIを追加。

  27. 音声出力対応

  28. 最初はビープ音(ビープ音の和音対応はぜひとも)、次はサウンドドライバ…(出来るのか?)

昨年やった組み込み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_rgbstatic記憶クラス指定子を外すか、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'節に行く。

以下、関連しそうな記事:

そもそも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つ。

  1. 結局再起動してしまう原因がわからない
  2. この修正で正しく動いている(ように見える)ので、セグメント絡みの問題であることは間違いない。セグメントの領域外アクセスになっているのだろうか。
  3. 最初から32bit全体を指すセグメントだけ用意しておけば良いのでは?
  4. カーネルローダからカーネルエントリに飛ぶ時は、飛び先はセグメント指定ではなく直接JMP 2*8:0x00280000とする

2.番の修正を取り込み、上手く動いているようならば良しとするか... →OK.

よし、今日はFIFO作成まで行こうか。直近では、FIFO作成→マウス割込み作成→メモリマネージャ作成になりそう。

一応ゴリゴリ書いて、32bit向けのFIFOを作成。でもすげえsplint先生に怒られる...

2017.10.8

ここ最近帰りが遅過ぎで何も出来ていない。絵を書きたい…

FIFOモジュールを作成してそれをキーボードに組み込んだらブラックアウトするようになった... しばらく苦闘していると、次の2点でブラックアウトが起こるようだった。

  1. ファイルグローバル変数ps2keyboardの追加
  2. FIFOモジュールの関数を呼び出し

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_mainputfonts8を呼び出す時は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_rgb0xffで埋めて確認。) この時、カーネルローダではソース(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までロードするものとする。)

f:id:aikiriao:20171104025843p:plain

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で補足したい...)