六、进阶 | 中断和中断处理(6)

中断和中断处理。第6部分。

不可屏蔽中断处理程序

这是Linux内核中断和中断处理章节的第六部分,上一部分我们看到了实现一些异常处理程序,例如一般保护故障、除法异常、无效操作码异常等。正如我在上一部分所写的,我们将在这一部分看到剩余异常的实现。我们将看到以下处理程序的实现:

现在,让我们开始。

不可屏蔽中断处理

不可屏蔽中断是一种硬件中断,不能通过标准的屏蔽技术忽略。一般来说,不可屏蔽中断可以通过以下两种方式之一产生:

  • 外部硬件在CPU上断言不可屏蔽中断引脚
  • 处理器在系统总线或APIC串行总线上接收到一个带有NMI传递模式的消息。

当处理器从这些源之一接收到NMI时,它会立即通过调用中断向量号为2NMI处理程序来处理它(参见第一部分的表)。我们已经用中断描述符表填充了向量号nmi中断处理程序的地址和NMI_STACK中断栈表条目

set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);

在定义在arch/x86/kernel/traps.c源代码文件中的trap_init函数中。在之前的部分中,我们看到所有中断处理程序的入口点都是用以下宏定义的:

.macro idtentry sym do_sym has_error_code:req paranoid=0 shift_ist=-1
ENTRY(\sym)
...
...
...
END(\sym)
.endm

来自arch/x86/entry/entry_64.S汇编源代码文件。但是,Non-Maskable中断的处理程序不是用这个宏定义的。它有自己的入口点:

ENTRY(nmi)
...
...
...
END(nmi)

在同一个arch/x86/entry/entry_64.S汇编文件中。让我们深入了解它,并尝试理解Non-Maskable中断处理程序是如何工作的。nmi处理程序从调用以下宏开始:

PARAVIRT_ADJUST_EXCEPTION_FRAME

但我们不会在这一部分深入细节,因为这个宏与准虚拟化有关,我们将在另一章中看到。在这之后,将rdx寄存器的内容保存到栈上:

pushq	%rdx

并分配检查cs在非屏蔽中断发生时是否不是内核段:

cmpl	$__KERNEL_CS, 16(%rsp)
jne	first_nmi

__KERNEL_CS宏定义在arch/x86/include/asm/segment.h中,并表示全局描述符表中的第二个描述符:

#define GDT_ENTRY_KERNEL_CS	2
#define __KERNEL_CS	(GDT_ENTRY_KERNEL_CS*8)

关于GDT的更多信息,你可以在Linux内核启动过程的第二部分中阅读。如果cs不是内核段,这意味着它不是嵌套的NMI,我们跳到first_nmi标签。让我们考虑这种情况。首先,我们在first_nmi标签中将当前栈指针的地址放入rdx,并在栈上推送1

first_nmi:
	movq	(%rsp), %rdx
	pushq	$1

我们为什么要在栈上推送1?正如注释所说:“我们允许在NMIs中设置断点”。在x86_64上,像其他架构一样,CPU在完成第一个NMI之前不会执行另一个NMINMI中断像其他中断和异常一样,以iret指令结束。如果NMI处理程序触发了页面错误断点或其他使用iret指令的异常,如果这发生在NMI上下文中,CPU将离开NMI上下文,新的NMI可能进来。用于从这些异常返回的iret将重新启用NMIs,我们将得到嵌套的非屏蔽中断。问题是NMI处理程序不会返回到它触发异常时的状态,而是返回到一个将允许新的NMIs抢占运行中的NMI处理程序的状态。如果另一个NMI在第一个NMI处理程序完成之前进来,新的NMI将覆盖被抢占的NMIs栈。我们可以有嵌套的NMIs,下一个NMI使用前一个NMI的栈顶。这意味着我们不能执行它,因为嵌套的非屏蔽中断会破坏前一个非屏蔽中断的栈。这就是为什么我们在栈上为临时变量分配了空间。我们将检查这个变量是否在执行前一个NMI时被设置,并在不是嵌套NMI时清除。我们在这里推送1到之前在栈上分配的空间,以表示当前正在执行non-maskable中断。记住,当NMI或其他异常发生时,我们有以下堆栈帧

+------------------------+
|         SS             |
|         RSP            |
|        RFLAGS          |
|         CS             |
|         RIP            |
+------------------------+

如果异常有错误代码,还会有错误代码。所以,在所有这些操作之后,我们的堆栈帧将看起来像这样:

+------------------------+
|         SS             |
|         RSP            |
|        RFLAGS          |
|         CS             |
|         RIP            |
|         RDX            |
|          1             |
+------------------------+

接下来,我们在栈上分配另外40个字节:

subq	$(5*8), %rsp

并在分配的空间后面推送原始堆栈帧的副本:

.rept 5
pushq	11*8(%rsp)
.endr

.rept汇编指令。我们需要原始中断堆栈的副本。一般来说,我们需要两份中断堆栈的副本。第一个是copied中断堆栈:saved堆栈帧和copied堆栈帧。现在我们将原始堆栈帧推送到saved堆栈帧,它位于刚刚分配的40字节之后(copied堆栈帧)。这个堆栈帧用于修复嵌套NMI可能更改的copied堆栈帧。第二个是copied堆栈帧,由任何嵌套的NMIs修改,以让第一个NMI知道我们触发了第二个NMI,我们应该重复第一个NMI处理程序。好的,我们已经制作了原始堆栈帧的第一份副本,现在该制作第二份副本了: assembly addq $(10*8), %rsp

.rept 5 pushq -68(%rsp) .endr subq $(58), %rsp


经过所有这些操作后,我们的堆栈帧将如下所示:

+-------------------------+ | original SS | | original Return RSP | | original RFLAGS | | original CS | | original RIP | +-------------------------+ | temp storage for rdx | +-------------------------+ | NMI executing variable | +-------------------------+ | copied SS | | copied Return RSP | | copied RFLAGS | | copied CS | | copied RIP | +-------------------------+ | Saved SS | | Saved Return RSP | | Saved RFLAGS | | Saved CS | | Saved RIP | +-------------------------+


之后,我们在栈上推送一个虚拟错误代码,就像我们在之前的异常处理程序中已经做过的那样,并为通用寄存器在栈上分配空间:

```assembly
pushq	$-1
ALLOC_PT_GPREGS_ON_STACK

我们已经在中断的第三部分看到了ALLOC_PT_GPREGS_ON_STACK宏的实现。这个宏定义在arch/x86/entry/calling.h中,并且又一次在栈上分配了120字节用于通用寄存器,从rdir15

.macro ALLOC_PT_GPREGS_ON_STACK addskip=0
addq	$-(15*8+\addskip), %rsp
.endm

为通用寄存器分配空间后,我们可以看到调用paranoid_entry

call	paranoid_entry

我们可以从之前的部分中记住这个标签。它将通用寄存器推送到栈上,读取MSR_GS_BASE模型特定寄存器并检查其值。如果MSR_GS_BASE的值为负,我们来自内核模式并且只是从paranoid_entry返回,否则意味着我们来自用户模式并且需要执行swapgs指令,这将更改用户gs与内核gs

ENTRY(paranoid_entry)
	cld
	SAVE_C_REGS 8
	SAVE_EXTRA_REGS 8
	movl	$1, %ebx
	movl	$MSR_GS_BASE, %ecx
	rdmsr
	testl	%edx, %edx
	js	1f
	SWAPGS
	xorl	%ebx, %ebx
1:	ret
END(paranoid_entry)

注意,在执行swapgs指令后,我们将ebx寄存器清零。下次我们将检查这个寄存器的内容,如果执行了swapgs,那么ebx必须包含0,否则为1。下一步,我们将cr2控制寄存器的值存储到r12寄存器中,因为NMI处理程序可能会导致页面错误并破坏这个控制寄存器的值:

movq	%cr2, %r12

现在是时候调用实际的NMI处理程序了。我们将pt_regs的地址推送到rdi,错误代码推送到rsi,并调用do_nmi处理程序:

movq	%rsp, %rdi
movq	$-1, %rsi
call	do_nmi

我们将在这部分稍后回到do_nmi,但现在让我们看看do_nmi执行完毕后会发生什么。do_nmi处理程序完成后,我们检查cr2寄存器,因为我们在执行do_nmi期间可能会遇到页面错误,如果遇到,我们恢复原始的cr2,否则我们跳到标签1。之后,我们测试ebx寄存器的内容(记住,如果我们使用了swapgs指令,它必须包含0,如果我们没有使用它,它包含1)并执行SWAPGS_UNSAFE_STACK,如果它包含1,或者跳到nmi_restore标签。SWAPGS_UNSAFE_STACK宏只是展开为swapgs指令。在nmi_restore标签中,我们恢复通用寄存器,清除为这些寄存器分配的栈空间,清除我们的临时变量,并使用INTERRUPT_RETURN宏退出中断处理程序:

	movq	%cr2, %rcx
	cmpq	%rcx, %r12
	je	1f
	movq	%r12, %cr2
1:
	testl	%ebx, %ebx
	jnz	nmi_restore
nmi_swapgs:
	SWAPGS_UNSAFE_STACK
nmi_restore:
	RESTORE_EXTRA_REGS
	RESTORE_C_REGS
	/* Pop the extra iret frame at once */
	REMOVE_PT_GPREGS_FROM_STACK 6*8
	/* Clear the NMI executing stack variable */
	movq	$0, 5*8(%rsp)
	INTERRUPT_RETURN

其中INTERRUPT_RETURN定义在arch/x86/include/asm/irqflags.h中,只是展开为iret指令。就这些。

现在让我们考虑另一种情况,当之前的NMI中断还没有完成执行时,另一个NMI中断发生了。你可以从这部分的开始记住,我们已经检查了我们是否来自用户空间,并在这种情况下跳转到first_nmi

cmpl	$__KERNEL_CS, 16(%rsp)
jne	first_nmi

请注意,在这种情况下,每次都是非屏蔽NMI,因为如果第一个NMI捕获了页面错误、断点或其他异常,它将在内核模式下执行。如果我们不是来自用户空间,首先我们测试我们的临时变量:

cmpl	$1, -8(%rsp)
je	nested_nmi

如果它设置为1,我们跳到nested_nmi标签。如果它不是1,我们测试IST栈。在嵌套NMIs的情况下,我们检查我们是否在repeat_nmi之上。在这种情况下,我们忽略它,否则我们检查我们是否高于end_repeat_nmi并跳到nested_nmi_out标签。

现在让我们看看do_nmi异常处理程序。这个函数定义在arch/x86/kernel/nmi.c源代码文件中,并且接受两个参数:

  • pt_regs的地址;
  • 错误代码。

像所有异常处理程序一样。do_nmi从调用nmi_nesting_preprocess函数开始,以调用nmi_nesting_postprocess结束。nmi_nesting_preprocess函数检查我们可能不在调试栈上工作,如果我们在调试栈上,将update_debug_stack每个CPU变量设置为1并调用arch/x86/kernel/cpu/common.c中的debug_stack_set_zero函数。这个函数增加了每个CPU的debug_stack_use_ctr变量并加载新的中断描述符表

static inline void nmi_nesting_preprocess(struct pt_regs *regs)
{
        if (unlikely(is_debug_stack(regs->sp))) {
                debug_stack_set_zero();
                this_cpu_write(update_debug_stack, 1);
        }
}

nmi_nesting_postprocess函数检查我们在nmi_nesting_preprocess中设置的update_debug_stack每个CPU变量,并重置调试栈,或者换句话说,它加载原始的中断描述符表。在调用nmi_nesting_preprocess函数后,我们可以看到在do_nmi中的nmi_enter调用。nmi_enter增加了被中断进程的lockdep_recursion字段,更新抢占计数器并通知RCU子系统关于NMI。还有一个nmi_exit函数,它和nmi_enter做同样的事情,但是反过来。在nmi_enter之后,我们在irq_stat结构中增加__nmi_count字段,并调用default_do_nmi函数。首先在default_do_nmi`中我们检查前一个nmi的地址并更新最后一个nmi的地址为当前的:

if (regs->ip == __this_cpu_read(last_nmi_rip))
    b2b = true;
else
    __this_cpu_write(swallow_nmi, false);

__this_cpu_write(last_nmi_rip, regs->ip);

在这之后,我们首先需要处理特定于CPU的NMIs

handled = nmi_handle(NMI_LOCAL, regs, b2b);
__this_cpu_add(nmi_stats.normal, handled);

然后是非特定的NMIs,取决于它的原因:

reason = x86_platform.get_nmi_reason();
if (reason & NMI_REASON_MASK) {
	if (reason & NMI_REASON_SERR)
		pci_serr_error(reason, regs);
	else if (reason & NMI_REASON_IOCHK)
		io_check_error(reason, regs);

	__this_cpu_add(nmi_stats.external, 1);
	return;
}

就这些。

范围超出异常

下一个异常是BOUND范围超出异常。BOUND指令确定第一个操作数(数组索引)是否在第二个操作数(界限操作数)指定的数组界限之内。如果索引超出界限,将发生BOUND范围超出异常或#BR#BR异常的处理程序是do_bounds函数,定义在arch/x86/kernel/traps.cdo_bounds处理程序从调用exception_enter函数开始,以调用exception_exit结束:

prev_state = exception_enter();

if (notify_die(DIE_TRAP, "bounds", regs, error_code,
	           X86_TRAP_BR, SIGSEGV) == NOTIFY_STOP)
    goto exit;
...
...
...
exception_exit(prev_state);
return;

在我们得到前一个上下文的状态后,我们将异常添加到notify_die链中,如果它返回NOTIFY_STOP,我们从异常中返回。关于通知链和上下文跟踪函数的更多信息,你可以在上一部分中阅读。接下来,我们使用contidional_sti函数启用中断,该函数检查IF标志,并根据其值调用local_irq_enable

conditional_sti(regs);

if (!user_mode(regs))
	die("bounds", regs, error_code);

并检查我们是否没有来自用户模式,我们使用die函数发送SIGSEGV信号。在这之后,我们检查MPX是否启用,如果这个特性被禁用,我们跳到exit_trap标签:

if (!cpu_feature_enabled(X86_FEATURE_MPX)) {
	goto exit_trap;

在那里我们执行do_trap函数(关于它的更多信息,你可以在上一部分找到):

exit_trap:
	do_trap(X86_TRAP_BR, SIGSEGV, "bounds", regs, error_code, NULL);
	exception_exit(prev_state);

如果MPX特性被启用,我们使用get_xsave_field_ptr函数检查BNDSTATUS,如果它为零,这意味着MPX不是这个异常的原因:

bndcsr = get_xsave_field_ptr(XSTATE_BNDCSR);
if (!bndcsr)
		goto exit_trap;

在这之后,仍然只有一种方式MPX是这个异常的原因。我们不会在这一部分深入Intel内存保护扩展的细节,但将在另一章中看到。

协处理器异常和SIMD异常

接下来的两个异常是x87 FPU 浮点错误异常或#MFSIMD 浮点异常或#XF。第一个异常发生在x87 FPU检测到浮点错误时。例如,除以零、数值溢出等。第二个异常发生在处理器检测到SSE/SSE2/SSE3 SIMD浮点异常时。它可能与x87 FPU相同。这些异常的处理程序是do_coprocessor_errordo_simd_coprocessor_error,定义在arch/x86/kernel/traps.c,彼此非常相似。它们都调用同一个源代码文件中的math_error函数,但传递不同的向量号。do_coprocessor_errormath_error传递X86_TRAP_MF向量号:

dotraplinkage void do_coprocessor_error(struct pt_regs *regs, long error_code)
{
	enum ctx_state prev_state;

	prev_state = exception_enter();
	math_error(regs, error_code, X86_TRAP_MF);
	exception_exit(prev_state);
}

do_simd_coprocessor_errormath_error传递X86_TRAP_XF

dotraplinkage void
do_simd_coprocessor_error(struct pt_regs *regs, long error_code)
{
	enum ctx_state prev_state;

	prev_state = exception_enter();
	math_error(regs, error_code, X86_TRAP_XF);
	exception_exit(prev_state);
}

首先,math_error函数定义当前被中断的任务,其FPU的地址,描述异常的字符串,将其添加到notify_die链中,并在它返回NOTIFY_STOP时从异常处理程序返回:

	struct task_struct *task = current;
	struct fpu *fpu = &task->thread.fpu;
	siginfo_t info;
	char *str = (trapnr == X86_TRAP_MF) ? "fpu exception" :
						"simd exception";

	if (notify_die(DIE_TRAP, str, regs, error_code, trapnr, SIGFPE) == NOTIFY_STOP)
		return;

在这之后,我们检查我们是否来自内核模式,如果是,我们将尝试使用fixup_exception函数修复异常。如果我们不能,我们用异常的错误代码和向量号填充任务,并终止:

if (!user_mode(regs)) {
	if (!fixup_exception(regs)) {
		task->thread.error_code = error_code;
		task->thread.trap_nr = trapnr;
		die(str, regs, error_code);
	}
	return;
}

如果我们来自用户模式,我们保存fpu状态,用异常的向量号填充任务结构,并用信号编号、errno、异常发生地址和信号代码填充siginfo_t

fpu__save(fpu);

task->thread.trap_nr	= trapnr;
task->thread.error_code = error_code;
info.si_signo		= SIGFPE;
info.si_errno		= 0;
info.si_addr		= (void __user *)uprobe_get_trap_addr(regs);
info.si_code = fpu__exception_code(fpu, trapnr);

在这之后,我们检查信号代码,如果它非零,我们返回:

if (!info.si_code)
	return;

或者在最后发送SIGFPE信号:

force_sig_info(SIGFPE, &info, task);

就这些。

结论

这是中断和中断处理章节的第六部分的结尾,我们在这一部分看到了一些异常处理程序的实现,比如non-maskable中断、SIMDx87 FPU浮点异常。最后,我们在这一部分以trap_init函数结束,并将进入下一部分。我们接下来关注的是外部中断和init/main.c中的early_irq_init函数。

如果你有任何问题或建议,请在评论中告诉我,或者在twitter上联系我。

请注意,英语不是我的第一语言,如果有任何不便,我深表歉意。如果你发现任何错误,请向我发送PR到linux-insides

链接

Linux嵌入式必考必会 文章被收录于专栏

"《Linux嵌入式必考必会》专栏,专为嵌入式开发者量身打造,聚焦Linux环境下嵌入式系统面试高频考点。涵盖基础架构、内核原理、驱动开发、系统优化等核心技能,实战案例与理论解析并重。助你快速掌握面试关键,提升竞争力。

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务