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]); } }
本文作者:上山砍大树
本文链接:https://www.cnblogs.com/shangshankandashu/p/18544939
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步