实模式→保护模式
8086处理器
-
16位且只有一种工作模式:实模式;具有20根地址线,因此可以借助内存分段模型访问1M的内存地址。
-
8086可以访问1MB的内存空间,地址范围为0x00000到0xFFFFF。
- DRAM:0x00000~0x9FFFF;
- ROM:0xF0000~0xFFFFF,里面固化了开机时要执行的指令;
ROM-BIOS:进行硬件的诊断、检测和初始化,提供最基本的软件例程。在ROM-BIOS完成使命前,最后一件事便是从外存储设备中读取更多的指令来交给处理器执行。
ROM-BIOS将试图读取硬盘的0面0道1扇区(主引导扇区MBR)到0x0000:0x7c00,然后判断MBR是否有效(即512字节的MBR的最后两个字节为0x55,0xAA),并使用jmp跳转到0x07c00处继续执行。
MBR中的代码将检测用来启动的计算机的操作系统,并计算出它所在的硬盘位置。然后把操作系统的自举代码加载到内存,也用jmp跳转到对应位置继续执行,直到操作系统完全启动。
- 0xA0000~0xEFFFF:由特定的外围设备提供;其中0xB8000 ~0xBFFFF为显卡。
-
16位处理器加电后,复位将使代码段寄存器(CS)的内容为0xFFFF,其他所有寄存器的内容都为0x0000。
由于0xFFFF0距离1MB内存的顶端只有16个字节长度,一旦IP寄存器的值超过0x000F,将绕回到1MB内存的底端。所以0xFFFF0处通常是一个跳转指令:
jmp 0xF000:0xE05B
8086实模式下的计算机启动过程
主引导扇区→程序加载器
和主引导扇区程序一样,操作系统也为于硬盘上。操作系统是需要安装到硬盘上的,这个安装过程不但要把操作系统的指令和数据写入硬盘,通常还要更新主引导扇区的内容,好让这块跳板直接连着操作系统。
boot→loader→kernel
SECTION header align=16 vstart=0 ;定义用户程序头部段
program_length dd program_end ;程序总长度[0x00]
;用户程序入口点
code_entry dw start ;偏移地址[0x04]
dd section.code_1.start ;段地址[0x06]
realloc_tbl_len dw (header_end-code_1_segment)/4
;段重定位表项个数[0x0a]
;段重定位表
code_1_segment dd section.code_1.start ;[0x0c]
code_2_segment dd section.code_2.start ;[0x10]
data_1_segment dd section.data_1.start ;[0x14]
data_2_segment dd section.data_2.start ;[0x18]
stack_segment dd section.stack.start ;[0x1c]
header_end:
- SECTION/SEGMENT 段名称:定义段
- align=16:段在内存中的起始物理地址起码是16字节对齐的
- vstart=0:表示该段中标号的汇编地址从所在段的开头计算,且从0开始计算;若没有则表示从整个程序的开头计算
- section.段名称.start:获取该段的汇编地址
用户程序头部起码要包含:用户程序的尺寸、应用程序的入口点、段重定位表
加载程序(器)的工作流程
通过硬盘控制器端口读扇区数据(LBA28)
0x1F0:硬盘接口的数据端口(16位)
0x1F1:
0x1F2:设置要读取的扇区数量,其数值写入该端口(8位)
0x1F3:存放扇区编号的0~7位
0x1F4:存放扇区编号的8~15位
0x1F5:存放扇区编号的16~23位
0x1F6:存放扇区编号的24~27位(由于传入的是8位,第4位用于构成扇区号的最后4位,高4位用于指示状态),第4位用于只是硬盘号(0:主盘,1:从盘),高3位是“111”,表示LBA模式
0x1F7:写入0x20,请求硬盘读;同时作为状态端口查看硬盘情况
read_hard_disk_0: ;从硬盘读取一个逻辑扇区
;输入:DI:SI=起始逻辑扇区号
; DS:BX=目标缓冲区地址
push ax
push bx
push cx
push dx
mov dx,0x1f2
mov al,1
out dx,al ;读取的扇区数
inc dx ;0x1f3
mov ax,si
out dx,al ;LBA地址7~0
inc dx ;0x1f4
mov al,ah
out dx,al ;LBA地址15~8
inc dx ;0x1f5
mov ax,di
out dx,al ;LBA地址23~16
inc dx ;0x1f6
mov al,0xe0 ;LBA28模式,主盘
or al,ah ;LBA地址27~24
out dx,al
inc dx ;0x1f7
mov al,0x20 ;读命令
out dx,al
.waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;不忙,且硬盘已准备好数据传输
mov cx,256 ;总共要读取的字数
mov dx,0x1f0
.readw:
in ax,dx
mov [bx],ax
add bx,2
loop .readw
pop dx
pop cx
pop bx
pop ax
ret
jmp far [0x04] → 跳转到用户程序进行执行,此时DS保存的是用户程序在内存中的段地址,DS:0x04保存用户程序起始地址的内存,将起始地址传给CS以及IP并进行跳转
中断
外部硬件中断
通过NMI(非屏蔽中断)和INTR(可屏蔽中断)两个信号线引入处理器内部
- NMI被赋予了统一的中断号2
- INTR的中断号并不固定(可编程中断控制器)。8259芯片(引脚电平0:允许,1:阻断)常用作中断代理,来接受外部设备发出的中断信号,同时对中断的优先级进行仲裁。
注:Intel处理器允许256个中断。
IF标志位:0(cli)→ 忽略中断,1(sti)→ 允许中断
- 实模式下的中断向量表
a. 中断向量表:0x00000~0x003FF
b. 每个中断在中断向量表中占2个字(偏移地址+段地址)
内部中断
内部中断发生在处理器内部,是由执行的指令引起的(指令出错、指令非法等)
内部中断和软中断不受标志寄存器IF位的影响,也不需要中断识别总线周期(4个以上时钟周期),可以立即转入相应的处理过程
软中断
软中断是由int指令引起的中断处理,中断号在指令中给出
BIOS中断:这些中断功能在计算机加电之后,BIOS程序执行期间建立起来,即在加载和执行主引导扇区之前,BIOS中断就可以使用。
软中断:当把某个例程写入中断向量表之后,用户程序可以在需要该功能的时候,直接发出一个软中断即可,而不需要具体的地址。
x86处理器
- 32位,支持保护模式,具有32根地址线,可以访问4GB的内存地址。x86从8086发展而来,具有延续性和兼容性。具有实模式和保护模式。
- 32位处理器在16位处理器的基础上,将通用寄存器扩展为32位,但高16位不能独立使用,低16位保持同16位处理器的兼容性。
16位模式是默认的编译模式,在86处理器处于16位模式下时,依然可以使用32位寄存器。
16位实模式下,不允许把sp作为内存寻址方式,但是32位时可以;且32位处理器支持立即数直接入栈。
- 平坦模型:由于(IA-32)处理器基于分段模型,32位处理器依然需要以段位单位访问内存,则可以选择只分一个段,即段的基地址是0x00000000,段的长度是4GB。
- 32位模式下,段寄存器,如CS、SS、DS、ES,保存的不再是16位段基地址,而是段的选择子,即用于选择要访问的段,同时还包括一个不可见的描述符高速缓存器。描述符高速缓存器:里面有段的基地址和各种访问属性(这部分内容不可访问,由处理器自动使用)。
现代处理器的结构和特点
- 流水线:为了提高处理器的执行效率和速度,可以把一条指令的执行过程分解成若干个细小的步骤
- 高速缓存:CPU Cache
- 乱序执行:
- 寄存器重命名:
- 分支目标预测:当处理器执行了一条分支语句后,它会在BTB中记录当前指令的地址、分支目标的地址、以及本次分支预测的结果。(缺点:当该指令实际执行时,如果预测是失败的,那么清空流水线,同时书信BTB中的记录,这个代价较大)。
32位处理器加电后,段寄存器CS的内容是0xF000,其他段寄存器都是0;而CS段描述符高速缓存器中的基地址被预置为0xFFFF0000,EIP被预置为0x0000FFF0。故32位处理器第一次取指令时发出的地址是0xFFFFFFF0,但为了兼容性,第一条指令为
jmp far 0xF000:0xE05B
跳转到0x000FE05B,即跳转到低地址端的BIOS执行。
x86中计算机启动过程:从实模式到保护模式
16位实模式下,一个程序可以自由地访问不属于它的内存位置,甚至可以对那些地方的内容进行修改,既不安全也不合法
保护模式:处理器在加载程序时,先定义该程序所拥有的段,然后允许使用这些段。定义段时,除了基地址(起始地址)外,还附加了段界限、特权级别、类型等属性。当程序访问一个段时,处理器将用固件实施各种检查工作,以防对内存的违规访问。
x86处理器启动时默认是实模式
进入保护模式
全局描述表
- 段描述符:8个字节,每个段都有一个描述符。
- 描述符表:所有的描述符集中存放,就构成了一个描述符表。其中,最重要的是全局描述符表,该表为整个软硬件系统服务。在进入保护模式前,必须定义全局描述符表。
理论上,GDT的界限是16位的,因此不能超过8192个描述符
必须在保护模式之前定义GDT,故GDT通常都定义在1MB以下的内存范围中。允许进入保护模式之后换个位置重新定义GDT。
- 全局描述符表寄存器(GDTR,48位):32位的线性地址→全局描述符表在内存中的起始线性地址+16位的边界→全局描述符表的边界(其值等于表的字节数减一)。
- 描述符是由操作系统根据用户程序结构建立的,而用户程序通常是无法建立和修改GDT的
- 下面是段描述符格式以及数据段描述符的type字段
描述符中指定了32位的段起始地址,以及20位的段边界。在实模式下,段地址并非真实的物理地址,在计算物理地址时,需左移4位。在32位保护模式下,段地址是32位的线性地址,若未开启分页功能,该线性地址就是物理地址。
存储器的保护:设置段的基址和界限,保证程序不会越界
保护模式的准备及开启
- 这里把GDT设在主引导程序之后,即物理地址0x00007E00处
- 确定了GDT在内存中的起始位置后确定要访问的段,并在GDT中为这些段创建各自的描述符
;设置堆栈段和栈指针
mov ax,cs
mov ss,ax
mov sp,0x7c00
;计算GDT所在的逻辑段地址
mov ax,[cs:gdt_base+0x7c00] ;低16位
mov dx,[cs:gdt_base+0x7c00+0x02] ;高16位
mov bx,16
div bx
mov ds,ax ;令DS指向该段以进行操作
mov bx,dx ;段内起始偏移地址
;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [bx+0x00],0x00
mov dword [bx+0x04],0x00
;创建#1描述符,保护模式下的代码段描述符
mov dword [bx+0x08],0x7c0001ff
mov dword [bx+0x0c],0x00409800
;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区)
mov dword [bx+0x10],0x8000ffff
mov dword [bx+0x14],0x0040920b
;创建#3描述符,保护模式下的堆栈段描述符
mov dword [bx+0x18],0x00007a00
mov dword [bx+0x1c],0x00409600
;初始化描述符表寄存器GDTR
mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(总字节数减一)
lgdt [cs: gdt_size+0x7c00]
in al,0x92 ;南桥芯片内的端口
or al,0000_0010B
out 0x92,al ;打开A20
cli ;保护模式下中断机制尚未建立,应
;禁止中断
mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位
;以下进入保护模式... ...
jmp dword 0x0008:flush ;16位的描述符选择子:32位偏移
;清流水线并串行化处理器
[bits 32]
flush:
mov cx,00000000000_10_000B ;加载数据段选择子(0x10)
mov ds,cx
;以下在屏幕上显示"Protect mode OK."
mov byte [0x00],'P'
mov byte [0x02],'r'
mov byte [0x04],'o'
mov byte [0x06],'t'
mov byte [0x08],'e'
mov byte [0x0a],'c'
mov byte [0x0c],'t'
mov byte [0x0e],' '
mov byte [0x10],'m'
mov byte [0x12],'o'
mov byte [0x14],'d'
mov byte [0x16],'e'
mov byte [0x18],' '
mov byte [0x1a],'O'
mov byte [0x1c],'K'
;以下用简单的示例来帮助阐述32位保护模式下的堆栈操作
mov cx,00000000000_11_000B ;加载堆栈段选择子
mov ss,cx
mov esp,0x7c00
mov ebp,esp ;保存堆栈指针
push byte '.' ;压入立即数(字节)
sub ebp,4
cmp ebp,esp ;判断压入立即数时,ESP是否减4
jnz ghalt
pop eax
mov [0x1e],al ;显示句点
ghalt:
hlt ;已经禁止中断,将不会被唤醒
;-------------------------------------------------------------------------------
gdt_size dw 0
gdt_base dd 0x00007e00 ;GDT的物理地址
times 510-($-$$) db 0
db 0x55,0xaa
保护模式下的中断机制和实模式不同,原有的中断向量表不再适用且BIOS中断也不能用,因此使用cli进行关中断。
- 实模式下创建段描述符并加载GDTR
- GDT的第一个描述符必须是空描述符,阻止不安全的访问(一个忘了初始化的指针往往默认值就是0,因此空描述的用意就是阻止不安全的访问)
- 创建段描述符
mov dword [bx+0x08],0x7c0001ff
mov dword [bx+0x0c],0x00409800
- 加载描述符表的线性基地址和界限到GDTR寄存器
lgdt [cs: gdt_size+0x7c00]
把从标号gdt_size开始的6字节加载到GDTR寄存器- 实际段界限计算
以4KB为粒度时
- 开启保护模式
CR0:32位,包含了用于控制处理器操作模式和运行状态的标志位。PE=1:开启保护模式
- 清空流水线并串行化处理器
- 进入保护模式后,实模式的中断不能再用,需要使用cli来关闭中断
- jmp/call会清空流水线,并串行化执行。同时远眺转会重新加载段选择器CS,并刷新描述符高速缓存器中的内容
jmp dword 0x0008:flush ;16位的描述符选择子:32位偏移
因为此时已经处于保护模式下,因此处理器把0x0008视为段选择子;dword修饰偏移量,即要求使用32位偏移量
- 加载数据段
段选择子的组成
mov cx,00000000000_10_000B ;加载数据段选择子(0x10)
mov ds,cx
但是不允许使用mov指令改变寄存器CS的内容
程序的动态加载及执行
GDT、LDT、分段、描述符等是在程序加载时,由操作系统负责创建的,且应用程序的加载和开始执行也是由才做系统所主导的。而操作系统将应用程序放在特权级3上,当应用程序开始执行时,CPL==3。
特权级保护
特权级存在的意义:当一个程序安稳地访问只属于自己的段时,基本的段保护机制仍然有效;但是,一个失控的程序,或者一个恶意程序依然可以通过追踪和修改描述符表来达到它们访问任何内存位置的目的。
特权级:也叫特权级别,是存在于描述符(DPL)
及其选择子(RPL)
中的一个数值。Intel处理器可以识别4个特权级,分别是0到3。
特权级检查:特权级检查不是在实际访问内存时进行的,而是在将选择子代入段寄存器时进行的。
特权级的使用
特权级分类:
DPL
、RPL
、CPL
- CPL:当处理器正在一个代码段中取指令和执行指令时,那个代码段的特权级叫做当前特权级;正在执行的这个代码段,其选择子位于段寄存器CS中,其最低两位就是当前特权级的数值。
- 特权指令:只有在CPL=0时才能执行的指令,如hlt和CR0的改写等
- IOPL:位于处理器的标志寄存器EFLAGS中(位13、位12),代表着当前任务的I/O特权级别。
- RPL:请求特权级,位于选择子的最后两位。
arpl
调整段选择子RPL字段的值arpl r/m16, r16
:目的操作数:传递给操作系统的段选择子;源操作数:应用程序的段选择子- 防止对段的不安全访问,不管是恶意的还是编程疏漏
控制转移
-
除了从高特权级的例程返回外,不允许高特权级→低特权级,因为操作系统不会引用可靠性比自己低的代码。
-
低特权级→同等特权级/高特权级
- 一般情况下,控制转移只发生在两个特权级相同的代码段之间,即
CPL==目标代码段描述符DPL
,RPL==目标代码段描述符DPL
- 低特权级→高特权级:
- 将高特权级的代码段定义为依从的,依从的代码段在调用程序的特权级上运行,即不改变CPL。
CPL>=目标代码段描述符的DPL
,RPL>=目标代码段描述符的DPL
- 调用门:(另一种形式的描述符,描述可执行的代码)定义了目标过程所在代码段的选择子,以及段内偏移。实现方式如下:
- jmp far:转移到高特权级,但是CPL不变
- call far:CPL变为目标代码段的特权级别
- 一般情况下,控制转移只发生在两个特权级相同的代码段之间,即
-
对段寄存器SS进行修改时,要求当前特权级CPL和请求特权级RPL必须等于目标段描述符的DPL
主要是为了防止栈空间不足而产生不可预料的问题,同时也是为了防止栈数据的交叉引用。
因此,每个任务除了自己固有的栈之外,还必须额外定义特权级比它大的栈。这些额外的栈由操作系统加载程序时自动创建。且需登记到LDT及TSS中。栈的切换也是处理器固件自动进行的,并检查栈空间是否足够。
CPL==目标代码段描述符DPL
RPL==目标代码段描述符DPL
栈切换过程:
- 使用目标代码段的DPL(也就是新的CPL)到当前任务的TSS中选择一个栈,包括栈段选择子和栈指针
- 用上述栈段选择子和栈指针读取栈段描述符。期间,任何违反段界限检查的行为都将引发处理器异常中断(无效TSS)
- 检查栈段描述符的特权级和类型,并可能引发处理器异常中断(无效TSS)
- 临时保存当前栈段寄存器SS和栈指针ESP的内容
- 把新的栈段选择子和栈指针代入SS和ESP寄存器,切换到新栈
- 把刚才临时保存的SS和ESP内容压入当前栈
- 依据调用门描述符“参数个数”字段的指示,从旧栈中将所有参数都复制到新栈中。
- 将当前段寄存器CS和指令指针寄存器EIP的内容压入新栈。通过调用门实施的控制转移一定是远转移,所以要压入CS和EIP。
- 从调用门描述符中一次将目标代码段选择子和段内偏移传送到CS和EIP寄存器,开始执行被调用过程。
- 调用门(任务内的控制转移):
type字段:1100表示调用门
CPL<=调用门描述符DPL
RPL<=调用门描述符DPL
调用门的特权级检查:
;以下开始安装为整个系统服务的调用门。特权级之间的控制转移必须使用门
mov edi,salt ;C-SALT表的起始位置
mov ecx,salt_items ;C-SALT表的条目数量
.b3:
push ecx
mov eax,[edi+256] ;该条目入口点的32位偏移地址
mov bx,[edi+260] ;该条目入口点的段选择子
mov cx,1_11_0_1100_000_00000B ;特权级3的调用门(3以上的特权级才
;允许访问),0个参数(因为用寄存器
;传递参数,而没有用栈)
call sys_routine_seg_sel:make_gate_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [edi+260],cx ;将返回的门描述符选择子回填
add edi,salt_item_len ;指向下一个C-SALT条目
pop ecx
loop .b3
调用门的创建和安装:
- 传入条目入口点的32位偏移地址以及所在段的段选择子以及属性
- 调用门的特权级设置为3,故可供3以上特权级的程序访问
- 将构建的调用门描述符加载到gdt中
- 将调用门描述符的选择子(其最低3位都为0,RPL=0)回填到该条目装载选择子的位置
- 注:由于调用门描述符内包含偏移量,因此在使用调用门时所提供的偏移量被忽略
特权级变化的远返回:
- 检查栈中保存的CS寄存器的内容,根据其RPL字段决定返回时是否需要改变特权级别
- 从当前栈中读取CS和EIP寄存器的内容,并针对代码段描述符和代码段选择子的RPL字段实施特权级检查
- 如果远返回指令是带参数的,则将参数和ESP寄存器的当前值相加,以跳过栈中的参数部分。最后的结果是ESP寄存器指向调用者SS和ESP的压栈值。注:retf指令的字节数值必须等于调用门中的参数个数乘以参数长度
- 如果返回后需要改变特权级,从栈中将SS和ESP的压栈值代入段寄存器SS和指令指针寄存器ESP,切换到调用者的栈。在此期间,一旦检测到有任何界限违例的情况都将引发处理器异常中断
- 如果返回时需要改变特权级,检查DS、ES、FS和GS寄存器的内容,根据它们找到相应的段描述符。要是有任何一个段描述符的DPL高于调用者的特权级(返回后的新CPL),则处理器将把数值0传送到该段寄存器。
- 注:这是因为特权级检查只在引用一个段的时候进行,即只在将选择子传送到段寄存的时候进行,那么后面使用这个段寄存器的内存访问都是合法的。因此,要是有任何一个段描述符的DPL高于调用者的特权级(返回后的新CPL),则处理器将把数值0传送到该段寄存器,使用这样的段寄存器访问内存时,会引发处理器异常中断。
数据访问
CPL<=目标代码段描述符的DPL
RPL<=目标代码段描述符的DPL
- EFLAGS寄存器的IOPL位决定了当前任务的I/O特权级别。当
CPL<=IOPL
,所有的I/O操作都是允许的。但当CPL>=IOPL
时,并不意味着所有的硬件端口都禁止当前任务使用,个别端口除外,其相应信息保存在当前任务的TSS的I/O许可位串中。
- 能够修改IOPL位和IF位的指令有:popf、iret、cli、sti
- 当
CPL<=IOPL
时,允许执行以上4条指令,也允许访问所有的硬件端口。- 但当
CPL>=IOPL
时,执行popf和iret指令会引发处理器异常中断;执行cli和sti不会引发异常中断,但不改变标志寄存器的IF位。
任务及多任务切换
任务相关基础知识
任务:程序是记录在载体上的指令和数据,总是为了完成某个特定的工作,其在执行中的一个副本,叫做任务。
LDT:局部描述符表,能有效地在任务之间实施隔离(每个任务都应当具有自己的描述符表,并把专属于自己的段放到LDT中)。LDTR
注:在局部描述符的选择子中,TI位为1,故LDT的第一个描述符是有效的、可以使用的。
TSS:针对每个任务的额外内存区域,用于保存任务的状态,并在下次重新执行时恢复它们。其最小尺寸为104字节。处理器固件能够识别TSS中的每个元素,并在任务切换的时候读取其中的信息。TR
注:
- SS0、SS1和SS2分别是0、1和2特权级的栈段选择子,ESP0、ESP1和ESP2分别是0、1和2特权级栈的栈顶指针。这些内容应当由任务的创建者填写,且属于填写后一般不变的静态部分(从来不会改变,除非手工改写),当通过门进行特权级之间的控制转移时,处理器用这些信息来切换栈。
- LDT段选择子由内核或者操作系统填写,以指向当前任务的LDT
- EFLAGS寄存器的副本,其内容在任务创建的时候由内核或者操作系统初始化
- I/O映射基地址保存的是I/O许可位串的起始地址,如果该字单元的内容大于或者等于TSS的段界限,则表明没有I/O许可位串。
- TSS描述符的TYPE字段为
10B1
,其中B位是“busy”位,当任务开始执行或挂起时,由处理器把B位置为1,防止任务重入。
标志寄存器EFLAGS:
NT
位为1,表示当前正在执行的任务嵌套于其他任务中,并且能够通过TSS任务链接域的指针返回到前一个任务
TCB:内核为每一个任务创建的一个内存区域,用来记录任务的信息和状态,比如程序的大小、加载的位置等等。当任务执行结束时,还要依据这些信息来回收它所占用的内存空间。
注:为了能够追踪到所有任务,应当把每个任务控制块TCB串起来,形成一个链表。
全局空间和局部空间:
- 全局部分是所有任务共有的,含有操作系统的软件和库程序,以及可以调用的系统服务和数据,即全局空间,由GDTR指定;
- 私有部分是每个任务各自的数据和代码,与任务所要解决的具体问题有关,即私有空间,由LDTR指定。
使用栈传递过程参数:
- 栈的隐式访问:由处理器在执行注入push、pop、call、ret等指令时自动进行,需要用到ESP
- 把栈当作是一般的数据段,直接访问其中的任何内容,而不依赖于先进后出机制,需要用到EBP,
mov edx, [ebp]
任务切换前的设置
流程:
- 分配内存用于创建TCB,并追加到TCB链表中
;创建任务控制块。这不是处理器的要求,而是我们自己为了方便而设立的
mov ecx,0x46
call sys_routine_seg_sel:allocate_memory
call append_to_tcb_link ;将任务控制块追加到TCB链表
push dword 50 ;用户程序位于逻辑50扇区
push ecx ;压入任务控制块起始线性地址
call load_relocate_program
- 分配内存,供LDT使用,为创建用户程序各个段的描述符做准备
- 将LDT的大小和起始线性地址登记在任务控制块TCB中
mov ebp,esp ;为访问通过堆栈传递的参数做准备
mov ecx,mem_0_4_gb_seg_sel
mov es,ecx
mov esi,[ebp+11*4] ;从堆栈中取得TCB的基地址
;以下申请创建LDT所需要的内存
mov ecx,160 ;允许安装20个LDT描述符
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x0c],ecx ;登记LDT基地址到TCB中
mov word [es:esi+0x0a],0xffff ;登记LDT初始的界限到TCB中
- 分配内存并加载用户程序,将它的大小和起始线性地址登记到TCB中
;以下开始加载用户程序
mov eax,core_data_seg_sel
mov ds,eax ;切换DS到内核数据段
mov eax,[ebp+12*4] ;从堆栈中取出用户程序起始扇区号
mov ebx,core_buf ;读取程序头部数据,core_buf为内核数据区的buffer区域
call sys_routine_seg_sel:read_hard_disk_0
;以下判断整个程序有多大
mov eax,[core_buf] ;程序尺寸
mov ebx,eax
and ebx,0xfffffe00 ;使之512字节对齐(能被512整除的数低
add ebx,512 ;9位都为0
test eax,0x000001ff ;程序的大小正好是512的倍数吗?
cmovnz eax,ebx ;不是。使用凑整的结果
mov ecx,eax ;实际需要申请的内存数量
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x06],ecx ;登记程序加载基地址到TCB中
- 用户程序段的重定位和描述符的创建并加载到LDT中,这里段描述符
DPL==RPL==3
。
mov edi,[es:esi+0x06] ;获得程序加载基地址
;建立程序头部段描述符
mov eax,edi ;程序头部起始线性地址
mov ebx,[edi+0x04] ;段长度
dec ebx ;段界限
mov ecx,0x0040f200 ;字节粒度的数据段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor
;安装头部段描述符到LDT中
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0011B ;设置选择子的特权级为3
mov [es:esi+0x44],cx ;登记程序头部段选择子到TCB
mov [edi+0x04],cx ;和头部内
- salt重定位
mov ecx,64 ;检索表中,每条目的比较次数
repe cmpsd ;每次比较4字节
jnz .b4
mov eax,[esi] ;若匹配,则esi恰好指向其后的地址
mov [es:edi-256],eax ;将字符串改写成偏移地址
mov ax,[esi+4]
or ax,0000000000000011B ;以用户程序自己的特权级使用调用门
;故RPL=3
mov [es:edi-252],ax ;回填调用门选择子
- 创建0、1和2特权级的栈
通过调用门的控制转移通常会改变当前特权级CPL,同时还要切换到与目标代码段特权级相同的栈。因此,需要额外的栈,且这些栈是动态创建的,并登记在TSS和TCB中,以便处理器固件能够自动地访问到它们。
mov esi,[ebp+11*4] ;从堆栈中取得TCB的基地址
;创建0特权级堆栈
mov ecx,4096
mov eax,ecx ;为生成堆栈高端地址做准备
mov [es:esi+0x1a],ecx
shr dword [es:esi+0x1a],12 ;登记0特权级堆栈尺寸到TCB
call sys_routine_seg_sel:allocate_memory
add eax,ecx ;堆栈必须使用高端地址为基地址
mov [es:esi+0x1e],eax ;登记0特权级堆栈基地址到TCB
mov ebx,0xffffe ;段长度(界限)
mov ecx,0x00c09600 ;4KB粒度,读写,特权级0
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
;or cx,0000_0000_0000_0000 ;设置选择子的特权级为0
mov [es:esi+0x22],cx ;登记0特权级堆栈选择子到TCB
mov dword [es:esi+0x24],0 ;登记0特权级堆栈初始ESP到TCB
- 创建TSS,并登记基本的TSS表格内容
;创建用户程序的TSS
mov ecx,104 ;tss的基本尺寸
mov [es:esi+0x12],cx
dec word [es:esi+0x12] ;登记TSS界限值到TCB
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x14],ecx ;登记TSS基地址到TCB
;登记基本的TSS表格内容
mov word [es:ecx+0],0 ;反向链=0
mov edx,[es:esi+0x24] ;登记0特权级堆栈初始ESP
mov [es:ecx+4],edx ;到TSS中
mov dx,[es:esi+0x22] ;登记0特权级堆栈段选择子
mov [es:ecx+8],dx ;到TSS中
mov edx,[es:esi+0x32] ;登记1特权级堆栈初始ESP
mov [es:ecx+12],edx ;到TSS中
mov dx,[es:esi+0x30] ;登记1特权级堆栈段选择子
mov [es:ecx+16],dx ;到TSS中
mov edx,[es:esi+0x40] ;登记2特权级堆栈初始ESP
mov [es:ecx+20],edx ;到TSS中
mov dx,[es:esi+0x3e] ;登记2特权级堆栈段选择子
mov [es:ecx+24],dx ;到TSS中
mov dx,[es:esi+0x10] ;登记任务的LDT选择子
mov [es:ecx+96],dx ;到TSS中
mov dx,[es:esi+0x12] ;登记任务的I/O位图偏移
mov [es:ecx+102],dx ;到TSS中
mov word [es:ecx+100],0 ;T=0
mov dword [es:ecx+28],0 ;登记CR3(PDBR)
;访问用户程序头部,获取数据填充TSS
mov ebx,[ebp+11*4] ;从堆栈中取得TCB的基地址
mov edi,[es:ebx+0x06] ;用户程序加载的基地址
mov edx,[es:edi+0x10] ;登记程序入口点(EIP)
mov [es:ecx+32],edx ;到TSS
mov dx,[es:edi+0x14] ;登记程序代码段(CS)选择子
mov [es:ecx+76],dx ;到TSS中
mov dx,[es:edi+0x08] ;登记程序堆栈段(SS)选择子
mov [es:ecx+80],dx ;到TSS中
mov dx,[es:edi+0x04] ;登记程序数据段(DS)选择子
mov word [es:ecx+84],dx ;到TSS中。注意,它指向程序头部段
mov word [es:ecx+72],0 ;TSS中的ES=0
mov word [es:ecx+88],0 ;TSS中的FS=0
mov word [es:ecx+92],0 ;TSS中的GS=0
pushfd
pop edx
mov dword [es:ecx+36],edx ;EFLAGS,这是当前任务(程序管理器)EFLAGS寄存器的副本,新任务将使用这个副本作为初始的EFLAGS
- 在GDT中登记TSS和LDT描述符
当
call far
和jmp far
指令的操作数是TSS描述符选择子时,处理器执行任务切换操作
;在GDT中登记LDT描述符
mov eax,[es:esi+0x0c] ;LDT的起始线性地址
movzx ebx,word [es:esi+0x0a] ;LDT段界限
mov ecx,0x00408200 ;LDT描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [es:esi+0x10],cx ;登记LDT选择子到TCB中
;在GDT中登记TSS描述符
mov eax,[es:esi+0x14] ;TSS的起始线性地址
movzx ebx,word [es:esi+0x12] ;段长度(界限)
mov ecx,0x00408900 ;TSS描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [es:esi+0x18],cx ;登记TSS选择子到TCB
- 加载LDTR以及TR
TR→当前任务的TSS,是任务的主要标志
LDTR→当前任务的LDT,在任务执行期间加速段的访问
ltr [ecx+0x18] ;加载任务状态段,处理器将该TSS描述符中的B位置为1,但并不执行任务切换
lldt [ecx+0x10] ;加载LDT
- 创建任务管理器
创建0特权级的内核任务,并将当前正在执行的内核代码段划归该任务。创建TSS,并将该TSS的描述符安装到GDT。同时,内核任务可以没有自己的LDT,因此可以将自己所使用的段描述符安装在GDT中。
;为程序管理器的TSS分配内存空间
mov ecx,104 ;为该任务的TSS分配内存
call sys_routine_seg_sel:allocate_memory
mov [prgman_tss+0x00],ecx ;保存程序管理器的TSS基地址
;在程序管理器的TSS中设置必要的项目
mov word [es:ecx+96],0 ;没有LDT。处理器允许没有LDT的任务。
mov word [es:ecx+102],103 ;没有I/O位图。0特权级事实上不需要。
mov word [es:ecx+0],0 ;反向链=0
mov dword [es:ecx+28],0 ;登记CR3(PDBR)
mov word [es:ecx+100],0 ;T=0
;不需要0、1、2特权级堆栈。0特级不
;会向低特权级转移控制。
;创建TSS描述符,并安装到GDT中
mov eax,ecx ;TSS的起始线性地址
mov ebx,103 ;段长度(界限)
mov ecx,0x00408900 ;TSS描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [prgman_tss+0x04],cx ;保存程序管理器的TSS描述符选择子
;任务寄存器TR中的内容是任务存在的标志,该内容也决定了当前任务是谁。
;下面的指令为当前正在执行的0特权级任务“程序管理器”后补手续(TSS)。
ltr cx
- 进行任务切换
任务切换的方法
多任务切换:
- 协同式:当前任务主动地请求暂时放弃执行权,或者在通过调用门请求操作系统服务时,由操作系统“趁机”将控制转移到另一个任务
- 抢占式:通过中断信号,在中断服务中实施任务切换。
处理器将控制转移到其他任务的四个方法:
- 当前程序、任务或者过程执行一个将控制转移到
GDT
内某个TSS
描述符的jmp
或者call
指令- 当前程序、任务或者过程执行一个将控制转移到
GDT
或者当前LDT
内某个任务门
描述符的jmp
或者call
指令在保护模式下,中断向量表不再使用,而是用中断描述符表,用于保存门描述符,包括中断门、陷阱门和任务门
其中p
位指示该门是否有效:0时,不允许通过此门实施任务切换;DPL
对因中断而发起的任务切换不起作用,处理器不按特权级施加任何保护,而对以非中断(即jmp、call:当前任务的CPL和新任务段选择子的RPL必须在数值上小于或等于目标TSS或者任务门的DPL
,符合数据访问的特权级检查规则)的方式通过任务门实施任务切换时有效。
- 一个异常或者中断发生时,中断号指向中断描述符表内的任务门
- 在EFLAGS寄存器的NT位置位的情况下,当前任务执行了一个
iret
指令
不同任务切换方式对B位、NT位和任务链接域的影响:
参考
《x86汇编语言:从实模式到保护模式》