ucore-lab1

操作系统实验

Posted by X1ng on August 5, 2020

重修ucore项目,感觉之前白学了

由于大一对操作系统属于是一知半解,自己是真写不出来,基本上代码都是网上抄一遍,重来一遍好好完成一下这个项目

环境

开了一个新的ubuntu 20.04虚拟机

x2ng@ubuntu:~$ uname -a
Linux ubuntu 5.11.0-41-generic #45~20.04.1-Ubuntu SMP Wed Nov 10 10:20:10 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

由于gdb实在是反人类,还是用比较方便的pwndbg插件

git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

下载实验代码

git clone -b master https://github.com/chyyuu/os_kernel_lab.git

基础知识

系统启动规范

BIOS

  • BIOS初始化:硬件自检、检查所连接的硬件、按照指定顺序读取扇区中主引导记录MBR
  • MBR中启动代码检查分区表(GPT)正确性、跳转到磁盘分区上的引导扇区(512字节,结束标志0x55 0xaa)
  • 分区上的引导扇区开头JMP指令和保存文件系统描述信息,JMP跳转到该扇区中真正的启动代码处(512字节,结束标志0x55 0xaa),启动代码识别文件系统,并跳转到保存在磁盘文件系统中的加载程序
  • 加载程序从磁盘文件系统上读取配置文件,再依据配置加载内核

UEFI

所有平台上一致的操作系统启动服务标准,通过可信计算保证加载引导记录安全性

中断处理机制

中断、异常和系统调用的过程

  • 在CPU初始化时依据内部或外部事件设置中断使能标志
  • 依据中断描述符表调用相应中断服务例程/异常服务例程/跳转系统调用表
  • 系统调用在中断向量表中只占一项,具体实现功能的函数地址保存在系统调用表

中断描述符表(IDT)的起始地址和大小包存在中断描述符表寄存器(IDTR)中,中断描述符表中的每一项保存着中断门/陷阱门,中断门/陷阱门中有中断服务例程的段选择子和段内偏移,根据段选择子在全局描述符表(GDT)找到相应的段地址,段地址+段内偏移即中断服务例程的起始地址

对于中断门和陷阱门的区别

中断门和陷阱门在使用上的区别不在于中断是外部产生的还是有CPU本身产生的,而在于通过中断门进入中断服务程序时CPU会自动将中断关闭(将EFLAGS寄存器中IF标志位置0),以防止嵌套中断产生,而通过陷阱门进入服务程序时则维持IF标志位不变。这是二者唯一的区别。

详细请看:任务门、中断门、陷阱门和调用门 - silenccfly - 博客园 (cnblogs.com)

内联汇编

//asm(assembler template
//   :output operands	(optional)
//   :input operands	(optional)
//   :clobbers				(optional)
//   );

long _res, arg1=2, arg2=22, arg3=222, arg4=233
_asm_volatile("int $0x80"
             :"=a"(_res)
             :"0"(11),"b"(arg1),"c"(arg2),"d"(arg3),"S"(arg4))

//不同字母符号对应的寄存器:
//0: 第一个寄存器
//a: %eax
//b: %ebx
//c: %eax
//d: %ebx
//S: %eax
//D: %ebx
//根据这些约束将寄存器和变量结合
movl $11,%eax
movl -28(%ebp),%ebx
movl -24(%ebp),%ecx
movl -20(%ebp),%edx
movl -16(%ebp),%esi
int $0x80
movl %eax,-12(%ebp)

lab1

练习1

练习1:理解通过make生成执行文件的过程。(要求在报告中写出对下述问题的回答)

列出本实验各练习中对应的OS原理的知识点,并说明本实验中的实现部分如何对应和体现了原理中的基本概念和关键知识点。

在此练习中,大家需要通过静态分析代码来了解:

  1. 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
  2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

补充材料:

如何调试Makefile

当执行make时,一般只会显示输出,不会显示make到底执行了哪些命令。

如想了解make执行了哪些命令,可以执行:

$ make "V="

要获取更多有关make的信息,可上网查询,并请执行

$ man make

操作系统镜像文件ucore.img是如何一步一步生成的?

  1. 大致了解一下makefile的语法规则
target ... : prerequisites ...
	command
	...
	...

即生成target依赖所有的prerequisites,command部分是make需要执行的shell命令

makefile提供了系统默认的自动化变量

$^:代表所有依赖文件

$@:代表目标

$<:代表第一个依赖文件

  1. linux命令 dd——dd可从标准输入或文件中读取数据,根据指定的格式来转换数据,再输出到文件、设备或标准输出

视频中说开始不必很深入研究makefile,生成ucore.img的过程大概为

  • gcc编译所有生成bin/kernel所需的文件
  • ld链接生成bin/kernel
  • 编译bootasm.S bootmain.c sign.c
  • 根据sign规范生成obj/bootblock.o
  • 生成ucore.img

一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

查看tools/sign.c

其特征是大小为512字节,第510个(倒数第二个)字节是0x55,第511个(倒数第一个)字节是0xAA。

练习2

练习2:使用qemu执行并调试lab1中的软件。(要求在报告中简要写出练习过程)

为了熟悉使用qemu和gdb进行的调试工作,我们进行如下的小练习:

  1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
  2. 在初始化位置0x7c00设置实地址断点,测试断点正常。
  3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
  4. 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。

提示:参考附录“启动后第一条执行的指令”,可了解更详细的解释,以及如何单步调试和查看BIOS代码。

提示:查看 labcodes_answer/lab1_result/tools/lab1init 文件,用如下命令试试如何调试bootloader第一条指令:

 $ cd labcodes_answer/lab1_result/
 $ make lab1-mon

补充材料: 我们主要通过硬件模拟器qemu来进行各种实验。在实验的过程中我们可能会遇上各种各样的问题,调试是必要的。qemu支持使用gdb进行的强大而方便的调试。所以用好qemu和gdb是完成各种实验的基本要素。

默认的gdb需要进行一些额外的配置才进行qemu的调试任务。qemu和gdb之间使用网络端口1234进行通讯。在打开qemu进行模拟之后,执行gdb并输入

target remote localhost:1234

即可连接qemu,此时qemu会进入停止状态,听从gdb的命令。

另外,我们可能需要qemu在一开始便进入等待模式,则我们不再使用make qemu开始系统的运行,而使用make debug来完成这项工作。这样qemu便不会在gdb尚未连接的时候擅自运行了。

*gdb的地址断点*

在gdb命令行中,使用b *[地址]便可以在指定内存地址设置断点,当qemu中的cpu执行到指定地址时,便会将控制权交给gdb。

*关于代码的反汇编*

有可能gdb无法正确获取当前qemu执行的汇编指令,通过如下配置可以在每次gdb命令行前强制反汇编当前的指令,在gdb命令行或配置文件中添加:

define hook-stop
x/i $pc
end

即可

*gdb的单步命令*

在gdb中,有next, nexti, step, stepi等指令来单步调试程序,他们功能各不相同,区别在于单步的“跨度”上。

next 单步到程序源代码的下一行,不进入函数。
nexti 单步一条机器指令,不进入函数。
step 单步到下一个不同的源代码行(包括进入函数)。
stepi 单步一条机器指令。

通过sudo apt-get install qemu-system命令安装过qemu后(sudo apt-get install qemu并不能安装qemu)

此时在terminal输入”qemu”还是显示command not found

如果想要通过qemu命令来启动的话,建立软链接sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu

在makefile中找到make qemu时,make用来运行qemu的命令

qemu-system-i386 -no-reboot -parallel stdio -hda ./bin/ucore.img -serial null

加上-s让qemu保持监听1234端口,以便gdb连接,但是这样往往还没来的及连接内核以及执行完毕退出了,所以加上-S让qemu在启动的时候等待gdb连接

  • 所以可以

    shell1:

      qemu-system-i386 -no-reboot -parallel stdio -hda ./bin/ucore.img -serial null -s -S
    

    shell2:

      gdb bin/kernel
      target remote 127.0.0.1:1234
    

    进行源码级调试

  • 或者

    创建/tools/gdbinit文件,在里面输入

      target remote 127.0.0.1:1234
      file bin/kernel
    

    保存后

    shell1:

      qemu-system-i386 -no-reboot -parallel stdio -hda ./bin/ucore.img -serial null -s -S
    

    shell2:

      gdb -x tools/gdbinit
    

在0x7c00处下断点

b*0x7c00
c

查看0x7c00处反汇编代码

boot/bootasm.S

之后就是正常的gdb使用

练习3

练习3:分析bootloader进入保护模式的过程。(要求在报告中写出分析)

BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。

提示:需要阅读小节“保护模式和分段机制”和lab1/boot/bootasm.S源码,了解如何从实模式切换到保护模式,需要了解:

  • 为何开启A20,以及如何开启A20
  • 如何初始化GDT表
  • 如何使能和进入保护模式

在保护模式下使用32位地址线,如果A20恒等于0,那么系统无法有效访问所有可用内存,所以进入保护模式必须打开A20

由boot/bootasm.S中

打开A20分为两步

都要先读取0x64端口,判断第2位确保输入缓冲区为空后才能进行写操作

seta20.1往端口0x64写数据0xd1,告诉CPU我要往8042芯片的P2端口写数据

seta20.2往端口0x60写数据0xdf,从而将8042芯片的P2端口的A20设置为1

(inb指令从端口读数据,outb指令向端口写数据)

往下是设置GDT和进入保护模式

lgdt gdtdesc将gdtdesc标签处的全局描述符表(gdt)加载到全局描述符表寄存器GDTR中

之后的指令将cr0寄存器的PE位(cr0寄存器的最低位)设置为1,就进入保护模式了

练习4

练习4:分析bootloader加载ELF格式的OS的过程。(要求在报告中写出分析)

通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,

  • bootloader如何读取硬盘扇区的?
  • bootloader是如何加载ELF格式的OS?

提示:可阅读“硬盘访问概述”,“ELF执行文件格式概述”这两小节。

bootmain主函数函数

先通过readseg ((uintptr_t)ELFHDR, SECTSIZE * 8, 0);读取磁盘中的内核ELF文件

readsect((void *)va, secno);用LBA模式的PIO(Program IO)方式来访问硬盘

视频中说不必深究其中的细节

然后通过判断魔数来判断是不是ELF文件

if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

struct proghdr *ph, *eph;
    
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph ++) {
    readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

由ELF Header里面e_phoff字段(用于记录program header table相对于文件头的偏移值)可以找到程序头表的起始地址,程序头表是一个结构体数组,每个元素记录对应segment的信息,再由e_phnum确定Program header table中个条目个数,将所有的段读取到内存中

然后跳转elf文件头中定义的入口地址e_entry,也就是将控制权交给内核,此时内核也就加载完毕了

练习5

练习5:实现函数调用堆栈跟踪函数 (需要编程)

我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。在如果能够正确实现此函数,可在lab1中执行 “make qemu”后,在qemu模拟器中得到类似如下的输出:

……
ebp:0x00007b28 eip:0x00100992 args:0x00010094 0x00010094 0x00007b58 0x00100096
   kern/debug/kdebug.c:305: print_stackframe+22
ebp:0x00007b38 eip:0x00100c79 args:0x00000000 0x00000000 0x00000000 0x00007ba8
   kern/debug/kmonitor.c:125: mon_backtrace+10
ebp:0x00007b58 eip:0x00100096 args:0x00000000 0x00007b80 0xffff0000 0x00007b84
   kern/init/init.c:48: grade_backtrace2+33
ebp:0x00007b78 eip:0x001000bf args:0x00000000 0xffff0000 0x00007ba4 0x00000029
   kern/init/init.c:53: grade_backtrace1+38
ebp:0x00007b98 eip:0x001000dd args:0x00000000 0x00100000 0xffff0000 0x0000001d
   kern/init/init.c:58: grade_backtrace0+23
ebp:0x00007bb8 eip:0x00100102 args:0x0010353c 0x00103520 0x00001308 0x00000000
   kern/init/init.c:63: grade_backtrace+34
ebp:0x00007be8 eip:0x00100059 args:0x00000000 0x00000000 0x00000000 0x00007c53
   kern/init/init.c:28: kern_init+88
ebp:0x00007bf8 eip:0x00007d73 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
<unknow>: -- 0x00007d72 –
……

请完成实验,看看输出是否与上述显示大致一致,并解释最后一行各个数值的含义。

提示:可阅读小节“函数堆栈”,了解编译器如何建立函数调用关系的。在完成lab1编译后,查看lab1/obj/bootblock.asm,了解bootloader源码与机器码的语句和地址等的对应关系;查看lab1/obj/kernel.asm,了解 ucore OS源码与机器码的语句和地址等的对应关系。

要求完成函数kern/debug/kdebug.c::print_stackframe的实现,提交改进后源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对上述问题的回答。

补充材料:

由于显示完整的栈结构需要解析内核文件中的调试符号,较为复杂和繁琐。代码中有一些辅助函数可以使用。例如可以通过调用print_debuginfo函数完成查找对应函数名并打印至屏幕的功能。具体可以参见kdebug.c代码中的注释。

根据每次内核运行结束后留下的地址信息

Special kernel symbols:
  entry  0x00100000 (phys)
  etext  0x0010388d (phys)
  edata  0x0010f950 (phys)
  end    0x00110dc0 (phys)
Kernel executable memory footprint: 68KB

可以快速定位到内核所在的物理地址,直接在0x100000处下断点即可停在kern_init函数

需要我们补充print_stackframe函数,那么这个函数是如何被调用的呢?

查看kern/init/init.c,可以找到kern_init函数中调用了grade_backtrace函数,寻找这个函数的定义可以看到一个套娃调用

void __attribute__((noinline))
grade_backtrace2(int arg0, int arg1, int arg2, int arg3) {
    mon_backtrace(0, NULL, NULL);
}

void __attribute__((noinline))
grade_backtrace1(int arg0, int arg1) {
    grade_backtrace2(arg0, (int)&arg0, arg1, (int)&arg1);
}

void __attribute__((noinline))
grade_backtrace0(int arg0, int arg1, int arg2) {
    grade_backtrace1(arg0, arg2);
}

void
grade_backtrace(void) {
    grade_backtrace0(0, (int)kern_init, 0xffff0000);
}

套娃结束的调用链为grade_backtrace2->mon_backtrace0->mon_backtrace1->mon_backtrace2(mon_backtrace在kern/debug/kmonitor.c

在导入符号表后调试就比较方便了

b*print_stackframe
c

即可跳转到print_stackframe函数

可以看到在这个函数中push ebp后esp地址为0x7b28,并且通过pwndbg的辅助可以看到往前所有栈帧ebp的内容

可以通过lab1提供的read_eip函数获取eip指向的地址,通过lab1提供的read_ebp函数获取最后一个栈帧的ebp地址,然后通过偏移获取返回地址和参数,再通过解引用可以回溯所有的栈帧,通过ebp是否为0来判断是否遍历完毕

linux下函数调用栈的知识

函数堆栈 · ucore_os_docs (gitbooks.io)

手把手教你栈溢出从入门到放弃(上) - 知乎 (zhihu.com)

补全print_stackframe函数

void
print_stackframe(void) {
    uint32_t ebp = read_ebp();
    uint32_t eip = read_eip();
      
    cprintf("ebp:0x%08x eip:0x%08x args:0x%08x 0x%08x 0x%08x 0x%08x\n", ebp, eip, *((uint32_t *)ebp+2), *((uint32_t *)ebp+3), *((uint32_t *)ebp+4), *((uint32_t *)ebp+5));
    print_debuginfo(eip-1);
    while(1){
        eip = *((uint32_t *)ebp+1);
        ebp = *((uint32_t *)ebp);
        if(!ebp) break;
        cprintf("ebp:0x%08x eip:0x%08x args:0x%08x 0x%08x 0x%08x 0x%08x\n", ebp, eip, *((uint32_t *)ebp+2), *((uint32_t *)ebp+3), *((uint32_t *)ebp+4), *((uint32_t *)ebp+5));
        print_debuginfo(eip-1);
    }
}

运行结果

练习6

练习6:完善中断初始化和处理 (需要编程)

请完成编码工作和回答如下问题:

  1. 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
  2. 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
  3. 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

【注意】除了系统调用中断(T_SYSCALL)使用陷阱门描述符且权限为用户态权限以外,其它中断均使用特权级(DPL)为0的中断门描述符,权限为内核态权限;而ucore的应用程序处于特权级3,需要采用`int 0x80`指令操作(这种方式称为软中断,软件中断,Tra中断,在lab5会碰到)来发出系统调用请求,并要能实现从特权级3到特权级0的转换,所以系统调用中断(T_SYSCALL)所对应的中断门描述符中的特权级(DPL)需要设置为3。

要求完成问题2和问题3 提出的相关函数实现,提交改进后的源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对问题1的回答。完成这问题2和3要求的部分代码后,运行整个系统,可以看到大约每1秒会输出一次”100 ticks”,而按下的键也会在屏幕上显示。

提示:可阅读小节“中断与异常”。

  1. 中断描述符表每个表项8字节(如下图),通过执行int N使cpu跳转到操作系统给出的编号为N的中断服务程序

    对于中断门,第16到31位为中断例程的段选择子,第0到15位 和 第48到63位分别为偏移量的地位和高位

  2. idt_init函数直接在kern_init函数中被调用来初始化中断描述符表,中断描述符表中的表项正是上一问提到的中断门

    kern/trap/trap.c中可以找到idt表的定义

     static struct gatedesc idt[256] = 0;
    

    idt_init函数的功能就是根据上一问中断门的结构将这个数组初始化

    在mmu.h中找到SETGATE宏

     #define SETGATE(gate, istrap, sel, off, dpl) {
        
        
         (gate).gd_off_15_0 = (uint32_t)(off) & 0xffff;        \
         (gate).gd_ss = (sel);                                 \
         (gate).gd_args = 0;                                   \
         (gate).gd_rsv1 = 0;                                   \
         (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;      \
         (gate).gd_s = 0;                                      \
         (gate).gd_dpl = (dpl);                                \
         (gate).gd_p = 1;                                      \
         (gate).gd_off_31_16 = (uint32_t)(off) >> 16;          \
     }
    

    还有一些可能会用到的宏定义

     //kern/mm/mmu.h
     #define STS_IG32        0xE            // 32-bit Interrupt Gate
        
     #define STS_TG32        0xF            // 32-bit Trap Gate
        
        
     //kern/mm/memlayout.h
     #define SEG_KTEXT    1
        
        
     #define GD_KTEXT    ((SEG_KTEXT) << 3)        // kernel text
        
        
     #define DPL_KERNEL    (0)
        
     #define DPL_USER    (3)
        
        
     #define KERNEL_CS    ((GD_KTEXT) | DPL_KERNEL)
        
        
     //kern/trap/trap.h
     #define T_SWITCH_TOK                121    // user/kernel switch
        
    

    根据代码中注释的提示,需要先定义外部数组变量

     extern uintptr_t __vectors[];
    

    __vectors数组中保存的就是中断入口地址,可以在gdb中直接找到这个数组的地址,查看其内容

    其内容与被打印出来的kern/trap/vector.S中的一致

    实验要求的就是初始化中断描述符表的中断入口地址等信息,从内存中可以看到__vectors数组中每个地址占4字节(32位地址当然是4字节2333),所以只需要将中断号和中断入口地址联系起来初始化idt数组即可

    根据提示对于T_SWITCH_TOK号系统调用需要设置dpl为3,保证用户态可以通过该系统调用陷入内核态,最后调用lidt(&idt_pd);来加载中断描述符表

    对于中断处理例程的段选择子可以使用kern/mm/memlayout.h中定义的GD_KTEXT

    补全kern/trap/trap.c中的idt_init函数

     extern uintptr_t __vectors[];
     void
     idt_init(void) {
           for(int i = 0; i<255; i++){
               if(i==T_SWITCH_TOK){
                   SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_USER);
               }
               else{
                   SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
               }
           }
           lidt(&idt_pd);
     }
    
  3. 这一问要求修改陷阱门调度操作,kern/trap/trap.c中的trap_dispatch函数是根据发送的陷阱类型来进行调度

    根据注释的提示可以找到kern/driver/clock.c中定义过的全局变量ticks

    通过全局变量来记录时钟中断的次数,实现每100次时钟中断打印一次的效果

    补全kern/trap/trap.c中的trap_dispatch函数

     static void
     trap_dispatch(struct trapframe *tf) {
         char c;
        
         switch (tf->tf_trapno) {
         case IRQ_OFFSET + IRQ_TIMER:
             if(ticks==100){
                 print_ticks();
                 ticks = 0;
             }
             else{
                 ticks += 1;
             }
             break;
         case IRQ_OFFSET + IRQ_COM1:
             c = cons_getc();
             cprintf("serial [%03d] %c\n", c, c);
             break;
         case IRQ_OFFSET + IRQ_KBD:
             c = cons_getc();
             cprintf("kbd [%03d] %c\n", c, c);
             break;
         case T_SWITCH_TOU:
         case T_SWITCH_TOK:
             panic("T_SWITCH_** ??\n");
             break;
         case IRQ_OFFSET + IRQ_IDE1:
         case IRQ_OFFSET + IRQ_IDE2:
             break;
         default:
             if ((tf->tf_cs & 3) == 0) {
                 print_trapframe(tf);
                 panic("unexpected trap in kernel.\n");
             }
         }
     }
    

challenge1

扩展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();
    }

switchto** 函数建议通过 中断处理的方式实现。主要要完成的代码是在 trap 里面处理 T_SWITCH_TO 中断,并设置好返回的状态。

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

pwn题做多了一直想着push各种值来伪造栈再iret返回,写了一天也没成功,本来决定看Kiprey师傅的笔记,然后看到

请注意:强烈建议学习完lab2中特权级切换的相关知识后再完成该扩展练习。

or2。。遂去看lab2视频,之后就有思路了

  1. 可以看到kern/trap/trap.c中的trap_dispatch函数中有这两个case,看宏定义的名称也能猜到两个中断分别是转换到USER和切换到KERNEL的,对应了前面练习6中设置的T_SWITCH_TOK中断的权限为3(用户态)

         //LAB1 CHALLENGE 1 : YOUR CODE you should modify below codes.
         case T_SWITCH_TOU:
         case T_SWITCH_TOK:
             panic("T_SWITCH_** ??\n");
             break;
    
  2. 需要先在kern/init/init.c中的kern_init函数中将被注释的调用lab1_switch_test函数的这段代码恢复,用于检测challenge的完成

     int
     kern_init(void) {
         extern char edata[], end[];
         memset(edata, 0, end - edata);
        
         cons_init();
        
         const char *message = "(THU.CST) os is loading ...";
         cprintf("%s\n\n", message);
        
         print_kerninfo();
        
         grade_backtrace();
        
         pmm_init();
        
         pic_init();
         idt_init();
        
         clock_init();
         intr_enable();
        
         lab1_switch_test();
        
         while (1);
     }
    
  3. kern/trap/trap.h中找到需要的两个中断的中断号

     #define T_SWITCH_TOU                120    // user/kernel switch
        
     #define T_SWITCH_TOK                121    // user/kernel switch
        
    
  4. 实验的内容应该就是通过编辑kern/init/init.c中的lab1_switch_to_user函数和lab1_switch_to_kernel函数、kern/trap/trap.c中的两个case分支来完成特权级切换的过程

    通过make grade来查看测试效果

     x2ng@ubuntu:~/Downloads/os_kernel_lab/labcodes/lab1$ make grade
     Check Output:            (1.4s)
       -check ring 0:                             OK
       -check switch to ring 3:                   WRONG
        -e !! error: missing '1: @ring 3'
        !! error: missing '1:  cs = 1b'
        !! error: missing '1:  ds = 23'
        !! error: missing '1:  es = 23'
        !! error: missing '1:  ss = 23'
        
       -check switch to ring 0:                   WRONG
        -e !! error: missing '+++ switch to kernel mode +++'
        !! error: missing '2: @ring 0'
        !! error: missing '2:  cs = 8'
        !! error: missing '2:  ds = 10'
        !! error: missing '2:  es = 10'
        !! error: missing '2:  ss = 10'
        
       -check ticks:                              WRONG
        -e !! error: missing '100 ticks'
        !! error: missing 'End of Test.'
        
     Total Score: 10/40
     make: *** [Makefile:241: grade] Error 1
    

    是通过检查四个段寄存器来判断是否成功修改特权级的

  5. 对于cs,ss,ds,es这些段寄存器而言,都有CPL/RPL用来表示段的权限,要实现从内核态向用户态的转换需要修改这些段寄存器的CPL/RPL位

    可以看到在发生中断的时候,有保存上下文的过程,将寄存器保存到中断处理例程的内核栈帧中,其中有我们需要修改的ds、es

    并且中断发生后首先会保存一些值,方便iret返回之前的状态(参考内核pwn构造rop链的过程),其中又有我们需要修改的cs、ss

         rop[i++] = &shell;      // ret addr
         rop[i++] = user_cs;     // cs
         rop[i++] = user_eflags; // eflags
         rop[i++] = user_sp;     // rsp
         rop[i++] = user_ss;     // ss
    

    以T_SWITCH_TOU为例,可以先进入T_SWITCH_TOU中断,然后在对应的case中通过修改这些保存在栈上的数据来实现修改段寄存器的效果

    计算一下中断栈上的偏移

    直接用内联汇编改掉保存这些寄存器上下文处内存中的值

     //kern/trap/trap.c
     asm volatile ("movl %%esp, %%ebx" ::);
     asm volatile ("addl $0x7c, %%ebx" ::);
     asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_DS));
     asm volatile ("addl $0x4, %%ebx" ::);
     asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_DS));
     asm volatile ("addl $0x10, %%ebx" ::);
     asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_CS));
     asm volatile ("addl $0xc, %%ebx" ::);
     asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_DS));
    

    并且在kern/init/init.c中的lab1_switch_to_user函数中进行中断,需要注意的是这里似乎由于中断切换的两个栈比较地址比较相近,可能会覆盖到原来栈的内容(或者是编译器没有平衡好栈帧?不太清楚),需要进行一些操作防止返回后的栈帧被破坏,这里用两个push操作正好能避免原来栈上的数据被覆盖

     //kern/init/init.c
     asm volatile ("pushl %%ebp" :: );
     asm volatile ("pushl %%ebp" :: );
     asm volatile ("int $120" :: );
    
  6. 然鹅做完上述的事情,可以控制程序在终端结束后正常返回了以后,会发现在调用cpeintf的时候会崩溃,没有办法正确的输出

    想了很久可能的bug,最后感觉可能是eflag的问题,搜eflag

    EFLAGS寄存器中的这部分标志用于控制操作系统或是执行操作

    IOPL(bits 12 and 13) [I/O privilege level field] 指示当前运行任务的I/O特权级(I/O privilege level),正在运行任务的当前特权级(CPL)必须小于或等于I/O特权级才能允许访问I/O地址空间。这个域只能在CPL为0时才能通过POPF以及IRET指令修改。

    详细请看:

    x86—EFLAGS寄存器详解_ars longa, vita brevis-CSDN博客_eflags寄存器

    需要将标志位设置为3

    找到kern/mm/mmu.h中控制IOPL的掩码

     /* Eflags register */
     #define FL_IOPL_MASK    0x00003000    // I/O Privilege Level bitmask
    

    将eflag的标志位设置一下就可以了

     asm volatile ("movl %%esp, %%ebx" ::);
     asm volatile ("addl $0x7c, %%ebx" ::);
     asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_DS));
     asm volatile ("addl $0x4, %%ebx" ::);
     asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_DS));
     asm volatile ("addl $0x10, %%ebx" ::);
     asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_CS));
     asm volatile ("addl $0x4, %%ebx" ::);
     asm volatile ("movl (%%ebx), %%eax" : "=a" (eax) : );
     eax = eax | FL_IOPL_MASK;
     asm volatile ("movl %%eax, (%%ebx)" :: "a" (eax));
     asm volatile ("addl $0x8, %%ebx" ::);
     asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_DS));
    
  7. 再根据上面的思路修改T_SWITCH_TOK的中断,不知道什么原因,在T_SWITCH_TOK中断返回内核态的时候,调用iret却没有正常地按照栈上的内容恢复原来的esp,中断返回后只是相当于将ret addr、cs、eflags出栈了,剩下的esp和ss还在栈上,所以ss段寄存器自然还是内核态的(进入中断后会设置ss为内核态),此时esp指向栈上用来保存原来esp的那块内存(stack0为中断后的栈地址,0x7b98为原来的栈地址)

    所以只需要加一个pop esp恢复esp指针即可,不用专门去修改ss段寄存器了

最终修改后代码如下

kern/init/init.c

static void
lab1_switch_to_user(void) {
    asm volatile ("pushl %%ebp" :: );
    asm volatile ("pushl %%ebp" :: );
    asm volatile ("int $120" :: );
    
}

static void
lab1_switch_to_kernel(void) {
    asm volatile ("int $121" :: );
    asm volatile ("popl %%esp" :: );
}

kern/trap/trap.c

static void
trap_dispatch(struct trapframe *tf) {
    char c;
    uint32_t eax = 0;
    
    switch (tf->tf_trapno) {
    case IRQ_OFFSET + IRQ_TIMER:
        if(ticks==100){
            print_ticks();
            ticks = 0;
        }
        else{
            ticks += 1;
        }
        break;
    case IRQ_OFFSET + IRQ_COM1:
        c = cons_getc();
        cprintf("serial [%03d] %c\n", c, c);
        break;
    case IRQ_OFFSET + IRQ_KBD:
        c = cons_getc();
        cprintf("kbd [%03d] %c\n", c, c);
        break;
    case T_SWITCH_TOU:
        asm volatile ("movl %%esp, %%ebx" ::);
        asm volatile ("addl $0x7c, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_DS));
        asm volatile ("addl $0x4, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_DS));
        asm volatile ("addl $0x10, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_CS));
        asm volatile ("addl $0x4, %%ebx" ::);
        asm volatile ("movl (%%ebx), %%eax" : "=a" (eax) : );
        eax = eax | FL_IOPL_MASK;
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (eax));
        asm volatile ("addl $0x8, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_DS));
        break;
    case T_SWITCH_TOK:
        asm volatile ("movl %%esp, %%ebx" ::);
        asm volatile ("addl $0x7c, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (KERNEL_DS));
        asm volatile ("addl $0x4, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (KERNEL_DS));
        asm volatile ("addl $0x10, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (KERNEL_CS));
        asm volatile ("addl $0x4, %%ebx" ::);
        asm volatile ("movl (%%ebx), %%eax" : "=a" (eax):);
        eax = eax & (~FL_IOPL_MASK);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (eax));
        asm volatile ("addl $0x8, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (KERNEL_DS));
        break;
    case IRQ_OFFSET + IRQ_IDE1:
    case IRQ_OFFSET + IRQ_IDE2:
        break;
    default:
        if ((tf->tf_cs & 3) == 0) {
            print_trapframe(tf);
            panic("unexpected trap in kernel.\n");
        }
    }
}

另外看了一下参考答案使用c写的,根据trap_dispatch函数的struct trapframe *tf结构体来修改各个段寄存器的值,其实本质是一致的

可以在kern/trap/trap.h中找到结构体的定义

struct pushregs {
    uint32_t reg_edi;
    uint32_t reg_esi;
    uint32_t reg_ebp;
    uint32_t reg_oesp;            /* Useless */
    uint32_t reg_ebx;
    uint32_t reg_edx;
    uint32_t reg_ecx;
    uint32_t reg_eax;
};

struct trapframe {
    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;
    /* below here defined by x86 hardware */
    uint32_t tf_err;
    uintptr_t tf_eip;
    uint16_t tf_cs;
    uint16_t tf_padding4;
    uint32_t tf_eflags;
    /* below here only when crossing rings, such as from user to kernel */
    uintptr_t tf_esp;
    uint16_t tf_ss;
    uint16_t tf_padding5;
} __attribute__((packed));

challenge2

完成了challenge1后challenge2就很简单了,直接在按键中断的case里编辑,设置接收到0和3分别goto T_SWITCH_TOK、T_SWITCH_TOU就可以了,使用print_trapframe函数来打印当前状态查看是否修改成功

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

详细请看:

扩展练习 · ucore_os_docs (gitbooks.io)

最终代码

kern/trap/trap.c

static void
trap_dispatch(struct trapframe *tf) {
    char c;
    uint32_t eax = 0;
    
    switch (tf->tf_trapno) {
    case IRQ_OFFSET + IRQ_TIMER:
        if(ticks==100){
            print_ticks();
            ticks = 0;
        }
        else{
            ticks += 1;
        }
        break;
    case IRQ_OFFSET + IRQ_COM1:
        c = cons_getc();
        cprintf("serial [%03d] %c\n", c, c);
        break;
    case IRQ_OFFSET + IRQ_KBD:
        c = cons_getc();
        cprintf("kbd [%03d] %c\n", c, c);
        if(c == '3'){
            goto tou;
        }
        else if(c == '0'){
            goto tok;
        }
        else{
            break;
        }
        
    case T_SWITCH_TOU:
tou:
        asm volatile ("movl %%esp, %%ebx" ::);
        asm volatile ("addl $0x7c, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_DS));
        asm volatile ("addl $0x4, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_DS));
        asm volatile ("addl $0x10, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_CS));
        asm volatile ("addl $0x4, %%ebx" ::);
        asm volatile ("movl (%%ebx), %%eax" : "=a" (eax) : );
        eax = eax | FL_IOPL_MASK;
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (eax));
        asm volatile ("addl $0x8, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (USER_DS));
        print_trapframe(tf);
        break;
    case T_SWITCH_TOK:
tok:
        asm volatile ("movl %%esp, %%ebx" ::);
        asm volatile ("addl $0x7c, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (KERNEL_DS));
        asm volatile ("addl $0x4, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (KERNEL_DS));
        asm volatile ("addl $0x10, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (KERNEL_CS));
        asm volatile ("addl $0x4, %%ebx" ::);
        asm volatile ("movl (%%ebx), %%eax" : "=a" (eax):);
        eax = eax & (~FL_IOPL_MASK);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (eax));
        asm volatile ("addl $0x8, %%ebx" ::);
        asm volatile ("movl %%eax, (%%ebx)" :: "a" (KERNEL_DS));
        print_trapframe(tf);
        break;
        
    case IRQ_OFFSET + IRQ_IDE1:
    case IRQ_OFFSET + IRQ_IDE2:
        break;
    default:
        if ((tf->tf_cs & 3) == 0) {
            print_trapframe(tf);
            panic("unexpected trap in kernel.\n");
        }
    }
}

参考资料

Introduction · ucore_os_docs (gitbooks.io)

任务门、中断门、陷阱门和调用门 - silenccfly - 博客园 (cnblogs.com)

x86—EFLAGS寄存器详解_ars longa, vita brevis-CSDN博客_eflags寄存器