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修改寄存器的话,应该在调用之前保存寄存器状态。