読者です 読者をやめる 読者になる 読者になる

φ(.. )メモシテオコウ x86_64のセグメントディスクリプタ(主にGDTで確認)

kernel linux x86_64

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()にて呼んでいる。
何はともあれ、スレッド関連の処理から呼ばれますね。

ということで、まとめ。

  • セグメントディスクリプタの1エントリのサイズは64bitで32bitのデータを2個持つ
  • Linuxカーネルはgdtをcpuコア数分持ち、宣言&初期化時にカーネル空間とユーザー空間用のCS・DSセグメント、カーネル空間とユーザー空間用のIA-32e Modeを設定
  • TLSはプロセスが切り替わるときや、スレッドの作成時に設定する(process_64.cだとIA-32e Modeの時のみ)