TSS中保存的栈信息,是在特权级转移的时候被用到,具体就是在从低特权级转移到高特权级的时候会用。注意 从低到高
问题:为什么要保存 段寄存器值和通用寄存器的值?
既然是为了实现多任务,那么任务之间显然要进行切换,切换任务的时候需要保存当前任务上下文,那么什么是任务上下文? 具体就是任务执行时的关键寄存器的值,包括段寄存器和通用寄存器的值。把这些寄存器值保存之后,等任务切换回来的时候,直接把这些寄存器值恢复 任务的执行状态就恢复了。
TSS这个数据结构中很大一部分就是保存了寄存器的值的。
在X86处理器中,如果说特权级进行了转移,那么所使用的栈也会发生变化,每一个特权级使用自己一个独立的栈,不同特权级的栈是相互独立的。所以TSS中也保存了 任务所需要的不同特权级的栈信息,这些不同特权级的栈信息是怎么表示的呢? 其实就是保存 ss寄存器的值 和 esp寄存器的值。众所周知 栈需要两个寄存器来表示,第一个是ss寄存器 保存栈基地址,第二个是esp寄存器 保存栈顶地址。
这样保存之后的好处是什么?
很明显,假如我们现在要从 3特权级 跳转到 0特权级执行,发生特权级的转移了,要切换栈,此时所需要的 0特权级的栈信息 就可以直接到 TSS结构体中找就可以了。
注意 TSS结构体中 只保存了3个栈的信息 分别是特权级 0 1 2 的栈信息
调用门可以做 特权级的转移,从低特权级转移到高特权级,转移的时候栈的变化是这样的:
1 首先从 TSS中获取 高特权级栈的信息(包括栈基地址 ss寄存器值,栈顶指针位置 esp寄存器值)
2 获取之后,将 低特权级的栈信息(包括栈基地址 ss寄存器值,栈顶指针位置 esp寄存器值) 压入到 高特权级栈中,此操作的意义就是为了返回,函数调用完需要返回,返回的时候就从 高特权级转移到低特权级了,也就发生了特权级变化,那么栈就会发生变化。
回忆上一节知识,高特权级跳转到低特权级,我们在跳转的时候,手工的将低特权级栈的信息压入了栈中,并且将低特权级代码段入口压入栈中,然后使用 retf指令 。
如果是 调用门的话 在调用的瞬间,低特权级的栈信息以及低特权级的代码段信息 都会被压入栈中,这样 当遇到 retf指令的时候,就会将低特权级的栈信息 从高特权级的栈中取出来 恢复到 ss寄存器和 esp寄存器。
TSS中保存的栈信息,是在特权级转移的时候被用到,具体就是在从低特权级转移到高特权级的时候会用。注意 从低到高,而一共只有0 1 2 3,四个特权级,没有比3特权级更低的特权级,所以不可能有特权级从比3更低的特权级 转移到 3特权级。
注意:
1, 32位核心代码段和数据段 特权级为0 模拟内核态
2, 32位任务代码段和数据段 特权级为3 模拟用户态
3, 在系统启动后会首先执行内核态核心代码 之后就会跳转到 任务代码去执行,此时就是 高特权级 到 低特权级的转移(retf 远返回)。这里模拟的就是操作系统启动后去执行某个应用程序。模拟操作系统内核加载执行 应用程序
4, 在用户态的应用程序中 调用 内核高特权级代码,即系统函数。这个时候就涉及到了特权级转移,必然要陷入内核态,对应实验当中 就是使用调用门来做特权级的转移,完成某个任务,然后返回。
1 当系统开始执行了之后,显然是在实模式的,我们需要转换到保护模式执行,转换到保护模式之后,特权级为0,对应的就是核心代码段的执行。
2 核心代码段做好工作后,通过远返回执行指令 从高特权级0的内核态 跳转到 低特权级为3的用户态执行任务,模拟操作系统内核加载执行 应用程序
3 低特权级为3的用户态 执行任务中 需要调用一个系统函数,那么就要陷入内核态,这个过程的本质就是特权级转移了,通过调用门从3特权级的用户态转移到特权级为0的内核态,然后执行系统函数,执行完之后 又通过远返回执行 做特权级的转移,从特权级为0的内核态系统函数代码段 转移到 特权级为3的用户态任务代码段。
特权级转移时会发生栈变换,栈信息到TSS结构体中查找,TSS结构体存在于内存当中,既然TSS 要存在于内存当中,那么他就应该是保护模式下的一个段,所以必然要有相应的段描述符和选择子,但凡内存中的一个段 就会有相应的段描述符 和 选择子,TSS结构体也不例外。
在TSS结构体定义好之后,如何使用呢?
通过 ltr指令加载使用。
实验 :
实验说明:
32位保护模式下的
CODE32_DESC 代码段
DATA32_DESC 数据段
STACK32_DESC 栈段
特权级都是0,用来模拟 内核态,CODE32_DESC 模拟内核 核心代码段
32位保护模式下的 FUNCTION_DESC 特权级为0 用来模拟 内核态系统函数
32位保护模式下的
TASK_A_CODE32_DESC
TASK_A_DATA32_DESC
TASK_A_STACK32_DESC
特权级都是3 用来模拟用户态 任务代码段
makefile
; Segment Attribute
DA_32 equ 0x4000
DA_DR equ 0x90
DA_DRW equ 0x92
DA_DRWA equ 0x93
DA_C equ 0x98
DA_CR equ 0x9A
DA_CCO equ 0x9C
DA_CCOR equ 0x9E
; Segment Privilege
DA_DPL0 equ 0x00 ; DPL = 0
DA_DPL1 equ 0x20 ; DPL = 1
DA_DPL2 equ 0x40 ; DPL = 2
DA_DPL3 equ 0x60 ; DPL = 3
; Special Attribute
DA_LDT equ 0x82
DA_TaskGate equ 0x85 ; 任务门类型值
DA_386TSS equ 0x89 ; 可用 386 任务状态段类型值
DA_386CGate equ 0x8C ; 386 调用门类型值
DA_386IGate equ 0x8E ; 386 中断门类型值
DA_386TGate equ 0x8F ; 386 陷阱门类型值
; Selector Attribute
SA_RPL0 equ 0
SA_RPL1 equ 1
SA_RPL2 equ 2
SA_RPL3 equ 3
SA_TIG equ 0
SA_TIL equ 4
; 描述符
; usage: Descriptor Base, Limit, Attr
; Base: dd
; Limit: dd (low 20 bits available)
; Attr: dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3 ; 段基址, 段界限, 段属性
dw %2 & 0xFFFF ; 段界限1
dw %1 & 0xFFFF ; 段基址1
db (%1 >> 16) & 0xFF ; 段基址2
dw ((%2 >> 8) & 0xF00) | (%3 & 0xF0FF) ; 属性1 + 段界限2 + 属性2
db (%1 >> 24) & 0xFF ; 段基址3
%endmacro ; 共 8 字节
; 门
; usage: Gate Selector, Offset, DCount, Attr
; Selector: dw
; Offset: dd
; DCount: db
; Attr: db
%macro Gate 4
dw (%2 & 0xFFFF) ; 偏移地址1
dw %1 ; 选择子
dw (%3 & 0x1F) | ((%4 << 8) & 0xFF00) ; 属性
dw ((%2 >> 16) & 0xFFFF) ; 偏移地址2
%endmacro
loader.asm
%include "inc.asm"
org 0x9000
jmp ENTRY_SEGMENT
[section .gdt]
; GDT definition
; 段基址, 段界限, 段属性
GDT_ENTRY : Descriptor 0, 0, 0
CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 + DA_DPL0
;注意显存段特权级为3
VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32 + DA_DPL3
DATA32_DESC : Descriptor 0, Data32SegLen - 1, DA_DR + DA_32 + DA_DPL0
STACK32_DESC : Descriptor 0, TopOfStack32, DA_DRW + DA_32 + DA_DPL0
;32位保护模式下的代码段,特权级0,模拟系统函数
FUNCTION_DESC : Descriptor 0, FunctionSegLen - 1, DA_C + DA_32 + DA_DPL0
;局部段描述附表,该段中定义 特权级为3的用户态 任务代码,模拟务代码段
TASK_A_LDT_DESC : Descriptor 0, TaskALdtLen - 1, DA_LDT + DA_DPL0
;TSS任务状态段 段描述符
TSS_DESC : Descriptor 0, TSSLen - 1, DA_386TSS + DA_DPL0
; Gate Descriptor 调用门描述符
;定义调用门描述符,对应 FUNCTION_DESC 代码段中两个函数的入口地址
;该调用门描述符中保存 FUNCTION_DESC段(函数段)的选择子,以及段内偏移地址(PrintString)(指的是该代码段中某个函数的入口地址)
;参数个数为0 表明我们没有使用栈来传递参数,而是使用寄存器来传递参数
;属性 是 调用门类型值 DA_386CGate equ 0x8C ; 386 调用门类型值,而特权级只能定义为3,因为他是给应用程序调用的,太高了应用程序就没权限使用它了
; 选择子, 偏移, 参数个数, 属性
FUNC_PRINTSTRING_DESC : Gate FunctionSelector, PrintString, 0, DA_386CGate + DA_DPL3
; GDT end
GdtLen equ $ - GDT_ENTRY
GdtPtr:
dw GdtLen - 1
dd 0
; GDT Selector
Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0
VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL3
Data32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0
Stack32Selector equ (0x0004 << 3) + SA_TIG + SA_RPL0
FunctionSelector equ (0x0005 << 3) + SA_TIG + SA_RPL0
TaskALdtSelector equ (0x0006 << 3) + SA_TIG + SA_RPL0
;TSS任务状态段 段描述符选择子
TSSSelector equ (0x0007 << 3) + SA_TIG + SA_RPL0
; Gate Selector
FuncPrintStringSelector equ (0x0008 << 3) + SA_TIG + SA_RPL3
; end of [section .gdt]
;任务状态段 本质上也是一块内存,所以也需要遵循32位保护模式的编程规则,需要定义对应的段描述符+选择子
[section .tss]
[bits 32]
TSS_SEGMENT:
dd 0
dd TopOfStack32 ; 0 特权级对应的栈信息 栈顶指针
dd Stack32Selector ; 0 特权级对应的栈信息 栈段基指针
dd 0 ; 1 特权级对应的栈信息 暂时为空
dd 0 ;
dd 0 ; 2 特权级对应的栈信息 暂时为空
dd 0 ;
times 4 * 18 dd 0 ; 往下全部定义为0,因为次实验不涉及多任务切换,所以不需要使用那些保存寄存器值的字段
dw 0
dw $ - TSS_SEGMENT + 2
db 0xFF ; 结束符
TSSLen equ $ - TSS_SEGMENT
TopOfStack16 equ 0x7c00
[section .s16]
[bits 16]
ENTRY_SEGMENT:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, TopOfStack16
; initialize GDT for 32 bits code segment
mov esi, CODE32_SEGMENT
mov edi, CODE32_DESC
call InitDescItem
mov esi, DATA32_SEGMENT
mov edi, DATA32_DESC
call InitDescItem
mov esi, STACK32_SEGMENT
mov edi, STACK32_DESC
call InitDescItem
mov esi, FUNCTION_SEGMENT
mov edi, FUNCTION_DESC
call InitDescItem
;初始化 任务A的局部段描述符表 的段描述符的段基址信息
mov esi, TASK_A_LDT_ENTRY
mov edi, TASK_A_LDT_DESC
call InitDescItem
;初始化 局部段描述符表中的 数据段 的 段描述符的段基址信息
mov esi, TASK_A_DATA32_SEGMENT
mov edi, TASK_A_DATA32_DESC
call InitDescItem
;初始化 局部段描述符表中的 可执行代码段 的 段描述符的段基址信息
mov esi, TASK_A_CODE32_SEGMENT
mov edi, TASK_A_CODE32_DESC
call InitDescItem
;初始化 局部段描述符表中的 栈段 的 段描述符的段基址信息
mov esi, TASK_A_STACK32_SEGMENT
mov edi, TASK_A_STACK32_DESC
call InitDescItem
;初始化 TSS 段描述符段基址信息
mov esi, TSS_SEGMENT
mov edi, TSS_DESC
call InitDescItem
; initialize GDT pointer struct
mov eax, 0
mov ax, ds
shl eax, 4
add eax, GDT_ENTRY
mov dword [GdtPtr + 2], eax
; 1. load GDT
lgdt [GdtPtr]
; 2. close interrupt
cli
; 3. open A20
in al, 0x92
or al, 00000010b
out 0x92, al
; 4. enter protect mode
mov eax, cr0
or eax, 0x01
mov cr0, eax
; 5. jump to 32 bits code
jmp dword Code32Selector : 0
; esi --> code segment label
; edi --> descriptor label
InitDescItem:
push eax
mov eax, 0
mov ax, cs
shl eax, 4
add eax, esi
mov word [edi + 2], ax
shr eax, 16
mov byte [edi + 4], al
mov byte [edi + 7], ah
pop eax
ret
[section .dat]
[bits 32]
DATA32_SEGMENT:
DTOS db "D.T.OS!", 0
DTOS_OFFSET equ DTOS - $$
Data32SegLen equ $ - DATA32_SEGMENT
[section .s32]
[bits 32]
CODE32_SEGMENT:
mov ax, VideoSelector
mov gs, ax
mov ax, Data32Selector
mov ds, ax
mov ax, Stack32Selector
mov ss, ax
mov eax, TopOfStack32
mov esp, eax
;打印内核数据段当中的字符串,用于说明已经执行核心代码段
mov ebp, DTOS_OFFSET
mov bx, 0x0c
mov dh, 12
mov dl, 33
;通过 段选择子:段内偏移地址 的方式 调用打印函数
;不发生特权级转移,因为本段内存段特权级 与 FunctionSelector段特权级 都是0
call FunctionSelector : PrintString
;加载 TSS 结构体
mov ax, TSSSelector
ltr ax
;加载局部段描述符表
mov ax, TaskALdtSelector
lldt ax
;从高特权级内核态核心代码段 跳转到 低特权级用户态任务代码段
;模拟操作系统内核加载执行 应用程序
;将关键寄存器压入栈
;将低特权级用户态任务代码段的栈信息 压入到 当前内核态核心代码段的高特权级栈中
push TaskAStack32Selector ;栈选择子
push TaskATopOfStack32 ;栈顶地址
;将低特权级用户态任务代码段的选择子,即入口地址 压入到 当前内核态核心代码段的高特权级栈中
push TaskACode32Selector
;将低特权级用户态任务代码段中目标代码的 偏移地址 0 压入 当前内核态核心代码段的高特权级栈中
push 0
;跳转到 低特权级用户态任务代码段,此时就会发生特权级转移
retf
Code32SegLen equ $ - CODE32_SEGMENT
[section .gs]
[bits 32]
STACK32_SEGMENT:
times 1024 * 4 db 0
Stack32SegLen equ $ - STACK32_SEGMENT
TopOfStack32 equ Stack32SegLen - 1
; ==========================================
;
; Global Function Segment
; 模拟内核系统函数,从操作系统的角度看 这就是内核中的函数
; ==========================================
[section .func]
[bits 32]
FUNCTION_SEGMENT:
; ds:ebp --> string address
; bx --> attribute
; dx --> dh : row, dl : col
PrintStringFunc:
push ebp
push eax
push edi
push cx
push dx
print:
mov cl, [ds:ebp]
cmp cl, 0
je end
mov eax, 80
mul dh
add al, dl
shl eax, 1
mov edi, eax
mov ah, bl
mov al, cl
mov [gs:edi], ax
inc ebp
inc dl
jmp print
end:
pop dx
pop cx
pop edi
pop eax
pop ebp
retf
PrintString equ PrintStringFunc - $$
FunctionSegLen equ $ - FUNCTION_SEGMENT
; ==========================================
;
; Task A Code Segment
;
; ==========================================
[section .task-a-ldt]
; Task A LDT definition
; 段基址, 段界限, 段属性
TASK_A_LDT_ENTRY:
TASK_A_CODE32_DESC : Descriptor 0, TaskACode32SegLen - 1, DA_C + DA_32 + DA_DPL3
TASK_A_DATA32_DESC : Descriptor 0, TaskAData32SegLen - 1, DA_DR + DA_32 + DA_DPL3
TASK_A_STACK32_DESC : Descriptor 0, TaskAStack32SegLen - 1, DA_DRW + DA_32 + DA_DPL3
TaskALdtLen equ $ - TASK_A_LDT_ENTRY
; Task A LDT Selector
TaskACode32Selector equ (0x0000 << 3) + SA_TIL + SA_RPL3
TaskAData32Selector equ (0x0001 << 3) + SA_TIL + SA_RPL3
TaskAStack32Selector equ (0x0002 << 3) + SA_TIL + SA_RPL3
[section .task-a-dat]
[bits 32]
TASK_A_DATA32_SEGMENT:
TASK_A_STRING db "This is Task A!", 0
TASK_A_STRING_OFFSET equ TASK_A_STRING - $$
TaskAData32SegLen equ $ - TASK_A_DATA32_SEGMENT
;用户态低特权级任务代码段的 栈 大小1024Byte
[section .task-a-gs]
[bits 32]
TASK_A_STACK32_SEGMENT:
times 1024 db 0
TaskAStack32SegLen equ $ - TASK_A_STACK32_SEGMENT
;栈顶
TaskATopOfStack32 equ TaskAStack32SegLen - 1
;模拟用户态低特权级 任务代码段
[section .task-a-s32]
[bits 32]
TASK_A_CODE32_SEGMENT:
;设置 用户态任务数据段寄存器
mov ax, TaskAData32Selector
mov ds, ax
;打印函数参数,通过寄存器传参,没有用栈传参
mov ebp, TASK_A_STRING_OFFSET
mov bx, 0x0c
mov dh, 14
mov dl, 29
;通过调用门描述符的选择子 调用高特权级的目标函数
;说明 这里的0是语法需要 表示是段间跳转 0本身无意义,但是不能删除
;如果删除后 就变成了 call FuncPrintStringSelector,成了段内跳转,意义本身发生变化,所以必须留着
;通过调用门描述符的选择子 调用高特权级的目标函数 那么此时就会发生特权级转移,而特权级转移的时候栈
;也会变化,所以我们就要找到高特权级的栈信息,到TSS中去找高特权级的栈信息
call FuncPrintStringSelector : 0
jmp $
TaskACode32SegLen equ $ - TASK_A_CODE32_SEGMENT
关于显存段特权级设置为3的合理性分析:
将显存段特权级设置为3,即便应用程序向显存段中写入了乱七八糟的数据,后果最多就是屏幕上显示乱七八糟的数据,不会有更严重的后果,可以接受
将机器码 反编译成 32位的汇编代码
ndisasm -b 32 -o 0x9000 loader > loader.txt
可以定位到 特权级切换前后,查看关键寄存器值 CS寄存器值,CS寄存器最低2位 为CPL 即特权级。可证明跳转成功。
如果说 当前所执行的代码段的 CPL 为 0,那是不是就意味着 他可以访问任何资源了呢?
答案是否定的,因为还要看它的请求特权级 RPL,如果太低 也是不能够访问对应资源的。
当前代码想要访问某个放在数据段中的资源,于是就要去做请求,通过选择子进行请求,处理器通过 CPL RPL DPL 共同确定该请求是否合法,如果合法就放行,可以访问数据段中的资源。