一,TSS简介
我们知道在进行任务上下文切换的时候,往往需要保存上一个任务的状态,而我们保存状态是在栈中进行保存。实际上我们可以使用一个专门的结构来保存维护任务的内容,当下次要调度时就从该地方取出保存的任务状态,而这个东西就是TSS。
当任务发生切换时,CPU将当前的旧任务状态态存入当前任务的TSS,再将新任务的TSS载入到相应的寄存器中,这便实现了任务切换。
TR寄存器:TR寄存器始终指向当前正在运行的任务,专门存储TSS信息的寄存器,任务切换本质就是TR寄存器指向不同的TSS
ltr "16位寄存器or内存单元" ;tss加载到寄存器TR
GDT注册:我们要从某一个地方保存相应的TSS,那么这个地方肯定就是内存,既然要使用内存就必须要要去TSS中注册,这样才能找到TSS
① TSS描述符
- S:s默认为0,因为他是系统段
- B:表示busy,为0代表任务不繁忙,为1代表任务繁忙(1.正在CPU上执行,2.嵌套调用)
B位不仅仅是表达是否繁忙,还表达不可重入,即当前任务不能嵌套调用自己,因为在同一个TSS中保存后再载入就会导致严重错误。
② TSS
esp0~2:代表的是0~2特权级下的栈,为什么没有3特权级,因为除了中断和调用返回以外,CPU不允许高特权级转向低特权级,该栈中的数值基本上是一成不变的。
CPU切换TSS的流程:
TSS被换下CPU时,CPU自动地把当前任务的资源状态保存到该任务对应的TSS中,CPU通过新任务的TSS选择子寻找任务时,会把该TSS中的数据载入到CPU的寄存器中,同时用TSS更新TR,以上操作都是自动完成的,但是第一个任务的TSS需要手工加载。
③ CPU原生支持的任务切换方式
ps:(咋们用不到,不想了解的可以跳到④)
我们使用的任务切换方式其实并不是CPU原生的任务切换方式,CPU提供了TSS与LDT来供开发者进行任务切换,由于我们不用到LDT且任务切换的关键是TSS,我们就跳过LDT
首先要回忆一下任务门,任务门我们之前说过,里面存放的是TSS,且可以存放到IDT中,如果发生中断,描述符是中断描述符就执行中断程序,如果是任务门就执行任务切换
- 中断+任务门
实现简单,抢占式多任务调度,就是我们使用的时钟中断+schedule进行调度,只不过我们用的是中断描述符不是任务门
- call 或 jmp+任务门
④ 基于TSS任务切换的缺点
说了这么多,为什么Linux并没用这一套呢?
- 耗时长,不简洁:可以看到上述步骤多达10步,光是加载,保存,设置B位,设置寄存器eflags就要耗费CPU很多精力,而且call和jmp指令实现任务切换的时钟都是以百为单位的。
- GDT数量有限:GDT本身只能存储8192个描述符,数量有限,任务增加就要占用一个描述符,当任务减少时你还需要及时删减描述符
这时候有的同学就要问了,好家伙我搁着看了半天的TSS,结果TSS毛病这么多,那讲这些有啥用呢?
还记不记得TSS的另一个作用,存储特权级栈,没错我们要用到的便是他的这个功能,我们仿照Linux那样内核处于特权级0,用户处于特权级3,TSS给0特权级任务提供栈
我们要做的是,为每个CPU创建一个TSS,各个CPU上的所有任务共享一个TSS,在一开始的时候使用ltr指令加载TSS,TR便永远指向一个TSS,任务切换的时候也只修改TSS中的ss0与esp0。
所以说 在Linux中TSS只初始化了SS0,esp0和IO位图,TSS便没有作用了,不进行任务状态保存。
二,初始化TSS为用户进程披荆斩棘
首先我们先来在global.h文件中增加一些内容
#ifndef _KERNEL_GLOBAL_H
#define _KERNEL_GLOBAL_H
#include "stdint.h"
#define RPL0 0
#define RPL1 1
#define RPL2 2
#define RPL3 3
#define TI_GDT 0
#define TI_LDT 1
//-------------- GDT描述符属性 ------------
#define DESC_G_4K 1
#define DESC_D_32 1
#define DESC_L 0
#define DESC_AVL 0
#define DESC_P 1
#define DESC_DPL_0 0
#define DESC_DPL_1 1
#define DESC_DPL_2 2
#define DESC_DPL_3 3
#define DESC_S_CODE 1
#define DESC_S_DATA DESC_S_CODE
#define DESC_S_SYS 0
#define DESC_TYPE_CODE 8
#define DESC_TYPE_DATA 2
#define DESC_TYPE_TSS 9
/*内核态*/
#define SELECTOR_K_CODE ((1<<3)+(TI_GDT<<2) + RPL0) //代码
#define SELECTOR_K_DATA ((2<<3)+(TI_GDT<<2) + RPL0) //数据
#define SELECTOR_K_STACK SELECTOR_K_DATA
#define SELECTOR_K_GS ((3<<3)+(TI_GDT<<2) + RPL0) //显存
/*这里是TSS*/
/*USER段*/
#define SELECTOR_U_CODE ((5 << 3) + (TI_GDT << 2) + RPL3) //用户态 代码选择子
#define SELECTOR_U_DATA ((6 << 3) + (TI_GDT << 2) + RPL3) //用户态 数据选择子
#define SELECTOR_U_STACK SELECTOR_U_DATA //用户态 栈
/*用户态的GDT描述符*/
#define GDT_ATTR_HIGH ((DESC_G_4K << 7) + (DESC_D_32 << 6) + (DESC_L << 5) + (DESC_AVL << 4))
#define GDT_CODE_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_CODE << 4) + DESC_TYPE_CODE)
#define GDT_DATA_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_DATA << 4) + DESC_TYPE_DATA)
#define IDT_DESC_P 1
#define IDT_DESC_DPL0 0
#define IDT_DESC_DPL3 3
#define IDT_DESC_32_TYPE 0xE
#define IDT_DESC_16_TYPE 0x6
#define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P<<7) + (IDT_DESC_DPL0<<5) +IDT_DESC_32_TYPE)
#define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P<<7) + (IDT_DESC_DPL3<<5) +IDT_DESC_32_TYPE
//-------------- TSS描述符属性 ------------
#define TSS_DESC_D 0
#define TSS_ATTR_HIGH ((DESC_G_4K << 7) + (TSS_DESC_D << 6) + (DESC_L << 5) + (DESC_AVL << 4) + 0X0)
#define TSS_ATTR_LOW ((DESC_P << 7) + (DESC_DPL_0 << 5) + (DESC_S_SYS << 4) + DESC_TYPE_TSS)
#define SELECTOR_TSS ((4 << 3) + (TI_GDT << 2) + RPL0) //对应上面的这里是TSS,他是第四位
/*描述符结构*/
struct gdt_desc
{
uint16_t limit_low_word;
uint16_t base_low_word;
uint8_t base_mid_byte;
uint8_t attr_low_byte;
uint8_t limit_high_attr_high;
uint8_t base_high_byte;
};
#define NULL 0
#endif // ! _KERNNEL_GLOBAL_H
编写TSS模块
新建一个userprog文件夹,用来存放用户态文件,于此同时我们之前gdt初始化是在汇编里面完成的,但是我们已经写C语言写习惯了,便在tss中实现一个gdt构建函数并且注册到gdt中(按道理这个模块应该在gdt中的),然后还要牢记一点就是你本身gdt的位置,我的位置是0x00000903,如果你的位置和我不一样要记得更改(可以在bochs调试中利用info gdt查看)
此次要加载三个gdt描述符(TSS,用户代码,用户数据)
目前为止我们的GDT描述符有以下这些
- 0 空描述符,GDT本身规定
- 1 内核代码段
- 2 内核数据段
- 3 显存段
- 4 TSS
- 5 用户代码段
- 6 用户数据段
userprog/tss.c
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "tss.h"
#include "print.h"
#include "global.h"
struct tss
{
uint32_t backlink;
uint32_t* esp0;
uint32_t ss0;
uint32_t* esp1;
uint32_t ss1;
uint32_t* esp2;
uint32_t ss2;
uint32_t cr3;
uint32_t(*eip) (void);
uint32_t eflags;
uint32_t eax;
uint32_t ecx;
uint32_t edx;
uint32_t ebx;
uint32_t esp;
uint32_t ebp;
uint32_t esi;
uint32_t edi;
uint32_t es;
uint32_t cs;
uint32_t ss;
uint32_t ds;
uint32_t fs;
uint32_t gs;
uint32_t ldt;
uint32_t trace;
uint32_t io_base;
};
struct tss tss;
void update_tss_esp(struct task_struct* pthread) {
tss.esp0 = (uint32_t*)((uint32_t)pthread + PG_SIZE);
}
struct gdt_desc make_gdt_desc(uint32_t* desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high) {
struct gdt_desc desc;
uint32_t desc_base = (uint32_t)desc_addr;
desc.limit_low_word = limit & 0x0000ffff;
desc.base_low_word = desc_base & 0x0000ffff;
desc.base_mid_byte = ((desc_base & 0x00ff0000) >> 16);
desc.attr_low_byte = (uint8_t)(attr_low);
desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t)(attr_high));
desc.base_high_byte = desc_base >> 24;
return desc;
}
void tss_init()
{
put_str("tss_init start\n");
uint32_t tss_size = sizeof(tss);
memset(&tss, 0, tss_size);
tss.ss0 = SELECTOR_K_STACK;
tss.io_base = tss_size;
*((struct gdt_desc*)0xc0000923) = make_gdt_desc((uint32_t*)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH); //第四个gdt位置为TSS
*((struct gdt_desc*)0xc000092b) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH); //第五个gdt位置为用户代码块
*((struct gdt_desc*)0xc0000933) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH); //第六个gdt位置为用户数据块
uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)0xc0000903 << 16));
asm volatile("lgdt %0" :: "m"(gdt_operand));
asm volatile("ltr %w0" :: "r"(SELECTOR_TSS));
put_str("tss_init and ltr done\n");
}
userprog/tss.h
#ifndef __USERPROG_TSS_H
#define __USERPROG_TSS_H
#include "thread.h"
#include "stdint.h"
void updata_tss_esp(struct task_struct* pthread);
struct gdt_desc make_gdt_desc(uint32_t* desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high);
void tss_init(void);
#endif
别忘记在init.c中添加tss_init()函数,这里我就不贴出来了
运行结果如下:
三, 用户进程
① 用户进程虚拟空间
现在开始我们要来编写用户进程,首先要明确一点,用户进程与内核线程的最大区别就是:用户进程拥有自己单独的4GB空间(虚拟空间),所以我们要先在task_struct中加入一个成员变量
/*PCB*/
struct task_struct {
uint32_t* self_kstack;
enum task_status status;
char name[16];
uint8_t priority;
uint8_t ticks; //每次在处理器上执行的时间
uint32_t elapsed_ticks; //此任务已经执行的时间
struct list_elem general_tag; //线程在一般队列中的节点
struct list_elem all_list_tag; //线程队列thread_all_list的节点
uint32_t* pgdir; //进程自己页表的虚拟地址
struct virtual_addr userprog_vaddr; //用户进程虚拟地址
uint32_t stack_magic; //标记栈溢出
};
② 为进程创建页表和3特权级栈
我们在创建进程的过程中需要为每个进程单独创建一个页表,页表则是指"页目录表+页表",所以我们需要为每个进程单独申请存储页目录项及页表项的虚拟内存页
kernel/memory.c 新增部分
struct pool {
struct bitmap pool_bitmap;
uint32_t phy_addr_start; //内存池的管理物理内存的起始地址
uint32_t pool_size; //内存池字节容量
struct lock lock;
};
static void* vaddr_get(enum pool_flags pf, uint32_t pg_cnt) {
int vaddr_start = 0, bit_idx_start = -1;
uint32_t cnt = 0;
//1,根据你的pflags决定获取哪个池子
if (pf == PF_KERNEL) {
bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
if (bit_idx_start == -1) {
return NULL;
}
//根据页数去填充bitmap
while (cnt < pg_cnt) {
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
}
//返回虚拟分配后的虚拟地址 注意bitmap中一个bit位代表一页 4KB
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
}
else {
struct task_struct* cur = running_thread();
bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap, pg_cnt);
if (bit_idx_start == -1) {
return NULL;
}
while (cnt < pg_cnt) {
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
}
vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE));
}
return (void*)vaddr_start;
}
/*在用户空间中申请4K内存*/
void* get_user_pages(uint32_t pg_cnt) {
try_lock(&user_pool.lock);
void* vaddr = malloc_page(PF_USER, pg_cnt);
memset(vaddr, 0, pg_cnt * PG_SIZE);
try_release(&user_pool.lock);
return vaddr;
}
/*将地址vaddr与pf池中的物理地址关联,仅支持一页空间分配*/
void* get_a_page(enum pool_flags pf, uint32_t vaddr) {
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
try_lock(&mem_pool->lock);
struct task_struct* cur = running_thread();
int32_t bit_idx = -1;
if (cur->pgdir != NULL && pf = PF_USER) {
bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
ASSERT(bit_idx > 0);
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1);
}
else if (cur->pgdir == NULL && pf == PF_KERNEL) {
bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
ASSERT(bit_idx > 0);
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx, 1);
}
void* page_phyaddr = palloc(mem_pool);
if (page_phyaddr == NULL) {
return NULL;
}
page_table_add((void*)vaddr, page_phyaddr);
try_release(&mem_pool->lock);
return (void*)vaddr;
}
/*得到虚拟地址映射得到的物理地址*/
uint32_t addr_v2p(uint32_t vaddr) {
uint32_t* pte = pte_ptr(vaddr);
return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));
}
static void mem_pool_init(uint32_t all_mem) {
//...
lock_init(&kernel_pool.lock);
lock_init(&user_pool.lock);
}
③ 进入特权级3
我们知道从0特权级进入3特权级只有中断或调用返回才能做到,但是我们当前一直处于0特权级,甚至没有用户进程,我们该怎么进入特权级3呢?
这个时候我们可以动一点歪心思,来骗过CPU。在用户进程运行之前,假装我们一开始就是处于中断环境 ,然后便“假装”从中断返回。由此我们来说一下5个进入中断环境的要点:
1. 从中断返回我们要使用iret指令,在我们之前写kernel.S文件时,已经有一个intr_exit函数,在intr_exit函数中通过pop指令,来将栈中的数据返回至CPU寄存器中。
2. 提前准备好栈结构以及栈结构中的数据,栈结构即intr_stack。
3. 由于当前的特权级是由CPU的RPL来决定 即 CS.RPL,所以要在栈中保存CS选择子要被加载刀代码段寄存器中。
4. 由于用户进程的特权级为3,用户进程只能访问DPL为3的内存段(代码,数据,栈),因此我们栈中的寄存器选择子必须指向DPL为3的内存段。
5. 对于可屏蔽中断来说,任务之所以能进入中断,是因为IF位为1,退出中断后,还得保存IF位为1,响应新的中断。
6. 用户进程特权级最低,不允许访问硬件,只允许操作系统访问硬件,所以elfags的IOPL位为0
④ 用户进程创建流程
⑤ 构造上下文环境
kernel/global.h
#define NULL 0
#define PG_SIZE 4096
#define EFLAGS_MBS (1<<1)
#define EFLAGS_IF_1 (1<<9)
#define EFLAGS_IF_0 0
#define EFLAGS_IOPL_3 (3<<12)
#define EFLAGS_IOPL_0 (0<<12)
#define DIV_ROUND_UP(X,STEP) ((X+STEP-1)/(STEP))
userprog/process.c(含代码分析)
#include "global.h"
#include "stdint.h"
#include "thread.h"
#include "memory.h"
#include "tss.h"
extern void intr_exit(void);
/*构建用户的初始上下文信息*/
void start_process(void* filename_) {
void* function = filename_;
struct task_struct* cur = running_thread();
cur->self_kstack += sizeof(struct thread_stack);
struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;
proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
proc_stack->gs = 0; //显存段寄存器不允许用户访问
proc_stack->ds = proc->es = proc_stack->fs = SELECTOR_U_DATA;
proc_stack->eip = function;
proc_stack->cs = SELECTOR_U_CODE;
proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);
proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);
proc_stack->ss = SELECTOR_U_DATA;
asm volatile ("movl %0, %%esp; jmp intr_exit": : "g" (proc_stack) : "memory");
}
/*激活页表*/
void page_dir_activate(struct task_struct* pthread) {
uint32_t pagedir_phy_addr = 0x100000;
if (pthread->pgdir != NULL) {
pagedir_phy_addr = addr_v2p((uint32_t)pthread->pgdir);
}
asm volatile("movl %0,%%cr3" : : "r" (pagedir_phy_addr) : "memory");
}
/*激活线程或进程的页表,更新tss中的esp0为进程的特权级0栈*/
void process_activate(struct task_struct* pthread) {
ASSERT(pthread != NULL);
page_dir_activate(pthread);
if (pthread->pgdir) {
update_tss_esp(pthread);
}
}
代码分析
cur->self_kstack += sizeof(struct thread_stack); struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;
为了还原中断环境,我们需要将栈指针移动到intr_stack栈底层,如下图所示
C程序内存分布
紧接着我们来介绍一些C程序的内存分布
在汇编源码中含有关键字section或segment,相同的 节section 合成 段segment,至于这么做的原因是因为方便保护模式下的安全检测以及操作系统加载程序时省事(具体的在0day有说)
按照各种功能可以划分为以下几种类型:
(1) 可读写的数据,数据节.data和未初始化节.bss
(2) 只读可执行的代码 .text,.init
(3)只读数据 .rodata
我们知道我们用户态的内存最高地址是0xc0000000-1,所以我们仿照C应用程序内存空间,0xc000000-1以下为命令行参数与环境变量,再往下便是栈等等,由于命令参数和环境变量都是被压入用户栈的,所以位于栈的最高处,按照上述说法为我们的用户进程分配空间,我们的用户栈栈底地址为0xc0000000,所以我们申请栈地址应该是0xc00000000-0x1000这个是用户栈空间栈顶的下边界。
proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);
接下来我们来聊聊虚拟空间隔离,众所周之页表存储再页表寄存器CR3中,CR3寄存器只有一个,所以进程切换的时候我们要换上与之配套的页表实现虚拟地址空间隔离。
那实际上线程也是要被替换的,有人就可能说了,线程没有独立的地址空间哪来的页表呢,不应该只有进程才有吗,实际上在我们的操作系统里面,线程是为内核服务的,享用的是内核的页表,所以应该替换为0x100000
uint32_t pagedir_phy_addr = 0x100000; if (pthread->pgdir != NULL) { pagedir_phy_addr = addr_v2p((uint32_t)pthread->pgdir); } asm volatile("movl %0,%%cr3" : : "r" (pagedir_phy_addr) : "memory"); }
⑥ bss简介
在前面我们有简单介绍C程序内存分布,.text段是代码段,.data段是数据段,里面存访的是程序运行时的数据。
那么.bss是什么呢,bss是程序未初始化的全局变量和局部静态变量, 也就是说程序运行之初他们并没有值或者意义,等运行之后再附上初值,虽然起初是用不上的,但是我们也需要为这些未初始化的数据预留内存空间
总结:bss的内容是未初始化的数据,它们是变量,他们的意义是在运行过程中才产生的,不占文件大小,只在内存中存在。
⑦ 实现用户进程
写用户进程之前,我们首先来回顾一下内存划分
高1G是内核态,低3G是用户态,而且我们知道操作系统一开始的内存是固定好的,所以我们可以让所有用户进程共享这1G的内核内存