本文结合
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 和平台差异
