文章目录
- 前言
- 一、如何设计我们的打印函数?
- 二、实践检验!
查看系列文章点这里: 操作系统真象还原
前言
现在接力棒意见交到内核手中啦,只不过我们的内核现在可谓是一穷二白啥都没有,为了让我们设计的内核能被看见被使用,那首先就得有在屏幕输出信息的能力。因此,我们就先来实现打印函数,它的功能就是在屏幕上输出字符信息。
一、如何设计我们的打印函数?
首先,我们知道在屏幕上输出信息,其实都是操控显卡。我们在前面的步骤当中,都是通过直接操控显存来往屏幕上输出信息的,虽然这个方法简单,但是不用我说,大家也能知道这个方法局限性很大。
所以,我们需要实现一个函数,使其满足我们自己设计的操作系统的需要。也就是要实现字符串、数字以及基本的控制字符(回车。换行、退格)的输,并且要在一行输出满了的情况下,自己换行输出。
具体怎么实现呢?显卡除了显存,还有端口,也就是显卡用的寄存器。我们需要通过端口来控制显卡的一些行为(设置光标位置等等)或者获取一些信息(光标位置等等),进而将我们要输出的内容卸载正确的显存位置。是不是听起来很简单,就是换了种方式写显存而已,没什么难的。
在实现的思路上,我们先实现单个字符的输出,然后再实现字符串和数字的输出,相信大家能理解为什么这样做,接下来我们就来看看具体怎么实现。
二、实践检验!
现在我们的 code 目录下新建 lib 文件夹,存储我们自己实现的函数,在 lib 下在建 kernel 文件夹,存储内核将来会用到的函数,我们的打印函数就放在其中。
在开始之前,我们先给C语言中的数据类型起个别名,方便我们能清楚知道我们定义的变量是多少位的,是有符号还是无符号的。在 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 uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long int uint64_t;
#endif
print.h
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"
void put_char(uint8_t char_in_ascii);
void put_str(char* message);
void put_int(uint32_t num);
#endif
print.S
[bits 32]
section .data
put_int_buffer dq 0
section .text
;定义视频段的选择子
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
; ============================================================
; put_char: 从光标处打印堆栈中的字符
; ============================================================
global put_char
put_char:
; ------------------------
; 备份32位寄存器(共八个)
; ------------------------
pushad
; ------------------------
; 为gs安装正确的选择子
; ------------------------
mov ax, SELECTOR_VIDEO
mov gs, ax
; -------------------------------
; 从显卡寄存器中获取光标位置(16位)
; -------------------------------
; 高8位
mov dx, 0x03d4 ;指定索引寄存器
mov al, 0x0e ;指定子功能:获取光标高8位
out dx, al
mov dx, 0x03d5 ;指定读写数据寄存器(端口)
in al, dx
mov ah, al
; 低8位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
in al, dx
; 寄存器 bx 存储着光标的(线性)坐标
mov bx, ax
; -------------------------------------------------------
; 检索要打印的字符,32字节的寄存器空间 + 4字节的调用者返回地址
; -------------------------------------------------------
mov ecx, [esp+36]
; -------------------------------------------------------
; 处理要打印的字符
; 1.对控制字符进行特殊处理,并打印普通可见字符
; 2.如果可见字符超出屏幕(cmp bx, 2000),则添加回车处理操作
; -------------------------------------------------------
cmp cl, 0xd
jz .is_carriage_return ; 回车符
cmp cl, 0xa
jz .is_line_feed ; 换行符
cmp cl, 0x8
jz .is_backspace ; 退格键
jmp .put_other ; 其它字符
; ------------------------
; 处理退格键
; ------------------------
.is_backspace:
; 光标坐标减1,相当于光标向左移动
dec bx
; 光标是字符的坐标,而一个字符占据 2 字节,所以通过光标向视频内存写入数据时,光标需要乘以 2
shl bx, 1
mov byte [gs:bx], 0x20 ; 指定字符:空格->覆盖原有字符实现擦除
inc bx ; 加一指向设置属性的地址
mov byte [gs:bx], 0x07 ; 指定属性:黑屏白字
; 恢复 bx 值,使其重新为光标位置,而不是光标的内存地址
shr bx, 1
jmp .set_cursor ;处理光标的位置
; ------------------------
; 处理可见字符
; ------------------------
.put_other:
shl bx, 1
mov [gs:bx], cl
inc bx
mov byte [gs:bx], 0x07
shr bx, 1
inc bx ; 将光标指向下一个待打印的位置
cmp bx, 2000
jl .set_cursor ; 若光标值小于2000表示还可以显示,否则执行换行处理
; -----------------------------------
; 处理回车符和换行符(统一看做回车换行符)
; -----------------------------------
; "\n" --- 将光标移动到下一行的开头
.is_line_feed:
; "\r" --- 将光标移动到同一行的开头
.is_carriage_return:
; 16位除法,求模80的结果
xor dx, dx
mov ax, bx
mov si, 80
div si
; 减去余数,即回到行首
sub bx, dx
; 加80,即到了下一行
add bx, 80
; 如果光标超出了屏幕范围(即指令jl的结果为假),则滚动屏幕
cmp bx, 2000
jl .set_cursor
; ------------------------
; 滚屏
; ------------------------
.roll_screen:
; 将第 1 行到第 24 行的内容覆盖到第 0 行到第 23 行
cld ; 将eflags寄存器中方向标志位DF清0
mov ecx, 960 ; ((2000-80)*2)/4=960
mov esi, 0xc00b80a0 ; 第 0 行开始
mov edi, 0xc00b8000 ; 第 1 行开始
rep movsd ; 每次复制 4 字节
; 清除当前屏幕的最后一行,填充为白空格(0x0720)
mov ebx, 3840 ; 1920*2 = 3840
mov ecx, 80
.cls:
mov word [gs:ebx], 0x0720
add ebx, 2
loop .cls
; 更新光标位置信息->指向最后一行的开头
mov bx, 1920
; ------------------------
; 更新图形卡中的光标位置信息
; ------------------------
.set_cursor:
; 设置高 8 位
mov dx, 0x03d4
mov al, 0x0e
out dx, al
mov dx, 0x03d5
mov al, bh
out dx, al
; 设置低 8 位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
.put_char_end:
popad ; 恢复之前压入栈的 8 个寄存器
ret ; 执行完函数流程,返回
; ============================================================
; put_str: 通过 put_char 来打印以 0 字符结尾的字符串
; ============================================================
global put_str
put_str:
; -----------------------------------
; 备份寄存器,准备参数(字符串的起始地址)
; -----------------------------------
push ebx
push ecx
xor ecx, ecx ; 清空
mov ebx, [esp+12] ; 备份寄存器的8个字节 + 调用者返回地址的4个字节
; 通过调用 put_char 实现该函数
.goon:
mov cl, [ebx]
cmp cl, 0
jz .str_over ; 判断是不是到了结尾
push ecx
call put_char
add esp, 4
inc ebx
loop .goon
.str_over:
pop ecx
pop ebx
ret
; ====================================================================
; put_int: 打印栈中的数字(put_int_buffer 用作缓冲区,用于存储转换后的结果)
; ====================================================================
global put_int
put_int:
pushad
mov ebp, esp ; 获取esp的值,通过esp来访问栈
mov eax, [ebp+36] ; 32字节的寄存器 + 4字节的调用者返回地址
mov edx, eax
mov edi, 7 ; 指定 put_int_buffer 中初始的偏移量
mov ecx, 8 ; 待计算的位数(32/4=8)
mov ebx, put_int_buffer ; EBX代表缓冲区的基地址
; ------------------------------------------
; 将字符(32位数中的每4位)转换为相应的ASCII值
; ------------------------------------------
.16based_4bits:
and edx, 0x0000000F
cmp edx, 9
jg .is_A2F
add edx, '0'
jmp .store
.is_A2F:
sub edx, 10
add edx, 'A'
.store:
mov [ebx+edi], dl
dec edi
shr eax, 4
mov edx, eax
loop .16based_4bits
; ------------------------
; 去掉多余的 0
; ------------------------
.ready_to_print:
inc edi ; 使 edi 重新指向最高位
.skip_prefix_0:
; 如果所有位都是 0,做特殊处理
cmp edi, 8
je .full0
.detect_prefix_0:
mov cl, [put_int_buffer+edi]
inc edi
cmp cl, '0'
je .skip_prefix_0
dec edi
jmp .put_each_num
.full0:
mov cl, '0'
.put_each_num:
push ecx
call put_char
add esp, 4
inc edi
mov cl, [put_int_buffer+edi]
cmp edi, 8
jl .put_each_num
popad
ret
main.c
#include "print.h"
int main(void){
while(1){
put_str("I am kernel!");
put_char('\n');
put_int(0x66666);
put_char('\n');
}
return 0;
}
结果如下所示:
持续更新中!