简单来说
int 0x10
- 当前代码写字符的 ASCII 码到到 xxx 寄存器
- 当前代码触发 0x10 软中断
- BIOS 的中断程序执行一大堆指令将 ASCII 的对应的字符图形显示到显卡上
vidmem
- 当前代码写字符的 ASCII 码到某块固定地址的内存区域
- 显卡芯片轮询这块内存区域,将 ASCII 码转成字符图形
out_p
显卡芯片控制光标
framebuffer
framebuffer 的内存区域和 vidmem 一样,也是另一块内存区。但是不是固定的,由显卡驱动将内存地址发给显卡。所谓的发是走 pcie 等的映射区域。
但此时发送的像素了,由当前代码确定某字符的字形是什么样的。
decompress_kernel 的 outb_p 用的是 int 0x10 吗
不是。decompress_kernel 中的 outb_p 使用的绝对不是 int 0x10。
它们属于计算机底层两个完全不同的层级和概念:
• outb_p 是硬件端口 I/O 指令: 它本质上是 x86 汇编中的 out 指令(通常后面带一个显式延迟,如 jmp 1f; 1: 或读写空闲端口 outb %al, $0x80)。它用于直接向 CPU 的 I/O 总线端口发送 8 位数据,直接与硬件外设(如老式串口、定时器或显卡寄存器)通信。 • int 0x10 是 BIOS 中断调用: 它是实模式(Real Mode)下的软件中断,是由计算机 BIOS 固件提供的显卡服务接口。它内部封装了一系列的逻辑(包括读写各种寄存器、端口甚至内存),供操作系统引导早期或 DOS 程序调用来显示字符、改变分辨率等。
为什么在 decompress_kernel 中不使用 int 0x10?
运行模式不兼容(最核心原因): 当内核引导流程走到 decompress_kernel(即解压内核,通常位于 arch/x86/boot/compressed/misc.c)时,CPU 已经从实模式切换到了 32位保护模式(Protected Mode) 或 64位长模式(Long Mode)。 • int 0x10 依赖于实模式下的 BIOS 中断向量表。 • 在保护模式下,如果没有复杂的 VM86(虚拟8086模式)或特殊的逆向仿真,直接调用 int 0x10 会引发 General Protection Fault (GPF,一般保护性错误) 导致系统直接挂起或重启。
decompress_kernel 期间屏幕输出的真实原理: 如果你在解压内核的代码中看到了类似 outb 的操作,或者看到了屏幕上打印出 “Uncompressing Linux…",它的底层实现通常是以下两种方式之一: • 直接写显存(VGA Text Mode): 保护模式下,代码可以直接向显卡映射的物理内存地址(通常是 0xB8000)写入字符和颜色属性。这种方式不需要调用任何 BIOS 中断,也不需要通过 outb 端口输出字符。 • 串口输出(Serial Console): 如果配置了串口调试,内核会使用 outb / outb_p 向串口控制器(如 0x3F8 端口)的寄存器发送字符数据。这里的 outb_p 就是用来控制串口芯片的,与 int 0x10 毫无关系。 如果需要更深入的了解,请告诉我:
你是在阅读哪个具体体系架构(如 x86_32 还是 x86_64)或哪个特定内核版本的源码?
你是在排查屏幕打印(Console)相关的逻辑,还是在研究早期 I/O 延迟(io_delay)的实现?
所以在 16 位中需要 bios 的中断程序(多条 cpu 指令)来做显示这个动作,而在 32/64 位中一条 out 指令就可以让 cpu 硬件实现显示了?
这个理解存在一个很大的误区:在 32/64 位中,一条 out 指令并不能让 CPU 硬件实现“显示字符”或“打印字符串”这个动作。 无论是 16 位、32 位还是 64 位,CPU 硬件本身都没有“自动把字符画到屏幕上”的功能。 正确的对比和理解如下:
- 16 位实模式:通过 BIOS 帮我们“打工”
在 16 位实模式下,我们要显示一个字符,确实经常用 int 0x10。 • 本质: int 0x10 并不是一条指令就把事情做完了,它是一次软件函数调用。 • 底层: BIOS 固件在内存中提前写好了一大段极其复杂的汇编代码(包含几百条指令)。当你执行 int 0x10 时,CPU 实际上是跳去执行 BIOS 的代码。BIOS 的代码再去控制显卡控制器、计算光标位置、写入显存。 • 结论: 16 位下用 BIOS 中断,是因为初学者或引导程序偷懒,不想自己写控制显卡的复杂代码,所以让 BIOS 代劳。
- 32/64 位保护模式:抛弃 BIOS,内核自己直接操作硬件
到了 32/64 位,BIOS 中断不能用了,Linux 内核必须亲自动手来做显示动作。内核有两种主要的早期显示方式:
方式 A:直接写文本显存(最常用的方式,不用 out 指令)
在 x86 架构中,显卡有一块物理内存被映射到了 CPU 的地址空间,叫做文本显存(地址通常是 0xB8000)。 • 怎么显示? 内核不需要用 out 指令,而是用普通的内存写入指令(如 mov)。 • 例子: 内核想在屏幕左上角显示一个字符 ‘A’,它只需要把 ‘A’ 的 ASCII 码(0x41)和颜色属性直接写入到物理内存 0xB8000 这个地址。 • 结果: 显卡硬件(GPU/显示芯片)每秒会从 0xB8000 扫描几十次,发现这里变成 ‘A’ 了,显卡芯片就会自动把 ‘A’ 的字形变成信号输出到你的显示器上。 • 结论: 这是一个内存映射 I/O(MMIO)的过程,用的是 mov(内存写),不是 out。
方式 B:通过串口输出(这里才会用到 out 指令)
如果 Linux 内核配置了串口调试(Serial Console,黑客或嵌入式开发常用),它在早期解压时就会把日志发送到串口,而不是屏幕。 • 怎么显示? 串口控制器的寄存器是用 I/O 端口(Port I/O) 访问的(比如 0x3F8 端口)。 • 例子: 内核会执行 outb %al, $0x3F8(把字符发送到串口)。 • 结果: 这条 out 指令把数据给到了主板上的串口芯片(UART),串口芯片通过串口线(RS232/USB转串口)把数据传到另一台电脑的屏幕上。 • 结论: 这时确实用到了 out,但它只是把数据扔给了串口芯片,具体“怎么把字符显示出来”是另一台电脑上的接收软件(如 Putty)负责的。
总结:两者的真正区别
阶段 / 模式 核心区别 怎么把字符弄到屏幕上? 16位实模式 (int 0x10) 靠别人(BIOS 固件) 触发中断 → 执行 BIOS 里的数百条指令 → BIOS 帮你去写显存。 32/64位保护模式 (misc.c) 靠自己(内核代码) 内核用 mov 指令直接往文本显存(0xB8000)写数据。显卡硬件本身负责把这块内存变成屏幕上的图像。
所以,32/64位并没有让“显示”变得可以用一条指令搞定。相反,因为没有了 BIOS 的帮忙,Linux 内核必须自己在代码里管理光标位置、计算换行、滚动屏幕,写显存的每一步都得自己抓。 为了帮你彻底理清这里的硬件逻辑,如果可以的话请告诉我: • 你目前是否正在尝试修改 arch/x86/boot/compressed/misc.c 里的 error() 或 putstr() 函数? • 你是否了解 内存映射 I/O (MMIO) 与 端口 I/O (PMIO) 的区别? 我可以结合具体的内核源码行数,给你展示 “Uncompressing Linux…” 这串字符在保护模式下是如何通过 C 语言指针一步步写入显存的。
putstr 的 RM_SCREEN_INFO 是什么?是你说的往文本显存上写数据?那最后的 outb_p 是什么
你贴出的这段正是 Linux 内核早期解压阶段非常经典的 putstr 源码!这段代码完美印证了我们前面讨论的逻辑。 让我们直接拆解你的疑问,看看内核在这里到底做了什么:
- 这里的 vidmem 确实就是“往文本显存上写数据”
代码中的这一行: c vidmem [ ( x + cols * y ) * 2 ] = c; 请谨慎使用此类代码。 这就是我前面说的“直接写文本显存”(通过 mov 指令)。 • vidmem 是一个指针,在前面的代码里它被初始化为指向 0xB8000(VGA 文本显存的起始物理地址)。 • (x + cols * y) * 2 是在计算当前字符在屏幕上的内存偏移量。乘以 2 是因为在 VGA 文本模式下,每个字符占 2 个字节(1 个字节存 ASCII 码,1 个字节存颜色属性)。 • 这一行执行时,CPU 会通过内存总线直接把字符 c 写入显卡内存。不需要调用任何 BIOS 中断,显卡硬件看到这个内存变了,屏幕上对应的位置就会立刻亮起这个字符。
- 这里的 RM_SCREEN_INFO(你打错成的 real_mode)是什么?
代码里的 RM_SCREEN_INFO 是一个宏(通常指向 boot_params.screen_info)。这里的 RM 代表 Real Mode(实模式)。 它的作用是:承上启下,传递接力棒。 • 在它之前(16位实模式): Linux 刚开机时,最早阶段的 16 位引导代码(setup.elf)确实调用了 BIOS 的 int 0x10 中断。但它调用 BIOS 不是为了好玩,而是为了向 BIOS 索要当前屏幕的参数(比如:当前光标在第几行第几列?屏幕一共有多少列?)。BIOS 把这些信息给了内核,内核把它们存在了 boot_params.screen_info 结构体里。 • 在它之后(32/64位保护模式): 到了你看到的这段 putstr 代码,此时 BIOS 中断已经废了。内核为了知道从哪里开始接着画字符,就必须去读之前实模式保存下来的遗产——也就是 RM_SCREEN_INFO.orig_x 和 orig_y。 • 简单来说: 它是在保护模式下,读取当初在实模式下由 BIOS 记录的光标坐标,以保证屏幕打印的连续性。
- 那最后的 outb_p 是什么?
既然字符已经通过 vidmem 写进显存并在屏幕上显示出来了,为什么最后还要来四句 outb_p 呢? 答案是:为了移动屏幕上那个闪烁的硬件光标(Cursor)。 字符虽然显示出来了,但显卡上的那个闪烁的光标并不会自己移动。光标的位置是由 VGA 显卡芯片(CRT 控制器)内部的寄存器控制的。而这个寄存器,必须通过 I/O 端口(Port I/O) 来访问。 这就是最后四行代码做的事情: c pos = (x + cols * y) * 2; /* 计算新的光标内存位置 */
outb_p(14, vidport); /* 1. 告诉显卡:我现在要修改光标位置的高 8 位 / outb_p(0xff & (pos » 9), vidport+1); / 2. 把高 8 位的数据通过端口塞给显卡 / outb_p(15, vidport); / 3. 告诉显卡:我现在要修改光标位置的低 8 位 / outb_p(0xff & (pos » 1), vidport+1); / 4. 把低 8 位的数据通过端口塞给显卡 */ 请谨慎使用此类代码。 • vidport 通常是 0x3D4(彩色模式下的 VGA 索引端口)。 • 这四行代码使用的是 out 汇编指令。它们的作用并不是“显示字符”,而是通知显卡硬件:“我已经把字符写完了,请把那个闪烁的光标移动到新的 (x, y) 坐标去。”
最终总结
这段代码是低级硬件驱动的教科书范例:
- 显现字符靠普通的内存写入(vidmem[…] = c),利用内存映射直接把数据丢给显存。
- 移动光标靠硬件端口 I/O(outb_p),利用特殊的 I/O 指令去拨动显卡芯片的硬件开关。
- 全程没有任何 int 0x10 软件中断的参与。 如果你对这段代码的后续逻辑感兴趣,我们可以进一步探讨: • 那个在代码里被调用的 scroll() 函数,它是如何在没有 BIOS 的情况下,单纯靠内核自己搬运显存数据来实现屏幕“滚屏”的? • 或者你想了解 outb_p 后面那个 _p(Pause)具体是怎么在 CPU 指令级别实现硬件延迟的? 请告诉我你接下来想剖析哪一部分!
为什么在 bios 阶段不用 vidmem ?
其实,BIOS 内部也大量使用了 vidmem(直接写显存)。 之所以你在 16 位引导(BIOS)阶段看到内核代码用 int 0x10 而不直接写 vidmem,并不是因为“不能用”,而是因为在那个阶段,用 int 0x10 是最优、最省事的选择。 主要有以下四个核心原因:
- 16 位段地址寻址的限制
在 16 位实模式下,CPU 最大只能寻址 1MB 的空间,而且必须通过 段地址:偏移地址(Segment:Offset)的方式来访问。 • VGA 文本显存的物理地址是 0xB8000。 • 如果在 16 位汇编或 C 语言里直接写,你必须先小心翼翼地把段寄存器(比如 DS 或 ES)设置为 0xB800,然后再用偏移地址去写。 • 既然反正都要折腾段寄存器,而身边恰好有一个现成的 int 0x10 接口,内核开发者在极早期阶段自然更倾向于直接调用中断。
- 屏幕初始化与模式切换的“鸡生蛋”问题
在计算机刚开机时,显卡硬件处于一个未知的初始状态。直接写 0xB8000 产生显示的前提是:显卡已经被正确配置成了“80x25 16色文本模式”。 • 是谁把显卡配置成这个模式的?正是 BIOS 的 int 0x10(特别是 AX=0x0003 功能号)。 • 在 BIOS 阶段,内核需要适应各种奇奇怪怪的老旧显示器和显卡,甚至需要切换不同的显示分辨率(比如安装系统时有些引导界面是图形化的)。 • 切换模式涉及设置几十个复杂的显卡内部寄存器,Linux 内核在区区几百字节的早期引导扇区里,根本没有空间放下这么庞大的显卡驱动代码,只能完全依赖 BIOS 的 int 0x10 来初始化显卡。
- BIOS 中断是现成的“全功能字模和滚屏驱动”
打印字符串看似简单,但要处理 \n(换行)、\r(回车)、\t(制表符),以及最麻烦的“滚屏(Scroll)”(当屏幕满了,所有行整体往上提一行,最后一行清空)。 • 如果用 vidmem,内核必须自己写一套逻辑去搬运内存(用 memcpy 把后面的显存往前挪),就像你看到的 putstr 里调用了 scroll() 一样。 • 但在 16 位早期阶段,内核的代码体积被严格限制(比如早期 bootsect.S 只有 512 字节)。 • int 0x10 内部已经把换行、滚屏、光标跟随、甚至是字符的字模(Font)全部实现了。内核只需要传一个字符给它,它全权搞定,极大地节省了早期引导程序的空间。
- 彻底解耦和硬件兼容性
在 PC 发展的早期,市场上存在各种非标准的显卡(如 MDA, Hercules, CGA, EGA, VGA)。它们的显存起始地址并不全是 0xB8000(有些是 0xB0000 甚至 0xA0000)。 • 如果 Linux 内核在第一行代码就硬编码直接写 vidmem = 0xB8000,那么在某些老显卡上就会直接黑屏或死机。 • BIOS 固件是主板和显卡厂商专门为这台机器定制的。调用 int 0x10,相当于调用了一个统一的官方 API,由 BIOS 去适配底层各种奇葩的硬件。
总结
在 Linux 引导的两个阶段,开发者的设计哲学完全不同: • 16位 BIOS 阶段(极早期): 内核是“寄生”在 BIOS 上的。能省空间就省空间,能调用现成的就调用现成的,所以用 int 0x10 偷懒并保证极致的兼容性。 • 32/64位解压阶段(decompress_kernel): 内核准备“翻身做主人”了。此时 BIOS 已经无法访问,内核必须接管一切。虽然写 vidmem 和 scroll() 比较麻烦,但保护模式下的 32 位平坦内存模型让直接操作 0xB8000 变得非常简单直观,于是内核就自己写了一套简易的显卡驱动。 这就是为什么内核在不同阶段采取不同策略的原因。如果你感兴趣,我们可以继续聊聊: • 内核在哪个具体的汇编文件(例如 arch/x86/boot/main.c 或 video.c)中最后一次调用 BIOS 中断来保存屏幕信息的? • 或者你想了解进入保护模式(Protected Mode)那一步,CPU 到底发生了什么导致 BIOS 瞬间失效?
这个 vidmem 是否就是后续的 framebuffer?
不是。这里的 vidmem 并不是后续的 Framebuffer(帧缓冲),它们在硬件模式、数据格式以及内核子系统上有着本质的区别。 它们代表了计算机图形显示的两个完全不同的时代和技术演进阶段:
- 概念与本质的区别
• vidmem(VGA 文本显存): • 模式: 属于古老的 VGA 硬件文本模式(Text Mode)。 • 它存什么: 它里面存的不是像素点,而是字符的 ASCII 码和颜色属性(比如控制高亮、闪烁、前景色、背景色)。 • 谁来画图: CPU 把 ‘A’ 写入 0xB8000 之后就撒手不管了。显卡硬件内部有一个固化的“字模 ROM”(Font ROM),显卡芯片自己去查表,把 ‘A’ 变成 8x16 的像素方块输出到显示器上。 [1] ( https://stackoverflow.com/questions/73116712/is-the-0xb8000-controlling-an-integrated-or-discrete-graphics-card) , [2] ( https://opennet.ru/kernel/5.10.110.html) , [3] ( https://forum.osdev.org/viewtopic.php?t=36481) , [4] ( https://stackoverflow.com/questions/45997176/how-does-x86-cpu-translate-an-address-to-an-io-such-as-vga-text-buffer ) • Framebuffer(帧缓冲): • 模式: 属于 图形模式(Graphics Mode)。 • 它存什么: 它里面存的是屏幕上每一个像素点的具体颜色值(比如 1024x768 分辨率下,每个点由 RGB 3个字节组成)。 • 谁来画图: 显卡芯片变成了纯粹的“像素搬运工”,它不再懂得什么是字符。如果 Linux 内核想在 Framebuffer 上显示一个字符 ‘A’,内核必须自己写代码(通过 fbcon 驱动)去把 ‘A’ 的字型数据肉眼可见地“画”成一堆像素点写入显存。 [1] ( https://www.linuxjournal.com/article/3278) , [2] ( https://bellard.org/fbcon/) , [3] ( https://opennet.ru/kernel/5.10.110.html )
- 内核中完全不同的接力阶段
在 Linux 启动的整个生命周期中,屏幕输出经历了三次进化: [ 阶段 1: 16位实模式 ] -> 使用 BIOS int 0x10 打印早期信息。 ↓ [ 阶段 2: 解压内核阶段 ] -> 使用你看到的 putstr(),直接写 0xB8000 文本显存 (vidmem)。 ↓ 内核完全启动,加载真正的显卡驱动 [ 阶段 3: 现代内核阶段 ] -> 开启图形模式,启用 Framebuffer (fbdev / DRM KMS)。 当内核解压完成、正式进入 start_kernel 并且初始化了现代显卡驱动(如 Intel、AMD、NVIDIA 驱动或通用的 efifb / vesafb)后,内核会彻底关闭 VGA 文本模式,将其切换到图形模式。 此时,这块旧的 vidmem(0xB8000)就失效并停用了。取而代之的是,显卡驱动会映射一块巨大(几 MB 到几 GB)的物理显存作为 Framebuffer,内核的终端驱动 fbcon(Framebuffer Console)会接管屏幕,通过像素把命令行文字画出来。 [1] ( https://wiki.gentoo.org/wiki/Framebuffer) , [2] ( https://moi.vonos.net/linux/framebuffer-drivers/) , [3] ( https://bellard.org/fbcon/ )
- 核心对比
特性 解压阶段的 vidmem 后续的 Framebuffer 物理地址 固定的 0xB8000(仅 32KB 大小) 不固定,由显卡 PCI 栏位决定(通常极大) 写入内容 字符代码(1B ASCII + 1B 属性 = 2字节) 像素颜色(通常每个像素 3 到 4 字节,如 RGB888) 支持图形 不支持(只能显示 80x25 个纯文本字符) 完美支持(可以显示任意图片、高分辨率桌面) 内核驱动 内核极早期的简易硬编码硬刷逻辑 内核的 fbdev 驱动子系统或现代 DRM/KMS 框架 现代命运 在 UEFI 启动的现代电脑上已被废弃 成为目前 Linux 所有控制台和图形界面的底层基石
补充一点: 在现代采用 UEFI 固件 启动的电脑上,Linux 内核的解压阶段(阶段2)甚至连 0xB8000 都不用了,因为现代显卡在 UEFI 下默认就不支持传统的 VGA 文本模式。UEFI 阶段会直接提供一个极为初级的图形 Framebuffer 给内核(通过 GOP 协议),Linux 在解压阶段会直接用像素画字。 [1] ( https://www.reddit.com/r/coreboot/comments/eanr41/high_resolution_framebuffer_vs_legacy_vga_text/ ) 为了顺着这个脉络帮你建立起 Linux 完整的显示知识图谱,你可以告诉我: • 你是否想了解现代 Linux 是如何通过 fbcon(帧缓冲控制台) 驱动在图形像素屏幕上“画”出命令行终端的? • 或者你想知道在 UEFI 模式 下,Linux 内核早期的 putstr 是如何演变成通过像素画字的?
那么在 framebuffer 的内存区域和 vidmem 一样也是另一块内存区?但是不是固定的?由显卡驱动将内存地址发给显卡?所谓的发是走 pcie 等的映射区域?
是