dummy的系统调用
dummy的系统调用
os的异常处理
让Nanos-lite(后面统称为os)事件处理回调函数识别自陷事件EVENT_YIELD
.
os初始化CTE的时候,注册的回调函数为do_event
,所以修改其中的事件判断条件即可(在nanos-lite/src/irq.c
中定义)
static Context* do_event(Event e, Context* c) {
switch (e.event) {
case EVENT_YIELD: printf("EVENT_YIELD event!\n"); break;
case EVENT_SYSCALL: do_syscall(c); break;
default: panic("Unhandled event ID = %d", e.event);
}
return c;
}
void init_irq(void) {
Log("Initializing interrupt/exception handler...");
cte_init(do_event);
}
os可以正确触发自陷操作,为应用程序提供了执行流切换的入口。下一步就是将程序加载到os中。注意:在Nanos-lite中, Log()
宏通过你在klib
中编写的printf()
输出, 最终会调用TRM的putch()
.
加载程序
在操作系统中, 加载用户程序是由loader(加载器)模块负责的。其功能是:
- 从文件中获取要加载的信息
get_pt_load_segments()
- 加载获取到的信息到内存
load_segments
- 返回入口地址
做完了PA上面的操作后,可以得知可执行文件位于ramdisk偏移为0处, 访问它就可以得到用户程序的第一个字节.结合ELF文件的操作man 5 elf
,可以写出从ELF文件中获取要加载的信息函数get_pt_load_segments()
// 获取 PT_LOAD 段的数组和数量,并返回控制转移的入口地址
uintptr_t get_pt_load_segments(const char *filename, Elf_Phdr *pt_load_segments, size_t *num_pt_load_segments) {
Elf_Ehdr ehdr;
// Step 1: 读取 ELF 头部
ramdisk_read(&ehdr, 0, sizeof(Elf_Ehdr));
// 检查 ELF 魔数
if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0) {
panic("Not a valid ELF file.\n");
}
// Step 2: 读取所有 Program Headers
Elf_Phdr phdrs[MAX_SEGMENTS];
ramdisk_read(phdrs, ehdr.e_phoff, ehdr.e_phnum * sizeof(Elf_Phdr));
// Step 3: 筛选出所有 PT_LOAD 段
int count = 0;
for (int i = 0; i < ehdr.e_phnum; i++) {
if (phdrs[i].p_type == PT_LOAD) {
pt_load_segments[count++] = phdrs[i];
}
}
// 保存要加载程序段的条目
*num_pt_load_segments = count;
// Step 4: 返回控制转移的入口地址
return ehdr.e_entry;
}
以及将程序段加载到内存中的函数load_segments()
// 段加载函数,输入参数为pt_load_segments和其数量
void load_segments(Elf_Phdr *pt_load_segments, int num_segments){
for (int i = 0; i < num_segments; i++) {
Elf_Phdr *seg = &pt_load_segments[i];
// 获取段的偏移、虚拟地址、文件大小、内存大小
size_t offset = seg->p_offset;
uint32_t vaddr = seg->p_vaddr;
size_t filesz = seg->p_filesz;
size_t memsz = seg->p_memsz;
// 从ramdisk中读取段数据到内存
ramdisk_read((void *)(vaddr), offset, filesz);
// 清零 [VirtAddr + FileSiz, VirtAddr + MemSiz) 的内存
if (memsz > filesz) {
memset((void *)(vaddr + filesz) , 0, memsz - filesz);
}
}
}
两个主要功能做好后,loader的功能就ok了。
#define MAX_SEGMENTS 16 // 假设 ELF 文件中 Program Headers 的最大数量
static uintptr_t loader(PCB *pcb, const char *filename) {
size_t count;
Elf_Phdr pt_load_segments[MAX_SEGMENTS];
// 获取要加载的段信息
uintptr_t entry = get_pt_load_segments(filename, pt_load_segments, &count);
// 加载段到内存中
load_segments(pt_load_segments, count);
return entry;
}
系统调用
这里的系统调用包含两个操作:
- 系统调用参数:通过通用寄存器保存
- 系统调用指令:自陷指令
ecall
dummy
程序会执行系统调用方法_syscall_()
#define SYS_yield 1
_syscall_(SYS_yield, 0, 0, 0);
具体定义在navy-apps/libs/libos/src/syscall.c
中
intptr_t _syscall_(intptr_t type, intptr_t a0, intptr_t a1, intptr_t a2) {
register intptr_t _gpr1 asm (GPR1) = type;
register intptr_t _gpr2 asm (GPR2) = a0;
register intptr_t _gpr3 asm (GPR3) = a1;
register intptr_t _gpr4 asm (GPR4) = a2;
register intptr_t ret asm (GPRx);
asm volatile (SYSCALL : "=r" (ret) : "r"(_gpr1), "r"(_gpr2), "r"(_gpr3), "r"(_gpr4));
return ret;
}
结合繁多的宏定义,解析_syscall_()
的行为:
- 通用寄存器
a7
、a0
、a1
和a2
分别保存系统调用的参数 - 调用自陷指令
ecall
- 执行完毕后,将寄存器
a0
值传递给变量ret
作为返回值
联系之前CTE中yield()
在调用自陷指令之前,用a7
保存了一个-1
:
li a7, -1
其目的都是相同的。用a7
来保存系统调用的类型,这样不同的事件(访问文件、IO操作等)调用相同的自陷指令进行执行流切换的时候,就可以区分出来了。
这样就要修改CTE中的异常事件处理函数__am_irq_handle()
中,对于事件的判断条件。将之前的从CSR寄存器mcause
判断改为从寄存器a7
中判断
Context* __am_irq_handle(Context *c) {
if (user_handler) {
Event ev = {0};
int type = (int) c->GPR1;
if (type < 0) {
ev.event = EVENT_YIELD;
} else if (type >= 0 && type <= 16) {
ev.event = EVENT_SYSCALL;
}else {
printf("c->GPR1 = %d\n", c->GPR1);
assert(0);
}
c = user_handler(ev, c);
assert(c != NULL);
}
return c;
}
在abstract-machine/am/include/arch/riscv.h
中,根据RISCV32的ABI定义的寄存器名称,修改通用寄存器下标,实现正确的GPR?
宏, 让它们从上下文c
中获得正确的系统调用参数寄存器.
#define GPR1 gpr[17] // a7
#define GPR2 gpr[10] // a0
#define GPR3 gpr[11] // a1
#define GPR4 gpr[12] // a2
#define GPRx gpr[10] // a0
这样异常事件打包函数__am_irq_handle
就可以将自陷指令对应的系统调用事件EVENT_SYSCALL
打包起来,交付给os处理了。
os收到系统调用事件之后,将会调出系统调用处理函数do_syscall()
进行处理.并且按照用户进程之前设置好的系统调用参数(a7
保存的值),进行下一步的分发。这里dummy
的系统调用参数为1
,触发了一个SYS_yield
的系统调用。实现SYS_yield
系统调用宏:
- 调用CTE的
yield()
- 返回值设置为0
void sys_yield(Context *c) {
yield();
c->GPRx = 0;
}
void sys_exit() {
halt(SYS_exit);
}
void do_syscall(Context *c) {
uintptr_t a[4];
a[0] = c->GPR1;
switch (a[0]) {
case SYS_exit : sys_exit(c); break;
case SYS_yield: sys_yield(c); break;
default: panic("Unhandled syscall ID = %d", a[0]);
}
}