we choose to go to the |

上山砍大树

园龄:5年3个月粉丝:13关注:3

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_()的行为:

  • 通用寄存器 a7a0a1a2分别保存系统调用的参数
  • 调用自陷指令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 中国大陆许可协议进行许可。

posted @   上山砍大树  阅读(55)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起