处理器只能识别机器指令,不能识别汇编指令。汇编语言是直接面向处理器的程序设计语言,并且操作的对象不是数据,而是寄存器、内存。
汇编语言是一门非常底层的语言,许多其他语言,比如C++,都会编译成汇编语言。运行任何编译型语言之前都需要先生成汇编语言。
RISC-V精简指令集,优势在于指令少、简单。使用x86 CISC而不是RISC-V的唯一优势就是能得到性能的提升,但是这里的性能是以复杂度和潜在的安全为代价的。
sscratch:保存了trapframe page地址0x3fffffe000,RISCV-V提供了csrrw可以交换任意两个寄存器的值。
scause:trap的原因,8表示系统调用。
stvec:trampoline中断上半部处理代码地址,uservec、userret。
sepc:状态切换时用来暂存pc的,最终会保存到trapframe.epc。
sstatus:设置控制寄存器sstatus,SPP bit为0表示下次sret指令返回user mode,而不是supervisor mode。SPIE bit表示sret后是否要打开中断。
tp:保存CPU核编号,hartid。
s0:就是fp。
satp:页表顶级目录物理地址。
return address总在栈帧的第一位,第二位是前一栈帧的指针。
有关Stack Frame中有两个重要的寄存器,第一个是SP(Stack Pointer),它指向Stack的底部并代表了当前Stack Frame的位置。第二个是FP(Frame Pointer),它指向当前Stack Frame的顶部。因为Return address和指向前一个Stack Frame的的指针都在当前Stack Frame的固定位置,所以可以通过当前的FP寄存器寻址到这两个数据。
Caller Saved寄存器在函数调用的时候不会保存,比如a调用函数b时,a不会保存,b有可能会重写这个寄存器。
中断源包括系统调用、设备等,甚至还能嵌套中断。中断分两个部分,上半部由汇编实现,主要是关中断、保存pc、保存用户寄存器以及切换内核stack、内核页表等。下半部是C语言实现的,主要是中断的具体逻辑,会先开中断,然后执行内核函数,此时可以再次被中断。
与usertrap类似,还有一个kerneltrap,专门处理来自内核的中断。
ecall做了三件事:
至于保存用户寄存器、设置内核需要的寄存器,都在trampoline.S中做。ecall指令做的不是很多,RISC-V希望硬件完成较少的事情,其余的交给软件完成,给操作系统程序员最大的灵活性。
之前讲解过用户页表的布局,虚拟地址最高处的几页比较特殊,trampoline是trap代码,系统调用进入内核的必经之路,此时会执行trampoline.S中uservec代码进行保存用户寄存器到trapframe,并将trapframe中的内核参数设置到指定寄存器,trampoline这一页每个进程都有,共享同一个物理页,但是trampoline这一页没有PTE_U,不能被用户态执行,所以ecall要从user mode切换到supervisor mode。
进程的trapframe页中保存的是以下结构体的一个对象,暂存用户寄存器、内核寄存器,因为寄存器是cpu的,而这些参数是进程的,如果再次切换就可能被覆盖了。
struct trapframe {
//从内核进入用户态前设置的
/* 0 */ uint64 kernel_satp; // kernel page table
/* 8 */ uint64 kernel_sp; // top of process's kernel stack
/* 16 */ uint64 kernel_trap; // usertrap()
/* 24 */ uint64 epc; // saved user program counter
/* 32 */ uint64 kernel_hartid; // saved kernel tp
/* 40 */ uint64 ra;
/* 48 */ uint64 sp;
/* 56 */ uint64 gp;
/* 64 */ uint64 tp;
/* 72 */ uint64 t0;
/* 80 */ uint64 t1;
/* 88 */ uint64 t2;
/* 96 */ uint64 s0;
/* 104 */ uint64 s1;
/* 112 */ uint64 a0;
/* 120 */ uint64 a1;
/* 128 */ uint64 a2;
/* 136 */ uint64 a3;
/* 144 */ uint64 a4;
/* 152 */ uint64 a5;
/* 160 */ uint64 a6;
/* 168 */ uint64 a7;
/* 176 */ uint64 s2;
/* 184 */ uint64 s3;
/* 192 */ uint64 s4;
/* 200 */ uint64 s5;
/* 208 */ uint64 s6;
/* 216 */ uint64 s7;
/* 224 */ uint64 s8;
/* 232 */ uint64 s9;
/* 240 */ uint64 s10;
/* 248 */ uint64 s11;
/* 256 */ uint64 t3;
/* 264 */ uint64 t4;
/* 272 */ uint64 t5;
/* 280 */ uint64 t6;
};
.globl trampoline
trampoline:
.align 4
.globl uservec
uservec:
#
# trap.c sets stvec to point here, so
# traps from user space start here,
# in supervisor mode, but with a
# user page table.
#
# sscratch points to where the process's p->trapframe is
# mapped into user space, at TRAPFRAME.
#
# swap a0 and sscratch
# so that a0 is TRAPFRAME
# sscratch存放的是trampoline frame虚拟地址
# 交换a0和sscratch,那么后面的寄存器都会被保存到trampoline frame中。
csrrw a0, sscratch, a0
# save the user registers in TRAPFRAME
sd ra, 40(a0)
sd sp, 48(a0)
sd gp, 56(a0)
sd tp, 64(a0)
sd t0, 72(a0)
sd t1, 80(a0)
sd t2, 88(a0)
sd s0, 96(a0)
sd s1, 104(a0)
sd a1, 120(a0)
sd a2, 128(a0)
sd a3, 136(a0)
sd a4, 144(a0)
sd a5, 152(a0)
sd a6, 160(a0)
sd a7, 168(a0)
sd s2, 176(a0)
sd s3, 184(a0)
sd s4, 192(a0)
sd s5, 200(a0)
sd s6, 208(a0)
sd s7, 216(a0)
sd s8, 224(a0)
sd s9, 232(a0)
sd s10, 240(a0)
sd s11, 248(a0)
sd t3, 256(a0)
sd t4, 264(a0)
sd t5, 272(a0)
sd t6, 280(a0)
# save the user a0 in p->trapframe->a0
csrr t0, sscratch
sd t0, 112(a0)
# restore kernel stack pointer from p->trapframe->kernel_sp
ld sp, 8(a0)
# make tp hold the current hartid, from p->trapframe->kernel_hartid
//cpu核编号
ld tp, 32(a0)
# load the address of usertrap(), p->trapframe->kernel_trap
ld t0, 16(a0)
# restore kernel page table from p->trapframe->kernel_satp
ld t1, 0(a0)
csrw satp, t1
sfence.vma zero, zero
# a0 is no longer valid, since the kernel page
# table does not specially map p->tf.
# jump to usertrap(), which does not return
jr t0
问题:为什么要用trap frame保存用户寄存器,而不是用户栈?
因为栈和编程语言强绑定,编程语言定义的栈有所不一样,甚至有些语言没有栈,内核不能依赖这个。
内核中处理trap的逻辑。
void
usertrap(void)
{
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
//取决于trap来自于用户态还是内核态,两者处理方式不一样,来自于内核态可以少做很多事
//在内核中,先要将STVEC设置为kernelvec,trampoline是处理用户态的trap。
w_stvec((uint64)kernelvec);
//先前将cpu核编号hartid保存在tp寄存器,先从cpu数组里拿到cpu,再从cpu里拿到proc
struct proc *p = myproc();
// save user program counter.
//寄存器是cpu的,为了避免切换到另外一个进程,覆盖了sepc寄存器,将sepc值写入到trapframe
p->trapframe->epc = r_sepc();
if(r_scause() == 8){
// system call
if(p->killed)
exit(-1);
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
//返回用户态后需要执行ecall后一条指令,而不是重新执行ecall
p->trapframe->epc += 4;
// an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
//开中断,可以接受其他中断
intr_on();
syscall();
} else if((which_dev = devintr()) != 0){
// ok
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
if(p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
//返回
usertrapret();
}
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
所有系统调用都会返回一个uint64表示执行状态,也有可能是结果,放在a0中。
执行完syscall后就会调用usertrapret函数,它的主要流程是:
void
usertrapret(void)
{
struct proc *p = myproc();
// we're about to switch the destination of traps from
// kerneltrap() to usertrap(), so turn off interrupts until
// we're back in user space, where usertrap() is correct.
intr_off();
// send syscalls, interrupts, and exceptions to trampoline.S
w_stvec(TRAMPOLINE + (uservec - trampoline));
// set up trapframe values that uservec will need when
// the process next re-enters the kernel.
p->trapframe->kernel_satp = r_satp(); // kernel page table
p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
// set up the registers that trampoline.S's sret will use
// to get to user space.
// set S Previous Privilege mode to User.
unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
x |= SSTATUS_SPIE; // enable interrupts in user mode
w_sstatus(x);
// set S Exception Program Counter to the saved user pc.
w_sepc(p->trapframe->epc);
// tell trampoline.S the user page table to switch to.
uint64 satp = MAKE_SATP(p->pagetable);
// jump to trampoline.S at the top of memory, which
// switches to the user page table, restores user registers,
// and switches to user mode with sret.
uint64 fn = TRAMPOLINE + (userret - trampoline);
//执行trampoline.userret代码段,这两个参数分别放到a0、a1寄存器中
((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
}
.globl userret
userret:
# userret(TRAPFRAME, pagetable)
# switch from kernel to user.
# usertrapret() calls here.
# a0: TRAPFRAME, in user page table.
# a1: user page table, for satp.
# switch to the user page table.
csrw satp, a1
sfence.vma zero, zero
# put the saved user a0 in sscratch, so we
# can swap it with our a0 (TRAPFRAME) in the last step.
# 返回值写入到sscratch,而sscratch中原来保存的是trapframe page地址
ld t0, 112(a0)
csrw sscratch, t0
# restore all but a0 from TRAPFRAME
ld ra, 40(a0)
ld sp, 48(a0)
ld gp, 56(a0)
ld tp, 64(a0)
ld t0, 72(a0)
ld t1, 80(a0)
ld t2, 88(a0)
ld s0, 96(a0)
ld s1, 104(a0)
ld a1, 120(a0)
ld a2, 128(a0)
ld a3, 136(a0)
ld a4, 144(a0)
ld a5, 152(a0)
ld a6, 160(a0)
ld a7, 168(a0)
ld s2, 176(a0)
ld s3, 184(a0)
ld s4, 192(a0)
ld s5, 200(a0)
ld s6, 208(a0)
ld s7, 216(a0)
ld s8, 224(a0)
ld s9, 232(a0)
ld s10, 240(a0)
ld s11, 248(a0)
ld t3, 256(a0)
ld t4, 264(a0)
ld t5, 272(a0)
ld t6, 280(a0)
# restore user a0, and save TRAPFRAME in sscratch
csrrw a0, sscratch, a0
# return to user mode and user pc.
# usertrapret() set up sstatus and sepc.
sret
sret指令是我们在kernel中的最后一条指令,当执行完后会有以下效果:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。