自己动手写操作系统 第六章 :丰富中断处理程序

时间:2022-11-09 14:36:30
在上面部分,我们仅仅添加了中断,但是中断没有进行任何处理,下面,我们来丰富一下中断处理程序

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的设置问题;堆栈的切换是重头戏。