这一次我们正式进入内核,编写相关的内核代码,也就是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行开始处
}

操作系统实现-进入内核-LMLPHP

字符串函数实现

我们在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
#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);
    }
}

操作系统实现-进入内核-LMLPHP

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

05-12 09:46