目录
现在,我们做好了TSS的工作,就可以激流勇进,准备实现一个用户进程了。现在,"用户进程"这个词终于不是梦了。
第一步:添加用户进程虚拟空间
进程与内核线程最大的区别是进程有单独的 4GB 空间,这指的是虚拟地址,物理地址空间可未必 有那么大,看似无限的虚拟地址经过分页机制之后,最终要落到有限的物理页中。 每个进程都拥有 4GB 的虚拟地址空间,虚拟地址连续而物理地址可以不连续,这就是保护模式下分页机制的优势。
我们需要单独为每个进程维护一个虚拟地址池,用此地址池来记录该进程 的虚拟中,哪些已被分配,哪些可以分配
我们添加一行成员给我们的TaskStruct:
cpp
/**
* @brief Task control block structure.
*
* This structure represents a thread and stores its execution context.
*/
typedef struct __cctaskstruct
{
uint32_t *self_kstack; // Kernel stack pointer for the thread
TaskStatus status; // Current status of the thread
char name[TASK_NAME_ARRAY_SZ]; // Thread name
uint8_t priority; // Thread priority level
uint8_t ticks;
uint32_t elapsed_ticks;
list_elem general_tag;
list_elem all_list_tag;
uint32_t *pg_dir;
VirtualAddressMappings userprog_vaddr;
uint32_t stack_magic; // Magic value for stack overflow detection
} TaskStruct;
就是VirtualAddressMappings userprog_vaddr;
,这是我们维护用户进程的虚拟地址池。
我们马上为进程创建页表和用户特权级栈
cpp
// Physical Memory Pools definition
// The 'MemoryPool' structure represents a physical memory pool, managing the pool's bitmap,
// the starting address of the physical memory, and the pool's total size in bytes.
typedef struct __mem_pool
{
Bitmap pool_bitmap; // Bitmap used by this pool to track allocated and free memory pages
uint32_t phy_addr_start; // The start address of the physical memory managed by this pool
uint32_t pool_size; // Total size of this memory pool in bytes
CCLocker lock;
} MemoryPool;
看到我们给memory.c中添加的MemoryPool了嘛,就是这样,我们需要确保内存申请的互斥。这个没啥好说的,注意,需要在内存池初始化的函数添加锁的初始化:
cpp
// Function to initialize the physical memory pools
static void init_memory_pool(const uint32_t all_memory)
{
...
// init lockers
lock_init(&kernel_pool.lock);
lock_init(&user_pool.lock);
...
verbose_ccputs(" memory pool init done\n");
}
我们需要改进一下virtual_memory的分配,那里我们留下了一个坑:
cpp
/* Allocate virtual memory pages from the specified pool (kernel or user).
* If successful, return the starting virtual address; otherwise, return NULL. */
static void *vaddr_get(const PoolFlag pf, const uint32_t pg_cnt)
{
int vaddr_start = 0, bit_idx_start = -1;
uint32_t cnt = 0;
switch (pf) // Determine which pool to allocate from based on the PoolFlag
{
case PF_KERNEL: // If the pool is the kernel memory pool
{
bit_idx_start = bitmap_scan(&kernel_vaddr.virtual_mem_bitmap, pg_cnt); // Find the first free block of 'pg_cnt' pages
if (bit_idx_start == -1)
{
return NULL; // If no free block is found, return NULL
}
while (cnt < pg_cnt)
{
bitmap_set(&kernel_vaddr.virtual_mem_bitmap, bit_idx_start + cnt, 1); // Mark the pages as allocated in the bitmap
cnt++; // Increment the page count
}
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE; // Calculate the starting virtual address of the allocated pages
}break;// Exit the case block
case PF_USER: // If the pool is the user memory pool
{
TaskStruct *cur = current_thread();
bit_idx_start = bitmap_scan(&cur->userprog_vaddr.virtual_mem_bitmap, pg_cnt);
if (bit_idx_start == -1)
{
return NULL;
}
while (cnt < pg_cnt)
{
bitmap_set(&cur->userprog_vaddr.virtual_mem_bitmap, bit_idx_start + cnt++, 1);
}
vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
/* (KERNEL_V_START - PG_SIZE) is already allocated as the user-level 3rd
* stack in start_process */
KERNEL_ASSERT((uint32_t)vaddr_start < (KERNEL_V_START - PG_SIZE));
}
}
return (void *)vaddr_start; // Return the starting virtual address of the allocated pages
}
重点看case PF_USER
的部分,这个是我们新增的。这里的逻辑是一样的,我们就不多述了
之后是我们新增加的函数:
cpp
/* Allocate 4KB of memory from the user space and return the corresponding
* virtual address */
void *get_user_pages(uint32_t pg_cnt) {
lock_acquire(&user_pool.lock);
void *vaddr = malloc_page(PF_USER, pg_cnt);
if (vaddr != NULL) { // If the allocated address is not NULL, clear the
// pages before returning
k_memset(vaddr, 0, pg_cnt * PG_SIZE);
}
lock_release(&user_pool.lock);
return vaddr;
}
/* Associate the address vaddr with the physical address in the pf pool,
* supports only single page allocation */
void *get_a_page(PoolFlag pf, uint32_t vaddr) {
MemoryPool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
lock_acquire(&mem_pool->lock);
/* First, set the corresponding bit in the virtual address bitmap */
TaskStruct *cur = current_thread();
int32_t bit_idx = -1;
/* If the current thread is a user process requesting user memory, modify
* the user process's virtual address bitmap */
if (cur->pg_dir != NULL && pf == PF_USER) {
bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
KERNEL_ASSERT(bit_idx >= 0);
bitmap_set(&cur->userprog_vaddr.virtual_mem_bitmap, bit_idx, 1);
} else if (cur->pg_dir == NULL && pf == PF_KERNEL) {
/* If a kernel thread is requesting kernel memory, modify kernel_vaddr.
*/
bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
KERNEL_ASSERT(bit_idx > 0);
bitmap_set(&kernel_vaddr.virtual_mem_bitmap, bit_idx, 1);
} else {
KERNEL_PANIC_SPIN("get_a_page: not allowed to allocate user space in kernel "
"or kernel space in user mode");
}
void *page_phyaddr = palloc(mem_pool);
if (page_phyaddr == NULL) {
lock_release(&mem_pool->lock);
return NULL;
}
page_table_add((void *)vaddr, page_phyaddr);
lock_release(&mem_pool->lock);
return (void *)vaddr;
}
get_user_pages没啥好说的,实际上跟我们的get_kernel_pages简直一摸一样,不过换了对象而已。注意一下,我们还是需要给分配的时候上锁。不然的话容易出现竞争问题。
cpp
/* Associate the address vaddr with the physical address in the pf pool,
* supports only single page allocation */
void *get_a_page(PoolFlag pf, uint32_t vaddr) {
MemoryPool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
lock_acquire(&mem_pool->lock);
/* First, set the corresponding bit in the virtual address bitmap */
TaskStruct *cur = current_thread();
int32_t bit_idx = -1;
/* If the current thread is a user process requesting user memory, modify
* the user process's virtual address bitmap */
if (cur->pg_dir != NULL && pf == PF_USER) {
bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
KERNEL_ASSERT(bit_idx >= 0);
bitmap_set(&cur->userprog_vaddr.virtual_mem_bitmap, bit_idx, 1);
} else if (cur->pg_dir == NULL && pf == PF_KERNEL) {
/* If a kernel thread is requesting kernel memory, modify kernel_vaddr.
*/
bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
KERNEL_ASSERT(bit_idx > 0);
bitmap_set(&kernel_vaddr.virtual_mem_bitmap, bit_idx, 1);
} else {
KERNEL_PANIC_SPIN("get_a_page: not allowed to allocate user space in kernel "
"or kernel space in user mode");
}
void *page_phyaddr = palloc(mem_pool);
if (page_phyaddr == NULL) {
lock_release(&mem_pool->lock);
return NULL;
}
page_table_add((void *)vaddr, page_phyaddr);
lock_release(&mem_pool->lock);
return (void *)vaddr;
}
可以看到,这个函数比起来之前的分配还多了个参数 vaddr,vaddr 用来指定绑定的虚拟地址,所以此函数的功能是申请一页内存,并用 vaddr映射到该页,也就是说我们可以指定虚拟地址。而get_user_pages 和get_kernel_pages 不能指定虚拟地址, 只能由内存管理模块自动分配虚拟地址,分配什么咱们就用什么。此函数内部实现就是把之前介绍过的方法重新拼合了,都是熟悉的函数,较容易理解。
下一个函数是我们要在memory_tools中新添加的函数:
cpp
/* Get the physical address mapped to the virtual address */
uint32_t addr_v2p(uint32_t vaddr) {
uint32_t *pte = pte_ptr(vaddr);
/* (*pte) holds the physical page frame address of the page table,
* remove the lower 12 bits of the page table entry attributes,
* and add the lower 12 bits of the virtual address vaddr */
return ((*pte & PG_FETCH_OFFSET) + (vaddr & PG_OFFSET_MASK));
}
这个是将我们的虚拟地址映射回物理地址。
准备冲向我们的特权级3(用户特权级)
现在,我们终于准备冲向特权级三了:可是到目 前为止我们都工作在 0 特权级下,如何从特权级 0 迈向特权级 3 呢?
一直以来我们都在0 特权级下工作,即使是在创建用户进程的过程中也是。可是我们知道,一般情况 下,CPU 不允许从高特权级转向低特权级,除非是从中断和调用门返回的情况下。咱们系统中不打算使 用调用门,因此,咱们进入特权级 3 只能借助从中断返回的方式,但用户进程还没有运行,何谈被中断? 更谈不上从中断返回了......但是 CPU 比较呆头呆脑,我们可以骗过 CPU,在用户进程运行之前,使其以为 我们在中断处理环境中,这样便"假装"从中断返回。
如何假装呢?从大体上来看,首先得在特权级0 的环境中,其次是执行 iretd 指令。这么说太笼统了, 下面咱们说得具体点。
从中断返回肯定要用到 iretd 指令,iretd 指令会用到栈中的数据作为返回地址,还会加载栈中 eflags 的值到 eflags 寄存器,如果栈中 cs.rpl 若为更低的特权级,处理器的特权级检查通过后,会将栈中 cs 载入 到 CS 寄存器,栈中 ss 载入 SS 寄存器,随后处理器进入低特权级。因此我们必然要在栈中提前准备好数据供 iretd指令使用。您看,既然已经涉及到栈操作了,不如进行得更彻底一些,咱们将进程的上下文都存到栈中,通过一系列的 pop 操作把用户进程的数据装载到寄存器,最后再通过 iretd 指令退出中断,把退出中断彻底地"假装"一回。
现在完全可以再重复写一套退出中断的代码,虽然仅为短短几行,但依然"勤俭节约",我们可以复用之前的成果,回忆一下,退出中断的出口是汇编语言函数 intr_exit,这是我们定义在 interrupt.S 中的,此函数用来恢复中断发生时、被中断的任务的上下文状态,并且退出中断。
由此我们得出关键点 1:从中断返回,必须要经过 intr_exit,即使是"假装"。
在中断发生时,我们在中断入口函数"intr%1entry"中通过一系列的 push 操作来保存任务的上下文,因此在intr_exit 中恢复任务上下文要通过一系列的pop 操作,这属于"intr%1entry"的逆过程。
还记得我们的
cpp
/**
* @brief Interrupt stack structure.
*
* This structure stores the register values when an interrupt occurs.
* It is used to save and restore the thread's execution context.
*/
typedef struct
{
uint32_t vec_no; // Interrupt vector number
uint32_t edi;
uint32_t esi;
uint32_t ebp;
uint32_t esp_dummy; // Placeholder for alignment
uint32_t ebx;
uint32_t edx;
uint32_t ecx;
uint32_t eax;
uint32_t gs;
uint32_t fs;
uint32_t es;
uint32_t ds;
/* From low to high privilege level */
uint32_t err_code; // Error code pushed by the processor
void (*eip)(void); // Instruction pointer at the time of interrupt
uint32_t cs; // Code segment
uint32_t eflags; // CPU flags
void *esp; // Stack pointer
uint32_t ss; // Stack segment
} Interrupt_Stack;
任务的上下文信息被保存在任务pcb 中的 Interrupt_Stack 中,注意,Interrupt_Stack 并不要求有固定的位置,它只是一种保存任务上下文的格式结构。
以下为叙述方便,将称为栈。 在汇编函数intr_exit 里面的一系列的pop 操作是为了恢复任务的上下文,从它退出中断是不可避免的,这样才能"假装"从中断返回。
由此我们得出关键点2:必须提前准备好用户进程所用的栈结构,在里面填装好用户进程的上下文信息,借一系列pop 出栈的机会,将用户进程的上下文信息载入CPU 的寄存器,为用户进程的运行准备好环境
当执行完 intr_exit 中的 iretd 指令后,CPU 便恢复了任务中断前的状态:中断前是哪个特权级就进入哪个特权级。
CPU 是如何知道从中断退出后要进入哪个特权级呢?这是由栈中保存的CS 选择子中的RPL 决定的,我们知道,CS.RPL 就是 CPU 的 CPL,当执行 iretd 时,在栈中保存的 CS 选择子要被加载到代码段寄存 器 CS 中,因此栈中 CS 选择子中的 RPL 便是从中断返回后 CPU 的新的 CPL。 我们进入(假装返回)到3 特权级
由此我们得出关键点 3:我们要在栈中存储的 CS 选择子,其 RPL 必须为 3。
大伙知道,RPL 是选择子中的低2 位,用以表示(或者叫"揭露")访问者特权级,因此RPL 是为了避免低特权级任务作弊使用指向高特权级内存段的选择子而提供的一种检测手段。虽然RPL 是CPU提供的、硬件级的方案,但CPU 只负责接收选择子,它自己可不知道所提交的选择子是否是造假的。因此它把RPL 的维护权交给了操作系统,由操作系统去保证所有提交的选择子都是"真货"。
所以,为了避免任务提交一 个假的选择子(通常是指向特权级更高的内存段),操作系统会将选择子的RPL 置为用户进程的CPL,只有CPL 和RPL 在数值上同时小于等于选择子所指向的内存段的DPL 时,CPU 的安全检测才通过,从而避免了低特权级任务跨级访问高特权级的内存段。
既然用户进程的特权级为 3,操作系统不能辜负 CPU 的委托,它有责任把用户进程所有段选择子的RPL 都置为3,因此,在 RPL=CPL=3 的情况下,用户进程只能访问 DPL 为3 的内存段,即代码段、数据段、栈段。我们前面的工作中已经准备好了 DPL 为3 的代码段及数据段
由此我们得出关键点 4,栈中段寄存器的选择子必须指向 DPL 为 3 的内存段。
对于可屏蔽中断来说,任务之所以能进入中断,是因为标志寄存器 eflags 中的 IF 位为1,退出中断后,还得保持 IF 位为 1,继续响应新的中断。
由此我们得出关键点 5:必须使栈中 eflags 的 IF 位为 1。
用户进程属于最低的特权级,对于IO 操作,不允许用户进程直接访问硬件,只允许操作系统有直接的硬件控制。这是由标志寄存器 eflags 中 IOPL 位决定的,必须使其值为 0。
由此我们得出关键点 6:必须使栈中 eflags 的 IOPL 位为 0。
讨论下我们创建用户线程的基本步骤
笔者试试看使用Latex画图,大家一下流程,如果感觉这个图太糊了,看不清楚,到目录下本篇的目录下找advanced文件夹下看Latex生成的PDF比较好。
下面的图像是我们创建一个用户级线程的活:

cpp
/* Execute a user process */
void create_process(void *filename, char *name) {
/* Allocate memory for the process control block (PCB) in the kernel memory
* pool */
TaskStruct *thread = get_kernel_pages(PCB_SZ_PG_CNT);
init_thread(thread, name, DEFAULT_PRIO); // Initialize the thread's PCB
create_user_vaddr_bitmap(thread); // Create a virtual address bitmap for the user process
create_thread(thread, start_process,
filename); // Create the thread to start the user process
thread->pg_dir =
create_page_dir(); // Create a page directory for the process
/* Add the thread to the ready list and all thread list */
Interrupt_Status old_status = set_intr_status(INTR_OFF);
KERNEL_ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
list_append(&thread_ready_list, &thread->general_tag);
KERNEL_ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
list_append(&thread_all_list, &thread->all_list_tag);
set_intr_status(old_status); // Restore the interrupt state
} // if u feel puzzled, see below!
创建的时候,我们让我们的用户进程作为了一个filename发出去(这个奇怪的名字需要到我们搓文件系统的时候才能理解),最后就是调用filename的函数委托成用户进程发出去。其他部分跟我们创建内核线程简直一摸一样。
那又如何运行呢?前面说过了,进程的运行是由时钟中断调用 schedule
,由调用器 schedule
调度 实现的。当schedule
从就绪队列中获取的pcb恰好是新创建的进程时,该进程马上就要被执行了。在 schedule 中,调用了activate_process_settings
来激活进程或线程的相关资源(页表等),随后通过 switch_to 函数调度进程,根据先前进程创建时函数 thread_create 的工作,已经将 kernel_thread 作为函数 switch_to 的返回地址,即在 switch_to 中退出后,处理器会执行 kernel_thread 函数,"相当于"switch_to 调用 kernel_thread。
同样在之前的 thread_create 中,已经将 start_process 和 filename作为了 kernel_thread 的参 数,故在kernel_thread 中可以以此形式调用 start_process(user_prog)。函数 start_process 主要用来构建用户 进程的上下文,它会将filename作为进程"从中断返回"的地址,您懂的,这里的"从中断返回"是假装 的,目的是让用户进程顺利进入3 特权级。由于是从0 特权级的中断返回,故返回地址filename被iretd 指 令使用,为了复用中断退出的代码,现在需要跳转到中断出口 intr_exit(kernel.S 中汇编代码完成的函数) 处,利用那里的 iretd 指令使返回地址 filename作为 EIP 寄存器的值以使 filename得到执行,故相当于start_process 调用 intr_exit,intr_exit 调用 filename,最终用户进程 filename在 3 特权级下执行。
更加详细的分析代码
我们更详细的分析一下代码。首先,我们在之前的博客就提到了要处理Flags的事情,这里再把我们selectors.h文件中关于EFLAGS寄存器的事情再看看
cpp
#define EFLAGS_MBS (1 << 1) // Must be set to 1.
#define EFLAGS_IF_1 (1 << 9) // IF = 1, enables interrupts.
#define EFLAGS_IF_0 0 // IF = 0, disables interrupts.
#define EFLAGS_IOPL_3 (3 << 12) // IOPL = 3, allows user-mode I/O (for testing).
#define EFLAGS_IOPL_0 (0 << 12) // IOPL = 0, restricts I/O access to kernel mode.
这个没啥好说的,我们下一步看看我们的process.c文件下的函数
cpp
extern void intr_exit(void);
/* Build the initial context for a user process */
static void start_process(void *filename_) {
void *function = filename_; // The entry point of the user process
TaskStruct *cur = current_thread();
cur->self_kstack += sizeof(ThreadStack); // Move the stack pointer to the correct location
Interrupt_Stack *proc_stack =
(Interrupt_Stack *)cur->self_kstack; // Set up the interrupt stack
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; // User mode does not use the gs register, set to 0
proc_stack->ds = proc_stack->es = proc_stack->fs =
SELECTOR_U_DATA; // Set the data segment selector
proc_stack->eip = function; // The address of the function to execute
proc_stack->cs = SELECTOR_U_CODE; // Set the code segment selector
proc_stack->eflags =
(EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1); // Set flags for user mode
proc_stack->esp =
(void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) +
PG_SIZE); // Set up user stack
proc_stack->ss = SELECTOR_U_DATA; // Set the stack segment selector
asm volatile("movl %0, %%esp; \
jmp intr_exit"
:
: "g"(proc_stack)
: "memory"); // Switch to the user process context
}
这个我们需要好好说一说,等到我们的走完了这个流程之后,我们的PCB栈顶就变成了这个样子:
cpp
+---------------------------+ <- 高地址(栈底)
| ss (0x73) | 用户数据段选择子
+---------------------------+
| esp | 指向用户栈的顶端
+---------------------------+
| eflags | 标志寄存器 (0x202)
+---------------------------+
| cs (0x6B) | 用户代码段选择子
+---------------------------+
| eip | 入口函数地址
+---------------------------+
| err_code | 错误代码(如果有)
+---------------------------+
| ds (0x73) | 用户数据段选择子
+---------------------------+
| es (0x73) | 用户数据段选择子
+---------------------------+
| fs (0x73) | 用户数据段选择子
+---------------------------+
| gs (0x0) | 不使用,设为0
+---------------------------+
| eax (0x0) |
+---------------------------+
| ecx (0x0) |
+---------------------------+
| edx (0x0) |
+---------------------------+
| ebx (0x0) |
+---------------------------+
| esp_dummy (0x0) | 对齐填充
+---------------------------+
| ebp (0x0) |
+---------------------------+
| esi (0x0) |
+---------------------------+
| edi (0x0) |
+---------------------------+
| vec_no | 中断向量号
+---------------------------+ <- 低地址(栈顶)
用户进程的视图

就上面这个视图,我们实际上就是构建这样的用户进程视图,用户程序内存空间的最顶端用来存储命令行参数及环境变量,这些内容是由某操作系统下的 C 运行库写进去的,将来实现从文件系统加载用户进程并为其传递参数时会介绍这部分。紧接着是栈空间和堆空间栈向下扩展,堆向上扩展,栈与堆在空间上是相接的, 这两个空间由操作系统管理分配,由于栈与堆是相向扩展的,操作系统需要检测栈与堆的碰撞。最下面的未初始化数据段 bss、初始化数据段 data 及代码段 text 由链接器和编译器负责。
在4GB 的虚拟地址空间中,(0xc0000000-1)
是用户空间的最高地址,0xc0000000~0xffffffff
是内核空间。 我们也效仿这种内存结构布局,把用户空间的最高处即0xc0000000-1
,及以下的部分内存空间用于存储用户进程的命令行参数,之下的空间再作 为用户的栈和堆。命令行参数也是被压入用户栈的(在后面章节介绍加载 用户进程时会了解),因此虽然命令行参数位于用户空间的最高处,但它 们相当于位于栈的最高地址处,所以用户栈的栈底地址为 0xc0000000
。 由于在申请内存时,内存管理模块返回的地址是内存空间的下边界,所以 我们为栈申请的地址应该是(0xc0000000-PG_SIZE
),此地址是用户栈空间 栈顶的下边界。这里我们用宏来定义此地址,即USER_STACK3_VADDR了。
cpp
#define USER_STACK3_VADDR (KERNEL_V_START - PG_SIZE)
更好说明,我把代码单独给出来:
cpp
proc_stack->esp =
(void *)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) +
PG_SIZE); // Set up user stack
"get_a_page(PF_USER, USER_STACK3_VADDR)"先获取特权级3 的栈的下 边界地址,将此地址再加上PG_SIZE,所得的和就是栈的上边界,即栈底,将此栈底赋值给proc_stack->esp。
也许您会有疑问,此处为用户进程创建的 3 特权级栈,它是在谁的内存空间中申请的?换句话说, 是安装在谁的页表中了?不会是安装到内核线程使用的页表中了吧?当然不是,用户进程使用的 3 级栈 必然要建立在用户进程自己的页表中。在进程创建部分,有一项工作是 create_page_dir,这是提前为用户进入创建了页目录表,在进程执行部分,有一项工作是activate_process_settings,这是使任务(无论任务是否为新创建的进程或线程,或是老进程、老 线程)自己的页表生效。我们是在函数 start_process 中为用户进程创建了 3 特权级栈,start_process 是在 执行任务页表激活之后执行的,也就是在 activate_process_settings)之后运行,那时已经把页表更新为用户进程的 页表了,所以3 特权级栈是安装在用户进程自己的页表中的。
栈段可以用普通的数据段,第28 行为栈中SS 赋值为用户数据段选择子SELECTOR_U_DATA。 我们通过内联汇编,将栈 esp 替换为我们刚刚填充好的proc_stack,然后通过 jmp intr_exit 使程序流程跳转到中断出口地址 intr_exit,通过那里的一系列 pop 指令和 iretd 指令,将 proc_stack 中的数据载入 CPU 的寄存器,从而使程序"假装"退出中断,进入特权级 3。
我们下面说一下page_dir_activate函数。
cpp
/* Activate the page directory */
void page_dir_activate(TaskStruct *p_thread) {
/********************************************************
* This function may be called while the current task is a thread.
* The reason we also need to reload the page table for threads is that
* the last task switched might have been a process, and without restoring
* the page table, the thread will mistakenly use the process's page table.
********************************************************/
/* If it's a kernel thread, set the page directory to 0x100000 (kernel
* space) */
uint32_t pagedir_phy_addr = KERNEL_PAGE_MAPPINGS; // Default for kernel thread page directory
if (p_thread->pg_dir) { // If the thread has its own page directory (user process)
pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pg_dir);
}
/* Update the CR3 register to activate the new page table */
asm volatile("movl %0, %%cr3" : : "r"(pagedir_phy_addr) : "memory");
}
这里,我们的pagedir_phy_addr就是我们安排页表的物理地址所在的位置。当然,我们每一个进程是需要自己的一份页表视图的,我们要加载到cr3上去,如果我们当前的是用户进程(这显然是自己私有的页表),就需要我们直接进行一个加载挂上去(也就是pd_dir不是空的)
最后一个就是我们的activate_process_settings函数,请看:
cpp
void activate_process_settings(TaskStruct *p_thread) {
KERNEL_ASSERT(p_thread);
/* Activate the page table for the process or thread */
page_dir_activate(p_thread);
/* If it's a kernel thread, its privilege level is already 0,
so the processor doesn't need to fetch the stack address from the TSS
during interrupts. */
if (p_thread->pg_dir) {
/* Update esp0 in the TSS for the process's privilege level 0 stack
* during interrupts */
update_tss_esp(p_thread);
}
}
它的功能有两个,一是激活线程或进程的页表,二是更新tss 中的 esp0 为进程的特权级0 的栈。大伙儿知道,进程与线程都是独立的执行流,它们有各自的栈和页表,只不过线程的页表是和其他线程共用的,而进程的页表是单独的。进程或线程在被中断信号打断时,处理器会进入0特权级,并会在内核栈中保存进程或线程的上下文环境。如果当前被中断的是3特权级的用户进程,处理器会自动到 tss 中获取 esp0 的值作为用户进程在内核态(0 特权级)的栈地址,如果被中断的是0 特权级的内核线程,由于内核线程已经是0 特权级,进入中断后不涉及特权级的改变,所以处理器并不会到 tss 中获取 esp0,言外之意是即使咱们更新了线程 tss 中的 esp0,处理器也不会使用它,咱们就白费劲了
如果是用户进程的话才去更新 tss 中的 esp0。这两个功能的实现,就是调用 tss.c 中定义的 update_tss_esp 和上面刚介绍的 page_dir_activate 完成的。顺便说一下,只有在任务调度时才会切换页表及更新 0 级栈,因此activate_process_settings是被 schedule 调用的
说一说BSS段
为什么要介绍BSS段,很简单,我们要搞内存分配,仿照Linux 下 C 程序的布局方案来做,不说编译器如何处理BSS段,很容易把内存布局做错,导致出现问题。
在 C 程序的内存空间中,位于低处的三个段是代码段、数据段和 bss 段,它们由编译器和链接器规划地址空间,在程序被操作系统加载之前它们地址就固定了。而堆是位于 bss 段的上面,栈是位于堆的上面,它们共享 4GB 空间中除了代码段、数据段及顶端命令行参数和环境变量等以外的其余可用空间,它们的地址由操作系统来管理,在程序加载时为用户进程分配栈空间,运行过程中为进程从堆中分配内存。堆向上扩展,栈向下扩展,因此在程序的加载之初,操作系统必须为堆和栈分别指定起始地址。
在 Linux 中,堆的起始地址是固定的,这是由 struct mm_struct 结构中的 start_brk 来指定的,堆的结束地址并不固定,这取决于堆中内存的分配情况,堆的上边界是由同结构中的brk 来标记的。堆是要安排在bss 以上的,按理说我们只要找到bss 的结束地址就可以自由规划堆的起始地址了。
但其实我们不需要这么麻烦,有更简单省事的做法,为解释清楚这个问题,这里对bss 多说两句。 大伙儿知道,C 程序大体上分为预处理、编译、汇编和链接四个阶段。根据语法规则,编译器会在汇编阶段将汇编程序源码中的关键字 section 或 segment(汇编源码中,通常用语法关键字 section 或 segment来逻辑地划分程序区域,虽然很多书中都称此程序区域为段,但实际上它们就是节 section。注意,这里所说的 segment 和 section 是指汇编语法中的关键字,仅指它们在汇编代码中的语法意义相同,并不是指编译、链接中的 section 和 segment)编译成节,也就是之前介绍的 section,此时只是生成了目标文件,目标文件中的这些节还不是程序空间中的独立的代码段或数据段,或者说仅仅是代码段或数据段的一部分。链接器将这些目标文件中属性相同的节(section)合并成段(segment),因此一个段是由多个节组成的,我们平时所说的 C 程序内存空间中的数据段、代码段就是指合并后的 segment。为什么要将 section 合并成segment?
-
是为了保护模式下的安全检查
-
为了操作系统在加载程序时省事。
大伙儿已经知道,在保护模式下对内存的访问必须要经过段描述符,段描述符用来描述一段内存区域的访问属性,其中的S位和 TYPE 位可组合成多种权限属性,处理器用这些属性来限制程序对内存的使用,如果程序对某片内存的访问方式不符合该内存所对应的段描述符(由访问内存时所使用的选择子决定)中设置的权限,比如对代码这种具备只读属性的内存区域执行了写操作,处理器会检查到这种情况并抛出GP 常。
程序必须要加载到内存中才能执行,为了符合安全检查,程序中不同属性的节必须要放置到合适的段描述符指向的内存中。比如为程序中具有只读可执行的指令部分所分配的内存,最好是通过具有只读、可执行属性的段描述符来访问,否则若通过具有可写属性的段描述符来访问指令区域的话,程序有可能会将自己的指令部分改写,从而引起破坏。处理器对内存访问的安全检查主要体现在使用的段描述符,段描述符是由选择子决定的,而选择子是由操作系统提供的,所以针对程序中不同属性的区域,操作系统得知道用哪个段描述符来匹配程序中这些不同属性的区域片段,也就是要在程序运行之前提前设置程序在运行时各种段寄存器(如 cs、ds)中的选择子。
综上所述,在操作系统的视角中,它只关心程序中这些节的属性是什么,以便加载程序时为其分配不同的段选择子,从而使程序内存指向不同的段描述符,起到保护内存的作用。因此最好是链接器把目标文件中属性相同的节合并到一起,这样操作系统便可统一为其分配内存了。按照属性来划分节,大致上有三种类型。
-
可读写的数据,如数据节.data 和未初始化节.bss。
-
只读可执行的代码,如代码节.text 和初始化代码节.init。
-
只读数据,如只读数据节.rodata,一般情况下字符串就存储在此节。
经过这样的划分,所有节都可归并到以上三种之一,这样方便了操作系统加载程序时的内存分配。由链接器把目标文件中相同属性的节归并之后的节的集合,便称为 segment,它存在于二进制可执行文件中,也就是 C 程序运行时内存空间中分布的代码段、数据段等段。
现在可以正式说说 bss 了,我们已经知道,text 段是代码段,里面存放的是程序的指令,data 段是数据段,里面存放的是程序运行时的数据,它们的共同点是都存在于程序文件中,也就是在文件系统中存在,原因很简单,它们是程序运行必备的"材料",必须提前准备好,基本上是固定好的内容。而 bss 并不存在于程序文件中,它仅存在于内存中,其实际内容是在程序运行过程中才产生的,起初并无意义,换句话说,程序在运行时的那一瞬间并不需要 bss,因此完全不需要事先在程序文件中存在,程序文件中仅在 elf 头中有bss 节的虚拟地址、大小等相关记录,这通常是由链接器来处理的,对程序运行并不重要,因此程序文件中并不存在 bss 实体。bss 中的数据是未初始化的全局变量和局部静态变量,程序运行后才会为它们赋值,因此在程序运行之初,里面的数据没意义,由操作系统的程序加载器将其置为0 就可以了,虽然这些未初始化的全局变量及局部静态变量起初是用不上的,但它们毕竟也是变量,即使是短暂的生存周期也要占用内存,必须提前为它们在内存中"占好座",bss 区域的目的也正在于此,就是提前为这些未初始化数据预留内存空间。
我们来实际上看看,使用指令readelf -l -h -S /bin/find
(注意,-e这个选项现在已经没有了)
bash
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x8510
Start of program headers: 64 (bytes into file)
Start of section headers: 202280 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.pr[...] NOTE 0000000000000338 00000338
0000000000000030 0000000000000000 A 0 0 8
[ 3] .note.gnu.bu[...] NOTE 0000000000000368 00000368
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000038c 0000038c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003b0 000003b0
0000000000000028 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003d8 000003d8
0000000000000ed0 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 00000000000012a8 000012a8
000000000000061e 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 00000000000018c6 000018c6
000000000000013c 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000001a08 00001a08
00000000000000c0 0000000000000000 A 7 2 8
[10] .rela.dyn RELA 0000000000001ac8 00001ac8
00000000000030f0 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000004bb8 00004bb8
0000000000000d50 0000000000000018 AI 6 25 8
[12] .init PROGBITS 0000000000006000 00006000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000006020 00006020
00000000000008f0 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 0000000000006910 00006910
0000000000000020 0000000000000010 AX 0 0 16
[15] .plt.sec PROGBITS 0000000000006930 00006930
00000000000008e0 0000000000000010 AX 0 0 16
[16] .text PROGBITS 0000000000007210 00007210
000000000001daa2 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 0000000000024cb4 00024cb4
000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000025000 00025000
00000000000056d0 0000000000000000 A 0 0 32
[19] .eh_frame_hdr PROGBITS 000000000002a6d0 0002a6d0
0000000000000a8c 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 000000000002b160 0002b160
0000000000003318 0000000000000000 A 0 0 8
[21] .init_array INIT_ARRAY 000000000002f0f0 0002f0f0
0000000000000008 0000000000000008 WA 0 0 8
[22] .fini_array FINI_ARRAY 000000000002f0f8 0002f0f8
0000000000000008 0000000000000008 WA 0 0 8
[23] .data.rel.ro PROGBITS 000000000002f100 0002f100
0000000000001810 0000000000000000 WA 0 0 32
[24] .dynamic DYNAMIC 0000000000030910 00030910
0000000000000200 0000000000000010 WA 7 0 8
[25] .got PROGBITS 0000000000030b10 00030b10
00000000000004e8 0000000000000008 WA 0 0 8
[26] .data PROGBITS 0000000000031000 00031000
0000000000000478 0000000000000000 WA 0 0 32
[27] .bss NOBITS 0000000000031480 00031478
0000000000000a80 0000000000000000 WA 0 0 32
[28] .gnu_debugaltlink PROGBITS 0000000000000000 00031478
0000000000000049 0000000000000000 0 0 1
[29] .gnu_debuglink PROGBITS 0000000000000000 000314c4
0000000000000034 0000000000000000 0 0 4
[30] .shstrtab STRTAB 0000000000000000 000314f8
000000000000012f 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000005908 0x0000000000005908 R 0x1000
LOAD 0x0000000000006000 0x0000000000006000 0x0000000000006000
0x000000000001ecc1 0x000000000001ecc1 R E 0x1000
LOAD 0x0000000000025000 0x0000000000025000 0x0000000000025000
0x0000000000009478 0x0000000000009478 R 0x1000
LOAD 0x000000000002f0f0 0x000000000002f0f0 0x000000000002f0f0
0x0000000000002388 0x0000000000002e10 RW 0x1000
DYNAMIC 0x0000000000030910 0x0000000000030910 0x0000000000030910
0x0000000000000200 0x0000000000000200 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
NOTE 0x0000000000000368 0x0000000000000368 0x0000000000000368
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
GNU_EH_FRAME 0x000000000002a6d0 0x000000000002a6d0 0x000000000002a6d0
0x0000000000000a8c 0x0000000000000a8c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x000000000002f0f0 0x000000000002f0f0 0x000000000002f0f0
0x0000000000001f10 0x0000000000001f10 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .data.rel.ro .dynamic .got
我们找到了:第二十七节就是我们的find中bss段的大小:0000000000000a80。
我们的"Section to Segment mapping:"是链接器将节合并为段的结果展示.为了验证,我们可以用此索引03 在"Program Headers"中查看第03个段
LOAD 0x0000000000006000 0x0000000000006000 0x0000000000006000
0x000000000001ecc1 0x000000000001ecc1 R E 0x1000
该段是类型为 LOAD 的段,其Flg 标志中是"R E",这表示可读可执行,充分说明了代码节.text 确实被合并成了代码段,这里已经用线把它们的对应关系连接起来了。"Section to Segment mapping:"中索引为05的段,其包括一行的节,里面有数据节.data,这说明05段很可能是数据段,此段中还包括了.bss 节,这说明链接器将.bss 节、.data 节等合并到了同一个段中,该段很可能是数据段,为了验证是否为数据段,同样用此索引05 到"Program Headers"中查看第05 个段,
LOAD 0x000000000002f0f0 0x000000000002f0f0 0x000000000002f0f0
0x0000000000002388 0x0000000000002e10 RW 0x1000
该段是第二个LOAD 段,其Flg 为"RW",即"可读写",充分说明此段是数据段,.text 和.data 等节的合并关系也用线标出了。 通常情况下,段在文件中的大小 FileSiz 和在内存中的大小 MemSiz 应该是一致的,而且在任何情况下,段的MemSiz 都不会比FileSiz 小。如果在某段中合并了bss 节,该段的MemSiz 应该大于FileSiz,原因是bss节不占用文件系统空间,只占用内存空间。
所以这样看来,我们的堆的起始地址应该在 bss 之上,但由于 bss 已融合到数据段中,要实现用户进程的堆,已不需要知道 bss 的结束地址,将来咱们加载程序时会获取程序段的起始地址及大小,因此只要堆的起始地址在用户进程地址最高的段之上就可以了。
继续看process.c中的函数
cpp
/* Create a new page directory, copying the kernel space entries from the
current page directory. Returns the virtual address of the new page
directory, or NULL on failure. */
uint32_t *create_page_dir(void) {
/* The user process's page table cannot be directly accessed, so we allocate
* memory in the kernel space */
uint32_t *page_dir_vaddr = get_kernel_pages(1);
if (!page_dir_vaddr) {
console_ccos_puts("create_page_dir: get_kernel_page failed!");
return NULL;
}
/************************** 1. Copy the current page table entries for
* kernel space *************************************/
/* Copy the kernel page directory entries starting at 0x300 (768th entry in
* the page directory) */
k_memcpy((uint32_t *)((uint32_t)page_dir_vaddr + KERNEL_PGTB_INDEX * PDE_BYTES_LEN),
(uint32_t *)(PG_FETCH_OFFSET + KERNEL_PGTB_INDEX * PDE_BYTES_LEN), PDE_ENTRY_NR);
/*****************************************************************************/
/************************** 2. Update the page directory address
* **********************************/
uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr);
/* The page directory address is stored in the last entry of the page table.
Update it with the new page directory's physical address */
page_dir_vaddr[PDE_ENTRY_NR - 1] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;
/*****************************************************************************/
return page_dir_vaddr; // Return the virtual address of the newly created
// page directory
}
这里是我们需要创建页目录表。操作系统是为用户进程服务的,它提供了各种各样的系统功能供用户进程调用。为了用户进程可以访问到内核服务,必须确保用户进程必须在自己的地址空间中能够访问到内核才行,也就是说内核空间必须是用户空间的一部分。所有的用户进程应当共享的是同一个内核的内存视图,这个事情很简单:

看到这个表,我们这个表中,只需要内核页表的入口导入到每一个用户进程的页表上就好了。
cpp
/* Create the user process's virtual address bitmap */
static void create_user_vaddr_bitmap(TaskStruct *user_prog) {
user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;
uint32_t bitmap_pg_cnt =
ROUNDUP((KERNEL_V_START - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);
user_prog->userprog_vaddr.virtual_mem_bitmap.bits =
get_kernel_pages(bitmap_pg_cnt);
user_prog->userprog_vaddr.virtual_mem_bitmap.btmp_bytes_len =
(KERNEL_V_START - USER_VADDR_START) / PG_SIZE / 8;
bitmap_init(&user_prog->userprog_vaddr.virtual_mem_bitmap);
}
上面的函数创建用户进程的虚拟地址位图,实际上是初始化userprog_vaddr。这个没啥好说的,自行对照我们如何初始化内核的就好。
添加用户线程激活
我们还差对schedule的处理
cpp
/**
* @brief Task scheduling function.
*
* This function implements simple task scheduling by selecting the next thread
* to run from the ready list and performing a context switch.
*/
void schedule(void)
{
KERNEL_ASSERT(get_intr_status() == INTR_OFF); // Ensure interrupts are disabled
TaskStruct *cur = current_thread(); // Get the current running thread
if (cur->status == TASK_RUNNING)
{
KERNEL_ASSERT(!elem_find(&thread_ready_list, &cur->general_tag)); // Ensure it's not already in the list
list_append(&thread_ready_list, &cur->general_tag); // Add it to the ready list
cur->ticks = cur->priority; // Reset thread's time slice based on priority
cur->status = TASK_READY; // Set thread status to ready
}
KERNEL_ASSERT(!list_empty(&thread_ready_list)); // Ensure there is a next thread to run
thread_tag = NULL; // Clear the thread tag
// Pop the next thread from the ready list
thread_tag = list_pop(&thread_ready_list);
TaskStruct *next = elem2entry(TaskStruct, general_tag, thread_tag);
next->status = TASK_RUNNING; // Set the next thread as running
activate_process_settings(next);
switch_to(cur, next); // Perform context switch to the next thread
}
我们在函数switch_to的前面,添加激活页表设置的函数,现在就好了!
cpp
#include "include/device/console_tty.h"
#include "include/kernel/init.h"
#include "include/library/kernel_assert.h"
#include "include/thread/thread.h"
#include "include/user/process/process.h"
void thread_a(void *args);
void thread_b(void *args);
void u_prog_a(void);
void u_prog_b(void);
int test_var_a = 0, test_var_b = 0;
int main(void)
{
init_all();
thread_start("k_thread_a", 31, thread_a, "In thread A:");
thread_start("k_thread_b", 16, thread_b, "In thread B:");
create_process(u_prog_a, "user_prog_a");
create_process(u_prog_b, "user_prog_b");
interrupt_enabled();
while (1)
{
}
}
void thread_a(void *arg[[maybe_unused]])
{
while (1)
{
console_ccos_puts("v_a:0x");
console__ccos_display_int(test_var_a);
}
}
void thread_b(void *arg[[maybe_unused]])
{
while (1)
{
console_ccos_puts(" v_b:0x");
console__ccos_display_int(test_var_b);
}
}
void u_prog_a(void)
{
while (1)
{
test_var_a++;
}
}
void u_prog_b(void) {
while(1) {
test_var_b++;
}
}
就是这样,但是你的第一反应是------不对啊,你这两个真丁真嘛?真的是用户函数嘛?不着急,的确,这两个函数是在内核的地址空间上存在的,这个绝对没毛病,请看
> nm middleware/kernel.bin | grep -P "u_prog|test_var"
c00091c0 B test_var_a
c00091c4 B test_var_b
c0001609 T u_prog_a
c0001627 T u_prog_b
但是我们需要注意的是------我们的处理器实际上不关心我们的"用户态函数"在地址的什么地方,或者说,它判定是不是用户态函数,是根据当前所在的代码段的特权级看的,当用户进程u_prog_a 运行后,它的特权级为3,它拥有自己独立的页表,并且是通过为用户进程准备的DPL 为3的段描述符访问内存,这完全符合用户进程的特征和行为,而且最最重要的是我们早已把所有页目录项和页表项的US 位都置为1(loader.S和memory.c 中所有涉及到PDE 和 PTE 的地方都用的是 PG_US_U),这表示处理器允许所有特权级的任务可以访问目录项或页表项指向的内存,所以用内核空间中的函数来模拟用户进程是没有问题的。


关于变化的事情呢,多少有点难捕捉,所以我就懒得放图了,自行跑demo看看现象就好了。
测试一下丁不丁真,我们可以在用户线程的函数上打一个断点,这个时候看看我们的cs寄存器的值:
(0) [0x000000001643] 002b:c0001643 (unk. ctxt): jmp .-17 (0xc0001634) ; ebef
<bochs:11> sreg
es:0x0033, dh=0x00cff300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
cs:0x002b, dh=0x00cff900, dl=0x0000ffff, valid=1
Code segment, base=0x00000000, limit=0xffffffff, Execute-Only, Non-Conforming, Accessed, 32-bit
ss:0x0033, dh=0x00cff300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
ds:0x0033, dh=0x00cff300, dl=0x0000ffff, valid=31
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
fs:0x0033, dh=0x00cff300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
gs:0x0000, dh=0x00001000, dl=0x00000000, valid=0
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0020, dh=0xc0808b00, dl=0x9620006b, valid=1
gdtr:base=0xc0000900, limit=0x37
idtr:base=0xc0009360, limit=0x17f