TL;DR
syscall
指令本身就像是一个高级跳转,它是一种硬件支持的方式,可以有效且安全地从非特权用户空间跳转到内核。
syscall
指令跳转到一个内核入口点来分派调用。
在x86_64之前,还使用了另外两种机制: int
指令和sysenter
指令。
它们具有不同的入口点(32位内核中仍然存在,以及可以运行32位用户空间程序的64位内核中也存在)。
前者使用了x86中断机制,并且可能会与异常分派混淆(异常也使用了中断机制)。
但是,异常是虚假事件,而int
用于生成软件中断,同样是一种高级跳转。
C语言不涉及系统调用,它依赖
C运行时来执行与未来程序环境的所有交互。
C运行时通过特定于环境的机制实现上述交互。可能会有各种软件抽象层,但最终会调用操作系统API。
术语
API用于表示一个协议,严格地说,使用API并不需要调用内核代码(趋势是在用户空间实现非关键函数以限制可利用的代码),这里我们只对需要特权切换的API子集感兴趣。
在Linux下,内核提供了一组可从用户空间访问的服务入口,这些入口点被称为
系统调用。
在Windows下,内核服务(通过与Linux类似的机制访问)被认为是私有的,因为它们不需要跨版本稳定。
而是使用一组导出的DLL / EXE函数作为入口点(例如ntoskrnl.exe、hal.dll、kernel32.dll、user32.dll),这些函数再通过一个(私有的)系统调用来使用内核服务。
请注意,在Linux下,大多数系统调用都有一个POSIX包装器,因此可以使用这些包装器(普通的C函数)来调用系统调用。
底层ABI不同,错误报告也不同;包装器在两个世界之间进行转换。
C运行时调用操作系统API,在Linux中直接使用系统调用,因为它们是公共的(在跨版本上稳定),而对于Windows,像kernel32.dll这样的常规DLL被标记为依赖项并使用。
我们被迫到了这么一个地步,即用户模式程序,无论是作为C运行时的一部分(Linux)还是作为API DLL的一部分(Windows),都需要调用内核中的代码。
x86架构在历史上提供了不同的方法,例如 call gate。
另一种方法是通过int指令完成,它有一些优点:
- 这是BIOS和DOS在其时代所做的。在实模式下,使用
int
指令是合适的,因为向量号(例如21h
)比远地址(例如0f000h:0fff0h
)更容易记住。
- 它保存标志位。
- 设置中断服务程序(ISR)相对容易。
随着体系结构的现代化,这种机制的缺点变得很大:它很慢。在引入sysenter
指令之前,没有更快的替代方案(调用门也同样慢)。
随着Pentium Pro/II的问世
[1],一对新的指令
sysenter
和
sysexit
被引入以加快系统调用。
Linux从版本2.5开始使用它们{{link2:},在32位系统上至今仍在使用。
我不会解释整个
sysenter
指令机制和伴侣
VDSO的必要性,只需要说它比
int
机制更快(我找不到Andy Glew的文章,他说
sysenter
在Pentium III上变得很慢,我不知道它现在的表现如何)。
随着x86-64的出现,AMD对
sysenter
的回应,即
syscall
/
sysret
对,开始成为从用户模式到内核模式切换的事实标准。
这是因为
sysenter
实际上非常快速和简单(它将
rip
和
rflags
复制到
rcx
和
r11
中,分别屏蔽
rflags
并跳转到
IA32_LSTAR
中设置的地址)。
64位版本的Linux和Windows都使用syscall
。
总之,可以通过三种机制将控制权交给内核:
- 软件中断。
这是32位Linux(2.5之前)的int 80h
和32位Windows的int 2eh
。
- 通过
sysenter
。
32位Linux自2.5以来一直在使用。
- 通过
syscall
。
64位版本的Linux和Windows都在使用。
这是一个很好的页面,可以让它变得更好。
C运行时通常是静态库,因此预编译,并使用上述三种方法之一。
syscall
指令将控制权直接转移给内核入口点(参见entry_64.s)。
它只是执行此操作的指令,不是由操作系统实现的,而是由操作系统使用的。
术语异常在计算机科学中有多重含义,C++、Java和C#都有异常。
操作系统可以具有语言无关的异常捕获机制(在Windows下曾经称为SEH,现已被重写)。
CPU也有异常。
我认为我们谈论的是最后一种含义。
Exceptions被通过中断分发,它们是一种中断。
毋庸置疑,虽然异常是同步的(它们发生在特定的、可重复的点上),但它们是“不需要的”,也就是说,程序员倾向于避免它们,当它们发生时,要么是由于一个错误,一个未处理的边缘情况或者一个糟糕的情况。
因此,它们不用于将控制传递到内核(它们可以这样做)。
软件中断(也是同步的)被用来代替;机制几乎完全相同(异常可以在内核堆栈上推送状态码),但语义不同。
我们从未解引用过空指针,访问未映射的页面或类似的方式来调用系统调用,而是使用了
int
指令。
write
是一个 C 函数调用。它不会直接编译成syscall
。这是在函数内部处理的。syscall 0x80
不是有效的,您将其与旧的 32 位调用内核的int 0x80
混淆了。那是一个中断。syscall
不使用中断机制。 - Jesterwrite
是用汇编中的syscall
实现的。我想这就是你所说的。 - Max Koretskyi