导引:点亮什么、为什么、设计图
这一章我们要点亮什么
两件看得见的事。
第一件,内核启动后屏幕不再是黑的。bootloader 会把显卡切到一个 1024×768、每像素 32 位的图形模式,把一块连续的物理显存(线性帧缓冲,linear framebuffer)交给我们。我们只要往这块显存里写一个 32 位数,屏幕上对应位置就亮起一个像素。
第二件,内核能在这块显存上画字。光能画点不够——诊断信息总不能一个点一个点地拼。我们内置了一个 8×16 的点阵字体(PSF2 格式),每个字符就是 16 个字节、每字节 8 位,哪位是 1 哪个像素就亮。于是 'A' 不再是个抽象的码,而是一组实实在在的像素。
为了这两件事能成立,中间还卡着一个容易被忽略、但写错就一定 page fault 的环节:那块显存在物理地址空间里,内核跑在虚拟地址空间里,你得先把这段物理显存映射进内核能访问的虚拟地址,后面所有的写像素才有效。这一步我们用一个最小的页表助手 map_mmio 搞定,它也是这一章技术含量最高的部分。
为什么现在需要它
先说为什么是现在。到 012 为止,内核已经能在 sti 之后稳定响应时钟中断,异常链也走通了。但系统的「脸」还是黑的。接下来无论是想做个 shell、想画个简单的图形界面,还是仅仅想在调试时不依赖串口窗口就能看见内核在说什么,都绕不开「先把屏幕点亮」这一步。它是第一个真正「看得见」的驱动,也是后面所有用户态交互的舞台。
再往回看一层,这一章还顺手兑现了 012 埋下的一个承诺。012 把 kprintf 的格式化引擎和输出后端用回调解耦时,我们说过「013 之后还会多一路屏幕,到那时你就明白这层回调当初为什么要抽出来了」。这一章先把屏幕这路硬件备好(像素能画、字能显);真正把 kprintf 接上来、让那句承诺落地,是 013b 的事。所以这两章是一条线的两段:013 解决「屏幕能不能画」,013b 解决「kprintf 说不说得到屏幕上」。
设计图
从 bootloader 把显卡切到图形模式,到内核画出一个像素,中间是三层栈:
展开代码 (共 25 行)收起代码
┌──────────────────────────────────────────────────────────┐
│ bootloader (实模式, VBE INT 0x10) │
│ 设模式 0x144 = 1024x768x32 + LFB bit14 │
│ → 把 fb_addr/fb_width/fb_height/fb_pitch/fb_bpp 塞进 │
│ BootInfo (物理 0x7000) │
└────────────────────────────┬─────────────────────────────┘
▼ big kernel 读 BootInfo
┌──────────────────────────────────────────────────────────┐
│ arch::map_mmio(fb_phys, fb_size) ← 本章难点 │
│ 在 bootloader 留下的页表里补条目, 把 [phys, phys+size) │
│ 恒等映射 (virtual == physical), 用 2MB / 1GB 大页 │
└────────────────────────────┬─────────────────────────────┘
▼ 现在 fb_phys 这个物理地址
可以当虚拟地址直接访问了
┌──────────────────────────────────────────────────────────┐
│ Framebuffer │
│ addr_ = (volatile uint32_t*)fb_phys │
│ put_pixel(x,y,argb): addr_[y*(pitch/4) + x] = argb │
└────────────────────────────┬─────────────────────────────┘
▼ 能画点了, 但点不等于字
┌──────────────────────────────────────────────────────────┐
│ PSFFont │
│ assets/font.psf (.incbin 嵌进内核) → PSF2 header + 字形 │
│ render_char(fb,'A',x,y,fg,bg): 逐位 put_pixel │
└──────────────────────────────────────────────────────────┘最关键、也最容易写错的是中间那层 map_mmio。很多人第一次 framebuffer 不亮,都不是算错像素坐标,而是根本没把显存映射进来——往一个没映射的虚拟地址写,直接 page fault。所以这一章的「代码路线」会从那一步讲起,而不是急着画像素。