尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
从BIOS中断到屏幕输出:手写操作系统引导扇区实战
从零开始编写操作系统最难熬的不是后面的内存管理、任务调度而是最开始的“两眼一抹黑”屏幕全黑你完全不知道自己的代码到底执行了没有、执行到哪一步、卡死在哪个指令上。这时候BIOS 中断就是第一根救命稻草。它自带一套完整的底层服务让你连驱动都不用写就能在屏幕上打出字符。这篇文章就围绕 BIOS 中断和屏幕显示讲清楚实模式下怎么把字符和字符串输出到屏幕顺便把引导扇区、中断调用、模拟器调试这些基本功一起串起来。适合刚开始碰操作系统、对汇编有兴趣但没写过实模式代码的同学也适合想复习底层机制的老手。1. 为什么第一步是 BIOS 中断先搞懂实模式里的那套服务机制1.1 你面对的是一个“老古董”环境实模式开机之后、进入你的代码之前CPU 跑在实模式下。这个模式跟八十年代初期的处理器几乎一样物理地址空间只有 1MB寻址靠“段寄存器左移 4 位 偏移量”没有分页、没有多任务、没有特权级保护更没有虚拟内存。你的操作系统内核还躺在磁盘里设备驱动一个都没装载显卡驱动、键盘驱动、磁盘驱动统统不存在。听起来很原始但正因为原始很多东西反而好理解。你写的第一段引导代码就是从物理地址0x7C00开始执行的。BIOS 在自检之后会把启动设备的第一个扇区512 字节加载到0x7C00检查末尾两个字节是不是0x55AA如果是就跳过去执行。这就是整个引导过程的起点。在这个阶段主板固件传统 BIOS其实已经帮你干了很多活。它会初始化内存、显卡、键盘、磁盘控制器并且把这些设备的标准处理程序安装到内存最前面的中断向量表里。你在引导代码里能直接调用这些“现成的服务”不需要自己从头写驱动。1.2 中断不只是“打断”而是一张服务菜单很多新手看到“中断”两个字脑子里浮现的是硬件随时打断你执行流程所以下意识不敢碰。实际上你在引导阶段写的INT 10h更像是在拨打电话叫服务而不是真的被某种“突发事件”打断。CPU 里有一张中断向量表位于物理地址0x00000到0x00400之间一共 256 项每项 4 字节装着一个中断处理程序的入口地址。BIOS 在开机自检时已经把这张表填好了。当你执行INT 10hCPU 会先压入标志寄存器和返回地址然后从向量表第0x10项取出显示服务程序的入口跳过去执行最后通过IRET指令恢复现场回到你的代码里继续跑。INT 10h是视频显示服务的中断号功能码放在AH寄存器里。比如AH0x00设置显示模式AH0x02设置光标位置AH0x0E电传方式输出单个字符AH0x13输出一串字符这就是一张挂在墙上的服务菜单。你想用哪个功能就把对应的功能码填进AH然后把参数填进其他寄存器再调一次INT 10h就行。1.3 为什么引导阶段非它不可你手上只有 512 字节的引导扇区没有磁盘驱动、没有文件系统、没有中断处理程序甚至没有自己的显卡初始化代码。想往屏幕上输出字符其实只有两条路要么直接操作显卡硬件端口访问 显存写入要么调用 BIOS 中断。前者要求你把 VGA 文本模式的端口寄存器、显存布局、光标控制全部搞清楚代码量不小后者一行INT 10h就能搞定而且 BIOS 内部会处理大部分细节。所以在引导阶段几乎所有人的选择都是先依赖 BIOS 中断来做最原始的调试输出。等后面内核越来越大需要完全控制硬件的时候再写自己的驱动把这些依赖一项项拆掉。换句话说BIOS 中断不只是“用来显示字符的工具”它同时是你验证工具链、验证引导流程、验证代码能否真正跑起来的最小可用接口。2. 屏幕显示的两条路线BIOS 中断和直接写显存怎么选2.1 路线一直接写0xB8000显存在 VGA 文本模式下显卡会把显存映射到物理地址0xB8000。这里每个字符占两个字节低字节是 ASCII 码高字节是显示属性前景色、背景色、闪烁等。比如黑底白字就是属性字节0x07。你想显示字符就像操作普通内存一样写这个地址mov ax, 0xb800 mov es, ax mov byte [es:0], A ; 第一行第一列显示“A” mov byte [es:1], 0x07 ; 黑底白字这条路线的好处是速度快、无依赖、可控性强。你完全掌握显存布局想在哪里显示就写哪里不会受 BIOS 内部逻辑影响。但坏处也很明显光标位置要自己维护换行、滚动要自己写逻辑屏幕满了还得把几十行文本搬来搬去。对于只有 512 字节的引导扇区来说这套逻辑写起来挺占空间的。2.2 路线二调用INT 10h的显示服务INT 10h的最大优势是把光标、滚动、换行这类脏活都封装好了。你用AH0x0E输出一个字符它自己会推进光标到行尾自动换行滚到屏幕底部自动上卷。你根本不用管显存地址在哪儿只需要告诉它“输出什么字符”就够了。代价是什么呢第一慢。BIOS 内部要查表、判断模式、读状态、搬显存一条INT指令背后可能有几十上百条指令在执行。第二只能在实模式下用。一旦你的内核切到保护模式或者长模式传统 BIOS 中断基本就不可用了那时候必须自己写驱动。第三不同厂商的 BIOS 实现有些细节差异模拟器上跑得好好的真机上可能表现不同。2.3 引导阶段的实际选型我为什么先用INT 10h我把两种方案都试过引导阶段直接用显存方案完全可行尤其是你只想输出一个“Kernel loaded”之类的一行字甚至比调中断还干净。但综合来看对刚开始写 OS 的人我还是推荐先走INT 10h。原因很实在代码量少不容易错也不依赖你把显存那套概念吃透。引导扇区代码本身就容易出各种低级问题如果再叠一层“自己维护光标”的复杂度排查起来很痛苦。先用中断把链路跑通确认编译、链接、加载、跳转都没问题再回头研究显存方案也不迟。我自己的实际做法是引导阶段只用INT 10h做最原始的调试输出等内核初始化 VGA 驱动时再彻底切换到显存直写。这样每步只引入一个新的复杂点出了问题也好定位。3. 手把手实现第一个屏幕输出引导扇区代码实战3.1 工具链准备NASM 加 QEMU 就够了写引导扇区代码我用 NASM 做汇编器用 QEMU 做模拟器。NASM 的语法是 Intel 风格对新手比较友好而且nasm -f bin可以直接生成裸二进制文件不需要链接器。QEMU 可以直接加载裸镜像也可以随时按CtrlAlt2切到监视器查看寄存器状态调试体验很好。安装方式各家平台不太一样但基本都是包管理器一条命令的事。这里就不再啰嗦默认你已经准备好了。这个阶段的代码不需要任何库不需要头文件也不需要链接脚本。纯粹的.asm文件编译成.bin走起。3.2 最小可运行的引导扇区代码输出一个字符先来个最小的屏幕上显示一个字符A。代码量很短但里面每一行都有讲究。org 0x7c00 mov ax, 0 mov ds, ax mov es, ax ; 初始化栈栈顶放在 0x7c00 往下 mov ss, ax mov sp, 0x7c00 ; 用 INT 10h 的 0x0E 功能输出一个字符 A mov ah, 0x0e mov al, A int 0x10 halt: cli hlt jmp halt times 510-($-$$) db 0 dw 0xaa55编译运行nasm -f bin boot.asm -o boot.bin qemu-system-i386 -drive formatraw,fileboot.bin如果一切正常QEMU 窗口左上角会出现一个白色字符A。看到这个A出现意味着你的整个工具链、引导扇区格式、BIOS 中断调用全部通了这一步是整个操作系统开发里最重要的里程碑之一。3.3 逐行解读这些代码为什么是这么写先说org 0x7c00。这是一个关键指令。它告诉汇编器我们的代码被加载到物理地址0x7C00执行所有标签比如halt的地址都要按这个基址计算。如果漏掉这一行后面的jmp halt就会跳到一个错误地址程序大概率直接飞掉。再看段寄存器。mov ax, 0和mov ds, ax把数据段设为 0这样后面用DS:SI访问内存时偏移量就是物理地址。es设 0 是为了INT 10h的0x13功能虽然这个例子里没用但我习惯在引导开始就统一清一遍。栈也要初始化。mov ss, ax和mov sp, 0x7c00把栈顶设在0x7C00栈向下生长不会覆盖我们的引导代码。为什么必须设因为INT 10h内部会使用栈CPU 执行INT时也要压入返回地址。如果没有栈中断调用一进来就崩了。这是新手特别容易忽略的点。mov ah, 0x0e是选功能码mov al, A是准备字符int 0x10触发显示服务。字符串末尾最后用times 510-($-$$) db 0填充让整个文件到 512 字节前 2 字节的位置最后dw 0xaa55是引导扇区签名。少了签名BIOS 根本不会认为这是一个可引导扇区。3.4 从单字符到字符串循环打印任意文本单字符输出只是热身实际调试时你肯定想输出字符串。用INT 10h的0x0E配合循环可以逐字符打印org 0x7c00 start: mov ax, 0 mov ds, ax mov es, ax mov ss, ax mov sp, 0x7c00 mov si, msg mov ah, 0x0e print_loop: lodsb ; 把 DS:SI 指向的字节加载到 ALSI 自动加 1 or al, al ; 判断字符是否为 0 jz halt ; 是 0说明字符串结束 int 0x10 ; 输出 AL 中的字符 jmp print_loop halt: cli hlt jmp halt msg db Hello, OS World!, 0x0d, 0x0a, 0 times 510-($-$$) db 0 dw 0xaa55这里约定字符串以0结尾lodsb每次取一个字符并自动递增 SI判断到0就停下。注意我在msg末尾放了回车0x0d和换行0x0a这样输出完会换行模拟器里看起来更清晰。如果你想让字符串带颜色或者一次性输出而不逐字符循环可以用INT 10h的0x13功能后面会细说。4. 从单字符到字符串封装自己的输出函数与迷你终端4.1 逐字符循环和一次性输出怎么选AH0x0E适合逐字符处理因为你要在循环里判断每个字符控制符可以单独处理逻辑灵活。但如果你只是想一次性把一行文本打满或者想带上彩色属性更合适的是INT 10h的AH0x13功能。AH0x13的参数是这样的ES:BP指向要输出的字符串CX是字符串长度DH是行号DL是列号BH是页码通常填 0BL是显示属性比如0x07是黑底白字AL0或AL1时字符串每个字符只占 1 字节AL2或AL3时每个字符后面带一个属性字节AL的奇偶还决定写完是否移动光标AL1和AL3会移动光标AL0和AL2不会我觉得初学者用AL1最省心写完之后光标自动到字符串末尾下次继续输出不会覆盖内容。4.2 封装一个print_string代码比想象中短有了0x13写一个简单的输出函数非常容易。我把它做成一个小型的“引导终端”——设置文本模式、清屏、在指定位置输出字符串org 0x7c00 start: mov ax, 0 mov ds, ax mov es, ax mov ss, ax mov sp, 0x7c00 ; 设置 80x25 彩色文本模式顺便清屏 mov ax, 0x0003 int 0x10 ; 在第一行第一列输出 line1 mov dh, 0 mov dl, 0 mov bp, line1 mov cx, line1_len call print_string ; 在第二行第十列输出 line2 mov dh, 1 mov dl, 10 mov bp, line2 mov cx, line2_len call print_string halt: cli hlt jmp halt ; 打印 ES:BP 指向、CX 长度的字符串 ; 使用 INT 10h AH0x13AL01 写后移动光标BL0x07 黑底白字 print_string: mov ax, 0x1301 mov bx, 0x0007 ; BH0 页码BL0x07 黑底白字 int 0x10 ret line1 db Hello, OS World!, 0x0d, 0x0a line1_len equ $ - line1 line2 db BIOS INT 10h works., 0x0d, 0x0a line2_len equ $ - line2 times 510-($-$$) db 0 dw 0xaa55print_string函数只做了一件事把AX设为0x1301、BX设为0x0007然后触发INT 10h。行号和列号在调用前通过DH/DL传入字符串地址和长度通过BP/CX传入。这段代码编译运行后屏幕上第一行会显示Hello, OS World!第二行缩进 10 列显示BIOS INT 10h works.。有一个细节值得注意ES:BP指向字符串所以我在start一开始就把ES清零了。如果你在其他地方改了ES记得在这里重新设置否则字符串地址会指错。4.3 调试技巧QEMU 监视器和 Bochs 断点怎么配合屏幕上有输出之后下一步就是学会怎么“看”代码在干什么。QEMU 有一个很实用的功能运行中按CtrlAlt2切到监视器monitor按CtrlAlt1切回窗口。在监视器里输入info registers可以看到所有寄存器的当前值输入x /10bx 0x7c00可以查看内存内容还可以用si单步执行汇编指令。如果你需要更精细的调试比如想在INT 0x10调用前停下来看看参数可以上 Bochs。Bochs 自带调试器能直接查看中断向量表、设置指令断点、观察每次中断调用。不过 Bochs 的配置比 QEMU 麻烦一点需要写一个bochsrc文件。我的建议是先用 QEMU 跑通流程实在要追一个莫名其妙的问题再切 Bochs没必要一上来就在调试器上花太多时间。5. 新人最常踩的坑排查指南与防坑清单5.1 QEMU 黑屏没输出的排查顺序如果你照着上面的代码敲完QEMU 窗口一片黑那大概率不是INT 10h的问题而是更前面的引导链路出了问题。我踩过几次总结出这样一个排查顺序先确认编译命令。nasm -f bin有没有写如果写成了-f elf生成的是 ELF 文件不是裸二进制QEMU 加载进去自然什么都不对。确认文件确实是 512 字节。用xxd boot.bin | tail -1看一下最后两字节是不是55 aa。注意小端序内存里是55 AA但在十六进制文件里看到的是aa 55。确认 QEMU 命令没写错。直接用qemu-system-i386 -drive formatraw,fileboot.bin最稳避免-fda之类的参数歧义。确认org 0x7c00没漏。漏掉它标签跳转全乱程序大概率直接跑飞。确认栈初始化了。没有栈INT 10h一进一出返回地址都存在非法位置程序直接炸。这套顺序能解决九成的“屏幕没输出”。5.2 引导扇区大小和签名的坑times 510-($-$$) db 0这行代码新手容易误解。它的作用是从当前代码结束的位置往后填充510 - (当前位置 - 文件开头)个字节的0直到文件总长度正好到 512 字节前 2 字节的位置。如果你的代码超过了 510 字节NASM 会报错提示你填充数量是负数。这时候不是去删times指令而是要想办法精简代码或者把引导扇区做成多级加载器。dw 0xaa55也别写反。dw表示一个 16 位数据x86 采用小端序存储所以内存里的字节顺序是55 AA。如果你写dw 0x55aa效果其实一样因为0x55aa的小端序就是aa 55这是正的。真正要避免的是混淆db 0x55, 0xaa和dw 0xaa55前者在内存里是55 AA后者也是二者等价但别写成dw 0xaa55然后又期待内存里是AA 55。5.3 实模式编程的几个隐性规则写实模式代码有几个规则文档里不太会明说但实际每一条都会坑到人。第一不要假设INT调用不会破坏寄存器。BIOS 中断服务程序内部会用哪些寄存器不同实现不一样。安全起见你自己的关键数据要优先放变量或者栈里而不是放在BX、CX这种通用寄存器里。尤其是字符串地址最好每次调用前重新加载。第二段寄存器的状态不要猜。有些 BIOS 会在自检后把DS设成不同的值所以引导代码开头统一清零段寄存器是最稳妥的。我自己常写的固定动作就是DS/ES/SS清零SP设到0x7C00。第三能停在实模式就别做多余的事。别在引导代码里尝试切换到保护模式又切回来纯粹给自己找麻烦。引导阶段的任务是验证链路、加载内核保持简单。5.4 常见问题速查症状大概率原因解决办法QEMU 窗口全黑、直接退出boot.bin 不是合法引导扇区检查 512 字节和 55AA 签名有字符但全是乱码段寄存器值不对在代码开头重新清零 DS/ES字符显示了一部分就卡死字符串没有 0 结尾给msg末尾补0运行后 QEMU 不断重启栈没初始化导致 INT 返回地址丢失初始化SS:SP编译报times负值代码超过 510 字节精简代码或改用多级引导真机上屏幕显示异常不同 BIOS 的INT 10h实现差异用更基础的0x0E功能或转显存直写这个表基本覆盖了新手前两周会遇到的绝大部分问题。写引导扇区这一个月我的体会是这一课表面在讲“屏幕显示”实际上是在练“如何在极端受限的环境里写可靠的代码”。512 字节的上限实模式 1MB 的地址空间BIOS 中断那套固定的调用约定每一条都是约束也都是让你养成好习惯的契机。等你后面写自己的 VGA 文本模式驱动时回看这段用INT 10h输出的字符串就会明白当初为什么要在设备抽象层上花那么多心思。别嫌这一步慢这是整个操作系统内核从混沌走向有序的必经之路。
RELATED

相关推荐

轻量级大模型本地部署实战:从框架选型到性能调优

轻量级大模型本地部署实战:从框架选型到性能调优

1. 新模型发布的背后:为什么"小体积高智商"成了新方向 前阵子社区里又炸开锅了,一款代号里带"Banana"的第二代轻量模型发布,参数规模不大,跑分却不输给前一代的旗舰版本。我在第一时间拉完技术报告和实测数据…

📅 2026/10/23 8:11:59
NLP训练前必做的5大文本数据分析方法

NLP训练前必做的5大文本数据分析方法

很多朋友训练NLP模型时,最容易犯的一个错误就是“盲人摸象”——数据拿到手,简单看一眼样例,洗一洗,直接就扔进模型里开始训练。我以前也这么干过,结果模型效果差得莫名其妙,折腾了很久才发现问题根本不在模…

📅 2026/10/23 8:06:56
LLaMA-Factory实战指南:个人电脑也能玩转大模型微调

LLaMA-Factory实战指南:个人电脑也能玩转大模型微调

最近总有人跟我聊起大模型微调,第一反应都是“得几千张显卡吧”。其实在开源工具发展到今天的阶段,微调门槛已经低到个人电脑就能试着跑一版。LLaMA-Factory就是这类工具里绕不开的一个名字,它把LoRA、QLoRA、全参微调这些概念封装成了直观的…

📅 2026/10/23 8:06:56
MORE NEWS

更多资讯

📰

Qwen-Image-2.1云端GPU部署保姆级教程:从环境搭建到FastAPI接口封装

先说结论:Qwen-Image-2.1 这类开源图像生成模型,放在云端跑远比本地舒服。之前我在自己显卡上试过一版,效果确实能打,但只要把分辨率往上抬一点、连续生成几张图,显存就见底,风扇声音跟着起飞。后来换了台云…

📰

AI写Rust为何总卡壳?所有权与生命周期正在考验大模型

最近我在做一个跟网络请求收集中转相关的小模块,图省事直接用AI助手写了第一版。同样的任务,Python一遍过,改两个参数就跑起来了;换成Rust,同一个工具、同样的认真程度,第一段代码抛出来的借用检查错误直接…

📰

MySQL三条版本线选型与Linux安装避坑指南

1. 先把三条版本线说清楚,别在起跑线选错方向MySQL版本怎么选,这个问题在技术群里出现的频率,几乎和"为什么我的连接超时了"一样高。很多项目出问题,不是SQL写得烂,也不是索引建得差,而是从装上的…

📰

备赛“凯云杯”,想拿奖请先看完这套免费视频

这段时间,我们收到了不少参赛同学的留言:“今年凯云杯改用ETest5了,有没有教学视频?”“第一次用Python写测试脚本,该从哪学起?”今天统一回复:安排上了。ETest5_5.2.0版本功能介绍合集&#xf…

📰

国产测试的“西安时刻”,凯云携ETest5与大赛同款教学实验箱亮相古都

10月15日,2026国产测试技术与AI赋能方案大会西安站在西安高新区喜来登酒店二层成功举办。本次大会以 “自主创新・多厂商协同・系统级落地” 为主题,今年已先后走进多座城市,西安是聚焦装备、航空与试验测试方向的西部核心站。作为国家级专精…

📰

移动端脚本远程调试:ADB、端口与懒人精灵ROOT版

手机丢在家里跑一个要连挂七八个小时的采集脚本,人在公司盯着屏幕,想改一行判断逻辑就得等晚上回去——这种憋屈感,做过移动端自动化脚本的人多少都体验过。懒人精灵、Auto.js、AutoX.js 这类工具在本地跑得好好的,一旦手机和电脑…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬