声明:此文章为本人在知乎首发的原创文章
与本文同时编写的内核项目现已开源至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数据结构如下:
偏移量 | 大小(字节) | 说明 |
---|---|---|
0 | 4 | bootinfo总长度 |
4 | 4 | - |
8 | 4 | 第一个tag的type |
12 | 4 | 第一个tag的size=l1 |
16 | l1 | 第一个tag的内容 |
8+l1 +n(n使这个偏移8字节对齐) | - | 第二个tag |
可以根据以上结构,搜索到framebuffer info
的首地址,ramebuffer 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
有关寻址和页表会在下一章中讲解 ↩︎