21日目の内容です。セグメントやメモリの復習が多めです。
- hariboteOSのメモリマップ
- セグメントについておさらい。
- セグメントのアクセス属性を設定する
- 設定したアクセス属性により上がる例外をハンドルする
- crack2が破壊しているOSの部分。
- はじめて読む486
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種類をとり、小さいほど強い権限になります。
- 0:OS
- 1,2:デバイスドライバなど
- 3:アプリケーション
となり、今回はこの部分に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番地の位置のようです。 この位置のデータをバイナリエディタで確認すると次のようになっていました。
そしてこの位置は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
32ビットコンピュータをやさしく語る はじめて読む486 (アスキー書籍)
- 作者: 蒲地輝尚
- 出版社/メーカー: 角川アスキー総合研究所
- 発売日: 2014/10/21
- メディア: Kindle版
- この商品を含むブログを見る
参考にしています。 30日OS自作本がOSを30日で作るという目標のため省いている細かいCPUの機能についてかなり充実しています。あと図がかなりわかりやすい。
- 作者: 川合秀実
- 出版社/メーカー: 毎日コミュニケーションズ
- 発売日: 2006/03/01
- メディア: 単行本
- 購入: 36人 クリック: 735回
- この商品を含むブログ (299件) を見る
*1:arは権限です。acsess right