NEMU PA2 - 补充内容

image

from pixiv

基础设施(2)

bug诊断的利器 - 踪迹

函数调用的踪迹 - ftrace

image

  • 在我们执行make ARCH=$ISA-nemu ALL=xxx run时,通过查看Makefile可以发现Makefile帮我们实现了传入ELF文件到NEMU,具体表现为Makefile最终的执行程序命令为:

    /home/cilinmengye/ics2023/nemu/build/riscv32-nemu-interpreter -l /home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/nemu-log.txt -b -e /home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.elf /home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.bin
    

    可以看到-e后的参数就是elf文件的地址了,我们只要1.在parse_args()处识别-e选项,接收elf文件地址 2.打开elf文件准备解析elf文件

  • 根据翻阅RISCV手册,可以先保持KISS原则,认为:

    • 所有写回 x1(ra)JAL/JALR 都是“函数调用”(伪指令 call)。即JAL/JALR指令的目的寄存器都是ra(rd == ra)
      image
      image

    • ret指令有固定的格式,即opcode == jalr && imm == 0 && rs1 == ra/x1 && rd == x0
      image

知道如何识别call和ret指令后就很好做了,我们接下来只要到/home/cilinmengye/ics2023/nemu/src/isa/riscv32/inst.c下(即NEMU识别指令的C文件)插入相关对call和ret的识别代码即可

识别到call和ret指令后我们需要什么相关信息?

  • 对call指令我们需要知道1.指令地址 2.跳转地址 3.跳转到的地址对应函数的名称
  • 对ret指令我们需要知道1.指令地址 2.返回时是从哪个函数返回的,这个函数的名称是什么(即执行ret指令时指令地址是属于哪个函数的)

对于上面的问题其实都很好解决:

  • 执行call指令的指令地址就是当前的PC值,通过访问PC寄存器我们很容易得到
  • 执行call指令时要跳转到的地址可以很容易依据指令jal/jalr的行为计算出来
  • 跳转到的地址对应函数的名称得到方法为依据计算出来的跳转地址,通过解析ELF文件,查找跳转地址在ELF符号表中哪个符号的地址范围,再通过查找字符串表找到这个符号的字符串
  • 执行ret指令的指令地址就是当前的PC值,依据指令地址去解析ELF文件查找指令地址是属于哪个函数,步骤与上述相同

依据提供的地址,解析ELF文件返回所述函数的字符串实现

# ics2023/nemu/src/utils/trace.c
char* find_symbol_by_addr(Elf* elf, vaddr_t addr) {
  char* ret = NULL;
  Elf_Scn *scn = NULL;        // 当前遍历到的节 (Section)
  Elf_Data *sym_data = NULL;  // 指向符号表数据的指针
  Elf_Data *str_data = NULL;  // 指向符号名字符串表数据的指针
  GElf_Shdr sym_shdr;         // 用于保存当前符号表节头
  size_t sym_count = 0;       // 符号表中总的符号数量

  // 遍历所有节,寻找符号表节
  while ((scn = elf_nextscn(elf, scn)) != NULL) {
    // 读取当前节的节头信息到 sym_shdr
    if (gelf_getshdr(scn, &sym_shdr) == NULL) Assert(0, "gelf_getshdr failed");
    // 判断是否为符号表节
    if (sym_shdr.sh_type == SHT_SYMTAB) {
      // 获取符号表本身的数据指针
      sym_data = elf_getdata(scn, NULL);
      Assert(sym_data != NULL, "Get symbol table fail");
      // 获取字符串表,关联的字符串表位于节索引 sym_shdr.sh_link
      Elf_Scn *str_scn = elf_getscn(elf, sym_shdr.sh_link);
      Assert(str_scn != NULL, "Failed to get string table section");
      str_data = elf_getdata(str_scn, NULL);
      break;
    }
  }
  if (sym_data == NULL) Assert(0, "No symbol table found in ELF file");
  if (str_data == NULL) Assert(0, "No string table found in ELF file");
  // 计算符号条目数量 = 节大小 / 每条目大小
  sym_count = sym_shdr.sh_size / sym_shdr.sh_entsize;
  // 因为有可能这个地址并非是函数符号地址,所以需要记录下
  // 在符号表条目中查找地址最接近且不大于 addr 的函数符号
  for (size_t i = 0; i < sym_count; i++) {
    GElf_Sym sym;
    gelf_getsym(sym_data, i, &sym);
    // 只关注函数类型的符号
    if (GELF_ST_TYPE(sym.st_info) != STT_FUNC) continue;
    vaddr_t start = (vaddr_t)sym.st_value;
    vaddr_t end   = (vaddr_t)(start + sym.st_size);
    if (addr >= start && addr < end) {
      // 获取其在字符串表中的地址
      ret = (char*)str_data->d_buf + sym.st_name;
      return ret;
    }
  }
  return ret;
}

然后我们需要思考的问题是:我们要如何输出格式,以及应该输出到哪?

  • 对于输出格式,我将模仿讲义中的实现
  • 对于输出到哪,我将输出到nemu的日志文件中,可以通过nemu提供的APIlog_write函数实现
# ics2023/nemu/src/utils/trace.c
#ifdef CONFIG_FTRACE

int formBlank = 1;
char elfbuf[512];

void ftraceInst_get(char* type, vaddr_t instAddr, vaddr_t toAddr) {
  if (strcmp(type, "ret") == 0) formBlank--;
  char *sym_name = find_symbol_by_addr(elf, toAddr);
  if (sym_name == NULL) sym_name = "???";
  // 然后将内容输出到log_file中
  char* p = elfbuf;
  // 先输出指令地址
  p += snprintf(p, sizeof(elfbuf), FMT_WORD ":", instAddr);
  // 再输出层次空格
  for (int i = 0; i < formBlank; i++) p += snprintf(p, sizeof(elfbuf), " ");
  // 再输出主体内容
  p += snprintf(p, sizeof(elfbuf), "%s [%s@0x%08x]\n", type, sym_name, toAddr);
  Assert((p - elfbuf ) <= 512, "Ftrace elfbuf overflow");
  // 然后将p中的内容输出到log_file中
  log_write("%s", elfbuf);
  if (strcmp(type, "call") == 0) formBlank++;
}

/*
 * 需要初始化一下elf文件
 */
void init_ftrace(const char *elf_file) {
  Assert(elf_file != NULL, "ELF file can't be NULL");
  Assert(elf_version(EV_CURRENT) != EV_NONE, "ELF library initialization failed");
  int elf_fp = open(elf_file, O_RDONLY);
  Assert(elf_fp >= 0, "Failed to open ELF file");
  Log("ELF file path is %s", elf_file);
  elf = elf_begin(elf_fp, ELF_C_READ, NULL);
  Assert(elf != NULL, "elf_begin failed");

  snprintf(elfbuf, sizeof(elfbuf), "[Starting Ftrace]\n");
  log_write("%s", elfbuf);
}
#endif

当然为了开启Ftrace的配置我们需要到ics2023/nemu/Kconfig文件中更改下加上Ftrace选项:

config FTRACE
  bool "Enable ftrace"
  default n

然后输出的部分结果为,以make ARCH=$ISA-nemu ALL=recursion run为例:

0x8000000c: call [_trm_init@0x80000254]
0x80000264:  call [main@0x800001c8]
0x800001e8:   call [f0@0x80000010]
0x8000016c:    call [f2@0x800000a4]
0x800000f0:     call [f1@0x8000005c]
0x8000016c:      call [f2@0x800000a4]
0x800000f0:       call [f1@0x8000005c]
0x8000016c:        call [f2@0x800000a4]
0x800000f0:         call [f1@0x8000005c]
0x8000016c:          call [f2@0x800000a4]
0x800000f0:           call [f1@0x8000005c]
0x8000016c:            call [f2@0x800000a4]
0x800000f0:             call [f1@0x8000005c]
0x80000058:             ret [f0@0x80000058]
0x80000100:            ret [f2@0x80000100]
0x80000180:            call [f2@0x800000a4]
0x800000f0:             call [f1@0x8000005c]
0x80000058:             ret [f0@0x80000058]
0x80000100:            ret [f2@0x80000100]
0x800001a8:           ret [f3@0x800001a8]
0x80000100:          ret [f2@0x80000100]
0x80000180:          call [f2@0x800000a4]
0x800000f0:           call [f1@0x8000005c]
0x8000016c:            call [f2@0x800000a4]
0x800000f0:             call [f1@0x8000005c]
0x80000058:             ret [f0@0x80000058]
0x80000100:            ret [f2@0x80000100]
0x80000180:            call [f2@0x800000a4]
0x800000f0:             call [f1@0x8000005c]
0x80000058:             ret [f0@0x80000058]
0x80000100:            ret [f2@0x80000100]
0x800001a8:           ret [f3@0x800001a8]
0x80000100:          ret [f2@0x80000100]
0x800001a8:         ret [f3@0x800001a8]
0x80000100:        ret [f2@0x80000100]
...

尾调用问题

0x8000000c: call [_trm_init@0x80000260]
0x80000270:   call [main@0x800001d4]
0x800001f8:     call [f0@0x80000010]
0x8000016c:       call [f2@0x800000a4]
0x800000e8:         call [f1@0x8000005c]
0x8000016c:           call [f2@0x800000a4]
0x800000e8:             call [f1@0x8000005c]
0x8000016c:               call [f2@0x800000a4]
0x800000e8:                 call [f1@0x8000005c]
0x8000016c:                   call [f2@0x800000a4]
0x800000e8:                     call [f1@0x8000005c]
0x8000016c:                       call [f2@0x800000a4]
0x800000e8:                         call [f1@0x8000005c]
0x80000058:                         ret  [f0]              # 注释(2)
0x800000fc:                       ret  [f2]                # 注释(1)
0x80000180:                       call [f2@0x800000a4]
0x800000e8:                         call [f1@0x8000005c]
0x80000058:                         ret  [f0]
0x800000fc:                       ret  [f2]
0x800001b0:                     ret  [f3]                  # 注释(3)

image

尾调用(Tail Call)指的是:一个函数在它返回前的最后一步是调用另一个函数,而且它本身不会再做任何工作。当编译器发现函数的“最后一步”是尾调用,就可以做优化:

  • 不用保存返回地址(不用再留出一层栈帧)
  • 直接跳转到被调用函数
  • 共享当前栈帧,不会造成栈增长

例如下面这个代码:

int f1(int x) {
    return f0(x);  // 尾调用 f0,调用后就直接返回 f0 的返回值
}

从汇编角度指令如下:

jalr x0, f0       // 尾调用 f0,不保存返回地址(x0)!

所以这条指令在我的实现中并不能被识别出为call指令,因为我的实现call指令的目的寄存器rd == ra,这里其rd == x0即没有保存返回地址(在RISCV中ra寄存器是用到暂存函数返回地址的)


image
image

发生尾调用时栈中的变化可以如下图理解:

image

梳理下所以上述的输出为:

call [f2]
 call [f1]
 ret [f0]
ret [f2]

Differential Testing

问题: 我们在编写NEMU和AM时,因为我们同时编写了硬件(NEMU)和软件(AM), 当发生错误时我们并不知道到底是硬件出错误了还是软件出错误了


控制变量,保证其中一方一定是正确的,测试不一定正确的一方

测试软件

当测试软件AM时,我们可以使用make ARCH=native run进行构建项目,目的是让我们编写的软件AM运行在一定正确的硬件--真机上

运行在真机上需要主要编写可移植的程序
image

进行测试当然需要测试程序,在NEMU项目中测试程序在ics2023/am-kernels, 其中测试am库的测试程序在ics2023/am-kernels/tests/am-tests


讲义介绍了编写测试程序的方法,即依据程序的特性进行编写测试。

memset为例,该函数作用可以看做是对数组中的一段连续区间进行写入

考虑如下初始化一个数组:

void reset() {
  int i;
  for (i = 0; i < N; i ++) {
    data[i] = i + 1;
  }
}

当我们调用memset对数组一段区间进行写时,验证memset实现是否正确则把预期的输出分成三段来检查:

  • 第一段是函数写入区间的左侧, 这一段区间没有被写入, 因此应该有assert(data[i] == i + 1)
  • 第二段是函数写入的区间本身, 这一段区间的预期结果和函数的具体行为有关
  • 第三段是函数写入区间的右侧, 这一段区间没有被写入, 因此应该有assert(data[i] == i + 1)

那么我们就可以写一个测试程序,遍历区间[l, r]是数组data要被memset写入的区间,然后依据上述的方法检查三段区间[0, l), [l, r], (r, dataLen]是否都合理

这样问题变成了一个编程题目了,ACMer的强项地方了:

void test_memset() {
  int l, r;
  for (l = 0; l < N; l ++) {
    for (r = l + 1; r <= N; r ++) {
      reset();
      uint8_t val = (l + r) / 2;
      memset(data + l, val, r - l);
      check_seq(0, l, 1);
      check_eq(l, r, val);
      check_seq(r, N, r + 1);
    }
  }
}

am-kernels下并没有测试klib库的代码,为此讲义要求我们为其编写程序:
image

测试硬件

基本思想为:找一个实现好的正确的模拟器/真机与nemu一样,在初始时将状态(通用寄存器和PC)设置与nemu相同,执行相同的指令,并对比状态的不同

若状态不同则说明nemu某些地方实现出问题了.

这实际上是一种非常奏效的测试方法, 在软件测试领域称为differential testing(后续简称DiffTest). 通常来说, 进行DiffTest需要提供一个和DUT(Design Under Test, 测试对象) 功能相同但实现方式不同的REF(Reference, 参考实现), 然后让它们接受相同的有定义的输入, 观测它们的行为是否相同.

这里DUT为nemu,REF为spike

Spike是RISC-V社区的一款全系统模拟器

Spike模拟器在哪?

先看Makefile

nemu/tools/difftest.mk中已经设置了相应的规则和参数, 会自动进入nemu/tools/下的相应子目录(kvm-diff, qemu-diff或spike-diff)编译动态库, 并把其作为NEMU的--diff选项的参数传入.

  • nemu/include/generated/autoconf.h, 阅读C代码时使用
  • nemu/include/config/auto.conf, 阅读Makefile时使用

发现:

  • CONFIG_DIFFTEST_REF_PATH="tools/spike-diff"

ics2023/nemu/src/monitor/monitor.c-->依据参数-d设置diff_so_file文件路径,difftest_port,调用init_difftest(diff_so_file, img_size, difftest_port)

ics2023/nemu/tools/spike-diff/Makefile下git clone spike模拟器的代码

make menuconfig后在nemu目录下执行make ARCH=$ISA-nemu run会执行make -s -C /home/cilinmengye/ics2023/nemu/tools/spike-diff GUEST_ISA=riscv32 SHARE=1 ENGINE=interpreter

g++
 -std=c++17
 -O2
 -shared
 -fPIC
 -fvisibility=hidden
 -Irepo
 -Irepo/fesvr
 -Irepo/riscv
 -Irepo/disasm
 -Irepo/customext
 -Irepo/fdt
 -Irepo/softfloat
 -Irepo/spike_main
 -Irepo/spike_dasm
 -Irepo/build
 -I/home/cilinmengye/ics2023/nemu/include
 difftest.cc
 repo/build/libspike_main.a
 repo/build/libriscv.a
 repo/build/libdisasm.a
 repo/build/libsoftfloat.a
 repo/build/libfesvr.a
 repo/build/libfdt.a
 -o
 build/riscv32-spike-so


/home/cilinmengye/ics2023/nemu/build/riscv32-nemu-interpreter --log=/home/cilinmengye/ics2023/nemu/build/nemu-log.txt --diff=/home/cilinmengye/ics2023/nemu/tools/spike-diff/build/riscv32-spike-so
  1. 让NEMU和Spike模拟器状态相同

    1. diff_so_file文件是动态库文件,由ics2023/nemu/tools/spike-diff中的C文件编译动态库, 并把其作为NEMU的--diff选项的参数传入
    2. 打开DiffTest后, nemu/src/cpu/difftest/dut.c中的init_difftest()会额外进行以下初始化工作:
      • 打开传入的动态库文件ref_so_file.
      • 通过动态链接对动态库中的上述API符号进行符号解析和重定位, 返回它们的地址.
      • 对REF的DIffTest功能进行初始化, 具体行为因REF而异.
      • 将DUT的guest memory拷贝到REF中.
      • 将DUT的寄存器状态拷贝到REF中.
  2. 在NEMU中执行完一条指令后, 就在difftest_step()中让REF执行相同的指令,然后读出REF中的寄存器, 并进行对比. ics2023/nemu/src/cpu/difftest/dut.c属于nemu,所以这份代码会运行在真机上

    # difftest_step()代码在不考虑校准的指令的情况下,代码简化为:
    void difftest_step(vaddr_t pc, vaddr_t npc) {
      CPU_state ref_r;
      ref_difftest_exec(1);
      ref_difftest_regcpy(&ref_r, DIFFTEST_TO_DUT);
      checkregs(&ref_r, pc);
    }
    

    ref_difftest_exec(1); ref_difftest_regcpy(&ref_r, DIFFTEST_TO_DUT);会调用被编译链接成动态库/home/cilinmengye/ics2023/nemu/tools/spike-diff/build/riscv32-spike-so中的代码

共享库的加载方法

在 Linux 下,我们确实有两种常见的“使用共享库(.so)”的方法:

  1. 链接时动态链接(隐式加载)

    g++ main.o -o myapp -L/path/to/lib -lfoo
    
    • -L/path/to/mylibs:把 /path/to/mylibs 加入到链接器搜索路径
    • -lfoo:链接 libfoo.so 或 libfoo.a
    • 编译/链接阶段就把 libfoo.so 的符号(函数、变量)解析进来,生成的可执行文件在运行时会自动去加载这个库。
    • 运行时不需要再写任何代码,所有共享库依赖都已经记录在可执行文件中。
  2. 运行时动态加载(显式加载)

    void *h = dlopen("libfoo.so", RTLD_LAZY);
    auto fn = (foo_t)dlsym(h, "foo");
    fn();
    
    • 编译时只需加 -ldl,不提 libfoo.so
    gcc main.c -o myapp -ldl
    
    • 这里的 -ldl 是针对 动态加载库 libdl.so 本身的,而 不是 你后面要动态加载的那个 .so 文件。
    • 程序运行到 dlopen 那一行时,才去加载你指定路径下的库,并用 dlsym 拿到符号地址,最后调用。

输入输出

冯诺依曼计算机系统 -- PA2 必答题

编译与链接

image

Q1

  • 去掉static

    • 并没有报错
  • 去掉inline

    • 也没有报错
  • 两者都去掉,报错了

    + CC src/engine/interpreter/hostcall.c
    + CC src/isa/riscv32/inst.c
    + LD /home/cilinmengye/ics2023/nemu/build/riscv32-nemu-interpreter
    /usr/bin/ld: /home/cilinmengye/ics2023/nemu/build/obj-riscv32-nemu-interpreter/src/isa/riscv32/inst.o: in function `inst_fetch':
    /home/cilinmengye/ics2023/nemu/include/cpu/ifetch.h:20: multiple definition of `inst_fetch'; /home/cilinmengye/ics2023/nemu/build/obj-riscv32-nemu-interpreter/src/engine/interpreter/hostcall.o:/home/cilinmengye/ics2023/nemu/include/cpu/ifetch.h:20: first defined here
    collect2: error: ld returned 1 exit status
    make: *** [/home/cilinmengye/ics2023/nemu/scripts/build.mk:54: /home/cilinmengye/ics2023/nemu/build/riscv32-nemu-interpreter] Error 1
    

原因如下:

在 C(尤其是 GCC 的 GNU inline)里,staticinline 这两个关键字都会影响函数的“链接属性”(linkage),也就是说它们决定了编译器和链接器如何对待在多个翻译单元(.c/.h 文件)中出现的同名函数定义。


1. static inline 放在头文件:

// cpu/ifetch.h
static inline uint32_t inst_fetch(vaddr_t *pc, int len) {
  // … 
}
  • static:给函数一个内部链接(internal linkage),也就是每个包含这个头文件的 .c 文件都会得到自己独立的一份 inst_fetch,它们的符号不会泄漏到整个可执行文件中。
  • inline:告诉编译器 “尽量把它当作内联函数展开”,并且在 GNU C 下,inline 定义的函数会被当作弱符号(weak symbol)对待;即使真的生成了一个函数体,也会被标记为弱符号,允许多个同名弱符号同时存在,链接器会去重。

所以 static inline 的组合最常用于头文件——你既拿到了内联展开的效率,又不会因为每个翻译单元都有一个同名函数而产生符号冲突。


2. 只去掉 static,保留 inline

// cpu/ifetch.h
inline uint32_t inst_fetch(vaddr_t *pc, int len) { … }
  • static ⇒ 外部链接(external linkage)
  • 保留 inline ⇒ 弱符号

多个翻译单元都包含这个头文件,各自生产一个弱符号版本的 inst_fetch,链接器允许对弱符号“去重”——最终只保留一个,因此不报重复定义错误


3. 只去掉 inline,保留 static

// cpu/ifetch.h
static uint32_t inst_fetch(vaddr_t *pc, int len) { … }
  • static ⇒ 内部链接
  • inline ⇒ 仍旧是每个翻译单元有一份,但都是私有的

内部链接的符号互不冲突,链接时根本不会把它们当成重复定义,所以也不会报错。


4. 同时去掉 staticinline

// cpu/ifetch.h
uint32_t inst_fetch(vaddr_t *pc, int len) { … }
  • static ⇒ 外部链接
  • inline ⇒ 强符号(strong symbol)

这时每个包含了头文件的 .c 文件都会定义一个强符号 inst_fetch,链接器看到多个同名强符号,就会报 “multiple definition of inst_fetch” 的错误。


如何正确放置一个非内联、非静态的函数定义?

如果你确实想要一个全局可见(external linkage)、又不希望把它写成 inlinestatic,就应该把函数 声明 放在头文件,把 定义 放到一个单独的 .c(或者 .cpp)文件里:

// cpu/ifetch.h
#ifndef CPU_IFETCH_H
#define CPU_IFETCH_H
#include <stdint.h>
uint32_t inst_fetch(vaddr_t *pc, int len);   // 只有声明
#endif
// cpu/ifetch.c
#include "cpu/ifetch.h"
uint32_t inst_fetch(vaddr_t *pc, int len) {  // 唯一的一份定义
  // …
}

这样,无论多少翻译单元 #include "cpu/ifetch.h",都只会有一个 inst_fetch 的定义,链接就不会报错了。


总结:

  • static inline(头文件里最常用)→ 内部链接 + 弱符号,不冲突,可内联。
  • inline(无 static)→ 外部弱符号,可多次定义但去重。
  • static(无 inline)→ 内部强符号,各自独立不冲突。
  • static、无 inline → 外部强符号,多个定义冲突。

链接错误的根本原因在于,当你把 staticinline 都去掉以后,inst_fetch 就变成了一个在头文件中被“定义”的、具有外部链接(external linkage)的强符号(strong symbol)。

  • 你在 cpu/ifetch.h 里写了

    uint32_t inst_fetch(vaddr_t *pc, int len) { … }
    
  • 又在两个不同的 .c 文件(一个编译成 inst.o,一个编译成 hostcall.o)里都 #include 了这个头文件。

  • 结果这两个 .o 文件里各自都有一份对外可见的、同名的强符号 inst_fetch

  • 链接器发现同一个可执行或库里出现了两个同名强符号,按照 C/C++ 规则这是不允许的,就报 “multiple definition of inst_fetch” 错误了。


我们可以通过readelf -s查看符号表证明:

你关心的那些“static/inline 导致的符号属性”其实都能在 ELF 可执行文件(或者静态/动态库)里的符号表里看得到,主要体现在符号绑定(binding)字段上。

ELF 符号的 binding(绑定类型)主要有三种,你会看到对应的宏(STB_*)或者在 nm/readelf 中的字母表示:

Binding 类型 ELF 宏 nm/readelf -s 中的字母 含义
本地(Local) STB_LOCAL td(小写) 符号只在本翻译单元可见(static
弱(Weak) STB_WEAK WV(大写) 弱符号,允许多重定义,链接时去重(inline 导致的弱符号)
全局(Global) STB_GLOBAL TD(大写) 强符号,且对所有翻译单元可见(无 static、无 inline
  • static inline 放头文件:如果真的产生了符号,它会是 STB_LOCALstatic)和 STB_WEAKinline 混用时弱化)的混合,但实践中对静态内联函数常常只做内联展开,不留符号。
  • inline:符号变成弱符号 → STB_WEAKW
  • static:符号变成本地强符号 → STB_LOCALt
  • 既无 static 也无 inline:符号是全局强符号 → STB_GLOBALT

在我这里真实的情况为:

  • static inline两者都去掉inst.o符号表显示:
    182: 000000000000140a 31 FUNC GLOBAL DEFAULT 70 inst_fetch

  • 两者都没去掉:
    直接在符号表里没了,可能是因为inline把函数体给去掉了

  • inline去掉
    3: 0000000000000000 27 FUNC LOCAL DEFAULT 70 inst_fetch
    主要Binding字段变成LOCAL了

  • static 去掉
    inst_fetch直接在符号表里没了,果然是因为inline把函数体给去掉了

Q2

  1. readelf -s /home/cilinmengye/ics2023/nemu/build/riscv32-nemu-interpreter | awk 'NR>2 && $NF=="dummy"' | wc -l

    输出34,应该是引入了nemu/include/common.h的.c文件个数,因为voliate可以防止编译器优化掉dummy, static可以让其变成本地强符号

  2. nemu/include/debug.h中添加一行volatile static int dummy; 加上后
    readelf -s /home/cilinmengye/ics2023/nemu/build/riscv32-nemu-interpreter | awk 'NR>2 && $NF=="dummy"' | wc -l
    还是输出34,这是因为在nemu/include/debug.h中其引入了common.h, 然后相当于在同一个.c文件中写了多个volatile static int dummy;
    等同于:

    #include<stdlib.h>
    #include<stdio.h>
    
    volatile static int dummy;
    volatile static int dummy;
    
    int main() 
    {
        	printf("hello");
        	return 0;
    }
    

    诶?为什么不会报错?
    C 语言允许重复声明,只要:

    • 是在相同作用域;
    • 声明的类型完全一致;
    • 只有一次是定义(allocation)。
  3. 按其所说后会报错:

    In file included from 		/home/cilinmengye/ics2023/nemu/include/common.h:49,
                 from src/nemu-main.c:16:
    /home/cilinmengye/ics2023/nemu/include/debug.h:19:21: error: redefinition of ‘dummy’
    19 | volatile static int dummy = 0;
      |                     ^~~~~
    In file included from src/nemu-main.c:16:
    /home/cilinmengye/ics2023/nemu/include/common.h:19:21: note: previous definition of ‘dummy’ with type ‘int’
    19 | volatile static int dummy = 0;
      |                     ^~~~~
    

    这是因为重复定义了

posted @ 2025-07-06 12:16  次林梦叶  阅读(114)  评论(0)    收藏  举报