实模式下的“Hello World”
|
|
nasm -f bin boot.asm -o boot.bin
qemu-system-i386 -drive format=raw,file=boot.bin
mov ah, 0x00
mov al, 0x03
int 0x10
具有清屏效果,不加的话 qemu运行会多出如下图部分
org 0x7C00
指令解析
- 汇编器默认假设程序从地址
0x0000
开始汇编 org 0x7C00
用于显式指定代码的实际加载地址是内存中的0x7C00
- 设置此指令后,所有标签、偏移量和地址引用都会以
0x7C00
为基准计算,从而确保程序在内存中能正常运行
简而言之:这是告诉汇编器“代码虽然写在文件开头,但将来运行时会被放在 0x7C00,因此请按这个地址来排布和计算”
$$
和 $
NASM 汇编中,$$
表示当前段(section)的起始地址(不是 CPU 的段寄存器概念)
对于没有显式分段的简单引导程序,$$
默认就是整个程序的起始地址
$
表示当前行的地址(即当前指令/数据的汇编地址)
$-$$
的实际意义
-
计算的是:从程序开始到当前位置的字节数 因为
$$
是程序起点,$
是当前位置,所以$-$$
就是已生成的二进制代码的字节数 -
示例: 假设代码从
org 0x7C00
开始,前 3 条指令共占用 5 字节:0x7C00: mov ah, 0x00 (2字节) 0x7C02: mov al, 0x03 (2字节) 0x7C04: int 0x10 (1字节)
当汇编到
int 0x10
时,$ = 0x7C05
,$$ = 0x7C00
,所以$-$$ = 5
(已用 5 字节)
times 510-($-$$) db 0
的作用
-
填充至 510 字节
引导扇区必须是 512 字节,最后 2 字节是签名
0xAA55
,因此前 510 字节需要填满代码和数据 -
动态计算填充量
510-($-$$)
会根据已生成的代码长度,自动计算需要填充的 0 的个数例如:若已用 30 字节,则填充
510-30=480
个0
x86 引导扇区开发注意事项
基本限制
-
512字节大小
传统MBR必须严格限制在512字节内
-
签名要求
最后两个字节必须是
0x55
和0xAA
-
执行位置
BIOS会将引导扇区加载到
0x7C00
内存地址
开发注意事项
-
CPU模式
启动时CPU处于16位实模式
-
寄存器初始值
BIOS执行后DL寄存器包含启动驱动器号
-
堆栈设置
需要自行设置堆栈(通常SS:SP = 0x0000:0x7C00)
-
内存布局
0x0000-0x03FF是中断向量表,0x0040-0x005F是BIOS数据区
代码组织
-
ORG指令
需要使用
ORG 0x7C00
告诉汇编器代码加载位置 -
引导标志
分区表中通常需要设置活动分区标志(0x80)
-
分区表
如果需要分区表,它占用MBR的最后64字节(偏移0x1BE-0x1FD)
BIOS 中断
中断调用规范
通用调用步骤
- 设置功能号:
AH
寄存器 - 设置参数:其他寄存器
- 调用中断:
INT xxh
- 检查返回:通常 CF=0 成功,CF=1 失败
寄存器使用约定
寄存器 | 典型用途 |
---|---|
AH | 功能号 |
AL | 数据/子功能 |
BX | 缓冲区偏移/通用 |
CX | 计数/通用 |
DX | 端口/通用 |
ES:BX | 缓冲区指针 |
CF | 进位标志(错误指示) |
中断技术
中断向量表修改
cli ; 禁用中断
mov ax, 0
mov es, ax ; ES=0
mov word [es:10h*4], new_handler ; 设置IP
mov word [es:10h*4+2], cs ; 设置CS
sti ; 启用中断
中断链式调用
original_int dd 0
; 保存原中断
mov ax, [es:10h*4]
mov [original_int], ax
mov ax, [es:10h*4+2]
mov [original_int+2], ax
; 在自定义处理程序中
my_handler:
pushf
call far [original_int] ; 调用原中断
...
注意事项
- 实模式限制:BIOS 中断只能在实模式下使用
- 寄存器保存:中断处理程序必须保存和恢复所有使用的寄存器
- 速度问题:BIOS 调用比直接硬件访问慢
- 兼容性:不同 BIOS 实现可能有差异
INT 19h:启动加载中断
传统 BIOS 系统中,int 0x19
通常由 BIOS 在 POST 完成后自动调用,以启动操作系统的引导过程
int 0x19
是 BIOS 提供的 “Bootstrap Loader” 中断
INT 19h 的功能:
- 按启动顺序尝试读取启动设备第一个扇区(512 字节)
- 将其加载到内存地址
0x0000:0x7C00
(即物理地址0x07C00
) - 然后跳转执行
0x0000:0x7C00
的代码(即 MBR)
你写的 bootloader 就是放在这里运行的!
INT 10h:视频服务
功能设置方式:AH
指定功能,其他寄存器传递参数
AH | 功能 | 参数 | 返回值 |
---|---|---|---|
00h | 设置显示模式 | AL=模式号 | 无 |
01h | 设置光标形状 | CH=起始扫描线, CL=结束扫描线 | 无 |
02h | 设置光标位置 | BH=页号 DH=行, DL=列 |
无 |
03h | 获取光标位置 | BH=页号 | CH/CL=光标形状 DH/DL=位置 |
0Eh | 电传输出字符 | AL=字符 BH=页号, BL=颜色(图形模式) |
无 |
13h | 写字符串 | AL=模式 BH=页号, BL=颜色 CX=长度 DH/DL=行/列 ES:BP=字符串指针 |
无 |
示例
mov ah, 0Eh ; 电传输出功能
mov al, 'A' ; 要显示的字符
mov bh, 0 ; 显示页号
int 10h ; 调用视频中断
◉ 设置文本显示模式
mov ah, 0x00 ; 功能号 0x00 = 设置显示模式
mov al, 0x03 ; 模式参数 0x03 = 80x25 16色文本模式
int 0x10 ; 调用BIOS视频中断
功能详解
AH=0x00
BIOS 中断INT 0x10
的功能号,表示"设置显示模式"AL=0x03
指定具体的显示模式:- 0x03:经典的 80列×25行 文本模式,支持 16色 (DOS时代的标准控制台模式)
- 实际效果
- 清空屏幕
- 重置光标到左上角(0,0)
- 启用文本模式(非图形模式)
- 恢复默认配色(黑底灰字)
常见应用场景
-
系统启动时初始化显示
(许多Bootloader会首先调用此功能确保显示正常)
-
程序退出时恢复屏幕状态
(避免程序运行后遗留乱码)
-
清除屏幕内容
(比逐字符输出空格更高效)
其他常用显示模式(AL值)
模式 | 分辨率 | 类型 | 备注 |
---|---|---|---|
0x03 | 80×25 | 文本 | 最常用 |
0x12 | 640×480 | 图形 | 16色 VGA |
0x13 | 320×200 | 图形 | 256色 “Mode X” |
注意事项
- 实模式下有效(现代操作系统已不再使用BIOS中断)
- 某些虚拟机/模拟器可能不支持非标准显示模式
- 图形模式下(如0x13)需要额外设置显存操作
◉ AH = 0x0E:Teletype Output
- mov ah, 0x0E
- 功能:在屏幕上打印一个字符,并自动移动光标(类似老式打字机)
- 特点:
- 支持 ASCII 字符(
AL
寄存器存放字符) - 自动处理 换行(
\n
)、退格(\b
) 等控制字符 - 光标会跟随移动(类似现代终端)
- 支持 ASCII 字符(
-
mov al, ‘X’
-
AL 寄存器存放要打印的字符,这里传入
'X'
(ASCII 码0x58
) -
可以替换为任意 ASCII 字符,例如:
mov al, 'A' ; 打印 'A' mov al, 0x41 ; 同上(ASCII 'A' = 0x41) mov al, 10 ; 打印换行(\n) mov al, 13 ; 打印回车(\r,回到行首)
关键细节
- 字符颜色和属性
- 默认颜色:白字黑底(取决于 BIOS 设置)
- 如果要修改颜色,可以用
INT 0x10
的其他功能(如AH=0x09
写字符和属性)
- 光标位置
-
每次调用
AH=0x0E
后,光标自动右移 -
可以用
AH=0x02
手动设置光标位置:mov ah, 0x02 mov bh, 0 ; 页号(通常 0) mov dh, 12 ; 行(0~24) mov dl, 40 ; 列(0~79) int 0x10
- 换行处理
-
\n
(ASCII 10)只会换行,不回车 -
通常需要结合
\r
(ASCII 13)实现完整换行:mov ah, 0x0E mov al, 13 ; 回车(回到行首) int 0x10 mov al, 10 ; 换行(下移一行) int 0x10
-
打印字符串
[org 0x7C00] ; 引导程序加载地址
[bits 16]
start:
xor ax,ax
mov ds,ax
mov si, hello ; DS:SI 指向字符串
call print_string
sti
idle_loop:
hlt
jmp idle_loop
print_string:
mov ah, 0x0E
.next_char:
lodsb ; 取 DS:SI 指向的字节放入 AL,并 SI++
cmp al, 0
je .done
int 0x10
jmp .next_char
.done:
ret
hello db "hello,world!", 0
times 510-($-$$) db 0
dw 0xAA55
[org 0x7C00]
[bits 16]
start:
mov si,0
print:
mov ah,0x0e
mov al,[hello+si]
int 0x10
add si,1
cmp byte [hello+si] ,0
jne print
sti
idle_loop:
hlt
jmp idle_loop
hello:
db "hello,world!",0
times 510-($-$$) db 0
dw 0xAA55
区别在于 字符串遍历方式 和 代码结构
-
字符串遍历方式
第一段代码(
lodsb
+call
结构)
-
使用
lodsb
指令:lodsb
(Load String Byte)会自动从DS:SI
读取一个字节到AL
,并递增SI
- 更简洁,不需要手动计算偏移量
-
封装成
call
子程序:- 可复用,适合多次打印不同字符串
第二段代码(手动索引
[hello+si]
) -
手动计算字符串地址:
- 通过
[hello+si]
直接访问字符串的每个字符 - 需要手动
add si, 1
递增索引
- 通过
-
直接内联代码:
- 没有封装成子程序,适合简单的一次性打印
-
代码结构
第一段代码(结构化,可复用)
-
使用
call print_string
调用子程序,适合更复杂的程序 -
字符串
message
可以定义在任意位置,只要DS:SI
指向它即可
第二段代码(直接内联,简单) -
直接在
print
循环里完成所有操作,适合短小代码 -
字符串
hello
必须紧邻代码,因为[hello+si]
是硬编码偏移
INT 13h:磁盘服务
AH | 功能 | 参数 | 返回值 |
---|---|---|---|
00h | 复位磁盘 | DL=驱动器号(80h=主硬盘,81h=从硬盘) | CF=0成功,CF=1失败;AH=状态码 |
01h | 获取磁盘状态 | DL=驱动器号 | AH=上次操作的状态码(00h=无错误) |
02h | 读扇区(CHS) | AL=扇区数 CH=柱面(低8位),CL=扇区(位0-5)|柱面高2位(位6-7) DH=磁头,DL=驱动器 ES:BX=数据缓冲区 | CF=状态;AH=错误码,AL=实际读取扇区数 |
03h | 写扇区(CHS) | 同02h | 同02h |
04h | 校验扇区(CHS) | 同02h(无数据缓冲区) | CF=状态;AH=错误码 |
05h | 格式化磁道(CHS) | AL=交错值(通常为0) CH/DH/CL/DL=同02h ES:BX=格式化参数表地址 | CF=状态;AH=错误码 |
08h | 获取驱动器参数 | DL=驱动器号 | CF=状态; CH=最大柱面低8位,CL=扇区数(位0-5)|柱面高2位(位6-7) DH=最大磁头数,DL=驱动器总数 ES:DI=磁盘参数表地址(部分BIOS) |
09h | 初始化驱动器参数 | DL=驱动器号 | CF=状态;AH=错误码 |
0Ch | 寻道(CHS) | CH/CL=柱面,DH=磁头,DL=驱动器 | CF=状态;AH=错误码 |
0Dh | 复位磁盘控制器 | DL=驱动器号 | CF=状态;AH=错误码 |
15h | 检测LBA支持(EDD) | DL=驱动器号 | CF=状态; AH=扩展类型(0=不支持,1=EDD 1.0,2=EDD 3.0) CX:DX=总扇区数(若支持) |
41h | 检查EDD扩展是否安装 | BX=55AAh,DL=驱动器号 | CF=状态; BX=AA55h(若支持),AH=扩展版本(01h=1.x,20h=2.0+) C=1支持EDD |
42h | 读扇区(LBA) | DL=驱动器号 DS:SI=磁盘地址包(DAP)指针 | CF=状态;AH=错误码 |
43h | 写扇区(LBA) | 同42h | 同42h |
44h | 校验扇区(LBA) | 同42h(无数据缓冲区) | CF=状态;AH=错误码 |
45h | 锁定/解锁驱动器 | DL=驱动器号 | CF=状态;AH=错误码 |
46h | 弹出可移动介质 | DL=驱动器号 | CF=状态;AH=错误码 |
47h | 扩展寻道(LBA) | DL=驱动器号 DS:SI=DAP(仅需LBA地址) | CF=状态;AH=错误码 |
48h | 获取扩展驱动器参数 | DL=驱动器号 DS:SI=返回缓冲区地址 | CF=状态; 缓冲区包含:LBA总扇区数、物理扇区大小等信息 |
示例
mov ah, 02h ; 读磁盘功能
mov al, 1 ; 读取1个扇区
mov ch, 0 ; 柱面0
mov cl, 2 ; 扇区2
mov dh, 0 ; 磁头0
mov dl, 80h ; 硬盘0
mov bx, 0x7E00 ; 读取到内存0x7E00处
int 13h ; 调用磁盘中断
jc error ; 如果出错(CF=1)跳转
◉ AH = 0x02:读扇区
📘 参数要求(使用 CHS 模式):
寄存器 | 含义 |
---|---|
AH = 0x02 | 功能号:读取扇区 |
AL | 要读的扇区数(最多 128) |
CH | 柱面号低 8 位 |
CL | bits 0–5 = 扇区号(1–63)bits 6–7 + CH = 柱面高 2 位(合成 10 位柱面) |
DH | 磁头号 |
DL | 驱动器号(0x80=第一个硬盘) |
ES:BX | 数据缓冲区地址(目标内存) |
📗 返回:
- CF 清除 → 成功
- CF 设置 → 错误,AH 为错误代码
🚧 BIOS CHS 模式的局限
- 扇区号最大只能是 63(6 位)
- 磁头号最大只能是 255(8 位)
- 柱面号最大只能是 1023(10 位)
所以最大容量 ≈ 1024×256×63×512B ≈ 8.4GB
⚠️ 这就是为什么老旧 BIOS 无法启动大于 8.4GB 的硬盘
📚 想读取多个扇区怎么办?
比如你想读取第 2~4 个扇区,需要计算对应的 CHS 坐标并多次调用 INT 13h 或调整 AL 为多扇区读取
注意:不能跨越柱面/磁头边界,否则需重新设置 CH/DH/CL
关键功能说明
-
LBA扩展功能(AH=42h-48h):
-
需通过 DAP(Disk Address Packet) 传递参数,结构如下:
DAP STRUCT db 10h ; 包大小(16字节) db 0 ; 保留 dw 扇区数 ; 读取/写入的扇区数 dd 缓冲区地址 ; 内存缓冲区(32位段:偏移) dq LBA地址 ; 64位LBA起始扇区号 DAP ENDS
-
突破CHS的 8GB限制,支持大容量磁盘。
-
-
驱动器号约定:
- 00h-7Fh:软盘驱动器(如00h=第一软驱)
- 80h-FFh:硬盘驱动器(如80h=主硬盘,81h=从硬盘)
-
状态码(AH返回值):
- 00h:成功
- 01h:无效命令
- 02h:地址标记未找到
- 03h:写保护
- 04h:扇区未找到
- 80h:驱动器未响应
INT 16h:键盘服务
AH | 功能 | 参数 | 返回值 |
---|---|---|---|
00h | 读取键 | 无 | AH=扫描码, AL=ASCII码 |
01h | 检查键 | 无 | ZF=1(无键), ZF=0(有键) |
02h | 获取键盘状态 | 无 | AL=键盘状态字节 |
示例
mov ah, 00h ; 等待按键
int 16h ; 调用键盘中断
cmp al, 1Bh ; 检查是否按了ESC
je exit ; 如果是则退出
自定义中断向量
[org 0x7C00] ; 引导程序加载地址
[bits 16]
start:
; 设置自定义中断向量
cli ; 关闭中断
mov ax, 0
mov es, ax ; ES=0(中断向量表段址)
mov word [es:0x60*4], my_handler ; 设置IP
mov word [es:0x60*4+2], cs ; 设置CS
sti ; 重新开启中断
; 触发自定义中断测试
int 0x60
sti
idle_loop:
hlt
jmp idle_loop
; 自定义中断处理程序
my_handler:
pusha
push ds
mov ax, cs
mov ds, ax ; 确保DS=CS(方便访问数据)
; 中断处理逻辑(示例:打印字符)
mov ah, 0x0E ; BIOS tele-type功能
mov al, '!'
int 0x10 ; 调用BIOS显示中断
pop ds
popa
iret ; 中断返回
; 引导扇区填充
times 510-($-$$) db 0
dw 0xAA55
中断唤醒
键盘中断唤醒休眠中的CPU
[org 0x7C00] ; 引导程序加载地址
[bits 16]
start:
cli
; 初始化段寄存器
xor ax, ax ; AX = 0
mov ds, ax ; 数据段DS = 0
mov es, ax ; 附加段ES = 0
;===========================================
; 设置键盘中断向量(IRQ1对应INT 0x09)
; 中断向量表位于内存0x0000:0x0000处
; 每个中断向量占4字节(段:偏移)
;===========================================
mov word [0x9*4], keyboard_isr ; 设置中断处理程序的偏移地址
mov word [0x9*4+2], cs ; 设置中断处理程序的段地址(CS)
;===========================================
; 配置8259 PIC(可编程中断控制器)
; 允许键盘中断(IRQ1)
;===========================================
in al, 0x21 ; 读取主PIC的IMR(中断屏蔽寄存器)
and al, 0b11111101 ; 清除第1位(允许IRQ1)
; 0b11111101 = 0xFD
out 0x21, al ; 写回IMR
sti ; 开中断(Enable Interrupts)
;-------------------------------------------------
; 主循环:等待键盘中断
;-------------------------------------------------
wait_for_key:
hlt ; 暂停CPU,直到有中断发生
jmp wait_for_key ; 中断返回后继续等待
;=================================================
; 键盘中断服务例程(ISR)
; 触发条件:当有键盘按键被按下或释放时
; 中断号:INT 0x09(IRQ1)
;=================================================
keyboard_isr:
push ax ; 保存AX寄存器
in al, 0x60 ; 从键盘端口0x60读取扫描码
test al, 0x80 ; 检查扫描码最高位(1=按键释放,0=按键按下)
jnz .skip_print ; 如果是按键释放,跳过打印
; 如果是按键按下,打印字符'K'
mov ah, 0x0E ; BIOS显示功能号(Teletype输出)
mov al, 'K' ; 要显示的字符
int 0x10 ; 调用BIOS视频服务
.skip_print:
; 发送EOI(End Of Interrupt)信号给PIC
mov al, 0x20 ; EOI命令码
out 0x20, al ; 发送到主PIC的命令端口
pop ax ; 恢复AX寄存器
iret ; 中断返回
times 510-($-$$) db 0
dw 0xAA55
显存编程
显存版 “Hello,World”
|
|
分析:
0xb800
是 VGA 彩色文本模式的显存段- 每个字符占两个字节(字符 + 属性),这里只写了字符,属性默认为0(通常是黑白)
运行效果:
增强版:为字符添加颜色属性
通过一次写入 word
(两个字节),我们可以同时设置字符和其属性(如背景蓝、前景黄):
mov ax,0xb800
mov ds,ax
mov word [0x00], 0x1E61 ; 'a'
mov word [0x02], 0x2E73 ; 's'
mov word [0x04], 0x4E6D ; 'm'
sti
idle_loop:
hlt
jmp idle_loop
times 510-($-$$) db 0
db 0x55,0xaa
属性字节结构:高 4 位为背景色,低 4 位为前景色
颜色代码 | 含义 |
---|---|
0x1 | 蓝色背景 |
0xE | 黄色前景 |
显示效果如下:
显示字符串
[org 0x7C00] ; 设置代码起始偏移地址为0x7C00,这是BIOS加载引导扇区的默认地址
; 初始化段寄存器
mov ax, cs ; 将代码段寄存器的值赋给AX
mov ds, ax ; 设置数据段寄存器DS,保证可以正确访问msg字符串
mov ax, 0xB800 ; 文本模式下显存的段地址是0xB800
mov es, ax ; 设置额外段寄存器ES用于写显存(段:偏移方式)
mov si, msg ; SI指向字符串msg的起始地址
mov di, 0x0000 ; DI设置为0,表示显存中的起始偏移位置(左上角)
.next_char:
lodsb ; 从[DS:SI]加载一个字节到AL,并SI自动+1
cmp al, 0 ; 检查是否是字符串结束符
je .done ; 如果是0(null terminator),跳转到.done
mov ah, 0x1E ; 设置字符属性:红底黄字(背景红 0100,前景黄色 1110)
mov [es:di], ax ; 将字符(AL)和属性(AH)组合写入[ES:DI]
add di, 2 ; 显存每个字符占两个字节:字符+属性,故偏移加2
jmp .next_char ; 跳转回继续处理下一个字符
sti
idle_loop:
hlt
jmp idle_loop
; 存放要显示的字符串
msg db 'hello, asm world!', 0 ; 字符串内容,以0结尾
; 填充剩余字节,确保总大小为512字节(引导扇区标准)
times 510-($-$$) db 0 ; 用0填充,直到偏移510
db 0x55, 0xaa ; 引导扇区签名,必须位于末尾两个字节
执行流程
- BIOS 加载引导扇区至
0x7C00
- 设置段寄存器(DS/ES)
- SI指向字符串,DI指向显存起始
- 每个字符配上颜色属性写入显存
- 显示完毕后停机
显存中每个字符占用 2 字节,因此
DI
每次递增 2
显示效果:
分析
段寄存器初始化
; 设置代码段和数据段
mov ax, cs
mov ds, ax ; DS指向代码段,用于读取字符串
; 设置显存段
mov ax, 0xb800
mov es, ax ; ES指向显存段
- mov ax, cs:将代码段寄存器CS的值复制到AX
- mov ds, ax:将DS设置为代码段,这样字符串数据可以被正确访问
- mov ax, 0xb800:0xB800是彩色文本模式显存的段地址
- mov es, ax:将ES设置为显存段,用于写入显示内容
指针初始化
; SI = 字符串指针
mov si, msg
; DI = 显存偏移地址,从 0x0000 开始
mov di, 0x0000
- mov si, msg:SI寄存器指向字符串"hello, asm world!“的起始位置
- mov di, 0x0000:DI寄存器指向显存的起始位置(屏幕左上角)
字符串显示循环
.next_char:
lodsb ; AL = [DS:SI], SI++
cmp al, 0
je .done ; 若遇到字符串结尾(0),跳出
; 颜色:黄色前景(0xE),蓝色背景(0x1) → 0x1E
mov ah, 0x1E ; ah = 属性字节
mov [es:di], ax ; 写入显存:字符+属性,使用ES段
add di, 2 ; 显存指针移到下一个字符位置
jmp .next_char
lodsb:从内存 [DS:SI] 读取一个字节到AL,然后SI自动加1
cmp al, 0:比较AL是否为0(字符串结束符)
je .done:如果AL=0,跳转到.done标签结束循环
mov ah, 0x1E:设置字符属性
0x1E = 0001 1110b
低4位(1110b=0xE):黄色前景
高4位(0001b=0x1):蓝色背景
mov [es:di], ax:将字符(AL)和属性(AH)写入显存
add di, 2:显存中每个字符占用2字节(1字节字符+1字节属性)
jmp .next_char:继续处理下一个字符
数据定义
; 放置字符串(以0结尾)
msg db 'hello, asm world!', 0
msg:字符串标签
db:定义字节数据
0:字符串结束符(null terminator)
总结与应用场景
学到的内容
- BIOS 引导机制和MBR 格式
- 显存操作与字符显示
- 实模式段寄存器管理
- 字符属性控制
- 中断向量表修改
- PIC 编程与中断响应
应用场景
- 嵌入式/低功耗系统:用
hlt
等待中断触发 - 操作系统开发入门:掌握 BIOS 启动、实模式内存管理