要理解用户模式,首先就需要了解处理器的privilege levels(权限级别),在上面英特尔英文手册的第1989页到第1990页,有一个PRIVILEGE LEVELS章节,该章节里就对权限级别的含义做了详细的解释...

    v0.0.9的项目地址:

    github.com地址:https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)
   
    Dropbox地址:点此进入Dropbox网盘  (该版本位于zenglOX_v0.0.9的文件夹,该文件夹里的zip压缩包为源代码)

    sourceforge地址:https://sourceforge.net/projects/zenglox/files  (该版本位于zenglOX_v0.0.9的文件夹,该文件夹里的zip压缩包为源代码)

    另外再附加一个英特尔英文手册的共享链接地址:
    https://www.dropbox.com/sh/brrv4dnher09s2d/AABa6QRpN-uc6tDpvEd7KtmMa  (里面的intel_manual.pdf有权限级别以及TSS相关的内容)

    要理解用户模式,首先就需要了解处理器的privilege levels(权限级别),在上面英特尔英文手册的第1989页到第1990页,有一个PRIVILEGE LEVELS章节,该章节里就对权限级别的含义做了详细的解释。

    处理器的段保护机制里提供了4种privilege levels(权限级别),可以用0到3的四个数字来表示,数字越大表示权限越小,我们可以将这些权限级别用Protection Rings(保护环)来解释,如下图所示(该图位于英特尔手册的第1990页):


图1

    中间的0环主要用于操作系统内核使用,1环和2环可以用于一些重要的系统服务,最外层的3环则用于普通的用户程式,如果操作系统只使用其中的两个保护环的话,应该使用0环和3环,0环用于内核代码,3环用于普通的用户程式,中间的两环则不用,这样可以减少内核的设计难度,zenglOX在当前的v0.0.9版本里就只用了0环和3环。

    这些privilege levels(权限级别)在实际使用时,其实就是两个二进制位,例如CS和SS段寄存器里的位0和位1就连在一起表示CPL(Current privilege level 当前的权限级别),例如,在zlox_gdt.s文件里,有如下一段代码:

.global _zlox_gdt_flush	# Allows the C code to call _zlox_gdt_flush().
_zlox_gdt_flush:
	movl 4(%esp),%eax	# Get the pointer to the GDT, passed as a parameter.
	lgdt (%eax)			# Load the new GDT pointer
	
	movw $0x10,%ax		# 0x10 is the offset in the GDT to our data segment
	movw %ax,%ds		# Load all data segment selectors
	movw %ax,%es
	movw %ax,%fs
	movw %ax,%gs
	movw %ax,%ss
	ljmp $0x08,$_gdt_ljmp_flush	# 0x08 is the offset to our code segment: Far jump!
_gdt_ljmp_flush:
	ret


    上面代码是之前的版本里的用于刷新GDT表的代码,在这段代码的最后,会通过ljmp $0x08,$_gdt_ljmp_flush的长跳转指令,将CS段寄存器的值设置为0x08,0x08是前面的文章里提到过的段选择子,0x08的位0和位1都是0,说明当前系统的权限级别为0 。在权限级别为0的情况下,处理器可以执行几乎所有的指令。

    而在ring3(权限级别为3)的情况下,很多特殊的指令都不能执行,例如hlt指令,如果在ring 3下执行hlt之类的指令,就会产生GP(General protection 一般性保护异常),而且ring 3下执行的指令也不能对内核部分的数据进行随意修改。

    除了CPL外,还有DPL(Descriptor privilege level 描述符里的权限级别)以及RPL(Requested privilege level 请求的权限级别),DPL主要用于描述符里,例如,英特尔手册的第1931页介绍的Segment Descriptor(段描述符)里的第二个4字节里的位13和位14两个二进制,就表示当前描述符的权限级别。RPL主要用于指令里,例如上面zlox_gdt.s里的ljmp $0x08,$_gdt_ljmp_flush长跳转指令,0x08就是RPL请求的权限级别,当处理器认为当前环境可以将0x08设置到CS段寄存器里时,就会将0x08设置到CS寄存器,从而将0x08由RPL(请求的权限级别)变为正式的CPL(当前的权限级别)。

    在实际运行时,处理器会根据上面提到的CPL,RPL和DPL这几个权限级别进行综合分析,判断是否可以执行某项数据的访问操作,如下图所示(该图位于英特尔手册的第1991页):


图2

    至于具体是如何进行访问控制的,在英特尔手册的第1992页,也给出了一个示例图:
 

图3

    上图显示,当Code Segment  D(代码段D)要访问Data Segment E(数据段E)里的数据时,由于代码段D的CPL为0,所以只要操作指令里段选择子的RPL(请求的权限级别)为2和1,就可以访问到数据段E里的数据,即上图里Segment Sel E1和Segment Sel E2可以访问数据段E,而Segment Sel E3则不能访问数据段E,也就是说,首先CPL(即CS段寄存器里的权限级别)要小于等于数据段描述符里的DPL(数字越小权限越大),其次要求操作指令里的段选择子的RPL也要小于等于DPL,两个权限都判定通过,才可以访问对应的数据段。

    由此可以看出,使用权限级别的机制,既可以有效的保护某些特殊的指令不被普通用户程式直接访问到(后面会提到,可以通过中断的间接方式来访问),又可以确保重要的数据段不会被低权限级别的程式随意修改。

   对权限级别有了大概的了解后,下面来看下,从ring 0切换到ring 3(权限级别为3的用户模式)需要做些什么事呢? 在 http://wiki.osdev.org/Getting_to_Ring_3 该链接对应的文章里,就介绍了切换到ring 3需要先准备好下面的三件事:
  • 在GDT全局描述符表里设置好用户代码段和用户数据段的描述符
  • 安装一个全局的TSS(task-state segment 任务状态段),当CPU在ring 3用户模式下运行时,如果发生中断(包括处理器异常,IRQ外部设备的中断请求,以及软件中断等),CPU就会将TSS里的SS0字段的值,作为SS段寄存器的值,同时将TSS里ESP0字段的值作为esp栈顶指针寄存器的值,所以内核里至少要设置一个TSS,有关TSS的详细信息会在下面进行介绍。
  • 在IDT中断描述符表里设置一个软件中断作为system call(系统调用),这一步虽然是可选的,但是由于系统调用的重要性,所以一般情况下,内核切换到用户模式时,都会设置系统调用对应的软件中断,普通的ring 3用户程式只有通过系统调用才能访问到内核里高级的功能,例如,进程管理,内存管理之类的功能。
    上面第一件事已经在之前的版本里,提前准备好了,在zlox_descriptor_tables.c文件里有如下一段代码:

// Initialises the GDT (global descriptor table)
static ZLOX_VOID zlox_init_gdt()
{
	gdt_ptr.limit = (sizeof(ZLOX_GDT_ENTRY) * ZLOX_GDT_ENTRY_NUMBER) - 1;
	gdt_ptr.base  = (ZLOX_UINT32)gdt_entries;
	
	zlox_gdt_set_gate(0, 0, 0, 0, 0);			// Null segment
	zlox_gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);	// Code segment
	zlox_gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);	// Data segment
	zlox_gdt_set_gate(3, 0, 0xFFFFFFFF, 0xFA, 0xCF);	// User mode code segment
	zlox_gdt_set_gate(4, 0, 0xFFFFFFFF, 0xF2, 0xCF);	// User mode data segment
	zlox_write_tss(5, 0x10, 0x0);

	_zlox_gdt_flush((ZLOX_UINT32)&gdt_ptr);
	_zlox_tss_flush();
}


    上面的zlox_init_gdt函数里,在初始化GDT表时,通过zlox_gdt_set_gate(3, 0, 0xFFFFFFFF, 0xFA, 0xCF);和zlox_gdt_set_gate(4, 0, 0xFFFFFFFF, 0xF2, 0xCF);函数,将GDT表的索引值为3和索引值为4的描述符,分别设置为用户代码段和用户数据段。

    和内核代码段与内核数据段相比,用户代码段和用户数据段只是将描述符里的DPL(描述符的权限级别)设置为3 。

    GDT里准备好所需的描述符后,第二件事就是安装一个全局的TSS,英特尔英文手册的第2072页到第2073页就对TSS做了详细的介绍,TSS是一个数据结构,如下图所示(该图位于手册的第2073页):


图4

    针对上面手册里的TSS结构,在zlox_descriptor_tables.h头文件里就定义了对应的ZLOX_TSS_ENTRY结构体:

// A struct describing a Task State Segment.
struct _ZLOX_TSS_ENTRY
{
    ZLOX_UINT32 prev_tss;   // The previous TSS - if we used hardware task switching this would form a linked list.
    ZLOX_UINT32 esp0;       // The stack pointer to load when we change to kernel mode.
    ZLOX_UINT32 ss0;        // The stack segment to load when we change to kernel mode.
    ZLOX_UINT32 esp1;       // Unused...
    ZLOX_UINT32 ss1;
    ZLOX_UINT32 esp2;  
    ZLOX_UINT32 ss2;   
    ZLOX_UINT32 cr3;   
    ZLOX_UINT32 eip;   
    ZLOX_UINT32 eflags;
    ZLOX_UINT32 eax;
    ZLOX_UINT32 ecx;
    ZLOX_UINT32 edx;
    ZLOX_UINT32 ebx;
    ZLOX_UINT32 esp;
    ZLOX_UINT32 ebp;
    ZLOX_UINT32 esi;
    ZLOX_UINT32 edi;
    ZLOX_UINT32 es;         // The value to load into ES when we change to kernel mode.
    ZLOX_UINT32 cs;         // The value to load into CS when we change to kernel mode.
    ZLOX_UINT32 ss;         // The value to load into SS when we change to kernel mode.
    ZLOX_UINT32 ds;         // The value to load into DS when we change to kernel mode.
    ZLOX_UINT32 fs;         // The value to load into FS when we change to kernel mode.
    ZLOX_UINT32 gs;         // The value to load into GS when we change to kernel mode.
    ZLOX_UINT32 ldt;        // Unused...
    ZLOX_UINT16 trap;
    ZLOX_UINT16 iomap_base;

} __attribute__((packed));


    上面的ZLOX_TSS_ENTRY就是zenglOX里定义的TSS结构体,目前,该结构体里只会用到两个字段:SS0和ESP0,其他的字段都可以留空(清零即可),这两个字段的作用在上面已经讲解过了。

    那么处理器又是如何找到我们自己定义的TSS的呢? 处理器是通过TSS Descriptor(TSS描述符)和Task Register(任务寄存器)来定位到TSS结构体的。

    在英特尔手册的第2074页就有一个章节,专门介绍TSS Descriptor(TSS描述符),TSS描述符只能存储在GDT表里,TSS描述符的结构如下图所示(该图位于手册的第2075页):


图5

    上图里Base Address部分用于存储TSS结构体的基地址(基地址分三部分进行存储:位16到位31,第二个四字节的位0到位7,以及第二个四字节的位24到位31),Segment Limit部分用于存储TSS结构体的尺寸上限(尺寸上限分两部分进行存储:位0到位15,以及第二个四字节的位16到位19),当G(Granularity 粒度)位为0时,Segment Limit的值必须等于或大于67H,即TSS结构体的尺寸大小减一,在zenglOX里就是sizeof(ZLOX_TSS_ENTRY) - 1 。

    TSS描述符是通过zlox_descriptor_tables.c文件的zlox_write_tss函数设置到GDT表里的:

// Initialise our task state segment structure.
static ZLOX_VOID zlox_write_tss(ZLOX_SINT32 num, ZLOX_UINT16 ss0, ZLOX_UINT32 esp0)
{
    // Firstly, let's compute the base and limit of our entry into the GDT.
    ZLOX_UINT32 base = (ZLOX_UINT32) &tss_entry;
    ZLOX_UINT32 limit = sizeof(tss_entry) - 1;
    
    // Now, add our TSS descriptor's address to the GDT.
    zlox_gdt_set_gate(num, base, limit, 0xE9, 0x00);

    // Ensure the descriptor is initially zero.
    zlox_memset((ZLOX_UINT8 *)&tss_entry, 0, sizeof(tss_entry));

    tss_entry.ss0  = ss0;  // Set the kernel stack segment.
    tss_entry.esp0 = esp0; // Set the kernel stack pointer.
    
    // tss_entry.cs   = 0x0b;     
    // tss_entry.ss = tss_entry.ds = tss_entry.es = tss_entry.fs = tss_entry.gs = 0x13;
}


    上面的函数里,tss_entry是一个ZLOX_TSS_ENTRY结构体类型的全局变量,所以代码开头就根据tss_entry的地址和尺寸信息,得到base(描述符里的基地址)和limit(描述符里的尺寸上限)信息,然后通过zlox_gdt_set_gate函数将描述符设置到GDT表里,从上面介绍过的zlox_init_gdt函数的代码里可以看到,我们会调用zlox_write_tss(5, 0x10, 0x0)函数,将TSS描述符设置到GDT表的索引值为5的位置。

    在设置好TSS描述符后,代码里会调用zlox_memset((ZLOX_UINT8 *)&tss_entry, 0, sizeof(tss_entry));函数,先将TSS结构体里的所有字段清零,最后再设置TSS里的SS0与ESP0字段。

    有了TSS描述符,只需再将TSS描述符对应的段选择子设置到Task Register(任务寄存器)里,处理器就可以定位到TSS结构体了,具体的定位方式如下图所示(该图位于手册的第2077页):


图6

    Task Register(任务寄存器)里有Visible Part(可见部分)和Invisible Part(非可见部分),可见部分就是可以通过指令来读取和修改的部分,也就是TSS描述符的段选择子(段选择子的内容在之前的文章里介绍过,可以参考英特尔手册的第1928页,该页里有一个Segment Selectors(段选择子)章节),非可见部分是由处理器来维护的,软件程式无法访问到该部分。

    通过任务寄存器的可见部分里的段选择子,处理器就可以找到GDT表中对应的TSS描述符,然后从TSS描述符里可以提取出TSS结构体的基地址和尺寸上限信息,从而定位到我们自己定义的TSS结构体。

    要将TSS描述符的段选择子加载到Task Register(任务寄存器)里,可以使用LTR汇编指令,在zlox_gdt.s文件里就用到了该指令:

.global _zlox_tss_flush
_zlox_tss_flush:
	movl $0x2B,%eax		# The index is 5th in GDT and RPL is 3
	ltr %ax			# Load 0x2B into the task state register.
	ret


    上面代码里,0x2B是段选择子,0x2B的二进制格式为:00101011(最右侧的1为最低位),位0到位1是11即十进制3,即RPL的值为3(与TSS描述符里的DPL值相同,这样ring 3用户模式就可以正常访问该TSS),位2为0即TSS描述符位于GDT表里(如果为1则位于LDT(局部描述符表)里,当然TSS描述符只能位于GDT里,不能位于LDT里,zenglOX目前也没有LDT表),位3到位5为101即十进制5,说明TSS描述符位于GDT的索引值为5的描述符项里(该项的起始偏移值为5乘以8即40字节,即GDT的起始内存地址加上40就是TSS描述符的起始内存地址)。

    在将0x2B的段选择子设置到AX寄存器后,就可以通过ltr %ax指令将段选择子加载到Task Register(任务寄存器)里了。

    全局TSS准备好后,就可以准备第三件事:System Call(系统调用)。

    系统调用就是一个软件中断,我们通过该软件中断就可以从ring 3用户态切换到ring 0内核态,然后就可以请求内核执行一些用户态无法完成的功能,我们将系统调用的中断号设置为0x80(十进制为128),Linux里也是将0x80作为系统调用的软件中断。

    要添加一个软件中断,首先需要在zlox_interrupt.s里添加对应的中断处理程式:

# zlox_interrupt.s Contains interrupt service routine wrappers.
....................................
....................................
_ZLOX_INT_ISR_NOERRCODE	30
_ZLOX_INT_ISR_NOERRCODE	31
_ZLOX_INT_ISR_NOERRCODE	128
_ZLOX_INT_IRQ	0,	32
_ZLOX_INT_IRQ	1,	33
_ZLOX_INT_IRQ	2,	34
....................................
....................................


    在之前的zenglOX v0.0.3 初始化GDT和IDT的文章里,我们介绍过,由于中断处理例程较多,一个个去写太麻烦,所以采用的是汇编宏的形式,例如上面代码里 _ZLOX_INT_ISR_NOERRCODE    128 就定义了一个0x80(十进制128)的中断处理程式,该宏展开后对应的中断处理函数名为_zlox_isr_128。

    有了中断处理程式,就可以将该程式对应的函数名_zlox_isr_128设置到IDT(中断描述符表)里了,具体的代码位于zlox_descriptor_tables.c文件的zlox_init_idt函数:

// Initialise the interrupt descriptor table.
static ZLOX_VOID zlox_init_idt()
{
....................................
....................................
	zlox_idt_set_gate(30, (ZLOX_UINT32)_zlox_isr_30, 0x08, 0x8E);
	zlox_idt_set_gate(31, (ZLOX_UINT32)_zlox_isr_31, 0x08, 0x8E);
	zlox_idt_set_gate(31, (ZLOX_UINT32)_zlox_isr_31, 0x08, 0x8E);
	zlox_idt_set_gate(128, (ZLOX_UINT32)_zlox_isr_128, 0x08, 0x8E); // syscall
	zlox_idt_set_gate(ZLOX_IRQ0, (ZLOX_UINT32)_zlox_irq_0, 0x08, 0x8E);
	zlox_idt_set_gate(ZLOX_IRQ1, (ZLOX_UINT32)_zlox_irq_1, 0x08, 0x8E);
	zlox_idt_set_gate(ZLOX_IRQ2, (ZLOX_UINT32)_zlox_irq_2, 0x08, 0x8E);
....................................
....................................
}


    所有_zlox_isr_....开头的中断处理函数,最终都会调用进入zlox_isr.c文件的zlox_isr_handler函数里,在该函数里又会根据不同的中断来调用它们各自注册的中断回调函数:

ZLOX_VOID zlox_register_interrupt_callback(ZLOX_UINT8 n, ZLOX_ISR_CALLBACK callback)
{
    interrupt_callbacks[n] = callback;
}

// This gets called from our ASM interrupt handler stub.
ZLOX_VOID zlox_isr_handler(ZLOX_ISR_REGISTERS regs)
{
	// This line is important. When the processor extends the 8-bit interrupt number
	// to a 32bit value, it sign-extends, not zero extends. So if the most significant
	// bit (0x80) is set, regs.int_no will be very large (about 0xffffff80).
	ZLOX_UINT8 int_no = regs.int_no & 0xFF;
	if (interrupt_callbacks[int_no] != 0)
	{
		ZLOX_ISR_CALLBACK callback = interrupt_callbacks[int_no];
		callback(&regs);
	}
	else
	{
		zlox_monitor_write("zenglox recieved unhandled interrupt: ");
		zlox_monitor_write_hex(int_no);
		zlox_monitor_put('\n');
		for(;;)
			;
	}
}


    上面的int_no是中断号,interrupt_callbacks数组里存储着每个中断的回调函数,可以通过zlox_register_interrupt_callback函数来注册中断的回调函数。

    和系统调用相关的中断回调函数定义在zlox_syscall.c文件里:

// zlox_syscall.c Defines the implementation of a system call system.

#include "zlox_syscall.h"
#include "zlox_isr.h"
#include "zlox_monitor.h"

static ZLOX_VOID zlox_syscall_handler(ZLOX_ISR_REGISTERS * regs);

ZLOX_DEFN_SYSCALL1(monitor_write, 0, const char*);
ZLOX_DEFN_SYSCALL1(monitor_write_hex, 1, const char*);
ZLOX_DEFN_SYSCALL1(monitor_write_dec, 2, const char*);

static ZLOX_VOID * syscalls[3] =
{
    &zlox_monitor_write,
    &zlox_monitor_write_hex,
    &zlox_monitor_write_dec,
};

ZLOX_UINT32 num_syscalls = 3;

ZLOX_VOID zlox_initialise_syscalls()
{
    // Register our syscall handler.
    zlox_register_interrupt_callback (0x80, &zlox_syscall_handler);
}

static ZLOX_VOID zlox_syscall_handler(ZLOX_ISR_REGISTERS * regs)
{
    // Firstly, check if the requested syscall number is valid.
    // The syscall number is found in EAX.
    if (regs->eax >= num_syscalls)
        return;

    // Get the required syscall location.
    ZLOX_VOID * location = syscalls[regs->eax];

    // We don't know how many parameters the function wants, so we just
    // push them all onto the stack in the correct order. The function will
    // use all the parameters it wants, and we can pop them all back off afterwards.
    ZLOX_SINT32 ret;
    asm volatile (" \
      push %1; \
      push %2; \
      push %3; \
      push %4; \
      push %5; \
      call *%6; \
      pop %%ebx; \
      pop %%ebx; \
      pop %%ebx; \
      pop %%ebx; \
      pop %%ebx; \
    " : "=a" (ret) : "D" (regs->edi), "S" (regs->esi), "d" (regs->edx), "c" (regs->ecx), "b" (regs->ebx), "0" (location));

    regs->eax = ret;
}


    上面代码中的zlox_initialise_syscalls函数,就会调用zlox_register_interrupt_callback将zlox_syscall_handler注册为系统调用中断的回调函数,该回调函数里,会根据用户程式提供的EAX里的值作为功能号,从syscalls数组里找到对应的内核函数的指针值,然后将用户提供的EBX,ECX,EDX,ESI,EDI寄存器的值作为参数依次压入栈,最后调用对应的内核函数来完成所需的功能。

    另外,在zlox_syscall.h头文件里,定义了和系统调用相关的一些宏:

// zlox_syscall.h Defines the interface for and structures relating to the syscall dispatch system.

#ifndef _ZLOX_SYSCALL_H_
#define _ZLOX_SYSCALL_H_

#include "zlox_common.h"

ZLOX_VOID zlox_initialise_syscalls();

#define ZLOX_DECL_SYSCALL1(fn,p1) ZLOX_SINT32 zlox_syscall_##fn(p1);

#define ZLOX_DEFN_SYSCALL1(fn, num, P1) \
int zlox_syscall_##fn(P1 p1) \
{ \
  ZLOX_SINT32 a; \
  asm volatile("int $0x80" : "=a" (a) : "0" (num), "b" ((ZLOX_SINT32)p1)); \
  return a; \
}

ZLOX_DECL_SYSCALL1(monitor_write, const char*)
ZLOX_DECL_SYSCALL1(monitor_write_hex, const char*)
ZLOX_DECL_SYSCALL1(monitor_write_dec, const char*)

#endif // _ZLOX_SYSCALL_H_


    zlox_syscall.c里就用到了上面的ZLOX_DEFN_SYSCALL1宏来定义几个用户程式可以使用的系统调用函数,例如,ZLOX_DEFN_SYSCALL1(monitor_write, 0, const char*);的宏展开后,得到的代码如下:

int zlox_syscall_monitor_write(const char* p1) 
{ 
  ZLOX_SINT32 a; 
  asm volatile("int $0x80" : "=a" (a) : "0" (0), "b" ((ZLOX_SINT32)p1)); 
  return a; 
}


    在内核进入用户态后,用户态的程式就可以调用上面的zlox_syscall_monitor_write函数将字符串信息显示到屏幕上,从上面的代码可以看到,在内联汇编里其实是通过int $0x80的软件中断进入系统调用的,并且将0赋值给EAX寄存器作为系统调用的功能号,要显示的字符串的指针值则设置到EBX,作为系统调用的第一个参数。系统调用的返回值也设置在EAX里。

    进入ring 3用户模式的三个准备工作都完成了,现在看下如何切换到ring 3用户模式。

    x86处理器并没有什么直接的方式可以切换到用户模式,我们只能通过iret指令将保存在栈里的ring 3模式的CS段选择子弹出到CS段寄存器,来完成ring 3用户模式的切换:

// zlox_task.c Implements the functionality needed to multitask.
...........................................
...........................................
ZLOX_VOID zlox_switch_to_user_mode()
{
	// Set up our kernel stack.
	zlox_set_kernel_stack(current_task->kernel_stack + ZLOX_KERNEL_STACK_SIZE);
	
	// Set up a stack structure for switching to user mode.
	asm volatile(
		"cli\n\t"
		"mov $0x23, %ax\n\t"
		"mov %ax, %ds\n\t"
		"mov %ax, %es\n\t"
		"mov %ax, %fs\n\t"
		"mov %ax, %gs\n\t"
		"mov %esp, %eax\n\t"
		"pushl $0x23\n\t"
		"pushl %eax\n\t"
		"pushf\n\t"
		"pop %eax\n\t"	// Get EFLAGS back into EAX. The only way to read EFLAGS is to pushf then pop.
		"orl $0x200,%eax\n\t"	// Set the IF flag.
		"pushl %eax\n\t"	// Push the new EFLAGS value back onto the stack.
		"pushl $0x1B\n\t"
		"push $1f\n\t"
		"iret\n"
	"1:"
		); 
	  
}


    当iret指令执行时,如果没有发生权限级别的切换,则iret指令只会从栈里弹出三个元素,如果发生了权限级别的切换,例如上面代码,切换到了ring 3级别,则iret指令会从栈里弹出五个元素:


图7

    所以在上面的zlox_switch_to_user_mode函数的内联汇编里,就依次将SS(这里为0x23),ESP,EFLAGS,CS(这里为0x1B),EIP这几个值压入栈,在压入EFLAGS标志寄存器的值时,为了能在切换用户态的同时开启中断,就将压入栈的EFLAGS值再次弹出到EAX,然后通过orl $0x200,%eax指令将EFLAGS里对应的IF(中断)标志设置为1,这样iret执行后,中断就自动开启了,用户态的程式是不能直接调用sti指令的(会产生GP保护异常),所以要采用这种特殊的方式来开启中断。

    另外,在上面的zlox_switch_to_user_mode函数里,开头有一个zlox_set_kernel_stack(current_task->kernel_stack + ZLOX_KERNEL_STACK_SIZE)函数,该函数用于将当前任务的内核栈的栈顶指针设置到TSS结构体的ESP0字段,zlox_set_kernel_stack函数位于zlox_descriptor_tables.c文件里:

ZLOX_VOID zlox_set_kernel_stack(ZLOX_UINT32 stack)
{
    tss_entry.esp0 = stack;
}


    每个任务除了0xE0000000开始的用户栈外,还有一个在内核堆里分配的内核栈:

// zlox_task.c Implements the functionality needed to multitask.
...........................................
...........................................
ZLOX_SINT32 zlox_fork()
{
...........................................
...........................................
	new_task->init_esp = current_task->init_esp;
	new_task->page_directory = directory;
	new_task->kernel_stack = zlox_kmalloc_a(ZLOX_KERNEL_STACK_SIZE);
	new_task->next = 0;
...........................................
...........................................
}


    上面代码里,kernel_stack是任务结构里新增的内核栈字段,用于存储内核栈的所分配到的内存的起始地址。

    有了内核栈后,当多个任务同时在内核里执行系统调用之类的功能时,相互之间的栈结构就不会受到影响,在切换任务之前,只需通过zlox_set_kernel_stack函数将需要切换的任务的内核栈设置到TSS结构体的ESP0字段即可:

// zlox_task.c Implements the functionality needed to multitask.
...........................................
...........................................
ZLOX_VOID zlox_switch_task()
{
...........................................
...........................................

	// Change our kernel stack over.
	zlox_set_kernel_stack(current_task->kernel_stack + ZLOX_KERNEL_STACK_SIZE);

	_zlox_switch_task_do(eip,esp,ebp,current_directory->physicalAddr);
}


    在调用zlox_set_kernel_stack时,之所以要在current_task->kernel_stack的基础上加上ZLOX_KERNEL_STACK_SIZE,是因为栈是反方向(从高地址向低地址)增长的。

    最后,在zlox_kernel_main主入口函数里,在初始化系统调用,并切换到用户模式后,就通过zlox_syscall_monitor_write的系统调用函数将字符串输出显示到屏幕上:

/*zlox_kernel.c Defines the C-code kernel entry point, calls initialisation routines.*/
......................................
......................................
//zenglOX kernel main entry
ZLOX_SINT32 zlox_kernel_main(ZLOX_MULTIBOOT * mboot_ptr, ZLOX_UINT32 initial_stack)
{
......................................
......................................

	// 初始化系统调用
	zlox_initialise_syscalls();

	// 切换到ring 3的用户模式
	zlox_switch_to_user_mode();

	zlox_syscall_monitor_write("hello world!\nwelcome to zenglOX v0.0.9!\ni'm in user world!\n");

	for(;;)
		;

	return 0;
}


    该版本在bochs里的运行情况如下:


图8

    OK,就到这里,休息,休息一下 o(∩_∩)o~~
 
上下篇

下一篇: zenglOX v0.0.10 Keyboard(获取键盘的输入)

上一篇: zenglOX v0.0.8 Multitask(多任务)

相关文章

zenglOX v0.0.8 Multitask(多任务)

zenglOX v0.0.2 VGA输出显示字符串

zenglOX v0.0.1 开发自己的操作系统

zenglOX v0.0.3 初始化GDT和IDT

zenglOX v1.2.0 ISO 9660文件系统

zenglOX v0.0.4 IRQ(中断请求)与PIT(可编程间隔定时器)