课程主页:https://www.xuetangx.com/course/THU08091000267/5883104?channel=i.area.learn_title 实验指导书:https://objectkuan.gitbooks.io/ucore-docs/content/ github:https://github.com/chyyuu/ucore_os_lab (master 分支)
前置知识 make/gdb
使用、磁盘 MBR
格式规范、BIOS 执行流程、bootloader 执行流程、ELF
文件格式、函数调用底层过程、中断处理流程。
练习一 此练习用于了解编译 ucore
源代码为镜像文件 ucore.img
的整体流程。
因为我不是很懂 Makefile
,因此我会尽可能地避免介绍 Makefile
内部相关指令含义。
在命令行目录 labcodes_answer/lab1_result
中,依次输入命令 make clean,make "V="
,即可得到详细的编译过程 (已删除若干冗余输出结果):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 + cc kern/init/init.c gcc -Ikern/init/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap / -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o + cc kern/libs/stdio.c gcc -Ikern/libs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap / -Ikern/mm/ -c kern/libs/stdio.c -o obj/kern/libs/stdio.o ... gcc -Ilibs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/printfmt.c -o obj/libs/printfmt.o + ld bin/kernel ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/picirq.o obj/kern/driver/intr.o obj/kern/trap /trap.o obj/kern/trap /vectors.o obj/kern/trap /trapentry.o obj/kern/mm/pmm.o obj/libs/string.o obj/libs/printfmt.o + cc boot/bootasm.S gcc -Iboot/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o + cc boot/bootmain.c gcc -Iboot/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o + cc tools/sign.c gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign + ld bin/bootblock ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o 'obj/bootblock.out' size: 500 bytesbuild 512 bytes boot sector: 'bin/bootblock' success! dd if =/dev/zero of=bin/ucore.img count=10000 10000+0 records in 10000+0 records out 5120000 bytes (5.1 MB, 4.9 MiB) copied, 0.0307689 s, 166 MB/s dd if =bin/bootblock of=bin/ucore.img conv=notrunc 1+0 records in 1+0 records out 512 bytes copied, 0.000149858 s, 3.4 MB/s dd if =bin/kernel of=bin/ucore.img seek=1 conv=notrunc 146+1 records in 146+1 records out 74868 bytes (75 kB, 73 KiB) copied, 0.000415369 s, 180 MB/s
可详见 os_kernel_lab/labcodes_answer/lab1_result/tools/sign.c
以查看其功能。
ucore
源代码中的 bootblock
也是常说的 bootloader
。
ucore
的 MBR
十分简单,含 bootloader
而不含磁盘分区表信息。
练习二 此练习用于了解计算机启动至 bootblock
开始执行期间的指令执行顺序。
对于 intel 以往机器而言,地址线 20 位 (物理寻址空间 2^20 = 1M),而寄存器仅 16 位。为实现访问全部的地址空间,另设若干段寄存器 (例如 CS 表示代码段寄存器),其中存放段基址。此时实际物理地址 = (段基址 << 4 + IP)。鉴于此种地址访问方式直接访问物理地址,因此其称为 实模式 。
为兼容以往机器,intel x86 机器启动后,首先进入 实模式 ,并寻址第一条指令 CS:IP = 0xf000:0xfff0 => 0xffff0
以执行 BIOS 指令。
为了解此执行顺序,需要进行单步调试。
首先查看 labcodes/lab1/Makefile
中的 debug
部分指令:
1 2 3 4 5 6 7 $(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR) /q.log -monitor stdio -hda $< -serial null" $(V)sleep 2 $(V)$(TERMINAL) -e "gdb -q -x tools/gdbinit"
其中 gdbinit
内容如下 (相较于源代码,此部分已经修改):
1 2 3 4 5 6 7 8 9 10 11 file bin/kernel set architecture i8086target remote :1234 define hook-stop x/i $pc end
在命令行目录 labcodes/lab1
中,输入命令 make debug
,开始进行调试。
查看 CS:IP 取值及此处指令:
1 2 3 4 5 6 7 8 9 10 (gdb) p/x $cs $2 = 0xf000(gdb) p/x $eip $3 = 0xfff0(gdb) x /i 0xffff0 0xffff0: ljmp $0x3630 ,$0xf000e05b
简要叙述 BIOS 功能:
提供基本的输入输出功能 (例如,机器启动进入 BIOS 界面后,允许输入键盘信息并输出信息至显示屏)。
硬件自检 (检查与机器启动密切相关的硬件是否正常,如果不正常则直接启动失败,否则才允许继续启动)
加载 bootloader
(BIOS 允许从指定磁盘启动,那么它便会加载指定磁盘的 0 号扇区,即 MBR
,至 0x0:0x7C00
处,并设置 CS:IP
为此值,从而执行 bootloader
指令)。
输入如下指令,以设置断点并运行机器至断点指令处。
1 2 (gdb) b *0x7c00 (gdb) continue
查看断点处指令,可得 (其与 bootasm.S
文件中 start
处指令相同,即执行的是此处指令,相关原因可见上述的 make "V="
输出结果):
1 2 3 4 5 6 7 8 (gdb) p/x $pc $4 = 0x7c00 (gdb) x /5i $pc => 0x7c00: cli 0x7c01: cld 0x7c02: xor %eax,%eax 0x7c04: mov %eax,%ds 0x7c06: mov %eax,%es
练习三 此练习用于了解 bootasm.S
代码所做的工作。
bootasm.S
是 bootloader
的一部分,其主要完成三件事:1. 开启 A20;2. 初始化 GDT 表;3. 开启保护模式。
对于 intel x86 机器而言,其地址线 32 位 (物理寻址空间 2^32 = 4G),普通寄存器 32 位,段寄存器仍为 16 位 (兼容以往机器)。为访问全部的地址空间且基于分段提供保护功能,段寄存器此时存放 GPT 索引表位置及相关保护位,其中 GPT 表项存放段初始位置 (32 位)、段长、访问权限等信息,此时实际物理地址 = (段索引表示的段初始位置 + IP)。鉴于此种地址访问方式间接访问物理地址,因此其称为 保护模式 。(在我看来,分段机制基本没用,不如使用分页机制,而且现今 intel 采用扁平模式已经略过分段机制了)
bootasm.S
的具体实现,详见源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include <asm.h> # 注:Intel x86 中的段选择器,也称为段选择子。 # 段选择子仍存在一定规则,高 13 位表示其在 gdt 中的索引位,低 3 位为保护信息。 .set PROT_MODE_CSEG, 0x8 # kernel code segment selector .set PROT_MODE_DSEG, 0x10 # kernel data segment selector .set CR0_PE_ON, 0x1 # protected mode enable flag # start address should be 0:7c00, in real mode, the beginning address of the running bootloader .globl start start: # bootloader 所做的第一件事:使能 A20 地址线,如此便可访问 4G 物理地址空间 (仍是兼容以往机器的缘故)。 # 使能 A20 地址线比较复杂,涉及较多底层硬件,简单来说:禁中断、等待 Input Buffer 为空、输入写命令、等待 Input Buffer 为空、输入置位命令。 .code16 # Assemble for 16-bit mode # 禁中断 cli # Disable interrupts cld # String operations increment # Set up the important data segment registers (DS, ES, SS). xorw %ax, %ax # Segment number zero movw %ax, %ds # -> Data Segment movw %ax, %es # -> Extra Segment movw %ax, %ss # -> Stack Segment # Enable A20: seta20.1: inb $0x64, %al # 从64端口读取数据 # Wait for not busy(8042 input buffer empty). testb $0x2, %al # 读到的数据是2则input buffer 为空 jnz seta20.1 movb $0xd1, %al # 0xd1 -> port 0x64 outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port seta20.2: inb $0x64, %al # Wait for not busy(8042 input buffer empty). testb $0x2, %al jnz seta20.2 movb $0xdf, %al # 0xdf -> port 0x60 outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1 # 至此,完成使能 A20 操作。 # 加载 gdtdesc 所表示的值至 gdtr 寄存器。 lgdt gdtdesc # cr0 控制寄存器的部分作用在于控制处理器的工作模式,通过设置相关位,以开启保护模式。 movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0 # 跳转至 protcseg,执行相关指令 (因为目前基于分段机制,因此汇编码需要设置段选择子和段内偏移,注意:实模式和保护模式的段寄存器含义不同)。 ljmp $PROT_MODE_CSEG, $protcseg .code32 # Assemble for 32-bit mode protcseg: # 设置 cs 以外的段寄存器取值为 $PROT_MODE_DSEG,即均设置为数据段选择子的取值。 movw $PROT_MODE_DSEG, %ax # Our data segment selector movw %ax, %ds # -> DS: Data Segment movw %ax, %es # -> ES: Extra Segment movw %ax, %fs # -> FS movw %ax, %gs # -> GS movw %ax, %ss # -> SS: Stack Segment # 如此设置各个段寄存器的值后 (这些值应当都不会再发生变化了),编译器中的逻辑地址 => 隐式转换为同等的线性地址 => 再做它处。 # 设置栈顶指针 esp 和 栈顶指针所示栈帧底部的指针 ebp (esp 指向栈空间底部,此时并不存在栈帧,因此 ebp 没有含义,将其设为 0,以此作为到达栈底的条件判断),并调用 bootmain 函数 (因此栈空间为 0 ~ start/0x7c00)。 # 注意:栈空间的使用是不会覆盖即将执行的其他指令的,原因有二:1. 上述已执行了众多指令,这部分指令空间是可以被直接覆盖的;2. bootmain 中函数调用所需的栈空间并不多,并且加载完 OS 后,便会跳转至 0x100000 处执行。 movl $0x0, %ebp movl $start, %esp # 调用此函数,其为 bootloader 的另一部分,主要完成加载 OS 工作。 call bootmain # If bootmain returns (it shouldn't), loop. spin: jmp spin # Bootstrap GDT .p2align 2 # force 4 byte alignment # 对于 gdt 中的段描述符而言,其含有 64 位,不同部位表示特定的含义,此处使用定义于 <asm.h> 中的宏填充表项 # 对于 gdt 的段描述符组织结构而言,第一项应当为空,其余随便,在此处第二三项分别与代码段和数据段相关。 gdt: # 全 0。 SEG_NULLASM # null seg # 代码段具有读、执行权限,段基址 = 0,段界限 = 4G,即扁平化处理,以隐藏分段机制。 SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel # 数据段具有读、写执行权限,段基址 = 0,段界限 = 4G,即扁平化处理,以隐藏分段机制。 SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel # 对于 gptr 寄存器而言,其高 32 位为 gdt 所在地址 (对应 .long gdt),低 16 位为段界限 (对应 .word 0x17) gdtdesc: # 根据 gdt 具体结构可知,其含有 3 个段,因此段界限为 3 * 8 - 1 = 23 = 0x17 .word 0x17 # sizeof(gdt) - 1 .long gdt # address gdt
对于现有的操作系统而言,Bootloader
的实现代码往往很多,MBR 内部根本无法容纳全部的 Bootloader
。因此,其实现往往是这样的:MBR 内部的 Bootloader
代码用于定位、加载、运行另外一处位置的代码,而该部分代码为 Boot Loader 的实际实现代码。该部分代码通常包含一些通用的文件系统驱动程序,从而保证可以加载 OS。
练习四 此练习用于了解 bootmain.c
代码所做的工作。
bootmain.c
是 bootloader
的另一部分,其主要完成 OS 的加载。
bootmain.c
的具体实现,详见源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 #include <defs.h> #include <x86.h> #include <elf.h> #define SECTSIZE 512 #define ELFHDR ((struct elfhdr *)0x10000) static void waitdisk (void ) { while ((inb(0x1F7 ) & 0xC0 ) != 0x40 ) ; } static void readsect (void *dst, uint32_t secno) { waitdisk(); outb(0x1F2 , 1 ); outb(0x1F3 , secno & 0xFF ); outb(0x1F4 , (secno >> 8 ) & 0xFF ); outb(0x1F5 , (secno >> 16 ) & 0xFF ); outb(0x1F6 , ((secno >> 24 ) & 0xF ) | 0xE0 ); outb(0x1F7 , 0x20 ); waitdisk(); insl(0x1F0 , dst, SECTSIZE / 4 ); } static void readseg (uintptr_t va, uint32_t count, uint32_t offset) { uintptr_t end_va = va + count; va -= offset % SECTSIZE; uint32_t secno = (offset / SECTSIZE) + 1 ; for (; va < end_va; va += SECTSIZE, secno ++) { readsect((void *)va, secno); } } void bootmain (void ) { readseg((uintptr_t )ELFHDR, SECTSIZE * 8 , 0 ); 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 ))(); bad: outw(0x8A00 , 0x8A00 ); outw(0x8A00 , 0x8E00 ); while (1 ); }
练习五 此练习需要了解函数调用过程的底层原理,并基于此实现 print_stackframe()
函数。
函数调用过程时栈的组织结构可简化为下图:
1 2 3 4 5 6 7 8 9 +| 栈底方向 | 高位地址 | ... | | ... | | 参数3 | | 参数2 | | 参数1 | | 返回地址 | | 上一层[ebp] | <-------- [ebp] | 局部变量 | 低位地址
基于寄存器 ebp
取值,我们可以很容易地得到如下内容:当前函数的参数、当前函数的返回地址、父函数的 ebp
取值。基于父函数的 ebp
取值,我们可以递归得到祖先函数的相关信息。
基于上述知识,可以很容易实现 print_stackframe()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void print_stackframe (void ) { uint32_t ebp = read_ebp(); uint32_t eip = read_eip(); for (int i = 0 ; ebp != 0 && i < STACKFRAME_DEPTH; i++) { cprintf("ebp:0x%08x eip:0x%08x " , ebp, ra); uint32_t * args = (uint32_t *) (ebp + 8 ); cprintf("args:0x%08x 0x%08x 0x%08x 0x%08x\n" , args[0 ], args[1 ], args[2 ], args[3 ]); print_debuginfo(eip - 1 ); eip = ((uint32_t *)ebp)[1 ]; ebp = ((uint32_t *)ebp)[0 ]; } }
接下来分析最后一行输出各个数值的意义:
1 2 ebp:0x00007bf8 eip:0x00007d6e args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 <unknow>: -- 0x00007d6d --
根据上述打印栈帧信息的过程,可以推测出打印出的ebp是第一个被调用函数的栈帧的base pointer,eip是在该栈帧对应函数中调用下一个栈帧对应函数的指令的下一条指令的地址,而args是传递给这第一个被调用的函数的参数
为了验证这个想法,不妨在反汇编出来的kernel.asm和bootblock.asm中寻找0x7d6e这个地址,可以发现这个地址上的指令恰好是bootmain函数中调用OS kernel入口函数的指令的下一条,也就是说最后一行打印出来的是bootmain这个函数对应的栈帧信息,其中ebp表示该栈帧的base pointer,eip表示在该函数内调用栈上的下一个函数指令的返回地址,而后面的args则表示传递给bootmain函数的参数,但是由于bootmain函数不需要任何参数,因此这些打印出来的数值并没有太大的意义,后面的unkonw
之后的0x00007d6d
则是bootmain函数内调用OS kernel入口函数的该指令的地址;
关于其他每行输出中各个数值的意义为:ebp, eip等这一行数值意义与上述一致,下一行的输出调试信息,在*.c之后的数字表示当前所在函数进一步调用其他函数的语句在源代码文件中的行号,而后面的+22一类数值表示从该函数汇编代码的入口处到进一步调用其他函数的call指令的最后一个字节的偏移量,以字节为单位;
1 2 ebp:0x00007b38 eip:0x00100a28 args:0x00010094 0x00010094 0x00007b68 0x0010007f kern/debug/kdebug.c :306 : print_stackframe+22
练习六 此练习需要了解中断向量表及中断处理流程,从而实现中断初始化函数 idt_init()
及 trap_dispatch()
中的时钟中断处理。
首先简单介绍 labcodes/lab1/kern/init/init.c
文件内 kern_init()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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(); while (1 ); }
在 labcodes/lab1/kern/mm/pmm.c
中查看 pmm_init()
的具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #include <defs.h> #include <x86.h> #include <mmu.h> #include <memlayout.h> #include <pmm.h> static struct taskstate ts = {0 };static struct segdesc gdt [] = { SEG_NULL, [SEG_KTEXT] = SEG(STA_X | STA_R, 0x0 , 0xFFFFFFFF , DPL_KERNEL), [SEG_KDATA] = SEG(STA_W, 0x0 , 0xFFFFFFFF , DPL_KERNEL), [SEG_UTEXT] = SEG(STA_X | STA_R, 0x0 , 0xFFFFFFFF , DPL_USER), [SEG_UDATA] = SEG(STA_W, 0x0 , 0xFFFFFFFF , DPL_USER), [SEG_TSS] = SEG_NULL, }; static struct pseudodesc gdt_pd = { sizeof (gdt) - 1 , (uint32_t )gdt }; static inline void lgdt (struct pseudodesc *pd) { asm volatile ("lgdt (%0)" :: "r" (pd)) ; asm volatile ("movw %%ax, %%gs" :: "a" (USER_DS)) ; asm volatile ("movw %%ax, %%fs" :: "a" (USER_DS)) ; asm volatile ("movw %%ax, %%es" :: "a" (KERNEL_DS)) ; asm volatile ("movw %%ax, %%ds" :: "a" (KERNEL_DS)) ; asm volatile ("movw %%ax, %%ss" :: "a" (KERNEL_DS)) ; asm volatile ("ljmp %0, $1f\n 1:\n" :: "i" (KERNEL_CS)) ; } uint8_t stack0[1024 ];static void gdt_init (void ) { ts.ts_esp0 = (uint32_t )&stack0 + sizeof (stack0); ts.ts_ss0 = KERNEL_DS; gdt[SEG_TSS] = SEG16(STS_T32A, (uint32_t )&ts, sizeof (ts), DPL_KERNEL); gdt[SEG_TSS].sd_s = 0 ; lgdt(&gdt_pd); ltr(GD_TSS); } void pmm_init (void ) { gdt_init(); }
在 labcodes/lab1/kern/trap/trap.c
中查看 idt_init()
的具体实现 (相较于源代码,此部分已经修改):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 static struct gatedesc idt [256] = {{0 }};static struct pseudodesc idt_pd = { sizeof (idt) - 1 , (uintptr_t )idt }; void idt_init (void ) { extern uintptr_t __vectors[]; for (int i = 0 ; i < sizeof (idt) / sizeof (struct gatedesc); i++) { SETGATE(idt[i], 0 , GD_KTEXT, __vectors[i], DPL_KERNEL) } SETGATE(idt[T_SWITCH_TOK], 1 , GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER); lidt(&idt_pd); }
简单分析代码,我们可以得到中断处理流程:
CPU 接收中断号 i
–> 调用 idt[i] 进行处理,跳转到相应的中断中断处理入口,并在桟中压入相应的 error_code(是否存在与异常号相关) 以及 trap_no
–> 调用 __alltraps()
进行处理(在栈中保存当前被打断程序的 trapframe 结构,设置相应的寄存器)
–> 调用 trap()
进行处理
–> 调用 trap_dispatch()
进行处理
–> 依据中断号,执行相应功能,并返回。
在 trap_dispatch()
内部,针对时钟中断进行如下处理:
1 2 3 4 5 6 7 8 9 case IRQ_OFFSET + IRQ_TIMER: ticks++; if (ticks % TICK_NUM == 0 ) { print_ticks(); } break ;
对于机器而言,部分硬件的中断号是固定的,部分硬件的中断号是动态分配的,此处的时钟中断号便是固定的。
扩展练习 该练习需要理解 intel 机器处理中断的具体流程,从而实现用户空间与内核空间的自主切换。
首先简要说明 intel 的硬件处理机制:
1 2 3 4 5 6 7 8 9 10 11 - 每执行完一条指令,CPU 都会判断是否存在待处理的中断,如果存在,则先行获取中断号。 - 如果中断号所示的中断描述符权限 DPL >= CPL,则表明当前权限高于该中断的允许使用权限,故而可执行此次中断,否则为非法。 - 如果中断号所示的段选择子的段描述符权限 DPL < CPL, 则表明当前权限低于对应中断处理程序所需的权限,因此存在特权级转换。 - 如果存在特权级转换,则 CPU 会获取 TSS 中的 ss0/esp0,并将其设置给相应的寄存器,同时压入当前进程的 ss/esp 至内核栈(这是一个新的栈)。 +++++ - CPU 保存 cs/eip/eflags 等寄存器至内核栈。 +++++ - CPU 保存设置该中断号对应的中断处理程序的 cs/eip 至相应寄存器,从而开始执行中断处理程序。 +++++ - 中断处理程序首先保存 ds/es/fs/gs/各种通用寄存器至内核栈 (如果中断处理程序不会使用它们,则可不保存),然后真正执行中断处理程序。 ----- - 中断处理程序即将完成时,其会自动恢复上述保存的各种寄存器。 ----- - CPU 执行 iret 指令,内核栈中弹出 cs/eip/eflags 等寄存器。 +++++ - 如果存在特权级转换,则 CPU 保存当前 ss/esp 至 TSS,然后从内核栈中弹出 ss/esp。 +++++ # +++++ 表示由硬件自动完成,---- 表示由软件完成。
其次,我们简单看看源代码与上述机制间的联系:
接下来,我们准备实现该练习。
在 labcodes/lab1/kern/init/init.c
中,主函数调用 lab1_switch_test()
以测试功能实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 static void lab1_switch_to_user (void ) { asm volatile ( "sub $0x8, %%esp \n" "int %0 \n" "movl %%ebp, %%esp" : : "i" (T_SWITCH_TOU) ) ;} static void lab1_switch_to_kernel (void ) { asm volatile ( "int %0 \n" "movl %%ebp, %%esp \n" : : "i" (T_SWITCH_TOK) ) ;} static void lab1_switch_test (void ) { lab1_print_cur_status(); cprintf("+++ switch to user mode +++\n" ); lab1_switch_to_user(); lab1_print_cur_status(); cprintf("+++ switch to kernel mode +++\n" ); lab1_switch_to_kernel(); lab1_print_cur_status(); }
在 labcodes/lab1/kern/trap/trap.c
中,实现 T_SWITCH_TOU
和 T_SWITCH_TOK
的中断处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 case T_SWITCH_TOU: tf->tf_cs = USER_CS; tf->tf_ds = USER_DS; tf->tf_es = USER_DS; tf->tf_ss = USER_DS; tf->tf_eflags |= FL_IOPL_MASK; break ; case T_SWITCH_TOK: tf->tf_cs = KERNEL_CS; tf->tf_ds = KERNEL_DS; tf->tf_es = KERNEL_DS; tf->tf_eflags &= ~FL_IOPL_MASK; break ;
特权级 因为 intel 的特权级概念十分重要 (借助于此,可实现部分的保护机制),因此在此简单介绍一番。
对于 intel 而言,特权级分为四个等级,使用 2 bit 进行表示,具体如图所示。实际之中,往往仅使用等级 0 (用于内核使用) 和等级 3 (用于用户使用)。
特权级具体表现在各种段寄存器、段描述符、门描述符之内,使用 CPL/RPL/DPL
进行表示,简要如图所示:
对于代码段/栈段选择子而言,其中 CPL (存在于CS,SS寄存器)表示当前代码/进程所处的特权级;而 RPL(存在于DS,ES,FS,GS寄存器)说明的是进程对段访问的请求权限,是对于段选择子而言的,基于此可以提供更为细致的保护机制(RPL对每个段来说是不固定的,两次访问同一段时的RPL可以不同,RPL会削弱CPL的作用,例如当前CPL=0的进程要访问i一个数据段,段选择子的RPL设为3,这样它对该段就只有特权级3的访问权限);对于各种描述符而言,其中 DPL(存在于段描述符,门描述符) 表示访问此部分内容所需的权限。
现在,基于上述知识,简单说明 intel 提供的保护机制。
如果当前进程欲访问某数据段,则硬件判断 Max(CPL,RPL) <= 数据段的 DPL
,如果满足此等式,则允许执行此指令,否则存在权限冲突,直接报错。
如果当前进程欲访问某系统服务,则硬件判断 CPL <= 门描述符的 DPL
,如果满足此等式,则允许访问系统服务,否则表示不允许。另外,如果满足 CPL > 门描述符的段选择子对应的段描述符的 DPL
,则表明存在特权级转换。