第三章 在屏幕上显示点什么 - 从零开始开发UEFI引导的64位操作系统内核

声明:此文章为本人在知乎首发的原创文章


与本文同时编写的内核项目现已开源至github


一、将multiboot2头补充完整

multiboot2头中可以加入各种tag,使得grub为我们提供各种功能和信息,比如说内存布局信息、设置帧缓冲区功能、内核参数等。
这一章的标题是“在屏幕上显示点什么”,在屏幕上显示内容的一种方式就是写入帧缓冲区,因此我们需要在multiboot2头中加入framebuffer tag

    framebuffer_tag:
        dd 5
        dd 20
        dd 1920 ; width
        dd 1080 ; height
        dd 32   ; depth
    framebuffer_tag_end:
    dd  0

由于每个标签首地址都需要8字节对齐,为了保证下一个标签的首地址是8字节对齐的,需要用一个dd 0填充。
接下来我们还需要一个mbi tag,若multiboot头中有此tag,则会在寄存器rbx(仅x86架构)中提供boot information数据结构的地址,这个数据结构可以获得内存容量、帧缓冲区等数据。

    bootinfo_request_tag:
        dd 1
        dd bootinfo_request_tag_end - bootinfo_request_tag
        dd 4    ; basic mamory info
        dd 1    ; boot command line
        dd 8    ; framebuffer info
        dd 12   ; efi64 system table pointer
    bootinfo_request_tag_end:

从第8字节开始,每个双字都代表一项mbi requirement,这里分别请求了基本内存信息、命令行参数(grub用内置的命令行执行引导命令)、帧缓冲区、efi table。


二、从entry到内核主程序

entry32中,我们已经将大部分机器状态设置完毕,接下来只要设置堆栈,并合适地定义一个内核主函数就可以了。
我们可以定义如下内核主函数:

void kmain(void *mb2_bootinfo)
{
    while (1);
}

由于内核主函数永远不会返回(即使关机也不会),所以没有返回值。
因为所有我们需要的数据都在multiboot information中,因此我们只需传递一个bootinfo指针。
然后修改entry汇编程序:

    extern kmain
    global init64
init64:
    endbr64
    cli
    mov rax, 0x1000000
    mov rbp, rax
    mov rsp, rax
    mov rdi, rbx
    jmp kmain

由于还没有配置中断,首先用cli指令将中断关闭;然后将0x1000000设置为栈底(见第一章中的链接脚本,内核堆栈为4M到16M的空间);将原本在rbx中的multiboot information结构指针放在rdi中(C语言调用约定的第一个参数);最后直接跳转即可。
观察内核文件反汇编得到的汇编代码,会在kmain结尾看到一个跳转至本身地址的jmp指令,这就是我们C语言中的while (1)。启动qemu,会看到rip寄存器已经停在了这个地址处。


三、帧缓冲区

通过遍历multiboot information,我们可以获得有关帧缓冲区的信息。
通过multiboot2文档,我们可以总结出multiboot information数据结构如下:

偏移量大小(字节)说明
04bootinfo总长度
44-
84第一个tag的type
124第一个tag的size=l1
16l1第一个tag的内容
8+l1+n(n使这个偏移8字节对齐)-第二个tag

可以根据以上结构,搜索到framebuffer info的首地址,ramebuffer info结构如下:framebuffer info其中addr表示帧缓冲区的首地址;pitch代表每个像素之间相隔多少字节;bpp为bits per pixel,即比特每像素;type表示像素类型,根据不同的type值,color_info表示不同的数据结构,这些数据结构能够表示每个像素的色彩组成及在一个像素中的位置,经过我的grub的引导,帧缓冲区每个像素3字节,没有间隔,像素格式为bgr,与通常的像素表示相反,最低8比特表示红色。
获取了帧缓冲区后,我们还不能直接使用,需要将地址addr在页表中映射,经过页表的映射后,我们才能使用这部分内存。
由于以后会大量进行内存映射的操作,我们可以在这事先写好映射页表1的函数:

#define map_pagemap(addr) \
    map_pageframe_to((u64)addr, (u64)addr, false, true, MEMM_PAGE_SIZE_4K);

// 这里的physical必须保证根据ps对齐
static void map_pageframe_to(u64 target, u64 physical,
                             bool user, bool write, memm_page_size ps)
{
    if (!is_cannonical(target))
        return;

    usize pml4ei = memm_la_get_entry_index(target, MEMM_LA_PML4EI);
    usize pml4e = PML4[pml4ei];
    u64 *PDPT;
    if (memm_entry_flag_get(pml4e, MEMM_ENTRY_FLAG_PRESENT) == true)
        PDPT = (u64 *)memm_entry_get_address(pml4e);
    else
    {
        PDPT = (u64 *)(find_fitable_pages(1) * MEMM_PAGE_SIZE);
        map_pagemap(PDPT);
        memset(PDPT, 0, MEMM_PAGE_SIZE);

        PML4[pml4ei] =
            MEMM_ENTRY_FLAG_PRESENT |
            MEMM_ENTRY_FLAG_WRITE |
            (u64)PDPT;
    }

    usize pdptei = memm_la_get_entry_index(target, MEMM_LA_PDPTEI);
    if (ps == MEMM_PAGE_SIZE_1G)
    {
        PDPT[pdptei] =
            MEMM_ENTRY_FLAG_PRESENT |
            (write ? MEMM_ENTRY_FLAG_WRITE : 0) |
            (is_user_address(target) ? MEMM_ENTRY_FLAG_USER : 0) |
            MEMM_ENTRY_FLAG_PS |
            (is_user_address(target) ? 0 : MEMM_ENTRY_FLAG_GLOBAL) |
            physical;
        return;
    }
    usize pdpte = PDPT[pdptei];
    u64 *PDT;
    if (memm_entry_flag_get(pdpte, MEMM_ENTRY_FLAG_PRESENT) == true)
        PDT = (u64 *)memm_entry_get_address(pdpte);
    else
    {
        PDT = (u64 *)(find_fitable_pages(1) * MEMM_PAGE_SIZE);
        map_pagemap(PDT);
        memset(PDT, 0, MEMM_PAGE_SIZE);

        PDPT[pdptei] =
            MEMM_ENTRY_FLAG_PRESENT |
            MEMM_ENTRY_FLAG_WRITE |
            (u64)PDT;
    }

    usize pdei = memm_la_get_entry_index(target, MEMM_LA_PDEI);
    if (ps == MEMM_PAGE_SIZE_2M)
    {
        PDT[pdei] =
            MEMM_ENTRY_FLAG_PRESENT |
            (write ? MEMM_ENTRY_FLAG_WRITE : 0) |
            (is_user_address(target) ? MEMM_ENTRY_FLAG_USER : 0) |
            MEMM_ENTRY_FLAG_PS |
            (is_user_address(target) ? 0 : MEMM_ENTRY_FLAG_GLOBAL) |
            physical;
        return;
    }
    usize pde = PDT[pdei];
    u64 *PT;
    if (memm_entry_flag_get(pde, MEMM_ENTRY_FLAG_PRESENT) == true)
        PT = (u64 *)memm_entry_get_address(pde);
    else
    {
        PT = (u64 *)(find_fitable_pages(1) * MEMM_PAGE_SIZE);
        map_pagemap(PT);
        memset(PT, 0, MEMM_PAGE_SIZE);

        PDT[pdei] =
            MEMM_ENTRY_FLAG_PRESENT |
            MEMM_ENTRY_FLAG_WRITE |
            (u64)PT;
    }

    usize pei = memm_la_get_entry_index(target, MEMM_LA_PEI);
    PT[pei] =
        MEMM_ENTRY_FLAG_PRESENT |
        (write ? MEMM_ENTRY_FLAG_WRITE : 0) |
        (is_user_address(target) ? MEMM_ENTRY_FLAG_USER : 0) |
        MEMM_ENTRY_FLAG_PS |
        (is_user_address(target) ? 0 : MEMM_ENTRY_FLAG_GLOBAL) |
        physical;
    return;
}

extern void reload_pml4();

bool memm_map_pageframes_to(
    u64 target, u64 physical,
    usize size,
    bool user, bool write)
{
    if (!is_cannonical(target) || !is_cannonical(physical))
        return false;
    if (!is_aligned(target, MEMM_PAGE_SIZE) || !is_aligned(physical, MEMM_PAGE_SIZE))
        return false;
    while (size != 0)
    {
        memm_page_size align = memm_get_page_align(physical);
        if (align == MEMM_PAGE_SIZE_1G)
        {
            if (size < (usize)align * MEMM_PAGE_SIZE / 2)
                align = MEMM_2M_ALIGN_MASK;
        }
        if (align == MEMM_PAGE_SIZE_2M)
        {
            if (size < (usize)align * MEMM_PAGE_SIZE / 2)
                align = MEMM_4K_ALIGN_MASK;
        }

        map_pageframe_to(target, physical, user, write, align);
        usize step = min(size, (usize)align * MEMM_PAGE_SIZE);

        size -= step;
        target += step;
        physical += step;
    }
    reload_pml4();
    return true;
}

其中reload_pml4函数的功能是使修改后的页表生效,需要在汇编代码中定义:

    section .text
    global reload_pml4
reload_pml4:
    push rax

    mov rax, cr3
    mov cr3, rax

    pop rax

    ret

C语言代码中调用的find_fitable_pages(1)函数,为内存管理模块中的函数,目前我们可以通过设置一个固定的物理页代替。
其余的所有宏函数、宏常量定义如下,其中涉及到的线性地址、canonical地址等概念以及页表有关的内容会在下一章中集中讲解:

/* 页大小,以MEMM_PAGE_SIZE为单位 */
#define MEMM_PAGE_SIZE 4096
typedef enum __memm_page_size
{
    MEMM_PAGE_SIZE_4K = 1,      // 1个4KB页大小
    MEMM_PAGE_SIZE_2M = 512,    // 512个4KB页大小
    MEMM_PAGE_SIZE_1G = 262144, // 262144个4KB页大小
} memm_page_size;

extern u64 PML4[512];

#define MEMM_PAGE_TABLE_FLAGS_AREA ((u64)0xfff)

/* 页对齐掩码 */
#define MEMM_4K_ALIGN_MASK ((u64)0xfff)
#define MEMM_2M_ALIGN_MASK ((u64)0x1fffff)
#define MEMM_1G_ALIGN_MASK ((u64)0x3fffffff)

/* 页表项属性FLAGS */
#define MEMM_ENTRY_FLAG_PRESENT ((u64)1)
#define MEMM_ENTRY_FLAG_WRITE ((u64)1 << 1)
#define MEMM_ENTRY_FLAG_USER ((u64)1 << 2)
#define MEMM_ENTRY_FLAG_PWT ((u64)1 << 3)
#define MEMM_ENTRY_FLAG_PCD ((u64)1 << 4)
#define MEMM_ENTRY_FLAG_ACCECED ((u64)1 << 5)
#define MEMM_ENTRY_FLAG_DIRTY ((u64)1 << 6)
#define MEMM_ENTRY_FLAG_PS ((u64)1 << 7)
#define MEMM_ENTRY_FLAG_GLOBAL ((u64)1 << 8)
#define MEMM_ENTRY_FLAG_PAT ((u64)1 << 12)
#define MEMM_PTE_ENTRY_FLAG_PAT ((u64)1 << 7)
#define MEMM_ENTRY_FLAG_XD ((u64)1 << 63)
#define memm_entry_flag_get(entry, flag) \
    ((entry & flag) ? true : false)

/* 页表(大型页)项地址域掩码 */
#define MEMM_ENTRY_ADDRESS_MASK ((u64)0x000ffffffffff000)
#define MEMM_BP_ENTRY_ADDRESS_MASK ((u64)0x000fffffffffe000)
#define memm_entry_get_address(entry)                          \
    ((entry) & (memm_entry_flag_get(entry, MEMM_ENTRY_FLAG_PS) \
                    ? MEMM_BP_ENTRY_ADDRESS_MASK               \
                    : MEMM_ENTRY_ADDRESS_MASK))

/* 线性地址表项索引或页内偏移掩码 */
#define MEMM_LA_PML4EI_MASK ((u64)0x0000ff8000000000)
#define MEMM_LA_PDPTEI_MASK ((u64)0x0000007fc0000000)
#define MEMM_LA_PDEI_MASK ((u64)0x000000003fe00000)
#define MEMM_LA_PEI_MASK ((u64)0x00000000001ff000)

#define MEMM_LA_1GB_PAGE_OFFSET_MASK ((u64)0x000000003fffffff)
#define MEMM_LA_2MB_PAGE_OFFSET_MASK ((u64)0x00000000001fffff)
#define MEMM_LA_4KB_PAGE_OFFSET_MASK ((u64)0x0000000000000fff)

/* 线性地址表项索引偏移位数 */
#define MEMM_LA_PML4EI_OFFSET (39)
#define MEMM_LA_PDPTEI_OFFSET (30)
#define MEMM_LA_PDEI_OFFSET (21)
#define MEMM_LA_PEI_OFFSET (12)

/* 获取线性地址中某个表项索引以及获取页内偏移 */
#define MEMM_LA_PML4EI
#define MEMM_LA_PDPTEI
#define MEMM_LA_PDEI
#define MEMM_LA_PEI
#define memm_la_get_entry_index(addr, entry) \
    (((addr) & (entry##_MASK)) >> (entry##_OFFSET))

#define MEMM_LA_1GB_PAGE_OFFSET
#define MEMM_LA_2MB_PAGE_OFFSET
#define MEMM_LA_4KB_PAGE_OFFSET
#define memm_la_get_offset(addr, page_type) \
    ((addr) & (page_type##_MASK))

#define is_user_address(addr) \
    (((addr) > 0xffff7fffffffffff) ? true : false)

#define is_cannonical(addr) \
    (((addr) < 0x0000800000000000 || (addr) > 0xffff7fffffffffff) ? true : false)

#define memm_get_page_align(addr)               \
    (is_aligned(addr, MEMM_PAGE_SIZE_1G)        \
         ? MEMM_PAGE_SIZE_1G                    \
         : (is_aligned(addr, MEMM_PAGE_SIZE_2M) \
                ? MEMM_PAGE_SIZE_2M             \
                : MEMM_PAGE_SIZE_4K))

接着根据bootinfo中的framebuffer信息计算出帧缓冲区占用的内存容量size后调用memm_map_pageframes_to(framebuffer_addr, framebuffer_addr, size, 0, 1)即可将bootinfo中给出的framebuffer映射到内存中了。
我们可以向帧缓冲区中写入一些内容,再运行一下内核。比如直接向整个缓冲区写入白色:写入白色的运行效果我们已经成功地进入了内核并在屏幕上显示内容。同时我在我的内核项目中定义了一个ascii字体,读者可以直接把这个字体放在自己的内核代码中用于显示文字。


这一章我们经过了两级entry后进入了内核主程序并成功地使用了帧缓冲区在屏幕上显示内容。下一章将会针对这一章和上一章中略过的内容集中讲解x86_64架构中的分页机制以及有关的线性地址物理地址cannonical型地址等概念。


与本文同时编写的内核项目现已开源至github


  1. 有关寻址和页表会在下一章中讲解 ↩︎

  • 31
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

指向BIOS的野指针

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值