扩充内核
1.在引导过程中在屏幕上画一个图案
系统引导指的是将操作系统内核装入内存并启动系统的过程。在我们的操作系统中,引导由Boot.bin和Loader.bin程序负责。Boot.bin位于主引导扇区,而主引导扇区空间有限。Boot.bin的修改空间有限,不宜被修改。因此,我们修改Loader.asm,以实现在Loader加载内核完毕前,在屏幕上画一个ASCII图案。
首先,在Loader.asm增加一个数据区,用以存储ASCII图案。ASCII图案为一个由“#”构成的心形。图案尺寸为6 * 9(6行9列)。变量PatternWidth和PatternHeight分别为字符矩阵的列数和行数。
接着,在Loader.asm中添加一个名为DispPattern的函数。该函数将根据ASCII图案的尺寸将图案输出到显存中。DispPattern利用10h中断,将字符图案逐行显示。
DispPattern:
push ax
push cx
mov cx, PatternHeight
mov ax, PatternData
add dh, 3 ; 将第一个字符串输出到屏幕第4行
mov bp, ax ;
mov ax, ds ;
mov es, ax ; es:bp -> 第一个字符串
PatternLoop:
push cx ; 将待输出字符串的行数入栈保存
mov cx, PatternWidth ;设置cx为当前字符串宽
mov ax, 01301h
mov bx, 0004h
mov dl, 20
int 10h ; 调用中断,显示字符串
pop cx
add bp, PatternWidth ; es:bp -> 下一个字符串
add dh, 1 ; 行数加一
loop PatternLoop
pop cx
pop ax
ret
在找到内核kernel.bin后,调用图案显示函数,并显示字符串“Ready”。
在引导过程和内核初始化过程中,屏幕还会显示其它信息。最后,为了避免图案掩盖住这些信息,我们需要修改这些信息的输出位置。在进入实模式后,系统会调用DispMemInfo来输出内存信息。其中,字符串的显示是通过调用函数DispStr完成的。
查看DispStr,我们可以确定字符串的输出位置是由变量dwDispPos决定的,所以我们需要对dwDispPos修改,使输出位置下移,为ASCII图案腾出空间。
dwDispPos是_dwDispPos映射得到的,修改_dwDispPos即可。
另外,在kernel运行的过程中,会调用cstart函数完成GDT和堆栈切换。而cstart执行开始和结束时会输出两行提示信息。这两行提示信息也需要向下移动。
start.c中的cstart函数调用disp_str()函数完成提示信息的输出。
查看disp_str()的源码,我们可以得知:disp_str()会以变量disp_pos的值为字符出输出地址。在kernel.asm中,disp_pos最初被设置为0。cstart输出提示信息前,先输出了若干个换行,以避免其提示信息覆盖住之前输出的内存信息。因此,我们只需要再增加输出几个换行即可。
2.将内存管理模块添加进kernel
首先,创建一个新的汇编代码文件(memop.asm)并将之前实现的alloc_pages函数和free_pages函数加入其中。
接着,由于系统在跳入保护模式后,已经将段寄存器设置好了,所以之前实现的函数中的段寄存器设置部分可以省去。另外,在没有引入pm.inc的情况对memop.asm编译时,编译会出错:PG_U、PG_USU、PG_RWW未定义。我们直接用具体值代替对三者的引用。
; memory operation module
; import function
; import global variables
; export function
global alloc_pages
global free_pages
[SECTION .data]
BitMap:
times 1 db 0xff
times 63 db 0x0
BitMapLen equ $ - $$
StartLinearAddress dd 0x60000000
[SECTION .text]
alloc_pages:
mov ecx, eax
mov ebx, 4096
mul ebx
mov ebx, [es:StartLinearAddress]
add [es:StartLinearAddress], eax
push ebx
mov eax, ebx
mov ebx, cr3
and ebx, 0xfffff000
and eax, 0xffc00000
shr eax, 20
add ebx, eax
mov edx, ebx
mov ebx, [ebx]
test ebx, 0x00000001
jnz .pde_exist
mov ebx, cr3
mov ebx, [ebx]
and ebx, 0xfffff000
shl eax, 10
add ebx, eax
or ebx, 0x00000007
mov [edx], ebx
.pde_exist:
mov eax, [esp]
and ebx, 0xfffff000
and eax, 0x003ff000
shr eax, 10
add ebx, eax
.update_pte:
call alloc_a_4k_page
or eax, 0x00000007
mov [ebx], eax
add ebx, 4
loop .update_pte
pop ebx
ret
alloc_a_4k_page:
xor eax, eax
.search:
bts [BitMap], eax
jnc .find
inc eax
cmp eax, BitMapLen * 8
jl .search
hlt
.find:
shl eax, 12
ret
free_pages:
mov ecx, eax ; 将页的个数存入 ecx 循环计数
mov eax, ebx ; 获取传入的线性地址
mov edx, eax ;edx暂存线性地址
mov ebx, cr3
and ebx, 0xfffff000 ; 取 cr3 前 20 位
and eax, 0xffc00000 ; 取线性地址前 10 位
shr eax, 20 ; shr 22 + shl 2
add ebx, eax ; 计算 PDE 地址
mov ebx, [ebx] ; 取 PDE 值
test ebx, 0x00000001; 相与
jnz .pde_exist ; 不为 0,说明页表存在
ret
; 页表不存在,无需释放页面
.pde_exist:
mov eax, edx ; 恢复线性地址到 eax
and ebx, 0xfffff000 ; 取 PDE值 前 20 位
and eax, 0x003ff000 ; 取线性地址中间 10 位
shr eax, 10 ; shr 12 + shl 2
add ebx, eax ; 计算 PTE 地址
.update_pte:
call free_a_4k_page ; 调用该函数,用于释放一个 4KB 的页面
;更新页表项,将对应的页表项设置为无效
mov eax, 0
mov [ebx], eax ; 将页表项内容设置为0,标记为无效
add ebx, 4
loop .update_pte ; 继续循环,直到所有页面都释放完毕
ret
free_a_4k_page:
push ebx
mov ebx,[ebx]
and ebx, 0xfffff000
mov eax, edx
and eax, 0xfff
add ebx, eax ;现在ebx为物理地址
; 将传入的物理地址右移 12 位,得到页号
shr ebx, 12
; 将位图中对应的位设置为 0,表示该物理页未被占用
btr [BitMap], ebx
pop ebx
ret
然后,将alloc_pages和free_pages设置为引出函数并将memop.asm加入kernel文件夹中。同时,kernel.asm中增加了对这两个函数的引用和一段测试代码。
kernel导入函数:
测试代码:先分配两个页,再释放两个页:
调试结果:
因为我们分配了2个页,所以建立了从0x60000000到0x60001fff的映射关系(红色框标注处):其中0x60001fff对应的物理地址是0x8000,即我们使用BitMap规定的第一处空闲物理地址(0x8000-0x1fffff为空闲区域)。接着,调用free_pages函数,释放2个页。(蓝色框标注处)可见,再调用free_pages后,映射关系已恢复为调用alloc_pages前的状态了。
3. 将自己设计的中断添加进kernel
类似加入内存管理模块的步骤,将自己设计的中断单独装入一个文件并放入kernel文件夹。同时,将自定义的函数设置为导出函数,并在kernel.asm中导入。导入的中断模块代码如下:
;inserted keyboard interrupt
;import functions
;import global variables
;export functions
global change_color
change_color:
in al,60h
mov ah,0
sub al,1
je ESC
push cx
push edi
mov cx, 6
mov edi, (80 * 4 + 20) * 2 + 1
lines:
push cx
push edi
mov cx, 9
oneLine:
cmp byte [gs:edi - 1], '#'
jne next
inc byte [gs:edi]
next:
inc edi
inc edi
loop oneLine
pop edi
pop cx
add edi, 160
loop lines
pop edi
pop cx
mov al,20h
out 20h,al
ret
ESC:
mov al,20h
out 20h,al
mov al,11111111b
out 21h,al
call io_delay
ret
io_delay:
nop
nop
nop
nop
ret
该模块的功能为:每当由键盘按下时,改变屏幕上之前输出的ASCII图案颜色。按下ESC后,退出中断。
调用自定义的函数:
需要注意的是,之前实现的中断函数中,返回使用iretd。而此处change_color中使用ret返回,在change_color执行后,再使用iretd将处理器状态恢复到中断或异常发生时的状态并跳转回用户模式。
运行结果:
4.修改makefile,重新编译内核
前面导入内存管理和自定义中断函数都需要对makefile进行修改,但修改幅度较小:只需要增加编译指令,并将编译得到的文件的路径加入链接目标变量(OBJS)即可。
添加编译指令:
增加链接目标: