Linux-0.11 kernel目录进程管理asm.s详解

程序员小x大约 8 分钟LinuxLinux-0.11代码解读系列

Linux-0.11 kernel目录进程管理asm.s详解

模块简介

该模块和CPU异常处理相关,在代码结构上asm.straps.c强相关。 CPU探测到异常时,主要分为两种处理方式,一种是有错误码,另一种是没有错误码,对应的方法就是error_codeno_error_code。在下面的函数详解中,将主要以两个函数展开。

函数详解

no_error_code

对于一些异常而言,CPU在出现这些异常时不会将error code压入栈中。其和一般的中断类似,会将ss,esp,eflags,cs,eip这几个寄存器的值压入内核栈中。如下图所示:

无错误码的情景
无错误码的情景

接下来,以divide_error为例,详细解释这一过程。

divide_error也就是所谓的0号中断,通常指的是除零异常除法错误。这个中断在进行除法运算时如果被除数为0时会触发。

在x86架构的处理器上,除零异常会引发中断,中断向量号为0。当除零异常发生时,CPU会转移控制权到预定义的中断处理程序。在操作系统内核中,可以通过注册一个特定的中断处理程序来处理这个异常,通常是在中断描述符表(IDT)中指定中断向量0对应的处理程序地址。

Linux-0.11中在trap.c中设置0x0号中断的处理方法是divide_error

	set_trap_gate(0,&divide_error);

divide_error具体的定义是在asm.s中,首先会将do_divide_error的地址压入内核栈中。

xchgl 汇编指令用于交换指定寄存器和指定内存地址处的值。在这里,xchgl %eax,(%esp) 的作用是将 %eax 寄存器的值与栈顶指针指向的内存位置处的值进行交换,也就是将 %eax 的值压入栈中,并将栈顶位置处的值存入 %eax 中。

divide_error:
	pushl $do_divide_error
no_error_code:
	xchgl %eax,(%esp)

接下来就是需要保存一些CPU上下文,将 %ebx%ecx%edx%edi%esi%ebp%ds%es%fs 推入栈中,保存它们的值以便稍后恢复。

pushl %ebx
pushl %ecx
pushl %edx
pushl %edi
pushl %esi
pushl %ebp
push %ds
push %es
push %fs

在保护好CPU上下文之后,接下来就是为调用do_divide_error做一些准备,将入参压入栈。void do_divide_error的原型是void do_divide_error(long esp, long error_code)。这里入参esp是指中断调用之后堆栈指针的指向,esp指向的位置存储的是原eip

do_divide_error方法中,通过esp的值打印一些信息。

pushl $0		# "error code"
lea 44(%esp),%edx
pushl %edx

将下来初始化段寄存器,加载内核的数据段选择符。

movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
mov %dx,%fs

这些工作都准备完成之后,就通过call去调用do_divide_error这个c函数。

call *%eax

调用完毕之后,恢复现场。

addl $8,%esp
pop %fs
pop %es
pop %ds
popl %ebp
popl %esi
popl %edi
popl %edx
popl %ecx
popl %ebx
popl %eax
iret

通过divide_error的例子,我们知道当0号中断发生时,会在栈上构建出一些参数,最后将调用do_divide_error打印一些出错信息以提示用户发生中断的信息以用于检查问题。打印完毕之后,将栈上的环境恢复到中断之前的样子。

error_code

对于一些异常而言,CPU在出现这些异常除了会将ss,esp,eflags,cs,eip这几个寄存器的值压入内核栈中以外,还会将error_code压入内核栈中。如下图所示:

有错误码的情景
有错误码的情景

下面会以double_fault为例,来理解带有错误码的处理过程。

在计算机体系结构中,"double fault"(双重故障)是一种处理器异常的情况,它发生在处理一个异常时又发生了另一个异常。在 x86 架构中,通常指的是处理器在处理一个异常时(如页面错误或非法指令),由于某种原因(通常是由于处理第一个异常时的问题),导致触发了第二个异常。这个第二个异常就是双重故障。

出现该异常时,会将do_double_fault的地址压入栈中。

double_fault:
	pushl $do_double_fault

随后会将error_code的值写入eax寄存器中,将do_double_fault的地址写入ebx寄存器中。

error_code:
	xchgl %eax,4(%esp)		# error code <-> %eax
	xchgl %ebx,(%esp)		# &function <-> %ebx

接下来保存CPU的上下文

pushl %ecx
pushl %edx
pushl %edi
pushl %esi
pushl %ebp
push %ds
push %es
push %fs

接下来做的也是为调用c函数做准备。

do_double_fault有2个入参,因此需要将error_codeesp压入栈中

void do_double_fault(long esp, long error_code)

这段汇编就是将error_code和出错的地址压入栈中。

pushl %eax			    # error code
lea 44(%esp),%eax		# offset
pushl %eax

将下来初始化段寄存器,加载内核的数据段选择符。

movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
mov %dx,%fs

这些工作都准备完成之后,就调用do_double_fault这个c函数。

call *%ebx

最后的工作便是用于恢复CPU上下文,

addl $8,%esp
pop %fs
pop %es
pop %ds
popl %ebp
popl %esi
popl %edi
popl %edx
popl %ecx
popl %ebx
popl %eax
iret

Intel保留中断号的含义

asm.s剩下的部分定义了每种中断号的入口方法,例如上面我们举例的divide_error和double_fault。

下面总结了Linux-0.11中对于不同中断号的定义。

中断号名称类型信号说明
0Divide error故障SIGFPE进行除以0操作时产生
1Debug陷阱/故障SIGTRAP当进行程序单步跟踪调试时,设置了标志寄存器eflags的T标志时产生这个中断
2nmi硬件有不可屏蔽中断NMI产生
3Breakpoint陷阱SIGTRAP由断点指令int3产生,与debug处理相同
4Overflow陷阱SIGSEGVeflags的溢出标志OF引起
5Bounds check故障SIGSEGV寻址到有效地址以外时引起
6Invalid Opcode故障SIGILLCPU执行时发现一个无效的指令操作码
7Device not available故障SIGSEGV设备不存在,指协处理器。在两种情况下会产生该中断:(a)CPU遇到一个转义指令并且EM置位时。在这种情况下处理程序应该模拟导致异常的指令:(b)MP和TS都在置位状态时,CPU遇到WAIT或一个转义指令。在这种情况下,处理程序在必要时应该更新协处理器的状态。
8Double fault异常终止SEGSEGV双故障错误
9Coprocessor segment overrun异常终止SIGFPE协处理器段超出。
10Invalid TSS故障SIGSEGVCPU切换时发现TSS无效
11Segment not present故障SIGBUS描述符指定的段不存在
12Stack segment故障SIGBUS堆栈段不存在或寻址超出堆栈段
13General protection故障SIGSEGV没有符合80386保护机制的操作引起
14Page fault故障SIGSEGV页不存在内存
15Reserved
16Coprocessor error故障SIGFPE协处理器发出的出错信息引起。

下面我们梳理下每个中断方法的定义。

divide_error:

无error code,其将do_divide_error的地址压入栈中。

pushl $do_divide_error

随后进入no_error_code的处理流程。

debug

error code,其将do_int3的地址压入栈中,进而进入no_error_code的流程。

debug:
	pushl $do_int3		# _do_debug
	jmp no_error_code

nmi

error code,其将do_nmi的地址压入栈中,进而进入no_error_code的流程。

nmi:
	pushl $do_nmi
	jmp no_error_code

int3

error code,其将do_int3的地址压入栈中,进而进入no_error_code的流程。

int3:
	pushl $do_int3
	jmp no_error_code

overflow

error code,其将do_overflow的地址压入栈中,进而进入no_error_code的流程。

overflow:
	pushl $do_overflow
	jmp no_error_code

bounds

error code,其将do_bounds的地址压入栈中,进而进入no_error_code的流程。

bounds:
	pushl $do_bounds
	jmp no_error_code

invalid_op

error code,其将do_invalid_op的地址压入栈中,进而进入no_error_code的流程。

invalid_op:
	pushl $do_invalid_op
	jmp no_error_code

coprocessor_segment_overrun

无error code,其将coprocessor_segment_overrun的地址压入栈中,进而进入no_error_code的流程。

coprocessor_segment_overrun:
	pushl $do_coprocessor_segment_overrun
	jmp no_error_code

reserved

error code,其将reserved的地址压入栈中,进而进入no_error_code的流程。

reserved:
	pushl $do_reserved
	jmp no_error_code

double_fault

error code,其将do_double_fault的地址压入栈中,进而进入error_code的流程。

double_fault:
	pushl $do_double_fault

invalid_TSS

error code,其将do_invalid_TSS的地址压入栈中,进而进入error_code的流程。

invalid_TSS:
	pushl $do_invalid_TSS
	jmp error_code

segment_not_present

error code,其将do_segment_not_present的地址压入栈中,进而进入error_code的流程。

segment_not_present:
	pushl $do_segment_not_present
	jmp error_code

stack_segment

error code,其将do_stack_segment的地址压入栈中,进而进入error_code的流程。

stack_segment:
	pushl $do_stack_segment
	jmp error_code

general_protection

error code,其将do_general_protection的地址压入栈中,进而进入error_code的流程。

general_protection:
	pushl $do_general_protection
	jmp error_code
Loading...