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