alpine linuxでlsofでListen Portが確認できない

5分ほど困ったのでメモ。

問題

dockerのalpine linuxイメージ内でPATHが通っているlsofはbusyboxへのシンボリックリンクとなっており、 本来のlsofコマンドと挙動が異なっている。そのため、 lsof -i してもPortが表示されない。

対処

  • 代わりに本家verの lsof を以下でインストール&実行する。
  • 本家verでは、listen portの確認ができる
# lsofのインストール( 既存の/usr/bin配下のものはそのままで新規に/usr/sbin配下にインストールされる模様
$ apk update
$ apk add lsof

# busybox版が実行される
$ /usr/bin/lsof --help

# 本家が実行される
$ /usr/sbin/lsof --help

参考

stackoverflow.com

自社の社外向けイベントにk8sネタで登壇しました

自社の技術イベント にてKubernetesソースコードリーディング入門な発表をしました。準備が大変でしたがなんとかまとまってよかったです。 informer framework は全体的にドキュメント少なめな印象です。

speakerdeck.com

etcdの参考文献一覧

kube-apiserver

etcd

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

harib04a: キーコードの取得

DAY6で作った、キーボード入力時の割り込み処理を改善していく。

int.c - 修正前

void inthandler21(int *esp)
/* PS/2キーボードからの割り込み */
{
    struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
    boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15);
    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 21 (IRQ-1) : PS/2 keyboard");
    for (;;) {
        io_hlt();
    }
}

int.c - 修正後

#define PORT_KEYDAT     0x0060

void inthandler21(int *esp)
{
    struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
    unsigned char data, s[4];
    io_out8(PIC0_OCW2, 0x61);   /* IRQ-01受付完了をPICに通知 */
    data = io_in8(PORT_KEYDAT);

    sprintf(s, "%02X", data);
    boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);

    return;
}

コメントの通り。IRQ1だったので、0x61にしているが、 IRQ3の場合だったら、インクリメントされて0x63になるとのこと。 これをPICに実行してあげると、割り込み信号のブロッキングが解除されて、 PICによる割り込み信号のモニタが再開される?

io_out8(PIC0_OCW2, 0x61);  /* IRQ-01受付完了をPICに通知 */

その後、以下で装置番号0x0060から8bitを読み出し、 符号なしchar型のdataに代入。

data = io_in8(PORT_KEYDAT);

その後、sprintfでdataの内容をsにコピー。 putfont8で画面に描画している。

sprintf(s, "%02X", data);
    boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);

これで、入力した1文字に対応の文字コードが都度画面に表示されるようになった。

harib04b: キーボードからの割り込み処理と文字描画処理の分割

画面描画は処理コストが高く、今のコードだとそれに引きずられて割り込み処理も 処理が遅くなってしまうので分割を行う。

まずは、int.cに手を入れ、描画処理を外す。 新たにflagを立てる機構を作り、未出力の入力データがdataに 格納されているか否かを判定できるようにする。

int.c

#define PORT_KEYDAT     0x0060

struct KEYBUF keybuf;

void inthandler21(int *esp)
{
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61);   /* IRQ-01受付完了をPICに通知 */
    data = io_in8(PORT_KEYDAT);
    if (keybuf.flag == 0) {
        keybuf.data = data;
        keybuf.flag = 1;
    }
    return;
}

次にbootpack.cの処理を変えていく。(今までは無限ループで、io_hlt();するのみだった)

bootpack.c

for (;;) {
        io_cli();
        if (keybuf.flag == 0) {
            io_stihlt();
        } else {
            i = keybuf.data;
            keybuf.flag = 0;
            io_sti();
            sprintf(s, "%02X", i);
            boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
            putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
        }
    }

まず、 io_cli(); で割り込み処理を禁止する。

flagが0の場合は、未処理の入力データがない状態なので、 io_stihlt();で STI(割り込み禁止の解除?) と HLT(CPUお休み) を実行して終わり。

flagが1の場合は、flagを0にした後で、これまで int.cに書いていた描画処理を行う。

harib04c: キーボードからの割り込み処理のキューイング(FIFOバッファ)

04bの実装だと、入力された1byteを処理中に実行された割り込みは 全て読み捨てられてしまうため、そうではなくキューイングを行うようにする。

int.c

struct KEYBUF keybuf;

void inthandler21(int *esp)
{
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61);   /* IRQ-01受付完了をPICに通知 */
    data = io_in8(PORT_KEYDAT);
    if (keybuf.next < 32) {
        keybuf.data[keybuf.next] = data;
        keybuf.next++;
    }
    return;
}

使いされた部分のコードが以下のように動き、値を32個まで配列に格納していく。

1回目の処理

  • keybuf.data[0] = (初回で入力された値)
  • keybuf.next ++ / 0 → 1に代わる /

2回目の処理

  • keybuf.data[1] = (2回目入力された値)
  • keybuf.next ++ / 1 → 2に代わる /

bootpack.c

for (;;) {
        io_cli();
        if (keybuf.next == 0) {
            io_stihlt();
        } else {
            i = keybuf.data[0];
            keybuf.next--;
            for (j = 0; j < keybuf.next; j++) {
                keybuf.data[j] = keybuf.data[j + 1];
            }
            io_sti();
            sprintf(s, "%02X", i);
            boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
            putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
        }
    }
}

ここは本文の説明で問題なく理解できた。

harib04d: キューイング機構(FIFOバッファ)の改良

04cの実装だと、最大32個の配列の要素をfor文でずらす必要が出てくる。 この処理をなくしたいので、実装を変える。

まずは、構造体の定義から。 next_r , next_w ,len が追加された。

bootpack.h

/* int.c */
struct KEYBUF {
    unsigned char data[32];
    int next_r, next_w, len;
};

次にキューへの入力処理。 入力された値は、常に keybuf.data[keybuf.next_w] に入る。

keybuf.lenは「現在何個のキューを持っているのか」 keybuf.next_wは「現在添え字を何個使ったのか」をカウントしており、32に達すると0にリセットされる。

int.c

void inthandler21(int *esp)
{
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61);   /* IRQ-01受付完了をPICに通知 */
    data = io_in8(PORT_KEYDAT);
    if (keybuf.len < 32) {
        keybuf.data[keybuf.next_w] = data;
        keybuf.len++;
        keybuf.next_w++;
        if (keybuf.next_w == 32) {
            keybuf.next_w = 0;
        }
    }
    return;
}

キューからの取り出し処理もそれに合わせて書き直されている。

bootpack.c

for (;;) {
        io_cli();
        if (keybuf.len == 0) {
            io_stihlt();
        } else {
            i = keybuf.data[keybuf.next_r];
            keybuf.len--;
            keybuf.next_r++;
            if (keybuf.next_r == 32) {
                keybuf.next_r = 0;
            }
            io_sti();
            sprintf(s, "%02X", i);
            boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
            putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
        }
    }
}

harib04e:キューイング機構(FIFOバッファ)の共通モジュール化

04b-04dで開発のキューイング機構を他の処理(マウスの入力とか)にも使えるように コードを修正していく。

まずは構造体の定義から。FIFOバッファの最大長を変数で自由に設定できるように変更。

bootpack.h

/* fifo.c */
struct FIFO8 {
    unsigned char *buf;
    int p, q, size, free, flags;
};

構造体の初期化関数。

fifo.c

void fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf)
/* FIFOバッファの初期化 */
{
    fifo->size = size;
    fifo->buf = buf;
    fifo->free = size; /* 空き */
    fifo->flags = 0;
    fifo->p = 0; /* 書き込み位置 */
    fifo->q = 0; /* 読み込み位置 */
    return;
}

キューへのPUSH処理。 fifo->buf[fifo→p] 値を格納したあとで、fifo->p++;し、fifo->free–;でfree領域の残数をデクリメントする。 fifo→pがキューの最大中に達したら、カウンターをリセットする機能と、 溢れたときにflagを立てる処理付き。

fifo.c

int fifo8_put(struct FIFO8 *fifo, unsigned char data)
/* FIFOへデータを送り込んで蓄える */
{
    if (fifo->free == 0) {
        /* 空きがなくてあふれた */
        fifo->flags |= FLAGS_OVERRUN;
        return -1;
    }
    fifo->buf[fifo->p] = data;
    fifo->p++;
    if (fifo->p == fifo->size) {
        fifo->p = 0;
    }
    fifo->free--;
    return 0;
}

キューからのPOP処理。 やっていることは、PUSHとほぼ同じ。

fifo.c

int fifo8_get(struct FIFO8 *fifo)
/* FIFOからデータを一つとってくる */
{
    int data;
    if (fifo->free == fifo->size) {
        /* バッファが空っぽのときは、とりあえず-1が返される */
        return -1;
    }
    data = fifo->buf[fifo->q];
    fifo->q++;
    if (fifo->q == fifo->size) {
        fifo->q = 0;
    }
    fifo->free++;
    return data;
}

最後にステータスチェック用のコード。 FIFOバッファの最大中 - 残っているfree領域を引き算して、 キュー長を戻り値で返しているだけ。

fifo.c

int fifo8_status(struct FIFO8 *fifo)
/* どのくらいデータが溜まっているかを報告する */
{
    return fifo->size - fifo->free;
}

以上を踏まえて、init.cを書き直していく。

push側の処理は、fifo8_put(keyfifo, data); だけになった。 FIFO8型の構造体 keyinfoを作成後、fifo8_put関数でput処理している。

init.c

struct FIFO8 keyfifo;

void inthandler21(int *esp)
{
    unsigned char data;
    io_out8(PIC0_OCW2, 0x61);   /* IRQ-01受付完了をPICに通知 */
    data = io_in8(PORT_KEYDAT);
    fifo8_put(keyfifo, data);
    return;
}

POP側の処理も、 i = fifo8_get(keyfifo); とシンプルになった。

bootpack.c

for (;;) {
        io_cli();
        if (fifo8_status(keyfifo) == 0) {
            io_stihlt();
        } else {
            i = fifo8_get(keyfifo);
            io_sti();
            sprintf(s, "%02X", i);
            boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
            putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
        }
    }

harib04f:マウスが動くようにする

マウスは初期状態では、割り込み信号がブロックされている。 ので、その解除が必要であり、そのために bootpack.c を修正し、 キーボードの初期化処理と一緒にマウスの初期化処理を実装する。

キーボードが制御信号で制御可能な状態になると、 io_in8(PORT_KEYSTA)の2ビット目が0になる。のでそれまでひたすら無限ループを回して待つ。

bootpack.c

void wait_KBC_sendready(void)
{
    /* キーボードコントローラがデータ送信可能になるのを待つ */
    for (;;) {
        if ((io_in8(PORT_KEYSTA)  KEYSTA_SEND_NOTREADY) == 0) {
            break;
        }
    }
    return;
}

キーボードの初期化関数。 前述の wait_KBC_sendready(void) で制御Readyになったかを見つつ、 io_out8で

bootpack.c

#define PORT_KEYDAT             0x0060
#define PORT_KEYSTA             0x0064
#define PORT_KEYCMD             0x0064
#define KEYSTA_SEND_NOTREADY    0x02
#define KEYCMD_WRITE_MODE       0x60
#define KBC_MODE                0x47


void init_keyboard(void)
{
    /* キーボードコントローラの初期化 */
    wait_KBC_sendready();
    io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);  /* キーボードをWRITE_MODEに */
    wait_KBC_sendready();
    io_out8(PORT_KEYDAT, KBC_MODE);
    return;
}

最後に以下でマウスを有効化する。

bootpack.c

#define KEYCMD_SENDTO_MOUSE     0xd4
#define MOUSECMD_ENABLE         0xf4

void enable_mouse(void)
{
    /* マウス有効 */
    wait_KBC_sendready();
    io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE);
    wait_KBC_sendready();
    io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
    return; /* うまくいくとACK(0xfa)が送信されてくる */
}

harib04g:マウスからのデータ受信

割り込みは受け取れるようになったので、今度はマウスを動かす度に、 都度割り込みが発生するようにコードを変えていく。

キーボードと同様に割り込み処理を受け付けたら、 PICに受付完了を通知後に、 fifo8_put でputするようにする。

init.c

void inthandler2c(int *esp)
/* PS/2マウスからの割り込み */
{
    unsigned char data;
    io_out8(PIC1_OCW2, 0x64);   /* IRQ-12受付完了をPIC1に通知 */
    io_out8(PIC0_OCW2, 0x62);   /* IRQ-02受付完了をPIC0に通知 */
    data = io_in8(PORT_KEYDAT);
    fifo8_put(mousefifo, data);
    return;
}

次に、bootpack.cの中でのキューのPOP処理のelseにマウス処理のパターンを足す。(書き方はキーボードと同様)

fifo8_init(mousefifo, 128, mousebuf);

    enable_mouse();

    for (;;) {
        io_cli();
        if (fifo8_status(keyfifo) + fifo8_status(mousefifo) == 0) {
            io_stihlt();
        } else {
            if (fifo8_status(keyfifo) != 0) {
                i = fifo8_get(keyfifo);
                io_sti();
                sprintf(s, "%02X", i);
                boxfill8(binfo->vram, binfo->scrnx, COL8_008484,  0, 16, 15, 31);
                putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);
            } else if (fifo8_status(mousefifo) != 0) {
                i = fifo8_get(mousefifo);
                io_sti();
                sprintf(s, "%02X", i);
                boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 47, 31);
                putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
            }
        }
    }

マウスが動くたびに、座標が画面に描画されるようになった!

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

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

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

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

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

harib01a

画面に出力を行うために、 naskfunc.nasに以下の関数が追加された メモリ番地を指定して書き込むのが目的とのこと

_write_mem8:    ; void write_mem8(int addr, int data);
        MOV     ECX,[ESP+4]     ; [ESP+4]にaddrが入っているのでそれをECXに読み込む
        MOV     AL,[ESP+8]      ; [ESP+8]にdataが入っているのでそれをALに読み込む
        MOV     [ECX],AL
        RET

C言語(bootpack.c)側では、以下で呼び出している

for (i = 0xa0000; i <= 0xaffff; i++) {
        write_mem8(i, 15); /* MOV BYTE [i],15 */
    }*/

どうやら、C言語からnaskの関数を呼び出す時の引数の仕様として、以下のようになるっぽい。

  • 関数の読み込まれるメモリアドレス:[ESP] ※ESPレジスタに関数の格納された ‘アドレスが’ 格納される。アセンブラで ‘’ をつけることで、そのアドレスに格納された値を操作可能
  • 第1引数の格納場所:[ESP + 4]
  • 第1引数の格納場所:[ESP + 8]
  • 第1引数の格納場所:[ESP + 8]

上記をふまえコメントを書き換えてみる。

_write_mem8:   ; void write_mem8(int addr, int data);
        MOV     ECX,[ESP+4]     ; [ESP+4]にC言語側で関数がコールされた時の第一引数=address(書き込み先アドレス)が入っているので、その値をECXレジスタに格納する
        MOV     AL,[ESP+8]      ; [ESP+8]にC言語側で関数がコールされた時の第2引数=data(書き込むデータ)が入っているので、その値をALレジスタに格納する
        MOV     [ECX],AL        ; [ESP+4]で指定されたアドレスにdataの値を書き込む
        RET

結果的に、0xa0000 から 0xaffff までのアドレスにかたっぱしから’15’を書き込む処理となっている。

harib01b

bootpack.cのコードが変わった

void io_hlt(void);
void write_mem8(int addr, int data);


void HariMain(void)
{
    int i; /* 変数宣言。iという変数は、32ビットの整数型 */

    for (i = 0xa0000; i <= 0xaffff; i++) {

-       write_mem8(i, 15);
+       write_mem8(i, i  0x0f);

    }

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

i % 0x0f だが、はアンド演算子と呼ばれるらしい。

OR演算(どちらかが1だったら1にする)

0100 OR 0010 ⇒ 0110
1010 OR 0010 ⇒ 1010

AND演算(両方が1の時のみ1にする)

0100 AMD 1101 ⇒ 0100
1010 AND 1101 ⇒ 1000

最後にXOR演算(片方が0でもう片方が1の時のみ1にする)

0100 XOR 1101 ⇒ 1001
1010 XOR 0010 ⇒ 1000

harib01f

  • char型の変数には、以下の3つのモードがある。
    • signed型 データは-128~127を表す
    • unsigned型 データは0~255を表す
    • 型指定なし コンパイラに判断を委ねる?
  • EFLAGSとは、16bitレジスタのfflagsを拡張したもので、 キャリーフラグや割り込みフラグが詰まっている。
    void init_palette(void)
    {
        static unsigned char table_rgb[16 * 3] = {
            0x00, 0x00, 0x00,   /*  0:黒 */
            0xff, 0x00, 0x00,   /*  1:明るい赤 */
            0x00, 0xff, 0x00,   /*  2:明るい緑 */
            0xff, 0xff, 0x00,   /*  3:明るい黄色 */
            0x00, 0x00, 0xff,   /*  4:明るい青 */
            0xff, 0x00, 0xff,   /*  5:明るい紫 */
            0x00, 0xff, 0xff,   /*  6:明るい水色 */
            0xff, 0xff, 0xff,   /*  7:白 */
            0xc6, 0xc6, 0xc6,   /*  8:明るい灰色 */
            0x84, 0x00, 0x00,   /*  9:暗い赤 */
            0x00, 0x84, 0x00,   /* 10:暗い緑 */
            0x84, 0x84, 0x00,   /* 11:暗い黄色 */
            0x00, 0x00, 0x84,   /* 12:暗い青 */
            0x84, 0x00, 0x84,   /* 13:暗い紫 */
            0x00, 0x84, 0x84,   /* 14:暗い水色 */
            0x84, 0x84, 0x84    /* 15:暗い灰色 */
        };
        set_palette(0, 15, table_rgb);
        return;
    
        /* static char 命令は、データにしか使えないけどDB命令相当 */
    }

char型の配列 table_rgb[48] を作成し、48個のデータを格納後、 set_palette関数を起動している。

void set_palette(int start, int end, unsigned char *rgb)
{
    int i, eflags;
    eflags = io_load_eflags();  /* 割り込み許可フラグの値を記録する */
    io_cli();                   /* 許可フラグを0にして割り込み禁止にする */
    io_out8(0x03c8, start);
    for (i = start; i <= end; i++) {
        io_out8(0x03c9, rgb[0] / 4);
        io_out8(0x03c9, rgb[1] / 4);
        io_out8(0x03c9, rgb[2] / 4);
        rgb += 3;
    }
    io_store_eflags(eflags);    /* 割り込み許可フラグを元に戻す */
    return;
}

set_palette関数では、 割り込み不可状態を作った後で、 0x03c8 及び 0x03c9の装置に対し、out命令を実行する naskの関数を実行している。

io_out8の実装は以下

_io_out8:  ; void io_out8(int port, int data);
        MOV     EDX,[ESP+4]     ; port
        MOV     AL,[ESP+8]      ; data
        OUT     DX,AL
        RET

引数で受け取った装置名とrgb値を4で割った値を それぞれ、EDXレジスタとALレジスタに格納後、 「OUT DX,AL」している。

DXはEDXの下位16bitになるが、なぜここでEDXではなくDXを指定しているのか 理由がわからない。。なんにせよやっていることは変わらずで、 画面に出力する色を変える命令をビデオDAコンバータ?に送っている。

※この手の装置に直接命令を送る関数は言語には存在しないらしい

harib01g

最後に、図形を画面に描画してみる。

現在の画面はX軸が320、Y軸が200の合計64,000の点で構成されている。

上記の座標に何を描画するのかは、 VRAMに格納する値で管理されており(メモリに色番号を入れるとその座標の色が変わる)、 0xa000 + x + y *320 というルールでアドレスが採番される。

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

harib00a

; ディスクを読む

        MOV     AX,0x0820
        MOV     ES,AX
        MOV     CH,0            ; シリンダ0
        MOV     DH,0            ; ヘッド0
        MOV     CL,2            ; セクタ2

        MOV     AH,0x02         ; AH=0x02 : ディスク読み込み
        MOV     AL,1            ; 1セクタ
        MOV     BX,0
        MOV     DL,0x00         ; ドライブ番号0x00 = Aドライブ
        INT     0x13            ; ディスクBIOS呼び出し
        JC      error
  • JC命令=「jump if carry」
  • キャリーフラグ?が1(フラグが立っている)時は、jumpを実行する
  • INT命令の戻り値を見て、エラーハンドルしている?
  • 恐らく、INT命令は失敗っすると戻り値として1を返す */harib

AHレジスタの値で、ディスクに対する処理を指定する

AH = 0x02; 書き込み
AH = 0x03; 良い込み
AH = 0x04; ベリファイ?
AH = 0x0c; シーク
  • キャリーフラグとは、1bitしか記憶できないレジスタ。このようなレジスタはflagと呼ばれる。
  • CH, CL, DH, DLにはそれぞれ、シリンダ番号、セクタ番号、ヘッド番号、ドライブ番号を代入する

      CH シリンダ番号
      CL セクタ番号
      DH ヘッド番号
      DL ドライブ番号
    
  • シリンダ番号は外側⇒内側に番号がインクリメントされていく

  • シリンダ=cylinder=円筒
  • シリンダは片面に80本(#0 - #79)存在する セクタは各シリンダ内に18個(#1 - #18)存在する

*linuxのセクタサイズは以下で確認可能

[root@centos7 ~]# fdisk -l /dev/sda1 | grep 'Sector size'
Sector size (logical/physical): 512 bytes / 4096 bytes

バッファアドレス

  • 引用されているホームページに以下のような記述がある
  • ES:BX = バッファアドレス; ベリファイ時、シーク時にはこれを参照しない。
  • バッファアドレスとは、メモリのどこに読み込むかを表す番地
  • BXレジスタのみだと、0~0xffff(65535=63.99kb)までしか表せないが、ESレジスタを合わせて使うと、(EX×16)+BXまで表現できるようになる
  • ES=0x0820、BX=0の場合 0x0820(2080) × 16 = 0x8200(33280) / ESレジスタの値を16倍したものが起点になる /

セグメントとは

  • メモリをセグメントと呼ばれる一定サイズの大きさに区切ることをSegmentationと呼ぶ(用例としては、Segmentation Faultとか)
  • セグメントレジスタとはセグメントレジスタはセグメントの開始位置を決めるもので、具体的には以下が存在する。(いずれも16bit=2byteまで格納可能?)

    • CS
    • DS
    • SS
    • ES
    • FS
    • GS

セグメントレジスタにおけるセグメントの開始位置

  • セグメントの開始位置は、以下の計算式で計算される。
  • セグメントの開始位置 = セグメントレジスタに入れた値×16(16進数では0x10)

セグメントレジスタにおけるセグメントの大きさ

  • セグメントレジスタによって確保されるメモリ空間の大きさは 0xFFFF(65535)+1となる。 / 0始まりのため。時刻のHHの最大値は23だが、時間の合計値は24となるのと同じ理屈 /

  • DSレジスタの値が0xFFFF(DEC 65535)の時、 セグメントの開始位置 = セグメントレジスタに入れた値×16(16進数では0x10) なので、 セグメントの開始位置 = 0xFFFF × 16 = 0xFFFF0(DEC 1,048,560)となる。 / 16進数で末尾に0をつけると16倍したことになる /
  • また、 セグメントの大きさは 0xFFFF(DEX 65535)+1 = 0x10000(DEC 65536) となる。 そのためアドレスの末番号(アクセスできる最大アドレス)は、 0xFFFF0(DEX 1,048,560) + 0x10000(DEC 65536) = 0x10FFF(DEC 1,114,096)となる。
  • Ref: 0から作るOS開発 ブートローダその6 セグメント

余談

bitとbyteと2進/16進数

- bitとは'2進数で表した'数字の並び ( 0 or 1)
 - byteとは8bitを一つグループとして表した概念

 - 1byte = 8bitの最大値は、1111 1111(10進255)なので、
   0000 0000 から 1111 1111までの256個の数字を表すことができる。
   つまり、1byteで256通りのパターンを表現できる。

   2進数の'1111'は0xFとあらわすことができるため、
   1byte = 0xFFとなる。つまり、0xFFで256通りのデータを表現可能

英語での〇進数の呼び方

2進   binary(bin)
 10進  decimal(dex)
 16進  hexadecimal(hex)

同様に、本の例、つまり ES = 0x0820 BX = 0 の時は、 開始位置:0x08200(DEC 33,280) アクセス可能な最大アドレス = 0x08200(DEC 33,280) + 0x10000(DEC 65536) = 0x18200(DEC 98816) …と思ったが、本を読むと 0x08200(DEC 33,280)から0x83ff(DEC 33,791)までとなっている。 DEC 33,791 - DEC 33,280 = DEC 511(0x1FF) メモリ1区画で1byte(8bit)までの値が格納可能なので、 合計512byte分の領域があることになる。 納得いかないが、読み進めよう。。。

後は、MAKEFILEがだいぶ読みやすくなっていた。GJ

TOOLPATH = ../z_tools/
MAKE     = $(TOOLPATH)make.exe -r
NASK     = $(TOOLPATH)nask.exe
EDIMG    = $(TOOLPATH)edimg.exe
IMGTOL   = $(TOOLPATH)imgtol.com
COPY     = copy
DEL      = del

# デフォルト動作

default :
    $(MAKE) img

# ファイル生成規則

ipl.bin : ipl.nas Makefile
    $(NASK) ipl.nas ipl.bin ipl.lst

haribote.img : ipl.bin Makefile
    $(EDIMG)   imgin:../z_tools/fdimg0at.tek \
        wbinimg src:ipl.bin len:512 from:0 to:0   imgout:haribote.img

# コマンド

asm :
    $(MAKE) ipl.bin

img :
    $(MAKE) haribote.img

run :
    $(MAKE) img
    $(COPY) haribote.img ..\z_tools\qemu\fdimage0.bin
    $(MAKE) -C ../z_tools/qemu

install :
    $(MAKE) img
    $(IMGTOL) w a: haribote.img

clean :
    -$(DEL) ipl.bin
    -$(DEL) ipl.lst

src_only :
    $(MAKE) clean
    -$(DEL) haribote.img

harib00b

新たな命令として、JNC命令が紹介される。要するに直前の命令値を見て、戻り値(というかキャリーフラグ)が0(正常終了)であれば go to するとのこと

harib00c

突然、C0-H0-S18セクタと言われてわからなかったけど、シリンダ/ヘッダ/セクタの略称なんだろう。 AHレジスタの値が0x02の状態で、「INT 0x13」を実行するとESアドレスで指定のメモリ領域にシリンダの情報が書き込まれる。これを18回ループさせている。

harib00d

ディスクの裏面は、head0⇒head1に切り替えることで読み込み可能になる様子。 JB = Jump if below、 冒頭の「CYLS EQU 10 ; どこまで読み込むか」はC言語の#defineのようなものらしい。(CYLS=10、となる)

harib00e

ようやく、ブートセクタ読み取り用プログラム(ipl.nas)とは別にkernelっぽいものが現れた!(haribote.nas) ディスクイメージファイルに、OS本体を書き込むと、ファイル名とファイルの中身が所定の場所に保存されるらしい。

harib00g

なので、ファイルの中身のある場所(この場合0x8000 + 0x4200 = 0xc200)にJMP命令を発行すると、OSが実行できるという理屈。 ここでは、・OSファイル(haribote.nas)に「ORG 0xc200」を記載し、 読み込まれる目折り番地を固定・ブートローダ?(ipl10.nasの処理の最後にJMP命令を記載)することでOSを起動させるコードとなっている。

; 読み終わったのでharibote.sysを実行だ!
MOV [0x0ff0],CH ; IPLがどこまで読んだのかをメモ JMP 0xc200

OSの内容は以下

; haribote-os
; TAB=4

        ORG     0xc200          ; このプログラムがどこに読み込まれるのか

        MOV     AL,0x13         ; VGAグラフィックス、320x200x8bitカラー
        MOV     AH,0x00         ; AH = 0x00; でビデオモードの設定モードに
        INT     0x10            ; ビデオドライバへの割り込み処理発行
fin:
        HLT
        JMP     fin

harib00h

CPUには16bit modeと32bit modeが存在するBIOSは16bit modeでしか動作しないため、32bit modeに変更するとBIOSはもう使えなくなる

harib00i

ようやくC言語の世界へ!(長かった。。)今までharibote.nasだったファイルがasmhead.nasにかわった、 起動の順番としては、 ipl10.nas->asmhead.nas->bootpack.c となる。 bootpack.cの起動は、asmhead.nasによって行われているようだが詳細な解説は後程とのこと。後は、 16bit mode=特権モード 32bit mode=プロテクトモードでよかったっけ?

harib00j

最後に、C言語のファイル bootpack.c から、nasmの関数を記述したファイル naskfunc.nas を起動する方法を書いておわり。 nasm側で関数名の前に、アンダーバーをつけるのはLinuxカーネルでよく見る気がするのだけど、どういう技術的な制約なんだろう?

30日でできるOS自作入門_DAY1 & DAY2

DAY1

  • バイナリファイル自体は2進数だが、それでは冗長になるので、 以下のように2進数⇒16進数に変換することで情報量を減らしている

f:id:smatsuzaki:20191130120403p:plain

  • DB命令 : data byte命令、ファイルの内容を1byteだけ直接書く。文字列も書けるよ!
  • PRESS命令 : reserve byte 、引数 x byteを予約する、具体的には0x00を書きこんで埋める
  • DW命令 : data word、DB命令の仲間 word=16bit/2byte
  • DD命令 : data double word、DB命令の仲間 word=32bit/4bit

DAY2

その後、フロッピーからBIOS的なものを起動するアセンブラなコードを写経する。 以下、処置の流れのメモ。

ORG命令で、0x7c00(16進数7C00 = 10進31744)を読み込まれるアドレス番地として指定

ORG     0x7c00          ; プログラムが読み込まれるメモリアドレスを指定

その次に出てくるのは、JMP命令。entryラベルにgo toする。

JMP     entry

entryに入り、MOV AX,0で AX = 0; として処理される。0を値として入れることは初期化と呼ばれている。

entry:
        MOV     AX,0            ; レジスタ初期化
        MOV     SS,AX
        MOV     SP,0x7c00
        MOV     DS,AX
        MOV     ES,AX

        MOV     SI,msg

上記のentryラベルの区画の最後の「MOV SI,msg」においてSIに代入される「msg:」はラベルである。 アセンブラでラベルが代入されるときはラベルが格納されているメモリ番地が格納される。

putloop:
        MOV     AL,[SI]
        ADD     SI,1            ; SIに1を足す
        CMP     AL,0
        JE      fin
        MOV     AH,0x0e         ; 一文字表示ファンクション
        MOV     BX,15           ; カラーコード
        INT     0x10            ; ビデオBIOS呼び出し
        JMP     putloop

次のputloopラベルに移る。

最初の「MOV AL,[SI]」の「 [SI] 」はSIレジスタではなく、メモリ番地を指し示す。 例えばSIレジスタに987が値として格納されていれば、「987番地のメモリに格納された値を1byte取り出しALレジスタに代入(格納する)」となる 特に明示的に書かれていないが、

  • MOV AL,[SI] 1ビット取り出し
  • MOV BYTE AL,[SI] 8ビット取り出し
  • MOV WORD AL,[SI] 16ビット取り出し
  • MOV DWORD AL,[SI] 32ビット取り出し

となるらしい。

メモリの1区画の大きさについては、

メモリは基本、8bit(0か1を8個並べたもの)ごとに1区画として、1区画ごとにアドレス(番地)が振られている。

ざっくりアセンブラ入門

とのこと。

ADD        SI,1            ; SIに1を足す

その次のADD命令は足し算となる。 ADD SI,1 は SI = SI + 1;

CMP     AL,0
        JE      fin

ここは1セットの構文で、CMPが if ( AL == 0)

JEが then go to fin というような動きとなる。

JE は「jump if equal」の略称

INT        0x10            ; ビデオBIOS呼び出し

INT命令はinterrupt(ハードウェア割り込み?)を発行している?

以下を全て実行すると、BIOS経由で画面に文字が出力できるとのこと

一文字表示
AH = 0x0e
AL = キャラクターコード
BH = 0
BL = カラーコード
戻り値:なし

話を戻し、「 CMP AL,0 CMP AL,0 JE fin」からfinラベルに飛び

fin:
        HLT                     ; 何かあるまでCPUを停止させる
        JMP     fin             ; 無限ループ

HLT命令を実行すると、PC(サーバ)はハードウェア割り込みが検知される(キーボードを押すとか)までの 間スリープに入る模様。

最後に冒頭のORG命令で0x00007c00が指定された意味が語られているが以下の記事が理解できればOKだと思う。

https://ja.wikipedia.org/wiki/%E3%83%96%E3%83%BC%E3%83%88%E3%82%BB%E3%82%AF%E3%82%BF

要するにBIOSの仕様として、「OSは0x7c00にブートすること!」という決まりがあるとの理解でだいたい間違ってなさそう。

より詳しくは、ここのサイトを読むといいかも。

MBRの実行手順だけ、覚えておきたいのでメモ

  1. BIOSがPOST(Power On Self Test)を実行
  2. POST後、BIOSFDD or HDDのMBR0x7C00にロードし、そこから処理を続行する。(もちろんCD/DVDからブートすることも出来るが、本記事ではFDD/HDDからのブートを扱う。)
  3. MBRにはOSのカーネルをロードし、実行する為の機械語が格納されており、OSのブートが始まる。

その後はMakefileの話題。

# デフォルト動作

default :
    ../z_tools/make.exe img

# ファイル生成規則
# ipl.nasとMakefileが存在することを確認後、ipl.nasを元にipl.binとipl.lastを生成する
# ipl.binとMakefileが存在することを確認後、

ipl.bin : ipl.nas Makefile
    ../z_tools/nask.exe ipl.nas ipl.bin ipl.lst

helloos.img : ipl.bin Makefile
    ../z_tools/edimg.exe   imgin:../z_tools/fdimg0at.tek \
        wbinimg src:ipl.bin len:512 from:0 to:0   imgout:helloos.img

# コマンド

asm :
    ../z_tools/make.exe -r ipl.bin

img :
    ../z_tools/make.exe -r helloos.img

run :
    ../z_tools/make.exe img
    copy helloos.img ..\z_tools\qemu\fdimage0.bin
    ../z_tools/make.exe -C ../z_tools/qemu

install :
    ../z_tools/make.exe img
    ../z_tools/imgtol.com w a: helloos.img

clean :
    -del ipl.bin
    -del ipl.lst

src_only :
    ../z_tools/make.exe clean
    -del helloos.img

今まで作っていたバッチファイルが全てMakefileに統合された。素晴らしい!

cleanとかrc_onlyの書き方も参考になる。

30日でできるOS自作入門_環境構築編

掲題の本に従ってチュートリアルを進めようとしたログになります。 2018年の正月で終わらなかったので、次はGWにチャレンジか。。

Ref:

各種データがダウンロード可能な公式ページ

先人の方のBlog

環境構築

  • Refのページを参考に進めていく
  • Windows10です。

Cygwinの導入

  • Cygwin → https://cygwin.com/setup-x86_64.exe をダウロードしインストール ⇒後段のクロスコンパイラの構築に失敗したので、32bitでやり直す / 以下を追加 /
    • All→Devel→make:The GNU version of the ‘make’ utility
    • All→Devel→binutils:The GNU assember, linker and binary utilities
    • All→Devel→gcc-core:Core C compiler subpackage
    • All→Devel→nasm:The Netwide Assembler

Cygiwinでクロスコンパイラ環境を構築

Cygport が公開停止?しているためこの手順は実行できないことがわかった。。 諦める。

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