32位Linux下的系统调用
我们知道,Linux系统分为用户态和内核态,当用户态的应用程序请求执行内核态代码获取相关内核服务时,需要通过系统调用的形式来完成,比如利用系统调用getuid()请求获取执行程序的真实用户ID号。
当然,getuid()只是glibc封装的库函数,我们也可以直接通过syscall函数(这里是指glibc库里的syscall接口,请和后面文章将提到的syscall指令区分开)进行调用:
[root@lenky getuid]# cat getuid_glibc.c /** * filename: getuid_glibc.c */ #include <stdio.h> #include <unistd.h> #include <sys/types.h> int main(int argc, char *argv[]) { printf("uid:%d\n", getuid()); return 0; } [root@lenky getuid]# cat getuid_syscall.c /** * filename: getuid_syscall.c */ #include <stdio.h> #define _GNU_SOURCE #include <unistd.h> #include <sys/syscall.h> int main(int argc, char *argv[]) { printf("uid:%d\n", syscall(__NR_getuid)); return 0; }
不管是库函数,还是直接syscall,代表请求内核服务的数字(宏__NR_getuid)是唯一的,内核通过判断这个数字提供对应的服务。需要注意的是,同一个名称的系统调用所对应的这个数字在不同的硬件架构平台上会不一样,比如x86(即x86-32的简写,下同)上的__NR_getuid为24或199(即宏__NR_getuid32 ,这是因为从内核2.3.39开始,Linux引入了32位的UID/GIDs,在旧版本的glibc里会先判断当前内核是否支持宏__NR_getuid32,如果是则优先使用它,而新版本的glibc可能就直接使用getuid32了),而x64(即x86-64的简写,下同)上的__NR_getuid为102。
应用程序只是利用系统调用(接口)请求服务,而对“如何从用户态切换到内核态执行,然后又从内核态切换回用户态”并不关心,事实上,系统调用的具体实现由glibc与内核负责,而传统的方式是采用int 0×80中断指令,即内核在IDT(中断向量表)中提供int 0×80入口,而glibc库就通过该指令以中断的形式进入/退出内核,实现系统调用。
具体就是把请求服务对应数字(比如__NR_getuid)放到eax(本文讨论的是X86系统,所以是32位)寄存器,然后执行指令int 0×80,从而以中断形式进入到内核,内核执行该中断对应的回调函数,根据eax值,开始处理应用程序的对应系统调用请求。
看下实例(从uname可以看出是x86系统,2.6.30的内核):
[root@lenky getuid]# uname -a Linux lenky 2.6.30 #2 SMP Tue Sep 21 17:19:57 CST 2010 i686 i686 i386 GNU/Linux [root@lenky getuid]# gcc getuid_glibc.c -o getuid_glibc -static [root@lenky getuid]# gdb ./getuid_glibc -q (no debugging symbols found) (gdb) tb __getuid Breakpoint 1 at 0x8050503 (gdb) r Starting program: /home/work/getuid/getuid_glibc (no debugging symbols found) 0x08050503 in getuid () (gdb) disass Dump of assembler code for function getuid: 0x08050500 <getuid+0>: push %ebp 0x08050501 <getuid+1>: mov %esp,%ebp 0x08050503 <getuid+3>: mov $0xc7,%eax 0x08050508 <getuid+8>: int $0x80 0x0805050a <getuid+10>: pop %ebp 0x0805050b <getuid+11>: ret End of assembler dump. (gdb)
汇编代码“mov $0xc7,%eax”把数值199放到eax寄存器,然后执行“int $0×80”进行系统调用。再看利用syscall接口的代码:
[root@lenky getuid]# uname -a Linux lenky 2.6.30 #2 SMP Tue Sep 21 17:19:57 CST 2010 i686 i686 i386 GNU/Linux [root@lenky getuid]# gcc getuid_syscall.c -o getuid_syscall -static [root@lenky getuid]# gdb ./getuid_syscall -q (no debugging symbols found) (gdb) b main Breakpoint 1 at 0x8048236 (gdb) r Starting program: /home/work/getuid/getuid_syscall (no debugging symbols found) Breakpoint 1, 0x08048236 in main () (gdb) disassemble Dump of assembler code for function main: ... 0x08048236 <main+14>: sub $0x14,%esp 0x08048239 <main+17>: movl $0x18,(%esp) 0x08048240 <main+24>: call 0x80512a0 <syscall> ... (gdb) disassemble 0x80512a0 Dump of assembler code for function syscall: ... 0x080512bc <syscall+28>: mov 0x14(%esp),%eax 0x080512c0 <syscall+32>: int $0x80 ... ---Type <return> to continue, or q <return> to quit---
利用syscall接口时,直接使用的宏__NR_getuid,因此对应的值为0×18,把上面的汇编前后承接起来看,还是同样的逻辑,即把数值24放到eax寄存器,然后执行“int $0×80”。
利用int 0×80中断指令实现系统调用有一些缺点:
在 x86 保护模式中,处理 INT 中断指令时,CPU 首先从中断描述表 IDT 取出对应的门描述符,判断门描述符的种类,然后检查门描述符的级别 DPL 和 INT 指令调用者的级别 CPL,当 CPL<=DPL 也就是说 INT 调用者级别高于描述符指定级别时,才能成功调用,最后再根据描述符的内容,进行压栈、跳转、权限级别提升。内核代码执行完毕之后,调用 IRET 指令返回,IRET 指令恢复用户栈,并跳转会低级别的代码。
(引用参考1)
而针对系统调用的特点,CPU硬件开始提供有专门指令:
其实,在发生系统调用,由 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 快了不少。
(引用参考1)
跟随硬件的发展,从2.5版本开始,Linux内核也就利用sysenter/sysexit指令实现了一种新的系统调用机制,当然,新内核并不能完全摒弃int 0×80调用方式,因为还是要考虑有的机器可能不支持sysenter/sysexit指令,所以glibc与内核需进行了更亲密的合作,即内核与glibc约定:
你(glibc)以后需要进行系统调用的时候就统一使用我给你的一个地址,而在“这个地址”会根据当前机器情况,选择执行int 0×80调用方式,或者是sysenter/sysexit调用方式。
(引用参考4,我略做修改)
而充当“这个地址”角色的函数就是__kernel_vsyscall,显然,这个地址必须让所有应用程序进程都知晓,于是内核创建了一个可以映射到所有应用程序进程地址空间的单独页面:
[root@lenky getuid]# cat /proc/self/maps 00149000-00163000 r-xp 00000000 fd:00 1967404 /lib/ld-2.5.so 00163000-00164000 r--p 00019000 fd:00 1967404 /lib/ld-2.5.so 00164000-00165000 rw-p 0001a000 fd:00 1967404 /lib/ld-2.5.so 0016c000-002ab000 r-xp 00000000 fd:00 1967405 /lib/libc-2.5.so 002ab000-002ad000 r--p 0013f000 fd:00 1967405 /lib/libc-2.5.so 002ad000-002ae000 rw-p 00141000 fd:00 1967405 /lib/libc-2.5.so 002ae000-002b1000 rw-p 00000000 00:00 0 08048000-0804d000 r-xp 00000000 fd:00 12353611 /bin/cat 0804d000-0804e000 rw-p 00004000 fd:00 12353611 /bin/cat 0804e000-0806f000 rw-p 00000000 00:00 0 [heap] b7d38000-b7f38000 r--p 00000000 fd:00 8852004 /usr/lib/locale/locale-archive b7f38000-b7f3a000 rw-p 00000000 00:00 0 b7f44000-b7f45000 r-xp 00000000 00:00 0 [vdso] bf9a4000-bf9b9000 rw-p 00000000 00:00 0 [stack]
注意其中的[vdso](Virtual Dynamically linked Shared Objects),也就是那个包含__kernel_vsyscall的单独页面,使用ldd命令也可以看到(其中的linux-gate.so.1):
[root@lenky getuid]# ldd /bin/cat linux-gate.so.1 => (0xb8021000) libc.so.6 => /lib/libc.so.6 (0x0016c000) /lib/ld-linux.so.2 (0x00149000)
这个单独页面被映射到进程地址空间的地址是变化的,即同一个程序每一次执行,[vdso]的位置都不一样,但总是一页(4096)大小:
[root@lenky getuid]# cat /proc/self/maps | grep vdso b80ed000-b80ee000 r-xp 00000000 00:00 0 [vdso] [root@lenky getuid]# cat /proc/self/maps | grep vdso b8011000-b8012000 r-xp 00000000 00:00 0 [vdso] [root@lenky getuid]# cat /proc/self/maps | grep vdso b806c000-b806d000 r-xp 00000000 00:00 0 [vdso]
这当然是从安全角度着想(请Google:A Guide to Kernel Exploitation),可以关闭它:
[root@lenky getuid]# echo 0 > /proc/sys/kernel/randomize_va_space [root@lenky getuid]# cat /proc/self/maps | grep vdso b7fff000-b8000000 r-xp 00000000 00:00 0 [vdso] [root@lenky getuid]# cat /proc/self/maps | grep vdso b7fff000-b8000000 r-xp 00000000 00:00 0 [vdso] [root@lenky getuid]# ldd /bin/ls | grep gate linux-gate.so.1 => (0xb7fff000) [root@lenky getuid]# ldd /bin/ls | grep gate linux-gate.so.1 => (0xb7fff000)
不管[vdso]的地址是否会发生变化,按如下方法都可以把[vdso]导出来:
[root@lenky getuid]# echo 1 > /proc/sys/kernel/randomize_va_space [root@lenky getuid]# gdb -q /bin/ls (no debugging symbols found) (gdb) tb __open Function "__open" not defined. Make breakpoint pending on future shared library load? (y or [n]) y Breakpoint 1 (__open) pending. (gdb) r Starting program: /bin/ls (no debugging symbols found) (no debugging symbols found) 0x0015e240 in open () from /lib/ld-linux.so.2 (gdb) info program Using the running image of child process 18413. Program stopped at 0x15e240. It stopped at a breakpoint that has since been deleted. (gdb) shell cat /proc/18413/maps | grep vdso b7f8e000-b7f8f000 r-xp 00000000 00:00 0 [vdso] (gdb) dump memory /tmp/linux-gate.so.1 0xb7f8e000 0xb7f8f000 (gdb) q The program is running. Exit anyway? (y or n) y [root@lenky getuid]# readelf -h /tmp/linux-gate.so.1 ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Shared object file) Machine: Intel 80386 Version: 0x1 Entry point address: 0xffffe414 Start of program headers: 52 (bytes into file) Start of section headers: 1176 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 4 Size of section headers: 40 (bytes) Number of section headers: 13 Section header string table index: 12 [root@lenky getuid]#
看看其内容:
[root@lenky getuid]# objdump -d /tmp/linux-gate.so.1 | grep -A15 __kernel_vsyscall ffffe414 <__kernel_vsyscall>: ffffe414: 51 push %ecx ffffe415: 52 push %edx ffffe416: 55 push %ebp ffffe417: 89 e5 mov %esp,%ebp ffffe419: 0f 34 sysenter ffffe41b: 90 nop ffffe41c: 90 nop ffffe41d: 90 nop ffffe41e: 90 nop ffffe41f: 90 nop ffffe420: 90 nop ffffe421: 90 nop ffffe422: eb f3 jmp ffffe417 <__kernel_vsyscall+0x3> ffffe424: 5d pop %ebp ffffe425: 5a pop %edx ffffe426: 59 pop %ecx ffffe427: c3 ret
可以看到,我这台机器是支持sysenter的,那问题来了,为什么前面的实例里还是“int $0×80”呢?原因很简单,那里是静态连接,谁知道你静态连接编译出来的程序会在什么机器上执行,所以必须用最兼容的“int $0×80”调用方式。这也意味着只有动态连接才可能使用sysenter指令。
前面提到[vdso]在程序每次执行的位置都不一样,那么程序又如何调用到__kernel_vsyscall?这并不难解决,因为对于这个地址,内核是知道的,它通过elf参数AT_SYSINFO和AT_SYSINFO_EHDR告诉应用程序即可:
[root@lenky getuid]# LD_SHOW_AUXV=true cat /proc/self/maps | grep "AT_SYSINFO\|vdso" AT_SYSINFO: 0xb7f19414 AT_SYSINFO_EHDR: 0xb7f19000 b7f19000-b7f1a000 r-xp 00000000 00:00 0 [vdso] [root@lenky getuid]# LD_SHOW_AUXV=true cat /proc/self/maps | grep "AT_SYSINFO\|vdso" AT_SYSINFO: 0xb7fdc414 AT_SYSINFO_EHDR: 0xb7fdc000 b7fdc000-b7fdd000 r-xp 00000000 00:00 0 [vdso]
好,下面看实际情况如何:
[root@lenky getuid]# gcc getuid_glibc.c -o getuid_glibc [root@lenky getuid]# gdb ./getuid_glibc -q (no debugging symbols found) (gdb) b __getuid Function "__getuid" not defined. Make breakpoint pending on future shared library load? (y or [n]) y Breakpoint 1 (__getuid) pending. (gdb) r Starting program: /home/work/getuid/getuid_glibc (no debugging symbols found) (no debugging symbols found) (no debugging symbols found) Breakpoint 1, 0x001fe6f3 in getuid () from /lib/libc.so.6 (gdb) disass Dump of assembler code for function getuid: 0x001fe6f0 <getuid+0>: push %ebp 0x001fe6f1 <getuid+1>: mov %esp,%ebp 0x001fe6f3 <getuid+3>: mov $0xc7,%eax 0x001fe6f8 <getuid+8>: call *%gs:0x10 0x001fe6ff <getuid+15>: pop %ebp 0x001fe700 <getuid+16>: ret End of assembler dump. (gdb)
汇编语句“call *%gs:0×10”就会调入到函数__kernel_vsyscall内,至于为何是这样,这里有个解释:
Why %gs:0×10? Parsing process stack to find out AT_SYSINFO’s value can be a cumbersome task. So, when libc.so (C library) is loaded, it copies the value of AT_SYSINFO from the process stack to the TCB (Thread Control Block). Segment register %gs refers to the TCB.
(引用参考2)
可以接着验证:
(gdb) b __kernel_vsyscall Breakpoint 2 at 0xb808f414 (gdb) c Continuing. Breakpoint 2, 0xb808f414 in __kernel_vsyscall () (gdb) disassemble Dump of assembler code for function __kernel_vsyscall: 0xb808f414 <__kernel_vsyscall+0>: push %ecx 0xb808f415 <__kernel_vsyscall+1>: push %edx 0xb808f416 <__kernel_vsyscall+2>: push %ebp 0xb808f417 <__kernel_vsyscall+3>: mov %esp,%ebp 0xb808f419 <__kernel_vsyscall+5>: sysenter 0xb808f41b <__kernel_vsyscall+7>: nop 0xb808f41c <__kernel_vsyscall+8>: nop 0xb808f41d <__kernel_vsyscall+9>: nop 0xb808f41e <__kernel_vsyscall+10>: nop 0xb808f41f <__kernel_vsyscall+11>: nop 0xb808f420 <__kernel_vsyscall+12>: nop 0xb808f421 <__kernel_vsyscall+13>: nop 0xb808f422 <__kernel_vsyscall+14>: jmp 0xb808f417 <__kernel_vsyscall+3> 0xb808f424 <__kernel_vsyscall+16>: pop %ebp 0xb808f425 <__kernel_vsyscall+17>: pop %edx 0xb808f426 <__kernel_vsyscall+18>: pop %ecx 0xb808f427 <__kernel_vsyscall+19>: ret End of assembler dump. (gdb)
对于x86平台的系统调用,大致就是如本文所介绍的这样了,至于x64的情况,下文再看。
参考:
1,Linux 2.6 对新型 CPU 快速系统调用的支持
2,Sysenter Based System Call Mechanism in Linux 2.6
3,http://www.acsu.buffalo.edu/~charngda/x86assembly.html
4,linux下的vdso与vsyscall
5,inux syscalls on x86_64
6,Is syscall an instruction on x86_64?
7,x86_64 Assembly Linux System Call Confusion
8,What are the calling conventions for UNIX & Linux system calls on x86-64
9,http://www.win.tue.nl/~aeb/linux/lk/lk-4.html
转载请保留地址:http://lenky.info/archives/2013/02/04/2198 或 http://lenky.info/?p=2198
备注:如无特殊说明,文章内容均出自Lenky个人的真实理解而并非存心妄自揣测来故意愚人耳目。由于个人水平有限,虽力求内容正确无误,但仍然难免出错,请勿见怪,如果可以则请留言告之,并欢迎来讨论。另外值得说明的是,Lenky的部分文章以及部分内容参考借鉴了网络上各位网友的热心分享,特别是一些带有完全参考的文章,其后附带的链接内容也许更直接、更丰富,而我只是做了一下归纳&转述,在此也一并表示感谢。关于本站的所有技术文章,欢迎转载,但请遵从CC创作共享协议,而一些私人性质较强的心情随笔,建议不要转载。
法律:根据最新颁布的《信息网络传播权保护条例》,如果您认为本文章的任何内容侵犯了您的权利,请以或书面等方式告知,本站将及时删除相关内容或链接。