gdt, tss and switch_to

Author: Ben, Gemini
2026-04-15

Gemini talk

TSS 的功能

  • 特权级切换与堆栈保存:当处理器从用户态 (Ring 3) 进入内核态 (Ring 0) 时(例如发生中断或系统调用),TSS 提供了内核态所需的堆栈指针 (ESP0/RSP0)。没有它,CPU 将不知道把寄存器压入哪个堆栈,从而导致系统崩溃。
  • I/O 权限管理:TSS 包含一个 I/O 许可位图 (I/O Permission Bitmap),操作系统可以用它来精确控制用户进程可以访问哪些硬件端口。
  • 中断堆栈表 (IST):在 64 位模式下,TSS 引入了 IST,允许某些关键中断(如双重错误 Double Fault)使用独立、预定义的已知良好堆栈,确保即使内核堆栈溢出,系统也能处理异常。

TSS 与 GDT 的关系

TSS 本身并不是 GDT 的一部分,但它必须在 GDT 中拥有一个描述符 (TSS Descriptor) 才能被使用。

操作系统在内存中开辟一块空间定义 TSS。
在 GDT 中创建一个描述符,指向该空间的基地址和长度。
通过 LTR (Load Task Register) 指令将 GDT 中的选择子加载到处理器的 TR 寄存器 中,从而激活该 TSS。 

现代操作系统(如 Linux 或 Windows) 通常不再为每个进程创建单独的 TSS,而是为每个 CPU 核心维护一个全局 TSS。在进程切换时,内核只需动态更新这个全局 TSS 中的堆栈指针即可。

summary(by Ben)

TSS: task state segment: 在内存中,per task GDT: TSS 在内存中的地址存于 GDT 中,per CPU

任务切换时更新 TSS 即可

contents in TSS(by Ben)

简单来说 TSS 中存了当前 task 所需的所有状态,rsp rbp 等等

     15                   0
     +--------------------+
     | Task LDT Selector  | 42
     | DS Selector        | 40
     | SS Selector        | 38
     | CS Selector        | 36
     | ES Selector        | 34
     | DI                 | 32
     | SI                 | 30
     | BP                 | 28
     | SP                 | 26
     | BX                 | 24
     | DX                 | 22
     | CX                 | 20
     | AX                 | 18
     | FLAG Word          | 16
     | IP (Entry Point)   | 14
     | SS2                | 12
     | SP2                | 10
     | SS1                | 8
     | SP1                | 6
     | SS0                | 4
     | SP0                | 2
     | Previous Task Link | 0
     +--------------------+

Figure 10-10. 16-Bit TSS Format
> Volume 3A, 10.6, 16-BIT TASK-STATE SEGMENT (TSS)

64 位中还包括 I/O map base address

    31                      15                   0
    +----------------------+----------------------+
    | I/O Map Base Address | Reserved             | 100
    | Reserved                                    | 96 
    | Reserved                                    | 92 
    | IST7 (upper 32 bits)                        | 88 
    | IST7 (lower 32 bits)                        | 84 
    | IST6 (upper 32 bits)                        | 80 
    | IST6 (lower 32 bits)                        | 76
    | IST5 (upper 32 bits)                        | 72
    | IST5 (lower 32 bits)                        | 68
    | IST4 (upper 32 bits)                        | 64
    | IST4 (lower 32 bits)                        | 60
    | IST3 (upper 32 bits)                        | 56
    | IST3 (lower 32 bits)                        | 52
    | IST2 (upper 32 bits)                        | 48
    | IST2 (lower 32 bits)                        | 44
    | IST1 (upper 32 bits)                        | 40
    | IST1 (lower 32 bits)                        | 36
    | Reserved                                    | 32
    | Reserved                                    | 28
    | RSP2 (upper 32 bits)                        | 24
    | RSP2 (lower 32 bits)                        | 20
    | RSP1 (upper 32 bits)                        | 16
    | RSP1 (lower 32 bits)                        | 12
    | RSP0 (upper 32 bits)                        | 8
    | RSP0 (lower 32 bits)                        | 4
    | Reserved                                    | 0
    +----------------------+----------------------+
Reserved bits. Set to 0.

> Volume 3A, 10.7, TASK MANAGEMENT IN 64-BIT MODE
// include/asm-x86/processor_64.h 
struct tss_struct {
	u32 reserved1;
	u64 rsp0;	
	u64 rsp1;
	u64 rsp2;
	u64 reserved2;
	u64 ist[7];
	u32 reserved3;
	u32 reserved4;
	u16 reserved5;
	u16 io_bitmap_base;
	/*
	 * The extra 1 is there because the CPU will access an
	 * additional byte beyond the end of the IO permission
	 * bitmap. The extra byte must be all 1 bits, and must
	 * be within the limit. Thus we have:
	 *
	 * 128 bytes, the bitmap itself, for ports 0..0x3ff
	 * 8 bytes, for an extra "long" of ~0UL
	 */
	unsigned long io_bitmap[IO_BITMAP_LONGS + 1];
} __attribute__((packed)) ____cacheline_aligned;

TSS 中的 SP 指用于记录内核栈地址,而不记录用户栈地址,用户栈地址存于内核栈

kernel context switch(TODO)

当 CPU 在两个内核任务(即进程的内核态部分)之间切换时,由于不涉及特权级从 Ring 3 到 Ring 0 的跳转,TSS 此时是不参与的。 内核栈指针(SP)的存储位置取决于该任务的状态:

1. 对于正在运行的任务

  • 存储位置: 直接存储在 CPU 的物理寄存器 RSP (x86_64) 或 ESP (x86_32) 中。
  • CPU 指令(如 push, pop, call)直接操作这个寄存器。

2. 对于被切走(挂起)的任务

当内核执行进程切换(Context Switch)时,被换下的任务的内核栈指针需要找个地方存起来,以便下次换回来。

  • 存储位置: 存储在该任务的 进程控制块 (PCB) 中。
  • 在 Linux 内核中,具体的字段是:task_struct -> thread -> sp。

3. 切换过程的“接力”

当 Linux 调用 __switch_to(汇编部分为 switch_to)切换进程 A 和 B 时,发生了以下动作:

  1. 保存 A 的栈顶:内核将当前的 RSP 寄存器值压入 A 的内核栈,或者直接保存到 A->thread.sp 中。
  2. 加载 B 的栈顶:内核从 B->thread.sp 中读取之前保存的值,并将其写入物理寄存器 RSP。
  3. 接管 CPU:一旦 RSP 指向了 B 的内核栈,接下来的 ret 指令就会从 B 的栈中弹出返回地址。此时,CPU 正式进入进程 B 的执行流。

4. 关键点:TSS 在这里做什么?

虽然在两个内核任务切换时,CPU 不从 TSS 读取 RSP0,但 Linux 内核顺便会做一件事:

  • 内核会更新 TSS 中的 RSP0 字段,将其设置为进程 B 的内核栈顶。
  • 目的:这是为了“未雨绸缪”。如果进程 B 稍后返回了用户态,而在用户态运行时突然发生了一个中断,CPU 硬件需要通过 TSS 知道该跳回到进程 B 的哪个内核栈位置。

总结

  • 活动任务:在 RSP 寄存器里。
  • 睡眠任务:在 task_struct->thread.sp 内存里。
  • TSS 的作用:仅作为“入口指南”,记录如果从用户态进入内核态,该用哪个栈。

my trace(by Ben)

#define switch_to(prev,next,last) \
	asm volatile(SAVE_CONTEXT						    \
		     "movq %%rsp,%P[threadrsp](%[prev])\n\t" /* save RSP */	  \
		     "movq %P[threadrsp](%[next]),%%rsp\n\t" /* restore RSP */	  \
		     "call __switch_to\n\t"					  \
		     ".globl thread_return\n"					\
		     "thread_return:\n\t"					    \
		     "movq %%gs:%P[pda_pcurrent],%%rsi\n\t"			  \
		     "movq %P[thread_info](%%rsi),%%r8\n\t"			  \
		     LOCK_PREFIX "btr  %[tif_fork],%P[ti_flags](%%r8)\n\t"	  \
		     "movq %%rax,%%rdi\n\t" 					  \
		     "jc   ret_from_fork\n\t"					  \
		     RESTORE_CONTEXT						    \
		     : "=a" (last)					  	  \
		     : [next] "S" (next), [prev] "D" (prev),			  \
		       [threadrsp] "i" (offsetof(struct task_struct, thread.rsp)), \
		       [ti_flags] "i" (offsetof(struct thread_info, flags)),\
		       [tif_fork] "i" (TIF_FORK),			  \
		       [thread_info] "i" (offsetof(struct task_struct, stack)), \
		       [pda_pcurrent] "i" (offsetof(struct x8664_pda, pcurrent))   \
		     : "memory", "cc" __EXTRA_CLOBBER)


context_switch
    switch_to
        __switch_to
            struct thread_struct *prev = &prev_p->thread,
                *next = &next_p->thread;
            int cpu = smp_processor_id();
            struct tss_struct *tss = &per_cpu(init_tss, cpu);
            tss->rsp0 = next->rsp0;

switch_to 前两行更新了 rsp,

ldt 用于 per process 的内存管理现已被 page table 和 CR3 替换了,那里也涉及 GDT 的修改,但这不是我们关心的重点

cpu lgdt(by Ben)

// include/asm-x86/desc_64.h
static inline void load_gdt(const struct desc_ptr *ptr)
{
	asm volatile("lgdt %w0"::"m" (*ptr));
}

show current GDT(by Ben)

gdb cannot show GDT IDTR like registers

这是因为 GDB 本身并不直接支持通过 $name 访问 x86 的系统表寄存器(如 GDTR, IDTR, TR),这些寄存器不在标准远程调试协议(GDB Remote Serial Protocol)的普通寄存器列表中。

ctrl+a c: switch between qemu command mode and os tty

(qemu) info registers

CPU#0
RAX=0000000000000000 RBX=ffffffff8020b155 RCX=0000000000000000 RDX=ffffffff8058
RSI=0000000000000001 RDI=ffffffff804f4cc0 RBP=ffffffff80563f48 RSP=ffffffff8058
R8 =0000000000000000 R9 =0000000000ffff24 R10=0000000000000000 R11=000000000001
R12=0000000000000000 R13=ffffffffffffffff R14=ffffffff805900a0 R15=000000000000
RIP=ffffffff8020b182 RFL=00000246 [---Z-P-] CPL=0 II=0 A20=1 SMM=0 HLT=1
ES =0018 0000000000000000 ffffffff 00c09300 DPL=0 DS   [-WA]
CS =0010 0000000000000000 ffffffff 00a09b00 DPL=0 CS64 [-RA]
SS =0018 0000000000000000 ffffffff 00c09300 DPL=0 DS   [-WA]
DS =0018 0000000000000000 ffffffff 00c09300 DPL=0 DS   [-WA]
FS =0000 0000000000000000 ffffffff 00c00100
GS =0000 ffffffff8052a000 ffffffff 00c00100
LDT=0000 0000000000000000 ffffffff 00c00000
TR =0040 ffff810001005d00 0000206f 00008b00 DPL=0 TSS64-busy
GDT=     ffffffff80564000 00000080
IDT=     ffffffff805c7000 00000fff
CR0=8005003b CR2=0000000000516f90 CR3=0000000000201000 CR4=000006e0
DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=00000000000
DR6=00000000ffff0ff0 DR7=0000000000000400
EFER=0000000000000d01
FCW=037f FSW=0000 [ST=0] FTW=00 MXCSR=00001f80
FPR0=0000000000000000 0000 FPR1=0000000000000000 0000
FPR2=0000000000000000 0000 FPR3=0000000000000000 0000
FPR4=0000000000000000 0000 FPR5=0000000000000000 0000
FPR6=0000000000000000 0000 FPR7=0000000000000000 0000
XMM00=0000000000000000 0000000000000000 XMM01=0000000000000000 0000000000000000
XMM02=0000000000000000 0000000000000000 XMM03=0000000000000000 0000000000000000
XMM04=0000000000000000 0000000000000000 XMM05=0000000000000000 0000000000000000
XMM06=0000000000000000 0000000000000000 XMM07=0000000000000000 0000000000000000
XMM08=0000000000000000 0000000000000000 XMM09=0000000000000000 0000000000000000
XMM10=0000000000000000 0000000000000000 XMM11=0000000000000000 0000000000000000
XMM12=0000000000000000 0000000000000000 XMM13=0000000000000000 0000000000000000
XMM14=0000000000000000 0000000000000000 XMM15=0000000000000000 0000000000000000
avatar
除非注明,本博客所有文章皆为原创。
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。