扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
Linux 对 sysenter/sysexit 系统调用方式的支持
在 2.4 内核中,直到最近的发布的 2.4.26-rc2 版本,没有加入对 sysenter/sysexit 指令的支持。而对 sysenter/sysexit 指令的支持最早是2002 年,由 Linus Torvalds 编写并首次加入 2.5 版内核中的,经过多方测试和多次 patch,最终正式加入到了 2.6 版本的内核中。http://kerneltrap.org/node/view/531/1996,http://lwn.net/Articles/18414/。
具体谈到系统调用的完成,不能孤立的看内核的代码,我们知道,系统调用多被封装成库函数提供给应用程序调用,应用程序调用库函数后,由 glibc 库负责进入内核调用系统调用函数。在 2.4 内核加上老版的 glibc 的情况下,库函数所做的就是通过 int 指令来完成系统调用,而内核提供的系统调用接口很简单,只要在 IDT 中提供 INT 0x80 的入口,库就可以完成中断调用。
在 2.6 内核中,内核代码同时包含了对 int 0x80 中断方式和 sysenter 指令方式调用的支持,因此内核会给用户空间提供一段入口代码,内核启动时根据 CPU 类型,决定这段代码采取哪种系统调用方式。对于 glibc 来说,无需考虑系统调用方式,直接调用这段入口代码,即可完成系统调用。这样做还可以尽量减少对 glibc 的改动,在 glibc 的源码中,只需将 "int $0x80" 指令替换成 "call 入口地址" 即可。
下面,以 2.6.0 的内核代码配合支持 SYSENTER 调用方式的 glibc2.3.3 为例,分析一下系统调用的具体实现。
内核在启动时做的准备
前面说到的这段入口代码,根据调用方式分为两个文件,支持 sysenter 指令的代码包含在文件 arch/i386/kernel/vsyscall-sysenter.S 中,支持int中断的代码包含在arch/i386/kernel/vsyscall-int80.S中,入口名都是__kernel_vsyscall,这两个文件编译出的二进制代码由arch/i386/kernel/vsyscall.S所包含,并导出起始地址和结束地址。
2.6 内核在启动的时候,调用了新增的函数 sysenter_setup(参见 arch/i386/kernel/sysenter.c),在这个函数中,内核将虚拟内存空间的顶端一个固定地址页面(从 0xffffe000 开始到 0xffffeffff 的 4k 大小)映射到一个空闲的物理内存页面。然后通过之前执行 CPUID 的指令得到的数据,检测 CPU 是否支持 sysenter/sysexit 指令。如果 CPU 不支持,那么将采用 INT 调用方式的入口代码拷贝到这个页面中,然后返回。相反,如果 CPU 支持 SYSETER/SYSEXIT 指令,则将采用 SYSENTER 调用方式的入口代码拷贝到这个页面中。使用宏 on_each_cpu 在每个 CPU 上执行 enable_sep_cpu 这个函数。
在 enable_sep_cpu 函数中,内核将当前 CPU 的 TSS 结构中的 ss1 设置为当前内核使用的代码段,esp1 设置为该 TSS 结构中保留的一个 256 字节大小的堆栈。在 X86 中,TSS 结构中 ss1 和 esp1 本来是用于保存 Ring 1 进程的堆栈段和堆栈指针的。由于内核在启动时,并不能预知调用 sysenter 指令进入 Ring 0 后 esp 的确切值,而应用程序又无权调用 wrmsr 指令动态设置,所以此时就借用 esp1 指向一个固定的缓冲区来填充这个 MSR 寄存器,由于 Ring 1 根本没被启用,所以并不会对系统造成任何影响。在下面的文章中会介绍进入 Ring 0 之后,内核如何修复 ESP 来指向正确的 Ring 0 堆栈。关于 TSS 结构更细节的应用可参考代码 include/asm-i386/processor.h)。
然后,内核通过 wrmsr (msr,val1,val2) 宏调用 wrmsr 指令对当前 CPU 设置 MSR 寄存器,可以看出调用宏的第三个参数即 edx 都被设置为 0。其中 SYSENTER_CS_MSR 的值被设置为当前内核用的所在代码段;SYSENTER_ESP_MSR 被设置为 esp1,即指向当前 CPU 的 TSS 结构中的堆栈;SYSENTER_EIP_MSR 则被设置为内核中处理 sysenter 指令的接口函数 sysenter_entry(参见 arch/i386/kernel/entry.S)。这样,sysenter 指令的准备工作就完成了。
通过内核在启动时进行这样的设置,在每个进程的进程空间中,都能访问到内核所映射的这个代码页面,当然这个页面对于应用程序来说是只读的。我们通过新版的ldd工具查看任意一个可执行程序,可以看到下面的结果:
|
这个所谓的 "linux-gate.so.1" 的内容就是内核映射的代码,系统中其实并不存在这样一个链接库文件,它的名字是由ldd自己起的,而在老版本的 ldd中,虽然能够检测到这段代码,但是由于没有命名而且在系统中找不到对应链接库文件,所以会有一些显示上的问题。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者