由用户态经库函数进入内核态
为了配合内核使用新的系统调用方式,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 来验证。
首先是一个静态编译的程序,代码很简单:
将代码加上 static 选项用 gcc 静态编译,然后用 gdb 装载并反编译 main 函数。
[root@test opt]# gcc test.c -o ./static -static [root@test opt]
# gdb ./static (gdb) disassemble main 0x08048204 :
push %ebp 0x08048205 : mov %esp,%ebp 0x08048207 :
sub $0x8,%esp 0x0804820a :
and $0xfffffff0,%esp 0x0804820d :
mov $0x0,%eax 0x08048212 : sub
%eax,%esp 0x08048214 : call 0x804cb20
<__getuid> 0x08048219 : leave 0x0804821a : 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 :
push %ebp 0x08048205 : mov %esp,%ebp 0x08048207 :
sub $0x8,%esp 0x0804820a : and $0xfffffff0,
%esp 0x0804820d : mov $0x0,%eax 0x08048212 : sub
%eax,%esp 0x08048214 : call 0x8048288 0x08048219 :
leave 0x0804821a : 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 入口处。