1 前言
本文并非介绍当前使用的linux操作系统的用户态和内核态的切换细节,而是介绍ucore这个教学性质的系统,主要是为了说明用户态和内核态本质上到底是什么。
2 初始状态
操作系统在加电启动、完成各个部分的初始化之后,处于内核态,假设当前的栈顶指针寄存器的值为x,指向内存的某个位置,如图2.1所示。
3 内核态切换到用户态
#define T_SWITCH_TOU 120 // user/kernel switch
static void
lab1_switch_to_user(void) {
//LAB1 CHALLENGE 1 : TODO
asm volatile (
"sub $0x8, %%esp \n"
"int %0 \n"
"movl %%ebp, %%esp"
:
: "i"(T_SWITCH_TOU)
);
}
(1)让esp寄存器的值减8,此时esp = x-8 。
(2)执行软中断指令 int 120,调用120号中断处理程序。
中断处理程序依次会往栈中压入以下寄存器值:eflags(32bit)、cs(16bit)、eip(32bit),压入之后,esp = x-20 ,栈变成如下形态。
(3)120号中断处理程序,往栈中压入0和120(中断号),压入后,esp = x-28 。
.globl vector120
vector120:
pushl $0
pushl $120
jmp __alltraps
(4)跳转到 __alltraps 执行。
.globl __alltraps
__alltraps:
# 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
a)用32位对齐(pushl)的方式将一系列数据段寄存器夺入栈中。
b)pushal指令也会压入一系列寄存器,包括:edi、esi、ebp、oesp、ebx、edx、ecx、eax。由于本文要分析的内容与这些寄存器没有关系,因而这些东西统称为pushregs。
c)设置ds、es寄存器,指向内核数据段描述符。
d)将当前的esp值压栈,到此,栈的形态如图所求。
(5)“call trap”调用C语言的tap函数,而trap函数直接调用了trap_dispatch函数,我们假设直接call dispatch。
static void
trap_dispatch(struct trapframe *tf) {
case T_SWITCH_TOU:
if (tf->tf_cs != USER_CS) {
switchk2u = *tf;
switchk2u.tf_cs = USER_CS;
switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS;
switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;
// set eflags, make sure ucore can use io under user mode.
// if CPL > IOPL, then cpu will generate a general protection.
switchk2u.tf_eflags |= FL_IOPL_MASK;
// set temporary stack
// then iret will jump to the right stack
*((uint32_t *)tf - 1) = (uint32_t)&switchk2u;
}
break;
}
a)call指令会把当前指令的下一条指令的地址eip+4压栈, 然后跳转到函数trap_dispatch代码执行。
b)函数的第一第二条代码是把当前ebp压栈,然后设置ebp寄存器为当前esp寄存器的值,此时栈形态如图所示
pushl %ebp
movl %esp, %ebp
c)可以看到:ebp-8就是函数 trap_dispatch第一个参数的值(x-76),而x-76正好指向pushregs这一段数据,由x-76往上一段长为size_of(struct trapframe)的内存块刚好可以构成一个struct trapframe对象,这个对象刚好处于x-76到x这一段内存中!!
(6)trap_dispatch代码
a)首先找一个全局的变量(struct trapframe switchk2u ),在内存的某个地方,把参数tf指向的内容保存到switchk2u。
b)把switchk2u中保存的cs寄存器的值改为用户态代码段的段选择子。
c)把switchk2u中保存的ds、es、ss等寄存器的值改为用户态数据段的段选择子。
d)把switchk2u中,在一开始预留的两个32位内存块的第二块设置为 x - 8 。
e)设置switchk2u中保存的eflags寄存器的值,设置标志位 FL_IOPL_MASK 。
f)设置x-80位置的值为switchk2u对象的地址。
(7)trap_dispatch函数执行结束
leave ; 恢复栈帧指针
ret ; 返回到main
a)恢复栈帧指针:esp = ebp, popl %ebp。
b)返回:popl %eip
(8)返回(4)中的代码执行后续指令
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
a)弹出栈顶值作为写入esp寄存器,此时esp=y
b)弹出pushregs这8个寄存器;
c)弹出gs、fs、es和ds数据段寄存器;
d)让esp = esp+8 ,至此,栈形态如图所示。
e)iret指令弹出eip、cs和eflags;
f)由于iret返回变更了eflags的特权级,因此还会弹出ss和sp。此时esp=x-8 。
4 总结
用户态和内核态的切换本质上是eflags和cs、ds、es和ss等段寄存器值的变换。