x86_64をちゃんと覚えようということで資料はIntel®のSDM(使ったのはIntel® 64 and IA-32 ArchitecturesSoftware Developer’s Manual Volume 3 (3A, 3B & 3C):System Programming Guide)とLinuxのカーネルを見つつ。見ているカーネルのバージョンはv3.9.1。
セグメントディスクリプタの構造はSDMのチャプター3.4.5「Segment Descriptors」に書かれていて、32bitのデータ2個で64bitという感じになっている。
1つ目の32bitデータはこのような内容。
範囲 | サイズ(bit) | 内容 |
0-15 | 16 | Segment Limit 15:00 |
16-31 | 16 | Base Address 15:00 |
表1
もう一つの32bitデータはこちら。
範囲 | サイズ(bit) | 内容 |
0-7 | 8 | Base 23:16 |
8-11 | 4 | Type |
12 | 1 | S |
13-14 | 2 | DPL |
15 | 1 | P |
16-19 | 4 | Seg.Limit 19:16 |
20 | 1 | AVL |
21 | 1 | L |
22 | 1 | D/B |
23 | 1 | G |
24-31 | 8 | Base 31:24 |
表2
これがセグメントディスクリプタ。
それでLinuxがセグメントディスクリプタをどう表現しているかと言うとarch/x86/include/asm/desc_defs.hにてdesc_struct構造体として定義。
21/* 8 byte segment descriptor */ 22struct desc_struct { 23 union { 24 struct { 25 unsigned int a; 26 unsigned int b; 27 }; 28 struct { 29 u16 limit0; 30 u16 base0; 31 unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1; 32 unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8; 33 }; 34 }; 35} __attribute__((packed));
intが2個あるほうの無名構造体でaが表1、bが表2のデータを扱います。もう一個の方はbitのサイズ指定をして名前でアクセスできるようにしているだけ。
GDTはどうかというと、arch/x86/include/asm/desc.hで構造体の配列をメンバーとするgdt_page構造体として定義。
42struct gdt_page { 43 struct desc_struct gdt[GDT_ENTRIES]; 44} __attribute__((aligned(PAGE_SIZE)));
配列のサイズはarch/x86/include/asm/segment.hで定義されていて、値は16。
183#define GDT_ENTRIES 16
そして、16個のエントリを持ったGDTをどのように扱っているかというところで、まずは初期化部分。
91DEFINE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page) = { .gdt = { 92#ifdef CONFIG_X86_64 93 /* 94 * We need valid kernel segments for data and code in long mode too 95 * IRET will check the segment types kkeil 2000/10/28 96 * Also sysret mandates a special GDT layout 97 * 98 * TLS descriptors are currently at a different place compared to i386. 99 * Hopefully nobody expects them at a fixed place (Wine?) 100 */ 101 [GDT_ENTRY_KERNEL32_CS] = GDT_ENTRY_INIT(0xc09b, 0, 0xfffff), 102 [GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(0xa09b, 0, 0xfffff), 103 [GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(0xc093, 0, 0xfffff), 104 [GDT_ENTRY_DEFAULT_USER32_CS] = GDT_ENTRY_INIT(0xc0fb, 0, 0xfffff), 105 [GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f3, 0, 0xfffff), 106 [GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xa0fb, 0, 0xfffff), 107#else 108 [GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(0xc09a, 0, 0xfffff), 109 [GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(0xc092, 0, 0xfffff), 110 [GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xc0fa, 0, 0xfffff), 111 [GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f2, 0, 0xfffff), 112 /* 113 * Segments used for calling PnP BIOS have byte granularity. 114 * They code segments and data segments have fixed 64k limits, 115 * the transfer segment sizes are set at run time. 116 */ 117 /* 32-bit code */ 118 [GDT_ENTRY_PNPBIOS_CS32] = GDT_ENTRY_INIT(0x409a, 0, 0xffff), 119 /* 16-bit code */ 120 [GDT_ENTRY_PNPBIOS_CS16] = GDT_ENTRY_INIT(0x009a, 0, 0xffff), 121 /* 16-bit data */ 122 [GDT_ENTRY_PNPBIOS_DS] = GDT_ENTRY_INIT(0x0092, 0, 0xffff), 123 /* 16-bit data */ 124 [GDT_ENTRY_PNPBIOS_TS1] = GDT_ENTRY_INIT(0x0092, 0, 0), 125 /* 16-bit data */ 126 [GDT_ENTRY_PNPBIOS_TS2] = GDT_ENTRY_INIT(0x0092, 0, 0), 127 /* 128 * The APM segments have byte granularity and their bases 129 * are set at run time. All have 64k limits. 130 */ 131 /* 32-bit code */ 132 [GDT_ENTRY_APMBIOS_BASE] = GDT_ENTRY_INIT(0x409a, 0, 0xffff), 133 /* 16-bit code */ 134 [GDT_ENTRY_APMBIOS_BASE+1] = GDT_ENTRY_INIT(0x009a, 0, 0xffff), 135 /* data */ 136 [GDT_ENTRY_APMBIOS_BASE+2] = GDT_ENTRY_INIT(0x4092, 0, 0xffff), 137 138 [GDT_ENTRY_ESPFIX_SS] = GDT_ENTRY_INIT(0xc092, 0, 0xfffff), 139 [GDT_ENTRY_PERCPU] = GDT_ENTRY_INIT(0xc092, 0, 0xfffff), 140 GDT_STACK_CANARY_INIT 141#endif 142} }; 143EXPORT_PER_CPU_SYMBOL_GPL(gdt_page);
cpuコアごとにgdt持ちますよね~という当たり前なところは置いておいて、実際の初期化部、x86_64の場合は6個のエントリのみ設定される。x86_32と比べるとあっさりした感じが。
TLSに関してはgdt構造体の初期化時に設定しないで別途行われる。
ここの初期化処理は結局こういうことをしてて、GDT_ENTRY_XXXは配列のインデックスになる形。
gdt_page.gdt[GDT_ENTRY_KERNEL32_CS] = GDT_ENTRY_INIT(0xc09b, 0, 0xfffff)
さらにGDT_ENTRY_INITの中で変数a、bにデータをセットするように展開されますが。。。
gdtのインデックス0のエントリはi486等と同じくNULLエントリ。
GDT_ENTRY_KERNEL32_CSとかGDT_ENTRY_DEFAULT_USER32_CSはLbitが0にセットされるのでIA-32e Mode用になる。
そして、GDT_ENTRY_INITはarch/x86/include/asm/desc_defs.hでマクロ定義されている。
37#define GDT_ENTRY_INIT(flags, base, limit) { { { \ 38 .a = ((limit) & 0xffff) | (((base) & 0xffff) << 16), \ 39 .b = (((base) & 0xff0000) >> 16) | (((flags) & 0xf0ff) << 8) | \ 40 ((limit) & 0xf0000) | ((base) & 0xff000000), \ 41 } } }
処理としては素直な内容。
さて、TLSはどのように処理されるかというとarch/x86/include/asm/desc.hのnative_load_tls()にて実施。
245static inline void native_load_tls(struct thread_struct *t, unsigned int cpu) 246{ 247 struct desc_struct *gdt = get_cpu_gdt_table(cpu); 248 unsigned int i; 249 250 for (i = 0; i < GDT_ENTRY_TLS_ENTRIES; i++) 251 gdt[GDT_ENTRY_TLS_MIN + i] = t->tls_array[i]; 252}
TLSはThread Local Storageなのでスレッド構造体を引数で受け取るのはさもありなんと。cpuもマルチコアの場合は必須ですしね。
ただ、native_load_tls()はこの名前を使って使用されず、マクロのload_TLSが別名として定義されているのでload_TLSを使っています。
arch/x86/include/asm/desc.hにて定義。
101#define load_TLS(t, cpu) native_load_tls(t, cpu)
この他、native_load_gdt、native_load_idtなどもload_gdt、load_idtと言った名前が別途定義されている。
それで、このload_TLSの使用場所はというと、_switch_to()などで。プロセス切り替わるし、TLSも当然変えるタイミングですな。他にはdo_set_thread_area()、regset_tls_set()から呼ばれるset_tls_desc()にて呼んでいる。
何はともあれ、スレッド関連の処理から呼ばれますね。
ということで、まとめ。