加载第一个用户程序
找出ELF文件每一个需要加载的segment的Offset, VirtAddr, FileSiz和MemSiz这些参数
可执行文件在哪里?
可执行文件位于ramdisk偏移为0处, 访问它就可以得到用户程序的第一个字节.
代码和数据在可执行文件的哪个位置?
ELF 用 Type = PT_LOAD 明确告诉 loader:“只有这些 segment 才是运行时必须加载的”
位于ELF 文件的 Offset 位置
代码和数据有多少?
FileSize 字节
"正确的内存位置"在哪里?
内存的 VirtAddr 开始,长度为MemSiz
实现loader
size_t ramdisk_read(void *buf, size_t offset, size_t len);
static uintptr_t loader(PCB *pcb, const char *filename) {
// 利用<elf.h>里的结构体,类似ftrace
Elf_Ehdr eh;
ramdisk_read(&eh, 0, sizeof(eh));
// 检查ELF文件的魔数
assert(eh.e_ident[0] == 0x7f && eh.e_ident[1] == 'E' &&
eh.e_ident[2] == 'L' && eh.e_ident[3] == 'F');
for (int i = 0; i < eh.e_phnum; i++) {
Elf_Phdr ph;
size_t ph_off = eh.e_phoff + (size_t)i * eh.e_phentsize;
ramdisk_read(&ph, ph_off, sizeof(ph));
// 判断一个segment是否需要加载
if (ph.p_type != PT_LOAD)
continue;
if (ph.p_filesz > 0) {
ramdisk_read((void *)(uintptr_t)ph.p_vaddr, ph.p_offset, ph.p_filesz);
}
// .bss段置0
if (ph.p_memsz > ph.p_filesz) {
memset((void *)((uintptr_t)ph.p_vaddr + ph.p_filesz), 0,
ph.p_memsz – ph.p_filesz);
}
}
return (uintptr_t)eh.e_entry;
}
综上
loader分配内存区间[VirtAddr, VirtAddr + MemSiz),从文件拷贝数据(从 ELF 文件的 Offset 位置拷贝 FileSiz 字节到 内存的 VirtAddr 开始),清零剩余部分[VirtAddr + FileSiz, VirtAddr + MemSiz)
Q1:FileSiz通常不会大于相应的MemSiz,因为 内存里有一部分数据,在文件里根本不存在,比如.bss 段(未初始化data)。
Q2:[VirtAddr + FileSiz, VirtAddr + MemSiz)为什么必须清零这段内存?
系统调用
识别系统调用
EVENT_YIELD 和 EVENT_SYSCALL用的是同一条自陷指令ecall,区分它们的关键不是“指令不同”,而是“自陷时寄存器里带了什么标记”。首先用户态执行 yield() 或 syscall(),两者都会执行 ecall,但在执行前会把某个寄存器设置成不同的值。然后进入 CTE 的 trap 处理(abstract-machine 的 cte 实现):CTE 看到 trap 原因是“来自用户态的 ecall”后,再读取上下文里的某个通用寄存器(GPR1)。
Context *__am_irq_handle(Context *c) {
if (user_handler) {
Event ev = {0};
switch (c->mcause) {
case 0xb: { // ecall
c->mepc = c->mepc + 4;
if (c->GPR1 == (uintptr_t)–1)
ev.event = EVENT_YIELD;
else // OS中分发
ev.event = EVENT_SYSCALL;
break;
}
default:
ev.event = EVENT_ERROR;
break;
}
c = user_handler(ev, c);
assert(c != NULL);
}
return c;
}
目前Nanos-lite没有实现任何系统调用, 因此触发了panic.

实现SYS_yield系统调用
(1)在abstract-machine/am/include/arch/目录下的相应头文件中实现正确的GPR?宏, 让它们从上下文c中获得正确的系统调用参数寄存器.
#ifdef __riscv_e
#define GPR1 gpr[15] // a5
#else
#define GPR1 gpr[17] // a7
#endif
#define GPR2 gpr[10] // a0 = x10, arg0
#define GPR3 gpr[11] // a1 = x11, arg1
#define GPR4 gpr[12] // a2 = x12, arg2
#define GPRx gpr[10] // a0 = x10, return value
(2)添加SYS_yield系统调用.
do_syscall里根据c->GPR1处理,具体见下文。
(3)设置系统调用的返回值.
返回给_syscall_()的调用者, 告知其系统调用执行的情况(如是否成功等).
ret 的值是在内核态的 do_syscall() 里,通过改“上下文中的寄存器”设置的。
实现SYS_exit系统调用
void do_syscall(Context *c) {
uintptr_t a[4];
a[0] = c->GPR1;
intptr_t ret = 0;
switch (a[0]) {
case SYS_yield:
yield();
ret = 0;
break;
case SYS_exit:
halt(c->GPRx);
break;
default:
panic("Unhandled syscall ID = %d", a[0]);
}
c->GPRx = ret;
}
}
最后输出:

static Context *do_event(Event e, Context *c) {
switch (e.event) {
case EVENT_YIELD: {
printf("EVENT_YIELD!\\n");
break;
}
case EVENT_SYSCALL: {
printf("EVENT_SYSCALL!\\n");
do_syscall(c);
break;
}
default:
panic("Unhandled event ID = %d", e.event);
}
return c;
}
分析三个打印:(在nanos-lite/src/irq.c的static Context *do_event(Event e, Context *c)里暂时添加的打印,用于分析):
第一个打印EVENT_SYSCALL :sys_yield(作为 syscall 进入内核)
第二个打印EVENT_YIELD!yield 事件:CTE yield 的自陷
第三个打印EVENT_SYSCALL :sys_exit(程序结束,准备退出)
RISC-V Linux为什么没有使用a0来传递系统调用号呢?
原因可能是函数调用中a0是函数的第一个参数,同时也是返回值寄存器。如果用a0 既要承载返回值又原本存着 syscall 号,就会在返回时冲突。系统调用被刻意设计成“看起来像函数调用”,所以在riscv中用a7存 syscall 号(rv_e是a5)
系统调用的踪迹 – strace(这节我是做到精彩纷呈的应用程序补的,debug用)
为了避免系统调用->strace->系统调用套娃,只用了snprintf和putstr,而且写的很麻烦
用的时候如
make ARCH=riscv32-nemu run > build/log.txt 2>&1
效果见下文
#include "syscall.h"
#include <common.h>
#include <fs.h>
void strace_write(int sysid, uintptr_t arg0, uintptr_t arg1, uintptr_t arg2,
intptr_t ret) {
char buf[256];
switch (sysid) {
case SYS_brk: {
(void)snprintf(buf, sizeof(buf), "STRACE: sys_brk(increment:0x%lx)\\n",
(unsigned long)arg0);
break;
}
case SYS_open: {
const char *path = (const char *)(uintptr_t)arg0;
(void)snprintf(buf, sizeof(buf), "STRACE: fs_open(path:%s)\\n",
(path ? path : "(null)"));
break;
}
case SYS_lseek: {
int fd = (int)arg0;
const char *name = fd_to_name(fd);
(void)snprintf(buf, sizeof(buf),
"STRACE: fs_lseek(%s, offset:0x%lx, whence:%lu)\\n", name,
(unsigned long)arg1, (unsigned long)arg2);
break;
}
case SYS_read: {
int fd = (int)arg0;
const char *name = fd_to_name(fd);
(void)snprintf(buf, sizeof(buf),
"STRACE: fs_read(%s, buf:0x%lx, len:%lu) = %ld\\n", name,
(unsigned long)arg1, (unsigned long)arg2, (long)ret);
break;
}
case SYS_write: {
int fd = (int)arg0;
const char *name = fd_to_name(fd);
(void)snprintf(buf, sizeof(buf),
"STRACE: fs_write(%s, buf:0x%lx, len:%lu) = %ld\\n", name,
(unsigned long)arg1, (unsigned long)arg2, (long)ret);
break;
}
case SYS_close: {
int fd = (int)arg0;
const char *name = fd_to_name(fd);
(void)snprintf(buf, sizeof(buf), "STRACE: fs_close(%s) = %ld\\n", name,
(long)ret);
break;
}
case SYS_exit: {
(void)snprintf(buf, sizeof(buf), "STRACE: sys_exit(code:%ld)\\n",
(long)arg0);
break;
}
case SYS_yield: {
(void)snprintf(buf, sizeof(buf), "STRACE: sys_yield()\\n");
break;
}
case SYS_gettimeofday: {
struct timeval *tv = (struct timeval *)(uintptr_t)arg0;
(void)snprintf(buf, sizeof(buf),
"STRACE: sys_gettimeofday(tv:{sec=%ld,usec=%ld})\\n",
(long)tv->tv_sec, (long)tv->tv_usec);
break;
}
default: {
(void)snprintf(buf, sizeof(buf),
"STRACE: unknown sys_%d(arg0=0x%lx, arg1=0x%lx, arg2=0x%lx) = %ld\\n",
sysid, (unsigned long)arg0, (unsigned long)arg1,
(unsigned long)arg2, (long)ret);
break;
}
}
putstr(buf);
}
操作系统之上的TRM
实现标准输出:
用户层的库函数_write():
int _write(int fd, void *buf, size_t count) {
return _syscall_(SYS_write, fd, (intptr_t)buf, count);
}
内核态的SYS_write:
case SYS_write: {
int fd = (int)a[1];
const char *buf = (const char *)(uintptr_t)a[2];
size_t len = (size_t)a[3];
if (fd == 1 || fd == 2) {
for (size_t i = 0; i < len; i++) {
putch(buf[i]);
}
ret = (intptr_t)len;
} else {
ret = –1; // 返回失败
}
break;
}
输出:

实现堆区管理:
用户态库的 _sbrk()(在 navy-apps 的 libos 里):
extern char _end; // abstract-machine/scripts/linker.ld
void *_sbrk(intptr_t increment) {
static intptr_t cur_brk = 0;
if (cur_brk == 0)
cur_brk = (intptr_t)&_end;
intptr_t old = cur_brk;
intptr_t new_brk = cur_brk + (intptr_t)increment;
intptr_t ret = _syscall_(SYS_brk, (intptr_t)new_brk, 0, 0);
if (ret == 0) {
cur_brk = new_brk;
return (void *)old;
}
return (void *)–1;
}
内核态的 SYS_brk(在Nanos-lite的 do_syscall() 里分发):
case SYS_brk:
ret = 0; // 这阶段:总是成功
break;
之前

重新运行navy-apps/tests/hello/hello.c
输出:

分析输出:
很显然,实现_sbrk前printf没法成功申请缓冲区,所以逐个字符地通过write()进行输出, 实现后变为将格式化完毕的字符串通过一次性进行输出.
支持多个ELF的ftrace
跳过



