1,当前进程被中断打断后,处理器根据中断向量号找到对应的中断描述符,拿CPL和中断门描述符中选择子对应的目标代码段的DPL对比,若CPL权限比DPL低,即数值上CPL >DPL,表示要想高特权级转移,需要切换高特权级的栈。即:从用户进程中断去执行内核中断程序。中断执行完返回时也要回到之前的旧栈,所以处理器临时将SS_old 和 ESP_old 保存到一个内存中,然后CPU取出TSS中目标代码段的DPL 级别的栈加载到寄存器 SS 和 ESP 中,记作 SS_new 和 ESP_new,CPU在找到之前的旧栈,将其压入新栈中:将 SS_old 用0 扩展高16位,成为32位数据。
2,在新栈中压入EFLAGS寄存器
3,将要CS_old 和 EIP_old 压入新栈:CS_old 需要用0 来填充高16位为0。
4,某些异常有错误码,错误码用于报告哪个段上发生了异常,所以ERROR_CODE也压入栈。
特权级未发生转移时发生的中断:正在执行内核代码发生了中断
1,在新栈中压入EFLAGS寄存器
2,将要CS_old 和 EIP_old 压入新栈:CS_old 需要用0 来填充高16位为0。
3,某些异常有错误码,错误码用于报告哪个段上发生了异常,所以ERROR_CODE也压入栈。
用 iret 指令实现,iret 指令并不知道栈内容的正确性,一次弹出4字节,所以在使用iret 之前,一定要调整好esp的位置,使其依次弹出:EIP、CS、EFLAGS、ESP、SS。因此我们要手动跳过 ERROR_CODE。
1,当处理器执行到 iret 时,需要从栈中返回。CPU首先检查CS_old 的选择子的 RPL 位,判断在返回过程中是否要改变特权级。弹出 EIP_old 和 CS_old 。
2,弹出 EFLAGS。若需要改变特权级,则弹出旧栈。若不需要改变特权级,则是平级转移。
如果需要改变特权级,CPU还会检查数据段寄存器DS、ES、FS、GS的内容,如果它们之中某个寄存器的选择子所指向的数据段描述符的DPL权限比返回后的CPL高,则处理器将该段寄存器置为 0 。
8259A内部有两组寄存器,一组是初始化命令寄存器,用来保存初始化命令字,ICW1-ICW4。另一组寄存器是操作命令寄存器组,用来保存操作命令字,OCW1-OCW3。我们对8259A的编程,也分为初始化和操作两部分。
1,用ICW作初始化:确定是否需要级联、设置起始的中断向量号、设置中断结束模式等等
ICW1用来初始化8259A的连接方式:单片还是级联 中断信号触发方式:电平触发还是边沿触发
ICW2用来设置起始中断向量号。指定了 IRQ0 映射到的中断向量号,其他 IRQ 接口对应的中断向量号会顺着自动排列下去。
ICW3用来设置级联方式下主片和从片用哪个IRQ接口互连。
ICW4用来设置中断结束模式:自动还是手动。
2,用OCW来操作控制:中断屏蔽、中断结束
OCW1用来屏蔽连接在8259A上的外部设备中的中断信号。这些没有屏蔽的中断最终还是要受到eflags的IF位管束,IF=0,不管8259A怎么样设置全部屏蔽。
OCW2用来设置中断结束方式:发EOI来终止某一个中断 优先级模式:循环还是固定
OCW3用来设置特殊屏蔽方式和查询方式。
ICW1和OCW2、OCW3是用偶地端口 0x20(主片)或 0xA0(从片)写入。
ICW2-ICW4是用奇地址端口 0x21(主片) 或0xA1(从片)写入。
4个ICW要保证一定的次序写入,8259A就知道写入端口的数据是什么意思了。
OCW的写入顺序无关,各控制字有唯一标识可以辨别。
所以8259A 的编程步骤为:
1,首先必须进行初始化,必须依次写入 ICW1、ICW2 、ICW3 、ICW4
2,初始化后,可以进行OCW的操作。
Intel的8259A芯片位于主板的南桥芯片中。主片IR0引脚上就是时钟中断,这已经由内部电路实现了。我们只打开时钟中断,屏蔽其他外部设备的中断。中断向量号的前32个(0-31)都被系统的内部中断给占用了,所以我们的时钟中断向量号只能从0x20开始。我们设置0x20为时钟中断的向量号。所以我们要在中断描述符中的第33个中断描述符中加载时钟中断处理程序的选择子和偏移地址。因此我们要定义33个描述符的中断描述符表。由于我们可以屏蔽的中断信号只能是外部设备的可屏蔽中断信号。所以当处理器内部发生内部中断时也会产生0-31的向量号,到时候也会到中断描述符中去找中断处理程序的地址。这些程序都需要我们来写。所以我们写的程序就有中断向量号为0-31这32个内部中断的处理程序和中断向量号为32的时钟中断处理程序。
宏属于预处理指令。预处理指令被编译器的预处理器支持,属于伪指令。这类指令是在编译前,编译器需要预先处理的指令,预处理器会将这些伪指令展开替换成具体的编译器可以识别的语言符号,在预处理后,其中的预处理指令(伪指令)全部都会不见的。比如#include<>、#define等都是预处理指令,都会被预处理器在预处理阶段都替换成具体所代表的代码或数字。
宏:Macro。用来代替重复性输入,是一段代码的模板。
定义单行的宏,汇编中可以用%define 指令实现。
定义多行的宏,汇编中可以用%macro来实现。
%macro 宏名字+参数个数
宏代码体 ;要引用某个参数,需要用“% 数字” 的方式来引用。参数序号从 1 开始
%endmacro
总结我们的中断建立的步骤为:
1,编写中断处理程序,记录每个中断程序的地址
2,构造出中断描述符表,将中断程序的地址加载进去
3,设置完成8259A
4,加载IDTR
5,将eflags的IF位置1
我们采取汇编语言和C语言相结合的方式来编写中断处理程序。因为C语言编写程序更方便,我们用C语言来写中断处理程序。发生中断后,首先执行汇编语言写的中断处理程序,然后在调用C语言写的中断处理程序执行。即汇编中的中断程序只是提供了中断处理程序的入口,进入后在转去执行C语言的中断处理程序,C语言的程序才是真正的处理程序。
在内部中断的前32个中,有些中断类型有错误码,系统就自动压入了错误码,有些中断类型没有错误码,系统没有错误码,为了保持一致性,所以我们要统一压入错误码。
中断程序的汇编中断处理代码:只是提供一个入口功能,然后转去C中断处理程序。我们要编写33个汇编中断处理程序:0-32是内部中断的处理程序,33是时钟的中断程序。
[bits 32]
%define ERROR_CODE nop ; 若在相关的异常中cpu已经自动压入了错误码,为保持栈中格式统一,nop是不做操作.
%define ZERO push 0 ; 若在相关的异常中cpu没有压入错误码,就手工压入一个0
extern idt_table ;idt_table是C中注册的中断处理程序数组,数组元素是C语言的中断处理程序的地址。
section .data
global intr_entry_table ;定义intr_entry_table为全局变量数组名,每个数组元素为程序的地址
intr_entry_table: ;因为这个标号是在 .data段内的,所以这个标号指的是 .data里面的地址
%macro VECTOR 2
section .text
intr%1entry: ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少
%2 ; 中断若有错误码会压在eip后面
; 以下是保存上下文环境
push ds
push es
push fs
push gs
pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ; 中断结束命令EOI
out 0xa0,al ; 向从片发送
out 0x20,al ; 向主片发送
push %1 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
call [idt_table + %1*4] ; 调用idt_table中的C版本中断处理函数
jmp intr_exit
section .data
dd intr%1entry ; 存储各个中断入口程序的地址,形成intr_entry_table数组
%endmacro
section .text
global intr_exit
intr_exit:
; 以下是恢复上下文环境
add esp, 4 ; 跳过中断号
popad
pop gs
pop fs
pop es
pop ds
add esp, 4 ; 跳过error_code
iretd
;0-31个中断向量号是内部中断,有些有错误码,有些没有。32中断向量号为时钟中断
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO
编译器编译程序时会按照每个节的属性,将相同属性的节放在一起的。所以我们编译器会分别将代码段和数据段放在一起的。因为预处理器会在编译前提前将代码展开,所以最后 .data放在一起,可以构成一个数组,数组元素是代码段的地址。 idt_table的数组元素是每一个C处理程序的地址。所以 call [idt_table+4*num]==call 处理函数地址。
eflags、cs、eip、error_code是CPU自己压入栈的,我们不需要管,我们要负责的是其他入栈参数。注意 iret 时候要跳过error_code 使esp 指向eip。
中断的C语言中断处理程序:里面是真正的中断处理程序。这里我们只设置一个通用的,33个中断都是这一个中断函数。将中断函数的地址循环33编放入数组中即可。
intr_handler idt_table[IDT_DESC_CNT]; //定义C语言中断处理程序数组
/* 通用的中断处理函数,一般用在异常出现时的处理 */
//功能是:出现异常时显示中断向量号;
static void general_intr_handler(uint8_t vec_nr)
{
if (vec_nr == 0x27 || vec_nr == 0x2f) { // 0x2f是从片8259A上的最后一个irq引脚,保留
return; //IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。
}
put_str("int vector: 0x");
put_int(vec_nr);
put_char('\n');
}
static void exception_init(void)
{ // 完成一般中断处理函数注册注册
int i;
for (i = 0; i < IDT_DESC_CNT; i++)
{
idt_table[i] = general_intr_handler; // 默认为general_intr_handler。
}
}
中断描述符表IDT的建立
中断门描述符结构体,要按照中断门描述符中的顺序来,低地址的先定义,则先储存在内存中
struct gate_desc
{
uint16_t func_offset_low_word; //要先定义,则会先储存在内存中
uint16_t selector;
uint8_t dcount; //此项为双字计数字段,是门描述符中的第4字节。此项固定值,不用考虑
uint8_t attribute;
uint16_t func_offset_high_word;
};
//定义中断描述符表,有33个门描述符,0-31中断向量号被系统占用,我们从32(0x20)开始定义时钟中断
static struct gate_desc idt[IDT_DESC_CNT];
//声明出中断程序入口地址数组,有33个地址,0-31的地址属于内部中断程序地址,0x20的地址属于时钟中断程序地址
//这些地址在汇编的中断程序中已经得出
extern intr_handler intr_entry_table[IDT_DESC_CNT]; //汇编中断处理地址的中的全局数组,元素是每个中断程序的入口地址
//声明中断描述符构造函数
static void make_idt_desc(struct gate_desc* p_gdecs,uint8_t attr,intr_handler function);
//门描述符构造函数
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function)
{
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}
//构造出33个门描述符
static void idt_desc_init(void)
{
int i;
for (i = 0; i < IDT_DESC_CNT; i++)
{
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
put_str(" idt_desc_init done\n");
}
设置头文件io.h。通过这个头文件来读写端口。
static函数的作用:普通的函数名是全局变量,在一个文件中定义后,在所有的文件中都可以使用。static函数,在一个文件中定义后,只能在那个文件中使用,其他文件不能用。
inline函数的作用:当主调函数调用被调函数时候,主调函数要压栈,call,然后cs和ip转过去执行等等,被调函数也要备份等等,速度很慢。inline函数是当主调函数调用被调函数时,直接在调用出原封不动的将被调函数的函数体展开,这样就不用call了,这就不属于函数调用了,cs和ip的值也不用变,依次执行即可。
我们将static函数放在头文件中的目的在于:因为这是对端口进行操作,所以需要的条件就是一定要快。static函数放在头文件中,那么所有包含该头文件的文件都有这个static函数,static函数执行起来比普通函数要快很多,因为静态函数会被自动分配在一个一直使用的静态存储区,直到退出应用程序才清空这个区域,而普通的函数还生成了一些其他信息来保证安全,所以static函数速度快很多。
内存循环复制三剑客:cld、(ES:ESI、ES:EDI)、repmovs[dw]
端口循环读写三剑客:cld、(es:edi)、rep ins[dw] cld、(es:esi)、rep outs[dw]
两个指令都是在执行 rep前先读取ecx的值与0比较,循环执行后ecx的值减1,然后esi、edi自动加上相应的字节。
1,首先我们编写端口读写函数,不管什么端口读写,调用即可。
#ifndef _LIB_IO_H
#define _LIB_IO_H
//向端口port写入一个字节.N为立即数约束:表示0-255个端口号
static inline void outb(unit16_t port, unit_t data)
{
asm volatile("outb %b0,&w1"::"a"(data),"Nd"(port));//b和w来限制寄存器大小
}
//将addr处起始的 word_cnt 个字写入端口port。
//执行rep指令前先拿ecx与0比较,不相等则可以执行,执行完后edi加一个字,ecx减1。在执行rep之前再比较
static inline void outsw(uint16_t port,const void* addr,unit32_t word_cnt)
{
asm volatile("cld;rep outsw": "+S"(addr),"+c"(word_cnt): "d"(port));
}
//将从端口port读入一个字节返回
static inline uint8_t inb(uint16_t port)
{
uint8_t data;
asm volatile("inb %w1,%b0":"=a"(data):"Nd"(port));
return data;
}
//将从端口port读入的word_cnt个字写入addr
//
static inline void insw(uint16_t port,void* addr,uint32_t word_cnt)
{
asm volatile("cld;rep insw":"+D"(addr),"+c"(word_cnt):"d"(port):"memory");
}
#endif
2,当我们设置8259A时,我们调用端口读写函数即可
/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
/* 初始化主片 */
outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片.
outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 初始化从片 */
outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
outb (PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 只打开主片上IR0,也就是目前只接受时钟产生的中断 */
outb (PIC_M_DATA, 0xfe);
outb (PIC_S_DATA, 0xff);
put_str("pic_init done\n");
}
根据IDTR的结构,采用内联汇编的形式加载进去。
数组名便是指针,所以,
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));//将idt的基地址左移16位,
asm volatile("lidt %0" : : "m" (idt_operand));
asm volatile("sti");
计算机时钟分为两类:内部时钟和外部时钟
内部时钟:是内部元件,位于主板上,由晶体振荡器产生,其频率经过分频之后就是主板的外频,外频乘以某个倍数就是主频。内部定时无法改变。
外部时钟:是处理器与外部设备之间通信采用的一种时序
内部时钟和外部时钟是两套独立运行的定时体系。对于外部定时,我们有两种实现方式:一种软件实现,一种硬件实现为定时器。
计时器的功能就是定时发出信号,当到达所计数的时间,计数器可以发出一个中断信号,处理器转去执行相应的中断处理程序。计数器内部有计算脉冲信号,每一个时钟脉冲信号到来时都会修改计数值。
8253用的是 倒计时 的方式。比如为了放在DRAM(内存卡)数据丢失,每隔一段时间就要进行充电刷新,就是利用这个向CPU发出中断去刷新的。我们设置8253来调整时钟中断发生的频率。
每个计数器有3个引脚
CLK:时钟输入信号,即计数器自己的时钟频率,连接到8253的CLK的脉冲频率为 2MHZ。
GATE:门控输入信号
OUT:计数器输出信号,当计数值为0时,用于通知处理器或者某个设备:定时完成
计数开始之前的计数初值保存在计数初值寄存器中,减法计数器将此值载入后,CLK 收到一个脉冲信号,减法计数器就将计数值减1,同时将当前值保存到输出锁存器中。当计数值为0时,OUT引脚输出中断信号或者启动某个设备工作。
因此我们使用计数器0来产生时钟信号,这个时钟是连接饿到 IRQ0 引脚上的那个时钟,即计数器0决定了定时中断信号的发生频率。因此计数器0的计时到期后就会发出中断时钟中断信号,中断代理8259A就会感知引脚 IRQ0 有中断信号到来。
我们通过控制字寄存器来设置计数器0的工作方式。我们通过控制字寄存器选择用哪个计数器,指定该计数器的控制模式,再用该计数器写入计数初值就行。
默认情况下,三个计数器的工作频率位 1.19318MHz,即一秒内有 1193180 次脉冲信号,一秒内会减1193189次1,计数器0的默认值是0,即65536,那么一秒内的输出信号次数大约为 : 1193180/65536=18.206,所以时钟中断信号的频率为18.206Hz,大约55毫秒发生一次中断。这就是我们默认情况下的时钟中断频率了。
我们的目的是为了使中断发生的快一点。默认频率为18.206Hz,我们调整为100Hz,即一秒钟100次中断。我们通过8253,通过控制字指定使用计数器0,工作方式选择方式2(比率发生器),并为附上合适的初值 1193180/100=11932。
static void frequency_set(uint8_t counter_port, uint8_t counter_no, uint8_t rwl, uint8_t counter_mode, uint16_t counter_value)
{
/* 往控制字寄存器端口0x43中写入控制字 */
outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
/* 先写入counter_value的低8位 */
outb(counter_port, (uint8_t)counter_value);
/* 再写入counter_value的高8位 */
outb(counter_port, (uint8_t)counter_value >> 8);
}
可以定义枚举来表示中断状态
enum intr_status
{
INTR_OFF;
INTR_ON;
}
获取当前eflags的中断状态状态
我们定义宏函数,首先先看一下宏函数:
宏函数实现:
#define MAX( a, b) ( (a) > (b) (a) : (b) )
普通函数来实现:
int max( int a, int b)
{
return (a > b? a : b);
}
很显然,我们不会选择用函数来完成这个任务,原因有两个:
1,普通的函数会带来额外的开销:将实参压栈、压入地址、弹出地址、释放堆栈等等。这种开销不仅会降低代码效率,而且代码量也会大大增加,而使用宏定义则在代码调用处直接展开,省去了很多工作;
2,普通函数的参数必须被声明为一种特定的类型,所以它只能在类型合适的表达式上使用,我们若要改变类型,只能另外写一个函数。反之,宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的。
不过和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都会插入到程序中。除非宏非常短,否则使用宏会大幅度增加程序的长度。
//返回eflags寄存器到变量中
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl;popl %0":"=g"(EFLAG_VAR))
//获取当前状态
enum intr_status intr_get_status()
{
int eflags=0;
GET_EFLAGS(eflags);
return (EFLAGS_IF & eflags) ? INTR_ON:INTR_OFF;
}
//开中断并返回开中断前的状态
enum intr_status intr_enable()
{
enum intr_status old_status=INTR_ON;
if(old_status==intr_get_status())
return old_status;
old_status=INTR_OFF;
asm volatile("sti");
return old_status;
}
//关闭中断并返回中断前的状态
enum intr_status intr_enable()
{
enum intr_status old_status=INTR_OFF;
if(old_status==intr_get_status())
return old_status;
old_status=INTR_ON;
asm volatile("cti");
return old_status;
}