通常有三种方式使CPU从用户空间转入系统空间。
中断、异常、自陷(这三条非常重要所以列一下)
- 中断
当中断到来时,CPU就换转入系统空间,并从某个预定的地址开始执行指令,CPU无法预知何时中断到来;中断发生在两条指令之间,因而不会使正在执行的指令半途而废。
- 异常
不管用户空间还是系统空间,指令执行失败,则会引发一次异常,CPU转入系统空间,从某个预定的地址开始执行指令;异常发生在执行一条指令的过程中,所有当前指令的执行已经半途而废;异常通常也是无法预知的。
- 自陷
让CPU主动进行系统空间,且从某个预定位置执行指令,系统调用原来就是靠自陷指令实现的(int 0x2e指令);现在的x86CPU提供了快速系统调用硬件支持(sysenter指令)。
两种系统调用形式
int 0x2e 形式系统调用概述(后面通过实例详细讲解该方式细节)
sysenter 形式系统调用概述(现在简单理解为int 0x2e的优化,后面会详细讲解)
实例讲解系统调用过程
以NtReadFile()为例,int 0x2e 详细调用过程
kernel.dll中的导出函数ReadFile(),该函数定义如下:
BOOL WINAPI ReadFile(
_In_ HANDLE hFile,
_Out_ LPVOID lpBuffer,
_In_ DWORD nNumberOfBytesToRead,
_Out_opt_ LPDWORD lpNumberOfBytesRead,
_Inout_opt_ LPOVERLAPPED lpOverlapped
);
WINAPI 表名该函数为Win32 API函数(规范化,对于理解源码有益),该函数内部调用NtReadFile(在ntoskrnl.dll内核中也存在该函数)
由于直接从用户空间调用内核函数是不允许的,所以其实用户空间还有个NtReadFile(),上图看到的就是用户空间的NtReadFile,这个函数内部再想方法去调用内核中的NtReadFile()。这里之所以要这样中转下,在我看来是为了将相同的模板化的代码提取出来,一旦改动起来就方便,不用每个方法都去修改;就这里来说,可以将用户空间的NtCreateFile(后面知道是通过工具生成的)内容直接在ReadFile(上面图)方法中实现,但是类似ReadFile的Win32 API还有很多个,一旦修改了调用方式(如使用快速系统调用),那么每个Win32 API都得去修改,这样就麻烦了。类似用户空间的NtReadFile方法,由于具有类似的结构可以通过工具编译时候生成,不管是NtReadFile、NtCreateFile或者其他的都是相同结构。而用户空间的Ntxxxxx方法是精心构造的,如下:
__declspec(naked)是用来告诉编译器不需要添加任何汇编代码(通常函数编译时候编译器会添加堆栈平衡返回值等相关汇编代码)参考:(https://stackoverflow.com/questions/3536194/c-inline-assembly-function-not-working-properly);但是这里的参数数量以及个产生类型应该是匹配的,上图这里参数应该是不对的(这里容易被搞迷糊),应该也是9个。至 少 确 保 参 数对应的传入,且在堆栈形成了参数块,在mov ebp,esp 执行完成后堆栈应该如下图:
所以 lea edx,8[ebp] 使edx指向了FileHandle参数块的起点,然后执行 int 0x2e 自陷,进入预定地址,此时eax保存了系统调用号,edx指向堆栈参数起始地址,此时指向的堆栈为用户空间堆栈,但是cpu进入内核后,需要将这些参数从用户空间线程堆栈拷贝到内核线程堆栈,通过EDX寄存器。至此int 0x2e方式进入内核的过程就这些了。