本文结合
ysyx-workbench的实际代码,讲解 PA 讲义《程序、运行时环境与 AM》的核心内容。重点回答:NEMU 已经能执行指令了,为什么程序还是不能直接运行?AM 到底提供了什么?
摘要
NEMU 提供了:
指令执行 物理内存 PC 通用寄存器 MMIO 设备但是一个程序真正运行,还需要:
程序入口 栈 堆 main 参数 标准输出 程序退出 异常处理 设备访问这些内容不属于单条 CPU 指令,而属于运行时环境。
AbstractMachine 就是 ysyx 中为程序提供的统一运行时环境:
应用程序 / kernel | v klib | v AbstractMachine | v riscv32-nemu 平台实现 | v NEMU | v Linux 主机关键词
NEMU、AbstractMachine、AM、TRM、IOE、CTE、riscv32、运行时环境、PA
一、为什么需要运行时环境
NEMU 已经实现了很多指令,但只有指令并不能直接运行一个完整的程序。
一个程序运行至少需要这些条件:
程序加载到哪里? 第一条指令是什么? 栈指针 sp 初始化成什么? 堆从哪里开始? main 的参数从哪里来? printf 最后的输出由谁完成? 程序结束时如何通知 NEMU? 中断和异常由谁处理?这些问题分别对应:
| 问题 | 所属模块 |
|---|---|
| 程序入口 | start.S |
| 栈和堆 | linker.ld |
| main 参数 | trm.c+insert-arg.py |
| 输出 | putch |
| 停机 | halt+nemu_trap |
| 设备访问 | IOE |
| 异常处理 | CTE |
| 硬件模拟 | NEMU |
二、AM 与 NEMU 的分工
NEMU
NEMU 模拟硬件:
CPU 指令 物理内存 MMIO 设备 中断异常AbstractMachine
AM 提供运行时环境:
程序入口 栈 堆 参数 I/O 接口 异常接口 退出接口对应关系:
AM 负责“程序怎么运行” NEMU 负责“硬件怎么工作”三、构建阶段:ARCH 决定 ISA 和平台
以 hello 为例:
am-kernels/kernels/hello/ ├── hello.c └── MakefileMakefile:
NAME = hello SRCS = hello.c include $(AM_HOME)/Makefile执行:
make ARCH=riscv32-nemuAM Makefile 拆出:
ARCH_SPLIT = $(subst -, ,$(ARCH)) ISA = $(word 1,$(ARCH_SPLIT)) PLATFORM = $(word 2,$(ARCH_SPLIT))所以:
ARCH=riscv32-nemu ISA=riscv32 PLATFORM=nemu接着包含:
-include $(AM_HOME)/scripts/$(ARCH).mk也就是:
abstract-machine/scripts/riscv32-nemu.mk代码:
include $(AM_HOME)/scripts/isa/riscv.mk include $(AM_HOME)/scripts/platform/nemu.mk CFLAGS += -DISA_H=\"riscv/riscv.h\" COMMON_CFLAGS += -march=rv32im_zicsr -mabi=ilp32 LDFLAGS += -melf32lriscv AM_SRCS += riscv/nemu/start.S \ riscv/nemu/cte.c \ riscv/nemu/trap.S \ riscv/nemu/vme.c因此:
AM 的 ISA 和平台选择由 ARCH 决定。四、运行时内存布局:linker.ld
文件:
abstract-machine/scripts/linker.ld代码:
ENTRY(_start) SECTIONS { . = _pmem_start + _entry_offset; .text : { *(entry) *(.text*) } .rodata : { *(.rodata*) } .data : { *(.data*) } .bss : { _bss_start = .; *(.bss*) *(.sbss*) *(.scommon) } _stack_top = ALIGN(0x1000); . = _stack_top + 0x8000; _stack_pointer = .; end = .; _end = .; _heap_start = ALIGN(0x1000); }它决定:
_start 程序入口 .text 代码 .rodata 只读数据 .data 有初值数据 .bss 零初始化数据 _stack_pointer 栈顶 _heap_start 堆起点其中:
_pmem_start = 0x80000000 _entry_offset = 0所以程序从:
0x80000000开始。
五、程序入口:start.S
文件:
abstract-machine/am/src/riscv/nemu/start.S代码:
.section entry, "ax" .globl _start .type _start, @function _start: mv s0, zero la sp, _stack_pointer call _trm_init .size _start, . - _start它完成:
初始化 s0 设置 sp = _stack_pointer 调用 _trm_init这就是运行时环境建立的第一步。
六、TRM:最基础的运行时环境
文件:
abstract-machine/am/src/platform/nemu/trm.c标准形式:
#include <am.h> #include <nemu.h> extern char _heap_start; int main(const char *args); Area heap = RANGE(&_heap_start, PMEM_END); static const char mainargs[MAINARGS_MAX_LEN] = TOSTRING(MAINARGS_PLACEHOLDER); void putch(char ch) { outb(SERIAL_PORT, ch); } void halt(int code) { nemu_trap(code); while (1); } void _trm_init() { int ret = main(mainargs); halt(ret); }它提供:
heap 堆区信息 mainargs main 参数 putch 输出字符 halt 结束程序 _trm_init 启动入口七、mainargs 从哪里来
trm.c中:
static const char mainargs[MAINARGS_MAX_LEN] = TOSTRING(MAINARGS_PLACEHOLDER);编译时定义:
CFLAGS += -DMAINARGS_MAX_LEN=$(MAINARGS_MAX_LEN) CFLAGS += -DMAINARGS_PLACEHOLDER=$(MAINARGS_PLACEHOLDER)运行时:
make ARCH=riscv32-nemu run mainargs=helloAM 调用:
insert-arg: image @python $(AM_HOME)/tools/insert-arg.py ...脚本在二进制中寻找占位字符串,并替换成真正的:
hello所以:
运行时 main(args) 中的 args 来自 insert-arg.py八、putch:AM 的输出接口
void putch(char ch) { outb(SERIAL_PORT, ch); }SERIAL_PORT来自:
abstract-machine/am/src/platform/nemu/include/nemu.h最终对应 NEMU 的串口 MMIO:
0xa00003f8NEMU 中初始化:
void init_serial() { serial_base = new_space(8); add_mmio_map("serial", CONFIG_SERIAL_MMIO, serial_base, 8, serial_io_handler); }调用链:
AM putch -> outb(SERIAL_PORT) -> NEMU paddr_write -> mmio_write -> serial_io_handler -> 主机终端九、halt:AM 的退出接口
void halt(int code) { nemu_trap(code); while (1); }nemu_trap在 riscv32 下:
#define nemu_trap(code) \ asm volatile("mv a0, %0; ebreak" : : "r"(code))因此生成:
mv a0, code ebreakNEMU 解码到ebreak:
INSTPAT("000000000001 00000 000 00000 11100 11", ebreak, I, { s->dnpc = isa_raise_intr(3, s->pc); });最终:
AM halt -> nemu_trap -> ebreak -> NEMU_END十、AM 接口:am.h
文件:
abstract-machine/am/include/am.h定义了运行时接口:
extern Area heap; void putch(char ch); void halt(int code); bool ioe_init(void); void ioe_read(int reg, void *buf); void ioe_write(int reg, void *buf); bool cte_init(Context *(*handler)(Event ev, Context *ctx)); void yield(void); bool ienabled(void); void iset(bool enable); Context *kcontext(Area kstack, void (*entry)(void *), void *arg);对应:
TRM Turing Machine IOE I/O Extension CTE Context Extension VME Virtual Memory Extension MPE Multi-Processing Extension十一、IOE:AM 如何访问设备
例如定时器:
io_read(AM_TIMER_UPTIME);io_read()在:
abstract-machine/am/src/platform/nemu/ioe/ioe.c中查表:
typedef void (*handler_t)(void *buf); static void *lut[128] = { [AM_TIMER_CONFIG] = __am_timer_config, [AM_TIMER_RTC] = __am_timer_rtc, [AM_TIMER_UPTIME] = __am_timer_uptime, [AM_INPUT_CONFIG] = __am_input_config, [AM_INPUT_KEYBRD] = __am_input_keybrd, ... };调用链:
io_read(AM_TIMER_UPTIME) -> lut[AM_TIMER_UPTIME] -> __am_timer_uptime()最终读取 NEMU 的 RTC MMIO 寄存器。
十二、NEMU 如何加载 AM 程序
文件:
nemu/src/monitor/monitor.c代码:
static long load_img() { FILE *fp = fopen(img_file, "rb"); fseek(fp, 0, SEEK_END); long size = ftell(fp); fseek(fp, 0, SEEK_SET); int ret = fread(guest_to_host(RESET_VECTOR), size, 1, fp); assert(ret == 1); fclose(fp); return size; }RESET_VECTOR:
0x80000000所以:
AM 生成的 .bin -> NEMU pmem[0x80000000]CPU 启动:
cpu.pc = RESET_VECTOR;然后从:
0x80000000取第一条指令,执行_start。
十三、hello 程序的完整运行链路
代码:
#include <am.h> #include <klib-macros.h> int main(const char *args) { const char *fmt = "Hello, AbstractMachine!\n" "mainargs = '%'.\n"; for (const char *p = fmt; *p; p++) { (*p == '%') ? putstr(args) : putch(*p); } return 0; }执行链:
NEMU 从 0x80000000 取指 -> _start -> 设置 sp = _stack_pointer -> _trm_init -> main(mainargs) -> putstr / putch -> outb(SERIAL_PORT, ch) -> NEMU serial -> 终端输出 -> main 返回 0 -> halt(0) -> nemu_trap(0) -> ebreak -> NEMU_END十四、AM 为什么是运行时环境
AM 隐藏了不同 ISA 和平台的差异:
不同 ISA 不同平台 不同设备地址 不同启动代码 不同中断方式上层程序只看到:
putch() getch() io_read() io_write() yield() halt()具体实现由 AM 完成。
例如:
riscv32-nemu: scripts/riscv32-nemu.mk am/src/riscv/nemu/ am/src/platform/nemu/ x86_64-qemu: scripts/x86_64-qemu.mk am/src/x86/qemu/ am/src/platform/qemu/所以:
AM 是程序与硬件之间的一层运行时环境。十五、常见问题
1.mainargs没有正确替换
检查:
tools/insert-arg.py scripts/platform/nemu.mk trm.c 中 TOSTRING(MAINARGS_PLACEHOLDER)2.halt没有使用nemu_trap
如果写成串口0xFF后死循环,NEMU 不会停止。
正确:
void halt(int code) { nemu_trap(code); while (1); }3.ioe_init()没有调用
如果程序使用 IOE,需要调用:
ioe_init();否则io_read/io_write使用未初始化设备。
4.linker.ld和 NEMU 地址不一致
必须保持:
_pmem_start = 0x80000000 NEMU RESET_VECTOR = 0x800000005.__am_*函数没有实现
例如:
__am_timer_uptime() __am_gpu_fbdraw() __am_input_keybrd()可能表现为:
AM Panic: access nonexist register十六、总结
这份讲义的核心是:
NEMU 提供硬件模拟 AM 提供运行时环境 程序运行在 AM 之上完整链路:
AM 源代码 -> riscv32-nemu 编译 -> linker.ld 布局 -> start.S 建立栈 -> _trm_init -> main -> putch / io_read / io_write -> nemu_trap -> NEMU 停机对应代码:
| 运行环境组成 | 代码位置 |
|---|---|
| 程序入口 | am/src/riscv/nemu/start.S |
| 栈指针 | scripts/linker.ld |
| 堆信息 | trm.c+linker.ld |
| main 参数 | trm.c+insert-arg.py |
| 输出 | putch+ NEMU serial |
| 退出 | halt+nemu_trap+ebreak |
| 设备访问 | am/src/platform/nemu/ioe/ |
| 异常/中断 | am/src/riscv/nemu/cte.c+trap.S |
| 硬件模拟 | nemu/ |
一句话总结:
AM 不是硬件,也不是普通库,而是运行在 NEMU 模拟硬件之上的运行时环境。它负责程序入口、栈、堆、参数、I/O、异常和退出,让上层程序不必直接面对 ISA 和平台差异