Linux Kernel:中断和异常处理程序的早期初始化(续)

680 阅读23分钟

本文采用Linux 内核 v3.10 版本 x86_64架构

我们在 Linux kernel:中断和异常处理程序的早期初始化 里介绍了异常的早期初始化,并在 Linux Kernel:Page-Fault 异常的早期处理 里介绍了早期的 Page-Fault 异常处理,接下来我们看看中断和异常后续的初始化。

现在我们来到了 start_kernel 函数,内核主要的初始化工作都是在该函数内完成的,当然也包括异常和中断的初始化。我们顺着 start_kernel 的执行流程,梳理下异常和中断的初始化过程。

interrupt_init.png

一、 中断栈与内核栈简介

1.1 内核栈

x86_64 架构下,对于每一个线程,都有一个内核栈。内核栈是与线程关联的,其栈大小为 THREAD_SIZE (2*PAGE_SIZE)。只要这些线程还活着或者处于僵尸(zombie)状态,那么内核栈中就包含着与线程相关的有用信息。当线程处于用户空间时,内核栈是空的,此时只有内核栈底部的 thread_info 结构体是有用的,其中保存着线程的相关信息。

1.1.1 内核栈数据结构

内核栈对应的数据结构:

// file: include/linux/sched.h
union thread_union {
	struct thread_info thread_info;
	unsigned long stack[THREAD_SIZE/sizeof(long)];
};

内核栈的底部,是 thread_info结构体,该结构体保存着线程相关的信息。thread_info结构体中的成员 task 指向线程的 task_struct 结构体:

// file: arch/x86/include/asm/thread_info.h
struct thread_info {
	struct task_struct	*task;		/* main task structure */
	...
    ...
};

同样,task_struct 结构体中也有一个成员 stack 指向 thread_info 结构体。

// file: include/linux/sched.h
struct task_struct {
    ...
    ...
        
    void *stack;
    
    ...
    ...
    
}

由于每一个线程都有自己的 task_struct 结构,所以每个线程的内核栈空间是独立的。

内核栈相关的数据结构,如下图所示:

Thread_Union_Task_Struct.png

per-cpu 变量 kernel_stack 中,保存着当前线程内核栈的栈底指针。

1.1.2 内核栈的初始化

kernel_stack被初始化为 init_thread_union 的栈底指针:

// file: arch/x86/kernel/cpu/common.c
DEFINE_PER_CPU(unsigned long, kernel_stack) =
	(unsigned long)&init_thread_union - KERNEL_STACK_OFFSET + THREAD_SIZE;

init_thread_unionunion thread_union类型的变量;THREAD_SIZE定义了栈的大小,扩展为 8K(2*PAGE_SIZE):

// file: arch/x86/include/asm/page_64_types.h
#define THREAD_SIZE_ORDER	1
#define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER)

KERNEL_STACK_OFFSET表示栈底偏移量,扩展为 40(5 个 8 字节指针):

// file: arch/x86/include/asm/thread_info.h
#define KERNEL_STACK_OFFSET (5*8)

计算之后,kernel_stackinit_thread_union 最高地址减去 40 的处的地址,即内核栈的栈底指针。

init_thread_union 中引用了init_task

// file: init/init_task.c
union thread_union init_thread_union __init_task_data =
	{ INIT_THREAD_INFO(init_task) };
// file: arch/x86/include/asm/thread_info.h
#define INIT_THREAD_INFO(tsk)			\
{						\
	.task		= &tsk,			\
	...				
	...
}

init_tasktask_struct 类型的变量,其内部成员 stack 引用了 init_thread_info

// file: init/init_task.c
/* Initial task structure */
struct task_struct init_task = INIT_TASK(init_task);
// file: include/linux/init_task.h
/*
 *  INIT_TASK is used to set up the first task table, touch at
 * your own risk!. Base=0, limit=0x1fffff (=2MB)
 */
#define INIT_TASK(tsk)	\
{									\
	.state		= 0,						\
	.stack		= &init_thread_info,				\
	
	...
	...
       
}
	

init_thread_infoinit_thread_union 的成员,它们的起始地址是一样的:

// file: arch/x86/include/asm/thread_info.h
#define init_thread_info	(init_thread_union.thread_info)

初始化完成后,内核栈如下图所示:

Init_Kernel_Stack.png

栈指针之所以比最大值小 40,是由于除了系统调用,有些异常处理也用到了内核栈;而在发生异常时,有 5 个寄存器是自动压入到栈中的:

// file: arch/x86/include/asm/calling.h
/* cpu exception frame or undefined in case of fast syscall: */
#define RIP		128
#define CS		136
#define EFLAGS		144
#define RSP		152
#define SS		160

所以实际的栈指针要从比最大值小 40 (5*8)字节处开始。

1.1.3 内核栈的切换

每次线程切换时,kernel_stack也需要跟着切换。

// file: arch/x86/kernel/process_64.c
/*
 *	switch_to(x,y) should switch tasks from x to y.
 *
 * This could still be optimized:
 * - fold all the options into a flag word and test it with a single test.
 * - could test fs/gs bitsliced
 *
 * Kprobes not supported here. Set the probe on schedule instead.
 * Function graph tracer not supported too.
 */
__notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
    ...
    ...
        
   	this_cpu_write(kernel_stack,
		  (unsigned long)task_stack_page(next_p) +
		  THREAD_SIZE - KERNEL_STACK_OFFSET);
    
    ...
    ...
        
}

其中 prev_p指向切换前线程的 task_structnext_p 指向切换后线程的 task_struct

1.2 中断栈

内核栈是与线程关联的,每个线程都有一个内核栈;而中断栈是与 CPU 关联的,每个 CPU 有一个中断栈。中断栈大小为 IRQ_STACK_SIZE(4*PAGE_SIZE),即 16K。中断栈用于外部硬件中断和软中断。当外部硬件中断首次发生时(例如,没有嵌套的硬件中断),内核从当前任务切换到中断栈。由于中断栈是跟 CPU 关联的,所以不会增加线程栈的大小。

1.2.1 中断栈的数据结构

与内核栈类似,中断栈的数据结构也是一个联合体 irq_stack_union

// file: arch/x86/include/asm/processor.h
union irq_stack_union {
	char irq_stack[IRQ_STACK_SIZE];
	/*
	 * GCC hardcodes the stack canary as %gs:40.  Since the
	 * irq_stack is the object at %gs:0, we reserve the bottom
	 * 48 bytes of the irq stack for the canary.
	 */
	struct {
		char gs_base[40];
		unsigned long stack_canary;
	};
};

IRQ_STACK_SIZE 定义如下,该宏又引用了 PAGE_SIZE(扩展为 4096) 和 IRQ_STACK_ORDER(扩展为 2),最终 IRQ_STACK_SIZE扩展为 2142^{14},即 16K。

// file: arch/x86/include/asm/page_64_types.h
#define IRQ_STACK_ORDER 2
#define IRQ_STACK_SIZE (PAGE_SIZE << IRQ_STACK_ORDER)

PAGE_SIZE定义:

// file: arch/x86/include/asm/page_types.h
/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT	12
#define PAGE_SIZE	(_AC(1,UL) << PAGE_SHIFT)

从以上分析可以看到,中断栈的大小为 16KB;其最低的 40 字节是结构体成员 gs_base ,然后是 unsigned long 类型(x86_64 架构下为 8 字节)的成员 stack_canary。也就是说,中断栈 irq_stack 底部的 48 字节是保留的,用来检测栈溢出。

中断栈结构如下图所示:

Interrupt_Stack_Structure.png

1.2.2 中断栈的初始化

中断栈 irq_stack_union是个union irq_stack_union类型的 per-cpu 变量,其起始地址对齐到 PAGE_SIZE(4K)。

// file: arch/x86/kernel/cpu/common.c
DEFINE_PER_CPU_FIRST(union irq_stack_union,
		     irq_stack_union) __aligned(PAGE_SIZE);

DEFINE_PER_CPU_FIRST宏创建了名为 .data..per-cpu..first 的节( section),该节是 per-cpu 区域的第一个节,且该节只有 irq_stack_union一个变量。

section-data-percpu.png

二、中断栈中的”金丝雀“

boot_init_stack_canary 函数为中断栈设置”金丝雀“(canary),以检测栈溢出。该函数定义如下:

// file: arch/x86/include/asm/stackprotector.h
/*
 * Initialize the stackprotector canary value.
 *
 * NOTE: this must only be called from functions that never return,
 * and it must always be inlined.
 */
static __always_inline void boot_init_stack_canary(void)
{
	u64 canary;
	u64 tsc;

#ifdef CONFIG_X86_64
	BUILD_BUG_ON(offsetof(union irq_stack_union, stack_canary) != 40);
#endif
	/*
	 * We both use the random pool and the current TSC as a source
	 * of randomness. The TSC only matters for very early init,
	 * there it already has some randomness on most systems. Later
	 * on during the bootup the random pool has true entropy too.
	 */
	get_random_bytes(&canary, sizeof(canary));
	tsc = __native_read_tsc();
	canary += tsc + (tsc << 32UL);

	current->stack_canary = canary;
#ifdef CONFIG_X86_64
	this_cpu_write(irq_stack_union.stack_canary, canary);
#else
	this_cpu_write(stack_canary.canary, canary);
#endif
}

在 x86-64 架构下,会检查结构体成员 stack_canary irq_stack_union中的偏移量是否等于 40,不相等的话则报错。

上文已经介绍过,union irq_stack_union是一个联合体,其大小为 16KB;最低的 40 字节是结构体成员 gs_base ,然后是 unsigned long 类型(x86_64 架构下为 8 字节)的成员 stack_canary。也就是说,中断栈 irq_stack 底部的 48 字节是保留的,用来检测栈溢出。

2.1 计算成员偏移量 -- offsetof

offsetof 宏用来计算结构体中某个成员的偏移量,其定义如下:

// file: include/linux/stddef.h
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

通过将数字 0 转型为对应结构体类型的指针,然后获取到该类型的成员地址。由于结构体类型地址是从 0 开始,所以获取到的成员地址就是该成员在结构体的偏移量。

2.2 编译时错误检查 -- BUILD_BUG_ON

// file: include/linux/bug.h
#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))

BUILD_BUG_ON 用来检查编译时错误,这里使用了一点小技巧,我们一起来看一下。!!conditions 等同于 condition != 0, 当 condition 为真时, !!(condition) 的值为 1;否则为 0。 所以 2*!!(condition) 的值,要么为 2 ,要么为 0;而 1 - 2*!!(condition) 则可能为 -1 或者 1

这就会导致 2 种不同的结果:

  • 当条件 condition 为真时,组数下标为 -1 ,我们会得到一个编译时错误;
  • 否则,无编译错误。

2.3 生成“金丝雀”

接下来,基于随机数和时间戳计数器( Time Stamp Counter),生成”金丝雀“(canary)的值:

get_random_bytes(&canary, sizeof(canary));
tsc = __native_read_tsc();
canary += tsc + (tsc << 32UL);

2.4 current 是个宏

生成”金丝雀“(canary)后,会把 canary 的值写入当前进程 task_struct 的结构体成员 stack_canary 中。

current->stack_canary = canary;

current是个宏,该宏会获取到当前 CPU 上正在运行的进程的 task_struct 结构体指针。

// file: arch/x86/include/asm/current.h
#define current get_current()

static __always_inline struct task_struct *get_current(void)
{
	return this_cpu_read_stable(current_task);
}

current_taskper-cpu 变量,其初始化为 init_task 地址:

// file: arch/x86/kernel/cpu/common.c
DEFINE_PER_CPU(struct task_struct *, current_task) ____cacheline_aligned =
	&init_task;

每次进程切换时,都会把切换到的进程的 task_struct 指针写入 current_task

/*
 *	switch_to(x,y) should switch tasks from x to y.
 *
 */
__notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
    ...
    ...
        
    this_cpu_write(current_task, next_p);
    
    ...
    ...
    
}

__switch_to 函数中的 next_p是切换后要运行进程的 task_struct 指针。this_cpu_write(current_task, next_p);会把 next_p的值写入当前 CPU 对应的per-cpu 变量 current_task 中。

关于 per-cpu 变量的知识,请参考Linux Kernel 源码学习:PER_CPU 变量、swapgs及栈切换(一) 以及 Linux kernel documentation

task_struct结构体中的 stack_canary成员:

struct task_struct {
    ...
    ...
        
#ifdef CONFIG_CC_STACKPROTECTOR
	/* Canary value for the -fstack-protector gcc feature */
	unsigned long stack_canary;
#endif
    
    ...
    ...
}

2.5 把 canary 写入中断栈

并使用 this_cpu_write 宏把 canary 的值写入irq_stack_union中:

this_cpu_write(irq_stack_union.stack_canary, canary);

通过this_cpu_write,将 canary 的值写入 per-cpu 变量 irq_stack_union 的成员 stack_canary 中去。

三、禁止和开启本地中断

local_disable_irqlocal_irq_enable用来禁止和开启本地中断。这 2 个宏的实现依赖于内核配置选项 CONFIG_TRACE_IRQFLAGS_SUPPORT ,当 CONFIG_TRACE_IRQFLAGS_SUPPORT 开启时,它们分别包含 trace_hardirqs_offtrace_hardirqs_on函数,用来追踪硬件中断的禁止与开启事件。本文不会介绍追踪相关的内容,这两个函数我们暂时略过。

// file: include/linux/irqflags.h
/*
 * The local_irq_*() APIs are equal to the raw_local_irq*()
 * if !TRACE_IRQFLAGS.
 */
#ifdef CONFIG_TRACE_IRQFLAGS_SUPPORT
#define local_irq_enable() \
	do { trace_hardirqs_on(); raw_local_irq_enable(); } while (0)
#define local_irq_disable() \
	do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)
...
...
...

#else /* !CONFIG_TRACE_IRQFLAGS_SUPPORT */

#define local_irq_enable()	do { raw_local_irq_enable(); } while (0)
#define local_irq_disable()	do { raw_local_irq_disable(); } while (0)
    
...

#endif /* CONFIG_TRACE_IRQFLAGS_SUPPORT */

local_irq_disable 宏会引用 raw_local_irq_disable宏,该宏最终会调用 native_irq_disable 函数。

local_irq_enable 宏会引用 raw_local_irq_enable宏,该宏最终会调用 native_irq_enable 函数。

3.1 raw_local_irq_disable / raw_local_irq_enable

raw_local_irq_disableraw_local_irq_enable 宏,分别调用了 arch_local_irq_disablearch_local_irq_enable 函数。

// file: include/linux/irqflags.h
#define raw_local_irq_disable()		arch_local_irq_disable()
#define raw_local_irq_enable()		arch_local_irq_enable()

arch_local_irq_disablearch_local_irq_enable的调用过程中涉及到半虚拟化相关的知识,这部分我们暂不涉及。

// file: arch/x86/include/asm/paravirt.h
static inline notrace void arch_local_irq_disable(void)
{
	PVOP_VCALLEE0(pv_irq_ops.irq_disable);
}

static inline notrace void arch_local_irq_enable(void)
{
	PVOP_VCALLEE0(pv_irq_ops.irq_enable);
}
// file: arch/x86/kernel/paravirt.c
struct pv_irq_ops pv_irq_ops = {
	...
        
	.irq_disable = __PV_IS_CALLEE_SAVE(native_irq_disable),
	.irq_enable = __PV_IS_CALLEE_SAVE(native_irq_enable),
    
	...
};

local_irq_disable 宏最终会调用 native_irq_disable 函数;local_irq_enable 宏最终会调用 native_irq_enable 函数。

// file: arch/x86/include/asm/irqflags.h
static inline void native_irq_disable(void)
{
	asm volatile("cli": : :"memory");
}

static inline void native_irq_enable(void)
{
	asm volatile("sti": : :"memory");
}

native_irq_disable函数,内部实现为汇编指令 clicli 指令会将状态寄存器 RFLAGS 中的 IF 标志设置为 0,用来禁止外部(硬件)中断。

native_irq_enable函数,内部实现为汇编指令 stisti 指令会将状态寄存器 RFLAGS 中的 IF 标志设置为 1,用来开启外部(硬件)中断。

四、setup_arch中的初始化函数

4.1 early_trap_init

early_trap_init函数里,设置了与调试相关的异常处理程序。

// file: arch/x86/kernel/traps.c
/* Set of traps needed for early debugging. */
void __init early_trap_init(void)
{
	set_intr_gate_ist(X86_TRAP_DB, &debug, DEBUG_STACK);
	/* int3 can be called from all */
	set_system_intr_gate_ist(X86_TRAP_BP, &int3, DEBUG_STACK);
#ifdef CONFIG_X86_32
	set_intr_gate(X86_TRAP_PF, &page_fault);
#endif
	load_idt(&idt_descr);
}

其中 X86_TRAP_DBX86_TRAP_BP 分别表示 Debug(1号,#DB)异常 和 Breakpoint(3号,#BP) 异常。

// file: arch/x86/include/asm/traps.h
/* Interrupts/Exceptions */
enum {
	X86_TRAP_DE = 0,	/*  0, Divide-by-zero */
	X86_TRAP_DB,		/*  1, Debug */
	X86_TRAP_NMI,		/*  2, Non-maskable Interrupt */
	X86_TRAP_BP,		/*  3, Breakpoint */
	X86_TRAP_OF,		/*  4, Overflow */
	X86_TRAP_BR,		/*  5, Bound Range Exceeded */
	X86_TRAP_UD,		/*  6, Invalid Opcode */
	X86_TRAP_NM,		/*  7, Device Not Available */
	X86_TRAP_DF,		/*  8, Double Fault */
	X86_TRAP_OLD_MF,	/*  9, Coprocessor Segment Overrun */
	X86_TRAP_TS,		/* 10, Invalid TSS */
	X86_TRAP_NP,		/* 11, Segment Not Present */
	X86_TRAP_SS,		/* 12, Stack Segment Fault */
	X86_TRAP_GP,		/* 13, General Protection Fault */
	X86_TRAP_PF,		/* 14, Page Fault */
	X86_TRAP_SPURIOUS,	/* 15, Spurious Interrupt */
	X86_TRAP_MF,		/* 16, x87 Floating-Point Exception */
	X86_TRAP_AC,		/* 17, Alignment Check */
	X86_TRAP_MC,		/* 18, Machine Check */
	X86_TRAP_XF,		/* 19, SIMD Floating-Point Exception */
	X86_TRAP_IRET = 32,	/* 32, IRET Exception */
};

可以看到,在 early_trap_init 里,调用了 3 个不同的函数来安装中断处理程序:

  • set_intr_gate_ist
  • set_system_intr_gate_ist
  • set_intr_gate

其中 set_intr_gate函数的实现,我们在 Linux kernel:中断和异常处理程序的早期初始化中已经详细介绍过了。set_intr_gate_istset_system_intr_gate_ist函数的实现,与set_intr_gate类似。

我们先来看下 set_intr_gate_ist函数,该函数名称中带有 _ist 后缀。带有这种后缀的函数,说明异常处理程序使用了中断栈表(Interrupt Stack Table,IST)。

中断栈表( Interrupt Stack Table,IST)机制是 x86_64 架构下新增的一种机制,仅在 x86_64 架构下中断及异常处理需要栈切换时使用。中断栈表是一种可选机制,是否启用由中断描述符表( IDT )中门描述的 IST 字段决定。 IST 字段共 3 位,提供了8 种可能性。当 IST 字段为 0 时,表示不启用 IST 机制。IST 机制最多可以提供 7 个 IST 指针(1 ~ 7),每个指针 64 位大小,这些指针保存在任务状态段( Task-State Segment, TSS )中。当启用了 IST 机制,在栈切换时,TSS 中对应的 IST 指针会被加载到 RSP 中。

在进入实际代码分析前,我们介绍一下与异常处理相关的基本概念。

4.1.1 中断门及陷阱门格式

64 位模式下,中断门及陷阱门格式如下图所示:

IDT_gate_descriptor_64.png

x86 架构支持 4 种门,分别是:

  • 调用门
  • 中断门
  • 陷阱门
  • 任务门

不同的类型的门,通过门描述符中的 TYPE 字段来区分:

Sys_Segment_Gate_Desc_Type.png

可以看到,在 64 位模式下,调用门的 Type 值为 12(二进制 1100),中断门的 Type 值为14(二进制 1110),陷阱门的 Type 值为15(二进制 1111)。同时也可以看到,在 64 位模式下,并没有任务门。

4.1.2 任务状态段格式

64 位模式下,任务状态段 TSS 的格式如下所示:

TSS_64.png

4.1.3 中断栈表及 TSS 初始化

set_intr_gate_istset_system_intr_gate_ist 函数都使用了中断栈表,由于 #DB 和 #BP 均为调试用途,它们使用的是同一个异常栈 -- 调试栈。

	set_intr_gate_ist(X86_TRAP_DB, &debug, DEBUG_STACK);
	/* int3 can be called from all */
	set_system_intr_gate_ist(X86_TRAP_BP, &int3, DEBUG_STACK);

DEBUG_STACK扩展为 4,表示调试栈在中断栈表的索引为 4,中断栈表各索引定义如下:

// file: arch/x86/include/asm/page_64_types.h
#define STACKFAULT_STACK 1
#define DOUBLEFAULT_STACK 2
#define NMI_STACK 3
#define DEBUG_STACK 4
#define MCE_STACK 5
#define N_EXCEPTION_STACKS 5  /* hw limit: 7 */

其中宏 N_EXCEPTION_STACKS 定义了中断栈表实际使用的数量,该值扩展为 5。从上文可知,中断栈表最多可以容纳 7 个指针,即 7 个栈地址。

中断栈表中的各类栈,其大小和基地址定义如下:

// file: arch/x86/kernel/cpu/common.c
/*
 * Special IST stacks which the CPU switches to when it calls
 * an IST-marked descriptor entry. Up to 7 stacks (hardware
 * limit), all of them are 4K, except the debug stack which
 * is 8K.
 */
static const unsigned int exception_stack_sizes[N_EXCEPTION_STACKS] = {
	  [0 ... N_EXCEPTION_STACKS - 1]	= EXCEPTION_STKSZ,
	  [DEBUG_STACK - 1]			= DEBUG_STKSZ
};

static DEFINE_PER_CPU_PAGE_ALIGNED(char, exception_stacks
	[(N_EXCEPTION_STACKS - 1) * EXCEPTION_STKSZ + DEBUG_STKSZ]);

EXCEPTION_STKSZ扩展为 4K,宏 DEBUG_STKSZ 扩展为 8K:

// file: arch/x86/include/asm/page_64_types.h
#define EXCEPTION_STACK_ORDER 0
#define EXCEPTION_STKSZ (PAGE_SIZE << EXCEPTION_STACK_ORDER)	// EXCEPTION_STKSZ 扩展为 4K

#define DEBUG_STACK_ORDER (EXCEPTION_STACK_ORDER + 1)			// DEBUG_STACK_ORDER 扩展为 1
#define DEBUG_STKSZ (PAGE_SIZE << DEBUG_STACK_ORDER)			// DEBUG_STKSZ 扩展为 8K,PAGE_SIZE 扩展为 4K

综上所述,exception_stack_sizes变量定义了中断栈表中各异常栈的大小,除了调试栈为 8K 外,其它栈均为 4K。

exception_stacks 变量定义了各栈的基地址,该变量是 per-cpu 变量,也就是说每个处理器都有一份该变量的副本。

exception_stacks 结构图如下:

Exception_Stack.png

中断栈表及 TSS 的初始化过程如下:

// file: arch/x86/kernel/cpu/common.c
void __cpuinit cpu_init(void)
{
	struct orig_ist *oist;
	struct task_struct *me;
	struct tss_struct *t;
	unsigned long v;
	int cpu;
	int i;
    
    ...
        
    cpu = stack_smp_processor_id();
	t = &per_cpu(init_tss, cpu);
	oist = &per_cpu(orig_ist, cpu);
    
    ...
        
	/*
	 * set up and load the per-CPU TSS
	 */
	if (!oist->ist[0]) {
		char *estacks = per_cpu(exception_stacks, cpu);

		for (v = 0; v < N_EXCEPTION_STACKS; v++) {
			estacks += exception_stack_sizes[v];
			oist->ist[v] = t->x86_tss.ist[v] =
					(unsigned long)estacks;
			if (v == DEBUG_STACK-1)
				per_cpu(debug_stack_addr, cpu) = (unsigned long)estacks;
		}
	}
    
    ...
}

在上述代码中,有 2 个结构体需要关注,分别是 struct orig_iststruct tss_struct

struct orig_ist定义如下:

// file: arch/x86/include/asm/processor.h
/*
 * Save the original ist values for checking stack pointers during debugging
 */
struct orig_ist {
	unsigned long		ist[7];
};

结构体 orig_ist表示中断栈表,其成员 ist是一个长度为 7 的数组,对应着中断栈表的最大指针数量。

tss_struct 结构体定义如下:

// file: arch/x86/include/asm/processor.h
struct tss_struct {
	/*
	 * The hardware state:
	 */
	struct x86_hw_tss	x86_tss;

	...
    ...

} ____cacheline_aligned;

结构体 x86_hw_tss 定义如下:

// file: arch/x86/include/asm/processor.h
struct x86_hw_tss {
	u32			reserved1;
	u64			sp0;
	u64			sp1;
	u64			sp2;
	u64			reserved2;
	u64			ist[7];
	u32			reserved3;
	u32			reserved4;
	u16			reserved5;
	u16			io_bitmap_base;

} __attribute__((packed)) ____cacheline_aligned;

可以看到,结构体 x86_hw_tss 中的成员与 TSS 中的字段是一一对应的。

接下来,我们分析 TSS 与中断栈表的初始化过程。

首先,获取当前 cpu 编号。

cpu = stack_smp_processor_id();

stack_smp_processor_id 是一个宏,其定义如下:

// file: arch/x86/include/asm/smp.h
#define stack_smp_processor_id()					\
({								\
	struct thread_info *ti;						\
	__asm__("andq %%rsp,%0; ":"=r" (ti) : "0" (CURRENT_MASK));	\
	ti->cpu;							\
})

CURRENT_MASKTHREAD_SIZE(扩展到 8K)的掩码,其定义如下:

// file: arch/x86/include/asm/page_64_types.h
#define THREAD_SIZE_ORDER	1
#define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER)	//  THREAD_SIZE 扩展为 8K,其中 PAGE_SIZE 扩展为 4K,见上文
#define CURRENT_MASK (~(THREAD_SIZE - 1))

stack_smp_processor_id宏的工作原理如下所述:

处理器当前工作在内核态,内核栈的数据结构是 thread_unionthread_union是一个共同体,包括栈和 thread_info结构体。也就是说,在栈的底部,是thread_info结构体。thread_info结构体内部的成员变量 cpu,记录了当前 CPU 的编号:

// file: arch/x86/include/asm/thread_info.h
struct thread_info {
	...
        
	__u32			cpu;		/* current CPU */
    
	...
};

由于内核栈是对齐到 THREAD_SIZE的,所以通过汇编代码

    __asm__("andq %%rsp,%0; ":"=r" (ti) : "0" (CURRENT_MASK));

%rsp 的值和 CURRENT_MASK进行与操作,就得到了栈的最低地址,即 thread_union 的起始地址,同时也是 thread_info 的起始地址。

然后通过 ti->cpu,获取到 thread_info中的 cpu 成员,即当前 CPU 编号。

获取到当前 CPU 编号之后,我们来看后两行代码。

t = &per_cpu(init_tss, cpu);
oist = &per_cpu(orig_ist, cpu);

orig_istinit_tss 都是 per-cpu 变量,它们是通过 DEFINE_PER_CPU* 相关的API 创建出来的。

// file: arch/x86/kernel/cpu/common.c
DEFINE_PER_CPU(struct orig_ist, orig_ist);
// file: arch/x86/kernel/process.c
/*
 * per-CPU TSS segments. Threads are completely 'soft' on Linux,
 * no more per-task TSS's. The TSS size is kept cacheline-aligned
 * so they are allowed to end up in the .data..cacheline_aligned
 * section. Since TSS's are completely CPU-local, we want them
 * on exact cacheline boundaries, to eliminate cacheline ping-pong.
 */
DEFINE_PER_CPU_SHARED_ALIGNED(struct tss_struct, init_tss) = INIT_TSS;

per_cpu 宏接收 2 个参数,分别是变量名及 CPU编号。该宏可以获取到指定 CPU 的 per-cpu 变量的值,其定义如下:

// file: include/asm-generic/percpu.h
#define per_cpu(var, cpu) \
	(*SHIFT_PERCPU_PTR(&(var), per_cpu_offset(cpu)))

关于DEFINE_PER_CPUper_cpu 宏的具体实现,请参考:Linux Kernel 源码学习:PER_CPU 变量、swapgs及栈切换(一)

更多 per-cpu 变量相关的操作,可参考文档:this_cpu_ops.txt

另外,我们看到,init_tss 被初始化为 INIT_TSSINIT_TSS 定义如下:

// file: arch/x86/include/asm/processor.h
#define INIT_TSS  { \
	.x86_tss.sp0 = (unsigned long)&init_stack + sizeof(init_stack) \
}

可以看到,在 INIT_TSS 中,对 TSS 中的 sp0(特权级 0 的栈指针,也就是内核栈的栈底) 进行了初始化。init_stack定义如下:

// file: arch/x86/include/asm/thread_info.h
#define init_stack		(init_thread_union.stack)

init_thread_union是一个 union thread_union的实例。

// file: include/linux/sched.h
extern union thread_union init_thread_union;

union thread_union {
	struct thread_info thread_info;
	unsigned long stack[THREAD_SIZE/sizeof(long)];
};

(unsigned long)&init_stack + sizeof(init_stack)计算得到的是init_thread_union的最大内存地址。由于栈是向下增长的,所以该地址就是栈底地址。

SP0.png

再接下来,就是对中断栈表及 TSS 进行正式初始化了。

	/*
	 * set up and load the per-CPU TSS
	 */
	if (!oist->ist[0]) {
		char *estacks = per_cpu(exception_stacks, cpu);

		for (v = 0; v < N_EXCEPTION_STACKS; v++) {
			estacks += exception_stack_sizes[v];
			oist->ist[v] = t->x86_tss.ist[v] =
					(unsigned long)estacks;
			if (v == DEBUG_STACK-1)
				per_cpu(debug_stack_addr, cpu) = (unsigned long)estacks;
		}
	}

由于使用 DEFINE_PER_CPU 宏创建的 per-cpu 变量,其内存都会被初始化为 0。所以,一开始,通过 !oist->ist[0] 来检查中断栈表是否已经初始化。如果未初始化,则开始执行初始化流程。

上文已经提到,exception_stacks per-cpu 变量,为各异常栈分配了空间。通过 per_cpu宏,就能获取到指定 CPU 的exception_stacks变量的值。然后通过循环,先是通过 estacks += exception_stack_sizes[v] 获取到各异常栈的起始地址,然后将地址写入到 oistx86_tss中去。当异常栈类型为调试栈时,把调试栈的基地址写入 per-cpu 变量 debug_stack_addr中,供以后使用。至此,中断栈表及 TSS 填充完毕。

4.1.4 set_intr_gate_ist 函数

set_intr_gate_ist 函数实现如下:

// file: arch/x86/include/asm/desc.h
static inline void set_intr_gate_ist(int n, void *addr, unsigned ist)
{
	BUG_ON((unsigned)n > 0xFF);
	_set_gate(n, GATE_INTERRUPT, addr, 0, ist, __KERNEL_CS);
}

首先,检查传入的中断向量是否大于系统最大向量号 255(0xFF),如果比最大值还大,那么报错并挂起进程。然后通过 _set_gate函数,向中断描述符表中插入新的门描述符。门的类型为 GATE_INTERRUPT,即中断门,其值为 0xE(十进制 14)。

不同门的类型值定义在 arch/x86/include/asm/desc_defs.h 文件中:

// file: arch/x86/include/asm/desc_defs.h
enum {
	GATE_INTERRUPT = 0xE,
	GATE_TRAP = 0xF,
	GATE_CALL = 0xC,
	GATE_TASK = 0x5,
};

_set_gate函数实现如下:

// file: arch/x86/include/asm/desc.h
static inline void _set_gate(int gate, unsigned type, void *addr,
			     unsigned dpl, unsigned ist, unsigned seg)
{
	gate_desc s;

	pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);
	/*
	 * does not need to be atomic because it is only done once at
	 * setup time
	 */
	write_idt_entry(idt_table, gate, &s);
}

该函数我们在 Linux kernel:中断和异常处理程序的早期初始化 中详细分析过,此处不再赘述。

4.1.5 set_system_intr_gate_ist 函数

set_system_intr_gate_ist 函数定义如下:

// file: arch/x86/include/asm/desc.h
static inline void set_system_intr_gate_ist(int n, void *addr, unsigned ist)
{
	BUG_ON((unsigned)n > 0xFF);
	_set_gate(n, GATE_INTERRUPT, addr, 0x3, ist, __KERNEL_CS);
}

set_intr_gate_ist 函数类似,该函数先是检查了中断号,然后调用了 _set_gate 函数。与 set_intr_gate_ist 函数不同的是,该函数传给 _set_gate 的第 4 个参数值为 0x3;而 set_intr_gate_ist 函数中,_set_gate 的第 4 个参数为 0。观察 _set_gate 函数可以发现,该函数第 4 个参数为 dpl,即描述符特权级别。所以,breakpoint(#BP) 异常,其中断门描述符的 dpl0x3;debug(#DB) 异常,其中断门描述符的dpl0

我们在 x86-64:特权级保护及程序控制转移 中介绍过中断及异常的特权级保护。

只有通过 INT n, INT3, 或 INTO 指令生成的中断或异常,处理器才会检查中断或陷阱门的 DPL。此时,CPL 必须小于等于门的 DPL(CPL ≤ DPL)。

现在,breakpoint(#BP) 异常的门描述符的 dpl0x3,也就是说,在用户空间,就可以调用 int3 指令。

4.1.6 INT3 示例

我们写一个程序来测试下断点(breakpoint)异常。

// breakpoint.c
#include <stdio.h>

int main() {
    int i;
    for (i = 0; i < 10; i++){
        printf("i is: %d\n", i);
        __asm__("int3");
    }
}

编译

gcc -o breakpoint breakpoint.c 

如果直接运行会报错,输出结果如下:

$ ./breakpoint 
i is: 0
Trace/breakpoint trap (core dumped)

如果在 gdb 下运行,则可以看到断点效果:

$ gdb breakpoint 
...
...

(gdb) r
Starting program: /home/zhouz/csapp/c_test/breakpoint 
i is: 0

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000555555554672 in main ()
(gdb) c
Continuing.
i is: 1

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000555555554672 in main ()
(gdb) c
Continuing.
i is: 2

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000555555554672 in main ()
(gdb) c
Continuing.
i is: 3

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000555555554672 in main ()

...
...

4.2 early_trap_pf_init

early_trap_pf_init函数内部,调用 set_intr_gate函数,安装新的中断门。该中断门用来处理 Page-Fault 异常,即 14 号(#PF)异常。

// file: arch/x86/kernel/traps.c
void __init early_trap_pf_init(void)
{
#ifdef CONFIG_X86_64
	set_intr_gate(X86_TRAP_PF, &page_fault);
#endif
}

set_intr_gate函数在其它文章已经介绍过了,此处不再赘述。

五、参考资料

1、Intel 开发者手册:Intel 64 and IA-32 Architectures Software Developer Manuals Volume 3A, Chapter 4 Paging

2、Linux kernel:中断和异常处理程序的早期初始化

3、Linux Kernel 源码学习:PER_CPU 变量、swapgs及栈切换(一)

4、内核文档:this_cpu_ops.txt