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

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

相关推荐
呉師傅2 小时前
得力P2500、M2500系列打印机硒鼓加粉及清零方法【纯享版】
运维·网络·windows·电脑
Sayai2 小时前
Elasticsearch 日志检索 DSL 实战:时间范围查询、字段去重、分钟级统计与最新日志获取
大数据·运维·elasticsearch·搜索引擎·日志分析
qetfw2 小时前
Debian Chrony 时间同步、RAID5 与 LUKS 磁盘加密配置
linux·debian
fb_123454 小时前
MySQL运维实战:备份恢复+主从复制+读写分离+MHA高可用(超详细手把手教程)
运维·mysql·oracle
东城居士4 小时前
Linux驱动阻塞与非阻塞访问的理解
linux·嵌入式系统
..Dauntless..4 小时前
【Linux】进程地址空间初步理解
linux·运维·服务器
Julien20045 小时前
Docker 网络(一)
linux·运维·服务器·ssh·学习方法
字节跳动的猫5 小时前
LikeShop 积分体系全链路改造:获取规则、订单抵扣与积分商品兑换二开
运维·数据结构
杨云龙UP5 小时前
一次数据库查询缓慢故障复盘:大表数据增长、SQL全表扫描导致系统响应异常
linux·运维·服务器·数据库·sql·mysql