30日でできるOS自作入門_DAY6

harib03a:ソースコード分割

harib03b: Makefileのルールの一般規則化

  • Makefile の一般規則について。これも、なるほどな話だけど読めば大体理解できるので読むだけで。 要するに、Makefileのルールにワイルドカードを使う「一般規則」というもんが作れるという話

harib03c: ヘッダファイル整備

  • ここもヘッダファイルの話をしているだけなので、読むだけで。 ただヘッダファイの利便性が今まで一番よく伝わってきた。実際にコードが短くなると 「ほほー」ってなるなあ。また、なぜこれを使うのかわからなくなった時に戻ってきたい。

harib03c: GDT / GDTR / LGDT

DAY5の最後の説明の続き。

_load_gdtr:        ; void load_gdtr(int limit, int addr);
        MOV     AX,[ESP+4]      ; limit値をAXレジスタに格納
        MOV     [ESP+6],AX      ; AXレジスタに格納したlimit値を
        LGDT    [ESP+6]
        RET

関数名になっているGDTRは、GDT(global descriptor table)のアドレスを特殊な格納するレジスタという説明だった。 48bitの容量を持っているらしい。

GDTRレジスタへの値の代入はMOV命令を使うことができない。(そういう仕様らしい) 代わりに、 LGDT

 というような形でLGDT命令に特定のメモリ番地を渡すと、 LGDTがそこから先頭8byte=48bitを読み込んで、GDTRレジスタに格納してくれる

GDTRレジスタの下位2byteは、Limitを表すというのは下図の ‘16-Bit Table Limit’ の所だろう。 次の「GDTの有効バイト数 -1」 は説明が省略されすぎていて、ちょっとわからない。。

http://slideplayer.com/slide/9243430/

http://images.slideplayer.com/27/9243430/slides/slide_40.jpg

void load_gdtr(int limit, int addr)が呼び出された時、 「ESPレジスタに格納のアドレス + 4」にlimit値が、 「ESPレジスタに格納のアドレス + 8」にアドレスが、 それぞれ格納されている。

このうち、limit値のほうを、 ESP + 4 ⇒ AX ⇒ ESP + 6 ⇒ LGDT ⇒ GDTR という順番で渡しているようなコードに見える。

全体的に説明についていけていないが、この理解で進める。

harib03c: dsctbl.c

bootpack.h

/* dsctbl.c */
struct SEGMENT_DESCRIPTOR {
    short limit_low, base_low;   /* base addressをshort型のlow、char型のmid、highに3分割している。理由はintel80286時代との互換性とのこと。深追いはやめておく */
    char base_mid, access_right;
    char limit_high, base_high;  /* limitはセグメントの大きさを表す。この説明は5章終わりの段階でほしかった。。
};

dsctbl.c

void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
    if (limit > 0xfffff) {
        ar |= 0x8000; /* G_bit = 1 */
        limit /= 0x1000;
    }
    sd->limit_low    = limit  0xffff;
    sd->base_low     = base  0xffff;
    sd->base_mid     = (base >> 16)  0xff;
    sd->access_right = ar  0xff;
    sd->limit_high   = ((limit >> 16)  0x0f) | ((ar >> 8)  0xf0);
    sd->base_high    = (base >> 24)  0xff;
    return;
}

ここの説明はdiagramなしでの理解がきついので、他のサイトからもってきた。

http://slideplayer.com/3374589/12/images/18/G+bit+or+the+Granularity+bit.jpg

ざっくりとした絵としては、こっちのほうがシンプルでわかりやすいか。

http://slideplayer.com/9386031/28/images/63/Memory+Management+Flat+Model.jpg

要点だけ、

  • セグメント番地(上図の BaseAddress )は32bit。Intel80286との互換性のため、low / mid / highの3つに分割した。
  • Limit(上図の Limit )はセグメントサイズを表す。セグメント属性内のGビットを立てる(値を1にする)と、単位がbyte単位⇒ページ単位に変わる
  • セグメント属性(上図のAccess、他にarと言ったりaccess rightと言ったり)は、Intel80286では存在せずi386の機能拡張で追加された。(なので拡張アクセス権と言ったりする) Limitのところで述べたGビットとは別に、メモリセグメントのモードを決める?Dセグメントというものがある。 値=0の場合は16bit、値=1の場合は32bitで動作。D=1で使うのが一般的。 セグメント属性の下位8bitを使って、メモリ空間のカテゴリー化を行っている。 カテゴリーの類型は以下の通り。
    1. システム専用 RW- ※X不可
    2. システム専用 R-X ※W不可
    3. アプリケーション用 RW- ※X不可
    4. アプリケーション用 R-X ※W不可
  • CPUはprotection ringsという動作モードに応じて付与する権限を変えるアーキテクチャを採用している。 上記項番2のメモリ領域に展開されたプログラムを実行するときは、システムモード(ring 0)で起動する。 上記項番4のメモリ領域に展開されたプログラムが実行されたときは、アプリケーションモード( ring 3 )で起動する。 アプリケーションモードで動作時に項番1/2にアクセスしようとすると、怒られる。(segmentation faultとか?)

harib03d: PCI初期化

ここまでGDT/IDTの説明を延々としてきたのは、割り込み処理をしたかったからとのこと。

ここからは、PICの割り込み処理の説明ををしていく。 PIC(割り込みコントローラ)は、PCI(拡張バス)とは別ものらしい。名前がややこしい。

PICの概要:

  • 8個の割り込み信号(IRQ = interrupt request)を1つの割り込み信号にまとめる
  • CPUに直接つながるマスタPICとその下につながるスレーブPICの2種がある

[f:id:smatsuzaki:20191130123707p:plain]

上記を踏まえて以下のコードを概観していく。

/* 割り込み関係 */

#include "bootpack.h"

void init_pic(void)
/* PICの初期化 */
{
    io_out8(PIC0_IMR,  0xff  ); /* 全ての割り込みを受け付けない */
    io_out8(PIC1_IMR,  0xff  ); /* 全ての割り込みを受け付けない */

    io_out8(PIC0_ICW1, 0x11  ); /* エッジトリガモード */
    io_out8(PIC0_ICW2, 0x20  ); /* IRQ0-7は、INT20-27で受ける */
    io_out8(PIC0_ICW3, 1 << 2); /* PIC1はIRQ2にて接続 */
    io_out8(PIC0_ICW4, 0x01  ); /* ノンバッファモード */

    io_out8(PIC1_ICW1, 0x11  ); /* エッジトリガモード */
    io_out8(PIC1_ICW2, 0x28  ); /* IRQ8-15は、INT28-2fで受ける */
    io_out8(PIC1_ICW3, 2     ); /* PIC1はIRQ2にて接続 */
    io_out8(PIC1_ICW4, 0x01  ); /* ノンバッファモード */

    io_out8(PIC0_IMR,  0xfb  ); /* 11111011 PIC1以外は全て禁止 */
    io_out8(PIC1_IMR,  0xff  ); /* 11111111 全ての割り込みを受け付けない */

    return;
}
  • PICはCPUから見ると周辺装置のため、OUT命令(io_out8)を使う ※画面に描画するためにVRAMを操作するのに使っていた命令
  • コード中の PIC0 がマスターPIC、 PIC1 がスレーブPCI
  • PIC0 , PIC1の各レジスタのポートマッピングはincludeされている、bootpack.hにて定義されているがわかりにくい。。
    /* int.c */
    void init_pic(void);
    #define PIC0_ICW1       0x0020
    #define PIC0_OCW2       0x0020
    #define PIC0_IMR        0x0021
    #define PIC0_ICW2       0x0021
    #define PIC0_ICW3       0x0021
    #define PIC0_ICW4       0x0021
    #define PIC1_ICW1       0x00a0
    #define PIC1_OCW2       0x00a0
    #define PIC1_IMR        0x00a1
    #define PIC1_ICW2       0x00a1
    #define PIC1_ICW3       0x00a1
    #define PIC1_ICW4       0x00a1

コードを細かく読んでいく

io_out8(PIC0_IMR,  0xff  ); /* 全ての割り込みを受け付けない */
io_out8(PIC1_IMR,  0xff  ); /* 全ての割り込みを受け付けない */

第一引数で使われている、IMRレジスタは「interrupt mask register」という8bit のレジスタであり、 bitの各桁がIRQ×8個に対応、ビットが立っている(1が入っている)と割り込み信号が無視されるようになる。

0xff は「1111 1111」なので、これで8bit全てが1になる = 割り込みが無効化される。

        io_out8(PIC0_ICW1, 0x11  ); /* エッジトリガモード */
        io_out8(PIC0_ICW2, 0x20  ); /* IRQ0-7は、INT20-27で受ける */
        io_out8(PIC0_ICW3, 1 << 2); /* PIC1はIRQ2にて接続 */
        io_out8(PIC0_ICW4, 0x01  ); /* ノンバッファモード */
    
        io_out8(PIC1_ICW1, 0x11  ); /* エッジトリガモード */
        io_out8(PIC1_ICW2, 0x28  ); /* IRQ8-15は、INT28-2fで受ける */
        io_out8(PIC1_ICW3, 2     ); /* PIC1はIRQ2にて接続 */
        io_out8(PIC1_ICW4, 0x01  ); /* ノンバッファモード */

ICWは「initial control word」=「初期化制御データ」 ICWは1~4があって、合計で4バイトのデータになる、らしい。

ICW1,3,4は飛ばして、重点的に説明されているICW2番のみ見ていく。 ICW2は、割り込み番号(アセンブラで INT xxx と書くと、何番のIRQが対応して動くか)をPICから指定する。

具体的には、PIC0_ICW2 ( 0x0021 ) に、io_out命令で0x20を入れると、 それがIRQ0に対応の割り込み番号と認識され、以降IRQ1は0x21、IRQは0x22というように インクリメントされて番号が付与されていく模様。 ここで番号を指定しておくことで、後段の割り込みハンドラ実装時に、 割り込み番号で制御対処のIRQポートを指定できるようになる、らしい。

これでマッピングはざっくり以下のようになる

harib03e: 割り込みハンドラ作成

0dで作ったPIC操作用のコードを元に、割り込みハンドラを実装していく。

マウスはIRQ12, キーボードはIRQ1なので、 03dで作成のマッピングに従い、INT 0x2c, INT 0x20でそれぞれ操作する。

割り込みハンドラは、dsctbl.cに登録する(21-24行目)

void init_gdtidt(void)
{
    struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
    struct GATE_DESCRIPTOR    *idt = (struct GATE_DESCRIPTOR    *) ADR_IDT;
    int i;

    /* GDTの初期化 */
    for (i = 0; i <= LIMIT_GDT / 8; i++) {
        set_segmdesc(gdt + i, 0, 0, 0);
    }
    set_segmdesc(gdt + 1, 0xffffffff,   0x00000000, AR_DATA32_RW);
    set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);
    load_gdtr(LIMIT_GDT, ADR_GDT);

    /* IDTの初期化 */
    for (i = 0; i <= LIMIT_IDT / 8; i++) {
        set_gatedesc(idt + i, 0, 0, 0);
    }
    load_idtr(LIMIT_IDT, ADR_IDT);

    /* IDTの設定 */
    set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32); /* idtのアドレス+オフセット0x21のアドレスに、 (int) asm_inthandler21 を登録 */
    set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32); 
    set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);

    return;
}

set_gatedescの引数を見ていく。

第1引数で設定されたアドレスは、 GATE_DESCRIPTOR構造体へのポインタになる。 つまり、「ここの番地に登録の構造体に0x21の設定を入れますよ」となる。

第2引数の (int) asm_inthandler21 がなぜ、(メモリアドレスの)オフセットに代入させるのかわからず苦悶していたが、 こちらの方は関数ポインタではないかと推測していた。関数名をintに変換すると、関数へのポインタになる? なるほどそうかも。。

第3引数の ‘2 * 8’ は、最終的に ’gd→selector’に代入されている。 セレクタはharib02gで書いた通り、CPUがメモリセグメントを探すときのインデックスとして機能する。 10進数2は、2進数 0010になる。 他方、10進数16は、1110

http://sop.upv.es/gii-dso/en/t2-arquitectura/segment_selector.png

セグメントセレクタの下位3bitは、別の用途に使われ4bit目以降がIndex値として使われる。

なので、10進数16 = 1110、でセグメント番号2番を指し示すことになるらしい。

ディスクリプタ・テーブルのエントリ1個分は8バイトであるので、 「8*Index」で、ディスクリプタ・テーブルの先頭アドレスからのオフセット位置を示します。 http://d.hatena.ne.jp/yz2cm/20140502/1399012324

らしいので、セグメント番号1番 = オフセットなし、セグメント番号2番 = (8 * 1)分オフセット ということなのだろう。

第4引数の ‘AR_INTGATE32’ は、set_gatedescの引数 ‘ar’ として取得され、‘gd->access_right = ar 0xff;’ される。 値は、‘AR_INTGATE32’ の値は、bootpack.hで以下のように定義されている。

#define AR_INTGATE32   0x008e

0x008e = 1000 1110 0xff = 1111 1111 でAND演算される。

結果的にアクセス権に「このメモリは割り込み処理用に使いますよ!」という属性を付与することになるらしい。

AND演算というか、ビット演算をする意義については、あとで以下を読もう。。

【C言語入門】ビット演算子、シフト演算子の使い方(使い道も解説)

登録のために呼び出されている関数はやはり、dsctbl.c内で記述されている

void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar)
{
    gd->offset_low   = offset  0xffff;
    gd->selector     = selector;
    gd->dw_count     = (ar >> 8)  0xff;
    gd->access_right = ar  0xff;
    gd->offset_high  = (offset >> 16)  0xffff;
    return;
}

ここまでやると、各IRQへの割り込みに応じて asm_inthandler_xx_ が起動されるようになる。 このアセンブラ関数に対応する記述は、以下に書かれている。

14       GLOBAL  _asm_inthandler21, _asm_inthandler2c
   15       EXTERN  _inthandler21, _inthandler2c
   94 _asm_inthandler21:
  103       CALL    _inthandler21

ここの説明は以下ブログを参照。

CALLの説明

これは関数を呼び出す命令です。今回は、naskfunc.nasの中にかかれていない関数を呼び出さなくてはいけないので、 最初にEXTERN命令を使って、「今からこんな名前のラベルを使うけど、これは他のソースにあるからね~、間違いはないんだよー」とnaskに知らせています ということで EXTERN は CALL用に書かれているみたいです。C言語の関数をアセンブラ内での使用ためのものらしいです。 http://d.hatena.ne.jp/kobapan/20090501/1241132705

最後に、実際に呼び出されているアセンブラの関数の解説。

_asm_inthandler21:
        PUSH    ES                          ; ADD ESP, -4  / MOV [SS:ESP],ES , なのでESPレジスタの指し示すアドレスを-4し、そこにESの値を格納
        PUSH    DS
        PUSHAD

        MOV     EAX,ESP
        PUSH    EAX

        MOV     AX,SS
        MOV     DS,AX
        MOV     ES,AX 
        CALL    _inthandler21               ; int.c内の void inthandler21(int *esp) を呼び出す

        POP     EAX                         ; MOV EAX, [SS:ESP] , ADD ESP,4 EAXレジスタに格納していたアドレスを戻したのちに、ESPレジスタの指し示すアドレスを+4する
        POPAD
        POP     DS
        POP     ES

        IRETD                               ; 割り込み終了時には、return = RET命令ではなく、IRETD命令を実行する必要があるとのこと

スタックの要点はざっくりと以下。

  • スタックはFILO型のバッファ
  • PUSHはバッファ上にデータを1つ乗せる
  • POPはバッファ上からデータを1つ取り出す

スタックの動きはここがとてもわかりやすいのでおすすめ。

また、PUSHDは以下の命令を合体させたもので、POPADはそのPOP版とのこと。

PUSH EAX
PUSH ECX
PUSH EDX
PUSH EBX
PUSH ESP
PUSH EBP
PUSH ESI
PUSH EDI

というわけで処理の流れとしては、

  1. 各レジスタの値をひたすらESPレジスタの持つスタックバッファに避難させていく
  2. DSとESレジスタにSSの値を代入後、C言語の関数の _inethandler21 を実行
  3. その後、レジスタの値をスタックからひたすらPOPして戻していく
  4. 最後にIRETD

という感じ。

項番2で呼び出される、 _inethandler21 は引数にESPレジスタを取っているが、 本章では特に気にしなくてOKとのこと。

C言語の型毎のビット桁数 / キャスト(明示的な型変換) / ビット演算

ビット演算を今まであまり理解していなかったので、メモ。

AND演算は、16bit maskでビット桁数を抜き取るために使うらしい。

C言語は型毎に桁数が違うのをあまり意識してなかったな。。INT=32bit , short = 16bit

キャストするとビット単位で値に操作が行われる。これも意識する必要がある

/* https://sunrise033.com/entry/hatena-blog-how-to-hierarchicalize-categories */