免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 4380 | 回复: 2
打印 上一主题 下一主题

(转贴)What is linux-gate.so.1?(外一篇:vsyscall分析) [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-05-26 10:17 |只看该作者 |倒序浏览
当你在比较新的linux系统上使用ldd工具的时候,你会发现有一个奇怪的东东:linux-gate.so.1:

  1. ldd /bin/sh
  2.         linux-gate.so.1 =>  (0xffffe000)
  3.         libdl.so.2 => /lib/libdl.so.2 (0xb7fb2000)
  4.         libc.so.6 => /lib/libc.so.6 (0xb7e7c000)
  5.         /lib/ld-linux.so.2 (0xb7fba000)
复制代码


[译者注:作者的sh使用新版本glibc编译过,如果是就版本,比如2.3.3,则见到的可能会有些差别:

  1. [rick@Fedora-Core test]$ ldd /bin/sh
  2.         linux-gate.so.1 =>  (0xffffe000)
  3.         libtermcap.so.2 => /lib/libtermcap.so.2 (0x00ca0000)
  4.         libdl.so.2 => /lib/libdl.so.2 (0x00a91000)
  5.         libc.so.6 => /lib/tls/libc.so.6 (0x00943000)
  6.         /lib/ld-linux.so.2 (0x00926000)
复制代码


那这有什么奇怪的呢?只是一个动态库,是吗?

根据很多动态库的定义,是有几分像.从ldd的结果看,ldd找不到这个文件.事实上,无论是手动去找,或者借助一些软件工具,都无法找到相关文件.有时,当用户去寻找这个不存在的文件会很迷茫.你可以明确的告诉他们系统中根本没有这样一个文件存在,他只是一个虚拟的动态共享库,是由内核导出的存在于每个进程空间中的一个共享对象:

  1. cat /proc/self/maps
  2. 08048000-0804c000 r-xp 00000000 08:03 7971106    /bin/cat
  3. 0804c000-0804d000 rwxp 00003000 08:03 7971106    /bin/cat
  4. 0804d000-0806e000 rwxp 0804d000 00:00 0          [heap]
  5. b7e88000-b7e89000 rwxp b7e88000 00:00 0
  6. b7e89000-b7fb8000 r-xp 00000000 08:03 8856588    /lib/libc-2.3.5.so
  7. b7fb8000-b7fb9000 r-xp 0012e000 08:03 8856588    /lib/libc-2.3.5.so
  8. b7fb9000-b7fbc000 rwxp 0012f000 08:03 8856588    /lib/libc-2.3.5.so
  9. b7fbc000-b7fbe000 rwxp b7fbc000 00:00 0
  10. b7fc2000-b7fd9000 r-xp 00000000 08:03 8856915    /lib/ld-2.3.5.so
  11. b7fd9000-b7fdb000 rwxp 00016000 08:03 8856915    /lib/ld-2.3.5.so
  12. bfac3000-bfad9000 rw-p bfac3000 00:00 0          [stack]
  13. ffffe000-fffff000 ---p 00000000 00:00 0          [vdso]
复制代码

这里,cat 命令打印出了自身的内存映射情况.标记有[vdso]的那行就是这个进程的linux-gate.so.1对象,她是从0xffffe000开始映射的一页.程序可以从ELF辅助向量表的AT_SYSINFO项得到这个共享对象的载入地址.辅助向量(auxv)其实就是一个指针数组,像argv,env一样传给一个新进程.(关于辅助向量,在拙作stack explore中也提到过).

理论上,这个地址(linux-gate.so.1的载入地址)在每个进程之间可以不相同,但是据我所知,内核总是将他映射到一个固定的地址(其实就是 0xffffe000).上面例子的实验平台是:x86 cpu,32位地址空间,每页4KB,这样0xffffe000就是倒数第二页.最后一页被保留用来捕获不合法的指针访问,比如:解引用一个null指针,或者由mmap返回的映射失败的指针.

因为所有进程在相同的地址共享这个对象(linux-gate.so.1),所以如果我们想要仔细看看这个对象,可以很方便的得到他.比如,我们可以使用 dd工具(注意不要选择linux-gate.so.1作为文件名,以避免使以前假定的不存在的文件存在,也就是说,本来linux-gate.so.1 事实上是不存在的,如果你使用了这个名字,那么就令这个文件存在了,这样不大好):

  1. dd if=/proc/self/mem of=linux-gate.dso bs=4096 skip=1048574 count=1
  2. 1+0 records in
  3. 1+0 records out
复制代码


我们跳过了1048574,是因为一共有2^20=1048576个虚拟页面,而我们需要的是倒数第二页.得到的结果(linux-gate.dso文件)跟其他的ELF共享库文件看起来是一样的:

  1. file -b linux-gate.dso
  2. ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), stripped


  3. objdump -T linux-gate.dso

  4. linux-gate.dso:     file format elf32-i386

  5. DYNAMIC SYMBOL TABLE:
  6. ffffe400 l    d  .text  00000000              
  7. ffffe460 l    d  .eh_frame_hdr  00000000              
  8. ffffe484 l    d  .eh_frame      00000000              
  9. ffffe608 l    d  .useless       00000000              
  10. ffffe400 g    DF .text  00000014  LINUX_2.5   __kernel_vsyscall
  11. 00000000 g    DO *ABS*  00000000  LINUX_2.5   LINUX_2.5
  12. ffffe440 g    DF .text  00000007  LINUX_2.5   __kernel_rt_sigreturn
  13. ffffe420 g    DF .text  00000008  LINUX_2.5   __kernel_sigreturn
复制代码


这些符号是rt_sigreturn/sigreturn函数的入口点,同时为虚拟系统调用服务.在x86平台上,linux-gate.so.1开始的时候被称作linux-vsyscall.so.1,但是后来因为他的新作用(作为用户空间和内核空间的一扇门)而改变了.虚拟系统调用并不是在每个平台上都需要,但是他们对于x86平台来说却是一个很精巧的设计.

传统的x86的系统调用使用中断完成.你可能还记得在很久以前的ms-dos系统上使用33(21h)中断作为系统调用.windows系统调用被隐藏在用户态API的下层,但是有时也会使用int 0x2e陷入.相似的,linux和其他*nix内核也使用中断(int 0x80)实现系统调用.

但是,事实证明,通过中断方式的系统调用在新的x86处理器上表现的很差,速度很慢.一个int 0x80系统调用在Pentium 4 2.0 上比Pentium III还要慢.对于很频繁使用系统调用的程序而言,对性能影响还是很大的.

[译者注:下面一小段是一些历史,不感兴趣可以跳过]
Intel已经发现了这个问题,并且引入一个更有效的系统调用方式:使用sysenter和sysexit指令.这种快速系统调用首先在 Pentium Pro 处理器上使用,但是因为硬件bug,直到后来的Pentium II,甚至Pentium III.硬件问题也帮助结识了为什么操作系统用了很长时间才开始支持快速系统调用.如果我们不算早期的实验型patch,linux支持sysenter 首先是在开发2.5内核的时候(2002年12月,这已经是指令定义的十年后了).MS开始支持sysenter稍微早一点,是在win XP中支持的.

如果你想检查一下自己的linux机器是否在使用sysenter指令进行系统调用,可以反汇编__kernel_vsyscall:

  1. objdump -d --start-address=0xffffe400 --stop-address=0xffffe414 linux-gate.dso

  2. linux-gate.dso:     file format elf32-i386

  3. Disassembly of section .text:

  4. ffffe400 <__kernel_vsyscall>:
  5. ffffe400:       51                      push   %ecx
  6. ffffe401:       52                      push   %edx
  7. ffffe402:       55                      push   %ebp
  8. ffffe403:       89 e5                   mov    %esp,%ebp
  9. ffffe405:       0f 34                   sysenter
  10. ffffe407:       90                      nop   
  11. ffffe408:       90                      nop   
  12. ffffe409:       90                      nop   
  13. ffffe40a:       90                      nop   
  14. ffffe40b:       90                      nop   
  15. ffffe40c:       90                      nop   
  16. ffffe40d:       90                      nop   
  17. ffffe40e:       eb f3                   jmp    ffffe403 <__kernel_vsyscall+0x3>
  18. ffffe410:       5d                      pop    %ebp
  19. ffffe411:       5a                      pop    %edx
  20. ffffe412:       59                      pop    %ecx
  21. ffffe413:       c3                      ret   
复制代码

系统使用哪一种方式完成系统调用在内核启动时决定.很明显,这台机子使用了sysenter.在老一点的机子上,你可能看到的就是int $0x80了.如果想搞明白那个jump是怎么回事,可以参考linus Torvalds的这篇文章:http://lkml.org/lkml/2002/12/18/218



原文链接:
http://www.linuxsir.org/bbs/showthread.php?t=216758

[ 本帖最后由 albcamus 于 2006-5-26 10:27 编辑 ]

论坛徽章:
0
2 [报告]
发表于 2006-05-26 10:25 |只看该作者
Linux 2.6 新增的 vsyscall 系统服务调用机制


   与 Windows 的系统服务调用实现机制类似,Linux 内部为所有核心态系统调用,维护了一张按调用号排序的跳转表  (sys_call_table @ arch/i386/kernel/entry.S)。只不过对 Window 来说,类似的跳转表  (KeServiceDescriptorTable @ ntos/ke/kernldat.c) 按功能进一步细分为四部分,分别用于内核与  Win32 子系统等。而 Linux 的系统服务表,因为开放性、兼容性和移植性等问题,则相对稳定和保守得多。
以下内容为程序代码:

// sys_call_table @ arch/i386/kernel/entry.S

.data
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
...
.long sys_request_key
.long sys_keyctl

syscall_table_size=(.-sys_call_table)


以下内容为程序代码:

// KSERVICE_TABLE_DESCRIPTOR @ ntos/ke/ke.h

#define NUMBER_SERVICE_TABLES 4

typedef struct _KSERVICE_TABLE_DESCRIPTOR {
    PULONG Base;
    PULONG Count;
    ULONG Limit;
    PUCHAR Number;
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;

// KeServiceDescriptorTable @ ntos/ke/kernldat.c

KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable[NUMBER_SERVICE_TABLES];

     而对用户态 API 来说,Linux 和 Windows NT/2K 下面都是通过传统的中断方式,Linux 使用 int 0x80; Windows 使用 int 0x2E。对 glibc 来说,其实就是一系列的宏定义,如 _syscall0 - _syscall6 等不同形式,如
以下内容为程序代码:

#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile ("int 0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1))); \
__syscall_return(type,__res); \
}

    而在系统加载的时候,接管 0x80 中断服务历程,完成基于 sys_call_table 跳转表的派发。如
以下内容为程序代码:

static void __init set_system_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,3,addr,__KERNEL_CS); // 允许再 ring 3 调用此陷阱 (15) 的系统门
}

#define SYSCALL_VECTOR 0x80

asmlinkage int system_call(void);

void __init trap_init(void)
{
  ...
  set_system_gate(SYSCALL_VECTOR,&system_call);
  ...
}

     这里的 trap_init 函数 (arch/i386/kernel/traps.c) 负责初始化各种中断的处理例程,其将被系统初始化  start_kernel (init/main.c) 函数中调用。对 SYSCALL_VECTOR (0x80) 调用,由  system_call 函数 (arch/i386/kernel/entry.S) 进行实际处理工作。

    而对 Windows 用户态 DLL 如 NtDll 来说,也是通过类似的方式实现。因为这方面讨论的文章较多,这里就不再罗嗦。有兴趣的朋友可以参考 Inside Win2K 一书,以及 《剖析Windows系统服务调用机制》 等文章。

     但是这种基于中断的系统服务调用机制,对于 Intel P4 以上 CPU 来说存在着很大的性能隐患。根据实测结果 P3 850 在中断模式的系统调用上,比 P4 2G 有将近一倍的性能优势,而对 Xeon 等高端 CPU 来说中断的处理性能甚至更差。

    Intel P6 vs P7 system call performance

     这也是为什么从 Windows XP/2003 开始,MS 偷偷将 Intel 2E 的系统调用换成了 CPU 特殊的指令 sysenter  (Intel) 和 syscall (AMD)。例如在 Win2003 系统中,NTDLL 中的系统调用已经不再使用 Int 0x2E,而改为调用固定地址上的系统调用代码:
以下内容为程序代码:

0:001> u ntdll!ZwSuspendProcess
ntdll!NtSuspendProcess:
77f335bb b806010000       mov     eax,0x106
77f335c0 ba0003fe7f       mov     edx,0x7ffe0300
77f335c5 ffd2             call    edx
77f335c7 c20400           ret     0x4

0:001> u 0x7ffe0300
SharedUserData!SystemCallStub:
7ffe0300 8bd4             mov     edx,esp
7ffe0302 0f34             sysenter
7ffe0304 c3               ret

0:001> u 7ffe0314
SharedUserData!SystemCallStub+0x14:
7ffe0314 8bd4             mov     edx,esp
7ffe0316 0f05             syscall
7ffe0318 c3               ret


     对 Intel x86 架构来说,sysenter/sysexit 指令是从 PII 开始加入到指令集中,专门用于从用户态 (ring 1-3)  切换到核心态 (ring 0)。与普通的中断使用 IDT 或 call/jmp 直接给定目的地址不同,此系列命令直接从 CPU 相关的  MSR 寄存器中读取目标代码和堆栈的段选择符与地址偏移。因此只需要在系统加载的时候,一次性将这些设置好,就可以如上代码所示那样直接使用  sysenter 指令进行切换。正因为如此,使用 sysenter/sysexit 执行进行 ring 0 和 ring 3 之间切换,是在两个预定义好的稳定状态之间进行切换,所以无需进行中断处理时一系列的状态转换的特权检查,大大提高了切换处理的效率。而对 AMD 芯片, syscall 的实现原理基本类似。

    为了适应这种变化,提高系统调用的效率,Linux 内核从 2.5.53 开始增加了对  sysenter/sysexit 模式的系统服务调用机制的支持。新增的 sysenter.c (arch/i386/kernel/) 中代码,会根据当前启动 CPU 是否支持 sysenter/sysexit 指令,动态判断是否启用支持。
以下内容为程序代码:

#define X86_FEATURE_SEP (0*32+11) /* SYSENTER/SYSEXIT */

static int __init sysenter_setup(void)
{
void *page = (void *)get_zeroed_page(GFP_ATOMIC);

__set_fixmap(FIX_VSYSCALL, __pa(page), PAGE_READONLY_EXEC);

if (!boot_cpu_has(X86_FEATURE_SEP)) {
memcpy(page,
       &vsyscall_int80_start,
       &vsyscall_int80_end - &vsyscall_int80_start);
return 0;
}

memcpy(page,
       &vsyscall_sysenter_start,
       &vsyscall_sysenter_end - &vsyscall_sysenter_start);

on_each_cpu(enable_sep_cpu, NULL, 1, 1);
return 0;
}

__initcall(sysenter_setup);

    sysenter_setup  函数 (arch/i386/kernel/sysenter.c) 将在内核被加载时,获取一个只读并可执行的内存页,将基于 int 0x80 调用,或者 sysenter 调用的系统服务调用代码,加载到此内存页中。并根据情况,调用 on_each_cpu 函数对每个 CPU 启用  sysenter/sysexit 指令支持。
以下内容为程序代码:

#define MSR_IA32_SYSENTER_CS 0x174
#define MSR_IA32_SYSENTER_ESP 0x175
#define MSR_IA32_SYSENTER_EIP 0x176

extern asmlinkage void sysenter_entry(void);

void enable_sep_cpu(void *info)
{
int cpu = get_cpu();
struct tss_struct *tss = &per_cpu(init_tss, cpu);

tss->ss1 = __KERNEL_CS;
tss->esp1 = sizeof(struct tss_struct) + (unsigned long) tss;
wrmsr(MSR_IA32_SYSENTER_CS, __KERNEL_CS, 0);
wrmsr(MSR_IA32_SYSENTER_ESP, tss->esp1, 0);
wrmsr(MSR_IA32_SYSENTER_EIP, (unsigned long) sysenter_entry, 0);
put_cpu();
}

     可以看到 enable_sep_cpu 函数 (arch/i386/kernel/sysenter.c) 实际上是为每个 CPU 设置其  MSR 寄存器的相关值,以便 sysenter 指令被调用时,能够直接切换到预定义(包括代码和堆栈的段选择符和偏移地址)的内核状态。关于  sysenter/sysexit 和 MSR 的相关资料,可以参考 Intel IA-32 开发手册的第三卷,系统编程手册,4.8.7 节和附录 B。
以下内容为程序代码:

.text
.globl __kernel_vsyscall
.type __kernel_vsyscall,@function
__kernel_vsyscall:
.LSTART_vsyscall:
push %ecx
.Lpush_ecx:
push %edx
.Lpush_edx:
push %ebp
.Lenter_kernel:
movl %esp,%ebp
sysenter

...

    上述代码是 vsyscall-sysenter.S (arch/i386/kernel/) 中负责实际调用的,可以看到与前面 Win2003 的实现代码非常类似。

     而与 Win2003 不同的是,Linux 的系统调用存在一个被中断的问题。也就是说在一个系统调用执行时,因为调用本身等待某种资源或被阻塞,调用没有完成时可能就被中断而强制返回 -EINTR。此时系统调用本身并没有发生错误,因此应该提供某种自动重试机制。这也就是为什么 vsyscall- sysenter.S 中,在 sysenter 下面还有如下处理代码的原因。
以下内容为程序代码:

.Lenter_kernel:
movl %esp,%ebp
sysenter

/* 7: align return point with nop's to make disassembly easier */
.space 7,0x90

/* 14: System call restart point is here! (SYSENTER_RETURN - 2) */
jmp .Lenter_kernel
/* 16: System call normal return point is here! */
.globl SYSENTER_RETURN /* Symbol used by entry.S.  */
SYSENTER_RETURN:
pop %ebp
.Lpop_ebp:
pop %edx
.Lpop_edx:
pop %ecx
.Lpop_ecx:
ret
.LEND_vsyscall:
.size __kernel_vsyscall,.-.LSTART_vsyscall

     这里 sysenter 代码之后,实际上存在两个返回点:jmp .Lenter_kernel 指令是在调用被中断时的返回点; SYSENTER_RETURN 则是调用正常结束时的返回点。Linux 内核通过在实际调用函数中进行判断,调整返回地址 EIP 的方法,解决了这个自动重试的问题。
    Linus 在一篇邮件列表的讨论中解释了这个问题。具体关于系统调用中断与重试等机制的解释,可以参考 《The Linux Kernel》一书的 4.5 节。

     不过因为某些原因,Linux 2.6 内核仍然没有把 _syscall0 那套函数改用新的调用方法,而是在现有机制之外,增加了一套名为  vsyscall 的扩展机制,专供对系统服务调用效率要求较高的服务使用。这种机制实际上是将一部分固定地址的内核空间虚拟内存页面,直接暴露并允许用户态进行访问。也就是前面 sysenter_setup 函数中的 __set_fixmap 调用。
以下内容为程序代码:

#define __FIXADDR_TOP 0xfffff000

#define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)

#define PAGE_SHIFT 12

#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))

void __set_fixmap (enum fixed_addresses idx, unsigned long phys, pgprot_t flags)
{
unsigned long address = __fix_to_virt(idx);

if (idx >= __end_of_fixed_addresses) {
BUG();
return;
}
set_pte_pfn(address, phys >> PAGE_SHIFT, flags);
}

enum fixed_addresses {
FIX_HOLE,
FIX_VSYSCALL,
  ...
};

static int __init sysenter_setup(void)
{
  ...
  __set_fixmap(FIX_VSYSCALL, __pa(page), PAGE_READONLY_EXEC);
  ...
}

    可以看到 __set_fixmap 函数实际上是把 0xfffff000 开始的若干页虚拟内存地址,固定分配给 vsyscall 等不同用途的功能,由其自行分配物理内存并放置功能代码。对 FIX_VSYSCALL 这块内存,内存管理模块会特殊对待。
以下内容为程序代码:

/*
* This is the range that is readable by user mode, and things
* acting like user mode such as get_user_pages.
*/
#define FIXADDR_USER_START (__fix_to_virt(FIX_VSYSCALL))
#define FIXADDR_USER_END (FIXADDR_USER_START + PAGE_SIZE)

int in_gate_area(struct task_struct *task, unsigned long addr)
{
#ifdef AT_SYSINFO_EHDR
if ((addr >= FIXADDR_USER_START) && (addr < FIXADDR_USER_END))
return 1;
#endif
return 0;
}

int get_user_pages(...)
{
  ...
vma = find_extend_vma(mm, start);
  if (!vma && in_gate_area(tsk, start)) {
    ...
    if (write) /* user gate pages are read-only */
  return i ? : -EFAULT;
...
  }
  ...
}

     对 vsyscall 这一页内存,get_user_pages 函数将直接允许用户态的读操作。因此完全可以从用户态通过  call FIXADDR_USER_START 类似的代码,通过基于 sysenter/sysexit 的系统服务调用机制,快速执行指定服务号的系统服务。如 《The Linux Kernel》一书给出了一个例子:
以下内容为程序代码:

#include <stdio.h>

int pid;

int main() {
        __asm__(
                "movl 20, %eax    \n"
                "call 0xffffe400   \n"
                "movl %eax, pid    \n"
        printf("pid is %d\n", pid);
        return 0;
}

    而这种机制的系统服务调用,据其测试能有将近一倍的性能提升。

    以下为引用:


          An example of the kind of timing differences: John Stultz reports on an experiment where he measures gettimeofday() and finds 1.67 us for the int 0x80 way, 1.24 us for the sysenter way, and 0.88 us for the vsyscall.)



原文链接:
http://www.blogcn.com/User8/flier_lu/blog/5579109.html

论坛徽章:
0
3 [报告]
发表于 2006-05-26 10:40 |只看该作者

再来一篇:Linux 2.6 对新型 CPU 快速系统调用的支持

Linux 2.6 对新型 CPU 快速系统调用的支持

Linux 2.6 对新型 CPU 快速系统调用的支持
        developerWorks
       
       
文档选项
        将此页作为电子邮件发送       

将此页作为电子邮件发送

最新推荐
               


刘子锐, Linux 爱好者

2004 年 5 月 01 日

    文章分析了在 Linux 2.6 中引入的对 Intel CPU 快速系统调用指令 SYSENTER/SYSEXIT 支持的实现。Linux 驱动及内核开发者通过了解快速系统调用指令的机制,可以在自己的代码中通过利用这一机制,提高系统性能,并避开由快速系统调用方式带来的一些局限(如系统调用中嵌套系统调用)。

前言

在 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 microseconds        6.833 microseconds
基于中断 INT n 指令的系统调用        17.500 microseconds        7.000 microseconds

数据来源:[1]

数据来源:[2]

表2:各种 CPU 上 INT 0x80 和 SYSENTER 执行速度的比较
CPU        Int0x80        sysenter
Athlon XP 1600+        277        169
800MHz mode 1 athlon        279        170
2.8GHz p4 northwood ht        1152        442

上述数据为对 100000 次 getppid() 系统调用所花费的 CPU 时钟周期取的平均值
数据来源[3]

自这种技术推出之后,人们一直在考虑在 Linux 中加入对这种指令的支持,在 Kernel.org 的邮件列表中,主题为 "Intel P6 vs P7 system call performance" 的大量邮件讨论了采用这种指令的必要性,邮件中列举的理由主要是 Intel 在 Pentium 4 的设计上存在问题,造成 Pentium 4 使用中断方式执行的系统调用比 Pentium 3 以及 AMD Athlon 所耗费的 CPU 时钟周期多上 5~10 倍。因此,在 Pentium 4 平台上,通过 sysenter/sysexit 指令来执行系统调用已经是刻不容缓的需求。





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 的正确性。





如何得知 CPU 是否支持 sysenter/sysexit 指令

根据 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 指令。





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工具查看任意一个可执行程序,可以看到下面的结果:


[root@test]# 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 stripped
[root@test]# 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 (0x40000000)


这个所谓的"linux- gate.so.1"的内容就是内核映射的代码,系统中其实并不存在这样一个链接库文件,它的名字是由ldd自己起的,而在老版本的ldd中,虽然能够检测到这段代码,但是由于没有命名而且在系统中找不到对应链接库文件,所以会有一些显示上的问题。有关这个问题的背景,可以参考下面这个网址: http://sources.redhat.com/ml/libc-alpha/2003-09/msg00263.html






由用户态经库函数进入内核态

为了配合内核使用新的系统调用方式,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函数。



[root@test opt]# gcc test.c -o ./static -static
[root@test opt]# gdb ./static
(gdb) disassemble main
0x08048204 <main+0>:    push   %ebp
0x08048205 <main+1>:    mov    %esp,%ebp
0x08048207 <main+3>:    sub    $0x8,%esp
0x0804820a <main+6>:    and    $0xfffffff0,%esp
0x0804820d <main+9>:    mov    $0x0,%eax
0x08048212 <main+14>:   sub    %eax,%esp
0x08048214 <main+16>:   call   0x804cb20 <__getuid>
0x08048219 <main+21>:   leave
0x0804821a <main+22>:   ret

               


可以看出,main函数中调用了__getuid函数,接着反编译__getuid函数。



(gdb) disassemble 0x804cb20
0x0804cb20 <__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>:       mov    $0x18,%eax
0x0804cb31 <__getuid+17>:       call   *0x80aa054
0x0804cb37 <__getuid+23>:       pop    %ebp
0x0804cb38 <__getuid+24>:       ret

               


上面只是__getuid函数的一部分。可以看到__getuid将eax寄存器赋值为getuid系统调用的功能号0x18然后调用了另一个函数,这个函数的入口在哪里呢?接着查看位于地址0x80aa054的值。



(gdb) X 0x80aa054
0x80aa054 <_dl_sysinfo>:        0x0804d7f6

               


看起来不像是指向内核映射页面内的代码,但是,可以确认,__dl_sysinfo指针的指向的地址就是0x80aa054。下面,我们试着启动这个程序,然后停在程序第一条语句,再查看这个地方的值。



(gdb) b main
Breakpoint 1 at 0x804820a
(gdb) r
Starting program: /opt/static
Breakpoint 1, 0x0804820a in main ()
(gdb) X 0x80aa054
0x80aa054 <_dl_sysinfo>:        0xffffe400

               


可以看到,_dl_sysinfo指针指向的数值已经发生了变化,指向了0xffffe400,如果我们继续运行程序,__getuid函数将会调用地址0xffffe400处的代码。

接下来,我们将上面的代码编译成动态链接的方式,即默认方式,用gdb装载并反编译main函数



[root@test opt]# gcc test.c -o ./dynamic
[root@test opt]# gdb ./dynamic
(gdb) disassemble main
0x08048204 <main+0>:    push   %ebp
0x08048205 <main+1>:    mov    %esp,%ebp
0x08048207 <main+3>:    sub    $0x8,%esp
0x0804820a <main+6>:    and    $0xfffffff0,%esp
0x0804820d <main+9>:    mov    $0x0,%eax
0x08048212 <main+14>:   sub    %eax,%esp
0x08048214 <main+16>:   call   0x8048288
0x08048219 <main+21>:   leave
0x0804821a <main+22>:   ret

               


由于libc库是在程序初始化时才被装载,所以我们先启动程序,并停在main第一条语句,然后反汇编getuid库函数




(gdb) b main
Breakpoint 1 at 0x804820a
(gdb) r
Starting program: /opt/dynamic
Breakpoint 1, 0x0804820a 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>:       add    $0x84b0f,%ebx
0x40219e60 <__getuid+16>:       mov    0xffffd87c(%ebx),%eax
0x40219e66 <__getuid+22>:       test   %eax,%eax
0x40219e68 <__getuid+24>:       jle    0x40219e80 <__getuid+48>
0x40219e6a <__getuid+26>:       mov    $0x18,%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   %edx
0xffffe402:     push   %ebp
0xffffe403:     mov    %esp,%ebp
0xffffe405:     sysenter
0xffffe407:     nop
0xffffe408:     nop
0xffffe409:     nop
0xffffe40a:     nop
0xffffe40b:     nop
0xffffe40c:     nop
0xffffe40d:     nop
0xffffe40e:     jmp    0xffffe403
0xffffe410:     pop    %ebp
0xffffe411:     pop    %edx
0xffffe412:     pop    %ecx
0xffffe413:     ret
End 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 上,还没有支持 sysenter 指令,而在 AMD 推出的 AMD64 系列 CPU 上,处于某些模式的情况下,CPU 能够支持 sysenter/sysexit 指令。在 Linux 内核针对 AMD64 架构的代码中,采用的还是 SYSCALL/SYSRET 指令。至于这两种指令最终谁将成为标准,目前还无法得出结论。





未来

我们将 Intel 的 sysenter/sysexit 指令,AMD 的 SYSCALL/SYSRET 指令统称为"快速系统调用指令"。"快速系统调用指令"比起中断指令来说,其消耗时间必然会少一些,但是随着 CPU 设计的发展,将来应该不会再出现类似 Intel Pentium4 这样悬殊的差距。而"快速系统调用指令"比起中断方式的系统调用方式,还存在一定局限,例如无法在一个系统调用处理过程中再通过"快速系统调用指令"调用别的系统调用。因此,并不一定每个系统调用都需要通过"快速系统调用指令"来实现。比如,对于复杂的系统调用例如 fork,两种系统调用方式的时间差和系统调用本身运行消耗的时间来比,可以忽略不计,此处采取"快速系统调用指令"方式没有什么必要。而真正应该使用" 快速系统调用指令"方式的,是那些本身运行时间很短,对时间精确性要求高的系统调用,例如 getuid、gettimeofday 等等。因此,采取灵活的手段,针对不同的系统调用采取不同的方式,才能得到最优化的性能和实现最完美的功能。






参考资料

[1] VxWorks Optimized for Intel Architecture,
Hdei Nunoe, Wind River, Member of Technical Staff
Leo Samson, Wind River, Technical Marketing Engineer
David Hillyard, Intel Corporation, Mgr., Platform Architect

[2] Kernel Entry / Kernel Exit , Marcus Voelp & University Karlsruhe

[3] Dave Jones' blog, http://diary.codemonkey.org.uk/index.php?month=12&year=2002

[4] Linux 内核源码 v2.6.0
http://www.kernel.org
[Linus Torvalds,2004]

[5] GNU C Library glibc 2.3.3 源码
http://www.gnu.org/software/libc/libc.html

Linux Kernel Mailing List 中对系统调用方式的讨论:
[5] Linux Kernel Mailing List, "Intel P6 vs P7 system call performance"
http://www.ussg.iu.edu/hypermail ... 2.1/index.html#1286
http://www.ussg.iu.edu/hypermail ... 212.3/index.html#54

Linux 内核首次引入对 sysenter/sysexit 指令的支持:
[6] Linux Kernel Mailing List, "Add "sysenter" support on x86, and a "vsyscall" page."
http://lwn.net/Articles/18414/


关于作者


       

刘子锐:Linux 爱好者,从事过驱动程序开发和内核安全问题的研究,对 Linux 内核和 Java 虚拟机很感兴趣。通过 liuzirui@ustc.edu可以跟他联系。











原文链接:
http://www-128.ibm.com/developer ... -k26ncpu/index.html
               

刘子锐:Linux 爱好者,从事过驱动程序开发和内核安全问题的研究,对 Linux 内核和 Java 虚拟机很感兴趣。通过 liuzirui@ustc.edu可以跟他联系。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP