《操作系统真象还原》第六章 完善内核

配合视频学习效果更佳!
https://www.bilibili.com/video/BV1cN41117cq/?vd_source=701807c4f8684b13e922d0a8b116af31
https://www.bilibili.com/video/BV12X4y1h76c/?vd_source=701807c4f8684b13e922d0a8b116af31
https://www.bilibili.com/video/BV12z4y1B7WW/?vd_source=701807c4f8684b13e922d0a8b116af31

代码仓库地址:https://github.com/xukanshan/the_truth_of_operationg_system

C语言使用cdecl调用标准,如下图:

在这里插入图片描述

接下来我们实现一个简单的打印函数,不过为了开发方便,我们先要定义一些数据类型 (myos/lib/stdint.h)

#ifndef __LIB_STDINT_H
#define __LIB_STDINT_H
typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed long long int int64_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long int uint64_t;
#endif

p266剖析print.s代码: (myos/lib/kernel/print.S)

1、代码功能

写一个实现打印功能的汇编代码编译进入内核,来实现我们常见的显示字符功能

2、实现原理(核心)

A、通过对显存段操作,我们能够在屏幕上显示字符

B、通过与显卡寄存器打交道,我们可以获得光标位置,结合原理1,能够实现我们平常见的那种在光标处显示字符,然后光标向后移动的效果。光标位置需要与VAG寄存器(显卡的寄存器)中CRT Controller Registers组中索引号为0Eh与0Fh寄存器来打交道。详见p262

在这里插入图片描述

在这里插入图片描述

3、代码逻辑

判断输入字符,对不同情况作出对应处理

4、怎么写代码?(代码完整实现的思路)

A、保存调用者的执行环境

B、加载显存段选择子(要自己定义),显示字符就是对显存进行操作

C、通过与显卡寄存器打交道,获得光标位置

D、通过栈来取出传入的参数(调用者传入的字符)

E、判断D取出的字符:

  • a、回车,光标位置移置行首
  • b、换行,光标位置移置下一行行首(仿照linux系统做法\r,\n,\r\n的区别 - 荷树栋 - 博客园 (cnblogs.com)
  • c、退格,光标位置向前移动,并且显示一个空格(来实现我们期待的删除一个字符的功能)
  • d、其他,根据当前光标位置确定显存位置,然后显示后光标后移

F、E中A,B,D都有可能造成字符超出第一页显示范围,所以我们需要实现滚屏功能(采取p270方案2)

G、恢复调用者执行环境

5、代码实现如下: myos/lib/kernel/print.S

TI_GDT equ  0                                               ;从这里开始三步是在定义显存段段描述符的选择子
RPL0  equ   0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

[bits 32]                                                   ;采用32位编译
section .text                                               ;表明这是个代码段
                                                            ;------------------------   put_char   -----------------------------
                                                            ;功能描述:把栈中的1个字符写入光标所在处
                                                            ;-------------------------------------------------------------------   
global put_char                                             ;将put_char导出为全局符号,这样其他文件也可以使用
put_char:
    pushad	                                                ;备份32位寄存器环境
                                                            ;需要保证gs中为正确的视频段选择子,为保险起见,每次打印时都为gs赋值
    mov ax, SELECTOR_VIDEO	                                ; 不能直接把立即数送入段寄存器
    mov gs, ax

                                                            ;;;;;;;;;  获取当前光标位置 ;;;;;;;;;
                                                            ;先获得高8位
    mov dx, 0x03d4                                          ;索引寄存器
    mov al, 0x0e	                                        ;用于提供光标位置的高8位
    out dx, al
    mov dx, 0x03d5                                          ;通过读写数据端口0x3d5来获得或设置光标位置 
    in al, dx	                                            ;得到了光标位置的高8位
    mov ah, al

                                                            ;再获取低8位
    mov dx, 0x03d4
    mov al, 0x0f
    out dx, al
    mov dx, 0x03d5 
    in al, dx                                                  
    mov bx, ax	                                            ;现在bx中存的是光标的位置
                                                            ;下面这行是在栈中获取待打印的字符
    mov ecx, [esp + 36]	                                    ;pushad压入4×8=32字节,加上主调函数的返回地址4字节,故esp+36字节,现在ecx中是要打印的字符
    cmp cl, 0xd				                                ;判断是否是CR(回车)0x0d
    jz .is_carriage_return
    cmp cl, 0xa                                             ;判断是否是LF(换行)0x0a
    jz .is_line_feed

    cmp cl, 0x8				                                ;判断是否是BS(backspace退格)的asc码8
    jz .is_backspace
    jmp .put_other	   

.is_backspace:		      
                                                            ;;;;;;;;;;;;       backspace的一点说明	     ;;;;;;;;;;
                                                            ; 当为backspace时,本质上只要将光标移向前一个显存位置即可.后面再输入的字符自然会覆盖此处的字符
                                                            ; 但有可能在键入backspace后并不再键入新的字符,这时在光标已经向前移动到待删除的字符位置,但字符还在原处,
                                                            ; 这就显得好怪异,所以此处添加了空格或空字符0
    dec bx                                                  ;光标位置-1, 以符合我们的常识认知, 即按下退格符, 光标回退
    shl bx,1                                                ;光标的位置就转换成了对应字符的显存位置的偏移
    mov byte [gs:bx], 0x20		                            ;将待删除的字节补为0或空格皆可, 0x20是空格符的ascii码值 
    inc bx                                                  ;bx+1, 指向这个字符的属性位置, 也就是设定背景色, 字符颜色
    mov byte [gs:bx], 0x07                                  ;0x07, 就是黑底白字
    shr bx,1                                                ;bx虽然指向这个字符的颜色属性字节,但是除以2还是变回这个字符的光标位置
    jmp .set_cursor                                         ;去设置光标位置, 这样光标位置才能真正在视觉上更新

 .put_other:
    shl bx, 1				                                ; 光标位置是用2字节表示,将光标值乘2,表示对应显存中的偏移字节
    mov [gs:bx], cl			                                ; ascii字符本身
    inc bx
    mov byte [gs:bx],0x07		                            ; 字符属性
    shr bx, 1				                                ; 恢复老的光标值
    inc bx				                                    ; 下一个光标值
    cmp bx, 2000		   
    jl .set_cursor			                                ; 若光标值小于2000,表示未写到显存的最后,则去设置新的光标值
					                                        ; 若超出屏幕字符数大小(2000)则换行处理
 .is_line_feed:				                                ; 是换行符LF(\n)
 .is_carriage_return:			                            ; 是回车符CR(\r)
					                                        ; 如果是CR(\r),只要把光标移到行首就行了。
    xor dx, dx				                                ;要进行16位除法,高16位置会放在dx中,要先清零
    mov ax, bx				                                ;ax是被除数的低16位.
    mov si, 80				                                ;用si寄存器来存储除数80 由于是效仿linux,linux中\n便表示下一行的行首,所以本系统中,
    div si				                                    ; 把\n和\r都处理为linux中\n的意思,也就是下一行的行首。ax/80后,ax中存商,dx中存储的是余数,汇编除法https://blog.csdn.net/loovejava/article/details/7044242
    sub bx, dx				                                ; 光标值减去除80的余数便是取整
					                                        ; 以上4行处理\r的代码

 .is_carriage_return_end:		                            ; 回车符CR处理结束
    add bx, 80
    cmp bx, 2000
 .is_line_feed_end:			                                ; 若是LF(\n),将光标移+80便可。  
    jl .set_cursor

                                                            ;屏幕行范围是0~24,滚屏的原理是将屏幕的1~24行搬运到0~23行,再将第24行用空格填充
.roll_screen:				                                ; 若超出屏幕大小,开始滚屏
    cld                                                     
    mov ecx, 960				                            ; 一共有2000-80=1920个字符要搬运,共1920*2=3840字节.一次搬4字节,共3840/4=960次 
    mov esi, 0xb80a0			                            ; 第1行行首
    mov edi, 0xb8000			                            ; 第0行行首
    rep movsd				                                ;rep movs word ptr es:[edi], word ptr ds:[esi] 简写为: rep movsw

                                                            ;将最后一行填充为空白
    mov ebx, 3840			                                ; 最后一行首字符的第一个字节偏移= 1920 * 2
    mov ecx, 80				                                ;一行是80字符(160字节),每次清空1字符(2字节),一行需要移动80次
 .cls:
    mov word [gs:ebx], 0x0720		                        ;0x0720是黑底白字的空格键
    add ebx, 2
    loop .cls 
    mov bx,1920				                                ;将光标值重置为1920,最后一行的首字符.

.set_cursor:   
					                                        ;将光标设为bx值
                                                            ;;;;;;; 1 先设置高8位 ;;;;;;;;
    mov dx, 0x03d4			                                ;索引寄存器
    mov al, 0x0e				                            ;用于提供光标位置的高8位
    out dx, al
    mov dx, 0x03d5			                                ;通过读写数据端口0x3d5来获得或设置光标位置 
    mov al, bh
    out dx, al

                                                            ;;;;;;; 2 再设置低8位 ;;;;;;;;;
    mov dx, 0x03d4
    mov al, 0x0f
    out dx, al
    mov dx, 0x03d5 
    mov al, bl
    out dx, al
.put_char_done: 
    popad
    ret

6、其他代码详解查看书p267

为了方便其他函数调用我们写的print,所以我们为其建立一个头文件 myos/lib/kernel/print.h

#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"     //我们的stdint.h中定义了数据类型,包含进来
void put_char(uint8_t char_asci);      //在stdint.h中uint8_t得到了定义,就是unsigned char
#endif

接下来验证我们的打印函数是否能正常工作,编写一个内核文件 myos/kernel/main.c

#include "print.h"
void main(void)
{
    
    
    put_char('k');
    put_char('e');
    put_char('r');
    put_char('n');
    put_char('e');
    put_char('l');
    put_char('\n');
    put_char('1');
    put_char('2');
    put_char('\b');
    put_char('3');
    while(1);
    
}

编译print.s nasm -o /home/rlk/Desktop/print.o -f elf /home/rlk/Desktop/the_truth_of_operationg_system/chapter_6/a/lib/kernel/print.S

-f参数是指定编译成为elf文件格式

编译main.c gcc-4.4 -o /home/rlk/Desktop/main.o -c -m32 -I /home/rlk/Desktop/the_truth_of_operationg_system/chapter_6/a/lib/kernel/ /home/rlk/Desktop/the_truth_of_operationg_system/chapter_6/a/kernel/main.c

-I(大写的i)参数就是用来指定程序要链接的库,-c编译但是不链接

链接main.o与print.o ld -o /home/rlk/Desktop/kernel.bin -m elf_i386 -Ttext 0xc0001500 -e main /home/rlk/Desktop/main.o /home/rlk/Desktop/print.o

写入kernel.bin dd if=/home/rlk/Desktop/kernel.bin of=/home/rlk/Desktop/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9

接下来我们编写打印字符串的函数

p276与p277剖析代码:

1、代码功能

打印字符串

2、实现原理

C语言我们定义字符串时,会为字符串最后加上ascii 为0的字符表示字符串的结尾,我们可以通过传入字符串首地址,然后通过字符串首地址取出字符,不断判断这是不是ascii码为0的字符来判断是不是结尾,如果不是就打印字符

3、代码逻辑

打印字符串中每个字符,到结尾就结束

4、怎么写代码?(1、在print.h中加上我们的函数申明,这样写包含头文件就能引用我们写的函数;2、在print.S中加入我们打印字符串的代码)

A、保存执行环境

B、从栈中取出调用者传入的字符串首字符地址

C、判断B取出的首地址,取出字符串字符,判断是不是ascii码为0的字符,如果不是就调用之前写好的打印字符的函数,如果是就结束

D、恢复执行环境

5、代码实现如下:

print.s中加入如下代码: myos/lib/kernel/print.S

[bits 32]
section .text
                                                            ;--------------------------------------------
                                                            ;put_str 通过put_char来打印以0字符结尾的字符串
                                                            ;--------------------------------------------
                                                            ;输入:栈中参数为打印的字符串
                                                            ;输出:无

global put_str
put_str:
                                                            ;由于本函数中只用到了ebx和ecx,只备份这两个寄存器
   push ebx
   push ecx
   xor ecx, ecx		                                        ; 准备用ecx存储参数,清空
   mov ebx, [esp + 12]	                                    ; 从栈中得到待打印的字符串地址 
.goon:
   mov cl, [ebx]                                            ;ebx是字符串的地址,对地址进行取地址操作,然后取出一字节的数据,就是取出了字符串的第一个字符
   cmp cl, 0		                                        ; 如果处理到了字符串尾,跳到结束处返回
   jz .str_over
   push ecx		                                            ; 为put_char函数传递参数
   call put_char
   add esp, 4		                                        ; 回收参数所占的栈空间
   inc ebx		                                            ; 使ebx指向下一个字符
   jmp .goon
.str_over:
   pop ecx
   pop ebx
   ret

6、其他代码详解查看书p276

print.h中加入如下代码: myos/lib/kernel/print.h

void put_str(char* messags);

写一个新的内核来验证一下: myos/kernel/main.c

#include "print.h"
void main(void) {
    
    
   put_str("I am kernel\n");
   while(1);
}

分别编译main.c,pirnt.s,然后链接,最后写入,运行成功!

接下来我们来实现打印整数的功能,比如对于值0x00123,那么屏幕上就打印123(十六进制含义)

p277剖析代码:

1、代码功能

将一个值转换成字符显示出来

2、实现原理

数值的9需要转换成字符9对应的ASCII码值,才能用于显示。用待转换的数值减去各自的起始数字(如0或10)获得其对应字符相对于0字符或A字符的偏移量,再用此偏移量加上对应字符所在类别的起始字符ASCII码(如0或A的ascii码值),就是该数字对应的字符的ascii码值(实例见p279)

3、代码逻辑

将一个32位的值,从最低处开始,按照4位一组处理(因为每4位对应16进制的一位)成对应字符,倒着放入缓冲区。处理好前缀之后(出现连续的0需要跳过),一个一个打印出来

4、怎么写代码?

A、定义一个8字节的缓冲区,8字节是因为一个完整的32位值每次取四位转换成一个字符(1字节),需要8次。

B、保存执行现场

C、循环取32位值每4位,从最低4位开始取,转换成对应字符的ASCII码值,然后倒着存在缓冲区中

D、对于C的结果,不显示转换后从高位开始连续的字符0,如00123,显示123;如果是全0,则需要只显示一个0

E、恢复执行现场

5、代码实现如下:

print.s加入如下代码 myos/lib/kernel/print.S

section .data
put_int_buffer dq 0                                         ; 定义8字节缓冲区用于数字到字符的转换

global put_int
put_int:
   pushad
   mov ebp, esp
   mov eax, [ebp+4*9]		                                ; call的返回地址占4字节+pushad的84字节,现在eax中就是要显示的32位数值
   mov edx, eax                                             ;edx中现在是要显示的32位数值
   mov edi, 7                                               ; 指定在put_int_buffer中初始的偏移量,也就是把栈中第一个字节取出放入buffer最后一个位置,第二个字节放入buff倒数第二个位置
   mov ecx, 8			                                    ; 32位数字中,16进制数字的位数是8个
   mov ebx, put_int_buffer                                  ;ebx现在存储的是buffer的起始地址

                                                            ;32位数字按照16进制的形式从低位到高位逐个处理,共处理816进制数字
.16based_4bits:			                                    ;4位二进制是16进制数字的1,遍历每一位16进制数字
   and edx, 0x0000000F		                                ; 解析16进制数字的每一位。and与操作后,edx只有低4位有效
   cmp edx, 9			                                    ; 数字09和a~f需要分别处理成对应的字符
   jg .is_A2F 
   add edx, '0'			                                    ; ascii码是8位大小。add求和操作后,edx低8位有效。
   jmp .store
.is_A2F:
   sub edx, 10			                                    ; A~F 减去10 所得到的差,再加上字符A的ascii码,便是A~F对应的ascii码
   add edx, 'A'

                                                            ;将每一位数字转换成对应的字符后,按照类似“大端”的顺序存储到缓冲区put_int_buffer
                                                            ;高位字符放在低地址,低位字符要放在高地址,这样和大端字节序类似,只不过咱们这里是字符序.
.store:
   mov [ebx+edi], dl		                                ; 此时dl中是数字对应的字符的ascii码
   dec edi                                                  ;edi是表示在buffer中存储的偏移,现在向前移动
   shr eax, 4                                               ;eax中是完整存储了这个32位数值,现在右移4位,处理下一个4位二进制表示的16进制数字
   mov edx, eax                                             ;把eax中的值送入edx,让ebx去处理
   loop .16based_4bits

                                                            ;现在put_int_buffer中已全是字符,打印之前,
                                                            ;把高位连续的字符去掉,比如把字符00000123变成123
.ready_to_print:
   inc edi			                                        ; 此时edi退减为-1(0xffffffff),1使其为0
.skip_prefix_0:                                             ;跳过前缀的连续多个0
   cmp edi,8			                                    ; 若已经比较第9个字符了,表示待打印的字符串为全0 
   je .full0 
                                                            ;找出连续的0字符, edi做为非0的最高位字符的偏移
.go_on_skip:   
   mov cl, [put_int_buffer+edi]
   inc edi
   cmp cl, '0' 
   je .skip_prefix_0		                                ; 继续判断下一位字符是否为字符0(不是数字0)
   dec edi			                                        ;edi在上面的inc操作中指向了下一个字符,若当前字符不为'0',要恢复edi指向当前字符		       
   jmp .put_each_num

.full0:
   mov cl,'0'			                                    ; 输入的数字为全0时,则只打印0
.put_each_num:
   push ecx			                                        ; 此时cl中为可打印的字符
   call put_char
   add esp, 4
   inc edi			                                        ; 使edi指向下一个字符
   mov cl, [put_int_buffer+edi]	                            ; 获取下一个字符到cl寄存器
   cmp edi,8                                                ;当edi=8时,虽然不会去打印,但是实际上已经越界访问缓冲区了
   jl .put_each_num
   popad
   ret

6、其他代码详解查看书p278

print.h中加入函数申明 myos/lib/kernel/print.h

void put_int(uint32_t num);	        // 以16进制打印

编写main.c代码来检验 myos/kernel/main.c

#include "print.h"
void main(void) {
    
    
   put_str("I am kernel\n");
   put_int(0);
   put_char('\n');
   put_int(9);
   put_char('\n');
   put_int(0x00021a3f);
   put_char('\n');
   put_int(0x12345678);
   put_char('\n');
   put_int(0x00000000);
   while(1);
}

他代码详解查看书p278**

print.h中加入函数申明 myos/lib/kernel/print.h

void put_int(uint32_t num);	        // 以16进制打印

编写main.c代码来检验 myos/kernel/main.c

#include "print.h"
void main(void) {
    
    
   put_str("I am kernel\n");
   put_int(0);
   put_char('\n');
   put_int(9);
   put_char('\n');
   put_int(0x00021a3f);
   put_char('\n');
   put_int(0x12345678);
   put_char('\n');
   put_int(0x00000000);
   while(1);
}

猜你喜欢

转载自blog.csdn.net/kanshanxd/article/details/130913203