Yabu.log

ITなどの雑記

30日OS自作本21日目

21日目の内容です。セグメントやメモリの復習が多めです。

hariboteOSのメモリマップ

メモリに直接データを書き込むなどの危険行為を行なっているので、念のためどこらへんを潰しているのか分かり易くするために復習です。p171から引用したデータを元に作成しました。

start end size content
0x00000000 0x000fffff 1MB boot
0x00100000 0x00267fff 1440KB floppy content
0x00268000 0x0024f7ff 30KB empty
0x0026f800 0x0026ffff 2KB IDT
0x00270000 0x0027ffff 64KB GDT
0x00280000 0x002fffff 512KB bootpack.hrb
0x00300000 0x003fffff 1MB stack or etc
0x00400000 - - empty

合計4048KB+です。

$ echo $((1000 + 1440 + 30 + 2 + 64 + 512 + 1000 ))
4048

本エントリ中に生のアドレスが出てきた際は参考にしてください。 

セグメントについておさらい。

  • プロテクトモード時はセグメントは固定長ではない
    • リアルモード時はセグメントは固定長である。
  • セグメントの設定はset_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)関数で行なっている*1

今まで登録したセグメントをまとめます。

セグメントはこんな感じで登録してきました。

  • set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, AR_DATA32_RW);
  • set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);
  • set_segmdesc(gdt + TASK_GDT0 + i, 103, (int) &taskctl->tasks0[i].tss, AR_TSS32);
  • set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER);
  • set_segmdesc(gdt + 1004, 64 * 1024 - 1, (int) q, AR_DATA32_RW);

ざっくり表にまとめてみました。

position content base address limit ar
1 OS data 0x00000000 0xffffffff 0100000010010010
2 OS code 0x00280000 0x0007ffff 0100000010011010
3~1002 TSS alloc result 103B 0000000010001001
1003 app code alloc result file size 0100000010011010
1004 app data alloc result 64KB 0100000010010010
  • alloc resultというのはallocation系の関数の結果次第ということです。

範囲的にはbootpackで解放している

memman_free(memman, 0x00001000, 0x0009e000); /* 0x00001000 - 0x0009efff */
memman_free(memman, 0x00400000, memtotal - 0x00400000);
  • 0x00001000 - 0x0009efff
  • 0x00400000 - メモリの最後まで

この範囲のアドレスが使われます。

OS codeセグメントのベースアドレスが本エントリ冒頭に書かれているbootpack.hrbの開始地点0x00280000と一致していることが確認できます。

ちなみにTSSはこんな感じでセグメントの3~1002の範囲を割り当てています。

/*参考*/
#define MAX_TASKS       1000    /* 最大タスク数 */
#define TASK_GDT0       3       /* TSSをGDTの何番から割り当てるのか */
#define AR_TSS32        0x0089

struct TASK *task_init(struct MEMMAN *memman)
{
  /* 中略 */
  for (i = 0; i < MAX_TASKS; i++) {
    taskctl->tasks0[i].flags = 0;
    taskctl->tasks0[i].sel = (TASK_GDT0 + i) * 8;
    set_segmdesc(gdt + TASK_GDT0 + i, 103, (int) &taskctl->tasks0[i].tss, AR_TSS32);
  }

セグメントのアクセス属性を設定する

arの下位8ビットはセグメント属性と呼ばれるものであり 下位6,7bit目のフラグを立てる(0x60をOR演算する)と、アプリ用のセグメントとなり、OS用のセグメントにデータが書けなくなります。

セグメント属性について改めて説明します。

content ar
0x60 01100000
app code 10011010
app data 10010010
label PDDSTTTA

一番下に説明のため追加したlabelの行がセグメント属性の各ビットの役割です。上位から順に

  • P:セグメントが存在する(1=存在,0=存在しない)
  • D:DPL(セグメントの特権レベル)
  • S:セグメントフラグ= セグメントである場合に1
  • T:TYPE(セグメントの種類)
  • A:セグメントがアクセスされた

PとSは説明しなくてもわかると思います。またAはメモリ管理用のフラグです。

ここで説明が必要なのがtypeとdplだと思います。

DPL

DPL(Descriptor Privilege Level)の2ビットで権限を表現します。

0~4の4種類をとり、小さいほど強い権限になります。

となり、今回はこの部分に0b11=4を登録しますのでアプリケーションのセグメントであることをマークしたことになります。

type:セグメントのread,write,executeなど

この章ではtypeはあまり関係ないけど一応説明。

type binary read write execute content
0 000 × × data segment
1 001 × data segment
2 010 × × stack segment
3 011 × stack segment
4 100 × × code segment
5 101 × code segment
6 110 × × conforming code segment
7 111 × conforming code segment
  • コンフォーミングコードセグメントはアプリケーションからOSの権限の命令(0)を直接呼ぶためのセグメントです。
  • コードセグメントはcsレジスタ、データセグメントはdsレジスタを使って参照します。
  • スタックセグメントはssレジスタを使います。

設定したアクセス属性により上がる例外をハンドルする

x86ではアクセス属性に違反した挙動があればINT0x0dの割り込みが発生する。この割り込みを例外という。 例外をサポーするにはこのINT0x0dに対応する割り込みハンドラを作成してIDTに登録すればOK。

本書によるとqemuのバグ?などがあるらしく例外割り込みなどはうまく再現できないとのこと。古い本なので最近のqemuでは実は治ってたりしてwと思いましたが、私が利用しているバージョンは2.11.0の段階でまだ治っていません。

$ qemu-system-i386 --version
QEMU emulator version 2.11.0
Copyright (c) 2003-2017 Fabrice Bellard and the QEMU Project developers

crack2が破壊しているOSの部分。

crack2を実行するとdirコマンドの表示内容がなくなるとのことですが、 これがなぜ発生しているか本書に記載がなかったので調べてみました。

[INSTRSET "i486p"]
[BITS 32]
        MOV     EAX,1*8         ; OS用のセグメント番号
        MOV     DS,AX           ; これをDSにいれちゃう
        MOV     BYTE [0x102600],0
        MOV     EDX,4
        INT     0x40

冒頭にまとめた通り、セレクタ値1のセグメントはOSのデータ領域です。ここのベースアドレスはメモリ上の先頭(0x00000000)になります

position content base address limit ar
1 OS data 0x00000000 0xffffffff 0100000010010010

このセグメントをDSで指定した状態で,MOV BYTE [0x102600],0を実行しています。オフセットアドレス(0x102600)の位置に0を書き込むという意味です。今回はベースアドレスが0x00000000なのでオフセットもクソもありませんが。。。

そしてこの0x102600の位置を冒頭のメモリマップで確認しています。

start end size content
0x00000000 0x000fffff 1MB boot
0x00100000 0x00267fff 1440KB floppy content

どうやらフロッピーディスクの0x2600番地の位置のようです。 この位置のデータをバイナリエディタで確認すると次のようになっていました。

[:plane]

そしてこの位置は19日目に書いた通り、root directoryとなります。 crack2は

  • root directoryの先頭位置に0書き込みディレクトリの内容を消す、

という結果を引き起こすコードだったのです。

dirコマンドはファイル先頭位置のヌル文字(0x00)を読み取った時点で処理を中断していますので、これが理由でdirの出力結果がなくなった、ということになります。

void cmd_dir(struct CONSOLE *cons)
{
    struct FILEINFO *finfo = (struct FILEINFO *) (ADR_DISKIMG + 0x002600);
    int i, j;
    char s[30];
    for (i = 0; i < 224; i++) {
        if (finfo[i].name[0] == 0x00) {
            break;
        }
        /*dirの処理*/
        }
    }
    cons_newline(cons);
    return;
}

今回はそのような攻撃をセグメント属性と例外割り込みで守ろう、という趣旨の章でした。

はじめて読む486

参考にしています。 30日OS自作本がOSを30日で作るという目標のため省いている細かいCPUの機能についてかなり充実しています。あと図がかなりわかりやすい。

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

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

*1:arは権限です。acsess right