相关学习资料
《深入理解计算机系统(原书第2版)》.pdf http: // zh.wikipedia.org/zh/%E4%B8%AD%E6%96%B7 独辟蹊径品内核:Linux内核源代码导读 李云华著 中文 PDF版 https: // www.kernel.org/ http: // blog.csdn.net/orange_os/article/details/7485069 http: // blog.csdn.net/sunnybeike/article/details/6958473 http: // blog.163.com/di_yang@yeah/blog/static/861184922012124105030284/ http: // www.360doc.com/content/13/0829/21/7377734_310810291.shtm
目录
1 . 从异常控制流开始说起 2 . 中断类型 3 . 中断的初始化 4 . 门描述符 5 . IDT中断描述符表 6 . 异常控制类型
1. 从异常控制流开始说起
0x1: 异常控制流简介
从给处理器加电开始,知道断电为止,程序计数器假设一个值的序列:
A0, A1, ...., An-1
其中,每个Ak是某个相应的指令Ik的"地址"。每次从Ak到Ak+1的过渡称为控制转移(control transfer)。这样的控制转移序列叫作处理器的控制流(flow of control或control flow)
控制流可以大致分为:
1. " 平滑的 " 序列(即顺序执行) 其中每个Ik和Ik + 1在存储器中都是相邻的,CPU按照计数器进行逐条指令的依次执行 2. 非平滑控制流 Ik +1与Ik不相邻,是由诸如跳转、调用、和返回这样一些程序指令造成的。这样一些指令都是必要的机制,使得程序能够对由程序变量表示的内部状态中的变化做出反应。
但是系统也必须能够对系统状态变化做出反应,这些系统状态不是被内部程序变量捕获的,而且也不一定和程序的执行相关。比如:
但是系统也必须能够对系统状态变化做出反应,这些系统状态不是被内部程序变量捕获的,而且也不一定和程序的执行相关。比如: 1 . 一个硬件定时器定期产生信息,这个事件必须得到处理。 2 . 数据包到达网络适配器后,必须存放在存储器中。 3 . 程序向磁盘请求数据,然后休眠,知道被通知数据已就绪。 4 . 当子进程终止时,创造这些子进程的父进程必须得到通知。
现代系统通过使控制流发生突变来对这些情况做出反应(也就是异常控制流)。一般而言,我们把这些突变称为异常控制流(Exceptional Control Flow ECF)。异常控制流发生在计算机系统的各个层次。比如:
1 . 在硬件层,硬件检测到事件时,会触发控制,使CPU突然转移到异常处理程序。 2 . 在操作系统层,内核通过上下文切换将控制从一个用户进程转移到另一个用户进程(分时间片执行)。 3 . 在应用层,一个进程可以发送信号到另一个进程,而接收者会将控制突然转移到它的一个信号处理程序。一个程序可以通过回避通常的栈规则,并执行到其他函数中任意位置的非本地跳转来对错误做出反应。
0x2: 异常控制流处理机制和中断技术的关系
在继续深入学习之前,我们必须先理清一个基本概念:
异常控制流是操作系统中的一种控制流处理机制,异常控制流处理机制被用于实现操作系统中的CPU处理流程切换的实现,而中断技术是实现这一技术的方法。即中断是一个技术,而异常控制流处理是一种机制
2. 中断类型
中断是现代操作系统的一项重要技术,利用中断技术可以极大地提高系统吞吐量。从本质上理解,中断是CPU提供的一项硬件机制,CPU可以根据中断号跳转到相应的中断处理例程上去
0x1: 中断硬件实现
在硬件实现上,中断可以是:
1 . 包含控制线路的独立系统(本文重点学习的) 在IBM个人机上,广泛使用可编程中断控制器(Programmable Interrupt Controller,PIC)来负责中断响应和处理。PIC被连接在若干中断请求设备(各种外设)和处理器(CPU)的中断引脚之间,从而实现对处理器中断请求线路
(多为一针或两针)的复用 2 . 被整合进存储器子系统中 作为另一种中断实现的形式,即存储器子系统实现方式,可以将中断端口映射到存储器的地址空间,这样对特定存储器地址的访问实际上是中断请求
0x2: 中断分类
从实现机制上来分,中断可以分为以下2类:
1 . 外部中断(包括可屏蔽、不可屏蔽中断) 外部中断是由外部设备引发的中断,而引发中断的设备被称为中断源,中断源大致可以分为以下几种 1 ) 定时器、计时器 2 ) 键盘 3 ) 内部实时时钟 4 ) 通用接口 5 ) PS/ 2鼠标 6 ) 协处理器 7 ) IDE/ SATE硬盘 8 ) 串口 9 ) 并口 10 ) 软盘 ... 外部设备通过 " 可编程中断控制器(Programmable Interrupt Controller,PIC) " 向CPU报告的中断,大致流程如下: 1 ) 外部设备通过中断请求线(IRQ)连接到一个 " 中断控制器 " 上 2 ) 当一个外部设备需要发出中断时,会驱动对应的中断请求线进入有信号状态 3 ) 中断控制器检测这个中断是否被屏蔽了(CPU的IF位被置1,则不屏蔽任何外部中断;CPU的IF位被置0,则屏蔽所有外部中断),如果没有被屏蔽就驱动CPU的 " INTR中断请求线 " 进入信号状态 4 ) CPU随后就能检测到这个中断了(在每次CPU周期的下降沿检测一次中断) 5 ) 如果该中断被屏蔽(CPU中的中断屏蔽寄存器被选中),中断控制器中的寄存器中的某一位位将记录这一请求,等到中断被开启时再驱动CPU的 " INTR中断请求线 " 进入信号状态 6 ) 之后CPU通过 " 中断应答 " 从中断控制器的数据线上读取中断号,并通过中断号获取中断向量 7 ) 如果多个设备(外设中断源)在同一时刻通过不同的中断请求线发出中断请求,中断控制器也会将这些请求记录在不同的位中 8 ) 如果这些中断都没有被屏蔽,则中断控制器根据优先级,依次执行优先级高的中断(IRQn的数字n越小优先级越大) /* 关于中断屏蔽,这里需要补充几点 关闭外部中断的方式有: 1. 通过cli指令把标志寄存器中的IF位清零,这样就关闭了"所有的"外部中断 2. 通过中断控制器中的中断屏蔽寄存器,屏蔽某一特定的IRQn,从而屏蔽该中断(只是屏蔽某个中断,不影响其他中断) 3. 在很多外设上,也设有控制寄存器,可以通过外设的中断控制器从数据源上关闭外设上的某个IRQn,从而屏蔽某个中断 */ 2 . 内部中断 和外部中断相对的就是内部中断。从CPU的角度看,外部中断是一个异步事件,它可能在任何时候发送,而内部中断是一个同步事件,它是执行某条指令时产生的。 内部中断可以大致分为以下几种 1 ) 异常(faults) CPU在指令执行时产生的,异常是可以修复的。当异常发生时,压入堆栈的是产生异常的 " 那条指令 " ,当CPU执行异常处理程序结束后,将 " 重新执行那一条指令 " 。 1.1 ) 缺页异常: 14 : #PF 1.2 ) 保护错误(内存或其他保护检查): 13 : #GP 1.3 ) 堆栈段错误(堆栈操作或者加载SS): 12 : #SS 1.4 ) 段不存在(加载段寄存器后访问段): 11 : #NP 1.5 ) 除法错误(DIV/IDIV指令): 0 : #DE 1.6 ) 越界: 5 : #BR 1.7 ) 无效操作码(无效操作指令): 6 : #UD 1.8 ) 对齐校验(内存访问): 17 : #AC 2 ) 陷阱(traps) 在CPU执行陷阱指令后,立刻通过中断描述表执行预定的陷阱处理例程。陷阱处理例程执行结束后,将返回陷阱指令的 " 下一条指令 " 继续执行。 2.1 ) 系统调用(system call) 系统调用是一种软中断,软中断是一条CPU指令,用以自陷一个中断。由于软中断指令通常要运行一个切换CPU至内核态(Kernel Mode /Ring 0 )的子例程,它常被用作实现系统调用(System call)
这是最长使用到的中断,我们在编程中使用到的API最终都会通过系统调用这种内部中断来进行ring3到ring0的切换 2.2 ) 单步异常(调试异常): 1 : #DB 用于单步执行、内存断点 2.3 ) INT3(断点异常): 3 : #BP 2.4 ) 溢出(指令INT0): 4 : #OF 3 ) 终止(aborts) 3.1 ) 双重错误(所有能产生异常、NMI、或者INTR的指令): 8 : #DF /* 关于中断号(向量号)、中断向量表、中断描述符表的区别 1. 中断号(向量号) 中断号(向量号)是用来在中断描述符表中定位中断描述符的 2. 中断描述符表 保存中断描述符的一段连续内存(可以理解为一张表)。中断描述符是用来获取中断向量用的(知道了中断向量就知道中断服务程序的入口地址) 3. 中断向量表 保存中断向量的一段连续内存(可以理解为一张表)。中断向量代表着中断服务程序的入口地址 例如: INT 21H 21就是中断号 21H就就是一个中断描述符 21H*4 =84H 得到的就是中断向量 以84H为首地址(84H 85H 86H 87H)其中存放的就是中断服务程序的地址
注意,上面说的是在DOS系统中采用的中断例程寻址方法,随着计算机体系结构的发展,目前操作系统已经不采用这种寻址方法了,而是采用IDT机制进行中断例程的寻址。关于IDT,我们下面会讲到 */
3. 中断的初始化
我们知道,中断向量号是8位的,那么它一共有256项(0-255),也就意味着中断描述符表也是256项(其中记录着中断向量表的表项索引),同时中断向量表也是256项(其中记录着中断处理例程的入口地址)
对于不同的中断,在中断初始化和中断处理过程中,其处理方式是不一样的
1 . 内部中断( 0 ~ 31号、0x80作为中断号) 只要初始化: 1 ) 相关的中断向量表 2 . 外部中断( 0 ~255中的除了0~ 31号、0x80的其他中断号) 需要初始化: 1 ) 相关的中断向量表 2 ) 以及中断控制器(控制器负责优先级排队、屏蔽等工作)
0x1: 内部中断初始化
内部中断的初始化需要对0~31号和0x80号系统保留中断向量的初始化,这部分草走在trap_init()中完成
\linux-3.15.5\arch\x86\kernel\traps
void __init trap_init( void ) { int i; #ifdef CONFIG_EISA void __iomem *p = early_ioremap( 0x0FFFD9 , 4 ); if (readl(p) == ' E ' + ( ' I ' << 8 ) + ( ' S ' << 16 ) + ( ' A ' << 24 )) EISA_bus = 1 ; early_iounmap(p, 4 ); #endif /* trap_init()主要是调用set_xxx_gate(中断向量, 中断处理函数) set_xxx_gate()就是按照中断门的格式填写中断向量表的 Intel x86支持4种"门描述符": 1) 调用门(call gate) 2) 陷阱门(trap gate) 3) 中断门(iinterrupt gate) 4) 任务门(task gate) */ set_intr_gate(X86_TRAP_DE, divide_error); set_intr_gate_ist(X86_TRAP_NMI, & nmi, NMI_STACK); /* int4 can be called from all */ set_system_intr_gate(X86_TRAP_OF, & overflow); set_intr_gate(X86_TRAP_BR, bounds); set_intr_gate(X86_TRAP_UD, invalid_op); set_intr_gate(X86_TRAP_NM, device_not_available); #ifdef CONFIG_X86_32 set_task_gate(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS); #else set_intr_gate_ist(X86_TRAP_DF, & double_fault, DOUBLEFAULT_STACK); #endif set_intr_gate(X86_TRAP_OLD_MF, coprocessor_segment_overrun); set_intr_gate(X86_TRAP_TS, invalid_TSS); set_intr_gate(X86_TRAP_NP, segment_not_present); set_intr_gate_ist(X86_TRAP_SS, & stack_segment, STACKFAULT_STACK); set_intr_gate(X86_TRAP_GP, general_protection); set_intr_gate(X86_TRAP_SPURIOUS, spurious_interrupt_bug); set_intr_gate(X86_TRAP_MF, coprocessor_error); set_intr_gate(X86_TRAP_AC, alignment_check); #ifdef CONFIG_X86_MCE set_intr_gate_ist(X86_TRAP_MC, & machine_check, MCE_STACK); #endif set_intr_gate(X86_TRAP_XF, simd_coprocessor_error); /* Reserve all the builtin and the syscall vector: */ for (i = 0 ; i < FIRST_EXTERNAL_VECTOR; i++ ) set_bit(i, used_vectors); #ifdef CONFIG_IA32_EMULATION set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall); set_bit(IA32_SYSCALL_VECTOR, used_vectors); #endif // 设置系统调用中断 #ifdef CONFIG_X86_32 set_system_trap_gate(SYSCALL_VECTOR, & system_call); set_bit(SYSCALL_VECTOR, used_vectors); #endif /* * Set the IDT descriptor to a fixed read-only location, so that the * "sidt" instruction will not leak the location of the kernel, and * to defend the IDT against arbitrary memory write vulnerabilities. * It will be reloaded in cpu_init() */ __set_fixmap(FIX_RO_IDT, __pa_symbol(idt_table), PAGE_KERNEL_RO); idt_descr.address = fix_to_virt(FIX_RO_IDT); /* * Should be a barrier for any external CPU state: */ cpu_init(); x86_init.irqs.trap_init(); #ifdef CONFIG_X86_64 memcpy( &debug_idt_table, &idt_table, IDT_ENTRIES * 16 ); set_nmi_gate(X86_TRAP_DB, & debug); set_nmi_gate(X86_TRAP_BP, & int3); #endif }
0x2: 外部中断初始化
外部中断的初始化需要:
1 . 对除了0~ 31 、0x80中断号之外的其它中断向量 2 . 中断控制器的初始化(相比内部中断初始化多了这一步)
这两步操作都在在init_IRQ()中完成
\linux-3.15.5\arch\x86\kernel\i8259.c
void __init init_IRQ( void ) { int i; /* * We probably need a better place for this, but it works for * now ... */ x86_add_irq_domains(); /* * On cpu 0, Assign IRQ0_VECTOR..IRQ15_VECTOR's to IRQ 0..15. * If these IRQ's are handled by legacy interrupt-controllers like PIC, * then this configuration will likely be static after the boot. If * these IRQ's are handled by more mordern controllers like IO-APIC, * then this vector space can be freed and re-used dynamically as the * irq's migrate etc. */ for (i = 0 ; i < legacy_pic->nr_legacy_irqs; i++) // 对于单CPU结构, per_cpu(vector_irq, 0 )[IRQ0_VECTOR + i] = i; // x86_init.irqs.intr_init()等价于调用:native_init_IRQ() x86_init.irqs.intr_init(); } void __init native_init_IRQ( void ) { int i; /* Execute any quirks before the call gates are initialised: */ x86_init.irqs.pre_vector_init(); // 调用 init_ISA_irqs apic_intr_init(); /* * Cover the whole vector space, no vector can escape * us. (some of these will be overridden and become * 'special' SMP interrupts) */ /* interrupt数组,它保存的是每个中断服务程序的入口地址,它的定义是在\linux-3.15.5\arch\x86\kernel\entry_32.S中 */ for (i = FIRST_EXTERNAL_VECTOR; i < NR_VECTORS; i++ ) { // 设置32~255号中断 /* IA32_SYSCALL_VECTOR could be used in trap_init already. */ if (! test_bit(i, used_vectors)) { // 要除去0x80中断 set_intr_gate(i, interrupt[i- FIRST_EXTERNAL_VECTOR]); } } if (!acpi_ioapic && ! of_ioapic) setup_irq( 2 , & irq2); #ifdef CONFIG_X86_32 /* * External FPU? Set up irq13 if so, for * original braindamaged IBM FERR coupling. */ if (boot_cpu_data.hard_math && ! cpu_has_fpu) setup_irq(FPU_IRQ, & fpu_irq); irq_ctx_init(smp_processor_id()); #endif }
4. 门描述符
在开始学习IDT之前,我们必须先了解一下什么是门描述符。
在I386CPU中,除了"段描述符"(描述某种内存段)之外还有一种描述符叫做"门描述符"(描述控制转移的入口点,也就是异常控制中断的入口点),通过这种门可以实现特权级的转变和任务的切换。门描述符主要由以下几部分组成:
1 . 选择子 2 . 偏移地址 3 . DPL
门描述符共有四种
1 . 调用门描述符 调用门一般用在特权级的切换,存在于GDT中或者LDT中。调用门的选择子指向代码段描述符,偏移地址对应代码段中的偏移量。当jump和call指令的操作数是调用门的时候,就会跳转到对应的代码处,并发生特权级的变化,也就会发生
堆栈的切换 2 . 任务门描述符 任务门一般用在任务的切换,可以存放在GDT、LDT或IDT中。任务门的选择子指向GDT中的TSS选择符,偏移地址没有意义。当jmp和Call指令的操作数是任务门的时候,就会发生任务的切换。 3 . 中断门描述符 4 . 陷阱门描述符 中断门描述符、陷阱门描述符用来对中断服务例程进行寻址,从原理上来理解,中断例程的寻址本质上也是内存的寻址
看到这里,我们必须灵活、严谨地理解操作系统中的关于内存寻址的概念
1 . 操作系统一定是使用基于 " 段选择子 " + " GDT " + " LDT " 进行内存寻址 2 . 中断门、陷阱门只是一种数据结构,它包括了 " 段选择子 " 这个字段,同时还有别的字段用于中断例程寻址的目的,中断的寻址同样也是段+ 段内偏移方式寻址 3 . 在windows、linux操作系统中,这种数据结构的叠加、包含很常见,但我们始终要明确操作系统最底层的运行机制 /* 关于"段选择子"+"GDT"+"LDT"进行内存寻址的相关细节,请参阅另一篇文章 http://www.cnblogs.com/LittleHann/p/3453148.html (搜索"25. PVOID LdtInformation",从那开始就是了) */
我们这里学习一下中断门、陷阱门描述符的数据结构
/* 1. offset_low: 32位偏移的低16位 2. selector: 选择子 1) 段索引 2) 指示位 2.1) 0表示在GDT中选择 2.2) 1表示在LDT中选择 3) 当前请求特权级(想要访问什么级别的段) 3. reserved: 保留字段 4. type: 段类型 5. always: 总为1 6. DPL: (描述符特权级 Descriptor Privilege Level) 表示允许访问此段的最低特权级("段选择子"中有一个字段(RPL)是标识这个段选择子也即这个内存访问请求的特权级),这样是不是就把对应关系建立起来了,比如DPL为0的段只有当RPL=0时才能访问,而DPL为3的段,可由任何RPL的
代码访问。这样就解释了为什么ring3的内存空间ring0的内核代码可以任意访问,而ring0的内存空间ring3不能访问了 7. present 8. offset_high: 2位偏移的高16位 */ typedef struct IDT_GATE_DESCRIPTOR { P2C_U16 offset_low; P2C_U16 selector; P2C_U8 reserved; P2C_U8 type: 4 ; P2C_U8 always: 1 ; P2C_U8 dpl: 2 ; P2C_U8 present: 1 ; P2C_U16 offset_high; } IDT_GATE_DESCRIPTOR, *IDT_GATE_DESCRIPTOR
5. IDT中断描述符表
linux-2.6.32.63\arch\x86\kernel\traps.c
gate_desc idt_table[NR_VECTORS] __page_aligned_data = { { { { 0 , 0 } } }, }; struct desc_struct { unsigned long a,b; }; /* 可以看出 IDT表共256个表项,每一个表项是8个字节的gate_desc结构(这个gate_desc就叫门描述符),在idt_table数组被定义时静态初始化为0。 */
IDT表可以驻留在线性地址空间的任何地方,处理器使用IDTR寄存器来定位IDT表的位置(LIDT和SIDT指令分别用于加载和保存IDTR寄存器的内容)。这个寄存器中含有IDT表32位的基地址和16位的长度(限长)值,与GDT和LDT表类似,IDT也是由8字节长描述符组成的一个数组(联想GDT、LDT的结构)
总结一下整个中断例程的寻址过程
1 . 根据IDTR获取IDT基址(LIDT指令)-> 2 . 根据CPU获取的中断号得到当前中断对应于IDT中的某个表项(某个门描述符)-> 3 . 解析指定的中断门、陷阱门(就是上一步获取的IDT表项)-> 4 . 从门描述符中获得段选择子等信息-> 5 . 根据选择子+GDT+LDT最终中断例程虚拟内存寻址-> 6 . 获得指定中断例程的虚拟内存地址-> 7 . 执行中断程序
6. 异常控制类型
我们已经学习了中断有内部和外部中断,外部中断来自于硬件外设,内部中断又可以分为异常(faults)、陷阱(traps)、终止(aborts)。我们需要牢记的是,操作系统是不能执行命令的,整个机器中可以执行命令的只有是CPU,CPU通过硬件的方式提供中断机制,中断是所有异常处理的根本技术。不管是进程切换、硬件外设、除零异常、虚拟内存中的发生的缺页异常处理、SEH,归根结底,全部都要通过CPU的中断机制来实现。
异常控制流处理就是我们在中断的基础上,抽象出的一个理论性概念
异常(exception)就是控制流中的突变,用来响应处理器状态中的某些变化。在处理器中,状态被编码为不同的位和信号。状态变化称为事件(event)。
我们在学习异常处理流程的时候,要注意不要把异常和中断割裂成2个独立的概念去学习,相反,异常和中断说的都是一回事。只是一个从操作系统概念层面去阐述,一个从硬件技术原理角度去阐述
在任何情况下,当处理器检测到有事件(内部异常、外部异常)发生时,它就会通过一张叫做异常表(exception table)的跳转表(中断描述符表),进行一个间接过程调用(异常处理例程的调用)(通过中断向量表),到一个专门用来处理
这类事件的操作系统子程序(异常处理程序 exception handler)。进行相应的异常处理
0x1: 异常的分类
异常可以分为四类:
1 . 中断(interrupt) 来自I / O设备的信号,异步,总是返回到下一条指令 2 . 陷阱(trap) 有意的异常,是执行一条指令的结果。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫系统调用 同步, 总是返回到下一条指令 3 . 故障(fault) 潜在可恢复的错误。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。斗则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。 同步,可能返回到当前指令 4 . 终止(abort) 不可恢复的错误,同步,不会返回
Copyright (c) 2014 LittleHann All rights reserved