Yabu.log

ITなどの雑記

30日OS自作本27日目

27日目の記事になります。というよりほとんどLDTの解説になります。

www.youtube.com

LDT

  • OSのセグメントにアプリケーションからアクセスすることはできない。(21,22日目のセキュリティ対応)
  • ただしアプリは別のアプリのセグメントにアクセスできてしまう。(コード例:crack7)
  • CPUはこの問題に対処するためにLDT(Local Descriptor Table)という仕組みを持っている
  • LDTを使うことでタスクローカルなセグメントを作ることができる。

LDTのアドレス指定

タスク切り替えは以下のtask state segmentをGDTに登録してタスク切り替えのたびにレジスタ等に値をロードします。

struct TSS32 {
    int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
    int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
    int es, cs, ss, ds, fs, gs;
    int ldtr, iomap;
};

このTSSのldtrにLDTの開始アドレスを登録するとタスク切り替え時にLDTの場所をCPUが指定することになります。

hariboteOSでmtask.cでLDTを設定しています。

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

//こいつらは権限。一応書いていますがとりあえず今は関係ないので無視でOK
#define AR_TSS32       0x0089
#define AR_LDT         0x0082

set_segmdesc(gdt + TASK_GDT0 + i            , 103, (int) &taskctl->tasks0[i].tss, AR_TSS32); /*タスクのGDTの設定*/
set_segmdesc(gdt + TASK_GDT0 + MAX_TASKS + i, 15 , (int) taskctl->tasks0[i].ldt , AR_LDT  ); /*LDTの設定*/

LDTを使う前後の比較

アプリのセグメントの設定

LDTを使う前のcmd_app()ではGDT上のタスクのTSSの配置位置(task->sel)から相対的にコードセグメント、データセグメントを計算しています。

set_segmdesc(gdt + task->sel / 8 + 1000, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60); //コードセグメント
set_segmdesc(gdt + task->sel / 8 + 2000, segsiz - 1,      (int) q, AR_DATA32_RW + 0x60); //データセグメント

セグメントをセットするコードに関して整理します。

  • gdt + task->sel / 8 + 1000

これはコードセグメントですが、各項は以下の意味を持っています。

意味
gdt GDTのベースアドレス
task->sel/8 GDTを参照する時のセレクタ
1000 GDTの1000~2000をアプリケーションのコードセグメントとするための加算アドレス

task->selをなぜ8で割るかというと、GDTのセレクタ値は8の倍数で指定されているからです。

index sel(dec) sel(binary)
0 0 0b00000
1 8 0b01000
2 16 0b10000
3 24 0b11000
4 32 0b20000

これはGDT場では8飛びでデータが配置されているという意味ではなく、あくまでGDTを指定するためのインデックスの仕様です。 詳しくは後述するのでそちらを参照してください。

このようにLDT導入前ではTSSの配置indexからアプリケーションのコードセグメント、データセグメントを相対的に求めてGDT上に確保しています。

例:TSSがGDTの5の位置のアプリの場合,

  • index = 5
  • セレクタ値(sel) = 5*8
  • コードセグメント = sel/8 + 1000 = 1005
  • データセグメント = sel/8 + 2000 = 2005

LDT導入前のGDTのマッピングは以下のようになります。

index content
1003 task_a用(アプリはないので使っていない)
1004 idle用(アプリはないので使っていない)
1005 1個目に確保されるアプリのコードセグメント
1006 2個目に確保されるアプリのコードセグメント
... 省略
2005 1個目に確保されるアプリのデータセグメント
2006 2個目に確保されるアプリのデータセグメント

次にLDTを使う場合です。

set_segmdesc(task->ldt + 0, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60);
set_segmdesc(task->ldt + 1, segsiz - 1,      (int) q, AR_DATA32_RW + 0x60);

アプリケーションのセグメントはLDTを使ってこのように確保していきます。 セレクタ値を計算で使って相対的に決めている前バージョンよりコードはわかりやすくなっています。

LDTは結構上の方に書いていますが、gdt + TASK_GDT0 + MAX_TASKS + iの位置に配置されます。

例:TSSがGDTの3の位置のアプリの場合,

  • index = 5
  • セレクタ値(sel) = 5 * 8
  • ldtの位置 = TASK_GDT0 + MAX_TASKS = 1005
  • コードセグメント = ldtの0番目の位置
  • データセグメント = ldtの1番目の位置

LDT導入後のGDT内容は以下のようになります。。

index content
1003 taska用(アプリはないので使っていない)
1004 idle用(アプリはないので使っていない)
1005 1個目に確保されるアプリのLDTへの参照
1006 2個目に確保されるプリのLDTへの参照
... 省略

ちなみにLDTです。これはタスクごとに作成されます。

index content
0 アプリのコードセグメント
1 アプリのデータセグメント

アプリケーションの起動

void start_app(int eip, int cs, int esp, int ds, int *tss_esp0);
  • LDT導入前:start_app(0x1b, task->sel + 1000 * 8, esp, task->sel + 2000 * 8, &(task->tss.esp0));
  • LDT導入後:start_app(0x1b, 0 * 8 + 4 , esp, 1 * 8 + 4 , &(task->tss.esp0));

LDT導入後はどのアプリケーションもセレクタ値は

  • cs:0 * 8 + 4
  • ds:1 * 8 + 4

となります。導入前と比べてどうでしょうか。導入前はセレクタ値を加工してタスクごとに指定するセグメントが変わるようになっていますが、LDT導入後は固定値になっています。 これは衝突しないのか?と疑問に思うかもしれませんが、このセグメントレジスタの指す先はGDTではなくタスクごとに用意されるLDTなので衝突することはありません。

なぜGDTやLDTのセレクタ値は8倍したり4を加算したりするのか

そもそもなぜ8をかけているかというと、この本には解説されていませんが、

8倍の理由はセグメント番号の下位3bitは別の意味があって、ここでは0にしておかないと行けないからです。

p135(day7)より

一旦ここでセグメントの下位3bitを踏まえたセレクタ値について説明します。

セレクタ値の構造は以下の様になっています。

31       23       15       7        0
+--------+--------+--------+--------+
|             index            |T|RP|
+--------+--------+--------+--------+
図中の略称 正式名称 長さ(bit) 意味
index 13 DTの位置を示すindex
RP RPL:Requested Privilege Level 2 必要権限
T table indicator 1 0=GDT 1=LDT

具体例を書き起こしてみます。

GDTを指す場合:sel = index * 8

index sel(dec) sel(binary)
0 0 0b00000
1 8 0b01000
2 16 0b10000
3 24 0b11000
4 32 0b20000

LDTを指す場合:sel = index * 8 + 4

index sel(dec) sel(binary)
0 4 0b00100
1 12 0b01100
2 20 0b10100
3 28 0b11100
4 36 0b20100
  • 8を掛けることで下位3bitが全て0になる。
  • 8を掛けた後に4を足すことでtable indicatorの位置が1になる
    • 下位2bitは0のまま

表に起こすことで上記の3点の理解が深まると思います。

RPLの役目について

RPLは0が最強で3が最弱です。アプリケーションは通常3,OSは0のレベルで動作します。 この権限の設定はGDTにもあり、GDTのアクセス権の6,7bit目に当たります。(day6で解説しています)RPLの値がGDTの権限属性に満たない場合は一般保護例外が発生します。

アプリケーションからOSのセグメントに不正にアクセスすることはできませんが、 代理でOSに処理をさせる様な挙動、例えばコールゲート経由でOSに不正なアドレスを渡すと、OSの権限で動いてしまうので不正なアドレス操作をする恐れがあります。

RPLはこれを防ぐためにあるようです。

はじめての486の10章セキュリティによると、 RPLの設定はハードウェアが自動で行うのではなく、OSが責任をもつようです。

ところで、要求者特権レベルtのチェックは486が行うのですが、設定の作業はオペレーティングシステムが行わなければなりません。

ちなみに現時点でhariboteOSにはこのRPLの設定機構はありません

LDTは現在使われていない?

LDTは、ハードウェアに夜タスク切り替え機構のために提供されたが、OSメーカーが採用しなかった。現在のプログラムは仮想メモリによって隔離されており、LDTは使われていない。

低レベルプログラミングP52より

30日OS自作本は結構古い本なので、こういう最近の低レイヤー情報はありがたい。

低レベルプログラミング

低レベルプログラミング

参考リンク

私が知りたいことが全て書いてあった。 http://softwaretechnique.jp/OS_Development/kernel_loader2.html

毎度お世話になってる486本

感想

LDTは新要素なので486本を読み直したりググりまくって時間をかけたがあまり難しい概念ではない様に思います。ただ最近使われていないというのはちょと驚き。

OS自作本,486本をそろそろ読み終えるが、次に読みたい本の候補が決まってきた

  • Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識
  • はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ
  • 低レベルプログラミング
  • 詳解Linuxカーネル

うーん1冊消化するたびに積ん読が何冊も増えるような?w 本棚が爆発するんじゃないでしょうか。

Effective Javaも読みたいし、Deep Learningもかじりたい(超欲張り)。 秋に情報安全支援士受けるんでネットワークやセキュリティの総復習もしたい。

  • Effective java 3rd
  • ゼロから作るDeep Learning(続編もきましたね!)
  • コンピューターネットワーク
  • ソースコードで体感するネットワークの仕組み
  • 体系的に学ぶ 安全なWebアプリケーションの作り方 第2版
  • ルーター自作でわかるパケットの流れ

うーむ時間は有限なので効率的に生きたいですね。 平日の時間投入はこれ以上は難しそうなので休日頑張るしかないな。