4. 实现VGA字符输出函数
任务目标
在BIOS开发环境中,实现两个核心字符输出函数:putchar(输出单个字符到VGA屏幕)和printf(格式化字符串输出)。本案例聚焦于16位实模式下的VGA文本模式操作,直接操作显存地址0xB8000。
环境与依赖
- NASM汇编器(用于编译启动扇区)
- QEMU虚拟机(用于运行验证)
- 目标平台:x86 16位实模式,VGA文本模式(80×25,黑底白字)
核心实现
以下代码包含完整的putchar和printf实现。光标位置通过BIOS中断INT 0x10维护,字符写入直接映射到显存。格式化支持%d(十进制)、%x(十六进制)、%s(字符串)和%c(字符)。
;------------------------------------------
; VGA字符输出函数 - 完整实现
; 汇编:nasm -f bin vga_print.asm -o vga_print.bin
; 运行:qemu-system-x86_64 -drive format=raw,file=vga_print.bin
;------------------------------------------
BITS 16
ORG 0x7C00
start:
; 初始化段寄存器
xor ax, ax
mov ds, ax
mov es, ax
; 清屏(通过滚动窗口实现)
mov ax, 0x0600
mov bh, 0x07 ; 黑底白字
xor cx, cx
mov dx, 0x184F ; 80x25
int 0x10
; 设置光标到起始位置
mov ah, 0x02
xor bh, bh ; 页码0
xor dx, dx ; 行0列0
int 0x10
; 测试printf
mov si, msg_hello
call printf
mov si, msg_num
call printf
mov ax, 12345
push ax
mov si, fmt_dec
call printf
add sp, 2
mov si, msg_hex
call printf
mov ax, 0xABCD
push ax
mov si, fmt_hex
call printf
add sp, 2
; 死循环
cli
hlt
;------------------------------------------
; putchar - 输出单个字符到VGA
; 输入:AL = 字符ASCII码
; 破坏:AH, BX, CX, DX
;------------------------------------------
putchar:
pusha
; 获取当前光标位置
mov ah, 0x03
xor bh, bh
int 0x10 ; DH=行, DL=列
; 处理换行符
cmp al, 0x0A
je .newline
cmp al, 0x0D
je .carriage_return
; 写入字符到显存
push ax
mov ax, 0xB800
mov es, ax
pop ax
; 计算显存偏移 = (行*80 + 列) * 2
movzx bx, dh
imul bx, bx, 80
movzx cx, dl
add bx, cx
shl bx, 1
mov ah, 0x07 ; 属性:黑底白字
mov [es:bx], ax
; 光标右移
inc dl
cmp dl, 80
jb .set_cursor
mov dl, 0
inc dh
jmp .set_cursor
.newline:
inc dh
mov dl, 0
jmp .set_cursor
.carriage_return:
mov dl, 0
jmp .set_cursor
.set_cursor:
; 检查是否需要滚动
cmp dh, 25
jb .update_cursor
call scroll_screen
mov dh, 24
.update_cursor:
mov ah, 0x02
xor bh, bh
int 0x10
popa
ret
;------------------------------------------
; scroll_screen - 向上滚动一行
;------------------------------------------
scroll_screen:
pusha
mov ax, 0x0601 ; AL=1 表示滚动一行
mov bh, 0x07
xor cx, cx
mov dx, 0x184F
int 0x10
popa
ret
;------------------------------------------
; printf - 格式化输出
; 输入:SI = 格式字符串地址
; 参数通过栈传递(从右到左压栈)
; 支持:%d(十进制), %x(十六进制), %s(字符串), %c(字符)
;------------------------------------------
printf:
pusha
mov bp, sp
add bp, 18 ; 跳过pusha的8个寄存器+返回地址
.next_char:
lodsb ; 加载字符到AL
test al, al
jz .done
cmp al, '%'
jne .print_char
; 处理格式说明符
lodsb
cmp al, 'd'
je .format_dec
cmp al, 'x'
je .format_hex
cmp al, 's'
je .format_str
cmp al, 'c'
je .format_char
jmp .print_char
.format_dec:
mov ax, [bp] ; 获取参数
add bp, 2
call print_dec
jmp .next_char
.format_hex:
mov ax, [bp]
add bp, 2
call print_hex
jmp .next_char
.format_str:
mov si, [bp] ; 字符串指针
add bp, 2
call print_str
jmp .next_char
.format_char:
mov al, [bp]
add bp, 2
call putchar
jmp .next_char
.print_char:
call putchar
jmp .next_char
.done:
popa
ret
;------------------------------------------
; print_dec - 输出十进制数
; 输入:AX = 数值
;------------------------------------------
print_dec:
pusha
mov cx, 0 ; 数字计数
mov bx, 10
.div_loop:
xor dx, dx
div bx
push dx ; 保存余数
inc cx
test ax, ax
jnz .div_loop
.print_loop:
pop ax
add al, '0'
call putchar
loop .print_loop
popa
ret
;------------------------------------------
; print_hex - 输出十六进制数
; 输入:AX = 数值
;------------------------------------------
print_hex:
pusha
mov cx, 4 ; 输出4个十六进制位
.hex_loop:
rol ax, 4
push ax
and al, 0x0F
cmp al, 10
jb .digit
add al, 'A' - 10
jmp .print
.digit:
add al, '0'
.print:
call putchar
pop ax
loop .hex_loop
popa
ret
;------------------------------------------
; print_str - 输出字符串
; 输入:SI = 字符串地址
;------------------------------------------
print_str:
pusha
.str_loop:
lodsb
test al, al
jz .done
call putchar
jmp .str_loop
.done:
popa
ret
;------------------------------------------
; 数据段
;------------------------------------------
msg_hello db 'Hello from VGA!', 0x0D, 0x0A, 0
msg_num db 'Decimal: ', 0
msg_hex db 'Hex: 0x', 0
fmt_dec db '%d', 0x0D, 0x0A, 0
fmt_hex db '%x', 0x0D, 0x0A, 0
times 510-($-$$) db 0
dw 0xAA55
编译与运行
- 保存代码为
vga_print.asm - 编译:
nasm -f bin vga_print.asm -o vga_print.bin - 运行:
qemu-system-x86_64 -drive format=raw,file=vga_print.bin
验证方法
QEMU窗口应显示以下内容(预期输出,由代码逻辑直接推出):
Hello from VGA!
Decimal: 12345
Hex: 0xABCD
验证通过条件:
- 三行文本正确显示在QEMU屏幕左上角
- 十进制数
12345正确转换并显示 - 十六进制数
0xABCD以大写字母显示 - 光标停留在最后一行末尾
关键说明
- 光标位置通过BIOS中断
INT 0x10的AH=0x03(读取)和AH=0x02(设置)维护,确保字符写入位置正确 - 显存偏移计算公式:
(行号 × 80 + 列号) × 2,每个字符占2字节(低字节为ASCII码,高字节为颜色属性) - 参数传递约定:
printf从栈中按从右到左的顺序取参数,调用前需将参数压栈,调用后由调用者清理栈 - 换行处理:
0x0A(LF)使行号加1并回到列0;0x0D(CR)仅回到列0