找回密码
 立即注册
首页 业界区 安全 xv6:从第一个用户程序trap返回kernel态

xv6:从第一个用户程序trap返回kernel态

庞环 昨天 18:35
二、书接上文,上一节大概弄清了从通电到第一个程序运行的脉络。本节将深入探讨上节最后一部分:从 Kernel(内核态)切换到 User(用户态)的执行逻辑,并详细解析 从 User 返回 Kernel 的全过程。
kexec 进程加载与启动流程

阅读kexec所需声明:用户栈大小、程序头结构体定义、proc_pagetable和copyout用处
  1. #define USERSTACK    1     // user stack pages
  2. // Program section header
  3. struct proghdr {
  4.   uint32 type;
  5.   uint32 flags;
  6.   uint64 off;
  7.   uint64 vaddr;
  8.   uint64 paddr;
  9.   uint64 filesz;
  10.   uint64 memsz;
  11.   uint64 align;
  12. };
  13. // Create a user page table for a given process, with no user memory,
  14. // but with trampoline and trapframe pages.
  15. pagetable_t proc_pagetable(struct proc* p);
  16. // Copy from kernel to user.
  17. // Copy len bytes from src to virtual address dstva in a given page table.
  18. // Return 0 on success, -1 on error.
  19. int copyout(pagetable_t pagetable, uint64 dstva, char* src, uint64 len);
复制代码
kexec代码块
  1. int kexec(char* path, char** argv) {
  2.   char *s, *last;
  3.   int i, off;
  4.   uint64 argc, sz = 0, sp, ustack[MAXARG], stackbase;
  5.   struct elfhdr elf;
  6.   struct inode* ip;
  7.   struct proghdr ph;
  8.   pagetable_t pagetable = 0, oldpagetable;
  9.   struct proc* p = myproc();
  10.   begin_op();
  11.   // Open the executable file.
  12.   if ((ip = namei(path)) == 0) {
  13.     end_op();
  14.     return -1;
  15.   }
  16.   ilock(ip);
  17.   // Read the ELF header.
  18.   if (readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf)) goto bad;
  19.   // Is this really an ELF file?
  20.   if (elf.magic != ELF_MAGIC) goto bad;
  21.   if ((pagetable = proc_pagetable(p)) == 0) goto bad;
  22.   // Load program into memory.
  23.   for (i = 0, off = elf.phoff; i < elf.phnum; i++, off += sizeof(ph)) {
  24.     if (readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph)) goto bad;
  25.     if (ph.type != ELF_PROG_LOAD) continue;
  26.     if (ph.memsz < ph.filesz) goto bad;
  27.     if (ph.vaddr + ph.memsz < ph.vaddr) goto bad;
  28.     if (ph.vaddr % PGSIZE != 0) goto bad;
  29.     uint64 sz1;
  30.     if ((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0) goto bad;
  31.     sz = sz1;
  32.     if (loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0) goto bad;
  33.   }
  34.   iunlockput(ip);
  35.   end_op();
  36.   ip = 0;
  37.   p = myproc();
  38.   uint64 oldsz = p->sz;
  39.   // Allocate some pages at the next page boundary.
  40.   // Make the first inaccessible as a stack guard.
  41.   // Use the rest as the user stack.
  42.   sz = PGROUNDUP(sz);
  43.   uint64 sz1;
  44.   if ((sz1 = uvmalloc(pagetable, sz, sz + (USERSTACK + 1) * PGSIZE, PTE_W)) == 0) goto bad;
  45.   sz = sz1;
  46.   uvmclear(pagetable, sz - (USERSTACK + 1) * PGSIZE);
  47.   sp = sz;
  48.   stackbase = sp - USERSTACK * PGSIZE;
  49.   // Copy argument strings into new stack, remember their
  50.   // addresses in ustack[].
  51.   for (argc = 0; argv[argc]; argc++) {
  52.     if (argc >= MAXARG) goto bad;
  53.     sp -= strlen(argv[argc]) + 1;
  54.     sp -= sp % 16;  // riscv sp must be 16-byte aligned
  55.     if (sp < stackbase) goto bad;
  56.     if (copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0) goto bad;
  57.     ustack[argc] = sp;
  58.   }
  59.   ustack[argc] = 0;
  60.   // push a copy of ustack[], the array of argv[] pointers.
  61.   sp -= (argc + 1) * sizeof(uint64);
  62.   sp -= sp % 16;
  63.   if (sp < stackbase) goto bad;
  64.   if (copyout(pagetable, sp, (char*)ustack, (argc + 1) * sizeof(uint64)) < 0) goto bad;
  65.   // a0 and a1 contain arguments to user main(argc, argv)
  66.   // argc is returned via the system call return
  67.   // value, which goes in a0.
  68.   p->trapframe->a1 = sp;
  69.   // Save program name for debugging.
  70.   for (last = s = path; *s; s++)
  71.     if (*s == '/') last = s + 1;
  72.   safestrcpy(p->name, last, sizeof(p->name));
  73.   // Commit to the user image.
  74.   oldpagetable = p->pagetable;
  75.   p->pagetable = pagetable;
  76.   p->sz = sz;
  77.   p->trapframe->epc = elf.entry;  // initial program counter = ulib.c:start()
  78.   p->trapframe->sp = sp;          // initial stack pointer
  79.   proc_freepagetable(oldpagetable, oldsz);
  80.   return argc;  // this ends up in a0, the first argument to main(argc, argv)
  81. bad:
  82.   if (pagetable) proc_freepagetable(pagetable, sz);
  83.   if (ip) {
  84.     iunlockput(ip);
  85.     end_op();
  86.   }
  87.   return -1;
  88. }
复制代码
1. ELF 文件解析与内存布局
kexec 的任务是读取磁盘上的可执行文件(ELF 格式),并把它布置到内存中。ELF 文件由 ELF Header(elfhdr)、Program Header Table、Sections 三部分组成。其中 elfhdr 包含用于判断文件有效性的 magic,并存放了程序头表地址 phoff。通过 phoff 定位程序头后,根据其中 Segment 包含的信息,识别类型为 ELF_PROG_LOAD 的段。系统按 filesz 计算出所需的虚拟内存大小 memsz,并将其读入从 vaddr 开始的对应区域,完成用户进程代码和数据的加载。
2. 用户栈初始化与参数传递
随后,系统为用户分配 2 页内存,分别作为 userstack 和 guard 页。加载过程将参数逐个存入 userstack 中,并遵循 16B 对齐要求。为了让用户程序能够定位这些参数,系统还会将这些参数的地址同样保存到 userstack 中。最后将 a1 寄存器指向栈指针 sp,使得程序进入用户态后能根据地址找到对应的字符串。
3. 进程状态更新与硬件跳转
最后,更新用户进程的 name、pagetable 和 sz,并令 epc 指向 elf.entry。在准备返回阶段,epc 的值被赋给 sepc。当执行 userret 中的 sret 指令后,硬件执行 PC = sepc,处理器便从 elf.entry 开始正式执行用户态程序。
2. 从elf.entry到main

使用user.ld把程序+库链接成一个用户态ELF可执行文件
  1. _%: %.o $(ULIB) $U/user.ld
  2.     $(LD) $(LDFLAGS) -T $U/user.ld -o $@ $< $(ULIB)
复制代码
  1. //
  2. // wrapper so that it's OK if main() does not call exit().
  3. //
  4. void start(int argc, char** argv) {
  5.   int r;
  6.   extern int main(int argc, char** argv);
  7.   r = main(argc, argv);
  8.   exit(r);
  9. }
复制代码
使用反汇编得到如下结果
  1. objdump -f user/_init
  2. user/_init:        file format elf64-littleriscv
  3. architecture: riscv64
  4. start address: 0x00000000000000bc
复制代码
在得到ELF可执行文件的过程中,在链接环节,得到start的地址为0xbc,将0xbc赋值给了elf.entry,最后这个sret执行,PC指向start函数。
  1. void start(int argc, char** argv) {
  2.   int r;
  3.   extern int main(int argc, char** argv);
  4.   r = main(argc, argv);
  5.   exit(r);
  6. }
复制代码
start函数会调用init下的main函数
  1. char* argv[] = {"sh", 0};
  2. int main(void) {
  3.   int pid, wpid;
  4.   if (open("console", O_RDWR) < 0) {
  5.     mknod("console", CONSOLE, 0);
  6.     mknod("statistics", STATS, 0);
  7.     open("console", O_RDWR);
  8.   }
  9.   dup(0);  // stdout
  10.   dup(0);  // stderr
  11.   for (;;) {
  12.     printf("init: starting sh\n");
  13.     pid = fork();
  14.     if (pid < 0) {
  15.       printf("init: fork failed\n");
  16.       exit(1);
  17.     }
  18.     if (pid == 0) {
  19.       exec("sh", argv);
  20.       printf("init: exec sh failed\n");
  21.       exit(1);
  22.     }
  23.     for (;;) {
  24.       // this call to wait() returns if the shell exits,
  25.       // or if a parentless process exits.
  26.       wpid = wait((int*)0);
  27.       if (wpid == pid) {
  28.         // the shell exited; restart it.
  29.         break;
  30.       } else if (wpid < 0) {
  31.         printf("init: wait returned an error\n");
  32.         exit(1);
  33.       } else {
  34.         // it was a parentless process; do nothing.
  35.       }
  36.     }
  37.   }
  38. }
复制代码
1. 文件描述符与子进程创建
系统初始化时,将 console 对应的文件描述符设置为 0,并将标准输出与标准错误重定向到 console 中。随后通过 fork 创建子进程,子进程得到的 pid 为 0,并开始执行 sh 程序。子进程在执行完指定的命令后,通过 exit 退出 shell。
2. 父进程的监控与循环
与此同时,父进程拿到子进程的真实 pid。父进程进入循环状态,持续等待并检查子进程是否结束。一旦子进程结束,父进程则退出当前循环并重启一个新的 shell,从而实现交互界面的持续存在。
3. sh 程序的功能实现
sh 程序的核心功能是解析用户输入的命令。在解析完成后,它通过调用相应的系统调用并传递必要的参数,驱动内核完成具体的任务执行。
3. 系统调用从用户态到内核态的流转

以最常见的write命令为例:
  1. #!/usr/bin/perl -w
  2. # Generate usys.S, the stubs for syscalls.
  3. sub entry {
  4.     my $prefix = "sys_";
  5.     my $name = shift;
  6.     if ($name eq "sbrk") {
  7.         print ".global $prefix$name\n";
  8.         print "$prefix$name:\n";
  9.     } else {
  10.         print ".global $name\n";
  11.         print "$name:\n";
  12.     }
  13.     print " li a7, SYS_${name}\n";
  14.     print " ecall\n";
  15.     print " ret\n";
  16. }
  17. entry("fork");
  18. entry("exit");
  19. entry("wait");
  20. entry("pipe");
  21. entry("read");
  22. entry("write");
复制代码
批量生成usys.S,write如下:
  1. .global write
  2. write:
  3. li a7, SYS_write
  4. ecall
  5. ret
复制代码
uservec部分流程,其中 t0 指向 kernel/usertrap 函数。
  1. .section trampsec
  2. .globl trampoline
  3. .globl usertrap
  4. trampoline:
  5. .align 4
  6. .globl uservec
  7. uservec:   
  8.     # trap.c sets stvec to point here, so
  9.     # traps from user space start here,
  10.     # in supervisor mode, but with a
  11.     # user page table.
  12.     # load the address of usertrap(), from p->trapframe->kernel_trap
  13.     ld t0, 16(a0)
  14.     # call usertrap()
  15.     jalr t0
复制代码
构建系统调用函数的函数指针数组
  1. extern uint64 sys_fork(void);
  2. extern uint64 sys_exit(void);
  3. extern uint64 sys_wait(void);
  4. ...
  5. #define SYS_fork    1
  6. #define SYS_exit    2
  7. #define SYS_wait    3
  8. ...
  9. static uint64 (*syscalls[])(void) = {
  10. [SYS_fork]    sys_fork,
  11. [SYS_exit]    sys_exit,
  12. [SYS_wait]    sys_wait,
  13. ...
  14. }
复制代码
write系统调用到sys_write
  1. uint64 sys_write(void) {
  2.   struct file* f;
  3.   int n;
  4.   uint64 p;
  5.   argaddr(1, &p);
  6.   argint(2, &n);
  7.   if (argfd(0, 0, &f) < 0) return -1;
  8.   return filewrite(f, p, n);
  9. }
  10. void syscall(void) {
  11.   int num;
  12.   struct proc* p = myproc();
  13.   num = p->trapframe->a7;
  14.   if (num > 0 && num < NELEM(syscalls) && syscalls[num]) {
  15.     // Use num to lookup the system call function for num, call it,
  16.     // and store its return value in p->trapframe->a0
  17.     p->trapframe->a0 = syscalls[num]();
  18.   } else {
  19.     printf("%d %s: unknown sys call %d\n", p->pid, p->name, num);
  20.     p->trapframe->a0 = -1;
  21.   }
  22. }
  23. uint64 usertrap(void) {
  24.   int which_dev = 0;
  25.   // send interrupts and exceptions to kerneltrap(),
  26.   // since we're now in the kernel.
  27.   w_stvec((uint64)kernelvec);  // DOC: kernelvec
  28.   struct proc* p = myproc();
  29.   // save user program counter.
  30.   p->trapframe->epc = r_sepc();
  31.   if (r_scause() == 8) {
  32.     // system call
  33.     if (killed(p)) kexit(-1);
  34.     // sepc points to the ecall instruction,
  35.     // but we want to return to the next instruction.
  36.     p->trapframe->epc += 4;
  37.     // an interrupt will change sepc, scause, and sstatus,
  38.     // so enable only now that we're done with those registers.
  39.     intr_on();
  40.     syscall();
  41.   }
  42. }
复制代码
1. 异常触发与现场保存
执行流程首先将系统调用号写入 a7 寄存器,随后通过 ecall 指令触发一次异常(trap)。硬件自动记录 trap 原因为 user ecall 并存入 scause,同时将返回地址存入 sepc。此时权限提升至 S mode,硬件跳转到 uservec 进行异常处理。在 uservec 中,系统首先将当前进程的运行快照保存到 trapframe 中,最后跳转至寄存器 t0 所指向的 kernel/usertrap 函数。
2. 内核态异常处理与跳转
进入 usertrap 函数后,首先将异常向量表地址从 uservec 切换为 kernelvec,以处理内核态可能发生的异常。随后保存返回用户态时所需的指令地址,并正式进入 syscall 处理环节。
3. 函数分发与内核执行
在 syscall 函数内部,系统通过 a7 寄存器中的 num 确定本次调用的具体命令类型。接着利用该编号访问函数指针数组,精准跳转到对应的内核函数。例如,若本次调用号为 SYS_write,系统将获取相应参数并执行 filewrite 内核函数,最终完成实际的写操作。
从用户态到内核态的参数传递:
  1. static uint64 argraw(int n) {
  2.   struct proc* p = myproc();
  3.   switch (n) {
  4.   case 0:
  5.     return p->trapframe->a0;
  6.   case 1:
  7.     return p->trapframe->a1;
  8.   case 2:
  9.     return p->trapframe->a2;
  10.   case 3:
  11.     return p->trapframe->a3;
  12.   case 4:
  13.     return p->trapframe->a4;
  14.   case 5:
  15.     return p->trapframe->a5;
  16.   }
  17.   panic("argraw");
  18.   return -1;
  19. }
  20. // Fetch the nth 32-bit system call argument.
  21. void argint(int n, int* ip) {
  22.   *ip = argraw(n);
  23. }
复制代码
根据参数位次,使用p->trapframe用户态寄存器快照信息进行传参,从a0到a5都可用作传参。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册