4.8 进程管理
1. 创建进程
程序控制结构体 PCB(Process Control Block): task_struct (task.h)
thread负责在进程调度过程中保存或还原CR3控制寄存器的页目录基地址和通用寄存器值,
其他成员 变量基本属于进程调度策略的管理范畴。
struct task_struct
{
struct List list;
volatile long state;
unsigned long flags;
struct mm_struct *mm;
struct thread_struct *thread;
unsigned long addr_limit;
/*0x0000,0000,0000,0000 - 0x0000,7fff,ffff,ffff user*/
/*0xffff,8000,0000,0000 - 0xffff,ffff,ffff,ffff kernel*/
long pid;
long counter;
long signal;
long priority;
};
List 双向链表 用于连接各个PCB (lib.h)
struct List
{
struct List * prev;
struct List * next;
};
内存空间分布结构体: mm_struct (task.h)
struct mm_struct
{
pml4t_t *pgd; //page table point
unsigned long start_code,end_code;
unsigned long start_data,end_data;
unsigned long start_rodata,end_rodata;
unsigned long start_brk,end_brk;
unsigned long start_stack;
};
第一个进程 init_task_union
代码布局

进程控制结构体struct task_struct与进程的内核层樵空间融为一体
// 32KB
#define STACK_SIZE 32768
union task_union
{
struct task_struct task;
unsigned long stack[STACK_SIZE / sizeof(unsigned long)];
}__attribute__((aligned (8))); //8Bytes align
union task_union init_task_union
__attribute__((__section__ (".data.init_task")))
= {INIT_TASK(init_task_union.task)};

后面开辟的子进程也和init_task_union 类似, 低地址处存放struct task_struct结构体, 而余下的高地址空间则作为进程的栈空间使用
struct task_struct *tsk = NULL;
struct thread_struct *thd = NULL;
struct Page *p = NULL;
color_printk(WHITE,BLACK,"alloc_pages,bitmap:%#018lx\n",*memory_management_struct.bits_map);
// 开辟一个新的page
p = alloc_pages(ZONE_NORMAL,1,PG_PTable_Maped | PG_Active | PG_Kernel);
color_printk(WHITE,BLACK,"alloc_pages,bitmap:%#018lx\n",*memory_management_struct.bits_map);
tsk = (struct task_struct *)Phy_To_Virt(p->PHY_address);
color_printk(WHITE,BLACK,"struct task_struct address:%#018lx\n",(unsigned long)tsk);
memset(tsk,0,sizeof(*tsk));
// 复制当前的进程内容至新进程里面
*tsk = *current;
list_init(&tsk->list);
list_add_to_before(&init_task_union.task.list,&tsk->list);
tsk->pid++;
tsk->state = TASK_UNINTERRUPTIBLE;
thd = (struct thread_struct *)(tsk + 1);
tsk->thread = thd;
memcpy(regs,(void *)((unsigned long)tsk + STACK_SIZE - sizeof(struct pt_regs)),sizeof(struct pt_regs));
// 保留32k的 task_struct
thd->rsp0 = (unsigned long)tsk + STACK_SIZE;
thd->rip = regs->rip;
// sizeof(struct pt_regs) == 192 Bytes 栈 用于存放 192字节的寄存器值
thd->rsp = (unsigned long)tsk + STACK_SIZE - sizeof(struct pt_regs);
2. 切换进程
进程间的切换过程主要涉及页目录的切换和各寄存器值的保存与恢复等知识点。
/*
函数_ switch_to首先将 next进程的内核层楼基地址设置到 TSS结构体对应的成员变量中 。
随 后,保存当前进程的 FS与 GS段寄存器值,再将 next进程保存的 FS与 GS段寄存器值还原 。
*/
inline void __switch_to(struct task_struct *prev,struct task_struct *next)
{
/*
RSPn Canonical型的战指针(特权级0~2 )
在IA-32e模式下,处理器允许加载一个空段选择子NULL段选择子(第0个GDT项)到除cs以外的 段寄存器( 3特权级的SS段寄存器不允许加载NULL段选择子)。
处理器加载NULL段选择子到段寄存 器的过程,并非i卖取GDT的第0项到段寄存器,而是以 一个无效的段描述符来初始化段寄存器 。
在发 生特权级切换时, 新的SS段寄存器将强制加载-个NULL段选择子, 而RSP将根据特权级被赋值为RSPn (n=O~2)o
把新SS段寄存器设置为NULL段选择子是为了完成远跳转( far CALL, INTn,中断或异常) 动作 ,而旧SS段寄存器和RSP将被保存到新技中 。
IST ( Interrupt Stack Table,中断枝表)是IA-32e模式为任务状态段引人的新型战指针,其功能与 RSP相同,只不过IST切换中断棋指针时不会考虑特权级切换。
*/
init_tss[0].rsp0 = next->thread->rsp0;
set_tss64(init_tss[0].rsp0, init_tss[0].rsp1, init_tss[0].rsp2, init_tss[0].ist1, init_tss[0].ist2, init_tss[0].ist3, init_tss[0].ist4, init_tss[0].ist5, init_tss[0].ist6, init_tss[0].ist7);
// 函数_ switch_to首先将 next进程的内核层楼基地址设置到 TSS结构体对应的成员变量中 。
//随 后,保存当前进程的 FS与 GS段寄存器值,再将 next进程保存的 FS与 GS段寄存器值还原 。
__asm__ __volatile__("movq %%fs, %0 \n\t":"=a"(prev->thread->fs));
__asm__ __volatile__("movq %%gs, %0 \n\t":"=a"(prev->thread->gs));
__asm__ __volatile__("movq %0, %%fs \n\t"::"a"(next->thread->fs));
__asm__ __volatile__("movq %0, %%gs \n\t"::"a"(next->thread->gs));
color_printk(WHITE,BLACK,"prev->thread->rsp0:%#018lx\n",prev->thread->rsp0);
color_printk(WHITE,BLACK,"next->thread->rsp0:%#018lx\n",next->thread->rsp0);
}
#define switch_to(prev,next) \
do{ \
__asm__ __volatile__ ( "pushq %%rbp \n\t" \
"pushq %%rax \n\t" \
"movq %%rsp, %0 \n\t" \
"movq %2, %%rsp \n\t" \
"leaq 1f(%%rip), %%rax \n\t" \
"movq %%rax, %1 \n\t" \
"pushq %3 \n\t" \ /* kernel_thread_func */
"jmp __switch_to \n\t" \
/*
In simplified terms:
call address
This will push the updated program counter (which points to the instruction after the call) onto the stack then jump to the address indicated (addressing modes may apply).
ret
This instruction internally pops and address off the stack and jumps to it. This is nicely matched with call so it can return to the instruction after the prior call.
jmp address
This simply jumps to the given address (addressing modes may apply). It doesn't do anything with the stack at all.
So, you can also do this:
push address
ret
Which will pop and jump to the address that was pushed onto the stack as described above. It's a clever way to do an indirect jump in a microprocessor that doesn't support indirect addressing modes in their jump instructions.
The sequence:
push address
jmp someplace
Will simply jump to someplace and not affect the stack or use the address that was pushed onto the stack. If address is the instruction after the jmp, this is roughly equivalent to call someplace.
For instruction sets that don't support an indirect addressing jump, I've seen this nice little work-around:
push address
ret
Which will jump to whatever address is*/
"1: \n\t" \
"popq %%rax \n\t" \
"popq %%rbp \n\t" \
:"=m"(prev->thread->rsp)/* %0 */,"=m"(prev->thread->rip) /* %1 */ \
:"m"(next->thread->rsp) /* %2 */,"m"(next->thread->rip) /* %3 */,"D"(prev),"S"(next) \
:"memory" \
); \
}while(0)

下图描述与第二个进程 init 相关的完整流程 ,因此有关第一个进程init_task_union的部分代码都省略了,完整代码参见配书代码包程序4-11

(1). 创建第一个进程
/*
将联合体unio口 task_union实例化成全局变量init_task unio口,并将其作为 操作系统的第一个进程。
进程控制结构体数组init_task (指针数组)是为各处理器创建的初始进程 控制结构体,目前只有数组的第0个元素已投入使用,
剩余成员将在多核处理器初始化后予以创建
*/
#define INIT_TASK(tsk) \
{ \
.state = TASK_UNINTERRUPTIBLE, \
.flags = PF_KTHREAD, \
.mm = &init_mm, \
.thread = &init_thread, \
.addr_limit = 0xffff800000000000, \
.pid = 0, \
.counter = 1, \
.signal = 0, \
.priority = 0 \
}
union task_union init_task_union __attribute__((__section__ (".data.init_task"))) = {INIT_TASK(init_task_union.task)};
kernel_thread()
从这里开始,着手设置与第二个进程init相关的各种结构、数据
创建并声明了结构体regs ,也是在这里 填入了init的入口地址以及 kernel_thread_func() 的入口地址
struct pt_regs regs;
memset(®s,0,sizeof(regs));
struct pt_regs
{
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rbx;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
unsigned long rbp;
unsigned long ds;
unsigned long es;
unsigned long rax;
unsigned long func;
unsigned long errcode;
unsigned long rip;
unsigned long cs;
unsigned long rflags;
unsigned long rsp;
unsigned long ss;
};
初始化pt_regs
struct pt_regs regs;
memset(®s,0,sizeof(regs));
regs.rbx = (unsigned long)fn;
regs.rdx = (unsigned long)arg;
regs.ds = KERNEL_DS;
regs.es = KERNEL_DS;
regs.cs = KERNEL_CS;
regs.ss = KERNEL_DS;
regs.rflags = (1 << 9);
把上面的那个regs结构体的数据复制到了开辟的内存空间
memcpy(regs,
(void *)((unsigned long)tsk + STACK_SIZE - sizeof(struct pt_regs)),
sizeof(struct pt_regs));
调用 switch_to(current,p),会导致kernel_thread_func 方法的调用
/*
popq 出所有的pt_regs 结构体成员变量
*/
extern void kernel_thread_func(void);
__asm__ (
"kernel_thread_func: \n\t"
" popq %r15 \n\t"
" popq %r14 \n\t"
" popq %r13 \n\t"
" popq %r12 \n\t"
" popq %r11 \n\t"
" popq %r10 \n\t"
" popq %r9 \n\t"
" popq %r8 \n\t"
" popq %rbx \n\t" /*fn*/
" popq %rcx \n\t"
" popq %rdx \n\t" /*arg*/
" popq %rsi \n\t"
" popq %rdi \n\t"
" popq %rbp \n\t"
" popq %rax \n\t"
" movq %rax, %ds \n\t"
" popq %rax \n\t"
" movq %rax, %es \n\t"
" popq %rax \n\t"
" addq $0x38, %rsp \n\t"
/*
通过 addq $0x38, %rsp 指令 pop 出 如下寄存器
unsigned long func;
unsigned long errcode;
unsigned long rip;
unsigned long cs;
unsigned long rflags;
unsigned long rsp;
unsigned long ss;
使 thd->rsp = thd->rsp0 = (unsigned long)tsk + STACK_SIZE;
*/
/////////////////////////////////
" movq %rdx, %rdi \n\t" /* 新开辟的进程需要执行的函数的参数 */
" callq *%rbx \n\t" /* 新开辟的进程需要执行的函数 */
" movq %rax, %rdi \n\t" /* 将返回值存入 %rdi寄存器,调用 do_exit 函数*/
" callq do_exit \n\t"
);
在什么时候寄存器RSP的值被设置成了第一个进程的栈基地址
第一个进程的栈空间,本质来源于全局变量联合体init_task_union其中的数组字段,这个全局变量位于整个内核的.data.init_task段内
这里可以看到标号_stack_start处就是 就是数值 第一个进程栈空间栈基地址处
ENTRY(_stack_start)
.quad init_task_union + 32768
在源码文件 head.S可以看到代码的布局,其中movq _stack_start(%rip), %rsp设置寄存器的RSP值成了第一个进程的栈空间的基地址
#include "linkage.h"
.section .text
ENTRY(_start)
. . .
mov $0x7E00, %esp
//======= load GDTR
//======= load IDTR
//======= load cr3
//======= 64-bit mode code
switch_seg:
.quad entry64
entry64:
. . .
movq _stack_start(%rip), %rsp
/* rsp address */
setup_IDT:
. . .
rp_sidt:
. . .
setup_TSS64:
. . .
go_to_kernel:
.quad Start_Kernel
在反汇编文件里可以查出语句所在的线性地址0xffff800000100065
ffff800000100052 <entry64>:
ffff800000100065: 48 8b 25 49 01 00 00 mov 0x149(%rip),%rsp # ffff8000001001b5 <_stack_start>
设置断点 ,查看运行前后的RSP,发现从0x7E00变成了0x120000
<bochs:8> b 0x100065
<bochs:9> c
(0) Breakpoint 3, 0xffff800000100065 in ?? ()
Next at t=59413777
(0) [0x000000100065] 0008:ffff800000100065
(unk. ctxt): mov rsp, qword ptr ds:[rip+329] ; 488b2549010000
<bochs:10> info cpu
CPU0:
rsp: 00000000_00007e00
<bochs:11> s
<bochs:12> info cpu
CPU0:
rsp: ffff8000_00120000
何时才将寄存器RSP的值设置成第二个进程的栈基地址
刚进入switch_to(宏)的时候,寄存器RSP的值是rsp: ffff8000_0011ff58
<bochs:7> info cpu
CPU0:
rax: ffff8000_00118000 rcx: 00000000_00000000
rdx: ffff8000_00200000 rbx: ffff8000_0010d700
rsp: ffff8000_0011ff58 rbp: ffff8000_0011ffa8
rsi: ffff8000_00200000 rdi: ffff8000_00118000
r8 : 00000000_00ffffff r9 : 00000000_00000000
r10: ffff8000_00007c00 r11: ffff8000_00007c00
r12: ffff8000_00200058 r13: ffff8000_00200058
r14: ffff8000_0010d700 r15: 00000000_00000000
rip: ffff8000_0010b5b2
执行完 movq %2, %%rsp之后,寄存器的RSP存入第二个进程的rsp,成为rsp: ffff8000_00207f40,这个值可以手动计算出来
// sizeof(struct pt_regs) == 192 Bytes 栈 用于存放 192字节的寄存器值
thd->rsp = (unsigned long)tsk + STACK_SIZE - sizeof(struct pt_regs); // = 0x200000 + 0x8000 - 0xc0 = 0x207f40
<bochs:10> s
Next at t=62987240
(0) [0x00000010b5b8] 0008:ffff80000010b5b8
(unk. ctxt): mov rsp, qword ptr ds:[r12+16] ; 498b642410
<bochs:11> s
Next at t=62987241
(0) [0x00000010b5bd] 0008:ffff80000010b5bd
(unk. ctxt): lea rax, qword ptr ds:[rip+13] ; 488d050d000000
<bochs:12> info cpu
CPU0:
rax: ffff8000_00118000 rcx: 00000000_00000000
rdx: ffff8000_00200000 rbx: ffff8000_0010d700
rsp: ffff8000_00207f40 rbp: ffff8000_0011ffa8
rsi: ffff8000_00200000 rdi: ffff8000_00118000
r8 : 00000000_00ffffff r9 : 00000000_00000000
r10: ffff8000_00007c00 r11: ffff8000_00007c00
r12: ffff8000_00200058 r13: ffff8000_00200058
r14: ffff8000_0010d700 r15: 00000000_00000000
rip: ffff8000_0010b5bd
执行完pushq %3
<bochs:14> s
Next at t=62987243
(0) [0x00000010b5c8] 0008:ffff80000010b5c8
(unk. ctxt): push qword ptr ds:[r13+8] ; 41ff7508
<bochs:15> info cpu
CPU0:
rax: ffff8000_0010b5d1 rcx: 00000000_00000000
rdx: ffff8000_00200000 rbx: ffff8000_0010d700
rsp: ffff8000_00207f40 rbp: ffff8000_0011ffa8
rsi: ffff8000_00200000 rdi: ffff8000_00118000
r8 : 00000000_00ffffff r9 : 00000000_00000000
r10: ffff8000_00007c00 r11: ffff8000_00007c00
r12: ffff8000_00200058 r13: ffff8000_00200058
r14: ffff8000_0010d700 r15: 00000000_00000000
rip: ffff8000_0010b5c8
<bochs:16> s
Next at t=62987244
(0) [0x00000010b5cc] 0008:ffff80000010b5cc (unk. ctxt): jmp .-1138 (0xffff80000010b15f) ; e98efbffff
<bochs:17> info cpu
CPU0:
rax: ffff8000_0010b5d1 rcx: 00000000_00000000
rdx: ffff8000_00200000 rbx: ffff8000_0010d700
rsp: ffff8000_00207f38 rbp: ffff8000_0011ffa8
rsi: ffff8000_00200000 rdi: ffff8000_00118000
r8 : 00000000_00ffffff r9 : 00000000_00000000
r10: ffff8000_00007c00 r11: ffff8000_00007c00
r12: ffff8000_00200058 r13: ffff8000_00200058
r14: ffff8000_0010d700 r15: 00000000_00000000
rip: ffff8000_0010b5cc
查看当前的栈内容,栈顶[0xffff8000:0x0010b06b]
<bochs:18> print-stack
Stack address size 8
| STACK 0xffff800000207f38 [0xffff8000:0x0010b06b]
| STACK 0xffff800000207f40 [0x00000000:0x00000000]
| STACK 0xffff800000207f48 [0x00000000:0x00000000]
| STACK 0xffff800000207f50 [0x00000000:0x00000000]
. . .
[0xffff8000:0x0010b06b] 是kernel_thread_func的入口地址,这就是为什么函数__switch_to执行完后会跳转到kernel_thread_func执行的原因
ffff80000010b06b <kernel_thread_func>:
ffff80000010b06b: 41 5f pop %r15
ffff80000010b06d: 41 5e pop %r14
. . .
直接设置断点到函数__switch_to的retq处,可见当前处于内核代码段cs:0x0008 Code segment,而retq指令可以视为等价于IP=0x0010b06b(kernel_thread_func)
<bochs:19> b 0x10b2ed
<bochs:20> c
(0) Breakpoint 3, 0xffff80000010b2ed in ?? ()
Next at t=63156040
(0) [0x00000010b2ed] 0008:ffff80000010b2ed (unk. ctxt): ret ; c3
<bochs:22> sreg
cs:0x0008, dh=0x00209900, dl=0x00000000, valid=1
Code segment, base=0x00000000, limit=0x00000000, Execute-Only, Non-Conforming, Accessed, 64-bit
ss:0x0010, dh=0x00009300, dl=0x00000000, valid=1
Data segment, base=0x00000000, limit=0x00000000, Read/Write, Accessed
接着进行单步执行,就进入了函数10b06b <kernel_thread_func>,当前rip的数值就可以验证这一点
<bochs:23> s
Next at t=63156041
(0) [0x00000010b06b] 0008:ffff80000010b06b
(unk. ctxt): pop r15 ; 415f
<bochs:24> info cpu
CPU0:
rsp: ffff8000_00207f40
rip: ffff8000_0010b06b
sysexit
简单理解:
从内核层跳转至应用层。
背景:
由于系统内核位于 0特权级的内核层,而应用程序位于 3特权级的应用层,若想从内核层进入应用 层,在特权级跳转的过程中必须提供目标代码段和栈段以及其他跳转信息。读者首先想到的解决方法 可能是,借助 RET类指令从伪造的函数调用现场中返回到目标程序段。经过以上描述,我们相信读者对这个执行过程己经不再陌生 。 现在可参照-些图书或老版本的
Linux内核介绍的那样,通过 RET指令(调用返回指令)或 IRET指令(中断返回指令)来达到跳转至 应用层的目的。不过, RET类指令的执行速度特别慢,考虑到这类指令消耗的处理器时钊1周期数过多, Intel推出了 一套新的指令 SYSENTER/SYSEXIT实现快速系统调用 。
指令 SYSENTER/SYSEXIT的显著优点是,整个调用过程不会执行数据压钱,这样就免去了访问内
存的时间消耗,而且在跨特权级跳转时不会对段描述进行检测,从而使得 SYSENTER/SYSEXIT指令 能够执行得更快。 但SYSENTER指令只能从3特权级跳转至0特权级,而SYSEX工T指令只能从0特权级跳 转至3特权级,这使得两者无法完成其他特权级甚至是相同特权级间的跳转 。 因此,它们只能为应用 程序提供系统调用,无法在内核层执行系统调用,而中断型系统调用却可在任意权限下执行 。
使用:
本节将借助 SYSEXIT汇编指令来实现内核层向应用层的跳转,以下是对 SYSEXIT汇编指令的概括 描述。
SYSEXIT指令是一个快速返回 3特权级的指令,它只能执行在 oq夺权级下 。 在执行 SYSEXIT指令之 前,处理器必须为其提供外夺权级的衔接程序以及3特权级的检空间,这些数据将保存在MSR寄存器和通用寄存器中 。
口 IA32_SYSENTER_CS (位于MSR寄存器组地址 174h处 )。 它是一个32位寄存器,用于索引3特权
级下的代码段选择子和栈段选择子 。 在 IA-32e模式 下 , 代码段选 择子为 IA32_SYSENTER_
CS[15 : 0]+32,否则为 IA32 SYSENTER_ CS [15 : 0]+16; 而栈段选择 子是将代码段选择子加 8。
口 RDX寄存器。 该寄存器保存着一个Canonical型地址 ( 64位特有地址结构,将在第6章中详细介 绍),在执行指令时会将其载入到RIP寄存器中(这是用户程序的第一条指令地址)。 如果返回
到非64位模式, 那么只有低32位被装载到RIP寄存器中。
口 RCX寄存器。 该寄存器保存着一个Canonical型地址,执行指令时会将其载入到RSP寄存器中(这
是3特权级下的核指针)。 如果返回到非64位模式, 只有低32位被装载到RSP寄存器中。
实现:
由上文可知:
switch_to(current,p); ==> kernel_thread_func ==> unsigned long init(unsigned long arg)
现在来看下 init 方法,
unsigned long init(unsigned long arg)
{
struct pt_regs *regs;
color_printk(RED,BLACK,"init task is running,arg:%#018lx\n",arg);
current->thread->rip = (unsigned long)ret_system_call; // 首先跳转到应用层
current->thread->rsp = (unsigned long)current + STACK_SIZE - sizeof(struct pt_regs);
regs = (struct pt_regs *)current->thread->rsp;
__asm__ __volatile__ ( "movq %1, %%rsp \n\t"
"pushq %2 \n\t"
"jmp do_execve \n\t"
::"D"(regs),"m"(current->thread->rsp),"m"(current->thread->rip):"memory");
return 1;
}
其将ret_system_call方法push到栈顶,待jmp do_execve 指令执行后,去执行 do_execve 方法,do_execve 为 ret_system_call 做准备
unsigned long do_execve(struct pt_regs * regs)
{
regs->rdx = 0x800000; //RIP 这里会通过 ret_system_call 方法 的 popq %rdx 存入 %rdx
regs->rcx = 0xa00000; //RSP 这里会通过 ret_system_call 方法 的 popq %rdx 存入 %rcx
regs->rax = 1;
regs->ds = 0;
regs->es = 0;
color_printk(RED,BLACK,"do_execve task is running\n");
memcpy(user_level_function,(void *)0x800000,1024);
return 0;
}
现在看下 ret_system_call 方法
/* 函数system_call_function返回后,会继续执行 .global ret_system_call; */
/*
struct pt_regs
{
unsigned long r15; // 0x00
unsigned long r14; // 0x08
unsigned long r13; // 0x10
unsigned long r12; // 0x18
unsigned long r11; // 0x20
unsigned long r10; // 0x28
unsigned long r9; // 0x30
unsigned long r8; // 0x38
unsigned long rbx; // 0x40
unsigned long rcx; // 0x48
unsigned long rdx; // 0x50
unsigned long rsi; // 0x58
unsigned long rdi; // 0x60
unsigned long rbp; // 0x68
unsigned long ds; // 0x70
unsigned long es; // 0x78
unsigned long rax; // 0x80
unsigned long func;
unsigned long errcode;
unsigned long rip;
unsigned long cs;
unsigned long rflags;
unsigned long rsp;
unsigned long ss;
};
这段程序首先将系统调用的返回值更新到程序运行环境的RAX寄存器中,然后恢复应用程序 的
执行环境。由于 SYSEXIT指令需要借助 RDX与 RCX寄存器来恢复应用程序的执行现场 ,
所以在进入 内核层前应该对两者进行特殊处理(在应用程序中实现〉。最后,使用 SYSEX IT指令跳转至应用层执 行程序。
*/
.global ret_system_call; ret_system_call:
movq %rax, 0x80(%rsp) // regs.rax = %rax = do_execve 方法的返回值
popq %r15
popq %r14
popq %r13
popq %r12
popq %r11
popq %r10
popq %r9
popq %r8
popq %rbx
popq %rcx
popq %rdx
popq %rsi
popq %rdi
popq %rbp
popq %rax
movq %rax, %ds
popq %rax
movq %rax, %es
popq %rax
addq $0x38, %rsp
.byte 0x48
sysexit
执行 sysexit指令后,开始执行 user_level_function 方法
void user_level_function()
{
long ret = 0;
// color_printk(RED,BLACK,"user_level_function task is running\n");
char string[]="Hello World!\n";
__asm__ __volatile__ ( "leaq sysexit_return_address(%%rip), %%rdx \n\t"
"movq %%rsp, %%rcx \n\t"
"sysenter \n\t"
"sysexit_return_address: \n\t"
:"=a"(ret) /* %rax */:"0"(1),"D"(string):"memory"); /*"=a"(ret) 代表 系统调用sys_printf函数的返回值 */
// color_printk(RED,BLACK,"user_level_function task called sysenter,ret:%ld\n",ret);
while(1);
}
其中这个内嵌汇编语言里面的这两条语句,为sysenter后面的sysexit的执行准备执行环境
"leaq sysexit_return_address(%%rip), %%rdx \n\t"
"movq %%rsp, %%rcx \n\t"
sysenter
简单理解:
从应用层跳转至内核层。
背景:
系统调用 API作为应用层与内核层间的重要通信手段己被使用到各种应用场合,但应用层与内核层间的通信手段不只系统调用API一种,我们还可采用中断、地址重映射等方式在这两层间建立通信链接,不过最广泛使用的通信方式依然是系统调用API。
本节将会继续补充完系统调用 API的主体框架,即通过 SYSENTER汇编指令实现应用层到内核层的跳转。下面依然从SYSENTER汇编指令的功能描述开始逐步实现系统调用API。
使用:
SYSENTER指令是一个快速进入3特权级的指令。 在执行SYSENTER指令之前,处理器必须为其提 供0特权级的衔接程序以及0特权级的栈空间,这些数据将保存在MSR寄存器和通用寄存器中 。
口 IA32_SYSENTER_CS (MSR寄存器组地址174h处)。 这个MSR寄存器的低16位装载的是0特 权级的代码段选择子,该值也用于索寻 10特权级的战段选择子( IA32 SYSENTER CS [15:0]+8 ),因此其值不能为NULL。
口 IA32_SYSENTER_ESP ( MSR寄存器组地址 175h处 )。 这个MSR寄存器里的值将会被载入到 RSP寄存器中,该值必须是Canonical型地址。 在保护模式下,只有寄存器的低32位被载入到RSP 寄存器中。
口 IA32_SYSENTER_EIP( MSR寄存器组地址 176h处 )。 这个MSR寄存器里的值将会被载入到RIP 寄存器中,该值必须是Canonical型地址。 在保护模式下,只有寄存器的低32位被载入到RIP寄 存器中 。
在执行SYSENTER指令的过程中,处理器会根据IA32_SYSENTER_CS寄存器的值加载相应的段选择子到 CS和 SS寄存器 。 SYSENTER指令与 SYSEXIT指令都必须由操作系统负责确保段描述符的正确性。
SYSENTER/SYSEXIT指令与 CALL/RET指令的不同之处在于,执行 SYSENTER指令时,处理器不会
保存用户代码的状态信息(如 RIP和 RSP寄存器值),而且两者均不支持内存 参数方式。
实现:
在task_init方法中,已经提前准备好sysenter执行所需要的环境
void task_init()
{
...
wrmsr(0x174,KERNEL_CS);
wrmsr(0x175,current->thread->rsp0);
wrmsr(0x176,(unsigned long)system_call);
...
}
现在一起看下system_call和ret_system_call方法,在执行完system_call方法后,会自动执行ret_system_call方法,同时system_call_function系统调用函数的返回值会通过指令“movq %rax, 0x80(%rsp) // regs.rax = %rax = do_execve | system_call_function” 方法的返回值保存在%rax中
.global system_call; system_call:
sti
subq $0x38, %rsp
cld;
pushq %rax;
movq %es, %rax;
pushq %rax;
movq %ds, %rax;
pushq %rax;
xorq %rax, %rax;
pushq %rbp;
pushq %rdi;
pushq %rsi;
pushq %rdx;
pushq %rcx;
pushq %rbx;
pushq %r8;
pushq %r9;
pushq %r10;
pushq %r11;
pushq %r12;
pushq %r13;
pushq %r14;
pushq %r15;
movq $0x10, %rdx;
movq %rdx, %ds;
movq %rdx, %es;
movq %rsp, %rdi
callq system_call_function
/* 函数system_call_function返回后,会继续执行 .global ret_system_call; */
/*
struct pt_regs
{
unsigned long r15; // 0x00
unsigned long r14; // 0x08
unsigned long r13; // 0x10
unsigned long r12; // 0x18
unsigned long r11; // 0x20
unsigned long r10; // 0x28
unsigned long r9; // 0x30
unsigned long r8; // 0x38
unsigned long rbx; // 0x40
unsigned long rcx; // 0x48
unsigned long rdx; // 0x50
unsigned long rsi; // 0x58
unsigned long rdi; // 0x60
unsigned long rbp; // 0x68
unsigned long ds; // 0x70
unsigned long es; // 0x78
unsigned long rax; // 0x80
unsigned long func;
unsigned long errcode;
unsigned long rip;
unsigned long cs;
unsigned long rflags;
unsigned long rsp;
unsigned long ss;
};
这段程序首先将系统调用的返回值更新到程序运行环境的RAX寄存器中,然后恢复应用程序 的
执行环境。由于 SYSEXIT指令需要借助 RDX与 RCX寄存器来恢复应用程序的执行现场 ,
所以在进入 内核层前应该对两者进行特殊处理(在应用程序中实现〉。最后,使用 SYSEXIT指令跳转至应用层执 行程序。
*/
.global ret_system_call; ret_system_call:
movq %rax, 0x80(%rsp) // regs.rax = %rax = do_execve | system_call_function 方法的返回值
popq %r15
popq %r14
popq %r13
popq %r12
popq %r11
popq %r10
popq %r9
popq %r8
popq %rbx
popq %rcx
popq %rdx
popq %rsi
popq %rdi
popq %rbp
popq %rax
movq %rax, %ds
popq %rax
movq %rax, %es
popq %rax
addq $0x38, %rsp
.byte 0x48
sysexit
执行sysexit时,会跳转到sysexit_return_address,下面是sysexit_return_address的反汇编代码
sysexit_return_address:
ffff80000010accf: 48 89 45 f8 movq %rax, -8(%rbp)
ffff80000010acd3: eb fe jmp -2 <sysexit_return_address+0x4>
到此系统调用 sysenter 方法所有流程已经执行完毕。
总结:
创建第一个内核线程:init_task_union ==> 创建第二个内核线程:kernel_thread ==> 切换至第二个内核线程:switch_to ==> kernel_thread_func [callq *%rbx] ==> init ==> 为下一步ret_system_call方法做准备:do_execve ==> 跳转到应用层:ret_system_call ==> user_level_function ==> 设置%rdx、%rcx 为下一次ret_system_call方法做准备 ==>跳转到内核层 sysenter ==>执行系统调用:system_call_function ==> 顺带执行sysexit,返回应用层:sysexit ==> sysexit_return_address
从反汇编代码理解内核代码
sys_printf
sys_printf:
ffff800000104abc: 55 pushq %rbp
ffff800000104abd: 48 89 e5 movq %rsp, %rbp
ffff800000104ac0: 48 83 ec 10 subq $16, %rsp
ffff800000104ac4: 48 89 7d f8 movq %rdi, -8(%rbp)
ffff800000104ac8: 48 8b 45 f8 movq -8(%rbp), %rax
ffff800000104acc: 48 8b 40 60 movq 96(%rax), %rax
ffff800000104ad0: 48 89 c2 movq %rax, %rdx
ffff800000104ad3: be ff ff ff 00 movl $16777215, %esi /* 0x00FFFFFF WHITE */
ffff800000104ad8: bf 00 00 00 00 movl $0, %edi /* 0x00000000 BLACK */
ffff800000104add: b8 00 00 00 00 movl $0, %eax
ffff800000104ae2: 48 b9 4f 5f 10 00 00 80 ff ff movabsq $-140737487282353, %rcx /*同一个数值,原码、补码、反码的表示各不相同,但是可以根据 补码 = 原码求反 + 1 获取补码 原码:0x8000 7FFF FFEF A0B1 => 补码:0xFFFF 8000 0010 5F4F*/
ffff800000104aec: ff d1 callq *%rcx
ffff800000104aee: b8 01 00 00 00 movl $1, %eax
ffff800000104af3: c9 leave
ffff800000104af4: c3 retq
user_level_function
/*
* b = byte (8 bit)
* s = short (16 bit integer) or single (32-bit floating point)
* w = word (16 bit)
* l = long (32 bit integer or 64-bit floating point)
* q = quad (64 bit)
* t = ten bytes (80-bit floating point)
user_level_function:
ffff80000010ac90: 55 pushq %rbp
ffff80000010ac91: 48 89 e5 movq %rsp, %rbp
ffff80000010ac94: 48 c7 45 f8 00 00 00 00 movq $0, -8(%rbp)
ffff80000010ac9c: 48 b8 48 65 6c 6c 6f 20 57 6f movabsq $8022916924116329800, %rax. /* = 0x6F57206F6C6C6548 = oW olleH*/
ffff80000010aca6: 48 89 45 e0 movq %rax, -32(%rbp)
ffff80000010acaa: c7 45 e8 72 6c 64 21 movl $560229490, -24(%rbp) /* = 0x21646C72 = !dlr */
ffff80000010acb1: 66 c7 45 ec 0a 00 movw $10, -20(%rbp). /* = 0xa = \n */
ffff80000010acb7: b8 01 00 00 00 movl $1, %eax
ffff80000010acbc: 48 8d 55 e0 leaq -32(%rbp), %rdx
ffff80000010acc0: 48 89 d7 movq %rdx, %rdi
ffff80000010acc3: 48 8d 15 05 00 00 00 leaq 5(%rip), %rdx
ffff80000010acca: 48 89 e1 movq %rsp, %rcx
ffff80000010accd: 0f 34 sysenter
本文深入探讨Linux内核中的进程管理,包括进程创建、切换的关键步骤。重点讲解了kernel_thread()函数如何创建第一个进程,以及寄存器RSP在进程切换中的变化。详细解析了sysexit和sysenter指令在内核与用户层跳转中的作用,并介绍了如何通过反汇编代码理解内核代码执行流程。

541

被折叠的 条评论
为什么被折叠?



