操作系统实现-进入内核

145 阅读6分钟

博客网址:www.shicoder.top
微信:18223081347
欢迎加群聊天 :452380935

这一次我们正式进入内核,编写相关的内核代码,也就是kernel代码

数据类型定义

因为我们在内核中会使用一些数据,因此先提前定义一些数据类型

 #define EOF -1 
 ​
 #define NULL ((void *)0) // 空指针
 #define EOS '\0' // 字符串结尾
 ​
 #define bool _Bool
 #define true 1
 #define false 0
 ​
 #define _packed __attribute__((packed)) // 用于定义特殊的结构体 不对齐
 ​
 typedef unsigned int size_t;
 typedef char int8;
 typedef short int16;
 typedef int int32;
 typedef long long int64;
 typedef unsigned char u8;
 typedef unsigned short u16;
 typedef unsigned int u32;
 typedef unsigned long long u64;
 ​
 typedef u32 time_t;
 typedef u32 idx_t;

输入输出

我们知道,在操作系统启动的时候,刚开始都是黑乎乎的界面,然后光标闪烁等,那么这个是怎么实现的呢,一般这种都是通过向一些寄存器写入一些值和和获取一些值实现,因此就需要用一些输入输出函数

首先是四个函数

 extern u8 inb(u16 port); // 输入1个字节 从port端口中读一个字节
 extern u16 inw(u16 port); // 输入2个字节  从port端口中读2个字节
 ​
 extern void outb(u16 port,u8 value); // 输出1个字节 将value值输入到port端口中
 extern void outw(u16 port,u16 value); // 输出2个字节 将value值输入到port端口中

我们采用汇编实现

 global inb ; 将inb导出
 inb:
     ; 栈帧保存
     push ebp
     mov ebp, esp
 ​
     xor eax, eax ;清空
     mov edx, [ebp + 8] ;port [ebp + 8]就是传入进来的port
     in al, dx ;将dx所指向的端口,读取一个字放在al,也就是从port端口读一个字节
 ​
     jmp $+2 ;延迟
     jmp $+2 ;延迟
     jmp $+2 ;延迟
 ​
     leave ; 恢复栈帧
     ret
 global outb ; 将outb导出
 outb:
     ; 栈帧保存
     push ebp
     mov ebp, esp
 ​
 ​
     mov edx, [ebp + 8] ;port [ebp + 8]就是传入进来的port
     mov eax, [ebp + 12] ; value 参数入栈是从右往左 所以value地址更高
     out dx, al ;将al的8比特输出到dx的端口号
 ​
     jmp $+2 ;延迟
     jmp $+2 ;延迟
     jmp $+2 ;延迟
 ​
     leave ; 恢复栈帧
     ret
 ​
 global inw ; 将inw导出
 inw:
     ; 栈帧保存
     push ebp
     mov ebp, esp
 ​
     xor eax, eax ;清空
     mov edx, [ebp + 8] ;port [ebp + 8]就是传入进来的port
     in ax, dx ;将dx所指向的端口,读取2个字放在ax
 ​
     jmp $+2 ;延迟
     jmp $+2 ;延迟
     jmp $+2 ;延迟
 ​
     leave ; 恢复栈帧
     ret
 ​
 global outw ; 将outw导出
 outw:
     ; 栈帧保存
     push ebp
     mov ebp, esp
 ​
 ​
     mov edx, [ebp + 8] ;port [ebp + 8]就是传入进来的port
     mov eax, [ebp + 12] ; value 参数入栈是从右往左 所以value地址更高
     out dx, ax ;将ax的2个字输出到dx的端口号
 ​
     jmp $+2 ;延迟
     jmp $+2 ;延迟
     jmp $+2 ;延迟
 ​
     leave ; 恢复栈帧
     ret

我们在kernel中测试下获取光标的位置,相关的寄存器有以下几个

  • CRT 地址寄存器 0x3D4
  • CRT 数据寄存器 0x3D5
  • CRT 光标位置 - 高位 0xE
  • CRT 光标位置 - 低位 0xF

比如我们把光标高位位置给地址寄存器,那么就可以通过数据寄存器得到和设置光标位置的高位值

 // - CRT 地址寄存器 0x3D4
 // - CRT 数据寄存器 0x3D5
 // - CRT 光标位置 - 高位 0xE
 // - CRT 光标位置 - 低位 0xF
 ​
 #define CRT_ADDR_REG 0x3d4
 #define CRT_DATA_REG 0x3d5
 ​
 #define CRT_CURSOR_H 0xe
 #define CRT_CURSOR_L 0xf
 void kernel_init()
 {
     outb(CRT_ADDR_REG,CRT_CURSOR_H);
     u16 pos = inb(CRT_DATA_REG) << 8;
     outb(CRT_ADDR_REG,CRT_CURSOR_L);
     pos |= inb(CRT_DATA_REG); // 到这里,pos值为240,通过qemu也可以看到,光标在第4行,每行80字符
     u8 data = inb(CRT_DATA_REG);
 ​
     // 比如想把光标位置改为160
     outb(CRT_ADDR_REG,CRT_CURSOR_H);
     outb(CRT_DATA_REG,0);
     outb(CRT_ADDR_REG,CRT_CURSOR_L);
     outb(CRT_DATA_REG,160); // 到这里,就可以看到光标在第3行开始处
 }

image-20220505211423561

字符串函数实现

我们在C语言中,使用过很多字符串函数,比如

 char *strcpy(char *dest, const char *src);
 char *strcat(char *dest, const char *src);
 size_t strlen(const char *str);
 int strcmp(const char *lhs, const char *rhs);
 char *strchr(const char *str, int ch);
 char *strrchr(const char *str, int ch);
 int memcmp(const void *lhs, const void *rhs, size_t count);
 void *memset(void *dest, int ch, size_t count);
 void *memcpy(void *dest, const void *src, size_t count);
 void *memchr(const void *ptr, int ch, size_t count);

下面是其实现的代码

 char *strcpy(char *dest, const char *src)
 {
     char *ptr = dest;
     while (true)
     {
         *ptr++ = *src;
         if (*src++ == EOS)
             return dest;
     }
 }
 ​
 char *strcat(char *dest, const char *src)
 {
     char *ptr = dest;
     while (*ptr != EOS)
     {
         ptr++;
     }
     while (true)
     {
         *ptr++ = *src;
         if (*src++ == EOS)
         {
             return dest;
         }
     }
 }
 ​
 size_t strlen(const char *str)
 {
     char *ptr = (char *)str;
     while (*ptr != EOS)
     {
         ptr++;
     }
     return ptr - str;
 }
 ​
 int strcmp(const char *lhs, const char *rhs)
 {
     while (*lhs == *rhs && *lhs != EOS && *rhs != EOS)
     {
         lhs++;
         rhs++;
     }
     return *lhs < *rhs ? -1 : *lhs > *rhs;
 }
 ​
 char *strchr(const char *str, int ch)
 {
     char *ptr = (char *)str;
     while (true)
     {
         if (*ptr == ch)
         {
             return ptr;
         }
         if (*ptr++ == EOS)
         {
             return NULL;
         }
     }
 }
 ​
 char *strrchr(const char *str, int ch)
 {
     char *last = NULL;
     char *ptr = (char *)str;
     while (true)
     {
         if (*ptr == ch)
         {
             last = ptr;
         }
         if (*ptr++ == EOS)
         {
             return last;
         }
     }
 }
 ​
 int memcmp(const void *lhs, const void *rhs, size_t count)
 {
     char *lptr = (char *)lhs;
     char *rptr = (char *)rhs;
     while (*lptr == *rptr && count-- > 0)
     {
         lptr++;
         rptr++;
     }
     return *lptr < *rptr ? -1 : *lptr > *rptr;
 }
 ​
 void *memset(void *dest, int ch, size_t count)
 {
     char *ptr = dest;
     while (count--)
     {
         *ptr++ = ch;
     }
     return dest;
 }
 ​
 void *memcpy(void *dest, const void *src, size_t count)
 {
     char *ptr = dest;
     while (count--)
     {
         *ptr++ = *((char *)(src++));
     }
     return dest;
 }
 ​
 void *memchr(const void *str, int ch, size_t count)
 {
     char *ptr = (char *)str;
     while (count--)
     {
         if (*ptr == ch)
         {
             return (void *)ptr;
         }
         ptr++;
     }
 }

基础显卡驱动

我们知道比如在显示器显示hello,world\n,那么显示器就会先输出一句hello,world,然后换行,这一次就是实现这个操作,其实可以想下,换行,不就是设置一下光标位置嘛,那不就是第二个部分输入输出的样例吗,下面来实现吧,同时注意有以下寄存器

  • CRT 地址寄存器 0x3D4
  • CRT 数据寄存器 0x3D5
  • CRT 光标位置 - 高位 0xE
  • CRT 光标位置 - 低位 0xF
  • CRT 显示开始位置 - 高位 0xC
  • CRT 显示开始位置 - 低位 0xD
控制字符八进制十六进制描述
NUL00x00在输入时忽略,不保存在输入缓冲中
ENQ50x05传送应答消息
BEL70x07从键盘发声响
BS100x08将光标移向左边一个字符位置处;若光标已经处在左边沿,则无动作
HT110x09将光标移到下一个制表位;若右侧已经没有制表位,则移到右边缘处
LF120x0A此代码导致一个回车或换行操作
VT130x0B作用如LF
FF140x0C作用如LF
CR150x0D将光标移到当前行的左边缘处
SO160x0E使用由 SCS 控制序列设计的 G1 字符集
SI170x0F选择 G0 字符集,由 ESC 序列选择
XON210x11使终端重新进行传输
XOFF230x13使中断除发送 XOFF 和 XON 以外,停止发送其它所有代码
CAN300x18如果在控制序列期间发送,则序列不会执行而立刻终止,同时会显示出错字符
SUB320x1A作用同 CAN
ESC330x1B产生一个控制序列
DEL1770x7F在输入时忽略 不保存在输入缓冲中
 #define CRT_ADDR_REG 0x3D4 // CRT(6845)索引寄存器
 #define CRT_DATA_REG 0x3D5 // CRT(6845)数据寄存器
 ​
 #define CRT_START_ADDR_H 0xC // 显示内存起始位置 - 高位
 #define CRT_START_ADDR_L 0xD // 显示内存起始位置 - 低位
 #define CRT_CURSOR_H 0xE     // 光标位置 - 高位
 #define CRT_CURSOR_L 0xF     // 光标位置 - 低位
 ​
 #define MEM_BASE 0xB8000              // 显卡内存起始位置
 #define MEM_SIZE 0x4000               // 显卡内存大小
 #define MEM_END (MEM_BASE + MEM_SIZE) // 显卡内存结束位置
 #define WIDTH 80                      // 屏幕文本列数
 #define HEIGHT 25                     // 屏幕文本行数
 #define ROW_SIZE (WIDTH * 2)          // 每行字节数 一个字符由2个字节控制 ,一个是ascii,一个是样式
 #define SCR_SIZE (ROW_SIZE * HEIGHT)  // 屏幕字节数
 ​
 #define ASCII_NUL 0x00
 #define ASCII_ENQ 0x05
 #define ASCII_BEL 0x07 // \a
 #define ASCII_BS 0x08  // \b
 #define ASCII_HT 0x09  // \t
 #define ASCII_LF 0x0A  // \n
 #define ASCII_VT 0x0B  // \v
 #define ASCII_FF 0x0C  // \f
 #define ASCII_CR 0x0D  // \r
 #define ASCII_DEL 0x7F
 ​
 static u32 screen; // 记录当前显示器开始的内存位置
 static u32 pos;    // 记录当前光标内存位置
 static u32 x, y;   // 当前光标坐标
 ​
 // 删除后,会在那里显示一个类似橡皮擦的样式光标
 static u8 attr = 7;        // 字符样式
 static u16 erase = 0x0720; // 空格 07是字符,20是样式
 ​
 // 获得当前显示器的位置
 static void get_screen()
 {
     outb(CRT_ADDR_REG, CRT_START_ADDR_H); // 显示内存起始位置高地址
     screen = inb(CRT_DATA_REG) << 8;      // 显示内存起始位置值的高8位
     outb(CRT_ADDR_REG, CRT_START_ADDR_L); // 显示内存起始位置低地址
     screen |= inb(CRT_DATA_REG);          // 显示内存起始位置值的低8位
 ​
     screen <<= 1;       // screen *= 2 屏幕上每个位置是由2个字进行描述
     screen += MEM_BASE; // 真正的位置
 }
 ​
 // 设置显示器位置
 static void set_screen()
 {
     outb(CRT_ADDR_REG, CRT_START_ADDR_H);                  // 显示内存起始位置高地址
     outb(CRT_DATA_REG, ((screen - MEM_BASE) >> 9) & 0xff); // 因为screen获得时候,是左移1位,然后再移8位是高地址
     outb(CRT_ADDR_REG, CRT_START_ADDR_L);                  // 显示内存起始位置低地址
     outb(CRT_DATA_REG, ((screen - MEM_BASE) >> 1) & 0xff); // 因为screen获得时候,是左移1位
 }
 ​
 // 获得当前光标位置
 static void get_cursor()
 {
     outb(CRT_ADDR_REG, CRT_CURSOR_H); // 光标内存起始位置高地址
     pos = inb(CRT_DATA_REG) << 8;     // 光标内存起始位置值的高8位
     outb(CRT_ADDR_REG, CRT_CURSOR_L); // 光标内存起始位置低地址
     pos |= inb(CRT_DATA_REG);         // 光标内存起始位置值的低8位
     pos <<= 1;
     pos += MEM_BASE;
 ​
     // 获得光标的坐标
     get_screen();
     u32 delta = (pos - screen) >> 1;
     x = delta % WIDTH;
     y = delta / WIDTH;
 }
 ​
 // 设置当前光标位置
 static void set_cursor()
 {
     outb(CRT_ADDR_REG, CRT_CURSOR_H); // 光标内存起始位置高地址
     outb(CRT_DATA_REG, ((pos - MEM_BASE) >> 9) & 0xff);
     outb(CRT_ADDR_REG, CRT_CURSOR_L); // 光标内存起始位置低地址
     outb(CRT_DATA_REG, ((pos - MEM_BASE) >> 1) & 0xff);
 }
 ​
 void console_clear()
 {
     screen = MEM_BASE;
     pos = MEM_BASE;
     x = y = 0;
     set_cursor();
     set_screen();
 ​
     // 清空 让屏幕全为空格
     u16 *ptr = (u16 *)MEM_BASE;
     while (ptr < (u16 *)MEM_END)
     {
         *ptr++ = erase;
     }
 }
 ​
 void console_init()
 {
 ​
     // 相当于screen为第二行开始的地方,意思就是我们只能从显示器第二行开始看,第一行就看不到了
     // screen = 80 * 2 + MEM_BASE;
     // set_screen();
     // get_screen();
     // 比如设置光标为124, 第一行的后半截,124/2=62
     // pos = 124 + MEM_BASE;
     // set_cursor();
 ​
     console_clear();
 }
 // 超过屏幕显示大小,向上滚屏,也就是把最上面一行去掉
 static void scroll_up()
 {
     if (screen + SCR_SIZE + ROW_SIZE < MEM_END)
     {
         u32 *ptr = (u32 *)(screen + SCR_SIZE);
         for (size_t i = 0; i < WIDTH; i++)
         {
             *ptr++ = erase;
         }
         screen += ROW_SIZE;
         pos += ROW_SIZE;
     }
     // 超过,感觉是直接重头开始
     else
     {
         memcpy((void *)MEM_BASE, (void *)screen, SCR_SIZE);
         pos -= (screen - MEM_BASE);
         screen = MEM_BASE;
     }
     set_screen();
 }
 static void command_lf()
 {
     if (y + 1 < HEIGHT)
     {
         y++;
         pos += ROW_SIZE;
         return;
     }
     scroll_up();
 }
 ​
 static void command_bs()
 {
     if (x)
     {
         x--;
         pos -= 2;
         *(u16 *)pos = erase;
     }
 }
 ​
 static void command_cr()
 {
     pos -= (x << 1);
     x = 0;
 }
 ​
 static void command_del()
 {
 ​
     *(u16 *)pos = erase;
 }
 ​
 void console_write(char *buf, u32 count)
 {
     char ch;
     while (count--)
     {
         ch = *buf++;
         switch (ch)
         {
         case ASCII_NUL:
             break;
         case ASCII_ENQ:
             break;
         case ASCII_BEL: // \a
             break;
         case ASCII_BS: // \b
             command_bs();
             break;
         case ASCII_HT: // \t
             break;
         case ASCII_LF: // \n
             command_lf();
             command_cr();
             break;
         case ASCII_VT: // \v
             break;
         case ASCII_FF: // \f
             command_lf();
             break;
         case ASCII_CR: // \r
             command_cr();
             break;
         case ASCII_DEL:
             command_del();
             break;
         default:
             if (x >= WIDTH)
             {
                 x -= WIDTH;
                 pos -= ROW_SIZE;
                 command_lf();
             }
             *((char *)pos) = ch;
             pos++;
             *((char *)pos) = attr;
             pos++;
             
             x++;
             break;
         }
     }
     set_cursor();
 }

下面简单测试下吧,kernel主函数如下

 char message[] = "hello system...\n";
 void kernel_init()
 {    
     console_init();
     u32 count = 20;
     while (count--)
     {
         console_write(message, sizeof(message) - 1);
     }
 }

image-20220506094823489

可以看到打印了20次,且每次都换行了,成功啦