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

harib02a

bootpack.cのここの処理が

void HariMain(void)
{
    char *vram;
    int xsize, ysize;

    init_palette();
    vram = (char *) 0xa0000;
    xsize = 320;
    ysize = 200;

以下のように書き換わった。

void HariMain(void)
{
    char *vram;
    int xsize, ysize;
    short *binfo_scrnx, *binfo_scrny;
    int *binfo_vram;

    init_palette();
    binfo_scrnx = (short *) 0x0ff4;
    binfo_scrny = (short *) 0x0ff6;
    binfo_vram = (int *) 0x0ff8;
    xsize = *binfo_scrnx;
    ysize = *binfo_scrny;
    vram = (char *) *binfo_vram;

メインの描画処理はinit_screen関数にまとめられた模様

void init_screen(char *vram, int x, int y)
{
    boxfill8(vram, x, COL8_008484,  0,     0,      x -  1, y - 29);
    boxfill8(vram, x, COL8_C6C6C6,  0,     y - 28, x -  1, y - 28);
    boxfill8(vram, x, COL8_FFFFFF,  0,     y - 27, x -  1, y - 27);
    boxfill8(vram, x, COL8_C6C6C6,  0,     y - 26, x -  1, y -  1);

    boxfill8(vram, x, COL8_FFFFFF,  3,     y - 24, 59,     y - 24);
    boxfill8(vram, x, COL8_FFFFFF,  2,     y - 24,  2,     y -  4);
    boxfill8(vram, x, COL8_848484,  3,     y -  4, 59,     y -  4);
    boxfill8(vram, x, COL8_848484, 59,     y - 23, 59,     y -  5);
    boxfill8(vram, x, COL8_000000,  2,     y -  3, 59,     y -  3);
    boxfill8(vram, x, COL8_000000, 60,     y - 24, 60,     y -  3);

    boxfill8(vram, x, COL8_848484, x - 47, y - 24, x -  4, y - 24);
    boxfill8(vram, x, COL8_848484, x - 47, y - 23, x - 47, y -  4);
    boxfill8(vram, x, COL8_FFFFFF, x - 47, y -  3, x -  4, y -  3);
    boxfill8(vram, x, COL8_FFFFFF, x -  3, y - 24, x -  3, y -  3);
    return;
}

harib02b

構造体を使うように書き直された。

struct BOOTINFO {
    char cyls, leds, vmode, reserve;
    short scrnx, scrny;
    char *vram;
};

まずは構造体の定義。ここで BOOINFO と書かれている部分は所謂、構造体タグとかタグ名と呼ばれるもの。

binfo = (struct BOOTINFO *) 0x0ff0;   /* 0xff0を構造体BOOTINFOへのポインタ型にキャストしている */
    xsize = (*binfo).scrnx;               /* 括弧なしだと、エラーが出るのでこういう書き方にしているとのこと
    ysize = (*binfo).scrny;
    vram = (*binfo).vram;

その後、上記の変数を型変換し、使用している。 ※0x0ff0を型変換すると、なぜメンバ変数に自動的に値が入るのかがわからないけどここでは飛ばす

余談:strcutとtypedefの違いについて

struct _person {     /* _person がタグ名。C言語ではタグ名の先頭に _ をつける慣習があるらしい。 */
  char name[20];     /* 文字配列型のメンバ name */
  int  age;          /* 整数型のメンバ age */
};

と書いた時は

struct _person p;

というように、実際に構図体を使う時にも先頭に struct が必要となる。

他方、

typedef struct {     /* 構造体の型枠を定義して、同時にそれを person_t として定義する。この書き方では構造体タグ名は省略している */
struct _person {     /* _person がタグ名。C言語ではタグ名の先頭に _ をつける慣習があるらしい。 */
  char name[20];     /* 文字配列型のメンバ name */
  int  age;          /* 整数型のメンバ age */
} person_t;          /* こっちはtyepedefで付与される変数の別名 */

上記のように宣言するときは、

person_t p

みたいに、structなしで変数を宣言できる。

harib02c

アロー演算子を使って書き直されてコードがすっきりした。 比較的よく見る書き方の印象。

init_palette();
    init_screen(binfo->vram, binfo->scrnx, binfo->scrny);

harib02d

Aという文字列を表示するようにコードを書き換えるとのこと。

void putfont8(char *vram, int xsize, int x, int y, char c, char *font)
{
    int i;
    char *p, d /* data */;
    for (i = 0; i < 16; i++) {
        p = vram + (y + i) * xsize + x;
        d = font[i];
        if ((d  0x80) != 0) { p[0] = c; }
        if ((d  0x40) != 0) { p[1] = c; }
        if ((d  0x20) != 0) { p[2] = c; }
        if ((d  0x10) != 0) { p[3] = c; }
        if ((d  0x08) != 0) { p[4] = c; }
        if ((d  0x04) != 0) { p[5] = c; }
        if ((d  0x02) != 0) { p[6] = c; }
        if ((d  0x01) != 0) { p[7] = c; }
    }
    return;
}

変数pがchar型へのポイント型で宣言されているのに、

後段のif文で配列として使われているのはなぜなんだ。。わからん。

やっていることは画面に描画するためにVRAM用の各アドレスにカラーコードを入れているっぽい。

harib02e

文字を出力させるために、専用の文字コード定義ファイル(hankaku.txt)を作り、 それを構文解析してC言語に渡すための自作コンパイラを使っている。 変態だ。。

Makefileを追いかけていくと

MAKEFONT = $(TOOLPATH)makefont.exe

冒頭で、MAKEFONTを定義。

hankaku.bin : hankaku.txt Makefile
    $(MAKEFONT) hankaku.txt hankaku.bin

makefont.exeを使って、hankaku.txtからhankaku.binを生成。

その後、以下で定義したコマンドを使い・・・

BIN2OBJ  = $(TOOLPATH)bin2obj.exe

以下のように、 _hankaku を生成

hankaku.obj : hankaku.bin Makefile
    $(BIN2OBJ) hankaku.bin hankaku.obj _hankaku

Makefileで生成された、 _hankaku を読み込むよう、bootpack.cも修正

void HariMain(void)
{
    struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
    extern char hankaku[4096];

    init_palette();
    init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
    putfont8(binfo->vram, binfo->scrnx,  8, 8, COL8_FFFFFF, hankaku + 'A' * 16);
    putfont8(binfo->vram, binfo->scrnx, 16, 8, COL8_FFFFFF, hankaku + 'B' * 16);
    putfont8(binfo->vram, binfo->scrnx, 24, 8, COL8_FFFFFF, hankaku + 'C' * 16);
    putfont8(binfo->vram, binfo->scrnx, 40, 8, COL8_FFFFFF, hankaku + '1' * 16);
    putfont8(binfo->vram, binfo->scrnx, 48, 8, COL8_FFFFFF, hankaku + '2' * 16);
    putfont8(binfo->vram, binfo->scrnx, 56, 8, COL8_FFFFFF, hankaku + '3' * 16);

    for (;;) {
        io_hlt();
    }
}

4行目で、_hankakuを読み込む。 ※ソースプログラムで準備したファイル以外には、extern属性をつけるとのこと。 ※extern = externalで、外部変数を定義する

harib02f

ここまで適用に読み飛ばしていたが、まじめにコードを読まないと理解が厳しそう。。

void putfont8(char *vram, int xsize, int x, int y, char c, char *font)
{
    int i;
    char *p, d /* data */;
    for (i = 0; i < 16; i++) {
        p = vram + (y + i) * xsize + x;
        d = font[i];
        if ((d  0x80) != 0) { p[0] = c; }
        if ((d  0x40) != 0) { p[1] = c; }
        if ((d  0x20) != 0) { p[2] = c; }
        if ((d  0x10) != 0) { p[3] = c; }
        if ((d  0x08) != 0) { p[4] = c; }
        if ((d  0x04) != 0) { p[5] = c; }
        if ((d  0x02) != 0) { p[6] = c; }
        if ((d  0x01) != 0) { p[7] = c; }
    }
    return;
}

1文字が8×16マスで表現されるので、配列で横軸(8マス)

for分の中の p で縦軸(16マス)を表現している。

引数は前から順に、VRAMへのポインタ、xの値、xの値、yの値、色コード、書き込む文字列へのポインタ。

*font には

font[0] = 00000000 / 0x00

font[1] = 00011000 / 0x18

…

font[15] = 00000000 / 0x00

みたいな感じでビットマップデータというのかな?が格納される。

if ((d 0x80) != 0) { p[0] = c; }

d 0x80 はAND演算で、

0x80 1000 0000 0x40 0100 0000 0x20 0010 0000 0x10 0001 0000 0x08 0000 1000 0x04 0000 0100 0x02 0000 0010 0x01 0000 0001

となり、これとf(i)の値をAND演算することで、 各行の任意のビットに 0 が入っているのか、それとも、1が入っているのかを判定している。

んでもって、1が入っていると判定される !=0 の時は、

その桁に点を打つ。

次の関数はこちら。複数の文字列を打つためのラッパー関数?

void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
{
    extern char hankaku[4096];
    for (; *s != 0x00; s++) {
        putfont8(vram, xsize, x, y, c, hankaku + *s * 16);
        x += 8;
    }
    return;
}

引数は先頭から、vramのアドレス、xの座標、xの座標、yの座標、色コード、入力する文字列

実際の関数の呼び出し例は以下

putfonts8_asc(binfo->vram, binfo->scrnx,  8,  8, COL8_FFFFFF, "ABC 123");

上記で、putfonts8_ascが呼びされると、

引数の “ABC 123”を格納したアドレスが unsigned char *s (符号なし文字列へのポインタ型)に格納される。

for (; *s != 0x00; s++) {

*s は間接参照演算子で、s に格納されたアドレスが格納している文字列 ABC 123を取り出す。 この時、文字列は一つずつ取り出される。

具体的には、

*s[0] = A

*s[1] = B

…

*s[7] = 3

*s[8] = 0x00

という感じ。なのでこの書き方で文字列に格納されたアドレスが一つずつ取り出される。

それを、関数の内の以下のコードの第6引数で。。

putfont8(vram, xsize, x, y, c, hankaku + *s * 16);

まずは *s で関節参照演算子を使って、文字列を一個ずつ渡す。

文字コード一式はメモリ展開されていて、

先頭アドレスの hankaku を起点に、

文字コードに対応の各アドレス * 16byteに16行分のビットマップデータが格納されているので、(ASCII準拠)

この指定の仕方で出力したい文字列に応じたビットマップデータが printfont8 に渡され、文字が描画される。

なるほど。。。」

harib02g

  • sptinfは、Cコンパイラ(gccのこと?)付属のGOというライブラリの?一部とのこと
  • sprintf(番地、書式、値、値、値….); となっていて、引数で指定のメモリ番地にprintfと同じ形式で文字列を出力する

以下のコード内のsprintfにて、char型の配列sに文字列’binfo→scrnx)を格納後、

putfonts8_ascで描画している。

char s[40];

    sprintf(s, "scrnx = %d", binfo->scrnx);
    putfonts8_asc(binfo->vram, binfo->scrnx, 16, 64, COL8_FFFFFF, s);

harib02h

疲れてきたので要点のみ。

initi_mouse_cursor8

  • 2次元配列で矢印のASCIIアート的なものを作成
  • 各項目に入っている値毎に、引数として与えられた配列に、1文字ずつひたすら文字コードを割り当て

putlock8

  • buf(mcursor)に入っている値をひたすらvramにコピーしていく

harib02g

トピックが変わり、セグメンテーションとか割り込みとかそういう話しに。 難解に見えるがたぶん重要なところなので、リファレンスも読みつつじっくり読み込んでいこう。

セグメント

  • セグメント方式とは、連続したメモリ領域を区画に分割して管理する方式のこと。 分割された区画をセグメントと呼ぶ。
  • 各セグメントは以下の情報を持つ
    • 開始アドレス(segment base)
    • セグメントの大きさ
  • セグメント内のアドレスは、セグメントベースにオフセットを加えて指定する。 このアドレス形式をリニアアドレス(linear address)と呼ぶ

Linuxのブートプロセスを見る』P6

CPUのリニアアドレス算出の流れ

IA-32のCPUで、プログラムがメモリにアクセスする時は、 必ずセグメントとオフセットのペアを使って、リニアアドレスを指定する。

IA-32のCPUはその仕様上、直接メモリアドレスを指定することはできない。 だから、以下のようにセグメントレジスタとオフセットを”:”(コロン)で区切り表記する。

MOV AL,[DS:100H]

この場合、DSレジスタが示すsegment baseアドレスから数えて100H番目のアドレスが参照され、 そこに格納される値のうち、1byteがALレジスタアセンブラの文法についてはDAY2参照

Linuxのブートプロセスを見る』P6

プロテクトモードとセグメント識別子(segment descriptor)

  • プロテクトモードのCPUは、セグメントディスクリプタと呼ばれる特別なデータを必要とする。
  • セグメントディスクリプタとは、以下の情報を含む8byteのデータ構造体であり、メモリ上に作成される。
    • セグメントのベースアドレス
    • サイズ
    • タイプ
    • アクセス権限設定

セグメント識別子テーブル(segment descriptor table)

f:id:smatsuzaki:20191130122728p:plain

  • セグメント識別子テーブルは3種類存在する。
    • GDT(global secriptor table) 全てのプログラムから共通にアクセスするセグメントを定義する。GDTRという特殊レジスタにアドレスを格納。
    • LDT(local descriptor table) タスク(プロセス)単位に存在
    • IDT(Interruput Descriptor Table) 割り込み処理に使用
  • CPUは以下の手順でメモリにアクセスする
    1. セレクタ値とGDTRから、セグメント識別のアドレスを算出する
    2. セグメント識別子の内容をメモリから読み出す
    3. セグメントのベースアドレスを取り出し、それにオフセット値を加え、リニアアドレスを算出する 『Linuxのブートプロセスを見る』P14

f:id:smatsuzaki:20191130122757p:plain

マウスで割り込み処理をしてみよう

この章の本題。

struct SEGMENT_DESCRIPTOR {
    short limit_low, base_low;
    char base_mid, access_right;
    char limit_high, base_high;
};

struct GATE_DESCRIPTOR {
    short offset_low, selector;
    char dw_count, access_right;
    short offset_high;
};

SEGMENT_DESCRIPTOR構造体は、GDT(global descriptor table)、

GATE_DESCRIPTOR構造体はIDT(interrupt descriptor table)をそれぞれ表すとのこと。

void init_gdtidt(void)
{
    struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000;  /* 右辺はアドレスを構造体へのポインタ型にキャストしている。読み込む場所は作者がえいやで設定とのこと */
    struct GATE_DESCRIPTOR    *idt = (struct GATE_DESCRIPTOR    *) 0x0026f800;  /* 右辺はアドレスを構造体へのポインタ型にキャストしている。読み込む場所は作者がえいやで設定とのこと */
    int i;

    /* GDTの初期化 */
    for (i = 0; i < 8192; i++) {                       
        set_segmdesc(gdt + i, 0, 0, 0);                                         /* C言語ではポインタ型にインクリメントするときは、ポイントされている型のサイズずつ値が増えていく。この構造体は8byteなので8byteずつ増える */
    }

    set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);                      /* セグメント番号1番のレコードをGDTに挿入。
    set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
    load_gdtr(0xffff, 0x00270000);

    /* IDTの初期化 */
    for (i = 0; i < 256; i++) {
        set_gatedesc(idt + i, 0, 0, 0);
    }
    load_idtr(0x7ff, 0x0026f800);

    return;
}

void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar) /* 前からセグメント識別子、リミット、ベース、
{
    if (limit > 0xfffff) {
        ar |= 0x8000; /* G_bit = 1 */                                                  /* ar = ar | 0x8000 の省略形。演算子 | は OR演算。 */
        limit /= 0x1000;                                                               /* limit = 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;
}

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;
}

細かい説明が省かれているのでここまでかなあ。

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