SYSENTER指令

SYSENTER指令可以实现应用层到内核层的跳转。在执行SYSENTER之前,处理器必须为其提供0特权级的衔接程序以及0特权级的栈空间,这些数据将会保存在MSR寄存器组和通用寄存器中。

  • IA32_SYSENTER_CS,位于MSR寄存器组的地址174h处,这个MSR寄存器的低16位装载的是0特权级的代码段选择子,该值也用于所以0特权级的栈段选择子(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寄存器的值加载相应的段选择子到CSSS寄存器。

SYSENTER/SYSEXIT指令与CALL/RET指令的不同之处在于,执行SYSENTER指令时,处理器不会保存用户代码的状态信息(RIPRSP寄存器的值),而且两者均不支持内存参数方式。同时SYSENTER/SYSEXIT还必须遵循如下规则:

  • SYSENTER/SYSEXIT指令使用的段描述符皆位于同一个段描述符内,并且各个段描述符是相邻的。只有这样才能使处理器根据SYSENTER_CS_MSR寄存器值索引到段选择子。
  • 应用程序在执行SYSENTER指令进入内核层时,必须保存程序的运行环境,并在执行SYSEXIT指令返回应用层时恢复程序的运行环境。

实现系统调用

system_call模块实现:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// ...
ENTRY(system_call)
sti # SYSENTER汇编指令将会使RFLGAS.IF标志复位,在进入内核层后,必须手动使能中断(置位IF标志位)
subq $0x38, %rsp
cld

push %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

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

system_call模块必须在ret_system_call之前,在执行完成之后返回,也可以用JMP指令跳转。

system_call模块是系统调用API的接口模块。当应用程序执行SYSENTER指令进入内核层时,便会通过system_call模块保存应用程序执行现场,随后使用CALL指令调用system_call_function函数。在调用时system_call模块会将当前栈指针作为参数传递给system_call_function函数(movq %rsp, %rdi),此时的栈指针作为参数传递给system_call_function函数。

system_call_function函数实现:

1
2
3
unsigned long system_call_function(struct pt_regs *regs) {
return system_call_table[regs->rax](regs);
}

参数regs记录着进程执行环境,其中成员变量rax保存着系统调用API的向量号,暂定定128个。

数组system_call_table用于保存每个系统调用的处理函数,目前尚未实现,都赋值为no_system_call

1
2
3
4
5
6
7
8
9
10
11
12
#define MAX_SYSTEM_CALL_NR 128

typedef unsigned long (*system_call_t)(struct pt_regs *regs);

unsigned long no_system_call(struct pt_regs *regs) {
color_printk(RED, BLACK, "no_system_call is called, NR: %#04x\n", regs->rax);
return -1;
}

system_call_t system_call_table[MAX_SYSTEM_CALL_NR] = {
[0 ... MAX_SYSTEM_CALL_NR-1] = no_system_call
};

此外还需要为SYSENTER汇编指令内核层指针以及系统调用在内核层的入口地址(system_call的起始地址)。分别将两个值的写入MSR寄存器的175h176h地址处:

1
2
3
4
5
6
7
void task_init() {
// ...
wrmsr(0x174, KERNEL_CS);
wrmsr(0x175, current->thread->rsp0);
wrmsr(0x176, (unsigned long)system_call);
// ...
}

执行系统调用API。

在编写调用程序时,SYSEXIT指令执行须要向RCXRDX写入程序返回地址和栈顶地址。所以在执行SYSENTER指令前,将程序的返回地址和栈顶地址保存在这两个寄存器内:

1
2
3
4
5
6
7
8
9
10
11
12
void user_level_function() {
long ret = 0;
__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)
: "0"(15)
: "memory");
while (1)
;
}

通过汇编leaq指令取得标识符sysexit_return_address的有效地址,并将有效地址保存到RDX寄存器中。RCX寄存器保存着应用层当前的栈指针,RAX寄存器保存系统调用API的向量号。

运行结果:

Screenshot_20220828_003054