操作系统学习11 x86-C调用汇编程序与传参
操作系统学习11 x86-C调用汇编程序与传参
一、本章说明
1. 前置知识
请先阅读 lesson10 — 引导程序加载 C 语言程序。本章在 lesson10 的基础上,扩展 utilities.s / utilities.h,实现 键盘输入与屏幕回显,并说明 C 与汇编之间的 参数传递与返回值。
2. 本章目标
完成本章后,你将能够:
- 理解 cdecl 调用约定在裸机
-m16环境下的具体表现(栈传参、[esp+4]取参、EAX 返回值) - 在汇编中调用 BIOS INT 16h(读键)与 INT 10h(电传输出)
- 在 C 中通过
utilities.h声明调用read_char、print_char、print_str - 编写简易控制台:
read_char→print_char循环回显,回车自动换行
3. 目录结构
lesson11/
├── loader.s # 引导程序(与 lesson10 基本相同)
├── Makefile
└── kernel/
├── kernel.c # C 入口:欢迎语 + 键盘回显循环
├── utilities.s # 汇编子程序(清屏、读键、打印等)
└── utilities.h # 供 C 调用的声明
编译产物输出到 ../target/(loader.bin、kernel.bin、os.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 install 用 dd seek=33 把 kernel.bin 写入该位置。
二、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 |
EAX(char 实际使用 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 |
三、实现步骤
- 沿用 lesson10 的
loader.s与Makefile框架 - 扩展
utilities.s:增加read_char、print_char、print_str - 更新
utilities.h中的函数声明 - 编写
kernel.c:清屏 → 打印欢迎语 → 键盘回显循环 - 编译、安装、运行并验证回显与换行
四、源码讲解
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 文件,先执行 make 再 install(Windows 版 dd 会报 Error opening input file: 2,表示找不到输入文件)。
预期现象:
- 屏幕短暂显示
Loading NotOneOS... - 清屏后输出欢迎语(
NotOneOS Console等) - 在 Bochs 窗口中按键,字符即时回显
- 按回车(Enter)可换行

调试可在 Bochs 中断到内核入口:
make debug
b 0xC200
c
单步跟踪 read_char / print_char 的 call 与栈上参数。
七、常见问题
| 现象 | 可能原因 |
|---|---|
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 相同,本章重点在内核交互逻辑
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)