引言
- 我们在进行后续的内核开发过程中,肯定需要在屏幕上打印内容。所以,本章节中,我们就把打印相关的准备工作做好
准备
- 首先在工程根目录下创建文件夹 “core”,该文件夹中将放入操作系统核心必备组件,创建 “print.c” 和 “print.h” 两个文件,将 “print.c” 文件放到刚创建的 “core” 文件夹下,将 “print.h” 放到工程根目录下的 “include” 文件夹下。
- 修改最外层 BUILD.json,在 “dir” 中添加 “core”
- 在 “core” 文件夹下创建一个 “BUILD.json” 文件,其内容如下:
{ "dir" : [ ], "src" : [ "print.c" ], "inc" : [ "include" ] }
- 基础架构已经搭建完成,接下来只要在 “print.c” 中实现相关的打印函数,在 “print.h” 中实现相关打印函数的函数声明就可以了
设置光标位置
- 函数名称: E_RET SetCursorPos(U16 x, U16 y)
- 输入参数: U16 x --横坐标;U16 y --纵坐标
- 输出参数: 无
- 函数返回: E_OK:成功; E_ERR:失败
- 以前我们也设置过光标位置,但那是借助 BIOS 实现的,开启保护模式进入内核后,不能再使用 BIOS 提供的功能了,不过还有另外一种方式,那就是通过端口对显卡进行操作以达到目的
- 对于显卡我们暂时没必要深入研究,我们可以像 BIOS 使用那样,知道怎么写代码然后就能实现什么样的效果就可以了,没必要深入研究为什么
- 下面就是光标设置汇编代码
; 光标位置:(x, y) ; 光标位置须按照 bx = (80 * y + x) 公式转换一下放入 bx 寄存器中,比如设置光标位置 (0, 2): mov bx, (80 * 2 + 0) ; 设置光标高 8 位:先把数据 0x0E 写入端口 0x03D4,然后把 bh 写入端口 0x03D5 mov dx, 0x03D4 mov al, 0x0E out dx, al mov dx, 0x03D5 mov al, bh out dx, al ; 设置光标低 8 位:先把数据 0x0F 写入端口 0x03D4,然后把 bl 写入端口 0x03D5 mov dx, 0x03D4 mov al, 0x0F out dx, al mov dx, 0x03D5 mov al, bl out dx, al
- 可以把这段代码放到 loader.asm 中验证一下看看效果。但是,这种汇编方式是 nasm 方式,我们需要把这段代码内嵌到 C 语言中就必须把它转换成 AT&T 格式的,转换后的代码如下:
// 光标位置:(x, y) // 光标位置须按照 bx = (SCREEN_WIDTH * y + x) 公式转换一下放入 bx 寄存器中 U16 bx_c = SCREEN_WIDTH * y + x; asm volatile( // 设置光标高 8 位:先把数据 0x0E 写入端口 0x03D4,然后把 bh 写入端口 0x03D5 "movw %0, %%bx\n" "movw $0x03D4, %%dx\n" "movb $0x0E, %%al\n" "outb %%al, %%dx\n" "movw $0x03D5, %%dx\n" "movb %%bh, %%al\n" "outb %%al, %%dx\n" // 设置光标低 8 位:先把数据 0x0F 写入端口 0x03D4,然后把 bl 写入端口 0x03D5 "movw $0x03D4, %%dx\n" "movb $0x0F, %%al\n" "outb %%al, %%dx\n" "movw $0x03D5, %%dx\n" "movb %%bl, %%al\n" "outb %%al, %%dx\n" : : "b"(bx_c) // 相当于:bx = bx_c,等号左边是汇编中的寄存器 bx,等号右边是 C 代码中的变量 : "ax", "dx" // 告诉 gcc 编译器,ax,dx 寄存器被内嵌汇编使用,需要 gcc 自动添加保护和恢复操作(入栈和出栈) // 明明 ax, bx, dx 三个寄存器都被使用了,为什么这里却只有 ax 和 dx 寄存器? // "b"(bx_c) 中使用约束名 "b" 时 gcc 就已经自动对 bx 进行保护和恢复操作,所以这里就不需要重复了 );
- "b"(bx_c) :相当于:bx = bx_c,等号左边是汇编中的寄存器 bx,等号右边是 C 语言中的变量
- 完整实现:
void SetFontColor(E_FONT_COLOR color) { FontColor = color; }
E_RET SetCursorPos(U16 x, U16 y) { U16 bx_c = 0; // 对传入的坐标进行合法性检查 if(x >= SCREEN_WIDTH || y >= SCREEN_WIDTH) return E_ERR; CursorPosX = x; CursorPosY = y; // 光标位置须按照 bx = (80 * y + x) 公式转换一下放入 bx 寄存器中 bx_c = 80 * y + x; asm volatile( // 设置光标高 8 位:先把数据 0x0E 写入端口 0x03D4,然后把 bh 写入端口 0x03D5 "movw %0, %%bx\n" "movw $0x03D4, %%dx\n" "movb $0x0E, %%al\n" "outb %%al, %%dx\n" "movw $0x03D5, %%dx\n" "movb %%bh, %%al\n" "outb %%al, %%dx\n" // 设置光标低 8 位:先把数据 0x0F 写入端口 0x03D4,然后把 bl 写入端口 0x03D5 "movw $0x03D4, %%dx\n" "movb $0x0F, %%al\n" "outb %%al, %%dx\n" "movw $0x03D5, %%dx\n" "movb %%bl, %%al\n" "outb %%al, %%dx\n" : : "b"(bx_c) // 相当于:bx = bx_c,等号左边是汇编中的寄存器 bx,等号右边是 C 代码中的变量 : "ax", "dx" // 告诉 gcc 编译器,ax,dx 寄存器被内嵌汇编使用,需要 gcc 自动添加保护和恢复操作(入栈和出栈) // 明明 ax, bx, dx 三个寄存器都被使用了,为什么这里却只有 ax 和 dx 寄存器? // "b"(bx_c) 中使用约束名 "b" 时 gcc 就已经自动对 bx 进行保护和恢复操作,所以这里就不需要重复了 ); return E_OK; }
设置字体颜色
- 函数名称: void SetFontColor(E_COLOR color)
- 输入参数: E_COLOR color --字体颜色
- 输出参数: 无
- 函数返回: 无
- 由于设置字体颜色跟打印一个字符是在一起操作的,所以我们可以用一个 static 变量把颜色值记录下来 'static U08 FontColor;',等实现打印一个字符的时候再从这个变量中取值
- 函数实现:
void SetFontColor(E_FONT_COLOR color) { FontColor = color; }
打印一个字符
- 函数名称: void PrintChar(U08 ch)
- 输入参数: U08 ch --要打印的字符
- 输出参数: 无
- 函数返回: 无
- 字符打印当然使用显存方式了,既然操作显存,那么 loader.asm 的显存段描述符和段选择符加上,还有显存段寄存器 gs 要初始化赋值成显存段选择符。这些都是以前实现过的,这里不再讲解了
- 真正打印一个字符使用内嵌汇编格式
U32 edi_c = (SCREEN_WIDTH * CursorPosY + CursorPosX) * 2; U08 ah_c = FontColor; U08 al_c = ch; asm volatile( "movl %0, %%edi\n" // edi:显存中的偏移量 "movb %1, %%ah\n" // 显示属性 "movb %2, %%al\n" // 要打印的字符 "movw %%ax, %%gs:(%%edi)" // 把显示数据放入显存 : : "D"(edi_c), "r"(ah_c), "r"(al_c) // %0 替换成 edi_c,"D" 表示使用 edi 寄存器; %1 替换成 ah_c; %2 替换成 al_c; : "ax" );
- 再有就是要考虑打印完一个字符后光标位置的变化,处理特殊情况,比如一行显示满了,或者遇到换行符等情况
- 函数完整实现:
void PrintChar(U08 ch) { U32 edi_c = (80 * CursorPosY + CursorPosX) * 2; U08 ah_c = FontColor; U08 al_c = ch; // 每打印一个字符,光标都往后移动一个位置 CursorPosX++; // 如果光标达到最大横坐标值了,则设置光标到下一行起始位置 if(CursorPosX >= SCREEN_WIDTH) { CursorPosX = 0; CursorPosY++; } if('\n' == al_c || '\r' == al_c) // 遇到换行,则光标位置设置成下一行的起始位置处 { CursorPosX = 0; CursorPosY++; } else { // 打印一个字符 asm volatile( "movl %0, %%edi\n" // edi:显存中的偏移量 "movb %1, %%ah\n" // 显示属性 "movb %2, %%al\n" // 要打印的字符 "movw %%ax, %%gs:(%%edi)" // 把显示数据放入显存 : : "D"(edi_c), "r"(ah_c), "r"(al_c) // %0 替换成 edi_c,"D" 表示使用 edi 寄存器; %1 替换成 ah_c; %2 替换成 al_c; : "ax" ); } // 更新光标位置 SetCursorPos(CursorPosX, CursorPosY); }
打印字符串
- 函数名称: E_RET PrintString(U08 const * str)
- 输入参数: U08 *str --要打印的字符串
- 输出参数: 无
- 函数返回: E_OK:成功; E_ERR:失败
- 实现完成打印一个字符,接下来我们再来实现打印字符串功能
- 这个没啥好说的,把字符串中的字符一个一个打印出来就可以了,注意一下打印前判断一下传入的指针是否为空
- 判断指针为空的 NULL 是没有定义的,我们需要自己定义一下
#define NULL ((void*)0)
- 函数完整实现:
E_RET PrintString(U08 const *str) { // 检查 *str 是否为空 if(NULL == str) return E_ERR; while(*str) PrintChar(*str++); return E_OK; }
以十进制的方式打印一个整型数
- 函数名称: void PrintIntDec(S32 num)
- 输入参数: S32 num --要打印的整型数
- 输出参数: 无
- 函数返回: 无
- 先定义一个数组
staticconstU08IntToStr[10]={'0','1','2','3','4','5','6','7','8','9'};
- 一个 0 ~ 9 的数值 index ,就可以通过 IntToStr[index] 的方式转化成对应的字符
- 接下来的重点就是怎样先把十进制数值从高到低按位依次拆分出来,比如:数值 123 要拆分成 '1', '2', '3'
- 先判断要打印的整型数 num 是否为负数,如果为负数,则先把负号('-')打印出来,然后再取其绝对值 '-num'
- 我们来看一下下面的递归函数
void PrintIntDec(U32 num) { if(num < 10) // 递归出口条件 PrintChar(IntToStr[num]); // ① else { PrintIntDec(num / 10); // ② PrintIntDec(num % 10); // ③ } }
- 使用递归我们首先要考虑的就是递归出口条件,此处递归出口条件是: num < 10
- 当 num >= 10 时,则递归,每次递归都必须要像递归出口条件靠近,不然函数容易死循环无法跳出
- ③ 处调用不会再次递归深入,我们可以把它当成一个普通的执行语句,替换成 PrintChar(num)
- 从实例中体会调用递归函数 PrintIntDec(12345) 的执行顺序
- 第一次调用 PrintIntDec(12345) 后执行的语句应该是
PrintIntDec(12345 / 10); PrintIntDec(12345 % 10); // 把它当中普通语句
- 由于 12345 / 10 > 10, 所以继续深入一层,程序变为
PrintIntDec(1234 / 10); PrintIntDec(1234 % 10); // 把它当中普通语句 PrintIntDec(12345 % 10); // 把它当中普通语句
- 由于 1234 / 10 > 10, 所以继续深入一层,程序变为
PrintIntDec(123 / 10); PrintIntDec(123 % 10); // 把它当中普通语句 PrintIntDec(1234 % 10); // 把它当中普通语句 PrintIntDec(12345 % 10); // 把它当中普通语句
- 由于 123 / 10 > 10, 所以继续深入一层,程序变为
PrintIntDec(12 / 10); PrintIntDec(12 % 10); // 把它当中普通语句 PrintIntDec(123 % 10); // 把它当中普通语句 PrintIntDec(1234 % 10); // 把它当中普通语句 PrintIntDec(12345 % 10); // 把它当中普通语句
- 这次 12 / 10 = 1 < 10, 满足递归退出条件了,于是 PrintIntDec(12 / 10); 这个执行后执行语句
PrintChar(IntToStr[1]) => '1'
- 然后程序依次向下执行
- PrintIntDec(12 % 10); 相当于 PrintChar(IntToStr[2]) => '2'
- PrintIntDec(123 % 10); 相当于 PrintChar(IntToStr[3]) => '3'
- PrintIntDec(1234 % 10); 相当于 PrintChar(IntToStr[4]) => '4'
- PrintIntDec(12345 % 10); 相当于 PrintChar(IntToStr[5]) => '5'
- 程序运行结束,正好依次打印字符 '1', '2', '3', '4', '5'
- 函数完整实现:
void PrintIntDec(S32 num) { // 如果 num 为负数,则先把负号 '-' 打印出来; num = -num: 取一个负数的绝对值 if(num < 0) { PrintChar('-'); num = -num; } if(num < 10) // 递归出口条件 PrintChar(IntToStr[num]); else { PrintIntDec(num / 10); PrintIntDec(num % 10); } }
以十六进制的方式打印一个无符号整型数
- 函数名称: void PrintIntHex(U32 num)
- 输入参数: U32 num --要打印的无符号整型数
- 输出参数: 无
- 函数返回: 无
- 实现思路:把无符号整型数转成十六进制字符串,然后再调用字符串打印接口函数打印出来就可以了
- 先定义一个数组
staticconstU08IntToStr[16]={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
- 一个 0 ~ 15 的数值 index ,就可以通过 IntToStr[index] 的方式转化成对应的字符
- 对于一个整型变量 num ,我们分两种情况考虑,一种是正数,一种是负数
- 当 num 为正数时,"num & 0xF" 即可获得这个整型数 num 的最低位,例如:num = 0x1234, "num & 0xF" 即可获得数值 4,于是依次左移 4 位,就可以得到数值 3, 2, 1。用这些数值作为数组 IntToStr 的下标,就可以通过 IntToStr[index] 的方式获得与之字符 '4', '3', '2', '1' 。将这些字符放到要打印的字符串数组中,然后调用打印字符串函数将之打印出来即可,当然了,在这个字符串前面再加上 '0x' 不就能更好的看出这是 16 进制了嘛
- 函数完整实现:
void PrintIntHex(S32 num) { U08 print[12] = { 0 }; // 初始化时必须清零,最大占 12 字节,比如 "-0x12345678\0" U08 i = 0; U08 tmp = 0; if(num >= 0) // 正数 { print[0] = '0'; print[1] = 'x'; for(i = 9; i >= 2; i--) { tmp = num & 0xF; num = num >> 4; print[i] = IntToStr[tmp]; } } else // 负数 { print[0] = '-'; print[1] = '0'; print[2] = 'x'; num = -num; // 将负数取绝对值(去掉负号) for(i = 10; i >= 3; i--) { tmp = num & 0xF; num = num >> 4; print[i] = IntToStr[tmp]; } } PrintString(print); }
清空屏幕
- 函数名称: void ClearScreen(void)
- 输入参数: 无
- 输出参数: 无
- 函数返回: 无
- 这个实现起来简单,只要把整个屏幕都打印成空格字符 (' ') 不就可以了,这里就不再具体说明了
- 函数完整实现:
void ClearScreen(void) { U32 i = 0; SetCursorPos(0, 0); for(i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; i++) PrintChar(' '); SetCursorPos(0, 0); }
通用打印
- 函数名称: E_RET printk(const char * fmt, ...)
- 输入参数: const char * fmt --输出格式; ... --可变参数
- 输出参数: 无
- 函数返回: E_OK:成功; E_ERR:失败
- 前面我们已经分别实现了几种打印函数,但是不同的打印就需要调用不同的接口函数,有点麻烦,现在我们实现一个通用的打印函数 printk,我们把它实现成可变参数函数。
什么是可变参数函数
- 在C语言编程中有时会遇到一些参数可变的函数,例如printf()、scanf(),其函数原型为:
intprintf(constchar*format,...)
intscanf(constchar*format,...)
- 就拿 printf 来说吧,它除了有一个参数 format 固定以外,后面的参数其个数和类型都是可变的,用三个点“...”作为参数占位符。
参数列表的构成
- 任何一个可变参数的函数都可以分为两部分:固定参数和可选参数。至少要有一个固定参数,其声明与普通函数参数声明相同;可选参数由于数目不定(0个或以上),声明时用"..."表示。固定参数和可选参数共同构成可变参数函数的参数列表。
头文件:#include <stdarg.h>
- 该头文件包含一个类型(va_list)和三个宏(va_start, va_arg 和 va_end)
使用步骤:
- #include <stdarg.h> // 包含头文件
- va_list ap; // 定义一个 va_list 类型的变量
- va_start(ap, v); // 初始化 ap 指针,使其指向第一个可变参数。v 是变参列表的前一个参数
- va_arg(ap, type); // 返回当前变参值,并使 ap 指向列表中的下个变参, type是程序员需要显性的告诉编译器当前变量的类型
- va_end(ap); // 将指针 ap 置为无效,结束变参的获取
注意事项
- 变参宏无法智能识别可变参数的数目和类型,因此实现变参函数时需自行判断可变参数的数目和类型,这个需要自己想办法,比如显式提供参数个数或设定遍历结束条件,主调函数和被调函数约定好变参的数目和类型等
- va_arg(ap, type)宏中的 type 不可指定为以下类型:char short float
示例
- 代码:
#include <stdio.h> #include <stdarg.h> void test_func(int arg1, ...) { va_list va; va_start(va, arg1); printf("arg1 = %d\n", arg1); printf("arg2 = %d\n", va_arg(va, int)); printf("arg3 = %d\n", va_arg(va, int)); printf("arg4 = %d\n", va_arg(va, int)); va_end(va); } int main(void) { test_func(1, 2, 3, 4); return 0; }
- 运行结果:
arg1 = 1 arg2 = 2 arg3 = 3 arg4 = 4
- 由于我们实现的是一个操作系统,所以不可能包含库中 stdarg.h 头文件,一切都要自己实现。于是先创建一个 "stdarg.h" 头文件,放入 KOS 工程 "include" 目录下,其内容包括如下定义:
typedef U08 * va_list; #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(U32) - 1) & ~(sizeof(U32) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) #define va_end(ap) ( ap = (va_list)0 )
- 了解了C语言可变参数函数后,通用打印函数的主体实现就很简单了,通过遍历字符串 fmt,寻找 '%', '%' 之前的部分直接通过字符打印函数打印出来,找到 '%' 后,再根据 '%' 后面的第一个字符判断打印类型,从而调用上面我们已经实现好的各种类型的打印函数
- 函数实现框架如下
E_RET printk(const char * fmt, ...) { ... // 遍历字符串 fmt for(; *fmt; ++fmt) { // 寻找格式转换字符 '%', 如果不是 '%', 则直接打印出来 if(*fmt != '%') { PrintChar(*fmt); continue; } // 如果找到了 '%', 则取 '%' 后的第一个字符 ++fmt; switch (*fmt) // 根据 '%' 后的字符类型采取不同的打印接口 { // 分别调用对应的打印接口 case 'c': // 字符 case 's': // 字符串 case 'x': // 十六进制 case 'd': // 十进制 } } ... }
- 以上接口函数具体实现代码见: print.c