当前位置: 首页 > news >正文

一生一芯学习:程序,运行时环境与AM(一)

一生一芯学习:程序,运行时环境与AM(一)

目前我们已经跑通了cpu-test和实现了riscv-I型指令所需的42条指令,现在我们已经可以到跑简单程序的地步了,我们也希望运行简单的程序,因此我们需要运行时环境(runtime environment)。

比如现在要结束程序,那我们就要用提前准备好的API如void halt,调用这个halt()就可以来结束运行,那我们怎么实现这个halt呢,来看看源码。

void halt(int code) {nemu_trap(code);// should not reach herewhile (1);
}

nemu_trap(code)是什么意思呢,在/ysyx-workbench/abstract-machine/am/src/platform/nemu/include中有这样一段代码,判断所使用的架构并且给出对应架构退出的内联汇编。

#if defined(__ISA_X86__)
# define nemu_trap(code) asm volatile ("int3" : :"a"(code))
#elif defined(__ISA_MIPS32__)
# define nemu_trap(code) asm volatile ("move $v0, %0; sdbbp" : :"r"(code))
#elif defined(__riscv)
# define nemu_trap(code) asm volatile("mv a0, %0; ebreak" : :"r"(code))
#elif defined(__ISA_LOONGARCH32R__)
# define nemu_trap(code) asm volatile("move $a0, %0; break 0" : :"r"(code))
#else
# error unsupported ISA __ISA__
#endif

随便找一个程序来看反汇编,用户程序会以这种形式成为机器码。

80000fdc <halt>:void halt(int code) {nemu_trap(code);
80000fdc:	00050513          	mv	a0,a0
80000fe0:	00100073          	ebreak//里面是一个嵌入汇编语句,宏会把一个识别结束的结束码移动到通用寄存器中// should not reach herewhile (1);
80000fe4:	0000006f          	j	80000fe4 <halt+0x8>80000fe8 <_trm_init>:
}

正好对应上了。

此时我很好奇# define nemu_trap(code) asm volatile("mv a0, %0; ebreak" : :"r"(code))"r"(code)到底指的是什么。
首先明确说明这是一个内联汇编,具体看https://luyoung0001.github.io/2025/02/06/C%E8%AF%AD%E8%A8%80%E5%86%85%E8%81%94%E6%B1%87%E7%BC%96/
指的是操作数和约束,r意思是“通用寄存器”,编译器会为操作数分配一个合适的通用寄存器 (例如 x1 - x31)。
在汇编指令模板中,使用 %0, %1, %2… 来引用操作数。
%0 对应第一个操作数,%1 对应第二个操作数,以此类推。

编译生成一个可以在NEMU的运行时环境上运行的程序的过程大致如下:

gcc将$ISA-nemu的AM实现源文件编译成目标文件, 然后通过ar将这些目标文件作为一个库, 打包成一个归档文件abstract-machine/am/build/am-$ISA-nemu.a

gcc把应用程序源文件(如am-kernels/tests/cpu-tests/tests/dummy.c)编译成目标文件

通过gcc和ar把程序依赖的运行库(如abstract-machine/klib/)也编译并打包成归档文件

根据Makefile文件abstract-machine/scripts/$ISA-nemu.mk中的指示, 让ld根据链接脚本abstract-machine/scripts/linker.ld, 将上述目标文件和归档文件链接成可执行文件。

根据上述链接脚本的指示, 可执行程序重定位后的节从0x100000或0x80000000开始 (取决于_pmem_start和_entry_offset的值), 首先是.text节, 其中又以abstract-machine/am/src/$ISA/nemu/start.S中自定义的entry节开始, 然后接下来是其它目标文件的.text节. 这样, 可执行程序起始处总是放置start.S的代码, 而不是其它代码, 保证客户程序总能从start.S开始正确执行. 链接脚本也定义了其它节(包括.rodata, .data, .bss)的链接顺序, 还定义了一些关于位置信息的符号, 包括每个节的末尾, 栈顶位置, 堆区的起始和末尾.

我们对编译得到的可执行文件的行为进行简单的梳理:

第一条指令从abstract-machine/am/src/$ISA/nemu/start.S开始, 设置好栈顶之后就跳转到abstract-machine/am/src/platform/nemu/trm.c的_trm_init()函数处执行.

在_trm_init()中调用main()函数执行程序的主体功能, main()函数还带一个参数, 目前我们暂时不会用到, 后面我们再介绍它.
从main()函数返回后, 调用halt()结束运行.

有了TRM这个简单的运行时环境, 我们就可以很容易地在上面运行各种"简单"的程序了. 当然, 我们也可以运行"不简单"的程序: 我们可以实现任意复杂的算法, 甚至是各种理论上可计算的问题, 都可以在TRM上解决.

解读一下abstract-machine项目的Makefile
这里的MAKECMDGOALS就是你在命令行make后面带的参数,如果make后面没有参数,就让makecmdgoals等于image,并且让默认目标等于image

ifeq ($(MAKECMDGOALS),)MAKECMDGOALS  = image.DEFAULT_GOAL = image
endif

检查am环境目录以及ARCH架构是否为预估的riscv32-nemu or riscv32e-npc,再判断是否有正确的.c文件。

### Override checks when `make clean/clean-all/html`
### 清理和生成文档的时候跳过检查
ifeq ($(findstring $(MAKECMDGOALS),clean|clean-all|html),)### Print build info message
$(info # Buildin $(NAME)-$(MAKECMDGOALS) [$(ARCH)])### Check: environment variable `$AM_HOME` looks sane
ifeq ($(wildcard $(AM_HOME)/am/include/am.h),)$(error $$AM_HOME must be an AbstractMachine repo)
endif#notdir用于去掉文件的绝对路径,只保留文件名。path/nemu.mk ---> nemu.mk
#basename取前缀函数,	nemu.mk ---> nemu
ARCHS = $(basename $(notdir $(shell ls $(AM_HOME)/scripts/*.mk)))
#filter函数用于检查 $(ARCH) 是否在 $(ARCHS) 列表中。如果 $(ARCH) 在列表中,返回 $(ARCH)。如果不在,返回空字符串。
#arch如果不在archs中,返回空字符串 ifeq逻辑生效 直接error。
ifeq ($(filter $(ARCHS), $(ARCH)), )$(error Expected $$ARCH in {$(ARCHS)}, Got "$(ARCH)")
endif#subst字符串替换函数,$(subst <from>,<to>,<text>),from替换成to在text中  逻辑:将ARCH中的-替换成“空格”
### Extract instruction set architecture (`ISA`) and platform from `$ARCH`. Example: `ARCH=x86_64-qemu -> ISA=x86_64; PLATFORM=qemu`
#word取单词 1 2是第几个单词,逻辑就是isa取split中第一个单词,platform取split中第二个单词。
ARCH_SPLIT = $(subst -, ,$(ARCH))
ISA        = $(word 1,$(ARCH_SPLIT))
PLATFORM   = $(word 2,$(ARCH_SPLIT))$(info # Using ISA=$(ISA), PLATFORM=$(PLATFORM))
$(info # SRCS=$(SRCS))### Check if there is something to build
###flavor是寻找变量函数,当SRCS在makefile及其include中都没有时,函数输出undefined。ifeq就生效执行error
ifeq ($(flavor SRCS), undefined)$(error Nothing to build)
endif### Checks end here
endif

准备好编译所需的环境变量,镜像,链接等等。

## 2. General Compilation Targets### Create the destination directory (`build/$ARCH`)
WORK_DIR  = $(shell pwd)
DST_DIR   = $(WORK_DIR)/build/$(ARCH)
$(shell mkdir -p $(DST_DIR))### Compilation targets (a binary image or archive)
IMAGE_REL = build/$(NAME)-$(ARCH)
#将image_rel中的文件变成绝对路径之后返回给IMAGE
IMAGE     = $(abspath $(IMAGE_REL))
ARCHIVE   = $(WORK_DIR)/build/$(NAME)-$(ARCH).a### Collect the files to be linked: object files (`.o`) and libraries (`.a`)
#basename取SRCS的前缀  addsuffix给SRCS的后缀加一个.o  addprefix给SRCS.o加一个类似路径的前缀。
OBJS      = $(addprefix $(DST_DIR)/, $(addsuffix .o, $(basename $(SRCS))))
#sort是排序函数,将$(LIBS) am klib 按照首字母进行升序,并去除重复的单词。
LIBS     := $(sort $(LIBS) am klib) # lazy evaluation ("=") causes infinite recursions
LINKAGE   = $(OBJS)  # library archives are added by LIB_TEMPLATE below

制定好交叉编译所需的工具链,因为最终的可执行文件是需要在RISCV机器(nemu或者是npc)上运行,因此需要使用RV对应的工具链,如果是loonarch或者mips32也可以指定对应的工具链。

告诉编译器正确的路径如何找到库函数.h

并给予合适的编译选项。

## 3. General Compilation Flags### (Cross) compilers, e.g., mips-linux-gnu-g++
AS        = $(CROSS_COMPILE)gcc
CC        = $(CROSS_COMPILE)gcc
CXX       = $(CROSS_COMPILE)g++
LD        = $(CROSS_COMPILE)ld
AR        = $(CROSS_COMPILE)ar
OBJDUMP   = $(CROSS_COMPILE)objdump
OBJCOPY   = $(CROSS_COMPILE)objcopy
READELF   = $(CROSS_COMPILE)readelf### Compilation flags告诉编译器在哪里找到.h文件
#给LIBS加上amhome前缀之后加上include后缀再加上workdir/include,给库生成正确路径
INC_PATH += $(WORK_DIR)/include $(addsuffix /include/, $(addprefix $(AM_HOME)/, $(LIBS)))#给incpath加上-I前缀
INCFLAGS += $(addprefix -I, $(INC_PATH))ARCH_H := arch/$(ARCH).h
#-O2:启用中级优化(平衡性能与编译速度)。-MMD:生成依赖文件(.d),用于自动追踪头文件变更。
#-Wall -Werror:启用所有警告并将其视为错误(严格模式)。 $(INCFLAGS):可能包含额外的头文件搜索路径(如 -Iinclude)。
CFLAGS   += -O2 -MMD -Wall -Werror $(INCFLAGS) \-D__ISA__=\"$(ISA)\" -D__ISA_$(shell echo $(ISA) | tr a-z A-Z)__ \-D__ARCH__=$(ARCH) -D__ARCH_$(shell echo $(ARCH) | tr a-z A-Z | tr - _) \-D__PLATFORM__=$(PLATFORM) -D__PLATFORM_$(shell echo $(PLATFORM) | tr a-z A-Z | tr - _) \-DARCH_H=\"$(ARCH_H)\" \-fno-asynchronous-unwind-tables -fno-builtin -fno-stack-protector \-Wno-main -U_FORTIFY_SOURCE -fvisibility=hidden
CXXFLAGS +=  $(CFLAGS) -ffreestanding -fno-rtti -fno-exceptions
ASFLAGS  += -MMD $(INCFLAGS)
LDFLAGS  += -z noexecstack $(addprefix -T, $(LDSCRIPTS))

引入对应架构的makefile文件,如果是riscv32-nemu的话那就是引入scripts/riscv32-nemu.mk的makefile

### 4. Arch-Specific Configurations### Paste in arch-specific configurations (e.g., from `scripts/x86_64-qemu.mk`)
-include $(AM_HOME)/scripts/$(ARCH).mk

这里就是前面所说的编译过程,首先将各种.c .cpp .cc .S编译成.o后放到对应的dstdir中。
随后用ar工具将这些.o文件打包为一个.a静态库,最终根据riscv32-nemu.mk的指示通过链接脚本将上述目标文件和静态库等等变成一个可执行文件。

## 5. Compilation Rules### Rule (compile): a single `.c` -> `.o` (gcc)
#%.c是依赖的文件,要把.c .cc .cpp .S编译成.o放到dstdir中
$(DST_DIR)/%.o: %.c@mkdir -p $(dir $@) && echo + CC $<@$(CC) -std=gnu11 $(CFLAGS) -c -o $@ $(realpath $<)### Rule (compile): a single `.cc` -> `.o` (g++)
$(DST_DIR)/%.o: %.cc@mkdir -p $(dir $@) && echo + CXX $<@$(CXX) -std=c++17 $(CXXFLAGS) -c -o $@ $(realpath $<)### Rule (compile): a single `.cpp` -> `.o` (g++)
$(DST_DIR)/%.o: %.cpp@mkdir -p $(dir $@) && echo + CXX $<@$(CXX) -std=c++17 $(CXXFLAGS) -c -o $@ $(realpath $<)### Rule (compile): a single `.S` -> `.o` (gcc, which preprocesses and calls as)
$(DST_DIR)/%.o: %.S@mkdir -p $(dir $@) && echo + AS $<@$(AS) $(ASFLAGS) -c -o $@ $(realpath $<)###把所有的.s .c .cc .cpp文件转换为.o文件放到对应目录ifeq ($(MAKECMDGOALS),archive)
### Rule (archive): objects (`*.o`) -> `ARCHIVE.a` (ar)
#make archive用ar把所有的.o文件打包成一个.a静态库
#如果不是archive会递归调用每个依赖库如(am klib)的makefile
#把他们也编译成.a静态库,然后在链接的时候把这些.a静态库和.o文件一起链接成最终的elf文件
$(ARCHIVE): $(OBJS)@echo + AR "->" $(shell realpath $@ --relative-to .)@$(AR) rcs $@ $^
else
# $(1): library name
define LIB_TEMPLATE =
$$(AM_HOME)/$(1)/build/$(1)-$$(ARCH).a: force@$$(MAKE) -s -C $$(AM_HOME)/$(1) archive
LINKAGE += $$(AM_HOME)/$(1)/build/$(1)-$$(ARCH).a
endef### Rule (recursive make): build a dependent library (am, klib, ...)
$(foreach lib, $(LIBS), $(eval $(call LIB_TEMPLATE,$(lib))))
endif### 最终链接Rule (link): objects (`*.o`) and libraries (`*.a`) -> `IMAGE.elf`, the final ELF binary to be packed into image (ld)
### 把所有的目标文件和库文件连接成最终的ELF image 
$(IMAGE).elf: $(LINKAGE) $(LDSCRIPTS)@echo \# Creating image [$(ARCH)]@echo + LD "->" $(IMAGE_REL).elf
ifneq ($(filter $(ARCH),native),)@$(CXX) -o $@ -Wl,--whole-archive $(LINKAGE) -Wl,-no-whole-archive $(LDFLAGS_CXX)
else@$(LD) $(LDFLAGS) -o $@ --start-group $(LINKAGE) --end-group
endif### Rule (`#include` dependencies): paste in `.d` files generated by gcc on `-MMD`
-include $(addprefix $(DST_DIR)/, $(addsuffix .d, $(basename $(SRCS))))

makefile的最后是一些依赖和更新,确保有文件修改后makefile能及时更新并进行编译。

## 6. Miscellaneous各种依赖及更新
## .PHONY 保证这些命令每次都能执行,不受文件名影响
### Build order control
image: image-dep
archive: $(ARCHIVE)
image-dep: $(IMAGE).elf
.PHONY: image image-dep archive run### Force to rebuild a rule
force:
.PHONY: force### Clean a single project (remove `build/`)
clean:rm -rf Makefile.html $(WORK_DIR)/build/
.PHONY: clean### Clean all sub-projects within depth 2 (and ignore errors)
CLEAN_ALL = $(dir $(shell find . -mindepth 2 -name Makefile))
clean-all: $(CLEAN_ALL) clean
$(CLEAN_ALL):-@$(MAKE) -s -C $@ clean
.PHONY: clean-all $(CLEAN_ALL)

下一期主要讲一下
看一下am-tests中的用户程序hello.c是如何变成可执行文件并被nemu运行的。

http://www.hskmm.com/?act=detail&tid=20835

相关文章:

  • 如何用Java25编译Java17的项目
  • [MCP] MCP Resources
  • 【ACM出版】2025年第二届人工智能与未来教育国际学术会议(AIFE 2025)
  • HL工作日志
  • Halcon基础——图像增强
  • HTML 开发工具有哪些?常用 HTML 开发工具推荐、学习路线与实战经验分享
  • PS 商业级人像修图插件:Infinite Retouch V1.0.3 全面指南
  • NVIDIA 开源 Audio2Face:音频生成逼真面部动画;Gemini Live API 支持思考能力 丨日报
  • 深入解析:4、urbane-commerce 认证请求 DTO 设计规范
  • mp4/图片转gif
  • 详细介绍:09.【Linux系统编程】“文件“读写操作,Linux下一切皆文件!
  • 数据类型-元组
  • BindingList的应用与改进
  • 谷歌 SEO 新词 xx animate 等实操教程
  • 完整教程:【读书笔记】架构整洁之道 P6 实现细节
  • Print Conductor打印软件安装教程!一款非常好用的批量打印软件!支持PDF、Word、Excel、图片等
  • Python 面向对象编程基础:类与对象初体验
  • 面向对象的设计原则
  • 反电动势法控制BLDC电机的原理图分析
  • 完整教程:Altium Designer(AD)设计规则检查设置
  • 企业物联网安全必须优先考虑的5个不可否认的理由
  • PSM敏捷认证自考学习指南
  • 2025内网聊天工具排行 4款好用的内网聊天软件推荐
  • 独立开发在线客服系统手记:实现对 PostgreSQL 的支持,以及与 MySQL 的对比
  • 方言普通话识别大模型,支撑中英+202种方言识别
  • ffmpeg一些使用记录,防止忘记
  • BLE从机(20)BLE区分主机(IOS/安卓/WIN)
  • Windows 驱动开发基础
  • 基于MATLAB实现基于距离的离群点检测算法
  • 国产DevOps工具链的突围之路:Gitee如何重塑企业研发效能