Yabu.log

ITなどの雑記

30日OS自作本22日目3ウインドウAPI

22日目3記事目です。

ウインドウAPIを作ります。APIから値を戻すのにややトリッキーなことをやっているので、そこを整理して見ました。

私のこの記事はやや分かりにくいですが、ここは各自紙やエクセルにレジスタやスタックの内容を書き起こしながら頭の中で動かすことをお勧めします。

ウインドウAPIの処理を追ってみる

EDX=5
EBX=ウインドウのバッファ
ESI=ウインドウのx方向の大きさ
EDI=ウインドウのy方向の大きさ
EAX=透明度
ECX=ウインドウの名前

呼び出し後はこんな値が返されます

EAX=ウインドウを操作するための番号(リフレッシュとかに必要)

これを作ろうと思ったら、新しい問題にぶつかりました。それは、APIが呼ばれた後にレジスタに値を入れてアプリに返す方法を考えていなかった、ということです。

この問題ですが、結構変な解決策を取っており、出来上がったAPIの動きが難解で私の4bit脳味噌で理解できなかったので、スタックの値を中心に脳内ダンプしながら動きを追っていきます。

winhelo.c

以下のスタックの状態でapi_openwinを呼び出す

//定義
int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);

//呼び出し
win = api_openwin(buf, 150, 50, -1, "hello");

スタックの状態 no,stack 4,buf 8,xsiz=150 12,ysiz=50 16,col_inv=-1 20,title="hello"

api_openwin

呼び出し直後にスタックにレジスタの値を退避する

PUSH EDI
PUSH    ESI
PUSH    EBX

スタックの状態

no stack
4 edi_old
8 esi_old
12 ebx_old
16 buf
20 xsiz
24 ysiz
28 col_inv
32 title

レジスタにwinhelo.cから渡された引数を入れる,edx=5とする

edx=5はhrb_apiでのウインドウのAPIに分岐するための代入

MOV      EDX,5
MOV     EBX,[ESP+16]    ; buf
MOV     ESI,[ESP+20]    ; xsiz
MOV     EDI,[ESP+24]    ; ysiz
MOV     EAX,[ESP+28]    ; col_inv
MOV     ECX,[ESP+32]    ; title

レジスタの状態(popad/pushadで実行される順に表記を並べてある)

regster value
edi ysiz
esi xsiz
ebp
esp
ebx buf
edx 5
ecx title
eax col_inv

スタックの状態※変化なし

no stack
4 edi_old
8 esi_old
12 ebx_old
16 buf
20 xsiz
24 ysiz
28 col_inv
32 title

asm_hrb_api

レジスタに値を代入した状態で_asm_hrb_api(int 0x40)を呼ぶ

レジスタの退避

PUSH DS
PUSH    ES
PUSHAD      ; 保存のためのPUSH

レジスタの状態※変化なし

regster value
edi ysiz
esi xsiz
ebp
esp
ebx buf
edx 5
ecx title
eax col_inv

スタックの状態 ※面倒なので番号は振っていません。

stack
ysiz
xsiz
buf
title
col_inv
es
ds
edi_old
esi_old
ebx_old
buf
xsiz
ysiz
col_inv
title

hrb_apiを呼ぶ準備。セグメントレジスタと引数のpush

PUSHAD       ; hrb_apiにわたすためのPUSH
MOV     AX,SS
MOV     DS,AX       ; OS用のセグメントをDSとESにも入れる
MOV     ES,AX

レジスタの状態,ESとDSのみ変化(セグメントレジスタなのでここには表記しない)。AXは一時領域としての使用なので追いかけるような質のものではなさそう。

regster value
edi ysiz
esi xsiz
ebp
esp
ebx buf
edx 5
ecx title
eax OSのセグメント

スタックの状態

stack
ysiz
xsiz
buf
title
col_inv
ysiz
xsiz
buf
title
col_inv
es
ds
edi_old
esi_old
ebx_old
buf
xsiz
ysiz
col_inv
title

hrb_api

edx=5=ウインドウのAPIに分岐してウインドウ関連の処理をする。 ちなみにwindow関連の処理はこんな感んじで定義されている。

} else if (edx == 5) {
    sht = sheet_alloc(shtctl);
    sheet_setbuf(sht, (char *) ebx + ds_base, esi, edi, eax);
    make_window8((char *) ebx + ds_base, esi, edi, (char *) ecx + ds_base, 0);
    sheet_slide(sht, 100, 50);
    sheet_updown(sht, 3);   /* 3という高さはtask_aの上 */
    reg[7] = (int) sht;
}

スタックの16番目の位置に戻り値として確保したシートのアドレスを格納する

struct SHEET *sht;         //
・・・
int *reg = &eax + 1;       //regをスタックの9番目の値へのポインタとして宣言
                           //eax=8番目の引数のためeax+1=スタックの9番目のポインタとなる
・・・
sht = sheet_alloc(shtctl); //shtに確保したシートのアドレスを代入

・・・
reg[7] = (int) sht;         //スタックの16番目のアドレスに確保したシートのアドレスを代入

stack
ysiz
xsiz
buf
title
col_inv
ysiz
xsiz
buf
title
sht
es
ds
edi_old
esi_old
ebx_old
buf
xsiz
ysiz
col_inv
title

再びasm_hrb_apiに戻り戻り値(EAX)にshtが来るように操作する

CALL _hrb_api     ;これが終わった後。。。
CMP     EAX,0            ; EAXが0でなければアプリ終了処理
JNE     _asm_end_app
ADD     ESP,32       ;ESPの値を8進める

ADD ESP,32これで多分スタックの上から8個目まで、データを捨てるような動きになる。 この時のスタックの値

stack
ysiz
xsiz
buf
title
sht
es
ds
edi_old
esi_old
ebx_old
buf
xsiz
ysiz
col_inv
title

この時にPOPADをすると、

POPAD
POP     ES
POP     DS
regster value
edi ysiz
esi xsiz
ebp
esp
ebx buf
edx
ecx title
eax sht

となりeaxにaipを実行した結果shtを格納できるというわけです。

非常に疲れました。

22章で作成するAPI

結局本章ではAPIを3種類作っています。

新しいウインドウを表示する
EDX=5
EBX=ウインドウのバッファ
ESI=ウインドウのx方向の大きさ
EDI=ウインドウのy方向の大きさ
EAX=透明色
ECX=ウインドウの名前

ウインドウへの文字表示
EDX=6
EBX=ウインドウの番号
ESI=表示位置のx座標
EDI=表示いちのy座標
EAX=色番号
ECX=文字列の長さ
EBP=文字列

四角を描く
EDX=7
EBX=ウインドウの番号
EAX=x0
ECX=y0
ESI=x1
EDI=y1
EBP=色番号

現時点でウインドウを作成するAPIはシートの高さ、位置を渡せず、APIの中で決め打ちになっているので

  • 好きな位置・高さに出す
  • 複数ウインドウを出す

ができなくなっています。

www.youtube.com

新規に3つ渡すにはレジスタが足りないので構造体を作ってそのアドレスを渡す感じに改善されるのかな〜と予想してみます。

30日OS自作本22日目2hrbファイルの構造

.hrbファイルの構造とcmd_app()の改善

アプリケーションから利用できる文字表示APIを作ったところ、C言語から作ったアプリケーションでは データ(文字列)の取得がうまくいかないようです。本章では.hrbファイルの解説と件のデータ取得の改善が解説されています。

hrbファイルの解説

hrbファイルはそもそもharibote OSのオリジナルの実行ファイルです。 本章で多分はじめてhrbファイルの仕様が公開されます(多分)

  • 0x0000,OSに用意してもらうアプリ用のデータセグメントの大きさ
  • 0x0004,"Hari"(.hrbファイルであることのマーク)
  • 0x0008,データセグメント内の予備領域の大きさ
  • 0x000C,ESPの初期値&データ部分の転送先番地
  • 0x0010,hrbファイル内部のデータ部分の大きさ
  • 0x0014,hrbファイル内にあるデータ部分がどこから始まっているのか
  • 0x0018,0xe90000000
  • 0x001c,アプリの実行開始番地 - 0x20
  • 0x0020,malloc領域開始アドレス

hello4.hrbの例

00000000  00 10 00 00 48 61 72 69  00 00 00 00 00 04 00 00  |....Hari........|
00000010  0e 00 00 00 64 00 00 00  00 00 00 e9 39 00 00 00  |....d.......9...|
00000020  10 04 00 00 55 89 e5 68  00 04 00 00 e8 13 00 00  |....U..h........|
00000030  00 58 c9 e9 1a 00 00 00  ba 01 00 00 00 8a 44 24  |.X............D$|
00000040  04 cd 40 c3 53 ba 02 00  00 00 8b 5c 24 08 cd 40  |..@.S......\$..@|
00000050  5b c3 ba 04 00 00 00 cd  40 55 89 e5 5d e9 c2 ff  |[.......@U..]...|
00000060  ff ff 00 00 68 65 6c 6c  6f 2c 20 77 6f 72 6c 64  |....hello, world|
00000070  0a 00                                             |..|

hello4.hrbのデータと対応づけてみました。

addr data
0x0000 0x00100000
0x0004 0x48617269
0x0008 0x00000000
0x000C 0x00040000
0x0010 0x0E000000
0x0014 0x64000000
0x0018 0x000000E9
0x001c 0x39000000
0x0020 0x10040000
0x0024 0x5589E568

例えば

addr data
0x0014 0x64000000

0x0014,hrbファイル内にあるデータ部分がどこから始まっているのか

ですが、hello4.hrbファイルの0x64の位置からhello, worldのデータが格納されているのがわかります。 またdの書かれているアドレスが0x64+0xE=0x72であり、ファイル終端までがデータという扱いにしていることもわかります。

コマンド用の実行ファイルをCのソースから作る際、文字列がうまく参照できないようになっているようです。それをこちらのファイルフォーマットを参考に、cmd_app関数で、0x0014の位置に格納されている文字データをこのような感じでセグメント内ESP(0x000C)のスタックに転送しています。

/* ファイルが見つかった場合 */
p = (char *) memman_alloc_4k(memman, finfo->size);

・・・中略・・・
esp    = *((int *) (p + 0x000c)); //ESPの初期値&データ部分の転送先番地
dathrb = *((int *) (p + 0x0014)); //hrbファイル内にあるデータ部分がどこから始まっている
・・・中略・・・

q = (char *) memman_alloc_4k(memman, segsiz);

for (i = 0; i < datsiz; i++) {
    q[esp + i] = p[dathrb + i];
}

また.hrbファイルから得られる情報であるセグメントサイズの大きさ、データ部分の大きさもOSの改善に役立てています。

datsiz = *((int *) (p + 0x0010)); //hrbファイル内部のデータ部分の大きさ
segsiz = *((int *) (p + 0x0000)); //OSに用意してもらうアプリ用のデータセグメントの大きさ

セグメントは今まではとりあえず64KBとなっていますが、こちらを参照することでセグメントの利用効率が上がります。

//.hrbファイル中のセグメントサイズを使うように改善。
//q = (char *) memman_alloc_4k(memman, 64 * 1024);
q = (char *) memman_alloc_4k(memman, segsiz);

感想

アプリケーションのデータを参照するために、なぜこのような手間がかかるのか理解できませんでした。著者の自作ツール(bim2hrb)を利用されていない方のブログなどを見ると特にこの作業は不要そうです。bim2hrb等は普通のリンカに必要とされる何かが抜けているのでしょうか?まぁ私リンカがなんたるかもそんなにわかっていないので特にこの辺を探求するのはまだ早いかなと思いました。

次回はウインドウAPIについてです。

30日OS自作本22日目1セキュリティ編

22日目の内容になります。

長すぎるので3つに分けます。 3日目以降でやや難易度が高くなっている章だと思われます。

今回はセキュリティ編です。

バグ発見のための拡張

  • スタック例外の実装
    • スタック例外(配列に対して大きすぎるインデックス等)は0x0cでハンドルされる。ここに割り込みハンドラを登録すればOK
    • QEMUではうまく動かないようだ
  • 強制終了の実現方法
    • bashのctrl + c 的なやつ
    • 強制終了(asm_end_app)の命令を作成し、この命令をtask.eipに書き込む
    • 本書ではctrl + F1を押したタイミングで発動
_asm_end_app,
;   EAXはtss.esp0の番地
        MOV     ESP        ,[EAX]
        MOV     DWORD [EAX+4]        ,0
        POPAD
        RET                 ; cmd_appへ帰る
  • 元のESPをEAXに代入した状態で呼びます。
  • このサブルーチン内でESPや各レジスタの値を退避してあるスタックからPOPADで戻してreturn(呼び出し元cmd_appへ戻る)します。

個人的にはバグ発見のツールとしては以下のものが欲しい

  • ホスト側のOSでメモリやレジスタをリアルタイムでダンプする
  • ホスト側のOSからゲストOSの機械語を1命令ずつステップ実行する
    • かつ、任意のタイミングでレジスタ、メモリが参照できる
  • ホスト側のOSにログが残せる
    • log.println("message")みたいにかけばホスト側のtxtファイルにログが残せる
  • デバッグ時の詳細出力

なお最後のものは自力で作れる範囲で作ってみた。 一般保護例外捕捉時にスタックに自動的に積まれる値を表示しているだけ。

一応この部分のソースを貼っておく

int *inthandler0d(int *esp)
{
    struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
    struct TASK *task = task_now();
    char s[30];
    cons_putstr0(cons, "¥n0D General Protected Exception¥n");
    sprintf(s, "CS:EIP     = %04X:%08X¥n", esp[12],esp[11]);
    cons_putstr0(cons, s);
    sprintf(s, "SS:ESP     = %04X:%08X¥n",esp[15],esp[14]);
    cons_putstr0(cons, s);
    sprintf(s, "EFLAGS     = %08x¥n",esp[13]);
    cons_putstr0(cons, s);
    sprintf(s, "ERROR CODE = %08x¥n",esp[10]);
    cons_putstr0(cons, s);

    return &(task->tss.esp0);  /* 異常終了させる */
}

様々な攻撃を防ぐ

crack3,アプリケーションからタイマーを不正に弄る

GUI(マウス等)の描画崩壊を狙ったタイマーに不正な値を書き込むプログラムです。 アプリケーション領域からのIOポートの利用を制限されているのでこのプログラムはフォールトの例外が発生します。

EFLAGSレジスタのIOPLフィールドが動作レベル(CPL)より高い特権レベルを持っているのでIOポートの利用が制限されているということになります。

アプリケーション用のセグメントの特権レベルは21日目に3(一番弱い)で設定してあります。

EFLAGSレジスタはPOPFD命令で設定できるようです。 都合の良いことにnaskfunc.nasにPOPFD専用のサブルーチンがあったので利用してみます。

_io_store_eflags,    ; void io_store_eflags(int eflags);
        MOV     EAX        ,[ESP+4]
        PUSH    EAX
        POPFD       ; POP EFLAGS という意味
        RET

権限の高いセグメントで動いているPGでELGASを書き換えてcrack3を実行してみました。

//ファイル先頭らへんに定義
#define EFLAGS_IOPL_3 0x00003000

(中略)
//これをhariMainで実行
int eflg;
eflg = io_load_eflags();
eflg |= EFLAGS_IOPL_3; /* IOPL=3 */
io_store_eflags(eflg);

OSの方で画面出力してみましたが、

  • 改修前,0246
  • 改修後,3246

でうまく変更できているように思えます。

hex binary
eflags 00!!000000000000
0246 0000001001000110
3246 0011001001000110

eflagsの!のところがIOPL。上から3,4bit目

4バイト目の下位2ビットが1になっていることを確認できたのですが、

EFLAGSレジスタの値は正しく更新できているのですが、 特に変化はなしです。なんか俺が試したこと全部失敗してるな~

と思いましたが、例外補足時のログで実行時にIOPLが変化してしまっていることが発覚。 おそらくhariMain内で書いた後にどこかで変わっていたのだと思います。探しても見つかりませんでした。

IOPLが変化した理由

並行して読んでいるはじめて読む486がちょうどマルチタスクの章に入ったところで気づきました。

  • EFLAGSレジスタはOS全体で共有しているものではなくて、タスクごとに用意されている

そうです。hariMainでEFLAGSを変えても、別タスクであるconsoleのEFLAGSには変化はないのです。 TSSにEFLAGSが含まれているのをみて気付きましたが、これに気付いた時かなりスッキリしました。

...ということでなんとかしてconsoleのタスクからeflagsを変更する必要があります。

IOPLを変更できるコマンドを作成

とりあえずIOPLを変更できるコマンドを作ってみました。

IOPL SET <Privilege level>

最強の0から最弱の3まで全て設定可能です。これで実行時にアプリケーションがIOポートにアクセスすることができます。

www.youtube.com

cracks4,CLI + HLT命令で割り込みを無効化する

[INSTRSET "i486p"]
[BITS 32]
        CLI
fin,
        HLT
        JMP     fin

こちらも正しく例外でストップします。アプリケーションの権限で動いているときにHLTとCLI命令を実行すると例外扱いになります。 HLT命令をアプリケーションが直接実行するのはまずいので、タスクをスリープさせるAPIを呼ぶべきです。

crack5,OSの領域にあるCLI命令を含むサブルーチンを呼ぶ

[INSTRSET "i486p"]
[BITS 32]
        CALL    2*8,0xac1
        MOV     EDX        ,4
        INT     0x40

アプリのセグメントからOSのセグメントのPGを呼び出すfarCALLは失敗します。 コールゲート(ゲートディスクリプタ)、という仕組みを利用してCLIを含むサブルーチンがロードされているセグメントをアプリケーションから呼べるように設定すればうまくいく気がしますが、気が向いたら試してみます。

(おまけ),crack6

osのルートディレクトリを破壊するようなAPIを呼ぶcrack6です。 そんなAPI定義するなよ!!!  という感じですが、プロテクトモードのセグメントの設定をきちんとすれば、このくらいしかこのOSを攻撃する方法はないということです。 これは結構すごいことじゃないでしょうか。と思いましたが、多分まだまだ僕の知らない攻撃方法はあるのでしょう。

各攻撃バイナリのエラーを表示してみる

悪意のあるアプリ 攻撃内容 備考
crack1(p428) C言語でルートディレクトリにヌル文字(0x00)を書き込む QEMUで例外捕捉せず
crack2 アプリからOS用のセグメントにアクセスし、1と同じくルートディレクトリを壊す QEMUで例外捕捉
crack3 アプリからIOを不正利用 QEMUで例外捕捉
crack4 アプリからCLI HLT命令を実行 QEMUで例外補足
crack5 アプリからOSの命令をfar Call QEMUで例外捕捉

ログ内容

バイナリ CS,EIP SS,ESP FLAGS ERROR CODE
crack2 1F5B,00000005 1F63,00010000 00000206 00000008
crack3 1F5B,00000002 1F63,00010000 00000206 00000000
crack4 1F5B,00000000 1F63,00010000 00000206 00000000
crack5 1F5B,00000000 1F63,00010000 00000206 00000010

せっかく作ったログを使ってデータを参照したいのですが、どうやら.mapファイルがないと難しいようです。nasmでmapファイルを作る方法などをググって試してみましたが、うまくいきませんでした。(アセンブル時にエラー)

次回は.hrbファイル構造の解説とそれを利用したOSの改善について書きます。

Java読書会BOF「Kotlinイン・アクション」を読む会 第6回に参加

Kotlin in Action 第6回

ジェネリクスの後半(9章)とリフレクションの章(10章)とDSL(11章)を読みました。 10章は技術的にもちょっと面白い読み物になっていて、オープンソースjsonリアライザ/デシリアライザであるjKid*1の解説をしながらリフレクションを学ぶ章になっています。

ジェネリクスにつけるout,in

  • outをつけると共変
  • inをつけると反変
  • それ以外は不変(javaと同じ)

p322の表がおかしい

共変 反変 不変
Class<out T> Class<in T> MutableList<T>
クラスのサブタイピングは維持 サブタイピングは逆 サブタイピングなし
Tはoutポジションだけ Tはinポジションだけ Tはどこでも

inポジションだけにすべき箇所の文言がoutポジションになってしまっている。

::の意味

クラス参照の意味。p344に軽く説明。(なぜここ?)

  • String::ClassはJavaのString.classと同じ意味。
  • p146メンバ参照で解説されているものと同じ

久しぶりに読むと忘れちゃいますね。

関数インターフェイス?

p332ページのまとめ

・関数インターフェイスは、1つめの方パラメータについて反変、2つ目の方パラメータについて久代編として宣言されています。

とありますが、「関数インターフェイス」と書いてしまうと一般的な関数の意味になってしまうのでは?という違和感を示す方がいました。322ページにあるFunctionインターフェイスのことなので、Functionインターフェース、もしくはFunctionNインターフェースにすれば違和感がない気がする。

Deprecatedで推奨する関数を通知できる。

p335の内容です。

@Deprecated("Use removeAt(index_ instead.",ReplaceWith("removeAt(index)"))
fun remove(index int){...}

こんな感じで非推奨になったAPIに対して代替を示すことができます。この機能はJavaにはなかったため(多分)大変便利だと思います。

inteliJ IDEAではAlt+ ENterはかなり使う

p338

IntelliJ IDEAでは、コンパイラの警告状で[ALT] + [Enter]を押して表示されるオプションメニューから[Suppress]を選ぶことで、このアノテーションを挿入できます。

とありますが、コンパイラ抑制系のアノテーションの挿入の推奨をかなりしてくるとのことです。

これですね。

Java EE 8のJSONのライブラリが

などがかなり良いらしい

jackson,GSONの作者が言ってたらしい。

340文章がおかしい。

最後の塊の二行目。

data class Person(
  @jsonName("alias") val firstName: String,
  @JsonExclude val age: Int? = null  
  )

プロパティfirestNameを示すJSON内でのキーを変更するために、アノテーションをつけています。これは、プロパティageをシリアライゼーションやでシリアライゼーションから除外するためです。

「これは、プロパティageを~」の直前に@jsonExcludeの説明がないと変。何かコピペのし忘れ?のように思える。

p350 KFunctionNはどこに生成される?

KFunctionNはKFunction1,KFunction2,KFunction3...と無限に存在しているのか?と思ったがこれはコンパイラによって生成されるらしい。これがどこに生成されるのか?という話題になった。(宿題)

p377誤植

2こ目のコードブロック。

閉じのダブルクォート"が抜けている。

感想

この本の9章、10章は非常に難しいです。少し理解できない点があるので復習中です。 9章は4時間ほどかけてコードを動かしながら精読しました。 明日以降10章も復習したいと思います。

次回は11.2から!

おそらく午前中で終了する程度の分量です。午後は何かレクをするようです(卓球?)。

ちなみに高橋さんは利き手じゃなくても私に勝てるそうです。

Kotlinイン・アクション

Kotlinイン・アクション

ブルーボトルコーヒーの豆を買った

通勤経路にあるので覗いてみたところお土産コーナーだけ人がいなかったので買いました。行列並んでまで買うと負けな気がするので今まで飲む機会がありませんでした。

 

1500円/200gくらいしました。普段買ってる豆が1200円くらいなので少し高いです。挽き売りはやってないらしいです。コーヒーミルを自宅に持っていない人は多いと思うので挽き売りも普通にやって欲しい。

 

f:id:yuyubu:20180616202448j:image

 

パッケージ裏に焙煎日が書いているけど、なんと焙煎日がなんと1日前(6/6購入)。これは結構すごい。

 

f:id:yuyubu:20180616202709j:image

 

豆はこんな感じ。貝殻豆やちゃんと焼けてなさそうな色の豆が結構入ってたりして値段の割にはあまりいい印象はありません

 

f:id:yuyubu:20180616203352j:imagef:id:yuyubu:20180616203409j:image

 

適当に粗挽きにしてドリップして頂きました。焼きたてなだけあってブクブク膨らみますね。

 

f:id:yuyubu:20180616203452j:image

 

味はアフリカの豆を沢山混ぜたような味がしました。ブレンド名がスリーアフリカンだから当たり前か。

 

特にオチはありません。

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

プログラマのためのSQL 読書会(20)に参加

25.3.4の自己外部結合から25章最後まで読みました。

自己結合、自己外部結合などがテーマです。 SQLでパズル的なことをやろうとすると自己外部結合が役立ちそうです。 あとは障害調査くらいですかね。

正誤表と掲載SQL

今回読書会中に詰まることがあり必要になりました。どちらも翔泳社の公式サイトにあります。

www.shoeisha.co.jp

ただしSQLはプレーンテキストが置いてあるだけであり、またSQLを動かすためのinsert文がついていないものも多数あります。また正誤表で上がっているものの訂正もされていないようですので注意が必要です。

外部結合は順番が入れ替えられない

25.3.5の内容。外部結合は結合則が成り立たない

  • (A + B) + C
  • A + (B + C)

これが同じ結果にならない可能性がある。 どの順番で結合をするかで結果が変わってしまう。

サポートでも外部結合が入っているとお手上げらしい。

25.4のクエリの疑問

---生徒テーブル

CREATE TABLE Students
(student_nbr INTEGER NOT NULL PRIMARY KEY,
 student_type CHAR(1) NOT NULL DEFAULT 'D'
     CHECK (student_type IN ('D', 'F')));

---国内学生テーブル

CREATE TABLE DomesticStudents
(student_nbr INTEGER NOT NULL PRIMARY KEY
     REFERENCES Students(student_nbr));

---留学生テーブル

CREATE TABLE ForeignStudents
(student_nbr INTEGER NOT NULL PRIMARY KEY
     REFERENCES Students(student_nbr));

---データ投入

-- Students
INSERT INTO Students(student_nbr, student_type) VALUES('1', 'D');
INSERT INTO Students(student_nbr, student_type) VALUES('2', 'D');
INSERT INTO Students(student_nbr, student_type) VALUES('3', 'F');
INSERT INTO Students(student_nbr, student_type) VALUES('4', 'D');
INSERT INTO Students(student_nbr, student_type) VALUES('5', 'F');

-- DomesticStudents
INSERT INTO DomesticStudents(student_nbr) VALUES('1');
INSERT INTO DomesticStudents(student_nbr) VALUES('2');
INSERT INTO DomesticStudents(student_nbr) VALUES('4');


-- ForeignStudents
INSERT INTO ForeignStudents(student_nbr) VALUES('3');
INSERT INTO ForeignStudents(student_nbr) VALUES('5');




SELECT Students.*, DomesticStudents.*, ForeignStudents.*
  FROM Students
         LEFT OUTER JOIN
           DomesticStudents
    ON CASE Students.student_type
       WHEN 'D'
       THEN 1
       ELSE NULL END = 1
         LEFT OUTER JOIN
           ForeignStudents
             ON CASE Students.student_type
                WHEN 'F'
                THEN 1
                ELSE NULL END = 1
                --注意:正誤表を見ると以下が抜けていた。
                WHERE (   Students.student_nbr = DomesticStudents.student_nbr
                       OR Students.student_nbr = ForeignStudents.student_nbr);

結果

student_nbr | student_type | student_nbr | student_nbr
-------------+--------------+-------------+-------------
          1 | D            |           1 |            
          2 | D            |           2 |            
          3 | F            |             |           3
          4 | D            |           4 |            
          5 | F            |             |           5

なぜCASE式を使っているのか?という意見が出た。

  • Students.student_type = D
  • Students.student_type = F

の2つの結合で良いのではないかという意見が出た。

SELECT Students.*, DomesticStudents.*, ForeignStudents.*
  FROM Students
         LEFT OUTER JOIN
           DomesticStudents
    ON  Students.student_type = 'D'
         LEFT OUTER JOIN
           ForeignStudents
             ON  Students.student_type = 'F'
                WHERE (   Students.student_nbr = DomesticStudents.student_nbr
                       OR Students.student_nbr = ForeignStudents.student_nbr);

結果

student_nbr | student_type | student_nbr | student_nbr
-------------+--------------+-------------+-------------
          1 | D            |           1 |            
          2 | D            |           2 |            
          3 | F            |             |           3
          4 | D            |           4 |            
          5 | F            |             |           5

確かに結果は変わらないですね。

古いSQLの結合構文の有利な点

結合が宣言的にかけるというメリットがある。

-- JOINを使わない古い結合
SELECT x, y, z
  FROM Foo, Bar, Flub
 WHERE y BETWEEN x AND w;--結合条件が宣言的にかける

-- JOINを使う新しい結合
 SELECT x, y, z

   --Foo,BAR,Flubとインクリメンタルに結合する必要がる。
   --SQLで手続き的に書く必要が出てくる。
   FROM Foo INNER JOIN Bar
     ON y >= x
             INNER JOIN Flub
               ON y <= w;
  • シーター結合は普通はやらない。
  • 新しい結合句で問題ない。

新人研修や初学者がとくべき定番の問題

自己結合の章ではパズルのような問題を沢山ときます。そういえばSQLでは

などの他のプログラミング言語だと最初に解いて見る定番の演習問題が無いので 質問してみました。

スッキリわかる SQL 入門 ドリル215問付き! (スッキリシリーズ)

スッキリわかる SQL 入門 ドリル215問付き! (スッキリシリーズ)

改訂第3版 すらすらと手が動くようになる SQL書き方ドリル (WEB+DB PRESS plus)

改訂第3版 すらすらと手が動くようになる SQL書き方ドリル (WEB+DB PRESS plus)

複数冊ありますがどちらもおすすめとのこと。 後者はブラウザでSQLを試せる環境がついています。

  • キリン本。初学者が躓きやすいgroup byのサポートが充実。また入門書で集約関数まで扱っているものは少ない。
    • 私もSQLの学習はこの本からはじめました。*1

SQL 第2版 ゼロからはじめるデータベース操作 (プログラミング学習シリーズ)

SQL 第2版 ゼロからはじめるデータベース操作 (プログラミング学習シリーズ)

  • 会社がSQLのドリルを作ってる。
  • 林優子さんのSQLドリル(書籍? or 講習?)が良いらしい

どこかで話がそれてマニアックなデータベース本の話題になりました。

  • データベースリファクタリングという本がメチャクチャ良い。
    • 例のピアソンショックでプレミアがついてる
    • 積読している人は質屋に持っていくと金になるかも。。。

データベース・リファクタリング

データベース・リファクタリング

  • 作者: スコット W アンブラー,ピラモド・サダラージ,梅澤真史,越智典子,小黒直樹
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2008/03/26
  • メディア: 単行本
  • 購入: 10人 クリック: 211回
  • この商品を含むブログ (53件) を見る

  • 川崎の工業図書館?にSQL関連のレアな書籍が結構ある。

    横浜に県立図書館がすでに開館していることと、工業都市・川崎にあることから、開館当初から自然科学・工業・産業分野を重点とした資料収集・サービス方針を掲げている。

  • カーリル*2というサービスで全国の図書館を検索できる

  • 成熟している分野なので教材などは充実している感じですが、定番の問題みたいなのは特になさそうです。

人名

この本は質問サイト?のようなコミュニティの引用も多く、一般人の人名が多く登場します。 主催の木村さんは同名人物っぽい人をlinked inで見つけてメッセージを送ってみたそうですが、返事がなかったようです。

初版が古い本なので、昔から書かれている人とはもう連絡を取るのが難しいかもしれませんね。 最近の本だとtwitterIDを載せると思いますし、昔より連絡コストは大きく減りつつあると思います。

感想

パズル系のクエリは普段描かないような書方で描かれていることが多かったり、クエリそのものが長くなっている等で、SQLにコメントがついていないと理解が難しいです。本章は読書会で読むというより、家でじっくり試しながら読むほうがよさそうです。