Linux0.11内核源码分析3-main函数运行之初始化中断描述符表

500 阅读17分钟

基础知识

如果你知道DPLRPLCPL就不用看这段了,直接跳到下一节😂

如果想要知道这些概念,你需要知道什么是段描述符,选择子等数据结构。

DPL就是段描述符中得DPL 段描述符

RPL就是选择子中得RPL 选择子

CPL是段寄存器cs中选择子中得RPL,用来表示当前CPU运行得是哪个状态。

linux系统只有2个特权级,一个用户态(特权3 级),一个内核态(特权0 级)。用户态与内核态是对CPU 来讲的,是指CPU 运行在用户态(特权3 级)还是内核态(特权0 级), 很多人误以为是对用户进程来讲的。用户进程陷入内核态是指:由于内部或外部中断发生,当前进程被暂时终止执行,其上下文被内核的中断程序保存起来后,开始执行一段内核的代码。

保护模式中段寄存器中就不是段基址,而是选择子,选择子其实就是数组的索引,根据选择子就可以找到段选择符。为了避免出现非法引用内存段的情况, 在这时候,处理器会在以下几方面做出检查:

  • 首先根据选择子的值验证段描述符是否超越界限
  • 段描述符中还有个type 字段,这用来表示段的类型,也就是不同的段有不同的作用。在选择子检查过后,就要检查段的类型了,这里主要是检查段寄存器的用途和段类型是否匹配。大的原则如下。
    • 只有具备可执行属性的段(代码段)才能加载到 CS 段寄存器中。
    • 只具备执行属性的段(代码段)不允许加载到除 CS 外的段寄存器中。
    • 只有具备可写属性的段(数据段)才能加载到 SS 栈段寄存器中。
    • 至少具备可读属性的段才能加载到 DS、ES、FS、GS 段寄存器中
  • 检查完 type 后,还会再检查段是否存在。CPU 通过段描述符中的P 位来确认内存段是否存在,如果 P 位为1,则表示存在,这时候就可以将选择子载入段寄存器了,同时段描述符缓冲寄存器也会更新为选择子对应的段描述符的内容,随后处理器将段描述符中的A 位置为1,表示已经访问过了。如果P 位为0, 则表示该内存段不存在,不存在的原因可能是由于内存不足,操作系统将该段移出内存转储到硬盘上了。 这时候处理器会抛出异常,自动转去执行相应的异常处理程序,异常处理程序将段从硬盘加载到内存后并 将P位置为1,随后返回。CPU 继续执行刚才的操作,判断P位。

用户态有用户态的栈,内核态有内核态的栈,两个特权级切换时,cpu如何选择对应的栈?下面我们看看进程结构体中的一个属性TSS(Task State Segment),里面的ss0 esp0就是特权级0的栈,ss1 esp1就是特权级1的栈,ss2 esp2就是特权级2的栈,之所以没有ss3是因为特权级3已经时最低的权限了。

特权级的转换分为由低到高转换和由高到低转换

  • 特权级由低到高转换 可以由中断门、调用门等手段实现低特权级转向高特权级。 由于不知道目标特权级对应的栈地址在哪里,所以要提前把目标栈的地址记录在某个地方,当处理器向高特权级转移时再从中取出来加载到SS 和ESP 中以更新栈,这个保存的地方就是TSS。处理器会自动地从TSS 中找到对应的高特权级栈地址。

  • 特权级由高到低转换 首先明白一点,特权级越高,可以做到的事就越多,CPU希望一直在内核态运行,没有必要降低身价切换到低特权级。但是用户程序可是用户态的,用户态可以切换到内核态,怎么从内核态再次回到用户态呢? 只有一种办法就是由调用返回指令从高特权级返回到低特权级,这是唯一一种能让处理器降低特权级的情况。

处理器是不需要在TSS 中去寻找低特权级目标栈的。其中一个原因我想您已经猜到了:TSS 中只记录2、1、0 特权级的栈,假如是从2 特权级返回到3 特权级,上哪找3 特权级的栈?另一方面的原因是低特权级栈的地址其实已经存在了,这是由处理器的向高特权级转移指令(如int、call 等)实现的机制决定的,换句话说,处理器知道去哪里找低特权级的目标栈。当处理器由低向高特权级转移时,它自动地把当时低特权级的栈地址(SS 和ESP)压入了转移后的高特权级所在的栈中(复制),所以,当用返回指令如retf 或iret 从高特权级向低特权级返回时,处理器可以从当前使用的高特权级的栈中获取低特权级的栈段选择子及偏移量。由高特权级返回低特权级的过程称为“向外层转移”

32位tss结构

再来看TSS的结构,104 字节只是TSS 的最小尺寸,如果该进程需要响应io端口的信号,还可以在这里添加io位图,这个尺寸就不是104了。

CPU是执行代码的,我们定义正在执行的代码段称为访问者,访问者访问的资源被称为受访者,受访者可能是一个代码段或者一个数据段。

  • 对于受访者为数据段(段描述符中 type 字段中未有X 可执行属性)来说:

只有访问者的权限大于等于该受访者的DPL 表示的最低权限才能够继续访问,否则连这个门槛都迈不过去。 即数值上CPL <= DPL。

  • 对于受访者为代码段(段描述符中 type 字段中含有X 可执行属性)来说: 只有访问者的权限等于该DPL 表示的最低权限才能够继续访问,即只能平级访问。任何权限大于或 小于它的访问者都将被CPU 拒之门外。不过,有例外的时候,这是唯一一种处理器会从高特权降到低特权运行的情况:处理器从中断处理程序中返回到用户态的时候。

有一种方式既执行高特权级代码段上的指令,又不提升特权级,就是用一致性代码段。在段描述符中,如果该段为非系统段(段描述符的S 字段为0),可以用type字段中的C 位来表示该段是否为一致性代码段。C 为1 时则表示该段是一致性代码段,C 为0 时则表示该段为非一致性代码段。上面所提到的代码段是非一致性代码段,所以只能平级转移。

一致性代码段也称为依从代码段,Conforming,用来实现从低特权级的代码向高特权级的代码转移。一 致性代码段是指如果自己是转移后的目标段,自己的特权级(DPL)一定要大于等于转移前的CPL,即数值 上CPL≥DPL,也就是一致性代码段的DPL 是权限的上限,任何在此权限之下的特权级都可以转到此代码 段上执行。处理器遇到目标段为一致性代码段时,并不会将CPL 用该目标段的DPL 替换。既然是转移到特权级更高的一致性代码段后CPL 不变,这说明这种转移本身并没有提升特权级,只是可以跑到特权级更高的代码段中去执行指令,对计算机而言并未因特权级升高而产生潜在危险

门描述符

特权级由低到高只能通过门结构,包括中断门、陷阱门、调用门,还有一个任务门,但是基本不用,所以这里就不讲。调用门可以位于GDT、LDT 中,中断门和陷阱门仅位于IDT 中。

调用门可以用call 和jmp 指令直接调用,原因是这两个门描述符都位于描述符表中,要么 是GDT,要么是LDT,访问它们同普通的段描述符是一样的,也必须要通过选择子,因此只要在call 或 jmp 指令后接任务门或调用门的选择子便可调用它们了。陷阱门和中断门只存在于IDT 中,因此不能主动 调用,只能由中断信号来触发调用。

中断门描述符

陷阱门描述符

调用们描述符

trap_init()

上图表示的是main函数执前的内存分布,我们看到idt在什么地方。

有了上面得基础知识,下面得代码才能看懂。中断描述符表的初始化工作主要通过宏_set_gate来完成 kernel/traps.c

void trap_init(void)
{
	int i;

	set_trap_gate(0,&divide_error);
	set_trap_gate(1,&debug);
	set_trap_gate(2,&nmi);
	set_system_gate(3,&int3);	/* int3-5 can be called from all */
	set_system_gate(4,&overflow);
	set_system_gate(5,&bounds);
	set_trap_gate(6,&invalid_op);
	set_trap_gate(7,&device_not_available);
	set_trap_gate(8,&double_fault);
	set_trap_gate(9,&coprocessor_segment_overrun);
	set_trap_gate(10,&invalid_TSS);
	set_trap_gate(11,&segment_not_present);
	set_trap_gate(12,&stack_segment);
	set_trap_gate(13,&general_protection);
	set_trap_gate(14,&page_fault);
	set_trap_gate(15,&reserved);
	set_trap_gate(16,&coprocessor_error);
    // int17-47的陷阱门先均设置为reserved,以后各硬件初始化时会重新设置自己的陷阱门。
	for (i=17;i<48;i++)
		set_trap_gate(i,&reserved);
    // 设置协处理器中断0x2d(45)陷阱门描述符,并允许其产生中断请求。设置并行口中断描述符。
	set_trap_gate(45,&irq13);
	outb_p(inb_p(0x21)&0xfb,0x21);  // 允许8259A主芯片的IRQ2中断请求。
	outb(inb_p(0xA1)&0xdf,0xA1);    // 允许8259A从芯片的IRQ3中断请求。
	set_trap_gate(39,&parallel_interrupt); // 设置并行口1的中断0x27陷阱门的描述符。
}

include/asm/system.h

#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \  
	"movw %0,%%dx\n\t" \
	"movl %%eax,%1\n\t" \
	"movl %%edx,%2" \
	: \
	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	"o" (*((char *) (gate_addr))), \
	"o" (*(4+(char *) (gate_addr))), \
	"d" ((char *) (addr)),"a" (0x00080000))  // edx等于addr,eax等于0x00080000
   
//设置陷阱门函数,特权级0
#define set_trap_gate(n,addr) \
	_set_gate(&idt[n],15,0,addr) 
    
// 设置系统调用函数,特权级3
#define set_system_gate(n,addr) \
	_set_gate(&idt[n],15,3,addr)
    
//设置中断门函数,特权级0
#define set_intr_gate(n,addr) \
	_set_gate(&idt[n],14,0,addr)

就拿陷阱门来说明,type是111,与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时维持IF标志位不变,也就是说,不关中断。

  • "d" ((char *) (addr))表示把addr的偏移地址给edx寄存器

  • "a" (0x00080000) 代表把0x00080000值放入eax中,一共32位(高16位就是0x0008是段选择符,低16位会被放在edx中的过程偏移低16位代替,目前就是0)

  • "i" ((short) (0x8000+(dpl<<13)+(type<<8)))0x8000(0b1000_0000_0000_0000),2个字节16位,而其第16位上还是1,根据陷阱门描述符,这里就是P,P等于1表示描述符指向的内容存在内存中,设置了dpl以及type。

  • "movw %%dx,%%ax表示把低16位的dx值赋值给低16位的ax中,此时eax中的值就是0x0008+ addr偏移地址。0x0008是段选择符,二进制就是0b1000,表示的是RPL是0,在GDT中查找,1表示索引是1,索引0默认是null。

  • movw %0,%%dx表示把((short) (0x8000+(dpl<<13)+(type<<8)))给dx

  • movl %%eax,%1表示把32位寄存器eax的值给(*((char *) (gate_addr))),gate_addr是一个地址,在加一个*就是取值。

  • movl %%edx,%2把32位寄存器edx的值给(*(4+(char *) (gate_addr)))

divide_error

我们假设栈顶的值是A,先把do_divide_error函数的地址压入栈中,_do_divide_error是C函数do_divide_error被编译后的名字,然后把ebx、ecx、edx等等压栈,lea 44(%esp),%edx lea 表示把 44(%esp)的值给edx,44(%esp)就是esp + 44,因为栈是向低地址方向生长。栈低在高地址,栈顶在低地址。call *%eax就是在调用_do_divide_error这个函数。这个函数的作用就是退出进程,释放进程资源,把子进程给init进程,给父进程发信号,让父进程给自己的收尸。

kernel/asm.s

divide_error:
	pushl $do_divide_error      # 首先把将要调用的函数地址入栈
no_error_code:                  
	xchgl %eax,(%esp)           # _do_divide_error的地址→eax,eax被交换入栈
	pushl %ebx
	pushl %ecx
	pushl %edx
	pushl %edi
	pushl %esi
	pushl %ebp
	push %ds                    # 16位的段寄存器入栈后也要占用4个字节。
	push %es
	push %fs
	pushl $0		    # "error code"  #将数值0作为出错码入栈
	lea 44(%esp),%edx           # 取对堆栈中原调用返回地址处堆栈指针位置,并压入堆栈。
	pushl %edx
	movl $0x10,%edx             # 初始化段寄存器ds、es和fs,加载内核数据段选择符
	mov %dx,%ds
	mov %dx,%es
	mov %dx,%fs
# 下行上的 * 号表示调用操作数指定地址处的函数,称为间接调用。这句的含义是调用引起本次
# 异常的C处理函数,例如do_divide_error等。
	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                   # 弹出原来eax中的内容
	iret

kernel/traps.c

参数都在栈里, esp就是do_divide_error调用前的栈顶,用于返回之前的状态。


void do_divide_error(long esp, long error_code)
{
	die("divide error",esp,error_code);
}

// 该子程序用来打印出错中断的名称、出错号、调用程序的EIP、EFLAGS、ESP、fs段寄存器值、
// 段的基址、段的长度、进程号PID、任务号、10字节指令码。如果堆栈在用户数据段,则还
// 打印16字节的堆栈内容。
static void die(char * str,long esp_ptr,long nr)
{
	long * esp = (long *) esp_ptr;
	int i;

	....打印一些数据...
    
	if (esp[4] == 0x17) {
		printk("Stack: ");
		for (i=0;i<4;i++)
			printk("%p ",get_seg_long(0x17,i+(long *)esp[3]));
		printk("\n");
	}
	str(i);                 // 取当前运行任务的任务号
	printk("Pid: %d, process nr: %d\n\r",current->pid,0xffff & i);
	for(i=0;i<10;i++)
		printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0])));
	printk("\n\r");
	do_exit(11);		/* play segment exception */
}

kernel/exit.c 进程程序退出处理函数。

首先释放当前进程代码段和数据段所占的内存页,如果进程该有子进程,就把子进程的father改为1,即init进程。如果该子进程已经处于僵死(ZOMBIE)状态,则向进程1发送子进程中止信号SIGCHLD,让init进程收尸。关闭进程打开的文件。把自己的状态改为TASK_ZOMBIE,通知父进程给自己收尸。然后重新调度schedule()。

int do_exit(long code)
{
	int i;
	free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));  //0x0f是进程代码段的选择符
	free_page_tables(get_base(current->ldt[2]),get_limit(0x17));  //0x17是进城数据段的选择符
    // 如果当前进程有子进程,就将子进程的father置为1(其父进程改为进程1,即init进程)。
    // 如果该子进程已经处于僵死(ZOMBIE)状态,则向进程1发送子进程中止信号SIGCHLD。
	for (i=0 ; i<NR_TASKS ; i++)
		if (task[i] && task[i]->father == current->pid) {
			task[i]->father = 1;
			if (task[i]->state == TASK_ZOMBIE)
				/* assumption task[1] is always init */
				(void) send_sig(SIGCHLD, task[1], 1);
		}
    // 关闭当前进程打开着的所有文件。
	for (i=0 ; i<NR_OPEN ; i++)
		if (current->filp[i])
			sys_close(i);
    // 对当前进程的工作目录pwd,根目录root以及执行程序文件的i节点进行同步操作,放回
    // 各个i节点并分别置空(释放)。
	iput(current->pwd);
	current->pwd=NULL;
	iput(current->root);
	current->root=NULL;
	iput(current->executable);
	current->executable=NULL;
    // 如果当前进程是会话头领(leader)进程并且其有控制终端,则释放该终端。
	if (current->leader && current->tty >= 0)
		tty_table[current->tty].pgrp = 0;
    // 如果当前进程上次使用过协处理器,则将last_task_used_math置空。
	if (last_task_used_math == current)
		last_task_used_math = NULL;
    // 如果当前进程是leader进程,则终止该会话的所有相关进程。
	if (current->leader)
		kill_session();
    // 把当前进程置为僵死状态,表明当前进程已经释放了资源。并保存将由父进程读取的退出码。
	current->state = TASK_ZOMBIE;
	current->exit_code = code;
    // 通知父进程,也即向父进程发送信号SIGCHLD - 子进程将停止或终止。
	tell_father(current->father);
	schedule();                     // 重新调度进程运行,以让父进程处理僵死其他的善后事宜。
    // 下面的return语句仅用于去掉警告信息。因为这个函数不返回,所以若在函数名前加关键字
    // volatile,就可以告诉gcc编译器本函数不会返回的特殊情况。这样可让gcc产生更好一些的代码,
    // 并且可以不用再写return语句也不会产生假警告信息。
	return (-1);	/* just to suppress warnings */
}

debug

当EFLAGS中TF标志置位时而引发的中断

debug:
	pushl $do_int3		# _do_debug C函数指针入栈
	jmp no_error_code

kernel/traps.c

void do_int3(long * esp, long error_code,
		long fs,long es,long ds,
		long ebp,long esi,long edi,
		long edx,long ecx,long ebx,long eax)
{
	int tr;

	__asm__("str %%ax":"=a" (tr):"0" (0));  //把ax中的值给tr
	printk("eax\t\tebx\t\tecx\t\tedx\n\r%8x\t%8x\t%8x\t%8x\n\r",
		eax,ebx,ecx,edx);
	printk("esi\t\tedi\t\tebp\t\tesp\n\r%8x\t%8x\t%8x\t%8x\n\r",
		esi,edi,ebp,(long) esp);
	printk("\n\rds\tes\tfs\ttr\n\r%4x\t%4x\t%4x\t%4x\n\r",
		ds,es,fs,tr);
	printk("EIP: %8x   CS: %4x  EFLAGS: %8x\n\r",esp[0],esp[1],esp[2]);
}

nmi

把中断按事件来源分类,来自CPU 外部的中断就称为外部中断,来自CPU 内部的中断 称为内部中断。其实还可以再细分,外部中断按是否导致宕机来划分,可分为可屏蔽中断(INTeRrupt)和不可屏蔽中断(Non Maskable Interrupt) 两种,而内部中断按中断是否正常来划分,可分为软中断和异常。

nmi:
	pushl $do_nmi
	jmp no_error_code
void do_nmi(long esp, long error_code)
{
	die("nmi",esp,error_code);
}

int3

由int 3指令引发的中断,与硬件中断无关

int3:
	pushl $do_int3
	jmp no_error_code

overflow

EFLAGS 中 OF标志置位时CPU执行INT0指令就会引发该中断。通常用于编译器跟踪算术计算溢出。

overflow:
	pushl $do_overflow
	jmp no_error_code
void do_overflow(long esp, long error_code)
{
	die("overflow",esp,error_code);
}

bounds

当操作数在有效范围以外时引发的中断。当BOUND指令测试失败就会产生该中断

bounds:
	pushl $do_bounds
	jmp no_error_code
void do_bounds(long esp, long error_code)
{
	die("bounds",esp,error_code);
}

invalid_op

CPU执行机构检测到一个无效的操作码而引起的中断

invalid_op:
	pushl $do_invalid_op
	jmp no_error_code
void do_invalid_op(long esp, long error_code)
{
	die("invalid operand",esp,error_code);
}

device_not_available

如果控制寄存器CRO中EM(模拟)标志置位,则当CPU执行一个协处理器指令时就会引发该中断,这样CPU就可以有机会让这个中断处理程序模拟协处理器指令。CRO的交换标志TS是在CPU执行任务转换时设置的。TS可以用来确定什么时候协处理器中的内容与CPU正在执行的任务不匹配了。当CPU在运行一个协处理器转义指令时发现TS置位时,就会引发该中断。此时就可以保存前一个任务的协处理器内容,并恢复新任务的协处理器执行状态。该中断最后将转移到标号ret_from_sys_call处执行下去(检测并处理信号)。

kernel/system_call.s

device_not_available:
	push %ds
	push %es
	push %fs
	pushl %edx
	pushl %ecx
	pushl %ebx
	pushl %eax
    
	movl $0x10,%eax       # ds,es 置为指向内核数据段。
	mov %ax,%ds
	mov %ax,%es
	
    	movl $0x17,%eax
	mov %ax,%fs
	# 清CRO中任务已交换标志TS,并取CRO值。若其中协处理器仿真标志EM没有置位,说明不是EM
	# 引起的中断,则恢复任务协处理器状态,执行C函数math_state_restore(),并返回时去执行
	# ret_from_sys_call处的代码。
	
    	pushl $ret_from_sys_call    # 把下面跳转或调用的返回地址入栈。
	clts				# clear TS so that we can use math
	movl %cr0,%eax
	testl $0x4,%eax			# EM (math emulation bit)
	je math_state_restore   # 执行C函数
	# 若EM标志是置位的,则去执行数学仿真程序math_emulate().
	pushl %ebp
	pushl %esi
	pushl %edi
	call math_emulate
	popl %edi
	popl %esi
	popl %ebp
	ret

任务0啥都不做,不处理信号,只会重新调度,任务0开始执行,就会执行任务调度。

signal	= 12    # 是信号位图,每个bit代表一种信号,信号值=位偏移值+1
blocked = (33*16)   # 受阻塞信号位图的偏移量

ret_from_sys_call:
	# 首先判别当前任务是否是初始任务task0,如果是则不比对其进行信号量方面的处理,直接返回。
	movl current,%eax		# task[0] cannot have signals
	cmpl task,%eax
	je 3f                   # 向前(forward)跳转到标号3处退出中断处理

	# 通过对原调用程序代码选择符的检查来判断调用程序是否是用户任务。如果不是则直接退出中断。
	# 这是因为任务在内核态执行时不可抢占。否则对任务进行信号量的识别处理。这里比较选择符是否
	# 为用户代码段的选择符0x000f(RPL=3,局部表,第一个段(代码段))来判断是否为用户任务。如果不是
	# 则说明是某个中断服务程序跳转到上面的,于是跳转退出中断程序。如果原堆栈段选择符不为
	# 0x17(即原堆栈不在用户段中),也说明本次系统调用的调用者不是用户任务,则也退出。
	cmpw $0x0f,CS(%esp)		# was old code segment supervisor ?
	jne 3f
	cmpw $0x17,OLDSS(%esp)		# was stack segment = 0x17 ?
	jne 3f
	
    	# 下面这段代码用于处理当前任务中的信号。首先取当前任务结构中的信号位图(32位,每位代表1种
	# 信号),然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值最小的信号值,
	# 再把原信号位图中该信号对应的位复位(置0),最后将该信号值作为参数之一调用do_signal().
	# do_signal()在kernel/signal.c中,其参数包括13个入栈信息。
	
    	movl signal(%eax),%ebx          # 取信号位图→ebx,每1位代表1种信号,共32个信号
	movl blocked(%eax),%ecx         # 取阻塞(屏蔽)信号位图→ecx
	notl %ecx                       # 每位取反
	andl %ebx,%ecx                  # 获得许可信号位图
	bsfl %ecx,%ecx                  # 从低位(位0)开始扫描位图,看是否有1的位,若有,则ecx保留该位的偏移值
	je 3f                           # 如果没有信号则向前跳转退出
	btrl %ecx,%ebx                  # 复位该信号(ebx含有原signal位图)
	movl %ebx,signal(%eax)          # 重新保存signal位图信息→current->signal.
	incl %ecx                       # 将信号调整为从1开始的数(1-32)
	pushl %ecx                      # 信号值入栈作为调用do_signal的参数之一
	call do_signal                  # 调用C函数信号处理程序(kernel/signal.c)
	popl %eax                       # 弹出入栈的信号值
3:	popl %eax                       # eax中含有上面入栈系统调用的返回值
	popl %ebx
	popl %ecx
	popl %edx
	pop %fs
	pop %es
	pop %ds
	iret

double_fault

通常当CPU在调用前一个异常的处理程序而又检测到一个新的异常时,这两个异常会被 串行地进行处理,但也会碰到很少的情况,CPU不能进行这样的串行处理操作,此时就会引发该中断。

double_fault:
	pushl $do_double_fault  # C 函数地址入栈
error_code:
	xchgl %eax,4(%esp)		# error code <-> %eax 原来地址被保存在堆栈上
	xchgl %ebx,(%esp)		# &function <-> %ebx 原来地址被保存在堆栈上
	pushl %ecx
	pushl %edx
	pushl %edi
	pushl %esi
	pushl %ebp
	push %ds
	push %es
	push %fs
	pushl %eax			# error code  # 出错号入栈
	lea 44(%esp),%eax		# offset  # 程序返回地址处堆栈指针位置值入栈
	pushl %eax
	movl $0x10,%eax     # 置内核数据段选择符。
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	call *%ebx          # 间接调用,调用相应的C函数,其参数已入栈。
	addl $8,%esp        # 丢弃入栈的2个用作C函数的参数。
	pop %fs
	pop %es
	pop %ds
	popl %ebp
	popl %esi
	popl %edi
	popl %edx
	popl %ecx
	popl %ebx
	popl %eax
	iret
void do_double_fault(long esp, long error_code)
{
	die("double fault",esp,error_code);
}

coprocessor_segment_overrun

异常基本上等同于协处理器 出错保护。因为在浮点指令操作数太大时,我们就有这个机会来加载或保存超出数据段的浮点值

coprocessor_segment_overrun:
	pushl $do_coprocessor_segment_overrun
	jmp no_error_code
void do_coprocessor_segment_overrun(long esp, long error_code)
{
	die("coprocessor segment overrun",esp,error_code);
}

invalid_TSS

CPU企图切换到一个进程,而该进程的TSS无效。根据TSS中哪一部分引起了异常,当由于TSS长度超过104字节时,这个异常在当前任务中产生,因而切换被终止。其他问题则会导致在切换后的新任务产生本异常。

invalid_TSS:
	pushl $do_invalid_TSS
	jmp error_code
void do_invalid_TSS(long esp,long error_code)
{
	die("invalid TSS",esp,error_code);
}

segment_not_present

被引用的段不再内存中。段描述符中标志着段不再内存中

segment_not_present:
	pushl $do_segment_not_present
	jmp error_code
void do_segment_not_present(long esp,long error_code)
{
	die("segment not present",esp,error_code);
}

stack_segment

指令操作试图超出堆栈段范围,或者堆栈段不再内存中

stack_segment:
	pushl $do_stack_segment
	jmp error_code
void do_stack_segment(long esp,long error_code)
{
	die("stack segment",esp,error_code);
}

general_protection

表明是不属于任何其他类的错误。若一个异常产生时没有对应的处理向量(0 -- 16),

general_protection:
	pushl $do_general_protection
	jmp error_code
void do_general_protection(long esp, long error_code)
{
	die("general protection",esp,error_code);
}

page_fault

页异常中断处理程序(int14),主要分两种情况处理。一是由于缺页(当CPU 发现对应页目录项或页表项的存在位(P)标志为0)引起的页异常中断,通过调用do_no_page(error_code, address)来处理;二是由页写保护(当前进程没有访问指定页面的权限)引起的页异常,此时调用页写保 护处理函数do_wp_page(error_code, address)进行处理。其中的出错码(error_code)是由CPU 自动产生并压入堆栈的,出现异常时访问的线性地址是从控制寄存器CR2 中取得的。CR2 是专门用来存放页出错时的 线性地址。

对于页异常处理中断,CPU 提供了两项信息用来诊断页异常和从中恢复运行

  • 放在堆栈上的出错码。

该出错码指出了异常是由于页不存在引起的还是违反了访问权限引起的: - 位 2(U/S) - 0 表示在超级用户模式下执行,1 表示在用户模式下执行; - 位 1(W/R) - 0 表示读操作,1 表示写操作; - 位 0(P) - 0 表示页不存在,1 表示页级保护

  • CR2(控制寄存器2)。CPU 将造成异常的用于访问的线性地址存放在CR2 中。异常处理程序可以使用这个地址来定位相应的页目录和页表项。如果在页异常处理程序执行期间允许发生另一个页异常,那么处理程序应该将CR2 压入堆栈中。

控制寄存器 mm/page.s

page_fault:
	xchgl %eax,(%esp)       # 取出错码到eax
	pushl %ecx
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%edx         # 置内核数据段选择符 RPL=0,TI=0,index = 2,表示内核权限,在GDT中的第3个段描述符,第一个是null,第二个是代码段,第三个数数据段
	mov %dx,%ds
	mov %dx,%es
	mov %dx,%fs
	movl %cr2,%edx          # 取引起页面异常的线性地址
	pushl %edx              # 将该线性地址和出错码压入栈中,作为将调用函数的参数
	pushl %eax
	testl $1,%eax           # 测试页存在标志P(为0),如果不是缺页引起的异常则跳转
	jne 1f
	call do_no_page         # 调用缺页处理函数
	jmp 2f
1:	call do_wp_page         # 调用写保护处理函数
2:	addl $8,%esp            # 丢弃压入栈的两个参数,弹出栈中寄存器并退出中断。
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret

mm/memory.c

do_no_page是访问不存在页面处理函数。页异常中断处理过程中调用的函数。函数参数error_code和address是进程在访问页面时由CPU因缺页产生异常而自动生成。该函数首先尝试与已加载的相同文件进行页面共享,或者只是由于进程动态申请内存页面而只需映射一页物理内存即可。若共享操作不成功,那么只能从相应文件中读入所缺的数据页面到指定线性地址处。

void do_no_page(unsigned long error_code,unsigned long address)
{
	int nr[4];
	unsigned long tmp;
	unsigned long page;
	int block,i;

    // 首先取线性空间中指定地址address处页面地址。从而可算出指定线性地址在进程
    // 空间相对于进程基地址的偏移长度值tmp,即对应的逻辑地址。
	address &= 0xfffff000;
	tmp = address - current->start_code;
    // 若当进程的executable节点指针空,或者指定地址超出(代码+数据)长度,则申请
    // 一页物理内存,并映射到指定的线性地址处。executable是进程正在运行的执行文
    // 件的i节点结构。由于任务0和任务1的代码在内核中,因此任务0,任务1以及任务1
    // 派生的没有调用过execute()的所有任务的executable都为0.若该值为0,或者参数
    // 指定的线性地址超出代码加数据长度,则表明进程在申请新的内存页面存放堆或栈
    // 中数据。因此直接调用取空闲页面函数get_empty_page()为进程申请一页物理内存
    // 并映射到指定线性地址处。进程任务结构字段start_code是线性地址空间中进程代
    // 码段地址,字段end_data是代码加数据长度。对于Linux0.11内核,它的代码段和
    // 数据段其实基址相同。
	if (!current->executable || tmp >= current->end_data) {
		get_empty_page(address);
		return;
	}
	if (share_page(tmp))
		return;
	if (!(page = get_free_page()))
		oom();
/* remember that 1 block is used for header */
    // 因为块设备上存放的执行文件映象第1块数据是程序头结构,因此在读取该文件时
    // 需要跳过第1块数据。所以需要首先计算缺页所在数据块号。因为每块数据长度为
    // BLOCK_SIZE=1KB,因此一页内存课存放4个数据块。进程逻辑地址tmp除以数据块大
    // 小再加上1即可得出缺少的页面在执行映象文件中的起始块号block。根据这个块号
    // 和执行文件的i节点,我们就可以从映射位图中找到对应块设备中对应的设备逻辑块
    // 号(保存在nr[]数组中)。利用bread_page()即可把这4个逻辑块读入到物理页面page中。
	block = 1 + tmp/BLOCK_SIZE;
	for (i=0 ; i<4 ; block++,i++)
		nr[i] = bmap(current->executable,block);
	bread_page(page,current->executable->i_dev,nr);
    // 在读设备逻辑块操作时,可能会出现这样一种情况,即在执行文件中的读取页面位
    // 置可能离文件尾不到1个页面的长度。因此就可能读入一些无用的信息,下面的操作
    // 就是把这部分超出执行文件end_data以后的部分清零处理。
	i = tmp + 4096 - current->end_data;
	tmp = page + 4096;
	while (i-- > 0) {
		tmp--;
		*(char *)tmp = 0;
	}
    // 最后把引起缺页异常的一页物理页面映射到指定线性地址address处。若操作成功
    // 就返回。否则就释放内存页,显示内存不够。
	if (put_page(page,address))
		return;
	free_page(page);
	oom();
}

没写完。。。。明天继续~

参考: 英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A