内核中的打印

简介: 内核中的打印

引言

  • 我们在进行后续的内核开发过程中,肯定需要在屏幕上打印内容。所以,本章节中,我们就把打印相关的准备工作做好

准备

  • 首先在工程根目录下创建文件夹 “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)

使用步骤:

  1. #include <stdarg.h> // 包含头文件
  2. va_list ap; // 定义一个 va_list 类型的变量
  3. va_start(ap, v); // 初始化 ap 指针,使其指向第一个可变参数。v 是变参列表的前一个参数
  4. va_arg(ap, type); // 返回当前变参值,并使 ap 指向列表中的下个变参, type是程序员需要显性的告诉编译器当前变量的类型
  5. 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
目录
相关文章
|
15天前
|
Linux C++
【代码片段】Linux C++打印当前函数调用堆栈
【代码片段】Linux C++打印当前函数调用堆栈
14 0
|
10月前
如何定位strace中系统调用在内核中的位置
要了解内核函数的含义,最好的方法,就是去查询所用内核版本的源代码。
24 0
|
5月前
|
Linux
linux 系统调用打印功能
linux 系统调用打印功能
26 0
|
存储 Linux
Linux内核18-中断和异常的嵌套处理
Linux内核18-中断和异常的嵌套处理
Linux内核18-中断和异常的嵌套处理
|
Linux
Linux驱动开发 驱动程序的具体编写及出口入口函数解析,printk打印内核信息
Linux驱动开发 驱动程序的具体编写及出口入口函数解析,printk打印内核信息
189 0
|
Linux C语言 Shell
【Linux修炼】11.进程的创建、终止、等待、程序替换(二)
【Linux修炼】11.进程的创建、终止、等待、程序替换(二)
【Linux修炼】11.进程的创建、终止、等待、程序替换(二)
|
Linux 调度 C++
【Linux修炼】11.进程的创建、终止、等待、程序替换(一)
【Linux修炼】11.进程的创建、终止、等待、程序替换(一)
【Linux修炼】11.进程的创建、终止、等待、程序替换(一)
|
Linux
内核笔记](四)——内核常见调试手段(printf、dump_stack、devmem)
内核笔记](四)——内核常见调试手段(printf、dump_stack、devmem)
184 0
内核笔记](四)——内核常见调试手段(printf、dump_stack、devmem)

热门文章

最新文章