5.第二步——丰富中断处理程序
5.1打开时钟中断
这里,我们需要复习两个要点:1)要想使得中断控制器能够正常工作,还需要写OCW到中断控制器
OCW1:中断屏蔽寄存器的控制,端口是21h和A1h;
OCW2:EOI信号,在中断结束的时候发给20h端口和A0h端口
5.2现场的保护与恢复
回顾上面的过程,我们不难发现,我们从进程A进入中断处理程序中,并没有进行进程上下文的保护,而且我们使用了寄存器al,这无疑是很危险的。所以,我们先在中断程序中添加如下代码:push ds ; ┃ push es ; ┣ 保存原寄存器值 push fs ; ┃ push gs ; ┛ inc byte [gs:0] ; 改变屏幕第 0 行, 第 0 列的字符 mov al, EOI ; ┓reenable master 8259 out INT_M_CTL, al ; ┛ pop gs ; ┓ pop fs ; ┃ pop es ; ┣ 恢复原寄存器值 pop ds ; ┃ popad ; ┛
5.3赋值TSS.esp0
另外,ring0>>ring1的互相切换,也意味着堆栈的切换。而iretd仅仅保证了CS:IP的成功切换,SS:SP的切换需要用户来自己保证;SS的值是段基值,这个我们假定是一样的,然后我们需要处理的是sp0的值,要保证tss.sp0是正确的。每当进程运行的时候,tss.esp0应该是当前进程的进程表中保存寄存器值的地方,这样进程被挂起之后,才恰好保存寄存器到正确的位置。我们假设进程A在运行,那么tss.sp0应该是进程表A中regs的最高处,因为我们不肯能在A运行的时候来设置esp0的值。所以,必须在A被恢复运行之前,设置tss.esp0的值。
补充:关于lea指令:
1. MOV 的右值必须是常量,而不能是表达式,比如可以写MOV EAX, EBP,但不能写MOV EAX, EBP + 8。这是因为EBP + 8本身也需要一条指令来计算,所以不能跟MOV写在一条指令里。
2. 注意到在汇编指令的内存地址符[]内可以做算术运算,那是因为内存地址的计算在CPU里是由专门的处理单元AGU来处理的,并不占用算术运算单元ALU的时钟周期。但如果用MOV 接内存地址符号[]的话,会把[]里的地址指向的内存的内容取出来放入寄存器。比如 mov eax,[ebx+ecx*4h-20h],会把ebx+ecx*4h-20h计算的结果当成一个内存地址,然后去内存把该地址的内容取出送往eax。如果我们只是想得到算术运算结果怎么办呢?这时候就可以用到LEA指令了。因为LEA后面接内存地址符[]会把地址,而不是地址里的内容送入寄存器。比如,我们想计算ebx+ecx*4h-20h的结果,就可以这样写:
lea eax,[ebx+ecx*4h-20h]。
等价于:eax=ebx+ecx*4h-20h
让我们看一下改进之后的代码:
sub esp, 4 pushad ; ┓ push ds ; ┃ push es ; ┣ 保存原寄存器值 push fs ; ┃ push gs ; ┛ mov dx, ss mov ds, dx mov es, dx inc byte [gs:0] ; 改变屏幕第 0 行, 第 0 列的字符 mov al, EOI ; ┓reenable master 8259 out INT_M_CTL, al ; ┛ lea eax, [esp + P_STACKTOP] mov dword [tss + TSS3_S_SP0], eax pop gs ; ┓ pop fs ; ┃ pop es ; ┣ 恢复原寄存器值 pop ds ; ┃ popad ; ┛ add esp, 4 iretd
我们注意其中的一句:lea ax, [esp+p_STACKTOP]
其中lea的定义已经在上面说明,p_STACKTOP=SSREG+4,而EGSREG=0,所以,eax现在指向用户堆栈的栈顶。
5.4内核栈
我们不难看出,在5.3章节中,用户栈和内核栈是相邻的,如果我们在用户栈中利用堆栈,比如push数据,那么这势必毁坏了内核用于保存PCB的堆栈,所以,我们需要将用户栈和内核堆栈进行隔离。
56 sub esp, 4 157 pushad ; ┓ 158 push ds ; ┃ 159 push es ; ┣ 保存原寄存器值 160 push fs ; ┃ 161 push gs ; ┛ 162 mov dx, ss 163 mov ds, dx 164 mov es, dx 165 166 mov esp, StackTop ; 切到内核栈 167 168 inc byte [gs:0] ; 改变屏幕第 0 行, 第 0 列的字符 169 170 mov al, EOI ; ┓reenable master 8259 171 out INT_M_CTL, al ; ┛ 172 173 push clock_int_msg 174 call disp_str 175 add esp, 4 176 177 mov esp, [p_proc_ready] ; 离开内核栈; 178 179 lea eax, [esp + P_STACKTOP] 180 mov dword [tss + TSS3_S_SP0], eax 181 182 pop gs ; ┓ 183 pop fs ; ┃ 184 pop es ; ┣ 恢复原寄存器值 185 pop ds ; ┃ 186 popad ; ┛ 187 add esp, 4 188 189 iretd
5.5中断重入
接下来,我们遇到另外一个问题:是否运行中断嵌套?1)嵌套中断的结果:
首先,CPU在相应中断的过程中,会自动关闭中断——硬件决定,我们需要人为打开中断sti。为了保证中断能够成功嵌套,我们在中断处理程序中加入延迟函数,代码如下:
156 sub esp, 4 157 pushad ; ┓ 158 push ds ; ┃ 159 push es ; ┣ 保存原寄存器值 160 push fs ; ┃ 161 push gs ; ┛ 162 mov dx, ss 163 mov ds, dx 164 mov es, dx 165 166 mov esp, StackTop ; 切到内核栈 167 168 inc byte [gs:0] ; 改变屏幕第 0 行, 第 0 列的字符 169 170 mov al, EOI ; ┓reenable master 8259 171 out INT_M_CTL, al ; ┛ 172 sti 173 push clock_int_msg 174 call disp_str 175 add esp, 4 176 push 1 call delay add esp,4 cli 177 mov esp, [p_proc_ready] ; 离开内核栈; 178 179 lea eax, [esp + P_STACKTOP] 180 mov dword [tss + TSS3_S_SP0], eax 181 182 pop gs ; ┓ 183 pop fs ; ┃ 184 pop es ; ┣ 恢复原寄存器值 185 pop ds ; ┃ 186 popad ; ┛ 187 add esp, 4
改动代码之后,运行结果:
思路:在kernel的主体中加入k_reenter变量,初始化为-1,每次进入中断,变量+1,然后判断变量的值:
如果=0:打开中断,允许继续执行
如果!=0:说明存在中断重入,此时,中断直接返回(当然要先保存上下文。)
代码如下:(我们需要在global中增加变量的定义和声明,在main函数中将变量初始化)
169 mov al, EOI ; ┓reenable master 8259 170 out INT_M_CTL, al ; ┛ 171 172 inc dword[k_reenter] 173 cmp dword[k_reenter],0 174 jne .re_enter 175 176 sti 177 178 mov esp, StackTop ; 切到内核栈 179 push clock_int_msg 180 call disp_str 181 add esp, 4 182 183 push 1 184 call delay 185 add esp,4 186 187 cli 188 189 mov esp, [p_proc_ready] ; 离开内核栈; 190 191 lea eax, [esp + P_STACKTOP] 192 mov dword [tss + TSS3_S_SP0], eax 193 194 .re_enter: 195 dec dword[k_reenter]
改动以后,运行结果如下:
需要注意的是esp的设置问题;堆栈的切换是重头戏。