☰
从 NEMU 到 AbstractMachine:程序、运行时环境与 AM 完全解析
2026/10/2 2:33:05 网站建设 项目流程

本文结合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 └── Makefile

Makefile:

NAME = hello SRCS = hello.c include $(AM_HOME)/Makefile

执行:

make ARCH=riscv32-nemu

AM 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=hello

AM 调用:

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:

0xa00003f8

NEMU 中初始化:

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 ebreak

NEMU 解码到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 = 0x80000000

5.__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 和平台差异

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询