NEMU PA3 补充内容
PA3
穿越时空的旅程
让DiffTest支持异常响应机制
在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 指令时发生。
所以应该设置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)来的
ecall
,mcause
的低位字段被设为 11,最高位(interrupt 标志)为 0。
— 即不管你把寄存器装成什么值,硬件对 “发生了环境调用” 这件事,只会把mcause
写成0b0_01011
(在 32 位下是 0xB)。
- 从机器模式(M‑mode)来的
- 那为什么要先
li a7, -1
?
在 RISC‑V 上,Linux/POSIX‑风格的系统调用约定,或者在很多裸机/RTOS 的 semihosting 约定中,都是把 系统调用号(syscall number)放到寄存器a7
(也即 x17)里,然后执行ecall
。- 把
a7
设为-1
(也就是0xFFFFFFFF
),通常被约定为 “yield” 或 “调度让出 CPU” 这类的特殊调用号。 - 操作系统或模拟器在
ecall
异常处理例程中,会检查a7
的值,见到-1
就执行相应的 “让出 CPU” 或 “切换线程” 的逻辑。
- 把
所以:
- 硬件层面:
ecall
总会把mcause.interrupt=0
和mcause.exception_code=11
(environment call from M‑mode)写好,和你给a7
装的值无关。 - 软件层面:你给
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
表示事件编号, cause
和ref
是一些描述事件的补充信息, 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_SYSCALL
和EVENT_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一样
- 识别ecall指令和mret指令(但是目前只能识别出ecall系统调用),mret机器模式异常返回。
- 输出到日志文件中
用户程序和系统调用
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又是如何运行的?
Navy-apps 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()
...
以open
API为例,其在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
如果$(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(
printf
、read
)、内存管理(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.c
的do_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
这里其实有个很严重的问题是我并不知道其他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_begin → elf_nextscn → gelf_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
精彩纷呈的应用程序
实现更多的fixedptc API
fixedpt
的类型, 用于表示定点数, 可以看到它的本质是int32_t
类型.
31 30 8 0
+----+---------------------------+----------+
|sign| integer | fraction |
+----+---------------------------+----------+
这样, 对于一个实数a
, 它的fixedpt
类型表示A = a * 2^8
(截断结果的小数部分). 例如实数1.2
和5.6
用FLOAT
类型来近似表示, 就是
1.2 * 2^8 = 307 = 0x133
+----+---------------------------+----------+
| 0 | 1 | 33 |
+----+---------------------------+----------+
5.6 * 2^8 = 1433 = 0x599
+----+---------------------------+----------+
| 0 | 5 | 99 |
+----+---------------------------+----------+
fixedpt能够表示的最大正数和最小负数为[-0x7fffffff, +0x7fffffff],转变成10进制数为[-8388607.996, +8388607.996]
用fixedpt模拟表示float当然是因为我们不希望有浮点指令的出现,因为我们NEMU不支持浮点指令
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
。
即编译器会在优化阶段自动识别“全是常量的运算”并替换成单个常量值。这里我们的常量值为一个整数,自然没有浮点指令
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
Navy作为基础设施
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
LD_PRELOAD
是 Linux 下动态链接器(ld.so
或 ld-linux.so
)提供的一个环境变量,用来 在程序启动时强制让指定的共享库(.so
文件)最先被加载,从而可以实现对原有库函数的“拦截”或“替换”。具体作用和使用场景包括:
- 库函数拦截(Function Interposition)
-
原理:动态链接时,链接器会按加载顺序搜索符号(函数名、全局变量名等)。
LD_PRELOAD
指定的库会被优先搜索到,所以其中定义的同名函数会覆盖系统默认库(如 glibc)里的实现。 -
示例:假设你写了一个
mymalloc.so
,在里面实现了你自己的malloc
、free
:// 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
的地方,打印日志、统计内存使用,甚至改变分配行为。
- 调试与性能分析
- 故障注入:可以在关键函数里故意返回错误,模拟系统调用失败,测试程序的鲁棒性。
- 性能统计:拦截
read
/write
/open
等 I/O 函数,累计调用次数和耗时,用于简单的性能剖析。
- 兼容性补丁或功能扩展
- 当某些旧程序依赖的库里有 bug,生产环境又不方便升级,你可以写一个小的 shim 库,只修补或重写有问题的函数,然后通过
LD_PRELOAD
加载进去,达到修补的效果。
-
注意事项
-
安全风险
- 如果你在 root 或有高权限的场景下误用
LD_PRELOAD
,可能被恶意库劫持,从而遭受代码注入攻击。 - 出于安全考虑,setuid / setgid 程序(例如
passwd
、sudo
)默认会忽略LD_PRELOAD
。
- 如果你在 root 或有高权限的场景下误用
-
路径搜索
-
LD_PRELOAD
中指定的库需要能被动态链接器找到。通常写绝对路径更安全:LD_PRELOAD=/home/user/libfix.so ./app
-
-
与其他环境变量配合
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
中实现出错了?
运行时环境兼容
以WSL为案例,所谓通过Windows的运行时环境来实现Linux的API, 从而支撑Linux程序在Windows上的运行,即可以理解Linux程序(应用程序)在实现时调用的都是API,其不关心API的具体实现,即其并不关心API的实现架构或者OS是谁,只有你提供了API给我,我调用起来没有报错就行。
我们通过在Windows中依赖Windows的底层机制来实现了API,从而给Linux程序(应用程序)用
注意ramdisk镜像的大小
如何理解?
首先我们要知道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内存中的分布:
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_SetCaption
和SDL_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 文件?
- 进程崩溃时的快照
当一个程序发生严重错误(如非法内存访问segmentation fault
、总线错误等)而被操作系统强制终止时,内核会把该进程当时的内存状态、寄存器值、调用栈等信息,以二进制形式写入到一个文件里,这个文件就是 core dump,通常默认命名为core
或core.<pid>
。 - 配置与产生
- 通过
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
相关
实现 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;
}