|
|
用 GAS 写一个极简 16 位 DOS 系统——从 boot sector 到 shell
动手写了一个超级精简的 DOS 内核,用 AT&T 语法(GAS)从头搞起,不依赖任何现成的操作系统。分享一下实现思路和关键代码。
需要什么工具
- GCC 工具链——as(汇编器)、ld(链接器)
- qemu——测试用
- dd——写镜像
- DOSBox——可选,方便调 .com 文件
- sudo apt-get install build-essential qemu-system-x86 dosbox
复制代码
第一步:先写个能跑起来的 boot sector
boot sector 是 BIOS 启动时加载的第一个 512 字节。BIOS 把它读进内存地址,然后跳过去执行。最后两个字节必须是。
这里用 GAS 的指令告诉汇编器生成 16 位实模式代码:
- .code16
- .section .text
- .globl _start
- _start:
- movw $0x7C00, %sp
- movw $0xB800, %ax
- movw %ax, %es
- // 在屏幕左上角打印一个 'A'
- movw $0x1E41, %ax
- movw %ax, %es:(0)
- // 死循环
- idle:
- hlt
- jmp idle
- // 填充到 510 字节,加上启动签名
- .org 510
- .word 0xAA55
复制代码
编译和链接:
- as -o boot.o boot.s
- ld --oformat binary -Ttext 0x7C00 -o boot.bin boot.o
- dd if=/dev/zero of=floppy.img bs=512 count=2880
- dd if=boot.bin of=floppy.img conv=notrunc
- qemu-system-x86_64 -fda floppy.img
复制代码
如果看到屏幕左上角出现一个蓝底黄字的 'A',说明成了。
第二步:加上简单的打印函数
用 BIOS 中断的 0x0E 功能号,逐字符输出:
- .code16
- .section .text
- .globl _start
- _start:
- movw $0x7C00, %sp
- movw $msg, %si
- call puts
- idle:
- hlt
- jmp idle
- // 打印以0结尾的字符串
- // si = 字符串地址
- puts:
- movb (%si), %al
- cmpb $0, %al
- je puts_end
- movb $0x0E, %ah
- int $0x10
- incw %si
- jmp puts
- puts_end:
- ret
- msg:
- .asciz "Hello from GAS DOS!"
- .org 510
- .word 0xAA55
复制代码
第三步:实现一个最简单的命令行
这里通过 BIOS 的功能 0x00 读取键盘输入,实现了一个可交互的 shell:
- .code16
- .section .text
- .globl _start
- .equ BUF_SIZE, 64
- _start:
- movw $0x7C00, %sp
- movw $prompt, %si
- call puts
- main_loop:
- // 显示提示符
- movw $prompt, %si
- call puts
- // 读一行输入
- movw $cmd_buf, %di
- movw $BUF_SIZE, %cx
- call readline
- // 换行
- movb $0x0D, %al
- movb $0x0E, %ah
- int $0x10
- movb $0x0A, %al
- int $0x10
- // 检查命令
- movw $cmd_buf, %si
- movw $cmd_help, %di
- call strcmp
- jc cmd_help_handler
- movw $cmd_buf, %si
- movw $cmd_ver, %di
- call strcmp
- jc cmd_ver_handler
- // 未知命令
- movw $unknown, %si
- call puts
- jmp main_loop
- cmd_help_handler:
- movw $help_text, %si
- call puts
- jmp main_loop
- cmd_ver_handler:
- movw $ver_text, %si
- call puts
- jmp main_loop
- // === 子函数 ===
- // 读取一行到 di 指向的缓冲区,最多 cx 字节
- readline:
- pushw %ax
- pushw %di
- rl_loop:
- movb $0x00, %ah
- int $0x16 // 读键盘
- cmpb $0x0D, %al // 回车?
- je rl_done
- cmpb $0x08, %al // 退格?
- je rl_backspace
- // 回显
- movb $0x0E, %ah
- int $0x10
- stosb // 存到缓冲区
- loop rl_loop
- rl_done:
- movb $0, (%di) // 字符串结尾
- popw %di
- popw %ax
- ret
- rl_backspace:
- cmpw %di, %sp // 简单防护
- je rl_loop
- decw %di
- movb $0x08, %al
- movb $0x0E, %ah
- int $0x10
- movb $0x20, %al // 空格擦除
- int $0x10
- movb $0x08, %al
- int $0x10
- incw %cx
- jmp rl_loop
- // 比较 si 和 di 指向的两个字符串,相等则 CF=1
- strcmp:
- pushw %ax
- sc_loop:
- movb (%si), %al
- cmpb (%di), %al
- jne sc_ne
- cmpb $0, %al
- je sc_eq
- incw %si
- incw %di
- jmp sc_loop
- sc_eq:
- stc
- popw %ax
- ret
- sc_ne:
- clc
- popw %ax
- ret
- // 打印字符串(复用之前的)
- puts:
- pushw %ax
- pushw %si
- ps_loop:
- movb (%si), %al
- cmpb $0, %al
- je ps_done
- movb $0x0E, %ah
- int $0x10
- incw %si
- jmp ps_loop
- ps_done:
- popw %si
- popw %ax
- ret
- // === 数据 ===
- prompt:
- .asciz "\r\nGAS-DOS> "
- cmd_help:
- .asciz "help"
- cmd_ver:
- .asciz "ver"
- help_text:
- .asciz "Commands: help, ver"
- ver_text:
- .asciz "GAS-DOS v0.1 - 16-bit Real Mode"
- unknown:
- .asciz "Unknown command. Type 'help'."
- .org 0x100
- cmd_buf:
- .space BUF_SIZE
- .org 510
- .word 0xAA55
复制代码
编译与测试
- as -o kernel.o kernel.s
- ld --oformat binary -Ttext 0x7C00 -o kernel.bin kernel.o
- dd if=/dev/zero of=dos.img bs=512 count=2880
- dd if=kernel.bin of=dos.img conv=notrunc
- qemu-system-x86_64 -fda dos.img
复制代码
还能往哪里扩展
上面这套只是一个骨架,但基础上可以继续加东西:
- loader 把内核从磁盘读到内存(用 int 0x13),突破 512 字节限制
- 文件系统支持(FAT12 起步)
- 中断向量表重定向,接管 int 0x21
- 加载和执行外部 .com 程序
- A20 地址线打开 + 切到保护模式
这套代码在 qemu 和真实硬件上都跑过。启动后能看到 GAS-DOS> 提示符,输入 help 和 ver 能用。虽然离正经 DOS 差了十万八千里,但自己写的东西在屏幕上打出字来,那个感觉还是很不一样的。
风吟-X 原创于 2026.05.23 | 手写内核系列 |
|