




已阅读5页,还剩5页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
2 6 对新型 CPU 快速系统调用的支持Linux 2.6对新型CPU快速系统调用的支持2011-03-22 16:58在Linux 2.4内核中,用户态Ring3代码请求内核态Ring0代码完成某些功能是通过系统调用完成的,而系统调用的是通过软中断指令(int 0x80)实现的。在x86保护模式中,处理INT中断指令时,CPU首先从中断描述表IDT取出对应的门描述符,判断门描述符的种类,然后检查门描述符的级别DPL和INT指令调用者的级别CPL,当CPL=DPL也就是说INT调用者级别高于描述符指定级别时,才能成功调用,最后再根据描述符的内容,进行压栈、跳转、权限级别提升。内核代码执行完毕之后,调用IRET指令返回,IRET指令恢复用户栈,并跳转会低级别的代码。其实,在发生系统调用,由Ring3进入Ring0的这个过程浪费了不少的CPU周期,例如,系统调用必然需要由Ring3进入Ring0(由内核调用INT指令的方式除外,这多半属于Hacker的内核模块所为),权限提升之前和之后的级别是固定的,CPL肯定是3,而INT 80的DPL肯定也是3,这样CPU检查门描述符的DPL和调用者的CPL就是完全没必要。正是由于如此,Intel x86 CPU从PII 300(Family 6,Model 3,Stepping 3)之后,开始支持新的系统调用指令sysenter/sysexit。sysenter指令用于由Ring3进入Ring0,SYSEXIT指令用于由Ring0返回Ring3。由于没有特权级别检查的处理,也没有压栈的操作,所以执行速度比INT n/IRET快了不少。下面是一些来自互联网的有关sysenter/sysexit指令和INT n/IRET指令在Intel Pentium CPU上的性能对比:表1:系统调用性能测试测试硬件:Intel Pentium III CPU,450 MHzProcessor Family:6 Model:7 Stepping:2用户模式花费的时间核心模式花费的时间基于sysenter/sysexit指令的系统调用9.833 microseconds6.833 microseconds基于中断INT n指令的系统调用17.500 microseconds7.000 microseconds数据来源:1数据来源:2表2:各种CPU上INT 0x80和SYSENTER执行速度的比较CPUInt0x80sysenter Athlon XP 1600+277169 800MHz mode 1athlon 279170 2.8GHz p4 northwood ht 1152442上述数据为对100000次getppid()系统调用所花费的CPU时钟周期取的平均值数据来源3自这种技术推出之后,人们一直在考虑在Linux中加入对这种指令的支持,在K的邮件列表中,主题为Intel P6 vs P7 system call performance的大量邮件讨论了采用这种指令的必要性,邮件中列举的理由主要是Intel在Pentium 4的设计上存在问题,造成Pentium 4使用中断方式执行的系统调用比Pentium 3以及AMD Athlon所耗费的CPU时钟周期多上510倍。因此,在Pentium 4平台上,通过sysenter/sysexit指令来执行系统调用已经是刻不容缓的需求。在Intel的软件开发者手册第二、三卷(Vol.2B,Vol.3)中,4.8.7节是关于sysenter/sysexit指令的详细描述。手册中说明,sysenter指令可用于特权级3的用户代码调用特权级0的系统内核代码,而SYSEXIT指令则用于特权级0的系统代码返回用户空间中。sysenter指令可以在3,2,1这三个特权级别调用(Linux中只用到了特权级3),而SYSEXIT指令只能从特权级0调用。执行sysenter指令的系统必须满足两个条件:1.目标Ring 0代码段必须是平坦模式(Flat Mode)的4GB的可读可执行的非一致代码段。2.目标RING0堆栈段必须是平坦模式(Flat Mode)的4GB的可读可写向上扩展的栈段。在Intel的手册中,还提到了sysenter/sysexit和int n/iret指令的一个区别,那就是sysenter/sysexit指令并不成对,sysenter指令并不会把SYSEXIT所需的返回地址压栈,sysexit返回的地址并不一定是sysenter指令的下一个指令地址。调用sysenter/sysexit指令地址的跳转是通过设置一组特殊寄存器实现的。这些寄存器包括:SYSENTER_CS_MSR-用于指定要执行的Ring 0代码的代码段选择符,由它还能得出目标Ring 0所用堆栈段的段选择符;SYSENTER_EIP_MSR-用于指定要执行的Ring 0代码的起始地址;SYSENTER_ESP_MSR-用于指定要执行的Ring 0代码所使用的栈指针这些寄存器可以通过wrmsr指令来设置,执行wrmsr指令时,通过寄存器edx、eax指定设置的值,edx指定值的高32位,eax指定值的低32位,在设置上述寄存器时,edx都是0,通过寄存器ecx指定填充的MSR寄存器,sysenter_CS_MSR、sysenter_ESP_MSR、sysenter_EIP_MSR寄存器分别对应0x174、0x175、0x176,需要注意的是,wrmsr指令只能在Ring 0执行。这里还要介绍一个特性,就是Ring0、Ring3的代码段描述符和堆栈段描述符在全局描述符表GDT中是顺序排列的,这样只需知道SYSENTER_CS_MSR中指定的Ring0的代码段描述符,就可以推算出Ring0的堆栈段描述符以及Ring3的代码段描述符和堆栈段描述符。在Ring3的代码调用了sysenter指令之后,CPU会做出如下的操作:1.将SYSENTER_CS_MSR的值装载到cs寄存器2.将SYSENTER_EIP_MSR的值装载到eip寄存器3.将SYSENTER_CS_MSR的值加8(Ring0的堆栈段描述符)装载到ss寄存器。4.将SYSENTER_ESP_MSR的值装载到esp寄存器5.将特权级切换到Ring0 6.如果EFLAGS寄存器的VM标志被置位,则清除该标志7.开始执行指定的Ring0代码在Ring0代码执行完毕,调用SYSEXIT指令退回Ring3时,CPU会做出如下操作:1.将SYSENTER_CS_MSR的值加16(Ring3的代码段描述符)装载到cs寄存器2.将寄存器edx的值装载到eip寄存器3.将SYSENTER_CS_MSR的值加24(Ring3的堆栈段描述符)装载到ss寄存器4.将寄存器ecx的值装载到esp寄存器5.将特权级切换到Ring3 6.继续执行Ring3的代码由此可知,在调用SYSENTER进入Ring0之前,一定需要通过wrmsr指令设置好Ring0代码的相关信息,在调用SYSEXIT之前,还要保证寄存器edx、ecx的正确性。根据Intel的CPU手册,我们可以通过CPUID指令来查看CPU是否支持sysenter/sysexit指令,做法是将EAX寄存器赋值1,调用CPUID指令,寄存器edx中第11位(这一位名称为SEP)就表示是否支持。在调用CPUID指令之后,还需要查看CPU的Family、Model、Stepping属性来确认,因为据称Pentium Pro处理器会报告SEP但是却不支持sysenter/sysexit指令。只有Family大于等于6,Model大于等于3,Stepping大于等于3的时候,才能确认CPU支持sysenter/sysexit指令。在2.4内核中,直到最近的发布的2.4.26-rc2版本,没有加入对sysenter/sysexit指令的支持。而对sysenter/sysexit指令的支持最早是2002年,由Linus Torvalds编写并首次加入2.5版内核中的,经过多方测试和多次patch,最终正式加入到了2.6版本的内核中。具体谈到系统调用的完成,不能孤立的看内核的代码,我们知道,系统调用多被封装成库函数提供给应用程序调用,应用程序调用库函数后,由glibc库负责进入内核调用系统调用函数。在2.4内核加上老版的glibc的情况下,库函数所做的就是通过int指令来完成系统调用,而内核提供的系统调用接口很简单,只要在IDT中提供INT 0x80的入口,库就可以完成中断调用。在2.6内核中,内核代码同时包含了对int 0x80中断方式和sysenter指令方式调用的支持,因此内核会给用户空间提供一段入口代码,内核启动时根据CPU类型,决定这段代码采取哪种系统调用方式。对于glibc来说,无需考虑系统调用方式,直接调用这段入口代码,即可完成系统调用。这样做还可以尽量减少对glibc的改动,在glibc的源码中,只需将intmessagex80指令替换成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工具查看任意一个可执行程序,可以看到下面的结果:roottest#file dynamic dynamic:ELF 32-bit LSB executable,Intel 80386,version 1(SYSV),for GNU/Linux 2.2.5,dynamically linked(uses shared libs),not strippedroottest#ldd dynamic linux-gate.so.1=(0xffffe000)libc.so.6=/lib/tls/libc.so.6(0x4002c000)/lib/ld-linux.so.2=/lib/ld-linux.so.2(0x 40000000)这个所谓的linux-gate.so.1的内容就是内核映射的代码,系统中其实并不存在这样一个链接库文件,它的名字是由ldd自己起的,而在老版本的ldd中,虽然能够检测到这段代码,但是由于没有命名而且在系统中找不到对应链接库文件,所以会有一些显示上的问题。有关这个问题的背景,可以参考下面这个网址:。为了配合内核使用新的系统调用方式,glibc中要做一定的修改。新的glibc-2.3.2(及其以后版本中)中已经包含了这个改动,在glibc源代码的sysdeps/unix/sysv/linux/i386/sysdep.h文件中,处理系统调用的宏INTERNAL_SYSCALL在不同的编译选项下有不同的结果。在打开支持sysenter/sysexit指令的选项I386_USE_SYSENTER下,系统调用会有两种方式,在静态链接(编译时加上-static选项)情况下,采用call*_dl_sysinfo指令;在动态链接情况下,采用call*%gs:0x10指令。这两种情况由glibc库采用哪种方法链接,实际上最终都相当于调用某个固定地址的代码。下面我们通过一个小小的程序,配合gdb来验证。首先是一个静态编译的程序,代码很简单:main()getuid();将代码加上static选项用gcc静态编译,然后用gdb装载并反编译main函数。roottest opt#gcc test.c-o./static-staticroottest opt#gdb./static(gdb)disassemble main0x 08048204 main+0:push%ebp 0x 08048205 main+1:mov%esp,%ebp 0x 08048207 main+3:submessagex8,%esp 0x 0804820a main+6:andmessagexfffffff0,%esp 0x 0804820d main+9:movmessagex0,%eax 0x 08048212 main+14:sub%eax,%esp 0x 08048214 main+16:call 0x804cb20 _getuid 0x 08048219 main+21:leave0x 0804821a main+22:ret可以看出,main函数中调用了_getuid函数,接着反编译_getuid函数。(gdb)disassemble 0x804cb200x0804cb20 _getuid+0:push%ebp 0x0804cb21 _getuid+1:mov 0x80aa028,%eax 0x0804cb26 _getuid+6:mov%esp,%ebp 0x0804cb28 _getuid+8:test%eax,%eax 0x0804cb2a _getuid+10:jle 0x804cb40 _getuid+32 0x0804cb2c _getuid+12:movmessagex18,%eax 0x0804cb31 _getuid+17:call*0x80aa054 0x0804cb37 _getuid+23:pop%ebp 0x0804cb38 _getuid+24:ret上面只是_getuid函数的一部分。可以看到_getuid将eax寄存器赋值为getuid系统调用的功能号0x18然后调用了另一个函数,这个函数的入口在哪里呢?接着查看位于地址0x80aa054的值。(gdb)X 0x80aa0540x80aa054 _dl_sysinfo:0x0804d7f6看起来不像是指向内核映射页面内的代码,但是,可以确认,_dl_sysinfo指针的指向的地址就是0x80aa054。下面,我们试着启动这个程序,然后停在程序第一条语句,再查看这个地方的值。(gdb)b mainBreakpoint 1at 0x 804820a(gdb)rStarting program:/opt/static Breakpoint 1,0x 0804820a in main()(gdb)X 0x80aa054 0x80aa054 _dl_sysinfo:0xffffe400可以看到,_dl_sysinfo指针指向的数值已经发生了变化,指向了0xffffe400,如果我们继续运行程序,_getuid函数将会调用地址0xffffe400处的代码。接下来,我们将上面的代码编译成动态链接的方式,即默认方式,用gdb装载并反编译main函数roottest opt#gcc test.c-o./dynamicroottest opt#gdb./dynamic(gdb)disassemble main0x 08048204 main+0:push%ebp 0x 08048205 main+1:mov%esp,%ebp 0x 08048207 main+3:submessagex8,%esp 0x 0804820a main+6:andmessagexfffffff0,%esp 0x 0804820d main+9:movmessagex0,%eax 0x 08048212 main+14:sub%eax,%esp 0x 08048214 main+16:call 0x 8048288 0x 08048219 main+21:leave0x 0804821a main+22:ret由于libc库是在程序初始化时才被装载,所以我们先启动程序,并停在main第一条语句,然后反汇编getuid库函数(gdb)b mainBreakpoint 1at 0x 804820a(gdb)rStarting program:/opt/dynamic Breakpoint 1,0x 0804820a in main()(gdb)disassemble getuid Dump of assembler code for function getuid:0x40219e50 _getuid+0:push%ebp 0x40219e51 _getuid+1:mov%esp,%ebp 0x40219e53 _getuid+3:push%ebx 0x40219e54 _getuid+4:call 0x40219e59 _getuid+9 0x40219e59 _getuid+9:pop%ebx 0x40219e5a _getuid+10:addmessagex84b0f,%ebx 0x40219e60 _getuid+16:mov 0xffffd87c(%ebx),%eax 0x40219e66 _getuid+22:test%eax,%eax 0x40219 e68 _getuid+24:jle 0x40219e80 _getuid+48 0x40219e6a _getuid+26:movmessagex18,%eax 0x40219e6f _getuid+31:call*%gs:0x10 0x40219e76 _getuid+38:pop%ebx 0x40219e77 _getuid+39:pop%ebp 0x40219e78 _getuid+40:ret可以看出,库函数getuid将eax寄存器设置为getuid系统调用的调用号0x18,然后调用%gs:0x10所指向的函数。在gdb中,无法查看非DS段的数据内容,所以无法查看%gs:0x10所保存的实际数值,不过我们可以通过编程的办法,内嵌汇编将%gs:0x10的值赋予某个局部变量来得到这个数值,而这个数值也是0xffffe400,具体代码这里就不再赘述。由此可见,无论是静态还是动态方式,最终我们都来到了0xffffe400这里的一段代码,这里就是内核为我们映射的系统调用入口代码。在gdb中,我们可以直接反汇编来查看这里的代码(gdb)disassemble 0xffffe400 0xffffe414 Dump of assembler code from 0xffffe400 to 0xffffe414:0xffffe400:push%ecx 0xffffe401:push%edx0xffffe402:push%ebp 0xffffe403:mov%esp,%ebp0xffffe405:sysenter0xffffe407:nop 0xffffe408:nop0xffffe409:nop0xffffe40a:nop0xffffe40b:nop 0xffffe40c:nop0xffffe40d:nop0xffffe40e:jmp 0xffffe403 0xffffe410:pop%ebp0xffffe411:pop%edx 0xffffe412:pop%ecx0xffffe413:retEnd of assembler dump.这段代码正是arch/i386/kernel/vsyscall-sysenter.S文件中的代码。其中,在sysenter之前的是入口代码,在0xffffe410开始的是内核返回处理代码(后面提到的SYSENTER_RETURN即指向这里)。在入口代码中,首先是保存当前的ecx,edx(由于sysexit指令需要使用这两个寄存器)以及ebp。然后调用sysenter指令,跳转到内核Ring 0代码,也就是sysenter_entry入口处。sysenter_entry整个的实现可以参见arch/i386/kernel/entry.S。内核处理SYSENTER的代码和处理INT的代码不太一样。通过sysenter指令进入Ring 0之后,由于当前的ESP并非指向正确的内核栈,而是当前CPU的TSS结构中的一个缓冲区(参见上文),所以首先要解决的是修复ESP,幸运的是,TSS结构中ESP0成员本身就保存有Ring 0状态的ESP值,所以在这里将TSS结构中ESP0的值赋予ESP寄存器。将ESP恢复成指向正确的堆栈之后,由于SYSENTER不是通过调用门进入Ring 0,所以在堆栈中的上下文和使用INT指令的不一样,INT指令进入Ring 0后栈中会保存如下的值。返回用户态的EIP用户态的CS用户态的EFLAGS用户态的ESP用户态的SS(和DS相同)因此,为了简化和重用代码,内核会用pushl指令往栈中放入上述各值,值得注意的是,内核在栈中放入的相对应用户态EIP的值,是一个代码标签SYSENTER_RETURN,在vsyscall-sysenter.S可以看到,它就在sysenter指令的后面(在它们之间,有一段NOP,是内核返回出错时的处理代码)。接下来,处理系统调用的代码就和中断方式的处理代码一模一样了,内核保存所有的寄存器,然后系统调用表找到对应系统调用的入口,完成调用。最后,内核从栈中取出前面存入的用户态的EIP和ESP,存入edx和ecx寄存器,调用SYSEXIT指令返回用户态。返回用户态之后,从栈中取出ESP,edx,ecx,最终返回glibc库。值得一提的是,从Windows XP开始,Windows的系统调用方式也从软中断int 0x2e转换到采用sysenter方式,由于完全不再支持int方式,因此Windows XP的对CPU的最低配置要求是PentiumII 300MHz。在其它的操作系统例如*BSD系列,目前并没有提供对sysenter指令的支持。在CPU方面,AMD的CPU支持一套与之对应的指令SYSCALL/SYSRET。在纯32位的AMD CPU上,还没有支持sysen
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
评论
0/150
提交评论