一个操作系统的设计与实现——第23章 快速系统调用
23.1 什么是快速系统调用
系统调用是操作系统为3特权级任务提供服务的一种手段。在32位操作系统中,我们通过中断实现了系统调用。由于系统调用是一个使用非常频繁的机制,且中断也不是专门为系统调用设计的,因此,64位CPU提供了系统调用的专用机制:快速系统调用。
快速系统调用由专用的
syscall
指令发起,并由专用的
sysret
指令返回。
syscall
必须从3特权级转移到0特权级,
sysret
必须从0特权级返回到3特权级。快速系统调用全程使用寄存器传参,并且系统调用函数的
cs:rip
是预设好的,因此,
syscall/sysret
均不需要参数。
综上,快速系统调用的整套机制都是非常固定的,这就带来了高效率。
23.2 快速系统调用的安装
在使用快速系统调用之前,需要先安装好快速系统调用所需的组件,这涉及到4个MSR。
23.2.1
IA32_EFER
快速系统调用这个功能在初始状态下是关闭的,其开关位于
IA32_EFER
的第0位。这个MSR我们已经见过了,它的编号为
0xc0000080
。
23.2.2
IA32_STAR
这个MSR的低32位是保留位;第32~47位用于设定
syscall
使用的0特权级段选择子;第48~63位用于设定
sysret
使用的3特权级段选择子。
注意,这里没有说设定的是"代码段选择子",而仅仅是"段选择子",这是因为选择子的设定有一套比较奇怪的定义:
- 对于第32~47位,其数值本身会被视为0特权级代码段选择子;这个数值加8得到的数值会被视为0特权级数据段选择子
- 对于第48~63位,其数值本身会被视为3特权级兼容模式代码段选择子;这个数值加8得到的数值会被视为3特权级数据段选择子;这个数值加16得到的数值会被视为3特权级IA32-e模式代码段选择子。那么,当执行
sysret
时,其到底选择哪个代码段呢?这个问题将在下文中讨论
段选择子是描述符索引值左移3位得到的,因此加8即为GDT中的下一个描述符。也就是说,第32~47位设定的是两个连续的段描述符中的第一个;第48~63位设定的是三个连续的段描述符中的第一个。不过,由于我们的操作系统从不使用兼容模式代码段,因此在GDT中并没有定义这个描述符。
这个MSR的编号为
0xc0000081
。
23.2.3
IA32_LSTAR
这个MSR用于设定系统调用函数的地址,其编号为
0xc0000082
。
23.2.4
IA32_FMASK
这个MSR用于设定RFLAGS屏蔽掩码。具体来说,当执行
syscall
时,
rflags
会变成这样:
rflags &= ~IA32_FMASK
。在我们的操作系统中,这个MSR用于屏蔽IF位,屏蔽掩码为
0x200
。
这个MSR的编号为
0xc0000084
。
23.3
syscall
的执行细节
当执行
syscall
时,CPU会执行以下操作:
rcx = rip
r11 = rflags
cs = IA32_STAR[32:47]
rip = IA32_LSTAR
rflags &= ~IA32_FMASK
也就是说,
rcx
和
r11
会被
syscall
使用,它们不能用于传参。此外,
syscall
不会对
rsp
做任何处理,这是一个很重要的问题,我们将在下文中讨论。
23.4
sysret
的执行细节
当执行
sysret
时,CPU会执行以下操作:
rip = rcx
rflags = r11
- 如果
sysret
没有64位前缀,则:
cs = IA32_STAR[48:63]
;否则:
cs = IA32_STAR[48:63] + 16
也就是说:
- 操作系统需要保护
rcx
与
r11
sysret
需要具有64位前缀
上述第1点将在下文中讨论;第2点在nasm中可使用
o64 sysret
实现。
23.5 系统调用的实现
请看本章代码
23/Syscall.h
。
第3行,声明了
syscallInit
函数。这个函数是用汇编语言实现的。
接下来,请看本章代码
23/Syscall.s
。
第15~18行,将
IA32_EFER
的第0位置1,打开快速系统调用功能。
第20~23行,设定
IA32_STAR
。在GDT中,3号描述符是0特权级代码段,4号描述符是0特权级数据段,这两个段描述符对应于
IA32_STAR
的第32~47位;5号描述符是3特权级数据段,6号描述符是3特权级代码段,没有兼容模式代码段,因此,这里应强行将4号描述符安装到
IA32_STAR
的第48~63位,使得5号和6号描述符处于正确的位置。
第25~29行,将系统调用函数
syscallHandle
的地址安装到
IA32_LSTAR
。
第31~34行,将屏蔽掩码
0x200
安装到
IA32_FMASK
。
至此,快速系统调用准备完毕。
syscallHandle
函数为系统调用函数。在32位操作系统中,系统调用由中断实现,中断发生时,CPU会自动切换到0特权级栈,由于0特权级栈是操作系统提供的,所以能够保证它的安全。那么,什么叫"安全的栈"?如果不切换栈,到底有什么问题?请看下例:
void test()
{
char s[] = "666";
__asm__ __volatile__("syscall");
}
将这段代码翻译成汇编语言,可以是:
test:
mov dword [rsp - 4], '666'
syscall
ret
可以发现:这个函数的
rsp
是没有也不需要实际减去4的,但如果将这样的
rsp
提供给系统调用函数使用,就是错误的,因为系统调用函数不知道栈到底应该怎么用。这就是不安全栈带来的问题,因此,在系统调用时,切换到一个安全的栈是有必要的。
然而,
syscall
不会自动切换栈,我们需要手动完成这个操作。0特权级栈在TSS中,TSS的地址是
0xffff800000092000
,但想要使用这个地址,就必须先用一个寄存器周转64位立即数。用哪个寄存器呢?无关乎ABI,似乎用哪个都不完美。此时,我们之前设定的
IA32_GS_BASE
派上了用场,使用
gs
就可以直接操作TSS了。不仅如此,我们的操作系统的TSS是延长到128字节的,104字节以后的一小段内存可用于在换栈前备份当前的
rsp
。至此,换栈问题就完美解决了。
第44行,将
rsp
备份到
[TSS + 104]
。
第45行,切换到0特权级栈。
第47~48行,保护
rcx
与
r11
。现在的栈是安全的,可以放心使用。
第50~51行,调用
rax
指定的函数。
第53~54行,恢复
rcx
与
r11
。
第56行,恢复3特权级栈。
第58行,从快速系统调用返回。
第60~63行,定义了系统调用表。1号系统调用保留给后续章节使用。
接下来,请看本章代码
23/Start.s
。
_start
函数是3特权级任务的真正入口,其用于使任务在结束后自动退出。
23.6 编译与测试
本章代码
23/Makefile
增加了
Syscall.s
与
Start.s
的编译与链接命令。
本章代码
23/Kernel.c
与
23/Test.c
测试了0与2号系统调用。