44
NetBSD/i386 割り込みベクタテーブル スタート低レイヤー #4 リベンジの変 #start_printf @kusabanachi

NetBSD/i386 割り込みベクタテーブル

Embed Size (px)

DESCRIPTION

NetBSD-6.0.1のソースコードリーディング。

Citation preview

Page 1: NetBSD/i386 割り込みベクタテーブル

NetBSD/i386 割り込みベクタテーブル

   スタート低レイヤー #4 リベンジの変 #start_printf

                   @kusabanachi

Page 2: NetBSD/i386 割り込みベクタテーブル

前回のあらすじ

Page 3: NetBSD/i386 割り込みベクタテーブル

前回のあらすじ

libcのputcharで、gdbで逆アセンブル表示

Dump of assembler code for function write:=> 0xbbaf9080 <+0>: mov $0x4,%eax 0xbbaf9085 <+5>: int $0x80...

write関数で int $0x80 を呼んでいた

=>

int $0x80 の先はどこだろうか?

Page 4: NetBSD/i386 割り込みベクタテーブル

IDTの0x80番を見る

イメージ図

Page 5: NetBSD/i386 割り込みベクタテーブル

前回のあらすじ

IA32 マニュアルによると

INT n 命令は、割り込みベクタ番号n のハンドラをコールする命令

Page 6: NetBSD/i386 割り込みベクタテーブル

前回のあらすじ

0x80番の割り込みハンドラコール = システムコールの入口(ユーザーモードからカーネルモードへの移動)

と予想

割り込みベクタテーブルのソースを見れば何か分かりそう→ 割り込みベクタテーブルの場所、見つかりませんでした!(前回終)

Page 7: NetBSD/i386 割り込みベクタテーブル

i386の割り込みベクタテーブル

前回、ソースコードをgrepしても手掛かりが見つけられなかったので、CPUのマニュアルを見てみます

Page 8: NetBSD/i386 割り込みベクタテーブル

i386の割り込みベクタテーブル

IA-32 インテル ® アーキテクチャ ソフトウェア・デベロッパーズ・マニュアル 下巻 にそれっぽい内容

IA-32 システムレベルのレジスタおよびデータ構造

Page 9: NetBSD/i386 割り込みベクタテーブル

i386の割り込みベクタテーブル

関連する箇所だけ、ざっくり抜粋するとこんな感じ

Page 10: NetBSD/i386 割り込みベクタテーブル

=> つまり、、、

i386の割り込みベクタテーブル

マニュアルの 2.1.4.割り込み / 例外処理 より

外部割り込み、ソフトウェア割り込み、例外は、割り込みディスクリプタ・テーブル (IDT)を通じて処理される。IDT には、割り込みハンドラと例外ハンドラへのアクセスを提供するゲート・ディスクリプタが集められている。...IDT のベースのリニアアドレスは、IDT レジスタ(IDTR)に入っている。

IDTがi386の割り込みベクタテーブルのようだIDTはゲート・ディスクリプタの集まりゲート・ディスクリプタがハンドラへのアクセスを提供するIDTの場所はIDTRに書いてある

Page 11: NetBSD/i386 割り込みベクタテーブル

i386の割り込みベクタテーブル

IDTRには LIDT命令で値を入れる

=> LIDT命令を使っている場所を探せば、IDTが見つかりそう

Page 12: NetBSD/i386 割り込みベクタテーブル

LIDT命令を探す

Page 13: NetBSD/i386 割り込みベクタテーブル

システムの起動時とリセット時にIDTを設定しているようだ

LIDT命令を探す

i386のディレクトリ下に以下のLIDT命令を呼び出す関数の定義が見つかります (以降、lidt関数と呼ぶ)

src/sys/arch/i386/i386/cpufunc.S 68ENTRY(lidt) movl 4(%esp), %eax /* 第一引数をeaxに入れる */ lidt (%eax) /* eaxの参照先をIDTRへ */ ret /* 戻る */END(lidt)

上記lidt関数の呼び出し元で見つかるのは、src/sys/arch/i386/i386/machdep.c

内の1140行 cpu_init_idt() の中1696行 cpu_reset() の中

Page 14: NetBSD/i386 割り込みベクタテーブル

LIDT命令を探す

本当にlidt関数がシステム起動時に呼ばれているか、スタート低レイヤーのWikiページのbacktraceを調べるコードを真似て確認しました

lidtの名前でダミーの関数を作り、呼出し元の情報を得る

void lidt(struct region_descriptor *region) struct i386_frame *frame; // lidt関数の呼出し元を調べる asm volatile("movl %%ebp, %0" : "=r" (frame)); lidt_retaddr1 = frame­>f_retaddr; frame = frame­>f_frame; lidt_retaddr2 = frame­>f_retaddr; frame = frame­>f_frame; lidt_retaddr3 = frame­>f_retaddr; // lidt関数が呼び出された回数も調べてみる ++lidt_count; // 本来のlidt関数を呼ぶ (lidt_llにリネームしてある) lidt_ll(region);

Page 15: NetBSD/i386 割り込みベクタテーブル

LIDT命令を探す

ビルドしたカーネルを起動して、ddbを使って、システム起動時に取得しているはずの値を見る

lidt_countの値より、lidt関数は1度だけ呼ばれている

lidt_retaddr1-3 の値より呼出し元のアドレスが分かるbegin -> init386 -> cpu_init_idt -> lidt の順でcallされている ($ objdump -S --start-address 0xc05a4000 kernel_file でアドレスから対応する関数を調べる)

Page 16: NetBSD/i386 割り込みベクタテーブル

LIDT命令を探す

システムの起動時にIDTを設定していることと、その関数が確認できました

lidt関数の呼出し元を追い、IDTの中身を見ます

Page 17: NetBSD/i386 割り込みベクタテーブル

IDTの内容を見る

begin -> init386 -> cpu_init_idt -> lidt の中の" init386 -> cpu_init_idt " の間で、IDTの設定に関係ある箇所を見ます

Page 18: NetBSD/i386 割り込みベクタテーブル

IDTの内容を見る (cpu_init_idt内)

lidt関数から遡って見ていきます

src/sys/arch/i386/i386/machdep.c 1135void cpu_init_idt(void)#ifndef XEN struct region_descriptor region; setregion(&region, pentium_idt, NIDT * sizeof(idt[0]) ­ 1); lidt(&region);#else /* XEN */ ... // こっちは今回追わない#endif /* !XEN */

struct region_descriptor のポインタが lidt関数の引数

Page 19: NetBSD/i386 割り込みベクタテーブル

IDTの内容を見る (cpu_init_idt内)

src/sys/arch/i386/include/segments.h 177struct region_descriptor unsigned rd_limit:16; /* segment extent */ unsigned rd_base:32; /* base address */ __packed;

lidt関数の引数の構造体

CPUマニュアルより、IDTRに入る値16bitのリミットは、IDTのバイト数 - 1

32bitのベースは、IDT先頭のリニアアドレス(仮想アドレス)

lidt関数の引数は、IDTRに書き込む値の形式になっている

Page 20: NetBSD/i386 割り込みベクタテーブル

IDTの内容を見る (cpu_init_idt内)

src/sys/arch/i386/i386/machdep.c 1135 (さっき見た関数)void cpu_init_idt(void)#ifndef XEN struct region_descriptor region; setregion(&region, pentium_idt, NIDT * sizeof(idt[0]) ­ 1); lidt(&region);#else /* XEN */ ...#endif /* !XEN */

src/sys/arch/i386/i386/machdep.c 1096voidsetregion(struct region_descriptor *rd, void *base, size_t limit) rd­>rd_limit = (int)limit; rd­>rd_base = (int)base;

setregion は2つの引数を、IDTRに書き込む構造体に代入するだけlimit値 = NIDT * sizeof(idt[0]) - 1; // IDTのサイズ-1

base値 = pentium_idt;      // pentium_idt が IDT先頭のアドレス

Page 21: NetBSD/i386 割り込みベクタテーブル

IDTの内容を見る (init386内)

cpu_init_idt() の呼出し元の init386() を見る

src/sys/arch/i386/i386/machdep.c 1497voidinit386(paddr_t first_avail)... pmap_kenter_pa(idt_vaddr, idt_paddr, VM_PROT_READ|VM_PROT_WRITE, 0); pmap_update(pmap_kernel()); memset((void *)idt_vaddr, 0, PAGE_SIZE); #ifndef XEN idt_init(); idt = (struct gate_descriptor *)idt_vaddr; pmap_kenter_pa(pentium_idt_vaddr, idt_paddr, VM_PROT_READ, 0); pmap_update(pmap_kernel()); pentium_idt = (union descriptor *)pentium_idt_vaddr;...

pmapに関連する関数が呼ばれている

Page 22: NetBSD/i386 割り込みベクタテーブル

IDTの内容を見る (init386内)

pmap_kenter_pa(idt_vaddr, idt_paddr, VM_PROT_READ|VM_PROT_WRITE, 0);pmap_update(pmap_kernel());

PMAP(9) より 意訳

 -- 仮想メモリシステムのマシン依存部

void pmap_kenter_pa(vaddr_t va, paddr_t pa, vm_prot_t prot, u_int flags)物理アドレス pa を仮想アドレス va に prot の保護属性で"unmanaged" マッピングする。

void pmap_update(pmap_t pmap)pmap モジュールに、正しい物理マッピングが必要なタイミングを通知する。これにより、全ての遅延されていた仮想-to-物理マッピングは更新される。

Page 23: NetBSD/i386 割り込みベクタテーブル

IDTの内容を見る (init386内 "unmanaged" マッピングとは)

引き続き PMAP(9) より 意訳

pmapモジュールは仮想記憶のページ毎の変更/参照の情報を記録するこの情報はページデーモンがページをスキャンした際に使う参照情報は、ページが再配置対象か否か変更情報は、ページを記憶装置に書き戻す必要があるか

"unmanaged" マッピングのページは、変更/参照の情報の記録対象外"unmanaged" マッピングの使用は限定されるべき割り込みコンテキストの中での使用仮想記憶システムで管理しない物理アドレスのマッピング

"unmanaged" マッピングはカーネルの仮想アドレスにのみマッピング可

つまり、pmap_kenter_pa は、IDTを配置するメモリのために再配置の対象から外れるページをマッピングしているようだ

Page 24: NetBSD/i386 割り込みベクタテーブル

IDTの内容を見る (init386内)

src/sys/arch/i386/i386/machdep.c 1497 (もう一度戻って)...vaddr_t idt_vaddr;paddr_t idt_paddr;...voidinit386(paddr_t first_avail)... pmap_kenter_pa(idt_vaddr, idt_paddr, VM_PROT_READ|VM_PROT_WRITE, 0); // idt_vaddr(virtual) と idt_paddr(physical) を マッピング // ReadWrite属性 pmap_update(pmap_kernel()); // 遅延されたマッピングを更新する memset((void *)idt_vaddr, 0, PAGE_SIZE); // マップしたページはゼロクリアする...

マッピングされるidt_vaddrとidt_paddrは、それぞれ、仮想メモリと物理メモリの利用できるメモリページのアドレスが入っている変数

Page 25: NetBSD/i386 割り込みベクタテーブル

IDTの内容を見る (init386内)

src/sys/arch/i386/i386/machdep.c 1501voidinit386(paddr_t first_avail)... // 前ページの続き idt_init(); // この関数の中身は空 idt = (struct gate_descriptor *)idt_vaddr; // さっきマッピングしたアドレスを(キャストして)idt変数に割り当てる pmap_kenter_pa(pentium_idt_vaddr, idt_paddr, VM_PROT_READ, 0); // さっきマッピングした物理アドレス idt_paddr(physical) を、再度マッピング // 今度はReadOnly属性 pmap_update(pmap_kernel()); // 遅延されたマッピングを更新する pentium_idt = (union descriptor *)pentium_idt_vaddr; // マッピングしたアドレスを(キャストして) // lidt関数の呼出しでIDTのベースとして扱われる変数に割り当てる...

IDT中身の設定はReadWrite属性のメモリページから行い、IDTRにはReadOnly属性のメモリページのアドレスを書いている

Page 26: NetBSD/i386 割り込みベクタテーブル

IDTの内容を見る (init386内)

src/sys/arch/i386/i386/machdep.c 1537voidinit386(paddr_t first_avail)... /* exceptions */ for (x = 0; x < 32; x++) idt_vec_reserve(x); setgate(&idt[x], IDTVEC(exceptions)[x], 0, SDT_SYS386IGT, (x == 3 || x == 4) ? SEL_UPL : SEL_KPL, GSEL(GCODE_SEL, SEL_KPL)); // ↑ 0~32 番の例外 /* new­style interrupt gate for syscalls */ idt_vec_reserve(128); setgate(&idt[128], &IDTVEC(syscall), 0, SDT_SYS386IGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL)); // ↑ 128番 → これが 0x80 !! idt_vec_reserve(0xd2); setgate(&idt[0xd2], &IDTVEC(svr4_fasttrap), 0, SDT_SYS386IGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL)); // ↑ 0xd2番... cpu_init_idt(); // ← IDTRにIDTのアドレスを書く

idt_vec_reserve(ベクタ番号) → setgate() の流れ

Page 27: NetBSD/i386 割り込みベクタテーブル

IDTの0x80番を見る

idt_vec_reserve(0x80) → setgate() を見ます

Page 28: NetBSD/i386 割り込みベクタテーブル

IDTの0x80番を見る

idt_vec_reserve() を見ます

idt_vec_reserve(128);setgate(&idt[128], &IDTVEC(syscall), 0, SDT_SYS386IGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));

src/sys/arch/x86/x86/idt.c 105voididt_vec_reserve(int vec) int result; KASSERT(mutex_owned(&cpu_lock) || !mp_online); // ↑ 現在のLWPまたはCPUがmutexを保持している // または マルチプロセッサでない(mp_online が false)ので排他不要 result = idt_vec_alloc(vec, vec); // ↑ ベクタ番号を引数にallocを呼ぶ if (result != vec) panic("%s: failed to reserve vec %d", __func__, vec);

Page 29: NetBSD/i386 割り込みベクタテーブル

IDTの0x80番を見る

src/sys/arch/x86/x86/idt.c 83/* * 与えられた範囲内で、IDTベクタを1スロット割り当てる. * 初期ブート時のシングルスレッドでなければ、cpuのロックが行われる. */intidt_vec_alloc(int low, int high) int vec; KASSERT(mutex_owned(&cpu_lock) || !mp_online); for (vec = low; vec <= high; vec++) if (idt_allocmap[vec] == 0) /* idt_vec_free()はロックしないかもしれないので, membarする. */ membar_sync(); idt_allocmap[vec] = 1; // ↑ idt_allocmap[vec]でスロットの割り当てを管理して、 // 二重の割当てを避ける return vec; // ↑ 割り当てたベクタ番号を返す return 0;

Page 30: NetBSD/i386 割り込みベクタテーブル

type は が指定されている

IDTの0x80番を見る

setgate() を見ます

idt_vec_reserve(128);setgate(&idt[128], &IDTVEC(syscall), 0, SDT_SYS386IGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));

src/sys/arch/x86/x86/idt.c 1082voidsetgate(struct gate_descriptor *gd, void *func, int args, int type, int dpl, int sel) // 第一引数のidtのスロット(ゲート)の構造体に値を入れていく gd­>gd_looffset = (int)func; gd­>gd_selector = sel; gd­>gd_stkcpy = args; gd­>gd_xx = 0; gd­>gd_type = type; gd­>gd_dpl = dpl; gd­>gd_p = 1; gd­>gd_hioffset = (int)func >> 16;

src/sys/arch/i386/include/segments.h 229#define SDT_SYS386IGT 14 /* system 386 interrupt gate */

14 (interrupt gate)

Page 31: NetBSD/i386 割り込みベクタテーブル

IDTの0x80番を見る

CPUマニュアルより、IDTのゲートは3タイプあり、type 14(0x1110) の割り込みゲートが 0x80番のベクタには使われている

Page 32: NetBSD/i386 割り込みベクタテーブル

IDTの0x80番を見る

idt_vec_reserve(128);setgate(&idt[128], &IDTVEC(syscall), 0, SDT_SYS386IGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));

src/sys/arch/i386/i386/machdep.c 1121#define IDTVEC(name) __CONCAT(X, name)

&IDTVEC(syscall) は &Xsyscall

src/sys/arch/i386/include/segments.h 98#define SEL_UPL 3 /* user privilege level */

SEL_UPL はユーザー特権レベル

src/sys/arch/i386/include/segments.h 93,109,304#define GSEL(s,r) (((s) << 3) | r) /* a global selector */#define GCODE_SEL 1 /* Kernel code descriptor */#define SEL_KPL 0 /* kernel privilege level */

GSEL(GCODE_SEL, SEL_KPL)) はセグメント・セレクタ(次ページで説明)カーネルのコードセグメントに、カーネル特権レベルを指定してアクセスする

Page 33: NetBSD/i386 割り込みベクタテーブル

IDTの0x80番を見る (セグメントについて)

i386ではメモリがセグメントと呼ばれる複数のアドレス空間に割り当てられていて、セグメントとセグメント内のオフセットを指定してメモリアクセスすることができる。

セグメントのアドレスやアクセス権などはGDT(グローバル・ディスクリプタ・テーブル)が持っているセグメント・セレクタはGDTのインデックス。セグメントの指定に使うセグメント・セレクタの 3ビット以降がGDTのインデックスを示す。0-1ビットはセグメントで要求される特権レベル

Page 34: NetBSD/i386 割り込みベクタテーブル

IDTの0x80番を見る

セグメント指定のメモリアクセス

Page 35: NetBSD/i386 割り込みベクタテーブル

IDTの0x80番を見る (セグメントについて)

GDTもIDTと同様に初期化を行うが、GDTの初期化に関係するコードは今回詳しく見ません

src/sys/arch/i386/i386/machdep.c 1158/* make gdt gates and memory segments */setsegment(&gdt[GCODE_SEL].sd, 0, 0xfffff, SDT_MEMERA, SEL_KPL, 1, 1); // ↑ カーネルのコードセグメントのセット。ベースアドレス 0。リミット 4GBytesetsegment(&gdt[GDATA_SEL].sd, 0, 0xfffff, SDT_MEMRWA, SEL_KPL, 1, 1); // ↑ カーネルのデータセグメントのセット。ベースアドレス 0。リミット 4GByte

セグメントのベースがゼロなので、アクセス先のアドレスとセグメントの先頭からのオフセットは同じ値

Page 36: NetBSD/i386 割り込みベクタテーブル

IDTの0x80番を見る

仮想アドレス空間全体をセグメントにしている

Page 37: NetBSD/i386 割り込みベクタテーブル

IDTの0x80番を見る

idt_vec_reserve(128);setgate(&idt[128], &IDTVEC(syscall), 0, SDT_SYS386IGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));

src/sys/arch/x86/x86/idt.c 1082 (再度確認)voidsetgate(struct gate_descriptor *gd, void *func, int args, int type, int dpl, int sel) gd­>gd_looffset = (int)func; // セグメント内の下位16bitオフセット: Xsyscall の下位アドレス gd­>gd_selector = sel; // セグメント・セレクタ: カーネルのコードセグメント gd­>gd_stkcpy = args; // 移動前のスタックから移動後のスタックにコピーするデータサイズ: 0が入る gd­>gd_xx = 0; // 使用されない3bit gd­>gd_type = type; // ゲートのタイプ: 割り込みゲート gd­>gd_dpl = dpl; // 特権レベル: ユーザー特権レベル gd­>gd_p = 1; // このゲートが存在することを示すフラグ gd­>gd_hioffset = (int)func >> 16; // セグメント内の上位16bitオフセット: Xsyscallの上位アドレス

Page 38: NetBSD/i386 割り込みベクタテーブル

IDTの0x80番を見る

0x80番の設定を見たので、戻ります

src/sys/arch/i386/i386/machdep.c 1537voidinit386(paddr_t first_avail)... /* exceptions */ for (x = 0; x < 32; x++) idt_vec_reserve(x); setgate(&idt[x], IDTVEC(exceptions)[x], 0, SDT_SYS386IGT, (x == 3 || x == 4) ? SEL_UPL : SEL_KPL, GSEL(GCODE_SEL, SEL_KPL)); /* new­style interrupt gate for syscalls */ idt_vec_reserve(128); setgate(&idt[128], &IDTVEC(syscall), 0, SDT_SYS386IGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL)); idt_vec_reserve(0xd2); setgate(&idt[0xd2], &IDTVEC(svr4_fasttrap), 0, SDT_SYS386IGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL)); // ↑ ここまでIDTの0~32番、0x80番、0xd2番のゲートを設定... cpu_init_idt(); // ↑ この中で設定したIDTの先頭アドレスとリミットをIDTRに書き込む

Page 39: NetBSD/i386 割り込みベクタテーブル

まとめ

Page 40: NetBSD/i386 割り込みベクタテーブル

まとめ

システム起動時のIDT設定 1

IDTに使うページをマップする

Page 41: NetBSD/i386 割り込みベクタテーブル

まとめ

システム起動時のIDT設定 2

IDTのベクタの割り当て・設定

Page 42: NetBSD/i386 割り込みベクタテーブル

まとめ

システム起動時のIDT設定 3

IDTRへの書き込み

Page 43: NetBSD/i386 割り込みベクタテーブル

まとめ

アプリ実行時

Page 44: NetBSD/i386 割り込みベクタテーブル

まとめ

libc内の int 0x80 から Xsyscall が呼ばれるしくみを見ました