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]);
  }
}
posted @ 2024-11-13 22:14  上山砍大树  阅读(3)  评论(0编辑  收藏  举报