NEMU PA3 补充内容

image

from pixiv

PA3

穿越时空的旅程

让DiffTest支持异常响应机制

image

在DiffTest中是用来测量硬件是否实现正确的,因为我们在DiffTest中需要比较nemu和spike中CPU_state即寄存器和PC,按照要求在初始化时设置下mstatus

# ics2023/nemu/src/isa/riscv32/init.c
static void restart() {
  /* Set the initial program counter. */
  cpu.pc = RESET_VECTOR;

  /* The zero register is always 0. */
  cpu.gpr[0] = 0;

  // 为了让DiffTest机制正确工作, 针对riscv32, 你需要将mstatus初始化为0x1800.
  IFDEF(CONFIG_DIFFTEST, cpu.csrs.mstatus = 0x1800);
}

~/ics2023/am-kernels/tests/am-tests下运行 make ARCH=$ISA-nemu run mainargs=i开启DiffTest报错了,具体而言是在执行指令8000160c: 342022f3 csrr t0,mcause时:

DiffTest CheckRegs in Regs:  t0 
Ref = 0x0000000b
Local = 0xffffffff

注意查看报错信息时看PC是看src/cpu/cpu-exec.c:142 cpu_exec] nemu: ABORT at pc =这段的pc信息

我来看看我代码是如何实现的:

# ics2023/nemu/src/isa/riscv32/inst.c
# ics2023/nemu/src/isa/riscv32/system/intr.c

s->dnpc = isa_raise_intr(isa_reg_str2val("a7", &success), s->pc);

word_t isa_raise_intr(word_t NO, vaddr_t epc) {
  /* TODO: Trigger an interrupt/exception with ``NO''.
   * Then return the address of the interrupt/exception vector.
   */
  cpu.csrs.mcause = NO;
  cpu.csrs.mepc = epc;
  return cpu.csrs.mtvec;
}

依据手册:

异常分为两类:一类是同步异常,它是指令执行的一种结果,如访问无效的内存地址,

或执行操作码无效的指令;另一类是中断,它是与指令流异步的外部事件,如点击鼠

标。在 M 模式运行期间可能发生的同步异常有五种:其中有环境调用异常 在执行 ecall 指令时发生。

image

所以应该设置mcause为11

在这段 yield() 实现里:

void yield() {
#ifdef __riscv_e
  asm volatile("li a5, -1; ecall");
#else
  asm volatile("li a7, -1; ecall");
#endif
}
  • ecall 指令会触发一个同步异常,硬件自动在 mcause 寄存器中写入 环境调用(environment call)对应的异常号:
    • 从机器模式(M‑mode)来的 ecallmcause 的低位字段被设为 11,最高位(interrupt 标志)为 0。
      — 即不管你把寄存器装成什么值,硬件对 “发生了环境调用” 这件事,只会把 mcause 写成 0b0_01011(在 32 位下是 0xB)。
  • 那为什么要先 li a7, -1
    在 RISC‑V 上,Linux/POSIX‑风格的系统调用约定,或者在很多裸机/RTOS 的 semihosting 约定中,都是把 系统调用号(syscall number)放到寄存器 a7(也即 x17)里,然后执行 ecall
    • a7 设为 -1(也就是 0xFFFFFFFF),通常被约定为 “yield” 或 “调度让出 CPU” 这类的特殊调用号。
    • 操作系统或模拟器在 ecall 异常处理例程中,会检查 a7 的值,见到 -1 就执行相应的 “让出 CPU” 或 “切换线程” 的逻辑。

所以:

  1. 硬件层面ecall 总会把 mcause.interrupt=0mcause.exception_code=11(environment call from M‑mode)写好,和你给 a7 装的值无关。
  2. 软件层面:你给 a7 装的 -1,才是让上层 OS/调度器 知道 “这是一次 yield 请求”,然后去做任务切换。

总结:

  • li a7, -1 → 告诉 软件/OS “我要做 yield 这号调用”。
  • ecall → 告诉 硬件 “发生环境调用” → 硬件写 mcause=0b0_01011 → 跳到 trap 入口。

注意mcause和系统调用号的区别:

系统调用号(syscall number) mcause 寄存器
层面 软件/ABI 约定 硬件/特权架构
存放位置 通常放在 a7(x17)寄存器(也有平台差异) CSR 寄存器 mcause
作用 告诉操作系统/运行时 “我要哪种系统调用”(如读写文件、挂起进程、让出 CPU 等) 告诉硬件陷阱处理逻辑 “触发陷阱的原因是什么”(异常 vs. 中断,以及具体的 code)
取值范围 通常是正整数(或约定的负数/无符号数)映射到某个服务例程 最高位标明是中断(1)还是异常(0),低位是固定的异常/中断号(如 11 表示 environment call from M‑mode)
何时设置 调用 ecall 之前由软件(用户态或内核态)写入 发生陷阱时由硬件自动写入
后续用途 陷阱进入后,操作系统读取它来分发到对应的 syscall 处理函数 入口例程可能会读取它来区分 “是 environment call” 还是其它异常/中断

上下文管理的具体实现也是架构相关的: 例如上文提到, x86/mips32/riscv32中分别通过int/syscall/ecall指令来进行自陷, native中甚至可以通过一些神奇的库函数来模拟相应的功能; 而上下文的具体内容, 在不同的架构上也显然不一样(比如寄存器就已经不一样了). 于是, 我们可以将上下文管理的功能划入到AM的一类新的API中, 名字叫CTE(ConText Extension).

如何将不同架构的上下文管理功能抽象成统一的API呢? 换句话说, 我们需要思考, 操作系统的处理过程其实需要哪些信息?

  • 首先当然是引发这次执行流切换的原因, 是程序除0, 非法指令, 还是触发断点, 又或者是程序自愿陷入操作系统? 根据不同的原因, 操作系统都会进行不同的处理.
  • 然后就是程序的上下文了, 在处理过程中, 操作系统可能会读出上下文中的一些寄存器, 根据它们的信息来进行进一步的处理. 例如操作系统读出PC所指向的非法指令, 看看其是否能被模拟执行. 事实上, 通过这些上下文, 操作系统还能实现一些神奇的功能, 你将会在PA4中了解更详细的信息.

所以, 我们只要把这两点信息抽象成一种统一的表示方式, 就可以定义出CTE的API了. 对于切换原因, 我们只需要定义一种统一的描述方式即可. CTE定义了名为"事件"的如下数据结构(见abstract-machine/am/include/am.h):

typedef struct Event {
  enum { ... } event;
  uintptr_t cause, ref;
  const char *msg;
} Event;

其中event表示事件编号, causeref是一些描述事件的补充信息, msg是事件信息字符串, 我们在PA中只会用到event. 然后, 我们只要定义一些统一的事件编号(上述枚举常量), 让每个架构在实现各自的CTE API时, 都统一通过上述结构体来描述执行流切换的原因, 就可以实现切换原因的抽象了.

我如下代码处应该实现错误了, 在yield()函数的实现中其实通过ecall来实现的,同时其系统调用号为-1

/*irq Interrupt Request 中断请求*/
Context* __am_irq_handle(Context *c) {
  if (user_handler) {
    Event ev = {0};
    // debugContext(c);
    switch (c->mcause) {
      case (uintptr_t)-1: ev.event = EVENT_YIELD;   break;
      case (uintptr_t) 0:
      case (uintptr_t) 1: 
      case (uintptr_t) 2: 
      case (uintptr_t) 3: 
      case (uintptr_t) 4:
      case (uintptr_t) 7:
      case (uintptr_t) 8:
      case (uintptr_t) 9: 
      case (uintptr_t) 13: 
      case (uintptr_t) 19: ev.event = EVENT_SYSCALL; break;
      default: assert(0); ev.event = EVENT_NULL; break;
    }

    c = user_handler(ev, c);
    assert(c != NULL);
  }
  // 对于mips32的syscall和riscv32的ecall, 保存的是自陷指令的PC
  // 因此软件需要在适当的地方对保存的PC加上4, 使得将来返回到自陷指令的下一条指令.
  c->mepc = c->mepc + 4;
  return c;
}

正如讲义中所说的:

**处理器通常只会提供一条自陷指令, 这时EVENT_SYSCALLEVENT_YIELD 都通过相同的自陷指令来实现, 因此CTE需要额外的方式区分它们. **如果自陷指令本身可以携带参数, 就可以用不同的参数指示不同的事件, 例如x86和mips32都可以采用这种方式; 如果自陷指令本身不能携带参数, 那就需要通过其他状态来区分, 一种方式是通过某个寄存器的值来区分, riscv32采用这种方式.

以yield为例,顺序应该为:

在我们的Nanos-lite操作系统中将yield封装成了一个系统调用为SYS_yield(). 我们约定, 这个系统调用直接调用CTE的yield()即可, 然后返回0.

当调用SYS_yield时,首先就触发了ecall指令,系统调用号为1,即a7==1 -->

然后保存上下文,执行am中的__am_irq_handle,依据mcause和a7识别出ev.event==EVENT_SYSCALL -->

然后执行nanos-lite传递的user_handler,即do_event-->

do_event中依据ev.event识别出EVENT_SYSCALL需要执行do_syscall-->

依据a7==1识别出需要执行SYS_yield,因为SYS_yield中调用CTE的yield,所以执行-->

执行yield又调用ecall,但是这个时候的系统调用号为-1,即a7==-1-->

然后保存上下文,执行am中的__am_irq_handle,依据mcause和a7识别出ev.event==EVENT_YIELD-->

然后执行nanos-lite传递的user_handler,即do_event-->

do_event中依据ev.event识别出EVENT_YIELD需要执行Log("Nanos in yield");结束了-->

yield恢复上下文-->

SYS_yield恢复上下文


所以正确的实现应该为:

# ics2023/abstract-machine/am/src/riscv/nemu/cte.c
Context* __am_irq_handle(Context *c) {
  if (user_handler) {
    Event ev = {0};
    // debugContext(c);
    switch (c->mcause) {
      case (uintptr_t)(11): {
        #ifdef __riscv_e
          if (c->gpr[15] == (uintptr_t)(-1)) { // a5
            ev.event = EVENT_YIELD; 
            break;
          }
        #else
          if (c->gpr[17] == (uintptr_t)(-1)) { // a7
            ev.event = EVENT_YIELD;
            break;
          }
        #endif
        ev.event = EVENT_SYSCALL; break;
      }
      default: assert(0); ev.event = EVENT_NULL; break;
    }

    c = user_handler(ev, c);
    assert(c != NULL);
  }
  // 对于mips32的syscall和riscv32的ecall, 保存的是自陷指令的PC
  // 因此软件需要在适当的地方对保存的PC加上4, 使得将来返回到自陷指令的下一条指令.
  c->mepc = c->mepc + 4;
  return c;
}

# ics2023/nemu/src/isa/riscv32/system/intr.c
word_t isa_raise_intr(word_t NO, vaddr_t epc) {
  /* TODO: Trigger an interrupt/exception with ``NO''.
   * Then return the address of the interrupt/exception vector.
   */
  // 为了让DiffTest机制正确工作, 针对riscv32, 你需要将mstatus初始化为0x1800.
  IFDEF(CONFIG_DIFFTEST, cpu.csrs.mstatus = 0x1800);
  cpu.csrs.mcause = NO;
  cpu.csrs.mepc = epc;
  return cpu.csrs.mtvec;
}

# ics2023/nemu/src/isa/riscv32/inst.c
  INSTPAT("0000000 00000 00000 000 00000 11100 11", ecall  , I, 
          bool success = true; 
          s->dnpc = isa_raise_intr(11, s->pc);
          assert(success == true));

异常处理的踪迹 - etrace

  • 在NEMU中实现
    • 识别ecall指令和mret指令(但是目前只能识别出ecall系统调用),mret机器模式异常返回。
      • ecall指令中依据寄存器a7的值可以识别出是何种系统调用
    • 格式如ftrace一样
  • 输出到日志文件中

用户程序和系统调用

Makefile

主要想要搞清楚navy-apps, Nanos-lite和AM的关系是什么,他们之间的层次是什么?

用户程序运行在操作系统之上, 由于运行时环境的差异, 我们不能把编译到AM上的程序放到操作系统上运行. 为此, 我们准备了一个新的子项目Navy-apps, 专门用于编译出操作系统的用户程序.

如上这句话让我很糊涂,我不知道为何不能把编译到AM上的程序放到操作系统上运行?

难道是少了库?可以仔细看看navy-apps/apps或者是navy-apps/test中的用户程序是如何写的


同时我发现在navy-apps/libs/libos/src/crt0/start.S中也有_start()函数,同时在ics2023/abstract-machine/am/src/riscv/nemu/start.S中也有_start()函数

这两个_start函数的关系和区别是什么?

一种想法是navy-apps/libs/libos/src/crt0/start.S_start函数是指定用户程序的main函数

ics2023/abstract-machine/am/src/riscv/nemu/start.S_start函数是指定操作系统的main函数


navy-apps, Nanos-lite中的makefile又是如何运行的?

我先查看如何编译链接一个用户测试程序:ics2023/navy-apps/tests/dummy make ISA=$ISA

# Building dummy-app [riscv32] 即先编译用户程序下的.c文件-->.o文件
# 然后将navy-apps下各个库编译链接成静态库:
for t in compiler-rt libc libndl libos ; do make -s -C /home/cilinmengye/ics2023/navy-apps/libs/$t archive; done

# Building compiler-rt-archive [riscv32]  将compiler-rt下的全部.c文件-->.o文件
# compiler-rt is a replacement library for libgcc, compiler-rt从github中git clone下来然后对其进行编译链接
for t in  ; do make -s -C /home/cilinmengye/ics2023/navy-apps/libs/$t archive; done
+ AR -> build/compiler-rt-riscv32.a
ar
 rcsT
 /home/cilinmengye/ics2023/navy-apps/libs/compiler-rt/build/compiler-rt-riscv32.a
 ...
# ...表示都是compiler-rt编译出来的.o文件

# Building libc-archive [riscv32] 同上操作
for t in  ; do make -s -C /home/cilinmengye/ics2023/navy-apps/libs/$t archive; done
+ AR -> build/libc-riscv32.a
ar rcsT /home/cilinmengye/ics2023/navy-apps/libs/libc/build/libc-riscv32.a ...

# Building libndl-archive [riscv32]
+ AR -> build/libndl-riscv32.a
# Building libos-archive [riscv32]
+ AR -> build/libos-riscv32.a
+ LD -> build/dummy-riscv32

ics2023/navy-apps/tests/dummy下的Makefile主要是引入ics2023/navy-apps/Makefile下的Makefile

ics2023/navy-apps/libs/compiler-rt下的Makefile也主要是引入ics2023/navy-apps/Makefile下的Makefile

ics2023/navy-apps/Makefile下的Makefile默认目的为app, 如果目的为archive则会将目录下的.c文件编译为.o文件然后打包为静态库

注意在ics2023/navy-apps/Makefile中引入了-include $(NAVY_HOME)/scripts/$(ISA).mk, 其中有指定:

# 为了避免和Nanos-lite的内容产生冲突, 我们约定目前用户程序需要被链接到内存位置0x3000000(x86)
# 或0x83000000(mips32或riscv32)附近
LNK_ADDR = $(if $(VME), 0x40000000, 0x83000000)
LDFLAGS += --no-relax -Ttext-segment $(LNK_ADDR)

同时也引入了libc:

### Pull newlib from github if it does not exist
ifeq ($(wildcard $(NAVY_HOME)/libs/libc/Makefile),)
  $(shell cd $(NAVY_HOME)/libs && git clone git@github.com:NJU-ProjectN/newlib-navy.git libc)
endif

libc会使用系统调用实现自身无法完成的功能。系统调用在libos中定义:

_open()
_close()
_read()
_write()
_sbrk()
...

openAPI为例,其在libc库中的src/syscalls/sysopen.c:

int
open (const char *file,
        int flags, ...)
{
  va_list ap;
  int ret;

  va_start (ap, flags);
  ret = _open_r (_REENT, file, flags, va_arg (ap, int));
  va_end (ap);
  return ret;
}

_open_r 最终会调用 _open

应用程序与libc和libos(包含crt0和_start)静态链接后即可执行。

如果$(ISA)!= native, LIBS += libc libos libndl

如果$(ISA) == x86|mips32|riscv32|riscv32e|loongarch32r , LIBS += compiler-rt

如果目标不是app,则直接覆盖LIBS := $(LIB_DEP) ,而$(LIB_DEP)为空

LINKAGE   = $(OBJS) $(foreach l,$(LIBS),$(NAVY_HOME)/libs/$(l)/build/$(l)-$(ISA).a)

libs:
	@for t in $(LIBS); do $(MAKE) -s -C $(NAVY_HOME)/libs/$$t archive; done

### Rule (link): objects (`*.o`) and libraries (`*.a`) -> `$(APP)`, the final ELF binary to be packed into application (ld)
$(APP): $(OBJS) libs
	@echo + LD "->" $(shell realpath $@ --relative-to .)
	@$(LD) $(LDFLAGS) -o $@ $(WL)--start-group $(LINKAGE) $(WL)--end-group

### Rule (archive): objects (`*.o`) -> `ARCHIVE.a` (ar)
$(ARCHIVE): $(OBJS) libs
	@echo + AR "->" $(shell realpath $@ --relative-to .)
	@ar rcsT $@ $(LINKAGE)

最后:

riscv64-linux-gnu-ld --no-relax -Ttext-segment 0x83000000 -melf32lriscv -o /home/cilinmengye/ics2023/navy-apps/tests/dummy/build/dummy-riscv32
 --start-group
 /home/cilinmengye/ics2023/navy-apps/tests/dummy/build/riscv32/dummy.o
 /home/cilinmengye/ics2023/navy-apps/libs/compiler-rt/build/compiler-rt-riscv32.a
 /home/cilinmengye/ics2023/navy-apps/libs/libc/build/libc-riscv32.a
 /home/cilinmengye/ics2023/navy-apps/libs/libndl/build/libndl-riscv32.a
 /home/cilinmengye/ics2023/navy-apps/libs/libos/build/libos-riscv32.a
 --end-group

-Ttext-segment 0x83000000所有的 .text(函数、代码)放到从 0x83000000 开始的地址, 之后的节(.rodata/.data/.bss)会根据默认脚本,紧跟在它后面或者根据它们各自的默认基址排布。

让我们来关注下各个静态库的作用:

  • compiler-rt 即libgcc的替代库,内容主要包括软件算术例程(长整除、取模)、异常展开、栈保护钩子、TLS 辅助、内建函数等
  • libc, 内容包括I/O(printfread)、内存管理(malloc)、字符串处理(strcpy)、线程、网络、时间等
  • libndl 为 SDL库的简化版叫做NDL
  • libso 为系统调用的内容,以及start.S,crt0.c(为用户程序入口内容)

可见用户程序没有用到我们编写的任何API,都是在用上述静态库中提供的API

需要注意的是我们在ics2023/navy-apps/libs/libos/src/crt0/start.S设置了个全局的_start,在我们没有显示给出自定义脚本时,在 GNU ld(以及兼容的链接器)里有内置链接脚本来决定各个输入节(section)如何布局到输出文件里的那段默认脚本,其默认入口点ENTRY(_start)), 即只要存在一个全局的 _start,且你没在链接时另行指定,链接器就会把它作为 ELF 的入口地址,加载器也就从这儿开始执行。

Nanos-lite Makefile

编译成功后把navy-apps/tests/dummy/build/dummy-$ISA手动复制并重命名为nanos-lite/build/ramdisk.img, 然后在nanos-lite/目录下执行

make ARCH=$ISA-nemu

会生成Nanos-lite的可执行文件, 编译期间会把ramdisk镜像文件nanos-lite/build/ramdisk.img 包含进Nanos-lite成为其中的一部分(在nanos-lite/src/resources.S中实现).

需要注意的是Nanos-lite下的Makefile引入了include $(AM_HOME)/Makefile, 而其中的默认的目标为image,即编译链接出image可以运行在NEMU上

# Building nanos-lite-image [riscv32-nemu]
+ CC src/fs.c
+ CC src/proc.c
+ CC src/irq.c
+ AS src/resources.S
+ CC src/mm.c
# Building am-archive [riscv32-nemu]
+ CC src/platform/nemu/trm.c
+ CC src/riscv/nemu/cte.c
+ AR -> build/am-riscv32-nemu.a
# Building klib-archive [riscv32-nemu]
+ LD -> build/nanos-lite-riscv32-nemu.elf
# Creating image [riscv32-nemu]
+ OBJCOPY -> build/nanos-lite-riscv32-nemu.bin

ics2023/nanos-lite/src/resources.S中有如下内容:

.section .data
.global ramdisk_start, ramdisk_end
ramdisk_start:
.incbin "build/ramdisk.img"
ramdisk_end:

.section .rodata
.globl logo
logo:
.incbin "resources/logo.txt"
.byte 0

我们先把resource.S编译成resource.o, 只要把 resource.o 链到最终的可执行文件或固件中,.incbin 插入的原始字节就会变成那个目标文件里对应节(section)的一部分

再从在Nanos-lite下执行make ARCH=$ISA-nemu update看起

# Building nanos-lite-update [riscv32-nemu]
# 首先会调用navy-apps下的Makefile生成ramdisk.image

make -s -C /home/cilinmengye/ics2023/navy-apps ISA=riscv32 ramdisk
# Building -ramdisk [riscv32]

# 所谓的ramdisk.image即使Navy-apps下各个程序ELF文件的集合
for t in apps/nslider apps/menu apps/nterm tests/dummy tests/hello tests/file-test tests/timer-test tests/event-test tests/bmp-test; do make -s -C /home/cilinmengye/ics2023/navy-apps/$t install; done

# 如下为编译链接第一个用户程序nslider,其需要navy-app下各个静态库的支持,所以也需要编译链接各个静态库
# Building nslider-install [riscv32]
# Building compiler-rt-archive [riscv32]
+ AR -> build/compiler-rt-riscv32.a
# Building libbmp-archive [riscv32]
+ AR -> build/libbmp-riscv32.a
# Building libc-archive [riscv32]
+ AR -> build/libc-riscv32.a
# Building libminiSDL-archive [riscv32]
# Building libndl-archive [riscv32]
+ AR -> build/libndl-riscv32.a
+ AR -> build/libminiSDL-riscv32.a
# Building libndl-archive [riscv32]
+ AR -> build/libndl-riscv32.a
# Building libos-archive [riscv32]
+ AR -> build/libos-riscv32.a
+ LD -> build/nslider-riscv32
+ INSTALL -> nslider

...
# ... 表示其余内容都大同小异,都是如上述编译链接用户程序的过程
# LD -> build/nslider-riscv32的过程与上述dummy的过程很像:

riscv64-linux-gnu-ld
 --no-relax
 -Ttext-segment
 0x83000000
 -melf32lriscv
 -o
 /home/cilinmengye/ics2023/navy-apps/apps/nslider/build/nslider-riscv32
 --start-group
 /home/cilinmengye/ics2023/navy-apps/apps/nslider/build/riscv32/src/main.o
 /home/cilinmengye/ics2023/navy-apps/libs/compiler-rt/build/compiler-rt-riscv32.a
 /home/cilinmengye/ics2023/navy-apps/libs/libbmp/build/libbmp-riscv32.a
 /home/cilinmengye/ics2023/navy-apps/libs/libc/build/libc-riscv32.a
 /home/cilinmengye/ics2023/navy-apps/libs/libminiSDL/build/libminiSDL-riscv32.a
 /home/cilinmengye/ics2023/navy-apps/libs/libndl/build/libndl-riscv32.a
 /home/cilinmengye/ics2023/navy-apps/libs/libos/build/libos-riscv32.a
 --end-group

# 最后
mkdir -p /home/cilinmengye/ics2023/navy-apps/fsimg/bin
cp /home/cilinmengye/ics2023/navy-apps/apps/nslider/build/nslider-riscv32 /home/cilinmengye/ics2023/navy-apps/fsimg/bin/nslider

然后将全部编译链接好的用户程序ELF文件打包进ramdisk.image中, 即ramdisk.image是ELF文件和资源的集合:

cat
 ./fsimg/share/music/rhythm/Mi.ogg
 ./fsimg/share/music/rhythm/So.ogg
 ./fsimg/share/music/rhythm/empty.ogg
 ./fsimg/share/music/rhythm/La.ogg
 ./fsimg/share/music/rhythm/Re.ogg
 ./fsimg/share/music/rhythm/Si.ogg
 ./fsimg/share/music/rhythm/Do.ogg
 ./fsimg/share/music/rhythm/Fa.ogg
 ./fsimg/share/music/little-star.ogg
 ./fsimg/share/fonts/Courier-11.bdf
 ./fsimg/share/fonts/Courier-13.bdf
 ./fsimg/share/fonts/Courier-7.bdf
 ./fsimg/share/fonts/Courier-10.bdf
 ./fsimg/share/fonts/Courier-12.bdf
 ./fsimg/share/fonts/Courier-8.bdf
 ./fsimg/share/fonts/Courier-9.bdf
 ./fsimg/share/files/num
 ./fsimg/share/pictures/projectn.bmp
 ./fsimg/bin/nterm
 ./fsimg/bin/bmp-test
 ./fsimg/bin/hello
 ./fsimg/bin/timer-test
 ./fsimg/bin/nslider
 ./fsimg/bin/file-test
 ./fsimg/bin/event-test
 ./fsimg/bin/dummy
 ./fsimg/bin/menu
 >
 build/ramdisk.img

然后使用命令统计了各个ELF文件和资源在ramdisk.image中的偏移量,大小和名称:

wc -c ./fsimg/share/music/rhythm/Mi.ogg ./fsimg/share/music/rhythm/So.ogg ./fsimg/share/music/rhythm/empty.ogg ./fsimg/share/music/rhythm/La.ogg ./fsimg/share/music/rhythm/Re.ogg ./fsimg/share/music/rhythm/Si.ogg ./fsimg/share/music/rhythm/Do.ogg ./fsimg/share/music/rhythm/Fa.ogg ./fsimg/share/music/little-star.ogg ./fsimg/share/fonts/Courier-11.bdf ./fsimg/share/fonts/Courier-13.bdf ./fsimg/share/fonts/Courier-7.bdf ./fsimg/share/fonts/Courier-10.bdf ./fsimg/share/fonts/Courier-12.bdf ./fsimg/share/fonts/Courier-8.bdf ./fsimg/share/fonts/Courier-9.bdf ./fsimg/share/files/num ./fsimg/share/pictures/projectn.bmp ./fsimg/bin/nterm ./fsimg/bin/bmp-test ./fsimg/bin/hello ./fsimg/bin/timer-test ./fsimg/bin/nslider ./fsimg/bin/file-test ./fsimg/bin/event-test ./fsimg/bin/dummy ./fsimg/bin/menu | grep -v 'total$' | sed -e 's+ ./fsimg+ +' | awk -v sum=0 '{print "\x7b\x22" $2 "\x22\x2c " $1 "\x2c " sum "\x7d\x2c";sum += $1}' >> build/ramdisk.h

通过上述操作,生成的文件通过软链接在nanos-lite中得以使用:

ln -sf /home/cilinmengye/ics2023/navy-apps/build/ramdisk.img build/ramdisk.img
ln -sf /home/cilinmengye/ics2023/navy-apps/build/ramdisk.h src/files.h
ln -sf /home/cilinmengye/ics2023/navy-apps/libs/libos/src/syscall.h src/syscall.h

系统调用的踪迹 - strace

在Nanos-lite中实现一个简单的strace: Nanos-lite可以得到系统调用的所有信息, 包括名字, 参数和返回值. 这也是为什么我们选择在Nanos-lite中实现strace: 系统调用是携带高层的程序语义的, 但NEMU中只能看到底层的状态机.

很简单,只要在ics2023/nanos-lite/src/syscall.cdo_syscall函数中识别所有分发过来的系统调用即可,因为是在Nanos-lite层面的strace,可能不能在NEMU的make menuconfig中进行设置了

支持sfs的strace

由于sfs的特性, 打开同一个文件总是会返回相同的文件描述符. 这意味着, 我们可以把strace中的文件描述符直接翻译成文件名, 得到可读性更好的trace信息. 尝试实现这一功能, 它可以为你将来使用strace提供一些便利.

以文件系统为例 API 梳理

我发现一个现象:

在navy-apps中libc实现了read, write,open的API

然后再navy-apps中的用户程序或者其他库在读写文件时就用read, write,open这些API

当调用上述API时,实际上会触发系统调用syscall

触发syscall最终会执行我在Nanos-lite中编写的如sys_open,其中调用了虚拟文件系统的API fs_open

而在编写Nanos-lite时,其中的代码都是直接使用例如fs_open之类的API,不走系统调用,这就是内核实现吗...

支持多个ELF的ftrace

image

这里其实有个很严重的问题是我并不知道其他ELF文件的路径地址,即我要如何在NEMU中打开用户程序的ELF文件呢?

目前我想到可行的方案只有硬编码,即我知道各个用户程序在编译链接后生成的ELF文件会在ics2023/navy-apps/apps/xxx/build/xxx-riscv32(其中xxx表示用户程序名称如nslider),我可以通过libelf.h来直接解析ELF文件,即直接通过open打开ELF文件,调用libelf.h中的API

或者我知道ramdisk.image的位置ics2023/nanos-lite/build/ramdisk.img, 同时我知道ramdisk.image上各个文件内容的偏移分布ics2023/nanos-lite/src/files.h, 这样我可以通过elf.h来解析ELF文件

对ELF文件进行分析其实包括两个主要的库:

elf.h libelf.h
来自 libc-dev / kernel-headers elfutils (libelf-dev) 或 elftoolchain 库
定位 数据定义:ELF 文件格式的 C 结构体和常量 功能接口:读写、遍历、修改 ELF 文件的函数 API
依赖 头文件即可,无需额外库 编译时 -lelf,运行时要有 libelf.so
典型用途 手动 read(fd, &ehdr, sizeof(ehdr)) 然后按结构体解析 elf_beginelf_nextscngelf_getshdr

elf.h可以进行更加底层的操作,比如若知道ELF文件的地址,可以直接以字节粒度读取ELF文件,如:

#include <elf.h>
…
Elf64_Ehdr ehdr;
pread(fd, &ehdr, sizeof(ehdr), 0);
if (ehdr.e_type == ET_EXEC) …

libelf.h一般只能以文件粒度读取ELF文件

将Nanos-lite编译到native

image

精彩纷呈的应用程序

实现更多的fixedptc API

fixedpt的类型, 用于表示定点数, 可以看到它的本质是int32_t类型.

31  30                           8          0
+----+---------------------------+----------+
|sign|          integer          | fraction |
+----+---------------------------+----------+

这样, 对于一个实数a, 它的fixedpt类型表示A = a * 2^8(截断结果的小数部分). 例如实数1.25.6FLOAT类型来近似表示, 就是

1.2 * 2^8 = 307 = 0x133
+----+---------------------------+----------+
| 0  |             1             |    33    |
+----+---------------------------+----------+


5.6 * 2^8 = 1433 = 0x599
+----+---------------------------+----------+
| 0  |             5             |    99    |
+----+---------------------------+----------+

image

fixedpt能够表示的最大正数和最小负数为[-0x7fffffff, +0x7fffffff],转变成10进制数为[-8388607.996, +8388607.996]

用fixedpt模拟表示float当然是因为我们不希望有浮点指令的出现,因为我们NEMU不支持浮点指令

image

typedef int32_t fixedpt;
#define FIXEDPT_BITS	32
#define FIXEDPT_ONE	((fixedpt)((fixedpt)1 << FIXEDPT_FBITS))
#define fixedpt_rconst(R) ((fixedpt)((R) * FIXEDPT_ONE + ((R) >= 0 ? 0.5 : -0.5)))

在编译器的世界里,“编译时常量折叠”(constant folding)指的就是:把程序中完全由常量构成的表达式,在编译阶段就算出它的最终结果,而不是等到运行时再去做运算。这样做能减少生成的机器指令,提高运行时性能。

如果把 fixedpt_rconst(2.75) 这样的 字面量(literal)常量作为宏参数传进去,编译器在编译期就能算出 (2.75 * FIXEDPT_ONE + 0.5) 的结果(一个整数常量),再转换成 int32_t

即编译器会在优化阶段自动识别“全是常量的运算”并替换成单个常量值。这里我们的常量值为一个整数,自然没有浮点指令

image

man floor

RETURN VALUE
These functions return the floor of x.

If x is integral, +0, -0, NaN, or an infinity, x itself is returned.

DESCRIPTION
These functions return the largest integral value that is not greater than x.

For example, floor(0.5) is 0.0, and floor(-0.5) is -1.0.

man ceil

RETURN VALUE
These functions return the ceiling of x.

If x is integral, +0, -0, NaN, or infinite, x itself is returned.

DESCRIPTION
These functions return the smallest integral value that is not less than x.

For example, ceil(0.5) is 1.0, and ceil(-0.5) is 0.0.

单元测试模仿navy-apps/tests中的写法:

  • 创建ics2023/navy-apps/tests/libfixedptc-test

  • 定义Makefile, 因为我们是测试软件,所以要确保硬件正确,即make ISA=$ISA run 生成出ELF文件,可直接在本地机器上运行:

NAME = libfixedptc-test
SRCS = main.c
LIBS = libfixedptc
include $(NAVY_HOME)/Makefile
  • 定义main.c

native

我们在Navy中提供了一个特殊的ISA叫native来实现上述的解耦, 它和其它ISA的不同之处在于:

  • 链接时绕开libos和Newlib, 让应用程序直接链接Linux的glibc
  • 通过一些Linux native的机制实现/dev/events, /dev/fb等特殊文件的功能 (见navy-apps/libs/libos/src/native.cpp)
  • 编译到Navy native的应用程序可以直接运行, 也可以用gdb来调试(见navy-apps/scripts/native.mk), 而编译到其它ISA的应用程序只能在Nanos-lite的支撑下运行

虽然Navy的native和AM中的native同名, 但它们的机制是不同的: 在AM native上运行的系统, 需要AM, Nanos-lite, libos, libc这些抽象层来支撑上述的运行时环境, 在AM中的ARCH=native, 在Navy中对应的是ISA=am_native; 而在Navy native中, 上述运行时环境是直接由Linux native实现的.

你可以在bmp-test所在的目录下运行make ISA=native run, 来把bmp-test编译到Navy native上并直接运行, 还可以通过make ISA=native gdb对它进行调试. 这样你就可以在Linux native的环境下单独测试Navy中除了libos和Newlib之外的所有代码了(例如NDL和miniSDL). 一个例外是Navy中的dummy, 由于它通过_syscall_()直接触发系统调用, 这样的代码并不能直接在Linux native上直接运行, 因为Linux不存在这个系统调用(或者编号不同).

我需要解释下:

  • 纯粹的Linux native: 和Project-N的组件没有任何关系, 用于保证游戏本身确实可以正确运行. 在更换库的版本或者修改游戏代码之后, 都会先在Linux native上进行测试.
  • Navy中的native: 用Navy中的库替代Linux native的库, 测试游戏是否能在Navy库的支撑下正确运行.
  • AM中的native: 用Nanos-lite, libos和Newlib(libc)替代Linux的系统调用和glibc, 测试游戏是否能在Nanos-lite及其运行时环境的支撑下正确运行.
  • NEMU: 用NEMU替代真机硬件, 测试游戏是否能在NEMU的支撑下正确运行.

LD_PRELOAD

image

LD_PRELOAD 是 Linux 下动态链接器(ld.sold-linux.so)提供的一个环境变量,用来 在程序启动时强制让指定的共享库.so 文件)最先被加载,从而可以实现对原有库函数的“拦截”或“替换”。具体作用和使用场景包括:


  1. 库函数拦截(Function Interposition)
  • 原理:动态链接时,链接器会按加载顺序搜索符号(函数名、全局变量名等)。LD_PRELOAD 指定的库会被优先搜索到,所以其中定义的同名函数会覆盖系统默认库(如 glibc)里的实现。

  • 示例:假设你写了一个 mymalloc.so,在里面实现了你自己的 mallocfree

    // mymalloc.c
    #define _GNU_SOURCE
    #include <stdio.h>
    #include <dlfcn.h>
    #include <stdlib.h>
    
    static void* (*real_malloc)(size_t) = NULL;
    void* malloc(size_t size) {
        if (!real_malloc) {
            real_malloc = dlsym(RTLD_NEXT, "malloc");
        }
        void *p = real_malloc(size);
        printf("[my malloc] %zu bytes -> %p\n", size, p);
        return p;
    }
    

    编译成共享库:

    gcc -fPIC -shared mymalloc.c -o libmymalloc.so -ldl
    

    然后运行任何程序时加:

    LD_PRELOAD=./libmymalloc.so ./your_program
    

    这样你就能在不修改 your_program 源码的情况下,拦截所有调用 malloc 的地方,打印日志、统计内存使用,甚至改变分配行为。


  1. 调试与性能分析
  • 故障注入:可以在关键函数里故意返回错误,模拟系统调用失败,测试程序的鲁棒性。
  • 性能统计:拦截 read/write/open 等 I/O 函数,累计调用次数和耗时,用于简单的性能剖析。

  1. 兼容性补丁或功能扩展
  • 当某些旧程序依赖的库里有 bug,生产环境又不方便升级,你可以写一个小的 shim 库,只修补或重写有问题的函数,然后通过 LD_PRELOAD 加载进去,达到修补的效果。

  1. 注意事项

  2. 安全风险

    • 如果你在 root 或有高权限的场景下误用 LD_PRELOAD,可能被恶意库劫持,从而遭受代码注入攻击。
    • 出于安全考虑,setuid / setgid 程序(例如 passwdsudo)默认会忽略 LD_PRELOAD
  3. 路径搜索

    • LD_PRELOAD 中指定的库需要能被动态链接器找到。通常写绝对路径更安全:

      LD_PRELOAD=/home/user/libfix.so ./app
      
  4. 与其他环境变量配合

    • LD_LIBRARY_PATH:控制搜索动态库的目录;与 LD_PRELOAD 共同使用时,预加载库也必须放在 LD_LIBRARY_PATH 能访问的位置,或者直接给出完整路径。
    • LD_DEBUG=all:可以配合打开调试日志,看预加载库是否真的被加载。

具体在我们项目这里,是因为在编译链接时设定了:LD_PRELOAD=/home/cilinmengye/ics2023/navy-apps/libs/libos/build/native.so

native.so是通过native.cpp编译链接过来的:g++ -std=c++11 -O1 -fPIC -shared -o build/native.so src/native.cpp -ldl -lSDL2

ics2023/navy-apps/tests/bmp-test/main.c调用了:void *bmp = BMP_Load("/share/pictures/projectn.bmp", &w, &h);

BMP_Load的实现在ics2023/navy-apps/libs/libbmp/src/BMP.c, 其中调用了FILE *fp = fopen(filename, "r");

然后因为我们加载的是native.so其中有 fopen的实现,处理了在Linux native未存在的文件

# 具体而言是在native.c的Init中有如下代码:
    char *navyhome = getenv("NAVY_HOME");
    assert(navyhome);
    sprintf(fsimg_path, "%s/fsimg", navyhome);

    char newpath[512];
    get_fsimg_path(newpath, "/bin");
    setenv("PATH", newpath, 1); // overwrite the current PATH

FILE *fopen(const char *path, const char *mode) {
  char newpath[512];
  if (glibc_fopen == NULL) {
    glibc_fopen = (FILE*(*)(const char*, const char*))dlsym(RTLD_NEXT, "fopen");
    assert(glibc_fopen != NULL);
  }
  return glibc_fopen(redirect_path(newpath, path), mode);
}

static inline void get_fsimg_path(char *newpath, const char *path) {
  sprintf(newpath, "%s%s", fsimg_path, path);
}

static const char* redirect_path(char *newpath, const char *path) {
  get_fsimg_path(newpath, path);
  if (0 == access(newpath, 0)) {
    fprintf(stderr, "Redirecting file open: %s -> %s\n", path, newpath);
    return newpath;
  }
  return path;
}

效果就是Redirecting file open: /share/pictures/projectn.bmp->/home/cilinmengye/ics2023/navy-apps/fsimg/share/pictures/projectn.bmp

但是在~/ics2023/navy-apps/tests/bmp-test 下执行make ISA=native run,本来应该是要有画面的,但是确没有画面,一片黑;但是我将bmp-test放在nanos-lite中以make ARCH=$ISA-nemu run运行是有画面的,很奇怪,难道是官方提供的ics2023/navy-apps/libs/libos/src/native.cpp中实现出错了?

运行时环境兼容

image

以WSL为案例,所谓通过Windows的运行时环境来实现Linux的API, 从而支撑Linux程序在Windows上的运行,即可以理解Linux程序(应用程序)在实现时调用的都是API,其不关心API的具体实现,即其并不关心API的实现架构或者OS是谁,只有你提供了API给我,我调用起来没有报错就行。

我们通过在Windows中依赖Windows的底层机制来实现了API,从而给Linux程序(应用程序)用

注意ramdisk镜像的大小

image

如何理解?

首先我们要知道Nanos-lite,AM这些都是内核,依据ics2023/abstract-machine/scripts/linker.ld中的内容,我们可以得到内核的虚拟内存映射,实际上在现阶段我们的虚拟内存就是物理内存(我们还没有引入虚拟内存相关功能)


nemu中关于模拟内存的设置:

#define PMEM_LEFT  ((paddr_t)CONFIG_MBASE)
#define PMEM_RIGHT ((paddr_t)CONFIG_MBASE + CONFIG_MSIZE - 1)
#define RESET_VECTOR (PMEM_LEFT + CONFIG_PC_RESET_OFFSET)
CONFIG_MSIZE=0x8000000
CONFIG_PC_RESET_OFFSET=0
CONFIG_MBASE=0x80000000

x86的物理内存是从0开始编址的, 但对于一些ISA来说却不是这样, 例如mips32和riscv32的物理地址均从0x80000000开始. 因此对于mips32和riscv32, 其CONFIG_MBASE将会被定义成0x80000000

Monitor读入客户程序并对寄存器进行初始化后, 这时内存的布局如下:

pmem:

CONFIG_MBASE      RESET_VECTOR
      |                 |
      v                 v
      -----------------------------------------------
      |                 |                  |
      |                 |    guest prog    |
      |                 |                  |
      -----------------------------------------------
                        ^
                        |
                       pc

因为CONFIG_PC_RESET_OFFSET=0,所以 RESET_VECTOR与CONFIG_MSIZE是相等的

可知我们能够把NEMU的内存当作[0x80000000, 0x88000000]的一块区域


我们的内核image文件从开始加载进内存,其中包括内核ELF文件的.text.rodata.data、ramdisk、.bss、堆栈、堆空间各个内容。

然后用户程序的ELF加载被安排在0x83000000, 这理应是举例内核内容相离较远的空间地址,可以按照如下图示理解NEMU内存中的分布:

image

PAL

游戏数据:

assertion "0" failed: file "/home/cilinmengye/ics2023/navy-apps/libs/libminiSDL/src/general.c", line 27, function: SDL_WM_SetCaption

assertion "0" failed: file "/home/cilinmengye/ics2023/navy-apps/libs/libminiSDL/src/event.c", line 79, function: SDL_GetKeyState

还需要实现SDL_WM_SetCaptionSDL_GetKeyState, 其中SDL_WM_SetCaption无关紧要,不实现都可以运行

为了在Navy中运行仙剑奇侠传, 你还需要对miniSDL中绘图相关的API进行功能的增强. 具体地, 作为一款上世纪90年代的游戏, 绘图的时候每个像素都是用8位来表示, 而不是目前普遍使用的32位00RRGGBB. 而这8位也并不是真正的颜色, 而是一个叫"调色板"(palette)的数组的下标索引, 调色板中存放的才是32位的颜色. 用代码的方式来表达, 就是:

// 现在像素阵列中直接存放32位的颜色信息
uint32_t color_xy = pixels[x][y];

// 仙剑奇侠传中的像素阵列存放的是8位的调色板下标,
// 用这个下标在调色板中进行索引, 得到的才是32位的颜色信息
uint32_t pal_color_xy = palette[pixels[x][y]];

仙剑奇侠传中的代码会创建一些8位像素格式的Surface结构, 并通过相应的API来对这些结构进行处理. 因此, 你也需要在miniSDL的相应API中添加对这些8位像素格式的Surface的支持.

实现:

  • 使用API的案例?

我想要通过在Navy native上运行PAL,因为调色板还没有实现,所以应该是无法运行成功的,然后我通过gdb工具调试到调用调试板的API

我本来是这么想的,但是实际运行后却出现超乎我想象的错误:

(base) cilinmengye@cilinmengye:~/ics2023/navy-apps/apps/pal$ make ISA=native run
# Building pal-run [native]
# Building libfixedptc-archive [native]
+ AR -> build/libfixedptc-native.a
# Building libminiSDL-archive [native]
# Building libndl-archive [native]
+ AR -> build/libndl-native.a
+ AR -> build/libminiSDL-native.a
+ LD -> build/pal-native
make -C /home/cilinmengye/ics2023/navy-apps/libs/libos ISA=native
make[1]: Entering directory '/home/cilinmengye/ics2023/navy-apps/libs/libos'
make[1]: 'build/native.so' is up to date.
make[1]: Leaving directory '/home/cilinmengye/ics2023/navy-apps/libs/libos'
Segmentation fault (core dumped)
make: *** [/home/cilinmengye/ics2023/navy-apps/scripts/native.mk:9: run] Error 139

如何调试Segmentation fault错误?

AddressSanitizer (ASan)

使用方法

// 文件名:addressSanitizer2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    // 分配 10 字节,但下面我们要拷贝一个更长的字符串,必然越界
    char *buf = malloc(10);
    if (!buf) {
        perror("malloc");
        return 1;
    }

    // 故意拷贝超过分配大小的数据
    strcpy(buf, "This string is definitely longer than ten bytes!");
    printf("Buffer contents: %s\n", buf);

    free(buf);
    return 0;
}
# 不知为何编译链接选项加上-O0,-O1之类后 AddressSanitizer (ASan)就不起作用了
gcc -g \
    -fsanitize=address \
    -fno-omit-frame-pointer \
    -o addressSanitizer2 addressSanitizer2.c

然后运行有如下错误提示:

==30287==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000001a at pc 0x7ef8f903a2c3 bp 0x7fffb6d171b0 sp 0x7fffb6d16958
WRITE of size 49 at 0x60200000001a thread T0
    #0 0x7ef8f903a2c2 in __interceptor_memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827
    #1 0x5c513866d29a in main /home/cilinmengye/tmp/addressSanitizer_test/addressSanitizer2.c:12
    #2 0x7ef8f8c29d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #3 0x7ef8f8c29e3f in __libc_start_main_impl ../csu/libc-start.c:392
    #4 0x5c513866d184 in _start (/home/cilinmengye/tmp/addressSanitizer_test/addressSanitizer2+0x1184)

0x60200000001a is located 0 bytes to the right of 10-byte region [0x602000000010,0x60200000001a)
allocated by thread T0 here:
    #0 0x7ef8f90b4887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x5c513866d25e in main /home/cilinmengye/tmp/addressSanitizer_test/addressSanitizer2.c:6
    #2 0x7ef8f8c29d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827 in __interceptor_memcpy
Shadow bytes around the buggy address:
  0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa 00[02]fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==30287==ABORTING

具体到实际问题中,首先需要更改编译链接选项,即Makefile文件:

  • ics2023/navy-apps/Makefile
  • ics2023/navy-apps/libs/libos/Makefile
  • ics2023/navy-apps/scripts/native.mk

但是试过后AddressSanitizer (ASan)没有报出任何提示,尝试下一个方法

(base) cilinmengye@cilinmengye:~/ics2023/navy-apps/apps/pal$ make ISA=native run
# Building pal-run [native]
# Building libfixedptc-archive [native]
+ AR -> build/libfixedptc-native.a
# Building libminiSDL-archive [native]
# Building libndl-archive [native]
+ AR -> build/libndl-native.a
+ AR -> build/libminiSDL-native.a
+ LD -> build/pal-native
make -C /home/cilinmengye/ics2023/navy-apps/libs/libos ISA=native
make[1]: Entering directory '/home/cilinmengye/ics2023/navy-apps/libs/libos'
make[1]: 'build/native.so' is up to date.
make[1]: Leaving directory '/home/cilinmengye/ics2023/navy-apps/libs/libos'
Segmentation fault (core dumped)
make: *** [/home/cilinmengye/ics2023/navy-apps/scripts/native.mk:9: run] Error 139

等下我发现我在项目中make的不对,make clean只能删除掉当前Makefile下的build中的内容,再make ISA=native run可能有些文件没有更新,需要用make -B ISA=native run, 明天试一下,再次加上如下选项运行下程序:

	-g \
    -fsanitize=address \
    -fno-omit-frame-pointer \

好吧还是什么都没有报错,要换其他方法检测了

Gdb and Core

在命令

gdb ./myprog core

中,core 就是一个“核心转储”(core dump)文件。


什么是 core 文件?

  1. 进程崩溃时的快照
    当一个程序发生严重错误(如非法内存访问 segmentation fault、总线错误等)而被操作系统强制终止时,内核会把该进程当时的内存状态、寄存器值、调用栈等信息,以二进制形式写入到一个文件里,这个文件就是 core dump,通常默认命名为 corecore.<pid>
  2. 配置与产生
    • 通过 ulimit -c 可以查看或设置最大 core 大小(ulimit -c unlimited 允许生成任意大小的 core 文件)。
    • 生成位置和命名也可通过 /proc/sys/kernel/core_pattern 进行定制。

core 文件的作用

  • 事后调试:你可以用 GDB 或其它调试器加载可执行程序和对应的 core 文件,完整还原出当时崩溃现场的调用栈、变量值、内存内容等,从而分析出错误原因:

    # 允许生成任意大小的 core 文件
    ulimit -c unlimited
    
    gdb ./myprog core
    (gdb) bt           # 查看崩溃时的调用栈
    (gdb) info locals  # 查看当前帧的局部变量
    (gdb) x/32x $sp    # 查看栈内存
    
  • 脱离现场:不需要在线、实时复现崩溃场景,开发者只要拿到 core 文件就能重现当时的状态。

  • 主要编译链接文件时需要加上-g才能在gdb中看到完整的堆栈信息

一般而言,只要我们在当前目录下执行程序,程序崩溃后就会在当前目录下生成core/core.pid文件,如果没有看到,有可能系统把它重定向到其他地方了。执行:

cat /proc/sys/kernel/core_pattern

你会看到一个模板,比如 /tmp/core.%e.%p,或者是 |/usr/lib/systemd/systemd-coredump %p %u %g %s %t %e

  • 如果是 /tmp/...,就去 /tmp 下找对应的文件名。

  • 如果是|/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -F%F -- %E, 说明内核把所有崩溃生成的 core dump 都「管道」交给了 Ubuntu 的 Apport(崩溃上报工具),而并不会在当前目录写一个 core 文件。因此 GDB 自然也就找不到,也就没有任何堆栈信息。

    • Apport 是 Ubuntu(及基于 Debian 的发行版)自带的崩溃报告框架, 自动捕获程序崩溃, 收集并打包崩溃信息
    • 如果你在桌面环境下,在程序崩溃后会弹出一个对话框,询问是否要将崩溃报告发送给 Ubuntu 社区。
  • 关掉 Apport,让内核直接写 core 文件:

    • 临时让系统写本地 core

      sudo sysctl -w kernel.core_pattern=core
      ulimit -c unlimited
      
    • 重新跑你的程序,让它崩溃,这时在当前工作目录就会出现一个 core(或 core.<pid>)的文件。

    • 再用 GDB:

      gdb ./yourprogram ./core
      (gdb) bt
      

进入 gdb 后,最常用的命令是:

  • bt(backtrace):打印崩溃时的调用栈
  • frame <n>:切换到调用栈第 n 层,查看局部变量
  • print <var>:打印变量的值
  • list:显示当前源码位置

一个示例交互:

csharp复制编辑(gdb) bt
#0  0x0040056f in foo (p=0x0) at myprog.c:10
#1  0x004005a3 in main () at myprog.c:20
(gdb) frame 0
(gdb) list
5	    void foo(int *p) {
6	        *p = 42;    // ← 这里 p 是 NULL,就会段错误
7	    }
(gdb) print p
$1 = (int *) 0x0

这样就能精准定位是哪一行、哪个指针或数组越界访问导致了访问违例。


观察到的错误如下:

malloc(): invalid size (unsorted)
Aborted (core dumped)
(gdb) bt
#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=137855892154304)
    at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (signo=6, threadid=137855892154304) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (threadid=137855892154304, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3  0x00007d6114242476 in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007d61142287f3 in __GI_abort () at ./stdlib/abort.c:79
#5  0x00007d6114289677 in __libc_message (action=action@entry=do_abort, 
    fmt=fmt@entry=0x7d61143dbb77 "%s\n") at ../sysdeps/posix/libc_fatal.c:156
#6  0x00007d61142a0cfc in malloc_printerr (
    str=str@entry=0x7d61143debc0 "malloc(): invalid size (unsorted)") at ./malloc/malloc.c:5664
#7  0x00007d61142a40dc in _int_malloc (av=av@entry=0x7d611441ac80 <main_arena>, 
    bytes=bytes@entry=82) at ./malloc/malloc.c:4002
#8  0x00007d61142a5139 in __GI___libc_malloc (bytes=82) at ./malloc/malloc.c:3329
#9  0x00005675c572d7f8 in SDL_GetKeyState (numkeys=0x0)
    at /home/cilinmengye/ics2023/navy-apps/libs/libminiSDL/src/event.c:99
#10 0x00005675c572a2de in PAL_UpdateKeyboardState ()
    at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/device/input.c:215
#11 0x00005675c572a558 in PAL_ProcessEvent ()
    at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/device/input.c:415
#12 0x00005675c57129b8 in PAL_RNGPlay (iNumRNG=6, iStartFrame=0, iEndFrame=-1, iSpeed=25)
    at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/misc/rngplay.c:437
--Type <RET> for more, q to quit, c to continue without paging--
#13 0x00005675c56ed6dc in PAL_TrademarkScreen ()
    at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/main.c:167
#14 0x00005675c56ee0cc in main (argc=1, argv=0x7ffff5a86f68)
    at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/main.c:471


malloc(): unaligned tcache chunk detected
Aborted (core dumped)
(gdb) bt
#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=127580169885632) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (signo=6, threadid=127580169885632) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (threadid=127580169885632, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3  0x0000740893642476 in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007408936287f3 in __GI_abort () at ./stdlib/abort.c:79
#5  0x0000740893689677 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7408937dbb77 "%s\n")
    at ../sysdeps/posix/libc_fatal.c:156
#6  0x00007408936a0cfc in malloc_printerr (str=str@entry=0x7408937ded20 "malloc(): unaligned tcache chunk detected")
    at ./malloc/malloc.c:5664
#7  0x00007408936a53dc in tcache_get (tc_idx=<optimized out>) at ./malloc/malloc.c:3195
#8  __GI___libc_malloc (bytes=bytes@entry=8) at ./malloc/malloc.c:3313
#9  0x00007408936a858f in __GI___strdup (s=0x5e37fbe24f98 "pat.mkf") at ./string/strdup.c:42
#10 0x00005e37fbdf2203 in UTIL_GetFullPathName (
    buffer=0x5e37fbe309a0 <internal_buffer+4096> "/share/games/pal/rng.mkf", buflen=1024, 
    basepath=0x5e38084bfaa0 "/share/games/pal/", subpath=0x5e37fbe24f98 "pat.mkf")
    at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/misc/util.c:513
#11 0x00005e37fbdf2069 in UTIL_OpenFileAtPathForMode (lpszPath=0x5e38084bfaa0 "/share/games/pal/", 
    lpszFileName=0x5e37fbe24f98 "pat.mkf", szMode=0x5e37fbe249f5 "rb")
    at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/misc/util.c:448
#12 0x00005e37fbdf1fde in UTIL_OpenFileForMode (lpszFileName=0x5e37fbe24f98 "pat.mkf", szMode=0x5e37fbe249f5 "rb")
    at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/misc/util.c:424
--Type <RET> for more, q to quit, c to continue without paging--
#13 0x00005e37fbdf1e64 in UTIL_OpenRequiredFileForMode (lpszFileName=0x5e37fbe24f98 "pat.mkf", 
    szMode=0x5e37fbe249f5 "rb") at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/misc/util.c:349
#14 0x00005e37fbdf1e3b in UTIL_OpenRequiredFile (lpszFileName=0x5e37fbe24f98 "pat.mkf")
    at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/misc/util.c:325
#15 0x00005e37fbe0958a in PAL_GetPalette (iPaletteNum=1, fNight=0)
    at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/device/palette.c:52
#16 0x00005e37fbdcc713 in PAL_SplashScreen () at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/main.c:191
#17 0x00005e37fbdcd0d1 in main (argc=1, argv=0x7ffee7863118)
    at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/main.c:472


Program terminated with signal SIGSEGV, Segmentation fault.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000596840f5fcec in SDL_BlitSurface (src=0x596847596e10, srcrect=0x7ffd4de8ffc8, dst=0x59684759d2c0, 
    dstrect=0x7ffd4de8ffc0) at /home/cilinmengye/ics2023/navy-apps/libs/libminiSDL/src/video.c:73
73	      dp[(dy + i) * dst->w + dx + j] = sp[(sy + i) * src->w + sx + j];
[Current thread is 1 (Thread 0x7c66222f77c0 (LWP 13980))]
(gdb) bt
#0  0x0000596840f5fcec in SDL_BlitSurface (src=0x596847596e10, srcrect=0x7ffd4de8ffc8, dst=0x59684759d2c0, 
    dstrect=0x7ffd4de8ffc0) at /home/cilinmengye/ics2023/navy-apps/libs/libminiSDL/src/video.c:73
#1  0x0000596840f1fbd0 in PAL_SplashScreen () at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/main.c:313
#2  0x0000596840f200d1 in main (argc=1, argv=0x7ffd4de90578)
    at /home/cilinmengye/ics2023/navy-apps/apps/pal/repo/src/main.c:472

需要向可执行文件传入参数的场景:

你可以把 core 文件和可执行程序一起交给 gdb,这样就能在崩溃点拿到完整的调用栈,同时还保留了启动时的参数。常见的做法有两种:


方法一:一次性指定可执行文件、core 与启动参数

在命令行里这样调用:

gdb --args \
  /home/cilinmengye/ics2023/nemu/build/riscv32-nemu-interpreter \
  -l /home/cilinmengye/ics2023/nanos-lite/build/nemu-log.txt \
  -b \
  -e /home/cilinmengye/ics2023/nanos-lite/build/nanos-lite-riscv32-nemu.elf \
  /home/cilinmengye/ics2023/nanos-lite/build/nanos-lite-riscv32-nemu.bin \
  --core=core.36126
  • --args 后面跟的是可执行文件及其所有命令行参数。
  • 最后加上 --core=core.36126 就把 core 文件也交给 gdb。

启动后直接:

(gdb) bt          # 打印崩溃时的调用栈
(gdb) info args   # 查看传递给主程序的参数

方法二:先打开可执行文件和 core,再在 gdb 里设参数

gdb /home/cilinmengye/ics2023/nemu/build/riscv32-nemu-interpreter core.36126

进入 gdb 后,你还需要把当初运行时的参数补上:

(gdb) set args \
  -l /home/cilinmengye/ics2023/nanos-lite/build/nemu-log.txt \
  -b \
  -e /home/cilinmengye/ics2023/nanos-lite/build/nanos-lite-riscv32-nemu.elf \
  /home/cilinmengye/ics2023/nanos-lite/build/nanos-lite-riscv32-nemu.bin
(gdb) bt         # 打印崩溃时的调用栈

这样就可以在 gdb 里完整地重现当时的运行环境,查看崩溃原因了。

Valgrind
LD_PRELOAD= /home/cilinmengye/ics2023/navy-apps/libs/libos/build/native.so  \
valgrind \
  --tool=memcheck \
  --leak-check=full \
  /home/cilinmengye/ics2023/navy-apps/apps/pal/build/pal-native \
  2> valgrind.log

在我这里可以如上运行起valgrind,注意LD_PRELOAD要放到前面

当valgrind输出内容过多时,可以将其输出到文件中

但是valgrind输出内容实在太多了,这里也没能帮助到我

一个其他修复Segmentation fault的案例

static void draw_ch(int x, int y, char ch, uint32_t fg, uint32_t bg) {
  SDL_Surface *s = BDF_CreateSurface(font, ch, fg, bg);
  assert(s);
  SDL_Rect dstrect = { .x = x, .y = y };
  SDL_BlitSurface(s, NULL, screen, &dstrect);
  SDL_FreeSurface(s);
}

SDL_Surface* BDF_CreateSurface(BDF_Font *font, char ch, uint32_t fg, uint32_t bg) {
  uint32_t *bm = font->font[ch];
  if (!bm) return NULL;
  int w = font->w, h = font->h;
  uint32_t *pixels = (uint32_t *)malloc(w * h * sizeof(uint32_t));
  assert(pixels);
  for (int j = 0; j < h; j ++) {
    for (int i = 0; i < w; i ++) {
      pixels[j * w + i] = ((bm[j] >> i) & 1) ? fg : bg;
    }
  }
  SDL_Surface *s = SDL_CreateRGBSurfaceFrom(pixels, w, h, 32, w * sizeof(uint32_t),
      DEFAULT_RMASK, DEFAULT_GMASK, DEFAULT_BMASK, DEFAULT_AMASK);
  assert(s);
  s->flags &= ~SDL_PREALLOC;
  return s;
}

我有如上函数,我在运行程序时时常会报如下两种错误:

  • Segmentation fault.
  • assert s fail in draw_ch

我通过gdb 调试得到:

Thread 1 "nterm-native" received signal SIGSEGV, Segmentation fault.
0x000055555555961a in BDF_CreateSurface (font=0x55555626eeb0, ch=-28 '\344', fg=3289650, bg=15790320) at /home/cilinmengye/ics2023/navy-apps/libs/libbdf/src/SDL_bdf.cpp:13
13	      pixels[j * w + i] = ((bm[j] >> i) & 1) ? fg : bg;

发现真正的原因是参数ch=-28导致访问font->font[ch];越界

但是有个很奇怪的点是为何错误发生在font->font[ch];但是gdb给出的提示是pixels[j * w + i] = ((bm[j] >> i) & 1) ? fg : bg;呢?

一个猜想是非法访问的蝴蝶效应:

  • 因为访问font->font[ch];越界导致bm是给非法指针,导致靠访问bm获得值的pixels随时都会非法访问,这带来了不确定性
  • 同时与pixels相关的s随时都会出现问题

果然Segment fault问题好麻烦啊...

修复Segmentation fault

依据上述使用GDB和Core得到的发现可以重点重新实现下:

  • SDL_GetKeyState,特别是其内部维护的状态数组,查阅规范看其如何为何
  • 调试板不知道是否和上述报错的函数SDL_BlitSurface相关

image-20250717181504618

实现 SDL_FillRect 的 8bit

# ics2023/navy-apps/libs/libminiSDL/include/sdl-video.h

#define DEFAULT_RMASK 0x00ff0000
#define DEFAULT_GMASK 0x0000ff00
#define DEFAULT_BMASK 0x000000ff
#define DEFAULT_AMASK 0xff000000

static inline int maskToShift(uint32_t mask) {
  switch (mask) {
    case 0x000000ff: return 0;
    case 0x0000ff00: return 8;
    case 0x00ff0000: return 16;
    case 0xff000000: return 24;
    case 0x00000000: return 24; // hack
    default: assert(0);
  }
}

SDL_Surface* SDL_CreateRGBSurface(uint32_t flags, int width, int height, int depth,
    uint32_t Rmask, uint32_t Gmask, uint32_t Bmask, uint32_t Amask) {
    ...
 s->format->palette = NULL;
    s->format->Rmask = Rmask; s->format->Rshift = maskToShift(Rmask); s->format->Rloss = 0;
    s->format->Gmask = Gmask; s->format->Gshift = maskToShift(Gmask); s->format->Gloss = 0;
    s->format->Bmask = Bmask; s->format->Bshift = maskToShift(Bmask); s->format->Bloss = 0;
    s->format->Amask = Amask; s->format->Ashift = maskToShift(Amask); s->format->Aloss = 0;
    ...
}

uint32_t SDL_MapRGBA(SDL_PixelFormat *fmt, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
  assert(fmt->BytesPerPixel == 4);
  uint32_t p = (r << fmt->Rshift) | (g << fmt->Gshift) | (b << fmt->Bshift);
  if (fmt->Amask) p |= (a << fmt->Ashift);
  return p;
}
posted @ 2025-07-15 19:11  次林梦叶  阅读(32)  评论(0)    收藏  举报