27日目の記事になります。というよりほとんどLDTの解説になります。
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自作本は結構古い本なので、こういう最近の低レイヤー情報はありがたい。
- 作者: Igor Zhirkov,吉川邦夫
- 出版社/メーカー: 翔泳社
- 発売日: 2018/01/19
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (3件) を見る
参考リンク
私が知りたいことが全て書いてあった。 http://softwaretechnique.jp/OS_Development/kernel_loader2.html
毎度お世話になってる486本
32ビットコンピュータをやさしく語る はじめて読む486 (アスキー書籍)
- 作者: 蒲地輝尚
- 出版社/メーカー: 角川アスキー総合研究所
- 発売日: 2014/10/21
- メディア: Kindle版
- この商品を含むブログを見る
感想
LDTは新要素なので486本を読み直したりググりまくって時間をかけたがあまり難しい概念ではない様に思います。ただ最近使われていないというのはちょと驚き。
OS自作本,486本をそろそろ読み終えるが、次に読みたい本の候補が決まってきた
うーん1冊消化するたびに積ん読が何冊も増えるような?w 本棚が爆発するんじゃないでしょうか。
Effective Javaも読みたいし、Deep Learningもかじりたい(超欲張り)。 秋に情報安全支援士受けるんでネットワークやセキュリティの総復習もしたい。
- Effective java 3rd
- ゼロから作るDeep Learning(続編もきましたね!)
- コンピューターネットワーク
- ソースコードで体感するネットワークの仕組み
- 体系的に学ぶ 安全なWebアプリケーションの作り方 第2版
- ルーター自作でわかるパケットの流れ
うーむ時間は有限なので効率的に生きたいですね。 平日の時間投入はこれ以上は難しそうなので休日頑張るしかないな。