跳转过程

系统内核位于0特权级,应用程序位于3特权级,如果想从内核层进入应用层,在特权级跳转的过程中必须提供目标代码段和栈段以及其他跳转信息。

  1. 检测目标程序的访问权限,针对段模式的特权级进行检查
  2. 临时把SSESPCSEIP寄存器的当前值保存在处理器内部,以备调用返回使用
  3. 根据目标代码段的特权级,处理器从TSS结构中提取处相应特权级的段选择子和栈基地址,并将其作为目标程序的栈空间更新到SSESP寄存器
  4. 将步骤2临时保存的SSESP寄存器存入内目标程序的栈空间
  5. 根据调用门描述符记录的参数个数,从调用者栈空间复制参数到目标程序栈
  6. 将步骤2临时保存的CSEIP寄存器值存入目标程序的栈空间
  7. 将调用门描述符记录的目标代码段选择子和程序的起始地址加载到CSEIP寄存器中
  8. 处理器在目标代码段特权级下执行程序

Screenshot_20220825_200550

对于相同特权级的程序访问,处理器并不会切换程序的栈空间,所以只有参数、EIP寄存器以及CS寄存器会存入栈空间

还原调用者的执行环境:

  1. 检测目标程序的访问权限,此处同样针对段模式的特权级进行检查
  2. 还原调用者的CSEIP寄存器值,它们在调用过程中以保存在被调用者的栈空间
  3. 如果RET指令带有操作数n,那么栈指针将向上移动n个字节来释放被调用者栈空间。如果访问来自调用门,那么RET n指令将同时释放被调用者与调用者栈空间
  4. 还原调用者的SSEIP寄存器值,是的栈空间被调用者切换会调用者
  5. 如果RET指令带有操作数n,则按照步骤3的执行过程释放调用者栈空间
  6. 处理器继续执行调用者程序

RET(调用返回指令)和IRET(中断返回指令)类指令的执行速度特别慢,消耗处理器时钟周期数过多,INTEL退出一套新指令SYSENTER/SYSEXIT实现快速系统调用,这两个指令调用过程不会执行数据压栈,这样避免了访问内存的时间消耗。SYSENTER只能从3特权级跳转到0特权级,而SYSEXIT只能从0特权级跳转到3特权级。因此它们只能为应用程序提供系统调用,无法在内核层执行系统调用,而中断型系统调用确可以在任意权限下执行

在执行SYSEXIT指令之前,处理器必须为其提供3特权级的衔接程序以及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位特有的地址结构),在执行指令时会将其载入RIP寄存器中(这是用户程序的第一条指令地址)。如果返回到非64位模式,那么只有低32位被装载到RIP寄存器中
  • RCX寄存器。该寄存器保存者一个Canonical型地址,执行指令是会将其加载到RSP寄存器中(3特权级下的栈指针)。如果返回到非64位模式,只有低32位被装载到RSP寄存器。

IA32_SYSENTER_CS寄存器可借助RDMSR/WRMSR指令进行访问。在执行SYSEXIT指令的过程中,指令会根据IA32_SYSENTER_CS寄存器的值加载相应的段选择子到CSSS寄存器。SYSEXIT指令不会从描述符表中加载段描述符到CSSS寄存器,而是写入固定值 ,需由操作系统确保描述符的正确性。

切换实现

系统调用返回模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ENTRY(ret_system_call)
movq %rax, 0x80(%rsp)
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

首先将系统调用的返回值更新到程序运行环境的RAX寄存器中,然后恢复应用程序的执行环境。由于sysexit需要借助RDX和RCX寄存器来恢复应用程序的执行现场,所以在进入内核层前应该对两者特殊处理,最后使用sysexit跳转到应用层执行。

sysexit指令在64位模式下的默认操作数不是64位,如果要返回到64位模式的应用层,则必须在sysexit指令前插入指令前缀0x48加以修饰,以表示sysexit指令使用64位的操作数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// GDT Table
.section .data

.global GDT_Table

GDT_Table:
.quad 0x0000000000000000 // 0 null descriptor 00
.quad 0x0020980000000000 // 1 kernel code 64bit segment 08
.quad 0x0000920000000000 // 2 kernel data 64bit segment 10
.quad 0x0000000000000000 // 3 user data 32bit segment 18
.quad 0x0000000000000000 // 4 user data 32bit segment 20
.quad 0x0020f80000000000 // 5 user code 64bit segment 28
.quad 0x0000f20000000000 // 6 user data 64bit segment 30
.quad 0x00cf9a000000ffff // 7 kernel code 32bit segment 38
.quad 0x00cf92000000ffff // 8 kernel data 32bit segment 40
.fill 10, 8, 0 // 10~11 TSS(jmp one segment 9) in long-mode 128bit 50
GDT_END:

此处为GDT新增了一个32位的代码段描述符和一个32位的数据段描述符,它们被插入到GDT的第3个描述符处。这导致TSS段描述符向后移动了两个段描述符的位置,从而必须调整TSS段描述符的初始化程序setup_TSS64,以及调用宏函数load_tr时传入的参数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
setup_TSS64:
leaq TSS64_Table(%rip), %rdx
xorq %rax, %rax
xorq %rcx, %rcx
movq $0x89, %rax
shlq $40, %rax
movl %edx, %ecx
shrl $24, %ecx
shlq $56, %rcx
addq %rcx, %rax
xorq %rcx, %rcx
movl %edx, %ecx
andl $0xffffff, %ecx
shlq $16, %rcx
addq %rcx, %rax
addq $103, %rax
leaq GDT_Table(%rip), %rdi
movq %rax, 80(%rdi) // tss segment offset
shrq $32, %rdx
movq %rdx, 88(%rdi) // tss+1 segment offset

// mov $0x40, %ax
// ltr %ax

movq go_to_kernel(%rip), %rax
pushq $0x08
pushq %rax
lretq

初始化:

1
2
3
4
5
6
void start_kernel(void) {
。。。
load_TR(10);
。。。
}

这两段分别调整了TSS段描述符在GDT中的偏移值和TSS段选择子的值。

修改do_fork函数创建的新进程的返回地址:

1
2
3
4
5
6
7
8
unsigned long do_fork(struct pt_regs *regs, unsigned long clone_flags,
unsigned long stack_start, unsigned long stack_size) {
...
if (!(tsk->flags & PF_KTHREAD)) {
thd->rip = regs->rip = (unsigned long)ret_system_call;
}
...
}

系统数据结构准备就绪后,为IA32_SYSENTER_CS寄存器设置段选择子:

1
2
3
4
5
6
7
void task_init() {
...
init_mm.start_stack = _stack_start;

wrmsr(0x174, KERNEL_CS);
...
}

IA32_SYSENTER_CS寄存器位于MSR寄存器组的0x174地址处,所以处理器只能借助WRMSR汇编指令才能向MSR寄存器组写入数据。

wrmsr函数实现:

1
2
3
4
5
static inline void wrmsr(unsigned long address, unsigned long value) {
__asm__ __volatile__("wrmsr \n\t" ::"d"(value >> 32), "a"(value & 0xffffffff),
"c"(address)
: "memory");
}

系统目前还没有应用层程序,此时init还是个内核进程。执行execve系统调用API,可以使init内核线程执行新的程序,进而转变为应用程序。但是由于SYSENTER/SYSEXIT指令无法像中断指令那样,可以在内核层调用API,所以只能通过直接执行系统调用API处理函数的方法实现。参考switch_to函数的设计思路,调用execve系统调用API的处理函数do_execve,借助push指令,将程序的返回地址压入栈中,并采用JMP指令调用函数do_execve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

首先确定函数init的函数返回地址和栈指针,并取得进程的struct pt_regs结构体。接着采用内嵌汇编方法,更新进程的内核层栈指针,同时调用do_execve函数为新的程序(目标应用程序)准备执行环境,并将struct pt_regs结构体的首地址作为参数传递给do_execve函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// typedef int (*fun)(unsigned int, unsigned int, const char *, ...);

void user_level_function() {
// fun f = (void *)0xffff80000010a1b9;
// char msg[] = "user_level_function task is running...\n";
// f(RED, BLACK, msg);
// color_printk(RED, BLACK, "user_level_function task is running...\n");
while (1)
;
}

unsigned long do_execve(struct pt_regs *regs) {
regs->rdx = 0x800000; // RIP
// regs->rdx = (unsigned long) user_level_function; // RIP
regs->rcx = 0xa00000; // RSP
regs->rax = 1;
regs->ds = 0;
regs->es = 0;

color_printk(RED, BLACK, "do_execve task is running...\n");
color_printk(RED, BLACK, "do_execve address %#018lx...\n",
user_level_function);
color_printk(RED, BLACK, "do_execve address2 %#018lx...\n", color_printk);

memcpy(user_level_function, (void *)0x800000, 1024);

return 0;
}

此处有坑,当将user_level_function函数复制到0x800000地址处时,在user_level_function函数里调用color_printk时,显示物理地址访问不了。

启动bochs虚拟机,使用b 0x800000处打断点,当执行到call指令的时候,发现调用的call r8r8寄存器值不对,并不是color_printk函数的地址。

由此推测可能由于是将user_level_function函数直接拷贝到0x800000处,而获取color_printk函数的地址是根据user_level_function推出来的绝对地址,而相对位置变了,所以获取不到color_printk的地址。

三种曲线救国的方式

  1. 不要在拷贝的地址调用其他函数
1
2
3
4
5
void user_level_function() {
// color_printk(RED, BLACK, "user_level_function task is running...\n");
while (1)
;
}
  1. 指定函数的绝对地址
1
2
3
4
5
6
7
8
9
10
typedef int (*fun)(unsigned int, unsigned int, const char *, ...);

void user_level_function() {
fun f = (void *)0xffff80000010a1b9;
char msg[] = "user_level_function task is running...\n";
f(RED, BLACK, msg);
// color_printk(RED, BLACK, "user_level_function task is running...\n");
while (1)
;
}
  1. 不进行拷贝,直接跳转到原函数地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void user_level_function() {
color_printk(RED, BLACK, "user_level_function task is running...\n");
while (1)
;
}

unsigned long do_execve(struct pt_regs *regs) {
// regs->rdx = 0x800000; // RIP
regs->rdx = (unsigned long) user_level_function; // RIP
regs->rcx = 0xa00000; // RSP
regs->rax = 1;
regs->ds = 0;
regs->es = 0;

color_printk(RED, BLACK, "do_execve task is running...\n");
color_printk(RED, BLACK, "do_execve address %#018lx...\n",
user_level_function);
color_printk(RED, BLACK, "do_execve address2 %#018lx...\n", color_printk);

// memcpy(user_level_function, (void *)0x800000, 1024);

return 0;
}

本次采用第一种,不再用户级程序调用其他函数。

当处理器切换到应用层后,应用程序将从应用层的线性地址0x800000处开始执行,而选择线性地址0xa00000作为程序的栈顶地址,则是为了保证它与线性地址0x800000同在一个物理页内。

需要注释掉memory_init的页表映射的清理代码:

1
2
3
4
5
6
7
void memory_init() {
...
// for (i = 0; i < 10; i++)
// *(PHY_TO_VIRT(global_cr3) + i) = 0UL;
...
}

最后,改变页表属性的标志位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// init page
.align 8
.org 0x1000

__PML4E:
// bit 0,1,2 is 1, read/write, not allow user-mode access
.quad 0x102007
.fill 255, 8, 0
.quad 0x102007
.fill 255, 8, 0

.org 0x2000

__PDPTE:
// bit 0,1 is 1, read/write
.quad 0x103007 // 0x103003
.fill 511, 8, 0

.org 0x3000
__PDE:
// bit 0,1 is 1, read/write, bit 7 is 1, maps 2MB
// 0MB
.quad 0x000087
.quad 0x200087
.quad 0x400087
.quad 0x600087
.quad 0x800087 // 0x800083
// 10MB
.quad 0xe0000087 // 0xa00000
.quad 0xe0200087
.quad 0xe0400087
.quad 0xe0600087
.quad 0xe0800087
.quad 0xe0a00087
.quad 0xe0c00087
.quad 0xe0e00087
// 16MB
.fill 499, 8, 0

运行结果如下:

Screenshot_20220827_013450