Linux Kernel:异常处理程序的实现

769 阅读27分钟

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

本文不涉及追踪、调试相关内容

为了降低代码复杂性,本文假定内核未开启半虚拟化配置

Linux Kernel:中断和异常处理程序的早期初始化(续) 中,我们讲解到在中断描述符表中分别为 debug(#DB) 、int3(#BP)和 Page-Fault(#PF) 异常注册了处理程序。本文我们来介绍异常处理程序的通用实现。在此之前,为了方便理解,先来介绍下内核处理异常的背景知识。

一、背景知识

1.1 异常处理的复杂性

1.1.1 判断异常出现在内核空间还是用户空间

首先,我们来说说为什么要判断异常出现在内核空间还是用户空间。

由于异常处理程序工作在内核空间,肯定需要访问内核空间的数据结构。在 x86-64 架构中,许多内核数据结构是 per-cpu 变量,而 per-cpu 区域的基地址保存在 MSR(Model Specific RegisterIA32_KERNEL_GS_BASE 中。

如果异常发生在用户空间,当从用户空间切换到内核空间时,为了能够访问 per-cpu 变量,需要使用 swapgs 指令,交换 IA32_KERNEL_GS_BASEIA32_GS_BASE 寄存器的值。交换后,%gs 寄存器的基址就变成了 IA32_KERNEL_GS_BASE 中的值,内核程序就可以使用 %gs 前缀来访问 per-cpu 数据了。其中,IA32_GS_BASE 寄存器是对 %gs 寄存器中基址(base_address)部分的映射。

IA32_GS_BASE.png

swapgs 指令必须成对使用,从用户空间切换到内核空间时,需要使用 swapgs 指令将用户态 GS(IA32_GS_BASE) 切换到内核态 GS(IA32_KERNEL_GS_BASE),以便能够访问内核空间数据;当从中断返回时,也要使用 swapgs 指令将 GS 从内核态切换到用户态,程序才能正常运行。

如果异常发生在内核空间,就不必再次使用 swapgs 指令来切换 GS了,因为已经切换过了。

所以,我们检测异常发生在内核空间还是用户空间,是为了判断是否需要使用 swapgs 指令来切换 GS

其次,我们得知道如何检测异常发生在内核空间还是用户空间。

在 x86-64 架构下,当发生中断或异常时,处理器会自动将被中断程序的 SS、RSP、RFLAGES、CS 及 RIP 寄存器存入栈中。最简单的方法,就是检查栈中的 CS 值来判断发生异常时程序处于用户空间还是内核空间,进而决定是否要执行 swapgs 指令。如果栈中 CS 的 CPL(Current Privilege Level) 为 3,说明异常发生在用户空间;如果 CPL 为 0,说明异常发生在内核空间。

不幸的是,并不是所有异常都可以通过 CS 中的 CPL 值来判断其发生在用户空间还是内核空间。有些异常会在将 CS 写入栈和执行 swapgs 指令之间发生,这时只判断 CS 中的 CPL 是不够的,还需要判断 MSR_GS_BASE 寄存器的值。如果 CS 来自内核空间,而 MSR_GS_BASE 中的值是用户空间的地址,说明还未来得及执行 swapgs 指令就发生新的异常了,则需要执行 swapgs 指令;如果 MSR_GS_BASE 中的值是内核空间的地址,说明已经执行过 swapgs 指令了,就不需要再次执行了。这类异常包括 NMI/MCE/DEBUG 等,内核文档 对此说明如下:

But if we are in an NMI/MCE/DEBUG/whatever super-atomic entry context,which might have triggered right after a normal entry wrote CS to the stack but before we executed SWAPGS, then the only safe way to check for GS is the slower method: the RDMSR.

通过检测 CS 的 CPL 来确定异常来源的方法,简单快速,称为便宜(cheap)方法,其检测方法实例如下:

	testl $3,CS(%rsp)
	je error_kernelspace
	SWAPGS

通过检查 MSR_GS_BASE 寄存器来确定异常来源的方法,速度较慢,称为昂贵(paranoid)方法,其检测方法示例如下:

	movl $MSR_GS_BASE,%ecx
	rdmsr
	testl %edx,%edx
	js 1f   /* negative -> in kernel */
	SWAPGS
1:	ret

rdmsr 指令会读取指定 MSR 寄存器的值并保存到 %edx:%eax 寄存器中,其中 %edx 保存高 32 位,%eax 保存低 32 位。具体读取哪个 MSR 寄存器的值,通过 %ecx 来指定。由于内核空间地址从 0xffff800000000000 开始,而用户空间的最高地址为0x00007fffffffffff,我们只需要判断 MSR_GS_BASE 中的高 32 位,就能判断出该值是内核空间的地址还是用户空间的地址。

上述代码片段中,先是读取 MSR_GS_BASE 值到 %edx:%eax 寄存器,然后通过 testl 指令,来判断 %edx 中的值是否为负数。如果为负,说明是内核空间地址,不用再执行 swapgs 指令了,直接 ret 返回;否则,说明 MSR_GS_BASE 中仍然是用户空间地址,还没来得及执行 swapgs 指令,此时需要执行 swapgs 指令,才能使用 ret 返回。

1.1.2 其它复杂性

除了判断异常的发生空间,异常处理还有其它的复杂性。

有的异常有错误码,有的没有。当异常有错误码时,处理器会自动将错误码压入栈中,一共压栈了 6 个值;当使用 iret 返回时,却只会自动弹出 5 个值,错误码需要手动处理。当异常没有错误码时,为了保证栈帧布局的一致性,内核向栈中压入了伪错误码。

另外,有的异常使用了中断栈表(Interrupt Stack Table,IST),有的使用内核栈,也要区别对待。

还有就是,异常处理和系统调用都使用了内核栈,但是栈帧的布局也不完全相同,内核也做了统一处理。

1.1.3 内核的处理方式

针对异常是否有错误码、是否使用 paranoid 的方式来检查异常发生的空间,是否使用了中断栈表(IST),内核将异常处理程序入口分成了 5 类,分别是:

  • zeroentry
  • paranoidzeroentry
  • paranoidzeroentry_ist
  • errorentry
  • paranoiderrorentry

其中名字中带有 zeroentry 的,表示异常没有错误码;带有 errorentry 的,表示异常有错误码;带有 paranoid 的,表示使用了昂贵(paranoid)的方法来检测异常发生空间;带有 ist 的,表示使用了中断栈表。

这些异常处理程序入口,定义在 arch/x86/kernel/entry_64.S文件中:

zeroentry divide_error do_divide_error
zeroentry overflow do_overflow
zeroentry bounds do_bounds
zeroentry invalid_op do_invalid_op
zeroentry device_not_available do_device_not_available
zeroentry coprocessor_segment_overrun do_coprocessor_segment_overrun
zeroentry spurious_interrupt_bug do_spurious_interrupt_bug
zeroentry coprocessor_error do_coprocessor_error
zeroentry simd_coprocessor_error do_simd_coprocessor_error

errorentry invalid_TSS do_invalid_TSS
errorentry segment_not_present do_segment_not_present
errorentry alignment_check do_alignment_check
errorentry general_protection do_general_protection
errorentry page_fault do_page_fault					// page_fault 异常

#ifdef CONFIG_X86_MCE
paranoidzeroentry machine_check *machine_check_vector(%rip)
#endif

paranoiderrorentry stack_segment do_stack_segment
paranoiderrorentry double_fault do_double_fault

paranoidzeroentry_ist debug do_debug DEBUG_STACK
paranoidzeroentry_ist int3 do_int3 DEBUG_STACK

可以看到,带有 paranoid 的异常包括 double_fault、debug、int3、stack_segment 以及 machine_check。其中,debug 和 int3 异常处理程序使用了中断栈表,这一点从它们的入口程序中带有 ist 后缀也能体现出来。

另外,nmi 异常也需要使用 paranoid 方法来检查异常发生空间,且使用到了中断栈表(IST),但由于 nmi 处理过程更加复杂,内核单独设置了一个入口。

1.2 上下文切换

异常处理涉及到程序控制转移,其处理过程包括以下三个步骤:

  • 保存上下文
  • 执行实际的异常处理
  • 恢复上下文

其中上下文的保存与恢复,具体来说,就是在执行实际的异常处理之前,把被中断程序所到用的各种寄存器存入栈中;异常处理完成后,恢复各寄存器的值,使被中断的程序能够继续运行。

1.2.1 pt_regs 结构体

Linux 内核提供了结构体 pt_regs 用来存储各寄存器,该结构体在系统调用及异常处理时都会用到。

// file: arch/x86/include/asm/ptrace.h
struct pt_regs {
	unsigned long r15;
	unsigned long r14;
	unsigned long r13;
	unsigned long r12;
	unsigned long bp;
	unsigned long bx;
/* arguments: non interrupts/non tracing syscalls only save up to here*/
	unsigned long r11;
	unsigned long r10;
	unsigned long r9;
	unsigned long r8;
	unsigned long ax;
	unsigned long cx;
	unsigned long dx;
	unsigned long si;
	unsigned long di;
	unsigned long orig_ax;
/* end of arguments */
/* cpu exception frame or undefined */
	unsigned long ip;
	unsigned long cs;
	unsigned long flags;
	unsigned long sp;
	unsigned long ss;
/* top of stack page */
};

1.2.2 寄存器分类

上述寄存器中,%ss ~ %ip 这 5 个寄存器,是专门为异常处理准备的,因为在发生异常时,这 5 个寄存器的值会被处理器自动保存到栈中。系统调用时,syscall 指令不会在栈中保存任何寄存器,这 5 个寄存器的位置空间没有用到,但会被保留。

interrupt_stack_no_error_code.png

%orig_ax ~ %r11 这 10 个寄存器是传参用寄存器。

系统调用最多允许 6 个参数,分别用 %rdi、%rsi、%rdx、%r10、%r8 以及 %r9 来传参。另外,在系统调用时,%rax 用来保存系统调用号,%rcx 用来保存返回地址,%r11 用来保存 %rflags 寄存器。其中 %orig_ax 表示原始 %rax 寄存器,在系统调用时,该位置保存的是系统调用号;在异常处理时,该位置保存的是错误码。%ax 用来保存返回值。

剩下的 6 个寄存器 %bx ~ %r15 没有特定用途。

// file: arch/x86/kernel/entry_64.S
/*
 * System call entry. Up to 6 arguments in registers are supported.
 *
 * SYSCALL does not save anything on the stack and does not change the
 * stack pointer. 
 */
/*
 * Register setup:
 * rax  system call number
 * rdi  arg0
 * rcx  return address for syscall/sysret, C arg3
 * rsi  arg1
 * rdx  arg2
 * r10  arg3 	(--> moved to rcx for C)
 * r8   arg4
 * r9   arg5
 * r11  eflags for syscall/sysret, temporary for C
 
 ...
 ...
 
 */

1.2.3 pt_regs 结构体与内核栈的关系

不管是系统调用还是异常处理,都涉及到上下文的保存与恢复,也都会使用到 pt_regs 结构体。

pt_regs 结构体位于内核栈的栈底,如下图所示:

Kernel_Stack_pt_regs.png

1.2.4 pt_regs结构体的汇编实现

pt_regs 结构体是 C 语言实现,在汇编代码中,也有对应的实现。在文件 arch/x86/include/asm/calling.h 中,使用宏定义了各寄存器的位置:

// file: arch/x86/include/asm/calling.h
/*
 * 64-bit system call stack frame layout defines and helpers,
 * for assembly code:
 */

#define R15		  0
#define R14		  8
#define R13		 16
#define R12		 24
#define RBP		 32
#define RBX		 40

/* arguments: interrupts/non tracing syscalls only save up to here: */
#define R11		 48
#define R10		 56
#define R9		 64
#define R8		 72
#define RAX		 80
#define RCX		 88
#define RDX		 96
#define RSI		104
#define RDI		112
#define ORIG_RAX	120       /* + error_code */
/* end of arguments */

/* 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

#define ARGOFFSET	R11
#define SWFRAME		ORIG_RAX

Linux 内核中,针对 pt_regs 中的 3 类寄存器,分别定义了术语:

  • 栈顶(top of stack): 这是架构定义的中断帧,位于内核栈的最顶部,包括 %ss ~ %rip 共 5 个寄存器,
  • 部分栈帧(partial stack frame):到 %r11 寄存器
  • 全栈帧(full stack frame):全部寄存器

Linux 内核用汇编实现了对不同栈帧的操作:

  • SAVE_ALL/RESTORE_ALL: 保存和恢复全部寄存器
  • SAVE_ARGS/RESTORE_ARGS:保存和恢复传参用寄存器
  • SAVE_REST/RESTORE_REST:用来处理 SAVE_ARGS 中未涉及到的寄存器,即 %rbx ~ %r15 共 6 个寄存器,以形成完整的栈帧。

1.3 栈切换

当发生异常时,如果当前程序运行在用户空间,那么需要切换到内核空间去执行异常处理程序,此时会发生栈切换,从用户栈切换到内核栈。栈切换的过程是处理器自动执行的,那么处理器是怎么知道内核栈地址的呢?答案就是 TSS(Task State Segment,任务状态段) 中获取。任务状态段的格式如下,我们在其它文章里介绍过:

TSS_64.png

可以看到,TSS 中保存有 64 位的 RSP0(0 特权级的栈指针)。内核使用 per-cpu 变量 init_tss 来存储 TSS 数据,任务状态段描述符选择子保存在 TR(Task Register) 寄存器中。每个进程都有自己的内核栈,当进程切换时,只需更新 init_tss 中的 sp0 值即可。init_tss 的初始化过程,我们在 Linux Kernel:中断和异常处理程序的早期初始化(续) 中已经介绍过了,本文不再赘述。下面来看下进程切换时,sp0 的更新过程。

进程切换时,会调用 __switch_to 函数。

__notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
    ...
    struct thread_struct *next = &next_p->thread;
    ...
        
    struct tss_struct *tss = &per_cpu(init_tss, cpu);
    
    ...
    
	/*
	 * Reload esp0, LDT and the page table pointer:
	 */
	load_sp0(tss, next);
    
    ...
}

next_p是切换后进程的 task_struct 结构体指针。在 __switch_to 函数中,先从切换后进程的 task_struct 结构体中,获取到任务状态信息,这是一个 thread_struct 结构体,该结构体中保存有线程的 sp0 信息。

// file: include/linux/sched.h
struct task_struct {
    ...
	/* CPU-specific state of this task */
	struct thread_struct thread;
    ...
}
struct thread_struct {
	... 
	unsigned long		sp0;
	...
}

然后获取到 per-cpu 变量 init_tss 的指针。最后,调用 load_sp0 函数,更新 init_tss 中 sp0 的值。

// file: arch/x86/include/asm/processor.h
static inline void load_sp0(struct tss_struct *tss,
			    struct thread_struct *thread)
{
	native_load_sp0(tss, thread);
}

load_sp0 内部调用了native_load_sp0native_load_sp0 更新了 init_tss 中 sp0 的值。

// file: arch/x86/include/asm/processor.h
static inline void
native_load_sp0(struct tss_struct *tss, struct thread_struct *thread)
{
	tss->x86_tss.sp0 = thread->sp0;
#ifdef CONFIG_X86_32
	/* Only happens when SEP is enabled, no need to test "SEP"arately: */
	if (unlikely(tss->x86_tss.ss1 != thread->sysenter_cs)) {
		tss->x86_tss.ss1 = thread->sysenter_cs;
		wrmsr(MSR_IA32_SYSENTER_CS, thread->sysenter_cs, 0);
	}
#endif
}

tss_struct 结构体格式如下,其内部的 x86_hw_tss 结构体映射了 TSS 中的各字段:

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

	...

} ____cacheline_aligned;
// 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;

至此,sp0 加载完成。处理器通过 init_tss 中的 x86_tss.sp0,就能够找到进程内核栈的地址。

二、异常处理程序的实现

内核在 early_trap_pf_init 函数中,为 Page-Fault 异常注册了处理函数 page_fault。本文我们以 page_fault 为例,来看下异常处理程序的的实现。

page_fault 函数声明如下:

// arch/x86/include/asm/traps.h
asmlinkage void page_fault(void);

2.1 asmlinkage 宏的含义

可以看到,在函数声明中出现了修饰符 asmlinkageasmlinkage 在 x86 及 x86-64 架构下有着不同的含义。

在 x86 架构下,不同平台的函数调用习惯不同,有的要求全部使用栈来传参,有的使用了EAX、 EDX 以及 ECX 寄存器传参。asmlinkage 指示 GCC 编译器不使用寄存器传参,所有参数都通过栈来传递。

在 x86 架构下,宏asmlinkage 定义如下:

// file: arch/x86/include/asm/linkage.h
#ifdef CONFIG_X86_32
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))

...
    
#endif /* CONFIG_X86_32 */

其中,regparm 是 GCC 的函数属性,指示是否使用 EAX, EDX 以及 ECX 寄存器来传递参数。

regparm (number)

On x86-32 targets, the regparm attribute causes the compiler to pass arguments number one to number if they are of integral type in registers EAX, EDX, and ECX instead of on the stack. Functions that take a variable number of arguments continue to be passed all of their arguments on the stack.

需要特别指出的是,该函数属性只在 x86 架构下有效。

在 x86-64 架构下,由于使用寄存器来传递参数,所以 asmlinkage 没有实际意义:

// file: include/linux/linkage.h
#define asmlinkage CPP_ASMLINKAGE

#define CPP_ASMLINKAGE

2.2 有错误码的异常入口 - errorentry

page_fault 的入口点是 errorentry,从名称中就能判断出该异常有错误码,且未使用到 paranoid 检测方法。

// file: arch/x86/kernel/entry_64.S
errorentry page_fault do_page_fault

我们在上文中介绍过,异常处理程序都由 3 个阶段组成。第一、第三阶段是通用部分,即上下文的保存与恢复,每个异常处理程序基本一致。第二阶段,就是根据不同的异常类型,做一些针对性的处理工作。比如 Page Fault 异常要处理缺页,Divide Error 异常要处理 0 除错误等等。

errorentry 定义如下:

// file: arch/x86/kernel/entry_64.S
.macro errorentry sym do_sym
ENTRY(\sym)
	XCPT_FRAME
	ASM_CLAC
	PARAVIRT_ADJUST_EXCEPTION_FRAME
	subq $ORIG_RAX-R15, %rsp
	CFI_ADJUST_CFA_OFFSET ORIG_RAX-R15
	call error_entry
	DEFAULT_FRAME 0
	movq %rsp,%rdi			/* pt_regs pointer */
	movq ORIG_RAX(%rsp),%rsi	/* get error code */
	movq $-1,ORIG_RAX(%rsp)		/* no syscall to restart */
	call \do_sym
	jmp error_exit			/* %ebx: no swapgs flag */
	CFI_ENDPROC
END(\sym)
.endm

该宏的实现过程简述如下:

  • 调用 error_entry 函数来保存上下文
  • 调用 do_sym(本例中是 do_page_fault) 来实现具体的异常处理
  • 跳转到 error_exit 处,恢复上下文

接下来我们去分析具体的实现,在分析过程中会忽略掉调试及追踪相关的内容。

2.2.1 宏 XCPT_FRAME

XCPT_FRAME 定义如下,该宏又引用了 INTR_FRAME 宏:

// file: arch/x86/kernel/entry_64.S
/*
 * initial frame state for exceptions with error code (and interrupts
 * with vector already pushed)
 */
	.macro XCPT_FRAME start=1 offset=0
	INTR_FRAME \start, RIP+\offset-ORIG_RAX
	/*CFI_REL_OFFSET orig_rax, ORIG_RAX-ORIG_RAX*/
	.endm

INTR_FRAME定义如下,该宏又引用了 EMPTY_FRAME 宏:

// file: arch/x86/kernel/entry_64.S
/*
 * initial frame state for interrupts (and exceptions without error code)
 */
	.macro INTR_FRAME start=1 offset=0
	EMPTY_FRAME \start, SS+8+\offset-RIP
	/*CFI_REL_OFFSET ss, SS+\offset-RIP*/
	CFI_REL_OFFSET rsp, RSP+\offset-RIP
	/*CFI_REL_OFFSET rflags, EFLAGS+\offset-RIP*/
	/*CFI_REL_OFFSET cs, CS+\offset-RIP*/
	CFI_REL_OFFSET rip, RIP+\offset-RIP
	.endm

EMPTY_FRAME 宏定义如下:

// file: arch/x86/kernel/entry_64.S
/*
 * initial frame state for interrupts (and exceptions without error code)
 */
	.macro EMPTY_FRAME start=1 offset=0
	.if \start
	CFI_STARTPROC simple
	CFI_SIGNAL_FRAME
	CFI_DEF_CFA rsp,8+\offset
	.else
	CFI_DEF_CFA_OFFSET 8+\offset
	.endif
	.endm

XCPT_FRAME 宏展开后,全都是一些以 CFI_* 开头的宏,这些宏定义在文件 arch/x86/include/asm/dwarf2.h 中:

// file: arch/x86/include/asm/dwarf2.h
/*
 * Macros for dwarf2 CFI unwind table entries.
 * See "as.info" for details on these pseudo ops. Unfortunately
 * they are only supported in very new binutils, so define them
 * away for older version.
 */

#ifdef CONFIG_AS_CFI

#define CFI_STARTPROC		.cfi_startproc
#define CFI_ENDPROC		.cfi_endproc
#define CFI_DEF_CFA		.cfi_def_cfa
#define CFI_DEF_CFA_REGISTER	.cfi_def_cfa_register
#define CFI_DEF_CFA_OFFSET	.cfi_def_cfa_offset
#define CFI_ADJUST_CFA_OFFSET	.cfi_adjust_cfa_offset
#define CFI_OFFSET		.cfi_offset
#define CFI_REL_OFFSET		.cfi_rel_offset
#define CFI_REGISTER		.cfi_register
#define CFI_RESTORE		.cfi_restore
#define CFI_REMEMBER_STATE	.cfi_remember_state
#define CFI_RESTORE_STATE	.cfi_restore_state
#define CFI_UNDEFINED		.cfi_undefined
#define CFI_ESCAPE		.cfi_escape

#ifdef CONFIG_AS_CFI_SIGNAL_FRAME
#define CFI_SIGNAL_FRAME	.cfi_signal_frame
#else
#define CFI_SIGNAL_FRAME
#endif

#if defined(CONFIG_AS_CFI_SECTIONS) && defined(__ASSEMBLY__)
	/*
	 * Emit CFI data in .debug_frame sections, not .eh_frame sections.
	 * The latter we currently just discard since we don't do DWARF
	 * unwinding at runtime.  So only the offline DWARF information is
	 * useful to anyone.  Note we should not use this directive if this
	 * file is used in the vDSO assembly, or if vmlinux.lds.S gets
	 * changed so it doesn't discard .eh_frame.
	 */
	.cfi_sections .debug_frame
#endif

#else

/*
 * Due to the structure of pre-exisiting code, don't use assembler line
 * comment character # to ignore the arguments. Instead, use a dummy macro.
 */
.macro cfi_ignore a=0, b=0, c=0, d=0
.endm

#define CFI_STARTPROC		cfi_ignore
#define CFI_ENDPROC		cfi_ignore
#define CFI_DEF_CFA		cfi_ignore
#define CFI_DEF_CFA_REGISTER	cfi_ignore
#define CFI_DEF_CFA_OFFSET	cfi_ignore
#define CFI_ADJUST_CFA_OFFSET	cfi_ignore
#define CFI_OFFSET		cfi_ignore
#define CFI_REL_OFFSET		cfi_ignore
#define CFI_REGISTER		cfi_ignore
#define CFI_RESTORE		cfi_ignore
#define CFI_REMEMBER_STATE	cfi_ignore
#define CFI_RESTORE_STATE	cfi_ignore
#define CFI_UNDEFINED		cfi_ignore
#define CFI_ESCAPE		cfi_ignore
#define CFI_SIGNAL_FRAME	cfi_ignore

#endif

如果开启了内核配置选项 CONFIG_AS_CFI,这些以 CFI_ 开头的宏,会扩展为与 dwarf2 相关的伪指令,这些指令仅用来调试,并不影响代码功能;如果关闭了选项 CONFIG_AS_CFI,这些宏会扩展成无意义的宏 cfi_ignore

本文不涉及调试及追踪相关的内容,所以该宏略过,不做分析。

2.2.2 宏 ASM_CLAC

ASM_CLAC 定义如下,该宏内部又引用了ASM_NOP3__ASM_CLACX86_FEATURE_SMAPALTERNATIVE 宏:

// file: arch/x86/include/asm/smap.h
#define ASM_CLAC \
	ALTERNATIVE(ASM_NOP3, __stringify(__ASM_CLAC), X86_FEATURE_SMAP)

ASM_NOP3

ASM_NOP3表示 3 字节的 nop (No Operation,无操作)汇编指令。nop 相关的指令只影响 %rip 寄存器,对其它机器上下文没有任何影响。nop 指令有单字节和多字节等多种形式,不同处理器推荐的多字节 nop 指令也不相同。

Intel 文档推荐以下形式的多字节指令:

NOP.png

对于AMD Athlon 64 和 Opteron 处理器,推荐的是代码填充格式:

NOM-AMD.png

ASM_NOP3 定义在文件 arch/x86/include/asm/nops.h 中,根据配置不同,扩展后的指令也不同。

// file: arch/x86/include/asm/nops.h
#if defined(CONFIG_MK7)
...
#define ASM_NOP3 _ASM_MK_NOP(K7_NOP3)
... 
#elif defined(CONFIG_X86_P6_NOP)
...
#define ASM_NOP3 _ASM_MK_NOP(P6_NOP3)
...
    
#elif defined(CONFIG_X86_64)
...
#define ASM_NOP3 _ASM_MK_NOP(K8_NOP3)
...
#else
...
#define ASM_NOP3 _ASM_MK_NOP(GENERIC_NOP3)
...
#endif

如果开启了内核配置选项 CONFIG_X86_P6_NOP,那么 ASM_NOP3 最终会扩展为 .byte 0x0f,0x1f,0x00,这是 Intel 文档推荐的 nop3 指令。

// file: arch/x86/include/asm/nops.h
#define P6_NOP3	0x0f,0x1f,0x00

#define _ASM_MK_NOP(x) .byte x

如果没有开启CONFIG_X86_P6_NOP配置,但配置了CONFIG_X86_64 选项,那么宏 ASM_NOP3 最终会扩展为 .byte 0x66,0x66,0x90,这是 AMD 文档推荐的 nop3 指令。

// file: arch/x86/include/asm/nops.h
/* Opteron 64bit nops
   1: nop
   2: osp nop
   3: osp osp nop
   4: osp osp osp nop
*/
#define K8_NOP1 GENERIC_NOP1
#define K8_NOP2	0x66,K8_NOP1
#define K8_NOP3	0x66,K8_NOP2

#define GENERIC_NOP1 0x90

其它配置类似,但无论如何,都会扩展成 3 字节的 nop 指令。

X86_FEATURE_SMAP

X86_FEATURE_SMAP 表示 CPU 的 SMAP (Supervisor Mode Access Prevention) 特征,其定义如下:

// file: arch/x86/include/asm/cpufeature.h
#define X86_FEATURE_SMAP	(9*32+20) /* Supervisor Mode Access Prevention */

正常情况下,高特权级的程序是可以访问低特权级的数据的。但是部分 Intel 处理器支持 SMAP 特征,该特征让处理器有能力阻止高特权级程序访问低特权级数据。如果处理器支持 SMAP 特征,那么高特权级条件下能否访问低特权级数据,跟 CR4.SMAP、EFLAGS.AC 以及 CR0.WP 标志都有关系,具体可参考 Intel SDM Volume 3A, 4.6 ACCESS RIGHTS

在 Linux 系统下,可以通过 cpuid 指令查看当前 CPU 是否支持 SMAP 特征。在我的 Ubuntu 虚拟机下,查看结果如下:

# cpuid |grep SMAP
      SMAP: supervisor mode access prevention  = false

可见,虚拟机的处理器并不支持 SMAP 特征。

__ASM_CLAC

__ASM_CLAC扩展为汇编指令 clac 的操作码,操作码占用 3 个字节:

// file: arch/x86/include/asm/smap.h
#define __ASM_CLAC	.byte 0x0f,0x01,0xca

clac (Clear AC Flag in EFLAGS Register)指令会清除 EFLAGS寄存器中 AC (Access Control)标志。

EFLAGE寄存器状态图.png

Intel SDM 对该指令说明如下:

Clears the AC flag bit in EFLAGS register. This disables any alignment checking of user-mode data accesses. If the

SMAP bit is set in the CR4 register, this disallows explicit supervisor-mode data accesses to user-mode pages.

clac 指令只有在处理器支持 SMAP (Supervisor Mode Access Prevention) 特征时才能使用,否则会触发无效操作码异常(Invalid Opcode Exception,#UD)。

宏 ALTERNATIVE

接下来就是宏 ASM_CLAC 的主体部分了:

ALTERNATIVE(ASM_NOP3, __stringify(__ASM_CLAC), X86_FEATURE_SMAP)

ALTERNATIVE 接收 3 个参数,分别是:原始指令,替代指令及 CPU 特征。

该宏实现的功能:在编译时,默认将原始指令编译到二进制文件。在内核启动时,如果参数中的 CPU 特征可用,则用替代指令替换掉原始指令;否则使用原始指令。

在我们的案例中,如果 CPU 支持 SMAP 特征,则使用 clac 指令替换掉 nop3 指令。

该替换功能的实现细节,本文暂不涉及,会在后续文章中介绍。

总结

综上所述,如果处理器支持 SMAP 特征,该宏在运行时会替换成 clac 汇编指令;否则,该宏扩展成 3 字节的 nop 指令。所以,宏 ASM_CLAC 本意就是针对支持 SMAP 特征的处理器,使用汇编指令 clac,屏蔽掉处理器的 SMAP 功能,使高特权级的程序可以随意访问低特权级的数据

2.2.3 宏 PARAVIRT_ADJUST_EXCEPTION_FRAME

PARAVIRT_ADJUST_EXCEPTION_FRAME 定义如下:

// file: arch/x86/include/asm/paravirt.h
#define PARAVIRT_ADJUST_EXCEPTION_FRAME	/*  */

该宏无实际意义。

2.2.4 保存上下文 - error_entry

当异常发生时,如果程序处于用户态,那么会处理器会自动将栈切换到内核态,并将 SS、RSP、RFLAGS、CS、 RIP 及 错误码(如果有的话)压栈;如果程序处于内核态,那么不用切换栈,直接将上述几个寄存器及错误码(如果有的话)压栈。从 errorentry 入口进入的异常处理程序,都是有错误码的,所以此时内核栈布局如下图所示:

interrupt_stack.png

在执行如下指令后,栈指针 %rsp 向下移动 120 个字节,为其它寄存器入栈准备空间。

subq $ORIG_RAX-R15, %rsp

其中宏 ORIG_RAXR15 定义在 arch/x86/include/asm/calling.h文件中,表示对应寄存器值在 pt_regs 结构体中的偏移量:

// file: arch/x86/include/asm/calling.h
/*
 * 64-bit system call stack frame layout defines and helpers,
 * for assembly code:
 */

#define R15		  0
#define R14		  8
#define R13		 16
#define R12		 24
#define RBP		 32
#define RBX		 40

/* arguments: interrupts/non tracing syscalls only save up to here: */
#define R11		 48
#define R10		 56
#define R9		 64
#define R8		 72
#define RAX		 80
#define RCX		 88
#define RDX		 96
#define RSI		104
#define RDI		112
#define ORIG_RAX	120       /* + error_code */
/* end of arguments */

/* 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

#define ARGOFFSET	R11
#define SWFRAME		ORIG_RAX

虽然 %rsp 指针下移了 120 个字节,但栈中的数据没变化,此时内核栈布局如下:

interrupt_handler_stack2.png

接下来执行

call error_entry

由于 call 指令会将返回地址压栈,所以在执行完 call 指令之后,内核栈布局如下:

interrupt_handler_stack_with_return_addr.png

error_entry 函数主要执行上下文的保存工作,其定义如下:

/*
 * Exception entry point. This expects an error code/orig_rax on the stack.
 * returns in "no swapgs flag" in %ebx.
 */
ENTRY(error_entry)
	XCPT_FRAME
	CFI_ADJUST_CFA_OFFSET 15*8
	/* oldrax contains error code */
	cld
	movq_cfi rdi, RDI+8
	movq_cfi rsi, RSI+8
	movq_cfi rdx, RDX+8
	movq_cfi rcx, RCX+8
	movq_cfi rax, RAX+8
	movq_cfi  r8,  R8+8
	movq_cfi  r9,  R9+8
	movq_cfi r10, R10+8
	movq_cfi r11, R11+8
	movq_cfi rbx, RBX+8
	movq_cfi rbp, RBP+8
	movq_cfi r12, R12+8
	movq_cfi r13, R13+8
	movq_cfi r14, R14+8
	movq_cfi r15, R15+8
	xorl %ebx,%ebx
	testl $3,CS+8(%rsp)
	je error_kernelspace
error_swapgs:
	SWAPGS
error_sti:
	TRACE_IRQS_OFF
	ret

...
...

END(error_entry)

忽略调试和追踪相关的代码,我们从 cld 指令开始分析。

cld 指令会清除 RFLAGS 寄存器中的 DF 标志。 当 DF 位被设置为 0 时, 字符串操作相关指令,比如 stosmovs 等,会增加索引寄存器(ESI 或 EDI)的值;也就是说处理字符串时,从低地址到高地址开始处理。否则,当 DF 位置 1 时(通过 std 指令),字符串从高地址到低地址开始处理。

movq_cfi是个宏,其定义如下:

// file:arch/x86/include/asm/dwarf2.h
    .macro movq_cfi reg offset=0
    movq %\reg, \offset(%rsp)
    CFI_REL_OFFSET \reg, \offset
    .endm

忽略 CFI_ 开头的相关指令后,该宏会把指定寄存器中的值保存到栈中指定偏移处。保存后,栈布局如下:

interrupt_handler_stack3.png

将寄存器存入栈后,通过异或指令 xorl%ebx 寄存器设置为 0:

xorl %ebx,%ebx

%ebx 寄存器用来标识在恢复上下文(error_exit 函数)时是否需要执行 swapgs 指令。当 %ebx 为 0 时,需要执行 swapgs 指令;否则,不需要执行。

接下来需要检查异常是来自用户空间还是内核空间,如果是来自内核空间,跳转到 error_kernelspace 处执行:

testl $3,CS+8(%rsp)
je error_kernelspace

通过 CS+8(%rsp)获取到被中断程序的代码段选择子,如果异常发生在用户空间,那么该选择子特权级为 3,否则为 0。通过将选择子与立即数 3 进行与操作,就能判断出被中断程序来自用户空间还是内核空间。testl $3,CS+8(%rsp)指令将 2 个操作数进行与操作,如果结果为0,会将 RFLAGS 中的 ZF 标志置位。je 为条件跳转指令,它会检查 RFLAGS 中的 ZF 标志,如果 ZF 标志被置位,说明被中断程序来自内核空间,则跳转到 error_kernelspace 处执行。

异常发生在用户空间

如果被中断程序来自用户空间,那么直接执行 swapgs 指令,然后返回。

error_swapgs:
	SWAPGS
error_sti:
	TRACE_IRQS_OFF
	ret

异常发生在内核空间

如果被中断程序来自内核空间,会执行 error_kernelspace 代码。

/*
 * There are two places in the kernel that can potentially fault with
 * usergs. Handle them here. The exception handlers after iret run with
 * kernel gs again, so don't set the user space flag. B stepping K8s
 * sometimes report an truncated RIP for IRET exceptions returning to
 * compat mode. Check for these here too.
 */
error_kernelspace:
	incl %ebx
	leaq irq_return(%rip),%rcx
	cmpq %rcx,RIP+8(%rsp)
	je error_swapgs
	movl %ecx,%eax	/* zero extend */
	cmpq %rax,RIP+8(%rsp)
	je bstep_iret
	cmpq $gs_change,RIP+8(%rsp)
	je error_swapgs
	jmp error_sti

bstep_iret:
	/* Fix truncated RIP */
	movq %rcx,RIP+8(%rsp)
	jmp error_swapgs
	CFI_ENDPROC

error_kernelspace处,先让 %ebx 加 1,表示在恢复上下文时,不需要执行 swapgs 指令。这里的逻辑是,swapgs必须成对使用,既然发生异常时程序处于内核空间,那么肯定有其它程序已经执行过 swapgs 指令了,本着谁执行谁恢复的原则,其它程序在得到执行权后肯定还会再次执行 swapgs 指令,当前程序就没必要越俎代庖了。

按理说,程序执行到这里就该 ret 返回了。像这种不需要使用 paranoid 方法的异常,在判断出异常是发生在内核空间后,是不需要执行 swapgs 命令的,所以应该没啥工作要做了。 但是,凡事总有例外。根据注释描述,有 2 处发生在内核空间的异常,其 GS 却是用户态的,所以需要在这里修复。另外,当在 K8s 中从 iret 异常返回到兼容模式时,RIP 的值会被截断,这里也一并修复。

从代码可以看到,内核中需要修复的 2 处异常地址,分别是 irq_returngs_change

修复irq_return处产生的异常

irq_return主要功能就是执行 iret指令从中断返回,其实现如下:

// file: arch/x86/kernel/entry_64.S
irq_return:
	INTERRUPT_RETURN

其中,宏 INTERRUPT_RETURN扩展为 iretq指令。

// file: arch/x86/include/asm/irqflags.h
#define INTERRUPT_RETURN	iretq

也就是说,在执行 iret 指令时,有可能会发生异常。此时,GS 处于用户态,所以需要执行 swapgs 指令将 GS 切换到内核态。

为什么 iret 时 GS 处于用户态呢?因为,有些异常是发生在用户空间的,而异常处理程序需要在内核空间执行,在从用户空间切换到内核空间时,需要使用 swapgs 将 GS 切换到内核态;对应的,在使用 iret 返回到用户空间之前,需要先使用 swapgs 将 GS 切换到用户态。在这种情况下,如果执行 iret 指令发生异常,GS 必然处于用户态,所以需要再次使用 swapgs 指令切换到内核态 GS。

leaq irq_return(%rip),%rcxirq_return的地址存入 %rcx 寄存器。然后把irq_return地址与被中断程序的 RIP 进行比较。

如果相等,说明是在irq_return处执行 iret 时发生了异常,此时 GS 处于用户态,需要使用 swapgs 将 GS 切换成内核态。所以,程序跳转到 error_swapgs 处去执行 swapgs ,然后返回。

修复 K8s 内 iret 产生的异常

上文说了,当从 K8s 中通过 iret 返回时,被中断程序的 RIP 在上报时可能会被截断。这里就需要对 RIP 进行检查,判断是否是被截断了的 irq_return 地址。

movl %ecx,%eax 指令将 %ecx 的值保存到 %eaxmovl指令以寄存器作为目的时,会把该寄存器的高位 4 字节设置为 0,也就是会 0 扩展到 %raxmovl指令执行后,%rax 的低 4 字节是有效地址,高 4 字节被设置为 0。 然后比较 %rax 与被中断程序的 RIP 的值。如果相等,说明 RIP 被截断了,是在 k8s 中执行 iret 时出现的异常,则跳转到 bstep_iret 处执行。bstep_iret 先是使用 %rcx 中的值修复被中断程序的 RIP,然后跳转到 error_swapgs处,通过 swapgs 切换到内核态 GS,然后返回。

修复gs_change处产生的异常

除了在 irq_return 处执行 iret 指令发生异常时 GS 处于用户态之外;在 gs_change 处发生异常时, GS 也处于用户态,同样需要使用 swapgs 切换到内核态 GS 进行修复。gs_change 标签位于 native_load_gs_index 函数内部,该函数的功能是将用户态段选择子加载到 GS 寄存器。由于native_load_gs_index 函数运行在内核态,而要加载的是用户态选择子,所以需要先使用 swapgs 将 GS 切换到用户态才能加载。如果此时发生异常,那么 GS 处于用户态,所以需要使用 swapgs 指令切换 GS。

// file: arch/x86/kernel/entry_64.S
	/* Reload gs selector with exception handling */
	/* edi:  new selector */
ENTRY(native_load_gs_index)
	CFI_STARTPROC
	pushfq_cfi
	DISABLE_INTERRUPTS(CLBR_ANY & ~CLBR_RDI)
	SWAPGS
gs_change:
	movl %edi,%gs
2:	mfence		/* workaround */
	SWAPGS
	popfq_cfi
	ret
	CFI_ENDPROC
END(native_load_gs_index)

修复过程如下:

	cmpq $gs_change,RIP+8(%rsp)
	je error_swapgs

先是通过 cmpq 指令检查异常是否发生在 gs_change 处,如果是的话,跳转到 error_swapgs 处,执行 swapgs 指令切换到内核态 GS,然后执行 ret 指令返回。

如果以上三种情况都不存在,那么跳转到 error_sti 处,直接执行 ret 返回。

使用 ret 指令从 error_entry 返回时,处理器会自动弹出返回地址,此时内核栈布局如下:

interrupt_handler_stack4.png

2.2.5 DEFAULT_FRAME

DEFAULT_FRAME定义的是一些与调试相关的内容,本文暂不涉及。

2.2.6 调用 do_sym 程序

接下来,把 %rsppt_regs 结构体指针)传递到 %rdi,把错误码传递到 %rsi 寄存器,这两个寄存器会做为函数参数传递给实际处理程序。

然后,把 -1 保存到栈中保存错误码的地址处,由于 -1 不是一个正确的系统调用号,可以防止错误的使用了系统调用。

接下来,调用 do_sym 程序,在本例中,就是 do_page_fault 程序。

2.2.7 恢复上下文- error_exit

最后,跳转到 error_exit 处执行。

// file: arch/x86/kernel/entry_64.S
/* ebx:	no swapgs flag (1: don't need swapgs, 0: need it) */
ENTRY(error_exit)
	DEFAULT_FRAME
	movl %ebx,%eax
	RESTORE_REST
	DISABLE_INTERRUPTS(CLBR_NONE)
	TRACE_IRQS_OFF
	GET_THREAD_INFO(%rcx)
	testl %eax,%eax
	jne retint_kernel
	LOCKDEP_SYS_EXIT_IRQ
	movl TI_flags(%rcx),%edx
	movl $_TIF_WORK_MASK,%edi
	andl %edi,%edx
	jnz retint_careful
	jmp retint_swapgs
	CFI_ENDPROC
END(error_exit)

error_exit会恢复上下文,并执行其它收尾工作。

%ebx 寄存器中保存着是否需要执行 swapgs 指令的标志,如果 %ebx 为 1,不需要执行 swapgs 指令;如果为 0,需要执行。

movl %ebx,%eax%ebx 的值传送到 %eax,以备后用。

RESTORE_REST 从栈中恢复 %rbx ~ %r15 共 6 个寄存器的值,然后将 %rsp 增加相应的值 (6 * 8 = 48)。

// file: arch/x86/include/asm/calling.h
.macro RESTORE_REST
	movq_cfi_restore 0*8, r15
	movq_cfi_restore 1*8, r14
	movq_cfi_restore 2*8, r13
	movq_cfi_restore 3*8, r12
	movq_cfi_restore 4*8, rbp
	movq_cfi_restore 5*8, rbx
	addq $REST_SKIP, %rsp
	CFI_ADJUST_CFA_OFFSET	-(REST_SKIP)
.endm
        
#define REST_SKIP	(6*8)
// file: arch/x86/include/asm/dwarf2.h
.macro movq_cfi_restore offset reg
	movq \offset(%rsp), %\reg
	CFI_RESTORE \reg
.endm

DISABLE_INTERRUPTS(CLBR_NONE)宏禁止外部中断。

// file: arch/x86/include/asm/irqflags.h
#define DISABLE_INTERRUPTS(x)	cli

宏 GET_THREAD_INFO

// file: arch/x86/include/asm/thread_info.h
/* how to get the thread information struct from ASM */
#define GET_THREAD_INFO(reg) \
	movq PER_CPU_VAR(kernel_stack),reg ; \
	subq $(THREAD_SIZE-KERNEL_STACK_OFFSET),reg

#define KERNEL_STACK_OFFSET (5*8)

通过 GET_THREAD_INFO宏,获取到 thread_info 结构体的地址,保存到指定的寄存器中。本例中,将 thread_info 结构体的地址保存到了 %rcx 寄存器中。关于内核栈的详细信息,请参考 Linux Kernel:中断和异常处理程序的早期初始化(续)

testl %eax,%eax检测 %eax 是否为 0,以判断是否需要执行 swapgs 指令。

返回到内核空间

如果不为 0,则不需要执行 swapgs,说明异常发生在内核空间,跳转到 retint_kernel 执行。

retint_kernel 被定义为 retint_restore_args

#define retint_kernel retint_restore_args

retint_restore_args 实现如下:

retint_restore_args:	/* return to kernel space */
	DISABLE_INTERRUPTS(CLBR_ANY)
	/*
	 * The iretq could re-enable interrupts:
	 */
	TRACE_IRQS_IRETQ
restore_args:
	RESTORE_ARGS 1,8,1

irq_return:
	INTERRUPT_RETURN
	_ASM_EXTABLE(irq_return, bad_iret)

可以看到,retint_restore_args先是禁止外部中断,然后恢复传参用寄存器,最后通过 iret 返回。

RESTORE_ARGS宏定义于arch/x86/include/asm/calling.h文件,其又引入了movq_cfi_restore宏。movq_cfi_restore宏定义在arch/x86/include/asm/dwarf2.h文件中,我们介绍 RESTORE_REST 时已经遇到过了。

// file: arch/x86/include/asm/calling.h
#define ARG_SKIP	(9*8)

	.macro RESTORE_ARGS rstor_rax=1, addskip=0, rstor_rcx=1, rstor_r11=1, \
			    rstor_r8910=1, rstor_rdx=1
	.if \rstor_r11
	movq_cfi_restore 0*8, r11
	.endif

	.if \rstor_r8910
	movq_cfi_restore 1*8, r10
	movq_cfi_restore 2*8, r9
	movq_cfi_restore 3*8, r8
	.endif

	.if \rstor_rax
	movq_cfi_restore 4*8, rax
	.endif

	.if \rstor_rcx
	movq_cfi_restore 5*8, rcx
	.endif

	.if \rstor_rdx
	movq_cfi_restore 6*8, rdx
	.endif

	movq_cfi_restore 7*8, rsi
	movq_cfi_restore 8*8, rdi

	.if ARG_SKIP+\addskip > 0
	addq $ARG_SKIP+\addskip, %rsp
	CFI_ADJUST_CFA_OFFSET	-(ARG_SKIP+\addskip)
	.endif
	.endm

RESTORE_ARGS 1,8,1执行时,先是从栈中恢复 %rdi ~ %r11 共 9 个寄存器,然后 %rsp 增加 80,释放了 10 个寄存器空间。之所以释放 10 个,是因为 %rdi 寄存器之上是保存的是错误码,当使用 iret 指令返回时,错误码不会自动弹出,需要手动释放错误码占用的空间。

在调用 retint_restore_args 之前,已经通过宏 RESTORE_REST 恢复了 6 个寄存器。retint_restore_args 中恢复了 9 个寄存器,并手动释放了错误码占用的空间。在通过 iret 指令返回时,会自动恢复架构定义的 5 个寄存器。至此,所有寄存器全部恢复完成。

返回到用户空间

如果 testl %eax,%eax检测结果为 0,说明需要执行 swapgs 指令,同时也说明了,异常发生在用户空间。对于用户空间的程序,在从中断或异常返回时,有些线程标志需要处理。

movl TI_flags(%rcx),%edx

%rcx 中保存的是 thread_info 结构体地址,TI_flags 是成员 flags 在 thread_info 结构体中的偏移量,其值为 16。

// file: include/generated/asm-offsets.h
#define TI_flags 16 /* offsetof(struct thread_info, flags)	# */
// file: arch/x86/include/asm/thread_info.h
struct thread_info {
	struct task_struct	*task;		/* main task structure */
	struct exec_domain	*exec_domain;	/* execution domain */
	__u32			flags;		/* low level flags */
	...
};

TI_flags(%rcx) 获取到 thread_info 中的线程标志,然后保存到 %edx 寄存器。

然后把 _TIF_WORK_MASK的值拷贝到 %edi 寄存器。

movl $_TIF_WORK_MASK,%edi

有些工作需要在中断或异常返回时处理,_TIF_WORK_MASK 宏定义了需要处理的工作的掩码位。

// file: arch/x86/include/asm/thread_info.h
/* work to do on interrupt/exception return */
#define _TIF_WORK_MASK							\
	(0x0000FFFF &							\
	 ~(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|			\
	   _TIF_SINGLESTEP|_TIF_SECCOMP|_TIF_SYSCALL_EMU))

_TIF_WORK_MASK宏又引用了一些线程标志位相关的宏,这些标志主要跟系统调用和安全计算相关:

// file: arch/x86/include/asm/thread_info.h
#define _TIF_SYSCALL_TRACE	(1 << TIF_SYSCALL_TRACE)
#define _TIF_SINGLESTEP		(1 << TIF_SINGLESTEP)
#define _TIF_SYSCALL_EMU	(1 << TIF_SYSCALL_EMU)
#define _TIF_SYSCALL_AUDIT	(1 << TIF_SYSCALL_AUDIT)
#define _TIF_SECCOMP		(1 << TIF_SECCOMP)

#define TIF_SYSCALL_TRACE	0	/* syscall trace active */
#define TIF_SINGLESTEP		4	/* reenable singlestep on user return*/
#define TIF_SYSCALL_EMU		6	/* syscall emulation active */
#define TIF_SYSCALL_AUDIT	7	/* syscall auditing active */
#define TIF_SECCOMP		8	/* secure computing */

%edi%edx进行与操作,以判断是否有需要处理的工作

andl %edi,%edx

如果与操作后结果不为 0,说明有工作需要处理,跳转到 retint_careful 处进行处理。由于这部分主要是一些追踪、审计、调试、安全相关的内容,我们暂不讨论。

jnz retint_careful

否则,跳转到 retint_swapgs 处执行。

jmp retint_swapgs

我们来看下 retint_swapgs 处的执行流程:

retint_swapgs:		/* return to user-space */
	/*
	 * The iretq could re-enable interrupts:
	 */
	DISABLE_INTERRUPTS(CLBR_ANY)
	TRACE_IRQS_IRETQ
	SWAPGS
	jmp restore_args

...

restore_args:
	RESTORE_ARGS 1,8,1

irq_return:
	INTERRUPT_RETURN
	_ASM_EXTABLE(irq_return, bad_iret)

先是禁止外部中断,然后执行 swapgs 指令返回到用户空间的 GS,接下来跳转到 restore_args 恢复传参用寄存器,最后通过 iret 返回用户空间。

至此,异常入口 errorentry 的执行过程分析完毕。

2.3 无错误码的异常入口 zeroentry

说完了 errorentry 的执行流程,我们再来看下 zeroentry。zeroentry 表示没有错误码,且未使用 paranoid 方法的异常处理入口。其定义如下:

// file: arch/x86/kernel/entry_64.S
.macro zeroentry sym do_sym
ENTRY(\sym)
	INTR_FRAME
	ASM_CLAC
	PARAVIRT_ADJUST_EXCEPTION_FRAME
	pushq_cfi $-1		/* ORIG_RAX: no syscall to restart */
	subq $ORIG_RAX-R15, %rsp
	CFI_ADJUST_CFA_OFFSET ORIG_RAX-R15
	call error_entry
	DEFAULT_FRAME 0
	movq %rsp,%rdi		/* pt_regs pointer */
	xorl %esi,%esi		/* no error code */
	call \do_sym
	jmp error_exit		/* %ebx: no swapgs flag */
	CFI_ENDPROC
END(\sym)
.endm

对比 zeroentry 与 errorentry 的代码可以发现,两者流程基本相同。唯一的不同是对错误码的处理上。因为使用 zeroentry 入口的异常没有错误码,所以在调用 error_entry 保存上下文之前,先往栈里 ORIG_RAX 位置处压入了伪错误码 -1;而 errorentry 是在将错误码拷贝到 %rsi 寄存器后,将 ORIG_RAX 处的值修改为 -1。

.macro errorentry sym do_sym
ENTRY(\sym)
	XCPT_FRAME
	ASM_CLAC
	PARAVIRT_ADJUST_EXCEPTION_FRAME
	subq $ORIG_RAX-R15, %rsp
	CFI_ADJUST_CFA_OFFSET ORIG_RAX-R15
	call error_entry
	DEFAULT_FRAME 0
	movq %rsp,%rdi			/* pt_regs pointer */
	movq ORIG_RAX(%rsp),%rsi	/* get error code */
	movq $-1,ORIG_RAX(%rsp)		/* no syscall to restart */
	call \do_sym
	jmp error_exit			/* %ebx: no swapgs flag */
	CFI_ENDPROC
END(\sym)
.endm

三、参考资料

1、Intel 64 and IA-32 Architectures Software Developer Manuals

2、Software Optimization Guide for the AMD Athlon 64 and AMD Opteron Processors - 25112 Chapter 4.11

3、内核文档 entry_64.txt

4、x86 函数调用习惯

5、GCC 函数属性 regparm

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