date: 2019-02-26
tags: OS 6.828
这次的作业和之前的system call那次的作业非常像,这次是加入一个叫alarm的system call。其主要的功能是每间隔若干个cpu tick就触法一次handle函数。所以这里不再赘述如何创建一个system call,而是关注于不同的地方。
作业中给的测试代码如下:
// alarmtest.c
#include "types.h"
#include "stat.h"
#include "user.h"
void periodic();
int
main(int argc, char *argv[])
{
int i;
printf(1, "alarmtest starting\n");
alarm(10, periodic);
for(i = 0; i < 25*500000; i++){
if((i % 250000) == 0)
write(2, ".", 1);
}
exit();
}
void
periodic()
{
printf(1, "alarm!\n");
}注意到period是调用的定义在user.h中的printf这个函数是用户函数,而不是kernel函数,也就是kernel是不能调用这个函数的,所以只能在trap中把eip指向这个函数,返回让其在用户环境中运行。所以在trap.c中,需要加入的代码是:
case T_IRQ0 + IRQ_TIMER:
if(myproc() != 0 && (tf->cs & 3) == 3) {
myproc()->alarmcountdown--;
if(myproc()->alarmcountdown == 0) {
myproc()->alarmcountdown = myproc()->alarmticks;
tf->esp -= 4;
*(uint *) tf->esp = tf->eip;
tf->eip = (uint)myproc()->alarmhandler;
}
}
if(cpuid() == 0){
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}
lapiceoi();
break;注意,只加入了中间的一部分,后面的是原来就有的。相当于是认为的为handler创建了一个frame,或者说用c实现了汇编中的call。为什么这么做而不是在kernel里面直接调用函数在下面有讲解。
下面我们跟着lecture 8,对我们的代码进行测试,首先加入断点并运行alarmtest
(gdb) break sys_alarm首先,我们来看syscall是怎么知道用的是哪一个system call,在
(gdb) print myproc()->tf->eax
$1 = 23这里的tf->eax保存了vector number。
然后我们来看看stack
(gdb) x/4x myproc()->tf->esp
0x2fac: 0x00000034 0x00000003 0x00000080 0x00000000结合alarmtest.asm:
...
28: 68 80 00 00 00 push $0x80
2d: 6a 03 push $0x3
2f: e8 66 03 00 00 call 39a <alarm>
34: 83 c4 10 add $0x10,%esp
...可以知道0x34是return address,0x3和0x80都是alarm的参数。
我们再来看alarmhandler:
(gdb) print myproc()->alarmhandler
$2 = (void (*)()) 0x80对应的就是alarmtest.asm中periodic函数的起始位置。
为什么我们不在trap中直接调用alarmhandler呢?
不能这么做!因为这样会在kernel mode下运行user代码,让user有可能修改kernel stack,直接导致隔离失败,非常危险。
试过之后,会发现并不会crash,只会显示.而不会显示alarm。即使是在sys_alarm里面调用alarmhandler也会出现问题。如果我们在sys_write里面加断点可以发现:在alarmhandle里面调用的sys_write里
(gdb) print myproc()->tf->eax
$2 = 16
(gdb) x/4x myproc()->tf->esp
0x495: 0x8310c483 0xb60f01c6 0xdb84ff5e 0xff857774也就是说sys_write里面的%esp并没有保存write需要的参数,但是正确保存了%eax,说明trapframe还是正确地被保存了的。所以问题就在于esp。原因应该是从CPL=0到CPL=0的中断中硬件并没有切换stack,所以就没有保存%esp,所以%esp中有垃圾。详情可以见x86.h中trapframe最下面的注释:
struct trapframe {
...
// below here only when crossing rings, such as from user to kernel
uint esp;
ushort ss;
ushort padding6;
};虽然不能正常运行,能够在kernel里面直接调用alarmhandler是一件很讨厌的事。这说明kernel可以直接跳到user instruction里去,那么user就可以更改kernel stack,而且神奇的是system call(INT指令)竟然可以在kernel里面运行。这些都是在设计xv6的时候不希望发生的事!
出现上述现象是因为x86的硬件不提供isolation
x86提供的很多相互独立的feature(page table, INT, &c)之间是可能被隔离的,但这不是默认设置!
(不知道可不可以这么理解,就是x86允许你这么写代码,也就是不会报错,但是设计人员应当避免这种写法。)
如果trap不检查CPL==3会出现什么?
CPL == 0的状态,因为是从CPL=0到CPL=0的中断,所以不会保存%esp,esp中会有垃圾,会出现奇怪的现象。如果用户给的handler指向了合适的kernel 地址,就可以运行kernel中的指令。
如果运行handler之时有一个time interrupt出现了该怎么办?
如果handler修改寄存器的话,应该在调用之前保存寄存器状态。