一、本章说明

1. 前置知识

请先阅读 lesson10 — 引导程序加载 C 语言程序。本章在 lesson10 的基础上,扩展 utilities.s / utilities.h,实现 键盘输入与屏幕回显,并说明 C 与汇编之间的 参数传递与返回值

2. 本章目标

完成本章后,你将能够:

  • 理解 cdecl 调用约定在裸机 -m16 环境下的具体表现(栈传参、[esp+4] 取参、EAX 返回值)
  • 在汇编中调用 BIOS INT 16h(读键)与 INT 10h(电传输出)
  • 在 C 中通过 utilities.h 声明调用 read_charprint_charprint_str
  • 编写简易控制台:read_charprint_char 循环回显,回车自动换行

3. 目录结构

lesson11/
├── loader.s              # 引导程序(与 lesson10 基本相同)
├── Makefile
└── kernel/
    ├── kernel.c          # C 入口:欢迎语 + 键盘回显循环
    ├── utilities.s       # 汇编子程序(清屏、读键、打印等)
    └── utilities.h         # 供 C 调用的声明

编译产物输出到 ../target/loader.binkernel.binos.img)。

4. 准备工具

工具 用途
nasm 汇编 .s 为 ELF 目标文件
i686-elf-gcc 编译 C 内核(裸机,无标准库)
i686-elf-ld 链接并输出 flat binary
dd 把引导扇区/内核写入 os.img
make 一键编译、安装、运行
Bochs 运行与调试

工具已放在开源仓库tools/env/ 目录。Windows 下请将 tools/tools/bochs/ 加入 PATH。

5. 内存与磁盘布局

位置 物理地址 说明
引导扇区 0x7C00 BIOS 加载,loader.bin 链接地址
读盘缓冲区起点 0x8200 从软盘扇区 2 开始读入
内核入口 0xC200 kernel.bin 链接地址,引导程序 JMP 目标
SS:SP = 0:0x7C00 跳转 C 代码前由引导程序设置

FAT12 1.44MB 软盘布局与 lesson10 相同:引导区 1 扇区 + FAT 18 扇区 + 根目录 14 扇区 = 33 扇区,第 33 扇区起为数据区。make installdd seek=33kernel.bin 写入该位置。

nasm + ld 0x7C00

gcc + ld 0xC200

dd 扇区0

dd seek=33

loader.s

loader.bin

kernel.c + utilities.s

kernel.bin

os.img

BIOS 加载 0x7C00

INT 13h 读盘

JMP 0xC200

clear_screen → print_str

read_char → print_char 循环


二、cdecl 在本项目中的用法

本章使用 i686-elf-gcc -m16 -ffreestanding 编译 C,汇编子程序用 nasm -f elf32 生成 .o,再由同一链接器合并。虽然指令是 16 位实模式,但 GCC 生成的 栈操作是 32 位,参数与返回值的传递遵循 cdecl 约定。

1. C 调用汇编

C 侧用 extern 声明,汇编侧用 global 导出符号:

// kernel.c
extern int add(int a, int b);   // 声明汇编函数
int result = add(10, 20);
; add.asm(示意,非本章内核代码)
section .text
global add

add:
    mov eax, [esp+4]    ; 第一个参数 a(从右到左压栈,a 在较低地址)
    mov ebx, [esp+8]    ; 第二个参数 b
    add eax, ebx        ; 返回值放入 eax
    ret                 ; 被调函数只返回,不清栈

注意: 不能用 pop 取参数——call 之后 [esp]返回地址,先 pop 会把它当成第一个参数。

2. 汇编调用 C

参数同样 压栈(从右到左),由 调用方call 之后清理堆栈:

push 20             ; 第二个参数
push 10             ; 第一个参数
call myfunc
add  esp, 8          ; 调用方清栈(2 个 32 位参数)

注意: cdecl 下整数参数走栈,不是放进 eax / ebx

3. 栈布局与返回值

print_str(char *msg, int len) 为例,汇编进入函数时栈如下:

高地址
  [esp+8]  →  第二个参数 len
  [esp+4]  →  第一个参数 msg
  [esp]    →  返回地址(call 压入)
低地址

因此在本章 utilities.s 中:

print_str:
    mov edx, [esp+8]    ; len
    mov si,  [esp+4]    ; msg(实模式段地址为 0,低 16 位有效)

返回值约定:

类型 寄存器
char / int EAXchar 实际使用 AL)
void

read_char 通过 INT 16h 把键码放入 AL,函数 ret 后 C 从 EAX 读到返回值。

4. 与用户态程序的区别

项目 用户态 gcc -m32 本章裸机 -m16
运行环境 Linux / Windows 用户进程 16 位实模式,无 libc
系统调用 libc / syscall 直接 INT 10h / INT 16h
链接 动态 / 静态链接完整运行时 -ffreestanding + -Ttext 0xC200 裸 bin
操作系统建立 引导程序设 SP = 0x7C00

三、实现步骤

  1. 沿用 lesson10 的 loader.sMakefile 框架
  2. 扩展 utilities.s:增加 read_charprint_charprint_str
  3. 更新 utilities.h 中的函数声明
  4. 编写 kernel.c:清屏 → 打印欢迎语 → 键盘回显循环
  5. 编译、安装、运行并验证回显与换行

四、源码讲解

1. kernel.c — C 入口

#include "utilities.h"

int strlen(char* p);

void main(void){
    clear_screen();
    char* str = "NotOneOS Console .\r\nWelcome..\r\n";
    int   len = strlen(str);
    print_str(str, len);
    char a;
    fin:
        a = read_char();
        print_char(a);
        goto fin;
}

int strlen(char* p){
    int count = 0;
    while (*p++) count++;
    return count;
}

说明:

  • strlen 自行实现:裸机环境无标准 C 库
  • 欢迎语使用 \r\n 换行:BIOS 电传输出中 \n 只下移一行不回列,需 \r 才回到行首
  • fin 循环:read_char 阻塞等待按键,print_char 回显;goto 避免 main 返回后跑飞

2. utilities.s — BIOS 子程序

函数 中断 作用
clear_screen INT 10h, AX=0003h 清屏
read_char INT 16h, AH=00h 阻塞读键,键码在 AL
print_char INT 10h, AH=0Eh 电传输出一个字符
print_str INT 10h 循环 按长度打印字符串
io_hlt HLT 后返回
shutdown INT 15h APM 关机(已实现,本章未调用)

read_char — 读键盘

read_char:
    mov ah, 0x00        ; BIOS:等待按键
    int 0x16            ; 结果在 AL
    ret                 ; AL → EAX,C 侧得到 char

print_char — 回显与换行

print_char:
    mov al, [esp+4]     ; 从栈取 C 传入的 char
print_char_al:
    mov ah, 0x0e
    int 0x10
    cmp al, 0x0d        ; 回车 CR
    jne not_cr
    mov al, 0x0a        ; 补输出换行 LF
    int 0x10
not_cr:
    ret

终端通常只收到 \r(0x0D),显式补 \n(0x0A)才能正确换行。

print_str — 打印欢迎语

print_str:
    mov edx, [esp+8]    ; len
    mov si,  [esp+4]    ; msg
print:
    cmp edx, 0          ; 已输出 len 个字符
    je  end_print
    mov al, [si]
    cmp al, 0           ; 遇到 '\0' 提前结束
    je  end_print
    mov ah, 0x0e
    mov bx, 15
    int 0x10
    add si, 1
    dec edx
    jmp print
end_print:
    ret

字符串使用 \r\n 时,原样输出即可:\r 回列、\n 换行。循环先打印再 dec edx,避免旧写法「先 dec 为 0 就退出」导致最后一个字符(如末尾 \n)丢失、光标停在行首。

3. utilities.h — C 调用声明

#ifndef __UTILITIES_H_
#define __UTILITIES_H_

void io_hlt(void);
void clear_screen(void);
char read_char(void);
void print_char(char);
void print_str(char *msg, int len);

#endif

4. loader.s — 引导程序

与 lesson10 相同:FAT12 BPB + INT 13h 读盘 + 跳转到 0xC200。详细流程见 lesson10 README


五、Makefile 与工具链

编译选项与 lesson10 一致,核心片段如下:

CCFLAGS   = -march=i386 -m16 -mpreferred-stack-boundary=2 -ffreestanding

utilities.o: kernel/utilities.s
	$(NASM) -f elf32 -o $(TARGET)$@ $^

kernel.o: kernel/kernel.c kernel/utilities.h
	$(CC) $(CCFLAGS) -o $(TARGET)kernel.o -c kernel/kernel.c

kernel.bin: kernel.o utilities.o
	$(LD) -m elf_i386 -Ttext 0xC200 --oformat binary \
	      -o $(TARGET)$@ $(TARGET)kernel.o $(TARGET)utilities.o

install:
	$(DD) if=$(TARGET)loader.bin of=$(TARGET)os.img conv=notrunc
	$(DD) if=$(TARGET)kernel.bin of=$(TARGET)os.img conv=notrunc bs=512 seek=33

完整 Makefile 见仓库 lesson11/Makefile


六、构建与运行

lesson11/ 目录下执行:

make          # 生成 loader.bin、kernel.bin
make install  # 写入 ../target/os.img
make run      # Bochs 运行

../target/ 下尚无 .bin 文件,先执行 makeinstall(Windows 版 dd 会报 Error opening input file: 2,表示找不到输入文件)。

预期现象:

  1. 屏幕短暂显示 Loading NotOneOS...
  2. 清屏后输出欢迎语(NotOneOS Console 等)
  3. 在 Bochs 窗口中按键,字符即时回显
  4. 按回车(Enter)可换行

在这里插入图片描述

调试可在 Bochs 中断到内核入口:

make debug
b 0xC200
c

单步跟踪 read_char / print_charcall 与栈上参数。


七、常见问题

现象 可能原因
make 报找不到 i686-elf-gcc 检查 env/bin/ 是否存在
有欢迎语但按键无反应 Bochs 窗口未获得焦点;或 INT 16h 未正确链接
回车不换行 print_char 未补 0x0A;检查是否调用带换行逻辑的 print_char
乱码或花屏 kernel.bin 未写入镜像,或 seek 不是 33
链接报 undefined reference utilities.s 中缺少对应 global 符号

八、本章小结

  • C 与汇编通过 cdecl 互调:参数在栈上([esp+4][esp+8]…),整数返回值在 EAX
  • 汇编子程序封装 BIOS:INT 16h 读键、INT 10h 输出,C 代码保持简洁
  • print_char\r\n,实现终端式换行
  • 引导、链接、写盘流程与 lesson10 相同,本章重点在内核交互逻辑
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐