zhuzilin's Blog

about

6.828 Homework xv6 CPU alarm

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,0x30x80都是alarm的参数。

我们再来看alarmhandler

(gdb) print myproc()->alarmhandler
$2 = (void (*)()) 0x80

对应的就是alarmtest.asmperiodic函数的起始位置。

为什么我们不在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.htrapframe最下面的注释:

    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会出现什么?

  • 虽然lectue-note里面说seems to work,但是我这里是会卡住的
  • 对于CPL == 0的状态,因为是从CPL=0到CPL=0的中断,所以不会保存%espesp中会有垃圾,会出现奇怪的现象。
  • 不过从这个实验中我们可以得知,在kernel状态下仍然是可以有中断的。

如果用户给的handler指向了合适的kernel 地址,就可以运行kernel中的指令。

如果运行handler之时有一个time interrupt出现了该怎么办?

  • 可能work,但是会非常令人困惑,并且会导致user stack爆栈(这个不理解...)
  • 或许在完成handler之前,kernel应该关闭timer

如果handler修改寄存器的话,应该在调用之前保存寄存器状态。