查看: 9|回复: 0

用 GAS 写一个极简 16 位 DOS 系统——从 boot sector 到 shell

[复制链接]

142

主题

0

回帖

450

积分

中级会员

积分
450
发表于 2026-5-23 00:50:43 | 显示全部楼层 |阅读模式
用 GAS 写一个极简 16 位 DOS 系统——从 boot sector 到 shell




动手写了一个超级精简的 DOS 内核,用 AT&T 语法(GAS)从头搞起,不依赖任何现成的操作系统。分享一下实现思路和关键代码。




需要什么工具

  • GCC 工具链——as(汇编器)、ld(链接器)
  • qemu——测试用
  • dd——写镜像
  • DOSBox——可选,方便调 .com 文件


  1. sudo apt-get install build-essential qemu-system-x86 dosbox
复制代码





第一步:先写个能跑起来的 boot sector

boot sector 是 BIOS 启动时加载的第一个 512 字节。BIOS 把它读进内存地址
  1. 0x7C00
复制代码
,然后跳过去执行。最后两个字节必须是
  1. 0x55 0xAA
复制代码


这里用 GAS 的
  1. .code16
复制代码
指令告诉汇编器生成 16 位实模式代码:

  1. .code16
  2. .section .text
  3. .globl _start

  4. _start:
  5.     movw $0x7C00, %sp
  6.     movw $0xB800, %ax
  7.     movw %ax, %es

  8.     // 在屏幕左上角打印一个 'A'
  9.     movw $0x1E41, %ax
  10.     movw %ax, %es:(0)

  11.     // 死循环
  12. idle:
  13.     hlt
  14.     jmp idle

  15. // 填充到 510 字节,加上启动签名
  16. .org 510
  17. .word 0xAA55
复制代码


编译和链接:

  1. as -o boot.o boot.s
  2. ld --oformat binary -Ttext 0x7C00 -o boot.bin boot.o
  3. dd if=/dev/zero of=floppy.img bs=512 count=2880
  4. dd if=boot.bin of=floppy.img conv=notrunc
  5. qemu-system-x86_64 -fda floppy.img
复制代码


如果看到屏幕左上角出现一个蓝底黄字的 'A',说明成了。




第二步:加上简单的打印函数

用 BIOS 中断
  1. int 0x10
复制代码
的 0x0E 功能号,逐字符输出:

  1. .code16
  2. .section .text
  3. .globl _start

  4. _start:
  5.     movw $0x7C00, %sp
  6.     movw $msg, %si
  7.     call puts

  8. idle:
  9.     hlt
  10.     jmp idle

  11. // 打印以0结尾的字符串
  12. // si = 字符串地址
  13. puts:
  14.     movb (%si), %al
  15.     cmpb $0, %al
  16.     je puts_end
  17.     movb $0x0E, %ah
  18.     int $0x10
  19.     incw %si
  20.     jmp puts
  21. puts_end:
  22.     ret

  23. msg:
  24.     .asciz "Hello from GAS DOS!"
  25. .org 510
  26. .word 0xAA55
复制代码





第三步:实现一个最简单的命令行

这里通过 BIOS 的
  1. int 0x16
复制代码
功能 0x00 读取键盘输入,实现了一个可交互的 shell:

  1. .code16
  2. .section .text
  3. .globl _start

  4. .equ BUF_SIZE, 64

  5. _start:
  6.     movw $0x7C00, %sp
  7.     movw $prompt, %si
  8.     call puts

  9. main_loop:
  10.     // 显示提示符
  11.     movw $prompt, %si
  12.     call puts

  13.     // 读一行输入
  14.     movw $cmd_buf, %di
  15.     movw $BUF_SIZE, %cx
  16.     call readline

  17.     // 换行
  18.     movb $0x0D, %al
  19.     movb $0x0E, %ah
  20.     int $0x10
  21.     movb $0x0A, %al
  22.     int $0x10

  23.     // 检查命令
  24.     movw $cmd_buf, %si
  25.     movw $cmd_help, %di
  26.     call strcmp
  27.     jc cmd_help_handler

  28.     movw $cmd_buf, %si
  29.     movw $cmd_ver, %di
  30.     call strcmp
  31.     jc cmd_ver_handler

  32.     // 未知命令
  33.     movw $unknown, %si
  34.     call puts
  35.     jmp main_loop

  36. cmd_help_handler:
  37.     movw $help_text, %si
  38.     call puts
  39.     jmp main_loop

  40. cmd_ver_handler:
  41.     movw $ver_text, %si
  42.     call puts
  43.     jmp main_loop

  44. // === 子函数 ===

  45. // 读取一行到 di 指向的缓冲区,最多 cx 字节
  46. readline:
  47.     pushw %ax
  48.     pushw %di
  49. rl_loop:
  50.     movb $0x00, %ah
  51.     int $0x16        // 读键盘
  52.     cmpb $0x0D, %al  // 回车?
  53.     je rl_done
  54.     cmpb $0x08, %al  // 退格?
  55.     je rl_backspace
  56.     // 回显
  57.     movb $0x0E, %ah
  58.     int $0x10
  59.     stosb            // 存到缓冲区
  60.     loop rl_loop
  61. rl_done:
  62.     movb $0, (%di)   // 字符串结尾
  63.     popw %di
  64.     popw %ax
  65.     ret
  66. rl_backspace:
  67.     cmpw %di, %sp    // 简单防护
  68.     je rl_loop
  69.     decw %di
  70.     movb $0x08, %al
  71.     movb $0x0E, %ah
  72.     int $0x10
  73.     movb $0x20, %al  // 空格擦除
  74.     int $0x10
  75.     movb $0x08, %al
  76.     int $0x10
  77.     incw %cx
  78.     jmp rl_loop

  79. // 比较 si 和 di 指向的两个字符串,相等则 CF=1
  80. strcmp:
  81.     pushw %ax
  82. sc_loop:
  83.     movb (%si), %al
  84.     cmpb (%di), %al
  85.     jne sc_ne
  86.     cmpb $0, %al
  87.     je sc_eq
  88.     incw %si
  89.     incw %di
  90.     jmp sc_loop
  91. sc_eq:
  92.     stc
  93.     popw %ax
  94.     ret
  95. sc_ne:
  96.     clc
  97.     popw %ax
  98.     ret

  99. // 打印字符串(复用之前的)
  100. puts:
  101.     pushw %ax
  102.     pushw %si
  103. ps_loop:
  104.     movb (%si), %al
  105.     cmpb $0, %al
  106.     je ps_done
  107.     movb $0x0E, %ah
  108.     int $0x10
  109.     incw %si
  110.     jmp ps_loop
  111. ps_done:
  112.     popw %si
  113.     popw %ax
  114.     ret

  115. // === 数据 ===
  116. prompt:
  117.     .asciz "\r\nGAS-DOS> "
  118. cmd_help:
  119.     .asciz "help"
  120. cmd_ver:
  121.     .asciz "ver"
  122. help_text:
  123.     .asciz "Commands: help, ver"
  124. ver_text:
  125.     .asciz "GAS-DOS v0.1 - 16-bit Real Mode"
  126. unknown:
  127.     .asciz "Unknown command. Type 'help'."

  128. .org 0x100
  129. cmd_buf:
  130.     .space BUF_SIZE

  131. .org 510
  132. .word 0xAA55
复制代码





编译与测试

  1. as -o kernel.o kernel.s
  2. ld --oformat binary -Ttext 0x7C00 -o kernel.bin kernel.o
  3. dd if=/dev/zero of=dos.img bs=512 count=2880
  4. dd if=kernel.bin of=dos.img conv=notrunc
  5. 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 | 手写内核系列
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关注公众号

相关侵权、举报、投诉及建议等,请发 E-mail:admin@discuz.vip

Powered by Discuz! X5.0 Licensed © 2001-2026 Discuz! Team.

在本版发帖
关注公众号
QQ客服返回顶部