PA2报告

发布时间 2024-01-06 20:34:08作者: HiDark

PA2 报告


1.YEMU状态机

格式:(pc,r0,r1)
(0,x,x) -> (1,33,x) -> (2,33,33) -> (3,16,33) -> (4,49,33)-> (5,49,33)
  • YEMU如何执行一条指令:

​ 首先this.inst根据pc值从内存中取得指令,然后根据rtype的操作码op选择进行load/mov/add等操作,执行完一条指令后,让pc自加,这样就能读取下一个指令,进而顺序完成。

void exec_once() {
inst_t this;
this.inst = M[pc]; // 取指
switch (this.rtype.op) {
//  操作码译码       操作数译码           执行
 case 0b0000: { DECODE_R(this); R[rt]   = R[rs];   break; }
 case 0b0001: { DECODE_R(this); R[rt]  += R[rs];   break; }
 case 0b1110: { DECODE_M(this); R[0]    = M[addr]; break; }
 case 0b1111: { DECODE_M(this); M[addr] = R[0];    break; }
 default:
   printf("Invalid instruction with opcode = %x, halting...\n", this.rtype.op);
   halt = 1;
   break;
}
pc ++; // 更新PC
}

2.指令在NEMU中的执行过程.

​ 分析指令执行过程主要源于cpu_exec.c文件

  1. nemu-main.c为顶层,初始化monitor后运行engine_start()
int main(int argc, char *argv[]) {
 /* Initialize the monitor. */
#ifdef CONFIG_TARGET_AM
 am_init_monitor();
#else
 init_monitor(argc, argv);
#endif
 /* Start engine. */
 engine_start();
 return is_exit_status_bad();
}

  1. 默认进入sdb,sdb为自制调试器.
void engine_start() {
#ifdef CONFIG_TARGET_AM
 cpu_exec(-1);
#else
 /* Receive commands from user. */
 sdb_mainloop();
#endif
}
  1. sdb具体到控制指令执行,是调用cpu_exec(),检查nemu状态,然后调用execute(n)
void cpu_exec(uint64_t n) {
  g_print_step = (n < MAX_INST_TO_PRINT);
  switch (nemu_state.state) {
    case NEMU_END: case NEMU_ABORT:
      printf("Program execution has ended. To restart the program, exit NEMU and run again.\n");
      return;
    default: nemu_state.state = NEMU_RUNNING;
  }
  uint64_t timer_start = get_time();
  execute(n);
  uint64_t timer_end = get_time();
  g_timer += timer_end - timer_start;
  switch (nemu_state.state) {
    case NEMU_RUNNING: nemu_state.state = NEMU_STOP; break;
    case NEMU_END: case NEMU_ABORT:
        Log("nemu: %s at pc = " FMT_WORD,
          (nemu_state.state == NEMU_ABORT ? ANSI_FMT("ABORT", ANSI_FG_RED) :
           (nemu_state.halt_ret == 0 ? ANSI_FMT("HIT GOOD TRAP", ANSI_FG_GREEN) :
            ANSI_FMT("HIT BAD TRAP", ANSI_FG_RED))),
          nemu_state.halt_pc);
#ifdef CONFIG_ITRACE
        ibuf_printf();
#endif        
      // fall through
    case NEMU_QUIT: statistic();
  }
}
  1. 该函数包括指令的执行、trace和difftest以及设备的更新。
static void execute(uint64_t n) {
  Decode s;
  for (;n > 0; n --) {
    exec_once(&s, cpu.pc);// 运行!!!
    g_nr_guest_inst ++;  // 记录指令数
    trace_and_difftest(&s, cpu.pc);
    if (nemu_state.state != NEMU_RUNNING) break;
    IFDEF(CONFIG_DEVICE, device_update());
  }
}

exec_once()是有关指令运行的函数,并复制cpu.pc 到结构体s后,执行isa_exec_once(),到这一步才算真正进入CPU的运行范围。由此进入更新PC,取指,译码匹配,执行,详见src/isa/riscv32/inst.c

int isa_exec_once(Decode *s) {
  s->isa.inst.val = inst_fetch(&s->snpc, 4);
  return decode_exec(s);
}

在这里先获取指令i = s->isa.inst.val,然后decode_operand()进行译码,获取立即数、寄存器源操作数、rd、rs等,然后进行模式匹配,以addi为例

INSTPAT("??????? ????? ????? 000 ????? 00100 11", addi , I, R(rd) = src1 + imm);

匹配fun子串后执行,匹配成功后跳转到INSTPAT_END()


static int decode_exec(Decode *s) {
  uint32_t i = s->isa.inst.val;

  unsigned int shamt = BITS(i, 24, 20);
  int rd = 0;
  word_t src1 = 0, src2 = 0, imm = 0;
  s->dnpc = s->snpc; // ,默认不跳转

#define INSTPAT_INST(s) ((s)->isa.inst.val)
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \
  decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \
  __VA_ARGS__ ; \
}

  INSTPAT_START();
 // INSTPAT(模式字符串, 指令名称, 指令类型, 指令执行操作); 指令名称对应INSTPAT——MATCH的name,只起到注释作用

  INSTPAT("0000000 00001 00000 000 00000 11100 11", ebreak , N, NEMUTRAP(s->pc, R(10))); // R(10) is $a0
  /***MY CODE***/
.......................

  // 设定标签地址的意义结合INSTPAT,一旦找到符合的指令,就跳到末尾,否则进行下一条指令的匹配
  INSTPAT("??????? ????? ????? ??? ????? ????? ??", inv    , N, INV(s->pc));
  INSTPAT_END();
  R(0) = 0; // reset $zero to 0
  return 0;
}

3. 编译与链接

有两个问题

  • 问题一

nemu/include/cpu/ifetch.h中, 你会看到由static inline开头定义的inst_fetch()函数. 分别尝试去掉static, 去掉inline或去掉两者, 然后重新进行编译, 你可能会看到发生错误. 请分别解释为什么这些错误会发生/不发生? 你有办法证明你的想法吗?

全部去掉后会报错,因为函数的定义在头文件,一旦超过一个文件包含,就会重复定义.而保留任何一个都不会出错。

  1. 加入static不会报错,这样每次包含头文件,都会产生一个私有的函数。

  2. 加入inline后也不会报错,编译时内联函数被打平,便不存在调用过程。

    关键字 inline 告诉编译器,任何地方只要调用内联函数,就直接把该函数的机器码插入到调用它的地方。这样程序执行更有效率,就好像将内联函数中的语句直接插入到了源代码文件中需要调用该函数的地方一样。这样,对与需要经常调用的函数来说,免去频繁的跳转,大大提升效率。

证明:对于inline来说,使cpu_test测试集中的check()函数加入inline,编译后的elf文件中check()函数就会被打平消失.

  • 问题二
  1. nemu/include/common.h中添加一行volatile static int dummy; 然后重新编译NEMU. 请问重新编译后的NEMU含有多少个dummy变量的实体? 你是如何得到这个结果的?
  2. 添加上题中的代码后, 再在nemu/include/debug.h中添加一行volatile static int dummy; 然后重新编译NEMU. 请问此时的NEMU含有多少个dummy变量的实体? 与上题中dummy变量实体数目进行比较, 并解释本题的结果.
  3. 修改添加的代码, 为两处dummy变量进行初始化:volatile static int dummy = 0; 然后重新编译NEMU. 你发现了什么问题? 为什么之前没有出现这样的问题? (回答完本题后可以删除添加的代码.)
  1. 有两种方法,一种是在编译出来的.i文件中直接找,另外就是包含了common.h的必定会新增一个实体。

    在nemu目录find ./build -type f | xargs grep "volatile static int dummy" | wc -l得到25条。

  2. 会变成50,因为debug.h本身就被common.h包含,因此翻倍.

  3. 会发生重定义问题.赋初值就是定义而非声明。?

4.了解Makefile

​ 请描述你在am-kernels/kernels/hello/目录下敲入make ARCH=$ISA-nemu 后, make程序如何组织.c和.h文件, 最终生成可执行文件am-kernels/kernels/hello/build/hello-$ISA-nemu.elf. (这个问题包括两个方面:Makefile的工作方式和编译链接的过程.) 关于Makefile工作方式的提示:

  • Makefile中使用了变量, 包含文件等特性
  • Makefile运用并重写了一些implicit rules
  • man make中搜索-n选项, 也许会对你有帮助
  • RTFM
  1. hello/Makefile解析

    指定目标文件,并引导进入AM主目录下的Makefile

    NAME = hello
    SRCS = hello.c
    include $(AM_HOME)/Makefile
    
  2. AM Makefile解析

    • 第一步是环境检查和解析参数
    ## 1. Basic Setup and Checks
    
    ### Default to create a bare-metal kernel image
    ifeq ($(MAKECMDGOALS),)
      MAKECMDGOALS  = image
      .DEFAULT_GOAL = image
    endif
    
    ### Override checks when `make clean/clean-all/html`
    ifeq ($(findstring $(MAKECMDGOALS),clean|clean-all|html),)
    
    ### Print build info message
    $(info # Building $(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
    
    ### Check: environment variable `$ARCH` must be in the supported list
    # filter ,保留只符合$(ARCHS)的文件
    # notdir ,去掉目录
    # basename ,去掉目录和后缀
    ARCHS = $(basename $(notdir $(shell ls $(AM_HOME)/scripts/*.mk)))
    ifeq ($(filter $(ARCHS), $(ARCH)), )
      $(error Expected $$ARCH in {$(ARCHS)}, Got "$(ARCH)")
    endif
    
    ### Extract instruction set architecture (`ISA`) and platform from `$ARCH`. Example: `ARCH=x86_64-qemu -> ISA=x86_64; PLATFORM=qemu`
    ARCH_SPLIT = $(subst -, ,$(ARCH))
    ISA        = $(word 1,$(ARCH_SPLIT))
    PLATFORM   = $(word 2,$(ARCH_SPLIT))
    
    ### Check if there is something to build
    # $(flavor var) (var不需要加$)是检测var是否在makefile及其include中是否存在
    # 不存在输出undefine
    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     = $(abspath $(IMAGE_REL))
    ARCHIVE   = $(WORK_DIR)/build/$(NAME)-$(ARCH).a
    
    ### Collect the files to be linked: object files (`.o`) and libraries (`.a`)
    # addsuffix添加后缀.o ,addprefix添加前缀路径
    OBJS      = $(addprefix $(DST_DIR)/, $(addsuffix .o, $(basename $(SRCS))))
    # 因为=是全部展开再赋值,这里赋值的有它本身的内容就会导致无限展开。
    # sort 是按降序排列 
    LIBS     := $(sort $(LIBS) am klib) # lazy evaluation ("=") causes infinite recursions
    #添加静态库 
    #addsuffix和addprefix先给$(LIBS)添加前后缀,然后用join连接,最后加前缀$ARCH.a
    # 举个例子,最后是 $(AM_HOME)/build/$(LIBS)/$(LIBS)-$(ARCH).a
    LINKAGE   = $(OBJS) \
      $(addsuffix -$(ARCH).a, $(join $(addsuffix /build/, $(addprefix $(AM_HOME)/, $(LIBS))),  \
      $(LIBS) ))
    
    • 第三步是设置编译器(交叉编译)
    ## 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
    INC_PATH += $(WORK_DIR)/include $(addsuffix /include/, $(addprefix $(AM_HOME)/, $(LIBS)))
    INCFLAGS += $(addprefix -I, $(INC_PATH))
    
    
    ARCH_H := arch/$(ARCH).h
    # tr a-z A-Z 是将小写转化为大写
    # -D 把宏定义传给C
    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
    CXXFLAGS +=  $(CFLAGS) -ffreestanding -fno-rtti -fno-exceptions
    ASFLAGS  += -MMD $(INCFLAGS)
    LDFLAGS  += -z noexecstack
    
    • 第四步是指定交叉编译架构
    ## 4. Arch-Specific Configurations
    
    ### Paste in arch-specific configurations (e.g., from `scripts/x86_64-qemu.mk`)
    # -include的-,即使没找到也不会报错而是继续执行
    -include $(AM_HOME)/scripts/$(ARCH).mk
    # 没有交叉编译链就默认native架构
    ### Fall back to native gcc/binutils if there is no cross compiler
    ifeq ($(wildcard $(shell which $(CC))),)
      $(info #  $(CC) not found; fall back to default gcc and binutils)
      CROSS_COMPILE :=
    
    endif
    

    riscv32-nemu.mk为例

    # 又添加各自的规则
    include $(AM_HOME)/scripts/isa/riscv.mk  
    include $(AM_HOME)/scripts/platform/nemu.mk
    CFLAGS  += -DISA_H=\"riscv/riscv.h\"
    #-march 是指令集拓展的包含,有i[F|M|A|D],-mabi是abi标准
    COMMON_CFLAGS += -march=rv32im_zicsr -mabi=ilp32  # overwrite
    LDFLAGS       += -melf32lriscv                     # overwrite
    
    AM_SRCS += riscv/nemu/start.S \
               riscv/nemu/cte.c \
               riscv/nemu/trap.S \
               riscv/nemu/vme.c
    
    • 第五步是编译
    ## 5. Compilation Rules
    
    ### Rule (compile): a single `.c` -> `.o` (gcc)
    # && 是左边返回0(执行成功)再执行右边
    # || 是左边返回非0(执行失败)再执行右边
    # %.o : %.c 依次构建,
    # -c选项指定只编译源文件而不链接生成可执行文件。-o $@选项指定生成的目标文件的名称。$(realpath $<)将源文件的路径转换为绝对路径。
    $(DST_DIR)/%.o: %.c
    	@mkdir -p $(dir $@) && echo + CC $<
    	@$(CC) -std=gnu11 $(CFLAGS) -c -o $@ $(realpath $<)
    # 预编译宏定义
    	@$(CC) $(CFLAGS) -E -MF /dev/null $< | clang-format > $@.i
    ### 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 $<)
    
    
    ### Rule (recursive make): build a dependent library (am, klib, ...)
    # $(MAKE)变量表示调用make命令,-s选项表示静默模式,不显示命令的详细输出。-C $(AM_HOME)/$*选项表示在指定的目录下调用make命令,$(AM_HOME)/$*表示根据匹配到的目标文件生成对应的目录路径。archive表示在该目录下执行Makefile中的archive目标。
    $(LIBS): %:
    	@$(MAKE) -s -C $(AM_HOME)/$* archive
    
    ### Rule (link): objects (`*.o`) and libraries (`*.a`) -> `IMAGE.elf`, the final ELF binary to be packed into image (ld)
    # 生成可执行文件
    # --start-group $(LINKAGE) --end-group 指定顺序
    $(IMAGE).elf: $(OBJS) am $(LIBS)
    	@echo + LD "->" $(IMAGE_REL).elf
    	@$(LD) $(LDFLAGS) -o $(IMAGE).elf --start-group $(LINKAGE) --end-group
    
    ### Rule (archive): objects (`*.o`) -> `ARCHIVE.a` (ar)
    # 建立静态库
    $(ARCHIVE): $(OBJS)
    	@echo + AR "->" $(shell realpath $@ --relative-to .)
    	@$(AR) rcs $(ARCHIVE) $(OBJS)
    
    ### Rule (`#include` dependencies): paste in `.d` files generated by gcc on `-MMD`
    -include $(addprefix $(DST_DIR)/, $(addsuffix .d, $(basename $(SRCS))))
    
    • 构建完成后,到$(AM_HOME)/scripts/platform/nemu.mk
    AM_SRCS := platform/nemu/trm.c \
               platform/nemu/ioe/ioe.c \
               platform/nemu/ioe/timer.c \
               platform/nemu/ioe/input.c \
               platform/nemu/ioe/gpu.c \
               platform/nemu/ioe/audio.c \
               platform/nemu/ioe/disk.c \
               platform/nemu/mpe.c
    
    CFLAGS    += -fdata-sections -ffunction-sections
    LDFLAGS   += -T $(AM_HOME)/scripts/linker.ld \
                 --defsym=_pmem_start=0x80000000 --defsym=_entry_offset=0x0
    LDFLAGS   += --gc-sections -e _start
    NEMUFLAGS += -l $(shell dirname $(IMAGE).elf)/nemu-log.txt
    NEMUFLAGS += -b  -e $(IMAGE).elf
    CFLAGS += -DMAINARGS=\"$(mainargs)\"
    CFLAGS += -I$(AM_HOME)/am/src/platform/nemu/include
    .PHONY: $(AM_HOME)/am/src/platform/nemu/trm.c
    
    image: $(IMAGE).elf
    	@$(OBJDUMP) -d $(IMAGE).elf > $(IMAGE).txt
    	@echo + OBJCOPY "->" $(IMAGE_REL).bin
    	@$(OBJCOPY) -S --set-section-flags .bss=alloc,contents -O binary $(IMAGE).elf $(IMAGE).bin
    
    run: image
    	$(MAKE) -C $(NEMU_HOME) ISA=$(ISA) run ARGS="$(NEMUFLAGS)" IMG=$(IMAGE).bin ELF=$(IMAGE).elf
    
    gdb: image
    	$(MAKE) -C $(NEMU_HOME) ISA=$(ISA) gdb ARGS="$(NEMUFLAGS)" IMG=$(IMAGE).bin ELF=$(IMAGE).elf