操作系统

我现在开始深入操作系统。

我们现在了解了一个程序运行时,是通过ELF文件格式,在内存中进行了布局,而操作系统就是对ELF文件进行解释并加载进内存中。

而一个程序的布局如下图

操作系统通过加载ELF文件,将程序的加载并运行。

如果是静态链接,使用 PLT 直接替换,如果是动态链接则使用的是 GOT 和 PLT 共同来实现,找到程序的入口点(地址)entry_point 存放在IP寄存器中,而代码在代码段寄存器中,段寄存器中存的是偏移量,将IP寄存器中存放的基地址和段寄存描述符存放的偏移量相加得到虚拟地址,而在段寄存器中的段选择子表示GDT表中索引下标,使用虚拟地址 + 索引查询出来对应GDT的值,获取出段描述符,得到线性地址,而段寄存器中分为两个部分,索引叫段选择子,还有一部分是隐形的做缓存使用,第二次访问时,就不需要对GDT表进行再次访问,如果未使用分页,则线性地址即是物理地址,如果使用了分页,再通过PGT表项获得物理地址,就可以开始执行,操作系统的五大步骤,取值,译码,执行,访存,写回。

上述是一个程序是如何被操作系统调用执行的过程。而操作系统是一个管理这个许多程序的一个软件。

现在我们来探讨如果有多个程序需要操作系统进行管理。

假设我们有三个程序,只有一个CPU,内存够用的情况下:

现在操作系统有如下分配方式

现在运行三个程序。

第一种则是挨个执行,第二程序想要执行第一个程序必须得先执行完成。这个叫做单道操作系统。

第二种相对第一种来说,有了进步,每个程序都在一定时间内得到了运行,这个叫做分时操作系统。

分析这两种模式,在分时操作系统中单独看每一个程序,他出现了延时,如果他在单道操作系统中他应该早就执行结束了。人的眼睛在一定频率内是看不出来是否是并行执行的,所以在分时系统中这个切换时间,大了也不好,小了也不行。所以取了一个中间值,10ms。

现在我们引入一些名词

并行:在同一时刻执行的任务。分位伪并行和真并行。

时延:一个任务从从开始执行到结束执行的时间。

吞吐量:给定时间时间范围,执行的任务数。

QPS:以一秒为单位计算查询的任务数。

TPS:以一秒为单位事务完成数。

这样我们就来分析一下分时系统如何实现

我们了解到CPU中有,控制单元,运算单元,存储单元。

如果我们需要实现分时执行,那么CPU需要干什么?

两个程序交替执行,我们就需要保存上一个程序执行到了哪里,执行了什么等信息,但是存储单元只有一个,而且每个程序的代码和数据都是需要隔离的,而CPU不能为每个执行的程序都搞一套存储单元,所以这里两个程序就会有竞争关系,而控制单元和运算单元就是没有这样的竞争关系,因为他们都是原子性的。

那么我们来思考,如何实现两个程序交替执行,并且数据和代码隔离呢?

运算单元:可以复用

控制单元:可以复用

存储单元(寄存器):不可以复用

而在CPU在保护模式下,我们知道栈保存的是程序的私有数据,所以这部分就需要在切换程序的时候进行保存,然后再次调用程序时,我们就需要将数据恢复回来。

所以保存寄存器中的值,再将寄存器中的值切换为另一个程序需要执行的值,这个操作就被称为上下文切换。

而我们知道一个程序在执行时,是通过ELF来排布的,操作系统来加载。而我们在切换程序时,是需要保存上一个程序的寄存器信息的。而ELF里有这个数据排布的信息,但只是静态的数据信息, 而正在执行的程序,需要知道正在执行的元数据信息,我们就成为他是进程。

元数据:描述数据的数据。

下面是 Linux 0.11 对进程的描述

//task即进程的意思,这个结构体把进程能用到的所有信息进行了封装
struct task_struct {
/* these are hardcoded - don't touch */
	long state;	//程序运行的状态/* -1 unrunnable, 0 runnable, >0 stopped */
	long counter; //时间片
	//counter的计算不是单纯的累加,需要下面这个优先级这个参数参与
	long priority;//优先级
	long signal;//信号
	struct sigaction sigaction[32];//信号位图
	long blocked;//阻塞状态	/* bitmap of masked signals */
/* various fields */
	int exit_code;//退出码
	unsigned long start_code,end_code,end_data,brk,start_stack;
	long pid,father,pgrp,session,leader;
	unsigned short uid,euid,suid;
	unsigned short gid,egid,sgid;
	long alarm;//警告
	long utime,stime,cutime,cstime,start_time;//运行时间
	//utime是用户态运行时间 cutime是内核态运行时间
	unsigned short used_math;
/* file system info */
	int tty;	//是否打开了控制台	/* -1 if no tty, so it must be signed */
	unsigned short umask;
	struct m_inode * pwd;
	struct m_inode * root;
	struct m_inode * executable;
	unsigned long close_on_exec;
	struct file * filp[NR_OPEN];//打开了多少个文件
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
	struct desc_struct ldt[3];//ldt包括两个东西,一个是数据段(全局变量静态变量等),另一个是代码段,不过这里面存的都是指针
/* tss for this task */
	struct tss_struct tss;//进程运行过程中CPU需要知道的进程状态标志(段属性、位属性等)
};

我们进入 Linux 0.11 版本的代码:

//main函数 linux引导成功后就从这里开始运行
void main(void)		/* This really IS void, no error here. */
{			
    /* The startup routine assumes (well, ...) this */
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
    //前面这里做的所有事情都是在对内存进行拷贝
 	ROOT_DEV = ORIG_ROOT_DEV;//设置操作系统的根文件
 	drive_info = DRIVE_INFO;//设置操作系统驱动参数
	 //解析setup.s代码后获取系统内存参数
	memory_end = (1<<20) + (EXT_MEM_K<<10);
	//取整4k的内存大小
	memory_end &= 0xfffff000;
	if (memory_end > 16*1024*1024)//控制操作系统的最大内存为16M
		memory_end = 16*1024*1024;
	if (memory_end > 12*1024*1024) 
		buffer_memory_end = 4*1024*1024;//设置高速缓冲区的大小,跟块设备有关,跟设备交互的时候,充当缓冲区,写入到块设备中的数据先放在缓冲区里,只有执行sync时才真正写入;这也是为什么要区分块设备驱动和字符设备驱动;块设备写入需要缓冲区,字符设备不需要是直接写入的
	else if (memory_end > 6*1024*1024)
		buffer_memory_end = 2*1024*1024;
	else
		buffer_memory_end = 1*1024*1024;
	main_memory_start = buffer_memory_end;
#ifdef RAMDISK
	main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
//内存控制器初始化
	mem_init(main_memory_start,memory_end);
	//异常函数初始化
	trap_init();
	//块设备驱动初始化
	blk_dev_init();
	//字符型设备出动初始化
	chr_dev_init();
	//控制台设备初始化
	tty_init();
	//加载定时器驱动
	time_init();
	//进程间调度初始化
	sched_init();
	//缓冲区初始化
	buffer_init(buffer_memory_end);
	//硬盘初始化
	hd_init();
	//软盘初始化
	floppy_init();
	sti();
	//从内核态切换到用户态,上面的初始化都是在内核态运行的
	//内核态无法被抢占,不能在进程间进行切换,运行不会被干扰
	move_to_user_mode();
	if (!fork()) {	//创建0号进程 fork函数就是用来创建进程的函数	/* we count on this going ok */
		//0号进程是所有进程的父进程
		init();
	}
/*
 *   NOTE!!   For any other task 'pause()' would mean we have to get a
 * signal to awaken, but task0 is the sole exception (see 'schedule()')
 * as task 0 gets activated at every idle moment (when no other tasks
 * can run). For task0 'pause()' just means we go check if some other
 * task can run, and if not we return here.
 */
//0号进程永远不会结束,他会在没有其他进程调用的时候调用,只会执行for(;;) pause();
	for(;;) pause();
}

由于CPU提供保护模式使得程序安全运行,权限等级被分为 R0,R1,R2,R3

操作系统中运行在R0 而用户代码这运行在 R3 中

move_to_user_mode(); 当前代码就是将 R0 切换到 R3 中

#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
	"pushl $0x17\n\t" \
	"pushl %%eax\n\t" \
	"pushfl\n\t" \
	"pushl $0x0f\n\t" \
	"pushl $1f\n\t" \
	"iret\n" \
	"1:\tmovl $0x17,%%eax\n\t" \
	"movw %%ax,%%ds\n\t" \
	"movw %%ax,%%es\n\t" \
	"movw %%ax,%%fs\n\t" \
	"movw %%ax,%%gs" \
	:::"ax")

内联汇编:

__asm__ ("汇编代码"

:"输入":"输出":"会改变哪一个寄存器")

然后调用 fork 出一个进程。

.align 2
_sys_fork://fork的系统调用
	call _find_empty_process//调用这个函数
	testl %eax,%eax
	js 1f
	push %gs
	pushl %esi
	pushl %edi
	pushl %ebp
	pushl %eax
	call _copy_process//
	addl $20,%esp
1:	ret

操作系统需要对进程进行管理,在0.11版使用的是一个数组,找到一个空的进程位置

//大概意思就是一直循环重复找,直到找到一个空的位置
int find_empty_process(void)
{
	int i;

	repeat:
		if ((++last_pid)<0) last_pid=1;
		for(i=0 ; i<NR_TASKS ; i++)
			if (task[i] && task[i]->pid == last_pid) goto repeat;
	for(i=1 ; i<NR_TASKS ; i++)
		if (!task[i])
			return i;
	return -EAGAIN;//达到64的最大值后,返回错误码
}

然后创建一个进程出来

// 所谓进程创建就是对0号进程或者当前进程的复制
// 就是结构体的复制 把task[0]对应的task_struct 复制一份
//除此之外还要对栈堆拷贝 当进程做创建的时候要复制原有的栈堆
// nr就是刚刚找到的空槽的pid
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
		long ebx,long ecx,long edx,
		long fs,long es,long ds,
		long eip,long cs,long eflags,long esp,long ss)
{
	struct task_struct *p;
	int i;
	struct file *f;
	//其实就是malloc分配内存
	p = (struct task_struct *) get_free_page();//在内存分配一个空白页,让指针指向它
	if (!p)
		return -EAGAIN;//如果分配失败就是返回错误
	task[nr] = p;//把这个指针放入进程的链表当中
	*p = *current;//把当前进程赋给p,也就是拷贝一份	/* NOTE! this doesn't copy the supervisor stack */
	//后面全是对这个结构体进行赋值相当于初始化赋值
	p->state = TASK_UNINTERRUPTIBLE;
	p->pid = last_pid;
	p->father = current->pid;
	p->counter = p->priority;
	p->signal = 0;
	p->alarm = 0;
	p->leader = 0;		/* process leadership doesn't inherit */
	p->utime = p->stime = 0;
	p->cutime = p->cstime = 0;
	p->start_time = jiffies;//当前的时间
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;
	p->tss.ss0 = 0x10;
	p->tss.eip = eip;
	p->tss.eflags = eflags;
	p->tss.eax = 0;//把寄存器的参数添加进来
	p->tss.ecx = ecx;
	p->tss.edx = edx;
	p->tss.ebx = ebx;
	p->tss.esp = esp;
	p->tss.ebp = ebp;
	p->tss.esi = esi;
	p->tss.edi = edi;
	p->tss.es = es & 0xffff;
	p->tss.cs = cs & 0xffff;
	p->tss.ss = ss & 0xffff;
	p->tss.ds = ds & 0xffff;
	p->tss.fs = fs & 0xffff;
	p->tss.gs = gs & 0xffff;
	p->tss.ldt = _LDT(nr);
	p->tss.trace_bitmap = 0x80000000;
	if (last_task_used_math == current)//如果使用了就设置协处理器
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
	if (copy_mem(nr,p)) {//老进程向新进程代码段和数据段进行拷贝
		task[nr] = NULL;//如果失败了
		free_page((long) p);//就释放当前页
		return -EAGAIN;
	}
	for (i=0; i<NR_OPEN;i++)//
		if (f=p->filp[i])//父进程打开过文件
			f->f_count++;//就会打开文件的计数+1,说明会继承这个属性
	if (current->pwd)//跟上面一样
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
	set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->state = TASK_RUNNING;//把状态设定为运行状态	/* do this last, just in case */
	return last_pid;//返回新创建进程的id号
}

当进程创建出来,子进程和父进程就隔离开来,子进程和父进程使用的相同的代码,但是数据是不相同的,所以IP寄存器中代码是相同的,但是由于虚拟地址的存在,真实的代码的物理地址也是需要区分开,所以SS代码段寄存器就不相同,则在GDT中找到的表项就不一样,从中取到的物理地址就不一样,从而GTR表在创建进程的时候也是需要进行进行新增表项的,而私有的数据需要隔离,所以就需要将子进程所需要的修改的值进行重新赋值,并将子进程的返回值置位0,而父进程则返回last_pid,当函数执行完成的时候,因为父进程和子进程都执行的同一块代码,但是因为特权级不同,而创建了两个 task_struct , 回到 fork 时函数就分道扬镳,R0特权级 运行自己 task_struct 的代码,R3特权级 运行 task_struct 自己的代码。

我们了解操作系统运行在R0,应用程序运行R3中,如果R3想要运行R0的代码,这保护程序的有序执行而产生的保护模式的存在,这势必在执行时就需要进行特权级切换,R0 -> R3 R3 -> R0。

而我们知道CPU执行分为 取值,译码,执行,访存,写回 五个阶段,这五个阶段被称为一个指令流,而每个指令都是原子性的,而CPU在指令流后会增加一个高电平为检测,成为中断向量,作用就是改变指令流的执行,而每次中断被成为中断延迟。

首先在CPU中有各种类型的阵脚,每个阵脚的作用是不相同的,而中断控制器则检测到有中断产生就像对应的阵脚修改为高电平,并将数据使用数据总线来传输,而CPU在执行完指令流之后就会去查看是否有阵脚被改变,如果有阵脚被改变则就去从数据总线中获取数据,而CPU就知道了要去切换指令流的,这个时候就要将需要执行的指令流的首地址存储起来,以便CPU可以去加载执行,就需要一个寄存器来存储,而内容分为两块,一块是CPU自己的阵脚对应的编号,一块是由用户(操作系统)自定义的编号,这部分指令流都由操作系统来负责加载到寄存器中。

至此,我们来阅读Intel 开发手册:

过程调用,中断,异常处理。

机翻:

处理器支持以下两种不同的过程调用方式:

  1. CALL和RET指令。
  2. ENTER和LEAVE指令,以及CALL和RET

这两种过程调用机制都使用过程堆栈(通常简称为“堆栈”)来保存调用过程的状态,将参数传递给被调用过程,并存储当前正在执行的过程。

处理器处理中断和异常的类似于CALL和RET指令执行。

支持控制流强制技术(CET)的处理器支持称为“阴影堆栈”。当启用阴影堆栈时,CALL指令会另外保存调用的状态阴影堆栈上的过程;如果状态为堆栈和阴影堆栈匹配。

机翻:

堆栈(见图6-1)是一个连续的内存位置数组。它包含在一个段中,并由SS寄存器中的段选择器标识。使用平面内存模型时,堆栈可以位于程序线性地址空间的任何位置。堆栈最长可达4GB,这是一个段的最大大小。

指令流使用PUSH指令放置在堆栈上,并使用POP指令从堆栈中移除。

当指令被推送到堆栈上时,处理器会减少ESP寄存器,然后将项目写入新的堆栈顶部。当一个指令从堆栈中弹出时,处理器从堆栈顶部读取该指令,然后增加ESP寄存器。以这种方式,当项目被压入堆栈时,堆栈在内存中向下增长(朝向较小的地址),当指令从堆栈中弹出时,堆栈向上收缩(朝向较大的地址)。

一个程序或操作系统/执行程序可以建立许多堆栈。例如,在多任务系统中,每个任务都可以有自己的堆栈。系统中的堆栈数受最大段数和可用物理内存的限制。

当系统设置多个堆栈时,一次只能使用当前堆栈中的一个堆栈。当前堆栈是SS寄存器引用的段中包含的堆栈。

对于所有堆栈操作,处理器自动引用SS寄存器。例如,当ESP寄存器用作内存地址时,它会自动指向当前堆栈中的地址。此外,CALL、RET、PUSH、POP、ENTER和LEAVE指令都在当前堆栈上执行操作。

机翻:

CALL 指令允许将控制转移到当前代码段(近调用)和不同代码段(远调用)中的过程。

近调用通常提供对当前正在运行的程序或任务中的本地过程的访问。

远调用通常用于访问操作系统的过程或不同任务中的过程。

Near Call 与 Far Call , 改变什么保存是什么。

Near Call:保存EIP。Far Call : 保存 CS,EIP

执行近距离调用时,处理器执行以下操作(见图6-2):

  1. 将EIP寄存器的当前值推送到堆栈上。
  1. 在EIP寄存器中加载被调用过程的偏移量。
  2. 开始执行被调用的过程。

执行接近返回时,处理器执行以下操作:

  1. 将栈顶值(返回指令指针)弹出到EIP寄存器。
  2. 如果RET指令有一个可选的n参数,则按字节数递增堆栈指针用n个操作数指定以从堆栈中释放参数。
  3. 继续执行调用过程。

执行远调用时,处理器执行以下操作(见图6-2):

  1. 将CS寄存器的当前值推送到堆栈上。
  2. 将EIP寄存器的当前值推送到堆栈上。
  3. 加载CS寄存器中包含被调用过程的段的段选择器。
  4. 加载EIP寄存器中被调用过程的偏移量。
  5. 开始执行被调用的过程。

执行远距返回时,处理器执行以下操作:

  1. 将栈顶值(返回指令指针)弹出到EIP寄存器。
  2. 将栈顶值(返回到的代码段的段选择器)弹出到CS寄存器。
  3. 如果RET指令有一个可选的n参数,则按字节数递增堆栈指针用n个操作数指定以从堆栈中释放参数。
  4. 继续执行调用过程。

传参的方式:寄存器,堆栈

处理器不会在过程调用时保存通用寄存器的状态。调用过程可以因此,通过将参数复制到这些寄存器中的任何一个,最多可以向被调用过程传递六个参数(ESP和EBP寄存器除外)。调用的过程也可以通过参数通过通用寄存器返回到调用过程。

要向调用的过程传递大量参数,可以将参数放在堆栈中的调用过程的堆栈帧。这里,使用堆栈帧基指针(在EBP寄存器中)来制作一个框架边界以便于访问参数。堆栈还可用于将参数从被调用过程传递回调用过程。

在本例中,最高特权级别0(位于图的中心)用于包含系统中最关键的代码模块,通常是操作系统的内核。外环(特权逐渐降低)用于包含非关键软件代码模块的段。低权限段中的代码模块只能通过以下方式访问在高权限段中运行的模块一种被称为闸门的严密控制和保护接口。尝试访问更高权限段如果没有通过保护门,并且没有足够的访问权限,则会导致一般保护要生成的异常(#GP)。

如果操作系统或执行程序使用此多级保护机制,则对位于比调用过程更高的特权保护级别以与远调用类似的方式处理(参见第节6.4.2,“远呼叫和RET操作”)。差异如下:

  1. CALL指令中提供的段选择器引用称为调用门的特殊数据结构描述符。除其他外,调用门描述符提供以下内容:
    1. 访问权限信息
    2. 被调用过程的代码段的段选择器
    3. 代码段中的偏移量(即被调用过程的指令指针)
  1. 处理器切换到新堆栈以执行调用的过程。每个特权级别都有自己的堆栈。

特权级别3堆栈的段选择器和堆栈指针存储在SS和ESP寄存器中,当调用更高特权级别时,将自动保存。段选择器

特权级别2、1和0堆栈的堆栈指针存储在称为任务状态的系统段中段(TSS)。

堆栈切换期间调用门和TSS的使用对调用过程是透明的,除非引发一般保护异常。

当调用特权更高的保护级别时,处理器执行以下操作(见图6-5):

  1. 执行访问权限检查(特权检查)。
  1. 临时保存(内部)SS、ESP、CS和EIP寄存器的当前内容。
  2. 加载新堆栈的段选择器和堆栈指针(即,特权级别的堆栈调用)从TSS进入SS和ESP寄存器,并切换到新堆栈。
  3. 将临时保存的调用过程堆栈的SS和ESP值推送到新堆栈上。
  4. 将参数从调用过程的堆栈复制到新堆栈。调用门描述符中的值确定要复制到新堆栈的参数数量。
  5. 将调用过程临时保存的CS和EIP值推送到新堆栈。
  6. 将新代码段的段选择器和新指令指针从调用门加载到CS和EIP寄存器。
  7. 以新的特权级别开始执行被调用过程。

从特权过程执行返回时,处理器执行以下操作:

  1. 执行访问权限检查(特权检查)。
  2. 将CS和EIP寄存器恢复到调用前的值。
  3. 如果RET指令有可选的n参数,则按字节数递增堆栈指针用n个操作数指定以从堆栈中释放参数。如果调用门描述符指定了或者将多个参数从一个堆栈复制到另一个堆栈,则必须使用RET n指令来释放两个堆栈中的参数。这里,n操作数指定参数。返回时,处理器会为每个堆栈增加ESP n(有效删除)这些参数来自堆栈。
  4. 将SS和ESP寄存器恢复到调用前的值,这会导致切换回调用过程。
  5. 如果RET指令有可选的n参数,则按字节数递增堆栈指针用n个操作数指定以从堆栈中释放参数(请参阅步骤3中的说明)。
  6. 继续执行调用过程。

在保护模式下,Intel 64和IA-32体系结构提供了一种保护机制,在段级别和页面级别。这种保护机制提供了限制访问某些基于特权级别的段或页(段有四个特权级别,页有两个特权级别)。

例如,可以通过将关键操作系统代码和数据置于更高权限的位置来保护它们段,而不是包含应用程序代码的段。然后,处理器的保护机制将阻止应用程序代码以受控、定义的方式以外的任何方式访问操作系统代码和数据。段和页保护可用于软件开发的所有阶段,以帮助本地化和检测设计问题和错误。它还可以被纳入最终产品中,以提高操作的稳健性系统、实用程序软件和应用程序软件。

使用保护机制时,会检查每个内存引用,以验证它是否满足各种保护检查。在开始记忆循环之前进行所有检查;任何违反都会导致异常。因为检查是与地址转换并行执行的,所以没有性能损失。

  1. 限制检查。
  2. 类型检查。
  3. 特权级别检查。
  4. 可寻址域的限制。
  5. 程序入口点的限制。
  6. 指令集限制。

所有违反保护的行为都会导致生成异常。参见第6章“中断和异常处理”以了解异常机制的说明。本章描述了保护机制和导致例外的违规行为。

在寄存器CR0中设置PE标志会导致处理器切换到保护模式,从而启用段保护机制。一旦进入保护模式,就没有控制位来打开或关闭保护机制在仍处于保护模式时禁用,方法是将权限级别0(最高权限)分配给所有段选择器和段描述符。此操作禁用段之间的权限级别保护屏障,但其他仍在进行限制检查和类型检查等保护检查。启用分页时,页面级保护会自动启用(通过在寄存器CR0中设置PG标志)。在这里同样,在启用分页后,没有用于关闭页面级保护的模式位。但是,可以通过执行以下操作禁用页面级保护:

  1. 清除控制寄存器CR0中的WP标志。
  1. 为每个页面目录和页面表条目设置读/写(R/W)和用户/主管(U/S)标志。此操作使每个页面都成为可写的用户页面,这实际上禁用了页面级保护。

处理器的保护机制使用系统数据结构中的以下字段和标志来控制访问段和页面:

描述符类型(S)标志:(段描述符的第二个双字中的第12位)确定段描述符用于系统段、代码段或数据段。

类型字段:(段描述符第二个双字中的位8到11。)确定代码、数据或系统段。

限制字段:(段描述符。)确定段的大小以及G标志和E标志(用于数据段)。

G标志:(段描述符第二个双字中的第23位)确定段的大小带有限制字段和E标志(用于数据段)。

E标志:(数据段描述符第二个双字中的第10位。)确定段的大小,以及限制字段和G标志。

描述符特权级别(DPL)字段:(段描述符第二个双字中的位13和14。)确定段的权限级别。

请求的特权级别(RPL)字段:(任何段选择器的位0和1。)指定请求的段选择器的权限级别。

当前特权级别(CPL)字段:(CS段寄存器的位0和1。)表示特权级别当前正在执行的程序或过程。术语当前特权级别(CPL)是指此字段。

用户/主管(U/S)标志:(分页结构条目的第2位)确定页面类型:用户或监督人

读/写(R/W)标志:(分页结构项的第1位)确定允许对页面:只读或读/写。

执行禁用(XD)标志:(某些分页结构项的第63位。)确定访问类型允许页面:可执行或不可执行。

段描述符的限制字段防止程序或过程寻址外部的内存位置段。限制的有效值取决于G(粒度)标志的设置(见图5-1)。对于数据段,限制还取决于E(扩展方向)标志和B(默认堆栈指针大小和/或上限)标志。当段描述符用于数据段类型时,E标志是类型字段中的一个位。当G标志清除时(字节粒度),有效限制是段中20位限制字段的值描述符。这里的限制范围是从0到FFFFF H(1 MB)。设置G标志(4-KB页面粒度)时,处理器将限制字段中的值缩放212倍(4 KB)。在这种情况下,有效极限范围从FFFH(4 KB)到FFFFFF H(4 GB)。注意,当使用缩放时(设置G标志)未根据限制检查段偏移量(地址);例如,请注意,如果段限制为0,偏移量0到FFFH仍然有效。

对于除向下扩展数据段以外的所有类型的数据段,有效限制是允许的最后一个地址要在段中访问,它比段的大小(以字节为单位)小一。处理器导致任何时候尝试访问段中的以下地址:

•偏移量大于有效限制的字节

•偏移量大于(有效限制–1)的字

•偏移量大于(有效限制–3)的双字

•偏移量大于(有效极限–7)的四字

•偏移量大于(有效限制–15)的双四字

当有效限制为FFFFFF H(4 GB)时,这些访问可能会也可能不会导致所指示的异常。行为是特定于实现的,不同的执行可能会有所不同。对于向下展开的数据段,段限制具有相同的功能,但解释不同。在这里有效限制指定段内不允许访问的最后一个地址;有效范围如果设置了B标志,则偏移量从(有效限值+1)到FFFFFF H,如果设置了旗子是清晰的。当段限制为0时,向下展开段具有最大大小。限制检查捕获编程错误,例如失控代码、失控下标和无效指针计算。这些错误会在发生时检测到,因此更容易确定原因。无限制检查,这些错误可能会覆盖另一段中的代码或数据。除了检查段限制外,处理器还检查描述符表限制。GDTR和IDTR寄存器包含16位限制值,处理器使用这些限制值阻止程序选择段描述符在各自的描述符表之外。LDTR和任务寄存器包含32位段限制值(从当前LDT和TSS的段描述符)。处理器使用这些段限制来防止访问超出当前LDT和TSS的界限。参见第3.5.1节“段描述符表”有关GDT和LDT限制字段的更多信息;有关更多信息,请参阅第6.10节“中断描述符表(IDT)”IDT限制字段信息;有关TSS段的更多信息,请参阅第7.2.4节“任务寄存器”限制字段。


段描述符在两个位置包含类型信息:

  1. S(描述符类型)标志。
  1. 类型字段。

处理器使用此信息检测导致试图使用段或以不正确或意外的方式进入。

S标志指示描述符是系统类型还是代码或数据类型。类型字段提供4个额外的位,用于定义各种类型的代码、数据和系统描述符。表3-1显示了代码和数据描述符的类型字段;表3-2显示了系统描述符字段的编码。处理器在操作段选择器和段时,会在不同时间检查类型信息描述符。以下列表给出了执行类型检查的典型操作的示例(此列表不是详尽):

  1. 当段选择器加载到段寄存器时-某些段寄存器只能包含某些描述符类型,例如:
    1. CS寄存器只能与代码段选择器一起加载。
    2. 不可读的代码段或系统段的段选择器无法加载到数据段寄存器(DS、ES、FS和GS)。
    3. 只有可写数据段的段选择器才能加载到SS寄存器中。
  1. 当段选择器加载到LDTR或任务寄存器时,例如:
    1. LDTR只能加载LDT选择器。
    2. 任务寄存器只能与TSS的段选择器一起加载。
  1. 当指令访问描述符已加载到段寄存器中的段时某些段只能由指令以某些预定义的方式使用,例如:
    1. 任何指令都不能写入可执行段。
    2. 如果数据段不可写,则任何指令都不能写入数据段。
    3. 除非设置了可读标志,否则任何指令都不能读取可执行段。
  1. 当指令操作数包含段选择器时-某些指令可以访问段或仅特定类型的门,例如:
    1. far CALL或far JMP指令只能访问一致代码段的段描述符,不一致代码段、调用门、任务门或TSS。
    2. LLDT指令必须引用LDT的段描述符。
    3. LTR指令必须引用TSS的段描述符。
    4. LAR指令必须引用LDT、TSS、调用门、任务门、代码的段或门描述符段或数据段。
    5. LSL指令必须引用LDT、TSS、代码段或数据段的段描述符。
    6. IDT条目必须是中断门、陷阱门或任务门。
  1. 在某些内部操作期间-例如:
    1. 对于远调用或远跳转(使用远call或远JMP指令执行),处理器确定要执行的控制传输类型(调用或跳转到另一个代码段,调用或跳越门,或任务开关),方法是检查段(或门)选择器在CALL或JMP指令中作为操作数给出。如果描述符类型为代码段或调用门,指示对另一代码段的调用或跳转;如果描述符类型为TSS或任务门,指示任务开关。
    2. 在调用或跳转通过调用门时(或在中断或异常处理程序调用通过陷阱或中断门),处理器自动检查gate用于代码段。
    3. 在通过任务门调用或跳转到新任务时(或在中断或异常处理程序调用新任务时任务),处理器自动检查所指向的段描述符任务门旁边是TSS。
    4. 当通过直接引用TSS调用或跳转到新任务时,处理器会自动检查CALL或JMP指令指向的段描述符用于TSS。
    5. 从嵌套任务(由IRET指令启动)返回时,处理器检查前一个任务当前TSS中的链接字段指向TSS。

为了提供对具有不同特权级别的代码段的受控访问,处理器提供了一组特殊的描述符称为门描述符。 有四种门描述符:

• 呼叫门

• 陷阱门

• 中断门

• 任务门

调用门有助于在不同特权级别之间进行程序控制的受控转移。 他们通常是仅在使用特权级保护机制的操作系统或执行者中使用。 呼叫门也是用于在 16 位和 32 位代码段之间传输程序控制,如第 21.4 节所述,“在混合大小的代码段之间转移控制。”

图 5-8 显示了调用门描述符的格式。 调用门描述符可以驻留在 GDT 或 LDT 中,但不在中断描述符表(IDT)中。 它执行六个功能:

• 它指定要访问的代码段。

• 它定义了指定代码段中过程的入口点。

• 它指定尝试访问过程的调用者所需的特权级别。

• 如果发生堆栈切换,它指定要在堆栈之间复制的可选参数的数量。

• 它定义要压入目标堆栈的值的大小:16 位门强制 16 位压入和 32 位门强制 32 位推送。

• 它指定调用门描述符是否有效。

通过调用门的程序。P标志指示调用门描述符是否有效。(门所指向的代码段的存在由代码段描述器中的P标志表示。)参数计数字段表示要从调用过程堆栈复制到新堆栈的参数数量(如果堆栈发生切换(见第5.8.5节“堆栈切换”)。参数count指定16位调用门的字数和32位调用门中的双字数。

请注意,门描述符中的P标志通常总是设置为1。如果设置为0,则当程序尝试访问该描述符时,会生成不存在(#NP)异常。操作系统可以将P标志用于特殊目的。例如,它可以用来跟踪闸门的使用次数。这里,P标志最初是设置为0会导致不存在异常处理程序的陷阱。然后,异常处理程序递增一个计数器,并将P标志设置为1,以便从处理程序返回时,门描述符将有效。

要访问调用门,需要提供指向门的远指针作为 CALL 或 JMP 指令中的目标操作数。 来自该指针的段选择器标识调用门(见图 5-10); 指针的偏移量是必需的,但处理器不使用或检查。 (偏移量可以设置为任何值。)

当处理器访问调用门时,它使用调用门中的段选择器来定位目标代码段的段描述符。 (这个段描述符可以在 GDT 或 LDT 中。)然后它将来自代码段描述符的基地址与来自调用门的偏移量组合起来,形成

代码段中过程入口点的线性地址。

如图 5-11 所示,使用四种不同的特权级别来检查程序控制传输的有效性通过呼叫门:

• CPL(当前特权级别)。

• 调用门选择器的RPL(请求者特权级别)。

• 调用门描述符的 DPL(描述符特权级别)。

• 目标代码段的段描述符的DPL。

还检查目标代码段的段描述符中的 C 标志(符合)。

每当使用调用门将程序控制转移到特权更高的不合格代码段时(即,当不合格目标代码段的 DPL 小于 CPL 时),处理器会自动切换到目标代码段特权的堆栈等级。执行此堆栈切换是为了防止更多特权过程由于堆栈空间不足而崩溃。 它还可以防止特权较低的过程通过共享堆栈干扰(意外或有意)更多特权的过程。

每个任务必须定义最多 4 个堆栈:一个用于应用程序代码(以特权级别 3 运行),一个用于使用的每个特权级别 2、1 和 0。 (如果只使用两个特权级别 [3 和 0],那么只需要定义两个堆栈。)这些堆栈中的每一个都位于一个单独的段中,并通过段选择器和堆栈段的偏移量(堆栈指针)。

特权级 3 堆栈的段选择器和堆栈指针分别位于 SS 和 ESP 寄存器中,当特权级 3 代码正在执行时,并在发生堆栈切换时自动存储在相关过程的堆栈中。

指向特权级别 0、1 和 2 堆栈的指针存储在当前运行任务的 TSS 中(见图 7-2)。 这些指针中的每一个都包含一个段选择器和一个堆栈指针(加载到 ESP 寄存器中)。 这些初始指针是严格的只读值。 任务运行时,处理器不会更改它们。 它们仅用于在调用更多特权级别(数值较低的特权级别)时创建新堆栈。 当从被调用的过程返回时,这些堆栈将被释放。 下次调用该过程时,将使用初始堆栈指针创建一个新堆栈。 (TSS 没有为特权级别 3 指定堆栈,因为处理器不允许将程序控制从运行在 CPL 0、1 或 2 的过程转移到运行在 CPL 3 的过程,除了 返回时。)

操作系统负责为要使用的所有特权级别创建堆栈和堆栈段描述符,并将这些堆栈的初始指针加载到 TSS 中。每个堆栈都必须是可读写的(在其段描述符的类型字段中指定)并且必须包含足够的空间(在限制字段中指定)以容纳以下项目:

• 调用过程的SS、ESP、CS 和EIP 寄存器的内容。

• 被调用过程所需的参数和临时变量。

• EFLAGS 寄存器和错误代码,当对异常或中断处理程序进行隐式调用时。

堆栈需要足够的空间来包含这些项目的许多帧,因为过程经常调用其他过程,操作系统可能支持多个中断的嵌套。每个堆栈都应该很大足以在其特权级别允许最坏的嵌套情况。

(如果操作系统不使用处理器的多任务机制,它仍然必须为这个与堆栈相关的目的创建至少一个 TSS。)

当通过调用门的过程调用导致特权级别发生变化时,处理器执行以下步骤来切换堆栈并以新的特权级别开始执行被调用的过程:

  1. 使用目标代码段的DPL(新CPL)从TSS中选择指向新堆栈的指针(段选择器和堆栈指针)。
  2. 从当前TSS读取要切换到的堆栈的段选择器和堆栈指针。读取堆栈段选择器、堆栈指针或堆栈段描述符时检测到的任何限制冲突都会导致生成无效的TSS(#TS)异常。
  3. 检查堆栈段描述符的权限和类型是否正确,如果检测到冲突,将生成无效的TSS(#TS)异常。
  4. 临时保存SS和ESP寄存器的当前值。
  5. 在 SS 和 ESP 寄存器中加载新堆栈的段选择器和堆栈指针。
  6. 将临时保存的 SS 和 ESP 寄存器(用于调用过程)的值推送到新堆栈中(见图 5-13)。
  7. 从调用中复制调用门的参数计数字段中指定的参数个数过程的堆栈到新堆栈。如果计数为 0,则不复制任何参数。
  8. 将返回指令指针(CS 和 EIP 寄存器的当前内容)压入新堆栈。
  9. 将新代码段的段选择器和新指令指针从调用门加载到CS 和 EIP 寄存器,并开始执行被调用的过程。

请参阅 IA-32 Intel 体系结构中第 3 章指令集参考中对 CALL 指令的描述软件开发人员手册,第 2 卷,详细说明权限级别检查和其他保护检查处理器是否通过调用门执行远调用。

本章介绍IA-32体系结构的任务管理功能。这些设施仅在处理器以保护模式运行时可用。

本章重点介绍32位任务和32位TSS结构。有关16位任务和16位TSS结构的信息,请参阅第7.6节“16位任务状态段(TSS)”。有关64位模式下任务管理的特定信息,请参见第7.7节“64位模式中的任务管理”。

任务是处理器可以分派、执行和挂起的工作单元。它可以用于执行程序、任务或进程、操作系统服务实用程序、中断或异常处理程序、内核或执行实用程序。

IA-32体系结构提供了一种机制,用于保存任务的状态、分派要执行的任务以及从一个任务切换到另一个任务。当在保护模式下操作时,所有处理器的执行都是从任务中进行的。即使是简单的系统也必须定义至少一个任务。更复杂的系统可以使用处理器的任务管理功能来支持多任务应用程序。

任务由两部分组成:任务执行空间和任务状态段(TSS)。任务执行空间由一个代码段、一个堆栈段和一个或多个数据段组成(见图7-1)。如果操作系统或执行程序使用处理器的特权级别保护机制,则任务执行空间还为每个特权级别提供单独的堆栈。

TSS指定组成任务执行空间的段,并为任务状态信息提供存储位置。在多任务系统中,TSS还提供了一种链接任务的机制。

任务由其TSS的段选择器标识。当任务加载到处理器中执行时,TSS的段选择器、基址、限制和段描述符属性被加载到任务寄存器中(参见第2.4.4节“任务寄存器(TR)”)。如果为任务实现了分页,则任务使用的页面目录的基址将加载到控制寄存器CR3中。

以下各项定义了当前正在执行的任务的状态:

•任务的当前执行空间,由段寄存器(CS、DS、SS、ES、FS和GS)中的段选择器定义。

•通用寄存器的状态。

•EFLAGS寄存器的状态。

•EIP寄存器的状态。

•控制寄存器CR3的状态。

•任务寄存器的状态。

•LDTR寄存器的状态。

•I/O映射基址和I/O映射(包含在TSS中)。

•指向特权0、1和2堆栈(包含在TSS中)的堆栈指针。

•链接到以前执行的任务(包含在TSS中)。

•阴影堆栈指针(SSP)的状态。

在分派任务之前,除任务寄存器的状态外,所有这些项都包含在任务的TSS中。

此外,TSS中不包含LDTR寄存器的完整内容,只包含LDT的段选择器

软件或处理器可以通过以下方式之一分派任务以执行:

•使用call指令明确调用任务。

•使用JMP指令显式跳转到任务。

•(由处理器)对中断处理程序任务的隐式调用。

•对异常处理程序任务的隐式调用。

•在EFLAGS寄存器中设置NT标志时返回(用IRET指令启动)。

所有这些调度任务的方法都使用指向任务门或任务TSS的段选择器来标识要调度的任务。当使用CALL或JMP指令调度任务时,指令中的选择器可以直接选择TSS,也可以选择保存TSS选择器的任务门。当调度任务以处理中断或异常时,中断或异常的IDT条目必须包含一个任务门,该任务门包含中断或异常处理程序TSS的选择器。

当调度任务以执行时,当前运行的任务和调度的任务之间会发生任务切换。在任务切换期间,当前正在执行的任务的执行环境(称为任务的状态或上下文)保存在其TSS中,任务的执行被挂起。然后将调度任务的上下文加载到处理器中,该任务的执行从新加载的EIP寄存器指向的指令开始。如果自上次初始化系统后任务尚未运行,EIP将指向任务代码的第一次指令;否则,它将指向任务上次活动时执行的最后一条指令之后的下一条指令。

如果当前正在执行的任务(调用任务)调用了正在调度的任务(被调用任务),则调用任务的TSS段选择器存储在被调用任务的TS S中,以提供返回调用任务的链接。

对于所有IA-32处理器,任务都不是递归的。任务不能调用或跳转到自身。

通过将任务切换到处理程序任务,可以处理中断和异常。在这里,处理器执行任务切换来处理中断或异常,并在从中断处理程序任务或异常处理程序任务返回时自动切换回中断的任务。这种机制还可以处理中断任务期间发生的中断。

作为任务切换的一部分,处理器还可以切换到另一个LDT,允许每个任务对基于LDT的段具有不同的逻辑到物理地址映射。页面目录基址寄存器(CR3)也被重新加载到任务交换机上,允许每个任务都有自己的一组页面表。这些保护设施有助于隔离任务并防止它们相互干扰。

如果未使用保护机制,则处理器不会在任务之间提供保护。即使对于使用多个权限级别进行保护的操作系统也是如此。以特权级别3运行的任务与其他特权级别3任务使用相同的LDT和页表,可能会访问代码并损坏数据和其他任务的堆栈。

使用任务管理工具处理多任务应用程序是可选的。可以在软件中处理多任务,每个软件定义的任务都在单个IA-32体系结构任务的上下文中执行。

处理器定义了五种用于处理任务相关活动的数据结构:

•任务状态段(TSS)。

•任务门描述符。

•TSS描述符。

•任务登记簿。

•EFLAGS寄存器中的NT标志。

在保护模式下操作时,必须为至少一个任务创建TSS和TSS描述符,并且必须将TSS的段选择器加载到任务寄存器中(使用LTR指令)。

还原任务所需的处理器状态信息保存在称为任务状态段(TSS)的系统段中。图7-2显示了为32位CPU设计的任务的TSS格式。TSS的字段分为两大类:动态字段和静态字段。

有关16位Intel 286处理器任务结构的信息,请参阅第7.6节“16位任务状态段(TSS)”。有关64位模式任务结构的详细信息,请参见第7.7节“64位模式下的任务管理”。

当任务在任务切换期间暂停时,处理器会更新动态字段。以下是动态字段:

• 通用寄存器字段——任务切换前EAX、ECX、EDX、EBX、ESP、EBP、ESI和EDI寄存器的状态。

• 段选择器字段——在任务切换之前存储在 ES、CS、SS、DS、FS 和 GS 寄存器中的段选择器。

• EFLAGS 寄存器字段— 任务切换之前的EFLAGS 寄存器状态。

• EIP(指令指针)字段——任务切换前EIP寄存器的状态。

• 前一个任务链接字段— 包含前一个任务的TSS 的段选择器(在由调用、中断或异常启动的任务切换上更新)。该字段(有时称为反向链接字段)允许使用 IRET 指令将任务切换回前一个任务。

处理器读取静态字段,但通常不会更改它们。这些字段是在创建任务时设置的。以下是静态字段:

• LDT 段选择器字段— 包含任务LDT 的段选择器。

• CR3 控制寄存器字段——包含任务要使用的页目录的基本物理地址。

控制寄存器 CR3 也称为页目录基址寄存器 (PDBR)。

• 特权级 0、-1 和 -2 堆栈指针字段 — 这些堆栈指针由逻辑地址组成,该逻辑地址由堆栈段(SS0、SS1 和 SS2)的段选择器和堆栈偏移量(ESP0 、ESP1 和 ESP2)。请注意,这些字段中的值对于特定任务是静态的;然而,如果在任务中发生堆栈切换,则 SS 和 ESP 值将发生变化。

• T(调试陷阱)标志(字节 100,位 0)— 设置后,当任务切换到此任务时,T 标志会导致处理器引发调试异常(请参阅第 17.3.1.5 节,“任务切换异常条件” ”)。

• I/O 映射基地址字段 — 包含从 TSS 的基地址到 I/O 权限位图和中断重定向位图的 16 位偏移量。当存在时,这些映射存储在 TSS 中的较高地址。

I/O 映射基地址指向 I/O 权限位映射的开始和中断重定向位映射的结束。有关 I/O 权限位图的详细信息,请参阅 Intel 64 和 IA-32 架构软件开发人员手册第 1 卷中的第 19 章“输入/输出”。有关中断重定向位图的详细说明,请参见第 20.3 节“虚拟 8086 模式下的中断和异常处理”。

• 影子堆栈指针(SSP) — 包含任务的影子堆栈指针。任务的影子堆栈应该在任务 SSP 指向的地址(偏移量 104)处有一个主管影子堆栈令牌。当使用 CALL/JMP 指令切换到该影子堆栈时,该令牌将被验证并使其忙碌,并在使用 IRET 指令切换出该任务时使其空闲。如果使用分页:

• 对应于前一个任务的 TSS、当前任务的 TSS 和每个任务的描述符表条目的页面都应标记为读/写。

• 如果在启动任务切换之前内存中存在包含这些结构的页面,则任务切换执行得更快。

与所有其他段一样,TSS 由段描述符定义。图 7-3 显示了 TSS 描述符的格式。 TSS 描述符只能放在 GDT 中;它们不能放在 LDT 或 IDT 中。

尝试使用设置了 TI 标志(指示当前 LDT)的段选择器访问 TSS 会导致在 CALL 和 JMP 期间生成通用保护异常 (#GP);它会在 IRET 期间导致无效的 TSS 异常 (#TS)。如果尝试加载段,也会生成一般保护异常

将 TSS 的选择器放入段寄存器。

类型字段中的忙标志 (B) 指示任务是否忙。一个繁忙的任务当前正在运行或暂停。

type 字段值为 1001B 表示非活动任务;值 1011B 表示任务繁忙。任务不是递归的。处理器使用忙标志来检测尝试调用其执行已被中断的任务。为了确保只有一个忙标志与任务相关联,每个 TSS 应该只有一个指向它的 TSS 描述符。

base、limit 和 DPL 字段以及粒度和存在标志的功能类似于它们在数据段描述符中的使用(参见第 3.4.5 节,“段描述符”)。当 32 位 TSS 的 TSS 描述符中的 G 标志为 0 时,限制字段的值必须等于或大于 67H,比 TSS 的最小大小小一个字节。尝试切换到 TSS 描述符的限制小于 67H 的任务会生成无效 TSS 异常 (#TS)。如果包含 I/O 权限位图或操作系统存储附加数据,则需要更大的限制。处理器不会在任务切换上检查大于 67H 的限制;但是,它会在访问 I/O 权限位图或中断重定向位图时进行检查。

任何可以访问 TSS 描述符(即其 CPL 在数值上等于或小于 TSS 描述符的 DPL)的程序或过程都可以通过调用或跳转来分派任务。

在大多数系统中,TSS 描述符的 DPL 被设置为小于 3 的值,这样只有特权软件才能执行任务切换。但是,在多任务应用程序中,某些 TSS 描述符的 DPL 可能设置为 3,以允许在应用程序(或用户)权限级别进行任务切换。

任务寄存器保存当前任务的 TSS(参见图 2-6)。

此信息是从当前任务的 GDT 中的 TSS 描述符复制而来的。图 7-5 显示了处理器用来访问 TSS 的路径(使用任务寄存器中的信息)。

任务寄存器有可见部分(可由软件读取和更改)和不可见部分(由处理器维护,软件不可访问)。可见部分中的段选择器指向 GDT 中的 TSS 描述符。处理器使用任务寄存器的不可见部分来缓存 TSS 的段描述符。将这些值缓存在寄存器中可以更有效地执行任务。 LTR(加载任务寄存器)和 STR(存储任务寄存器)指令加载和读取任务寄存器的可见部分:

LTR 指令将段选择器(源操作数)加载到任务寄存器中,该寄存器指向 GDT 中的 TSS 描述符。然后它使用来自 TSS 描述符的信息加载任务寄存器的不可见部分。 LTR 是一条特权指令,只有在 CPL 为 0 时才能执行。它在系统初始化期间用于将初始值放入任务寄存器。之后,当发生任务切换时,任务寄存器的内容会隐式更改。

STR(存储任务寄存器)指令将任务寄存器的可见部分存储在通用寄存器或存储器中。该指令可以由以任何特权级别运行的代码执行,以识别当前正在运行的任务。但是,它通常仅由操作系统软件使用。

(如果 CR4.UMIP = 1,则只有在 CPL = 0 时才能执行 STR。)在处理器上电或复位时,段选择器和基地址设置为默认值 0;限制设置为 FFFFH。

任务门描述符提供对任务的间接、受保护的引用(见图 7-6)。 它可以放置在 GDT、LDT 或 IDT 中。 任务门描述符中的 TSS 段选择器字段指向 GDT 中的 TSS 描述符。 未使用此段选择器中的 RPL。

任务门描述符的 DPL 控制在任务切换期间对 TSS 描述符的访问。 当程序或过程通过任务门调用或跳转到任务时,指向任务门的门选择器的 CPL 和 RPL 字段必须小于或等于任务门描述符的 DPL。 请注意,当使用任务门时,不使用目标 TSS 描述符的 DPL。

可以通过任务门描述符或 TSS 描述符访问任务。这两种结构都满足以下需求:

• 需要一个任务只有一个繁忙标志——因为任务的繁忙标志存储在 TSS 描述符中,所以每个任务应该只有一个 TSS 描述符。然而,可能有几个任务门引用相同的 TSS 描述符。

• 需要提供对任务的选择性访问——任务门满足了这一需求,因为它们可以驻留在 LDT 中并且可以具有不同于 TSS 描述符的 DPL 的 DPL。没有足够权限访问 GDT 中任务的 TSS 描述符(通常 DPL 为 0)的程序或过程可能被允许通过具有更高 DPL 的任务门访问任务。任务门为操作系统提供了更大的自由度来限制对特定任务的访问。

• 需要由独立任务处理中断或异常 — 任务门也可能驻留在 IDT 中,这允许处理程序任务处理中断和异常。当中断或异常向量指向任务门时,处理器切换到指定的任务。

图 7-7 说明了 LDT 中的任务门、GDT 中的任务门和 IDT 中的任务门如何都指向同一个任务。

在以下四种情况之一中,处理器将执行转移到另一个任务:

• 当前程序、任务或过程对 GDT 中的 TSS 描述符执行 JMP 或 CALL 指令。

• 当前程序、任务或过程对 GDT 或当前 LDT 中的任务门描述符执行 JMP 或 CALL 指令。

• 中断或异常向量指向 IDT 中的任务门描述符。

• 当前任务在EFLAGS 寄存器中的NT 标志置位时执行IRET。

JMP、CALL 和 IRET 指令,以及中断和异常,都是重定向程序的机制。 TSS 描述符或任务门的引用(调用或跳转到任务时)或 NT 标志的状态(执行 IRET 指令时)决定是否发生任务切换。

处理器在切换到新任务时执行以下操作:

1. 从任务门或前一个任务链接字段(对于使用 IRET 指令启动的任务切换)获取新任务的 TSS 段选择器作为 JMP 或 CALL 指令的操作数。

2. 检查当前(旧)任务是否允许切换到新任务。数据访问权限规则适用于 JMP 和 CALL 指令。当前(旧)任务的 CPL 和新任务的段选择器的 RPL 必须小于或等于所引用的 TSS 描述符或任务门的 DPL。例外,

中断(下一句中标识的除外),并且允许 IRET 和 INT1 指令切换任务,而不管目标任务门的 DPL 或 TSS 描述符。对于由 INT n、INT3 和 INTO 指令生成的中断,检查 DPL,如果它小于 CPL.1,则会导致通用保护异常 (#GP)

3. 检查新任务的 TSS 描述符是否被标记为存在并且具有有效限制(大于或等于 67H)。如果任务切换是由 IRET 启动的,并且在当前 CPL 中启用了影子堆栈,则 SSP 必须对齐到 8 个字节,否则会生成 #TS(当前任务 TSS)故障。如果 CR4.CET 为 1,则 TSS 必须是 32 位 TSS,并且新任务的 TSS 限制必须大于或等于 107 字节,否则会产生 #TS(new task TSS) 故障。

4. 检查新任务是否可用(调用、跳转、异常或中断)或忙碌(IRET 返回)。

5. 检查当前(旧)TSS、新 TSS 和任务切换中使用的所有段描述符是否被分页到系统内存中。

6. 将当前(旧)任务的状态保存在当前任务的 TSS 中。处理器在任务寄存器中找到当前 TSS 的基地址,然后将以下寄存器的状态复制到当前 TSS 中:所有通用寄存器、来自段寄存器的段选择器、临时保存的 EFLAGS 寄存器映像,以及指令指针寄存器(EIP)。

7. 用新任务的 TSS 的段选择器和描述符加载任务寄存器。

任务切换成功时,始终保存当前执行任务的状态。如果任务恢复,则从保存的 EIP 值指向的指令开始执行,并且寄存器恢复到任务暂停时它们保存的值。

切换任务时,新任务的权限级别不会从挂起的任务中继承其权限级别。新任务以 CS 寄存器的 CPL 字段中指定的特权级别开始执行,该字段从 TSS 加载。因为任务由它们各自的地址空间和 TSS 隔离,并且因为特权规则控制对 TSS 的访问,所以软件不需要对任务切换执行显式特权检查。

表 7-1 显示了处理器在切换任务时检查的异常情况。如果检测到错误,它还显示为每次检查生成的异常以及错误代码引用的段。

(表中检查的顺序是在 P6 系列处理器中使用的顺序。确切的顺序是特定于模型的,并且对于其他 IA-32 处理器可能不同。)旨在处理这些异常的异常处理程序可能会受到递归调用如果他们试图重新加载产生异常的段选择器。异常的原因(或多个原因中的第一个)应在重新加载选择器之前修复。

每次发生任务切换时,都会设置控制寄存器 CR0 中的 TS(任务切换)标志。 当与处理器的其余部分生成浮点异常时,系统软件使用 TS 标志来协调浮点单元的动作。 TS 标志表示浮点单元的上下文可能与当前任务的上下文不同。 有关 TS 标志的功能和使用的详细说明,请参见第 2.5 节“控制寄存器”。

处理器提供了两种中断程序执行的机制,中断和异常:

• 中断是通常由I/O 设备触发的异步事件。

• 异常是处理器在执行指令时检测到一个或多个预定义条件时生成的同步事件。 IA-32 体系结构指定了三类异常:故障、陷阱和中止。

异步:并不是当前程序引起的中断。

同步:当前程序引起的中断。

处理器以基本相同的方式响应中断和异常。 当发出中断或异常信号时,处理器会暂停当前程序或任务的执行,并切换到专门为处理中断或异常情况而编写的处理程序过程。 处理器通过中断描述符表 (IDT) 中的条目访问处理程序过程。 当处理程序完成对中断或异常的处理时,程序控制权将返回给被中断的程序或任务。

操作系统、执行程序和/或设备驱动程序通常独立于应用程序或任务处理中断和异常。但是,应用程序可以通过汇编语言调用访问操作系统或执行程序中包含的中断和异常处理程序。本节的其余部分简要概述了处理器的中断和异常处理机制。有关此机制的说明,请参阅《英特尔® 64 位和 IA-32 架构软件开发人员手册》第 3A 卷中的第 6 章“中断和异常处理”。

IA-32 架构定义了 18 个预定义的中断和异常以及 224 个用户定义的中断,它们与 IDT 中的条目相关联。 IDT 中的每个中断和异常都用一个数字标识,称为向量。表 6-1 列出了 IDT 中的中断和异常及其各自的向量。向量 0 到 8、10 到 14 和 16 到 19 是预定义的中断和异常;向量 32 到 255 用于软件定义的中断,它们用于软件中断或可屏蔽的硬件中断。

请注意,处理器定义了几个额外的中断,它们不指向 IDT 中的条目;这些中断中最值得注意的是 SMI 中断。有关中断和异常的更多信息,请参阅《英特尔® 64 位和 IA-32 架构软件开发人员手册》第 3A 卷中的第 6 章“中断和异常处理”。

当处理器检测到中断或异常时,它会执行以下操作之一:

• 执行对处理程序过程的隐式调用。

• 执行对处理程序任务的隐式调用。

对中断或异常处理程序过程的调用类似于对另一个保护级别的过程调用(参见第 6.4.6 节,“权限级别之间的 CALL 和 RET 操作”)。 这里,向量引用 IDT 中的两种门之一:中断门或陷阱门。

中断和陷阱门与调用门相似,它们提供以下信息:

• 访问权限信息

• 包含处理程序的代码段的段选择器

• 到处理程序的第一条指令的代码段的偏移量 中断门和陷阱门之间的区别如下。

如果通过中断门调用中断或异常处理程序,处理器将清除 EFLAGS 寄存器中的中断使能 (IF) 标志,以防止后续中断干扰处理程序的执行。 当通过陷阱门调用处理程序时,IF 标志的状态不会改变。

中断门和陷阱门的区别如下。 如果通过中断门调用中断或异常处理程序,处理器将清除 EFLAGS 寄存器中的中断使能 (IF) 标志,以防止后续中断干扰处理程序的执行。 当通过陷阱门调用处理程序时,IF 标志的状态不会改变。

如果处理程序的代码段与当前执行的程序或任务具有相同的权限级别,则处理程序使用当前堆栈; 如果处理程序以更高的特权级别执行,则处理器切换到处理程序特权级别的堆栈。

如果没有发生堆栈切换,处理器在调用中断或异常处理程序时会执行以下操作(见图 6-7):

1. 将 EFLAGS、CS 和 EIP 寄存器的当前内容(按此顺序)压入堆栈。

2. 将错误代码(如果合适)压入堆栈。

3. 将新代码段的段选择器和新指令指针(来自中断门或陷阱门)分别加载到 CS 和 EIP 寄存器中。

4. 如果调用是通过中断门,清除 EFLAGS 寄存器中的 IF 标志。

5. 开始执行处理程序。

如果确实发生了堆栈切换,处理器将执行以下操作:

1. 临时保存(内部)SS、ESP、EFLAGS、CS 和 EIP 寄存器的当前内容。

2. 将新堆栈的段选择器和堆栈指针(即被调用的特权级别的堆栈)从 TSS 加载到 SS 和 ESP 寄存器中,并切换到新堆栈。

3. 将中断过程堆栈的临时保存的 SS、ESP、EFLAGS、CS 和 EIP 值推送到新堆栈上。

4. 在新堆栈上推送一个错误代码(如果合适)。

5. 将新代码段的段选择器和新指令指针(来自中断门或陷阱门)分别加载到 CS 和 EIP 寄存器中。

6. 如果调用是通过中断门,清除 EFLAGS 寄存器中的 IF 标志。

7. 以新的特权级别开始执行处理程序。

使用 IRET 指令启动中断或异常处理程序的返回。 IRET 指令类似于 far RET 指令,不同之处在于它还为中断的过程恢复 EFLAGS 寄存器的内容。 当从与中断过程相同的特权级别执行中断或异常处理程序的返回时,处理器执行以下操作:

1. 将 CS 和 EIP 寄存器恢复到中断或异常之前的值。

2. 恢复 EFLAGS 寄存器。

3. 适当增加堆栈指针。

4. 恢复中断过程的执行。

当从与中断过程不同的特权级别执行中断或异常处理程序的返回时,处理器执行以下操作:

1. 执行权限检查。

2. 将 CS 和 EIP 寄存器恢复到中断或异常之前的值。

3. 恢复 EFLAGS 寄存器。

4. 将 SS 和 ESP 寄存器恢复到中断或异常之前的值,导致堆栈切换回中断过程的堆栈。

5. 恢复执行被中断的程序。

总结:

根据 Intel 手册的阅读,我们来继续学习,这块的代码。

__asm__ (
	movl %esp,%eax
	pushl $0x17
	pushl %eax
	pushfl
	pushl $0x0f
	pushl $1f
	iret\n
	1:movl $0x17,%eax
	movw %ax,%ds
	movw %ax,%es
	movw %ax,%fs
	movw %ax,%gs
:::"ax")

Linux 切换栈,使用 IRET 指令,来进行从 R0 - R3 的特权级切换。特权级切换就需要切换栈,而切换栈,则就需要保存 (CS IP)回来时运行到那条指令 EFLAGS (SS ESP)哪一个堆栈 。

对比上图 6-17 :

SS寄存器:0x17 0001 0111

CS寄存器:0x0f 0000 1111

IP寄存器:$1f

为何这些值都是写死的?因为 IRET 是模拟了中断处理。

CS 里存储的代码段选择子 0x1f 存储了代码段的数据 和 SS 里存储的栈段选择子 0x17 里存储了栈段数据

那我们就需要找到这个 0x17 , 0x1f 是什么。

而每一个任务都有一个TSS段描述符,而 TSS段描述符是在 GDT表中存储的,说明操作系统在某一个地方将TSS设置到了这个表中,所以我们也需要知道这个 TSS段在哪里进行初始化的。

/*
 * Entry into gdt where to find 
 * first TSS. 
 * 0-nul, 1-cs, 2-ds, 3-syscall
 * 4-TSS0, 5-LDT0, 6-TSS1 etc ...
 */
#define FIRST_TSS_ENTRY 4
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)

union task_union {
	struct task_struct task;
	char stack[PAGE_SIZE];
};
static union task_union init_task = {INIT_TASK,};

#define INIT_TASK \
/* state etc */	{ 0,15,15, 
/* signals */	0,{{},},0, 
/* ec,brk... */	0,0,0,0,0,0, 
/* pid etc.. */	0,-1,0,0,0, 
/* uid etc */	0,0,0,0,0,0, 
/* alarm */	0,0,0,0,0,0, 
/* math */	0, 
/* fs info */	-1,0022,NULL,NULL,NULL,0, 
/* filp */	{NULL,}, 
	{ \
		{0,0}, 
		/* ldt */	
        {0x9f,0xc0fa00}, 
		{0x9f,0xc0f200}, 
	}, \
	/*tss*/	
    {0,
     PAGE_SIZE+(long)&init_task,
     0x10,0,0,0,0,(long)&pg_dir,
	 0,0,0,0,0,0,0,0, \
	 0,0,0x17, // es
         0x17, // cs
         0x17, // ss
         0x17, // ds
         0x17, // fs
         0x17, // gs
	 _LDT(0),
         0x80000000,
		{} 
	}, 
}

struct task_struct {
/* these are hardcoded - don't touch */
	long state;	//程序运行的状态/* -1 unrunnable, 0 runnable, >0 stopped */
	long counter; //时间片
	//counter的计算不是单纯的累加,需要下面这个优先级这个参数参与
	long priority;//优先级
	long signal;//信号
	struct sigaction sigaction[32];//信号位图
	long blocked;//阻塞状态	/* bitmap of masked signals */
/* various fields */
	int exit_code;//退出码
	unsigned long start_code,end_code,end_data,brk,start_stack;
	long pid,father,pgrp,session,leader;
	unsigned short uid,euid,suid;
	unsigned short gid,egid,sgid;
	long alarm;//警告
	long utime,stime,cutime,cstime,start_time;//运行时间
	//utime是用户态运行时间 cutime是内核态运行时间
	unsigned short used_math;
/* file system info */
	int tty;	//是否打开了控制台	/* -1 if no tty, so it must be signed */
	unsigned short umask;
	struct m_inode * pwd;
	struct m_inode * root;
	struct m_inode * executable;
	unsigned long close_on_exec;
	struct file * filp[NR_OPEN];//打开了多少个文件
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
	struct desc_struct ldt[3];//ldt包括两个东西,一个是数据段(全局变量静态变量等),另一个是代码段,不过这里面存的都是指针
/* tss for this task */
	struct tss_struct tss;//进程运行过程中CPU需要知道的进程状态标志(段属性、位属性等)
};

typedef struct desc_struct {
	unsigned long a,b;
} desc_table[256];

//tss就是这个意思,一个进程运行时肯定要往各种寄存器里填各种数据,这里保存的就是这些数据
struct tss_struct {
	long	back_link;	/* 16 high bits zero */
	long	esp0;
	long	ss0;		/* 16 high bits zero */
	long	esp1;//栈指针
	long	ss1;		/* 16 high bits zero */
	long	esp2;
	long	ss2;//寄存器		/* 16 high bits zero */
	long	cr3;
	long	eip; //程序的运行指针
	long	eflags;
	long	eax,ecx,edx,ebx;//通用寄存器
	long	esp;
	long	ebp;
	long	esi;
	long	edi;
	long	es;		/* 16 high bits zero */
	long	cs;		/* 16 high bits zero */
	long	ss;		/* 16 high bits zero */
	long	ds;		/* 16 high bits zero */
	long	fs;		/* 16 high bits zero */
	long	gs;		/* 16 high bits zero */
	long	ldt;		/* 16 high bits zero */
	long	trace_bitmap;	/* bits: trace 0, bitmap 16-31 */
	struct i387_struct i387;//协处理器
};

void sched_init(void)
{
	int i;
	struct desc_struct * p;

	if (sizeof(struct sigaction) != 16)
		panic("Struct sigaction MUST be 16 bytes");
	//gdt是全局描述符(系统级别)和前面所说的ldt(局部描述符)对应
	//内核的代码段
	//内核的数据段
	//进程0...n的数据
	set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
	set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
	p = gdt+2+FIRST_TSS_ENTRY;
	for(i=1;i<NR_TASKS;i++) {//0-64进程进行遍历
		task[i] = NULL;
		p->a=p->b=0;
		p++;
		p->a=p->b=0;
		p++;
	}//作用是清空task链表
/* Clear NT, so that we won't have troubles with that later on */
	__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
	ltr(0);
	lldt(0);
	//以下都是设置一些小的寄存器组
	outb_p(0x36,0x43);		/* binary, mode 3, LSB/MSB, ch 0 */
	outb_p(LATCH & 0xff , 0x40);	/* LSB */
	outb(LATCH >> 8 , 0x40);	/* MSB */
	set_intr_gate(0x20,&timer_interrupt);
	outb(inb_p(0x21)&~0x01,0x21);
	//设置系统中断
	set_system_gate(0x80,&system_call);
}

从代码中得知, GDT表项 0-nul, 1-cs, 2-ds, 3-syscall, 4-TSS0, 5-LDT0, 6-TSS1, 7-LDT1 依次类推。

我们观察代码可知 在 init_task.task 就是 task_struct的 tss_struct 结构体。

/*
 * Entry into gdt where to find 
 * first TSS. 
 * 0-nul, 1-cs, 2-ds, 3-syscall
 * 4-TSS0, 5-LDT0, 6-TSS1 etc ...
 */
#define FIRST_TSS_ENTRY 4
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)

typedef struct desc_struct {
	unsigned long a,b;
} desc_table[256];

extern desc_table idt,gdt;

// gdt + 4 (4-TSS0)
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
// gdt + 5 (5-LDT0)
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));

// 宏定义
#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82")

#define _set_tssldt_desc(n,addr,type)
__asm__ (
	movw $104,%1
	movw %ax,%2
	rorl $16,%eax
	movb %al,%3
	movb $"type",%4
	movb $0x00,%5
	movb %ah,%6
	rorl $16,%eax
::
	"a" (addr),  // 0
	"m" (*(n)),  // 1
    "m" (*(n+2)),// 2
    "m" (*(n+4)),// 3
	"m" (*(n+5)),// 4
    "m" (*(n+6)),// 5
    "m" (*(n+7)) // 6
)

将 TSS 和 LDT 设置到 GDT 表项中。

这里又出现了 0x89 和 0x82

0x89 : 0000 0000 1000 1001 1001 表示:TSS

0x82: 0000 0000 1000 0010 0010 表示:LDT

如果TI标识位为 LDT 则表示当前段选择子是 LDT

1(P)00 ( DPL ) 0 1001 (Type)

1(P)00 ( DPL ) 0 0010(Type)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值