PA2
RTFM
[!note] riscv32 如何处理 32 位的立即数? 可以使用两个指令:
addi
,lui
-lui
: load upper immediate,加载立即数的高位 (20 位) 首先利用 lui 加载立即数的高 20 位, 在使用addi
指令, 将立即数的低 12 位加到目标立即数 (寄存器)上
- 一条指令在 nemu 中的执行过程:
- 取指: 将 pc 存储在
Decode *s
结构体中, 传入decode_exec
进行译码工作 - 译码: 利用
INSTPAT
宏和模式识别来进行译码工作, 首先使用pattern_decode
识别当前的指令, 然后从指令中提取出操作码, 再使用对应 isa 的decode_operand
函数提取出对应指令所需要的操作数 (统一类别指令的操作数提取方法是固定的, 所以针对不同的类型定制了不同的提取方法, 相比 x86方便不少) - 执行: 通过 C 代码进行执行
- 取指: 将 pc 存储在
基础设施
elf 文件
符号表
[[链接#符号和符号表]]
符号解析
[[链接#符号解析]]
与 cpu-tests 相关的编译执行过程解析
以 cpu-tests 中的
add-longlong
测试点为例
在 ysyx-workbench/am-kernels/tests/cpu-tests
目录下执行命令 make ARCH=riscv32-nemu ALL=add-longlong run
, 其执行的内容为:
/bin/echo -e "NAME = add-longlong\nSRCS = tests/add-longlong.c\ninclude ${AM_HOME}/Makefile" > Makefile.add-longlong
if make -s -f Makefile.add-longlong ARCH=riscv32-nemu run; then \
printf "[%14s] \033[1;32mPASS!\033[0m\n" add-longlong >> .result; \
else \
printf "[%14s] \033[1;31mFAIL!\033[0m\n" add-longlong >> .result; \
fi
rm -f Makefile.add-longlong
echo "" add-longlong
cat .result
rm .result
NAME = add-longlong
SRCS = tests/add-longlong.c
include /home/jiunian/Program/ysyx-workbench/abstract-machine/Makefile
am
目录下的 Makefile 包含进来
生成该 Makefile 后, 即通过执行命令 make -s -f Makefile.add-longlong ARCH=riscv32-nemu run
来进行编译操作, 其中:
- -s
: 静默操作
- -f
: 指定目标 Makefile
若该命令执行成功, 则打印测试成功
接下来看看, 这个命令具体做了什么
- 相关的 Makefile: 在 abstract-machine
中的 Makefile
中, -include $(AM_HOME)/scripts/$(ARCH).mk
将 riscv32-nemu.mk
包含进来, 而在 riscv32-nemu.mk
中又包含了 isa/riscv32.mk
和 platform/nemu.mk
这两个 Makefile
- 以 nemu.mk
中的 run
为目标, 而 run
目标依赖于 image
, image
依赖于 $(IMG).elf
和 image-dep
, image-dep
依赖于 $(OBJS) am $(LIBS)
, 依赖链如下:
当我们执行了 make -s -f Makefile.add-longlong ARCH=riscv32-nemu run
命令后, SRCS = tests/add-longlong.c
, 可以看到仅有一个 add-longlong.c
文件, 根据以上的依赖关系, 我们在执行目标 run
时候, 需要先获得 $(OBJS)
, am
和 $(LIBS)
这三个依赖内容:
- $(OBJS)
: object file, 编译获得, 根据以下规则编译获得:
- am
: abstract machine
- $(LIBS)
: 链接库, 根据以下一连串规则获得链接库:
- 其中: $(LIBS): %:
中所执行的目标 archive
依赖于 $(ARCHIVE)
根据依赖的顺序, 会先将 $(OBJS)
编译获得, 此时仅有一个源文件, 则编译获得 add-longlong.o
.
接着执行 $(LIBS): %:
中的命令 @$(MAKE) -s -C $(AM_HOME)/$* archive
, 其中, 参数 -C
会在执行之前 cd
到目标目录, 也就是执行目标目录下的 Makefile
. 执行该命令后, 会转移到 $(AM_HOME)/$*
目录下执行 archive
目标, 在对应的 Makfile 中, 会将 SRCS
修改为其源文件, 而不是 add-longlong
了, 执行 archive
目标后, 就会将其归档为库文件, 供后续的链接使用.
以上工作是依赖关系 image-dep -> $(OBJS), am, $(LIBS)
所完成的工作.
归档文件创建完成后, 开始使用链接器生成 elf
文件, 即 $(IMAGE).elf
文件, 该工作是由依赖关系 $(IMAGE).elf -> $(OBJS), am, $(LIBS)
所完成的工作.
现在就具备了生成 image
的条件了, 执行 riscv32-nemu.mk
Makefile 中 image -> $(IMAGE).elf
依赖关系所指定的命令了, 执行命令: riscv64-linux-gnu-objcopy -S --set-section-flags .bss=alloc,contents -O binary /home/jiunian/Program/ysyx-workbench/am-kernels/tests/cpu-tests/build/add-longlong-riscv32-nemu.elf /home/jiunian/Program/ysyx-workbench/am-kernels/tests/cpu-tests/build/add-longlong-riscv32-nemu.bin
. 该命令提取出 elf 文件中的代码段和全局变量保存到 bin
文件中, 供 nemu
读取.
接下来的操作就是在 nemu
目录下执行 make -C /home/jiunian/Program/ysyx-workbench/nemu ISA=riscv32 run ARGS="-l /home/jiunian/Program/ysyx-workbench/am-kernels/tests/cpu-tests/build/nemu-log.txt -b" IMG=/home/jiunian/Program/ysyx-workbench/am-kernels/tests/cpu-tests/build/add-longlong-riscv32-nemu.bin
来对 nemu
进行编译
输入输出
- 理解
mainargs
:$ISA-nemu
: 通过-DMAINARGS=""
向main
传参native
: 使用getenv
在程序中获得环境变量
RTFSC
打字游戏
- 主函数框架:
- 相关的数据结构
- 字符结构体:
ch
: 字符x, y
: 位置v
: 速度, 每一帧下落的距离t
:
texture
: 26 个字母对应的材质, 包括了三种颜色, 为白绿红三色blank
: 空白区域, 紫色
- 字符结构体:
- 键盘按键到字符的 lut:
char lut[256] = { [AM_KEY_A] = 'A', [AM_KEY_B] = 'B', [AM_KEY_C] = 'C', [AM_KEY_D] = 'D', [AM_KEY_E] = 'E', [AM_KEY_F] = 'F', [AM_KEY_G] = 'G', [AM_KEY_H] = 'H', [AM_KEY_I] = 'I', [AM_KEY_J] = 'J', [AM_KEY_K] = 'K', [AM_KEY_L] = 'L', [AM_KEY_M] = 'M', [AM_KEY_N] = 'N', [AM_KEY_O] = 'O', [AM_KEY_P] = 'P', [AM_KEY_Q] = 'Q', [AM_KEY_R] = 'R', [AM_KEY_S] = 'S', [AM_KEY_T] = 'T', [AM_KEY_U] = 'U', [AM_KEY_V] = 'V', [AM_KEY_W] = 'W', [AM_KEY_X] = 'X', [AM_KEY_Y] = 'Y', [AM_KEY_Z] = 'Z', };
- 函数:
video_init
: 初始化每个字母的材质和空白格, 并且将画面铺满空白check_hit
: 根据键盘的输入找到屏幕上匹配的字符, 如果没有找到,wrong++
, 否则, 找出y
坐标轴最低的字符 (屏幕上可能同时出现多个相同字母), 且hit ++
, 且修改字符的速度, 使其向上
- 运行过程:
- 通过
nemu
运行编译链接过后的打字游戏 - 在打字游戏中调用
ioe_init
对am
中的输入输出扩展进行初始化, - 画面输出部分:
- 在打字游戏的程序中调用
io_write
, 调用__am_gpu_fbdraw
修改nemu
中的vmem
写入像素数据, 并且置sync
为高, 然后在nemu
的设备更新时候, 会检查到sync
为高, 使用 sdl 库更新输出的画面
- 在打字游戏的程序中调用
- 按键: 键盘的事件使用了 sdl 来实现, 在
update_device
时候, 会监测是否有按下键盘的事件发生, 若发生了键盘按下的事件, 则会将按下的键通过send_key
将其放入一个队列中, 当应用程序通过io_read
读取键盘输入时候, 就用调用am
中的__am_input_keybrd
函数, 在这个函数中, 首先读取KBD_ADDR
寄存器, 对这个寄存器的访问会触发回调函数的执行, 在对应的回调函数中, 会将键盘队列中的队头出队, 存储在i8042_data_port_base[0]
位置上, 此时程序就可以读取到键盘被按下的键了, 接下来返回到应用程序就可以了, 剩余的处理交给应用程序进行处理.[!note] SDL 事件 在 SDL 中, 会维护一个事件的队列, 当我们调用
SDL_PollEvent
时候会取出这个队列中队头的事件, 这样我们就可以针对不同的事件对程序进行处理了, 详情请见man SDL_Event
- 通过
必做题
程序是个状态机理解 YEMU 的执行过程
- 状态机与 YEMU: 状态机描述了 YEMU 的执行方式, YEMU 则按照状态机的描述实现
RTFSC 请整理一条指令在 NEMU 中的执行过程
取指 -> 译码 -> 执行 (写回)
与 isa 相关的定义在 isa_def.h
文件中, 包括了 riscv32_ISADecodeInfo
和 riscv32_CPU_state
执行一条指令的逻辑在对应的 isa_exec_once
中:
- 取指: 根据 pc
的值从内存中取出指令 (对于 riscv 来说可以忽略参数 len
, 因为每一条指令都是 4 字节的 (暂不考虑 rvc
), 每次都是取出 4 个字节的指令), 取指后, 先默认将 pc += 4
, 并且将取到的指令返回, 并存入到 s->isa.inst.val
中, 进行下一步的译码
- 译码: 译码和执行的逻辑一一对应, 因此将其写在一个函数中, 一起处理, 具体实现在 decode_exec
中, 先来介绍几个宏和函数:
- INSTPAT_START
: 生成译码开始标号, 这里用到了 gcc 的特性: [[GCC扩展-标签变量(Labels as Values)|标签变量]]
- INSTPAT_END
: 展开成译码结束标号, 用于生成标签变量以进行跳转操作, 译码出指令时候, 可以直接通过跳转结束译码
- INSTPAT_INST
: 用于从 Decode *s
中取出指令
- INSTPAT_MATCH
: 调用 decode_operand
和 inline pattern_decode
对指令进行译码:
- decode_operand
: 根据指令的类型, 从中提取出所需要的字段, 对于寄存器字段, 直接从寄存器中取出对应的值
- pattern_decode
: 是一段模式识别的代码, 利用这段代码, 可以获得该指令对应的 key
和 mask
和 shift
, 用于后续的比较
- INSTPAT
: 在这个宏展开中, 会首先进行 pattern_decode
, 也就是会先分析模式识别串, 以获得 key
, mask
和 shift
, 如果模式匹配符合的话, 再使用 decode_operand
提取出字段, 并且接下来执行相应的操作.
[!todo] 问题 这样的译码方式的性能似乎有点低, 每次都要重新提取模式识别串以获得
key
,mask
和shift
变量, 但如果有编译器的优化, 这些都是常量, 应该是可以在编译期计算出来的
程序如何运行理解打字小游戏如何运行
[[PA2#打字游戏]]
编译与链接 1
- 删除
static
, 剩余inline
: 使用inline
关键字声明的函数, 实际上并不会被当做一个真正的函数, 而是会被类似的处理为宏, 在编译期展开, 插入到调用"函数"的地方去, 因此, 这样的函数是不会出现在符号表中的. 以下的图片是仅删去了static
时,inst.o
和postcall.o
中的符号表, 由于其实际上都没有定义inst_fetch
符号, 自然就不会出现重定义的问题: - 删除
inline
, 剩余static
: 不会报错, 使用static
关键字声明的函数, 相当于private
, 只能在当前文件中调用, 属于私有, 即局部的, 不参与符号解析, 不会出现符号重定义问题. - 删除
static inline
, 剩余nothing
: 由于每个 c 文件都是独立编译的, 所以会产生重复定义的问题 (在独立编译的情况下, 用于避免重定义的#ifndef
宏失效了), 在链接时候就会出现重定义的问题 可以看到, 在最后的链接时候出现了重定义的问题, 我们再来查看inst.o
和hostcall.o
文件可以发现, 在这两个文件的符号表中都定义了inst_fetch
符号
编译与链接 2
当我在 common.h
和 debug.h
中声明 volatile static int dummy
但不进行初始化时候, 编译不会出现问题, 并不会出现 dummy
变量的实体, 因为声明但不初始化的静态全局变量属于弱符号, 会被添加到一个称为 COMMON
的伪节中, 在链接时候, 链接器会从这些弱符号中随机选择一个; 而当我们将 dummy
进行初始化时候, 就变为了强符号, 位于 .bss
节中, 链接器在链接的时候会检查 .bss
节中的符号是否有重定义