uCore lab1 操作系统实验 challenge

uCore lab1-challenge

我胡汉三又回来了!!!
怎么可以满足于10/40呢!!!
接受挑战,封印解除!!!

扩展练习 Challenge 1

扩展proj4,增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数 值),当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务(通过网络查询所需信息,可找老师咨询。如果完成,且有兴趣做代替考试的实验,可找老师商量)。需写出详细的设计和分析报告。完成出色的可获得适当加分。

提示: 规范一下 challenge 的流程。

kern_init 调用 switch_test,该函数如下:

static void switch_test(void) { 
    print_cur_status(); // print 当前 cs/ss/ds 等寄存器状态 
    cprintf("+++ switch to user mode +++\n"); 
    switch_to_user(); // switch to user mode 
    print_cur_status(); 
    cprintf("+++ switch to kernel mode +++\n"); 
    switch_to_kernel(); // switch to kernel mode 
    print_cur_status(); 
} 

主要要完成的代码是在 trap 里面处理,T_SWITCH_TO* 中断,并设置好返回的状态。

在 lab1 里面完成代码以后,执行 make grade 应该能够评测结果是否正确。

实验思路

这个扩展练习是要求设置两个中断处理程序。

一个是可以实现从内核态转换为用户态的程序。另一个是实现从用户态转换到内核态的程序。

说白了就是要弄明白:

  • int指令和iret指令到底做了什么
  • cpu是如何表示特权级状态的

int指令进行下面一些步骤:(来自xv6中文文档)

  • CPU根据中断向量,从 IDT 中获得第 n 个中断描述符,中断描述符里保存着中断服务例程的段选择子
  • CPU使用IDT查到的中断服务例程的段选择子从GDT中取得相应的段描述符,段描述符里保存了中断服务例程的段基址和属性信息,此时CPU就得到了中断服务例程的起始地址,并跳转到该地址
  • CPU会根据CPL和中断服务例程的段描述符的DPL(DPL 是描述符中记录的特权级)信息确认是否发生了特权级的转换。比如当前程序正运行在用户态,而中断程序是运行在内核态的,则意味着发生 了特权级的转换,这时CPU会从当前程序的TSS信息(该信息在内存中的起始地址存在TR寄存器中)里取得该程序的内核栈地址,即包括内核态的ss和esp的值,并立即将系统当前使用的栈切换成新的内核栈。这个栈就是即将运行的中断服务程序要使用的栈
  • 紧接着就将当前程序使用的用户态的ss和esp压到新的内核栈中保存起来
  • 依次将%eflags %cs %eip errorCode压栈
  • 置 %cs 和 %eip 为描述符中的值,开始执行中断程序

补充一些有关于TTS的信息:

TSS可以留在内存中的任何位置。 任务寄存器(TR)的特殊段寄存器包含一个段选择器,该段选择器指向驻留在GDT中的有效TSS段描述符。 因此,要使用TSS,必须在函数gdt_init中执行以下操作:

  • 在GDT中创建TSS描述符条目
  • 根据需要将足够的信息添加到内存中的TSS
  • 用该段的段选择器加载TR寄存器

TSS中有几个字段,用于在特权级别发生更改时指定新的堆栈指针。 但是在我们的os内核中,只有字段SS0和ESP0是有用的。字段SS0包含CPL = 0的堆栈段选择器,而ESP0包含CPL = 0的新ESP值。当在保护模式下发生中断时,x86 CPU将在TSS中查找SS0和ESP0并加载其值 分别进入SS和ESP。

/* task state segment format (as described by the Pentium architecture book) */
struct taskstate {
    uint32_t ts_link;        // old ts selector
    uintptr_t ts_esp0;        // stack pointers and segment selectors
    uint16_t ts_ss0;        // after an increase in privilege level
    uint16_t ts_padding1;
    uintptr_t ts_esp1;
    uint16_t ts_ss1;
    uint16_t ts_padding2;
    uintptr_t ts_esp2;
    uint16_t ts_ss2;
    uint16_t ts_padding3;
    uintptr_t ts_cr3;        // page directory base
    uintptr_t ts_eip;        // saved state from last task switch
    uint32_t ts_eflags;
    uint32_t ts_eax;        // more saved state (registers)
    uint32_t ts_ecx;
    uint32_t ts_edx;
    uint32_t ts_ebx;
    uintptr_t ts_esp;
    uintptr_t ts_ebp;
    uint32_t ts_esi;
    uint32_t ts_edi;
    uint16_t ts_es;            // even more saved state (segment selectors)
    uint16_t ts_padding4;
    uint16_t ts_cs;
    uint16_t ts_padding5;
    uint16_t ts_ss;
    uint16_t ts_padding6;
    uint16_t ts_ds;
    uint16_t ts_padding7;
    uint16_t ts_fs;
    uint16_t ts_padding8;
    uint16_t ts_gs;
    uint16_t ts_padding9;
    uint16_t ts_ldt;
    uint16_t ts_padding10;
    uint16_t ts_t;            // trap on task switch
    uint16_t ts_iomb;        // i/o map base address
};

/* gdt_init - initialize the default GDT and TSS */
static void gdt_init(void) {
    // 设置TSS,以便在从用户态切到内核态时能够获得正确的堆栈。 
    // 但是这里并不安全,这只是一个临时值,它将在lab2中设置为KSTACKTOP
    ts.ts_esp0 = (uint32_t)&stack0 + sizeof(stack0);
    ts.ts_ss0 = KERNEL_DS;

    // 初始化gdt的TSS字段
    gdt[SEG_TSS] = SEG16(STS_T32A, (uint32_t)&ts, sizeof(ts), DPL_KERNEL);
    gdt[SEG_TSS].sd_s = 0;

    // 重新加载所有段寄存器
    lgdt(&gdt_pd);

    // 加载TSS
    ltr(GD_TSS);
}

说白了,开始就是要知道怎么找中断程序在哪:

  • 拿着中断向量vector作为索引,去查IDT
  • 查到了,诶,有段选子、属性、偏移,但我不认得段选子啊;再拿着段选子去查GDT
  • 查到了,诶,找到段的基址,加上偏移

其中还有一个很重要的问题,那就是特权问题

产生中断后,CPU一定不会将运行控制从高特权环转向低特权环,特权级必须要么保持不变(当操作系统内核自 己被中断的时候),或被提升(当用户态程序被中断的时候)。无论哪一种情况,作为结果的CPL(Current Privilege Level)必须等于目的代码段的DPL。如果CPL发生了改变,一个堆栈切换操作(通过TSS完成)就会发生。

如果中断是被用户态程序中的指令所触发的(比如软件执行INT n生产的中断),还会增加一个额外的检查:门的DPL必须具有与CPL相同或更低的特权。这就防止了用户代码随意触发中断。如果这些检查失败,会产生一个一般保护异常(general-protection exception)

特权级0-3,内核态0,用户态3,数字越小,特权越高

至于iret指令的动作也是类似的,因为int指令和iret指令是一对的,其指令的步骤如下:

  • 将 %eip %cs %eflags 弹栈
  • 若发生特权级转换,将 %esp %ss 弹栈
  • 如果此次处理的是带有错误码(errorCode)的异常,要求相关的中断服务例程 在调用iret返回之前添加出栈代码主动弹出errorCode。

说了这么多,还是虚虚的啊,那我们结合代码说。

中断处理实现

  • 外设基本初始化设置

Lab1实现了中断初始化和对键盘、串口、时钟外设进行中断处理。

  • 中断初始化设置

操作系统如果要正确处理各种不同的中断事件,就需要安排应该由哪个中断服务例程负责处理特定的中断事件。系统将所有的中断事件统一进行了编号(0~255),这个编号称为中断向量。以ucore为例,操作系统内核启动以后,会通过 idt_init 函数初始化 idt 表 (参见trap.c),而其中 vectors 中存储了中断处理程序的入口地址。vectors 定义在 vector.S 文件中,通过一个工具程序 vector.c 生成。其中仅有 System call 中断的权限为用户权限 (DPL_USER),即仅能够使用 int 0x80 指令。此外还有对 tickslock 的初始化,该锁用于处理时钟中断。

vector.S 文件通过 vectors.c 自动生成,其中定义了每个中断的入口程序和入口地址 (保存在vectors 数组中)。其中,中断可以分成两类:一类是压入错误编码的 (error code),另一类不压入错误编码。对于第二类, vector.S 自动压入一个 0。此外,还会压入相应中断的中断号。在压入两个必要的参数之后,中断处理函数跳转到统一的入口 alltraps 处。

  • 中断的处理过程

trap函数(定义在trap.c中)是对中断进行处理的过程,所有的中断在经过中断入口函数 __alltraps预处理后 (定义在 trapasm.S中) ,都会跳转到这里。在处理过程中,根据不同的中断类型,进行相应的处理。在相应的处理过程结束以后,trap将会返回,被中断的程序会继续运行。整个中断处理流程大致如下:

(1)产生中断后,CPU 跳转到相应的中断处理入口 (vectors)。如果特权级发生变化,必须将当前的ss和esp压栈;然后是EFLAGS;清除标志触发器TF和IF;CS和EIP也跟着压进去;接着在栈中压入相应的error_code(是否存在与异常号相关) 以及 trap_no,然后跳转到 alltraps 函数入口:

# 这是执行INT指令后的内核栈状态
(high)
[..........]
[ss]
[esp]
[eflags]
[eip]		
[error_code](不一定存在)← esp
[..........]
(low)

# 这是trapframe
(high)
[..........]
[ss]
[esp]
[eflags]
[eip]		
[error_code](不一定存在)
[trap_no]
[ds]
[es]
[fs]
[gs]
[eax]
[ecx]
[edx]
[ebx]
[oesp]
[ebp]
[esi]
[edi]  ← esp
[..........]
(low)

在栈中保存当前被打断程序的 trapframe结构(参见过程trapasm.S)。设置 kernel的数据段寄存器,最后压入 esp,作为 trap 函数参数(struct trapframe* tf)并跳转到中断处理函数 trap 处:

trapentry.s

# push registers to build a trap frame
# therefore make the stack look like a struct trapframe
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal

# load GD_KDATA into %ds and %es to set up data segments for kernel
movl $GD_KDATA, %eax
movw %ax, %ds
movw %ax, %es

# push %esp to pass a pointer to the trapframe as an argument to trap()
pushl %esp

# call trap(tf), where tf=%esp
call trap

# pop the pushed stack pointer
popl %esp

# return falls through to trapret...
.globl __trapret
__trapret:
# restore registers from stack
popal

# restore %ds, %es, %fs and %gs
popl %gs
popl %fs
popl %es
popl %ds

# get rid of the trap number and error code
addl $0x8, %esp
iret

trap.h

struct trapframe {
    // ------------low addr---------------
    struct pushregs tf_regs;
    uint16_t tf_gs;
    uint16_t tf_padding0;
    uint16_t tf_fs;
    uint16_t tf_padding1;
    uint16_t tf_es;
    uint16_t tf_padding2;
    uint16_t tf_ds;
    uint16_t tf_padding3;   
    uint32_t tf_trapno;
    uint32_t tf_err;
    uintptr_t tf_eip;
    uint16_t tf_cs;
    uint16_t tf_padding4;
    uint32_t tf_eflags;
    uintptr_t tf_esp;
    uint16_t tf_ss;
    uint16_t tf_padding5;
    // ------------high addr-------------
} __attribute__((packed));

熟悉吗,朋友?

(2)详细的中断分类以及处理流程如下:根据中断号对不同的中断进行处理。其中,若中断号是IRQ_OFFSET + IRQ_TIMER 为时钟中断,则把ticks 将增加一。若中断号是IRQ_OFFSET + IRQ_COM1 为串口中断,则显示收到的字符。 若中断号是IRQ_OFFSET + IRQ_KBD 为 键盘中断,则显示收到的字符。若为其他中断且产生在内核状态,则挂起系统。

(3)结束 trap 函数的执行后,通过 ret 指令返回到 alltraps 执行过程。从栈中恢复所有寄存器的值。调整 esp 的值:跳过栈中的 trap_no 与error_code,使esp指向中断返回 eip,通过 iret 调用恢复 cs、eflag以及 eip,继续执行。

坑点

32位保护模式下中断发生时的压栈情况

中断是可以在任何特权级别下发生的,不同特权级别下处理器使用不同的栈,如果涉及到特权级变化,还要压入 SS 和 ESP 寄存器。

(1)当中断发生时,低特权级向高特权级转化时的压栈现象(用户→内核)

我们是否能访问这个目标段描述符,要做的就是将找到中断描述符时当前的CPL与目标段描述符的DPL进行对比。

这里我们讨论的是CPL特权级比DPL低的情况,即数值上CPL > DPL。这表示我们要往高特权级栈上转移,也意味着我们最后需要恢复旧栈,所以处理器先临时保存一下旧栈的SS和ESP(SS是堆栈段寄存器,因为换了一个栈,所以其也要变,ESP相当于在栈上的索引),然后加载新的特权级和DPL相同的段,将其加载到SS和ESP中,然后将之前保存的旧栈的SS和ESP压到新栈中

(2)当中断发生时,无特权级转化时的压栈现象(内核→用户)

此时由于不会切换栈,就不用保存SS和ESP

说白了:

  • 内核→用户,不压栈
  • 用户→内核,压栈

这种中断返 回是用 iret 指令实现的。注意在返回的时候,其errorCode不会自动跳过,所以需要我们手动跳过。(险些trapentry.S帮我们写了)

【指令手册原文】

the IRET instruction pops the return instruction pointer, return code segment selector, and EFLAGS image from the stack to the EIP, CS, and EFLAGS registers, respectively, and then resumes execution of the interrupted program or procedure. If the return is to another privilege level, the IRET instruction also pops the stack pointer and SS from the stack, before resuming program execution.

翻译:

IRET指令将返回指令指针,返回代码段选子和EFLAGS镜像分别从堆栈弹出到EIP,CS和EFLAGS寄存器,然后继续执行被中断的程序或过程。 如果返回到另一个特权级别,则IRET指令还会在继续执行程序之前从堆栈中弹出堆栈指针和SS。

所以内核→用户那边需要我们自个儿压!

代码

因此,内核→用户:

static void lab1_switch_to_user(void) {
    //LAB1 CHALLENGE 1 : TODO
	asm volatile (
        // 自己压
	    "pushl %%ss \n"
        "pushl %%esp \n"
	    "int %0 \n"
	    "movl %%ebp, %%esp"
	    : 
	    : "i"(T_SWITCH_TOU)
	);
}

case T_SWITCH_TOU:
    if (tf->tf_cs != USER_CS) {
        tf->tf_cs = USER_CS;
        tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
        tf->tf_eflags |= FL_IOPL_MASK;
    }
    break;

用户→内核:

static void lab1_switch_to_kernel(void) {
    //LAB1 CHALLENGE 1 :  TODO
	asm volatile (
	    "int %0 \n"
	    "movl %%ebp, %%esp \n"
	    : 
	    : "i"(T_SWITCH_TOK)
	);
}

case T_SWITCH_TOK:
    if (tf->tf_cs != KERNEL_CS) {
        tf->tf_cs = KERNEL_CS;
        tf->tf_ds = tf->tf_es = tf->tf_ss = KERNEL_DS;
        tf->tf_eflags &= ~FL_IOPL_MASK;
    }
    break;

跑一下

...
++ setup timer interrupts
0: @ring 0
0:  cs = 8
0:  ds = 10
0:  es = 10
0:  ss = 10
+++ switch to  user  mode +++
1: @ring 3
1:  cs = 1b
1:  ds = 23
1:  es = 23
1:  ss = 23
+++ switch to kernel mode +++
2: @ring 0
2:  cs = 8
2:  ds = 10
2:  es = 10
2:  ss = 10
100 ticks
100 ticks
100 ticks
...

OHHHHHHHHHHHHHHHHHHHHHHHHHHH!!!

查查分:

moocos-> make grade
Check Output:            (2.3s)
  -check ring 0:                             OK
  -check switch to ring 3:                   OK
  -check switch to ring 0:                   OK
  -check ticks:                              OK
Total Score: 40/40

OHHHHHHHHHHHHHHHHHHHHHHHHHHH!!!

扩展练习 Challenge 2

用键盘实现用户模式内核模式切换。具体目标是:“键盘输入3时切换到用户模式,键盘输入0时切换到内核模式”。 基本思路是借鉴软中断(syscall功能)的代码,并且把trap.c中软中断处理的设置语句拿过来。

注意:

1.关于调试工具,不建议用lab1_print_cur_status()来显示,要注意到寄存器的值要在中断完成后tranentry.S里面iret结束的时候才写回,所以再trap.c里面不好观察,建议用print_trapframe(tf)

2.关于内联汇编,最开始调试的时候,参数容易出现错误,可能的错误代码如下

asm volatile ( "sub $0x8, %%esp \n" 
"int %0 \n" 
"movl %%ebp, %%esp" 
: ) 

要去掉参数int %0 \n这一行

3.软中断是利用了临时栈来处理的,所以有压栈和出栈的汇编语句。硬件中断本身就在内核态了,直接处理就可以了。

实验思路

在我的理解看来,其实trap,就是软中断;而临时栈,应该说的就是trapframe这个数据结构了

我们再challenge1的基础上,先尝试着按一下:

kbd [048] 0
kbd [000] 
kbd [051] 3
kbd [000] 

这是他原来的代码:

case IRQ_OFFSET + IRQ_KBD:
        c = cons_getc();
        cprintf("kbd [%03d] %c\n", c, c);
        break;

emmm。。。。。先到这吧,搞了好久,先休息~

待更新。。。

你可能感兴趣的:(操作系统)