资讯动态

从 NEMU 到 AbstractMachine:程序、运行时环境与 AM 完全解析

发布时间:2026/10/2 2:33:10 来源:尧图企业网站定制
本文结合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.ldmain 参数trm.cinsert-arg.py输出putch停机haltnemu_trap设备访问IOE异常处理CTE硬件模拟NEMU二、AM 与 NEMU 的分工NEMUNEMU 模拟硬件CPU 指令 物理内存 MMIO 设备 中断异常AbstractMachineAM 提供运行时环境程序入口 栈 堆 参数 I/O 接口 异常接口 退出接口对应关系AM 负责“程序怎么运行” NEMU 负责“硬件怎么工作”三、构建阶段ARCH 决定 ISA 和平台以 hello 为例am-kernels/kernels/hello/ ├── hello.c └── MakefileMakefileNAME hello SRCS hello.c include $(AM_HOME)/Makefile执行make ARCHriscv32-nemuAM Makefile 拆出ARCH_SPLIT $(subst -, ,$(ARCH)) ISA $(word 1,$(ARCH_SPLIT)) PLATFORM $(word 2,$(ARCH_SPLIT))所以ARCHriscv32-nemu ISAriscv32 PLATFORMnemu接着包含-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 -marchrv32im_zicsr -mabiilp32 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 ARCHriscv32-nemu run mainargshelloAM 调用insert-arg: image python $(AM_HOME)/tools/insert-arg.py ...脚本在二进制中寻找占位字符串并替换成真正的hello所以运行时 main(args) 中的 args 来自 insert-arg.py八、putchAM 的输出接口void putch(char ch) { outb(SERIAL_PORT, ch); }SERIAL_PORT来自abstract-machine/am/src/platform/nemu/include/nemu.h最终对应 NEMU 的串口 MMIO0xa00003f8NEMU 中初始化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 - 主机终端九、haltAM 的退出接口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 解码到ebreakINSTPAT(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十一、IOEAM 如何访问设备例如定时器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_VECTOR0x80000000所以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.clinker.ldmain 参数trm.cinsert-arg.py输出putch NEMU serial退出haltnemu_trapebreak设备访问am/src/platform/nemu/ioe/异常/中断am/src/riscv/nemu/cte.ctrap.S硬件模拟nemu/一句话总结AM 不是硬件也不是普通库而是运行在 NEMU 模拟硬件之上的运行时环境。它负责程序入口、栈、堆、参数、I/O、异常和退出让上层程序不必直接面对 ISA 和平台差异

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑