与系统调用混淆
Confusion with system call
我想了解在 x86 中系统调用是如何进行的。我正在阅读 Smashing the stack for fun and profit。第7页给出了一个函数:
#include <stdio.h>
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
下面的函数给出了它的程序集转储:
函数 main 的汇编代码转储:
0x8000130 : pushl %ebp
0x8000131 : movl %esp,%ebp
0x8000133 : subl [=11=]x8,%esp
0x8000136 : movl [=11=]x80027b8,0xfffffff8(%ebp)
0x800013d : movl [=11=]x0,0xfffffffc(%ebp)
0x8000144 : pushl [=11=]x0
0x8000146 : leal 0xfffffff8(%ebp),%eax
0x8000149 : pushl %eax
0x800014a : movl 0xfffffff8(%ebp),%eax
0x800014d : pushl %eax
0x800014e : call 0x80002bc <__execve>
0x8000153 : addl [=11=]xc,%esp
0x8000156 : movl %ebp,%esp
0x8000158 : popl %ebp
0x8000159 : ret
函数 __execve:
的汇编代码转储
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
0x80002c0 <__execve+4>: movl [=12=]xb,%eax
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
0x80002ce <__execve+18>: int [=12=]x80
0x80002d0 <__execve+20>: movl %eax,%edx
0x80002d2 <__execve+22>: testl %edx,%edx
0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>
0x80002d6 <__execve+26>: negl %edx
0x80002d8 <__execve+28>: pushl %edx
0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location>
0x80002de <__execve+34>: popl %edx
0x80002df <__execve+35>: movl %edx,(%eax)
0x80002e1 <__execve+37>: movl [=12=]xffffffff,%eax
0x80002e6 <__execve+42>: popl %ebx
0x80002e7 <__execve+43>: movl %ebp,%esp
0x80002e9 <__execve+45>: popl %ebp
0x80002ea <__execve+46>: ret
0x80002eb <__execve+47>: nop
但是在我的机器上编写相同的代码并使用
进行编译
gcc test.c -m32 -g -o test -fno-stack-protector -static
并使用
生成转储
objdump -S test > test.dis
我得到以下主要的转储:
void main(){
8048e24: 55 push %ebp
8048e25: 89 e5 mov %esp,%ebp
8048e27: 83 e4 f0 and [=13=]xfffffff0,%esp
8048e2a: 83 ec 20 sub [=13=]x20,%esp
char *name[2];
name[0] = "/bin/sh";
8048e2d: c7 44 24 18 e8 de 0b movl [=13=]x80bdee8,0x18(%esp)
8048e34: 08
name[1] = NULL;
8048e35: c7 44 24 1c 00 00 00 movl [=13=]x0,0x1c(%esp)
8048e3c: 00
execve(name[0], name, NULL);
8048e3d: 8b 44 24 18 mov 0x18(%esp),%eax
8048e41: c7 44 24 08 00 00 00 movl [=13=]x0,0x8(%esp)
8048e48: 00
8048e49: 8d 54 24 18 lea 0x18(%esp),%edx
8048e4d: 89 54 24 04 mov %edx,0x4(%esp)
8048e51: 89 04 24 mov %eax,(%esp)
8048e54: e8 17 34 02 00 call 806c270 <__execve>
}
对于__execve:
0806c270 <__execve>:
806c270: 53 push %ebx
806c271: 8b 54 24 10 mov 0x10(%esp),%edx
806c275: 8b 4c 24 0c mov 0xc(%esp),%ecx
806c279: 8b 5c 24 08 mov 0x8(%esp),%ebx
806c27d: b8 0b 00 00 00 mov [=14=]xb,%eax
806c282: ff 15 f0 99 0e 08 call *0x80e99f0
806c288: 3d 00 f0 ff ff cmp [=14=]xfffff000,%eax
806c28d: 77 02 ja 806c291 <__execve+0x21>
806c28f: 5b pop %ebx
806c290: c3 ret
806c291: c7 c2 e8 ff ff ff mov [=14=]xffffffe8,%edx
806c297: f7 d8 neg %eax
806c299: 65 89 02 mov %eax,%gs:(%edx)
806c29c: 83 c8 ff or [=14=]xffffffff,%eax
806c29f: 5b pop %ebx
806c2a0: c3 ret
806c2a1: 66 90 xchg %ax,%ax
806c2a3: 66 90 xchg %ax,%ax
806c2a5: 66 90 xchg %ax,%ax
806c2a7: 66 90 xchg %ax,%ax
806c2a9: 66 90 xchg %ax,%ax
806c2ab: 66 90 xchg %ax,%ax
806c2ad: 66 90 xchg %ax,%ax
806c2af: 90 nop
我知道这篇文章很旧,所以它可能不完全符合当前的标准。事实上,我能够理解大部分差异。这是困扰我的事情:
据我所知:要进行 exec 系统调用,我需要将参数放入特定寄存器并调用指令
int 0x80
发送中断。我可以在文章中给出的转储中的地址 0x80002ce 看到这条指令。但是我在我的手册中找不到相同的说明。取而代之的是
call *0x80e99f0
并且地址 0x80e99f0 甚至不存在于我的转储中。我在这里错过了什么? 0x80e99f0之前的*有什么意义呢?地址 0x80e99f0 是否在 运行 时被动态加载?如果是这样,那么编译期间 -static 标志的用途是什么?我该怎么做才能使转储类似于文章中的转储?
我是 运行宁 64 位 ubuntu 14.04 英特尔处理器
在获得对带有 -DS 标志的 运行 objdump 的建议后进行编辑:
终于找到隐藏地址了:
080e99f0 <_dl_sysinfo>:
80e99f0: 70 ed jo 80e99df <_dl_load_lock+0x7>
80e99f2: 06 push %es
80e99f3: 08 b0 a6 09 08 07 or %dh,0x70809a6(%eax)
但仍然没有任何意义。
jo 80e99df 中的地址再次指向隐藏在这些行之间的内容:
080e99d8 <_dl_load_lock>:
...
80e99e4: 01 00 add %eax,(%eax)
...
从答案中可以明显看出,代码实际上跳转到内存位置 0x80e99f0 中的地址,最终指向 int [=17=]x80
指令。
尝试使用 objdump -DS
或 objdump -sS
在转储中包含地址 0x80e99f0。
本地示例:
0806bf70 <__execve>:
...
806bf82: ff 15 10 a3 0e 08 call *0x80ea310
地址 0x80ea310(显示为 objdump -sS
):
80ea310 10ea0608 60a60908 07000000 7f030000
10ea0608
是内存中的地址 0x806ea10 little-endian。
然后您会看到 _dl_sysinfo_int80
的地址位于此处:
0806ea10 <_dl_sysinfo_int80>:
806ea10: cd 80 int [=12=]x80
806ea12: c3 ret
它调用软件中断 0x80(执行系统调用)然后 returns 到调用者。
调用 *0x80ea310 因此实际上是在调用 0x806ea10(取消引用指针)
传统上,Linux 使用中断 0x80 来调用系统调用。从 PentiumPro 开始,有一种调用系统调用的替代方法:使用 SYSENTER 指令(AMD 也有自己的 SYSCALL 指令)。这是调用系统调用的更有效方法。
选择要使用的系统调用机制
linux 内核和 glibc 有一种机制可以在调用系统调用的不同方式之间进行选择。
内核为每个进程设置了一个虚拟共享库,称为VDSO(virtual dynamic shared object),你可以在cat /proc/<pid>/maps
的输出中看到:
$ cat /proc/self/maps
08048000-0804c000 r-xp 00000000 03:04 1553592 /bin/cat
0804c000-0804d000 rw-p 00003000 03:04 1553592 /bin/cat
[...]
b7ee8000-b7ee9000 r-xp b7ee8000 00:00 0 [vdso]
[...]
这个 vdso,除其他外,包含一个适当的系统调用调用序列,用于 CPU 在使用中,例如:
ffffe414 <__kernel_vsyscall>:
ffffe414: 51 push %ecx ; \
ffffe415: 52 push %edx ; > save registers
ffffe416: 55 push %ebp ; /
ffffe417: 89 e5 mov %esp,%ebp ; save stack pointer
ffffe419: 0f 34 sysenter ; invoke system call
ffffe41b: 90 nop
ffffe41c: 90 nop ; the kernel will usually
ffffe41d: 90 nop ; return to the insn just
ffffe41e: 90 nop ; past the jmp, but if the
ffffe41f: 90 nop ; system call was interrupted
ffffe420: 90 nop ; and needs to be restarted
ffffe421: 90 nop ; it will return to this jmp
ffffe422: eb f3 jmp ffffe417 <__kernel_vsyscall+0x3>
ffffe424: 5d pop %ebp ; \
ffffe425: 5a pop %edx ; > restore registers
ffffe426: 59 pop %ecx ; /
ffffe427: c3 ret ; return to caller
在arch/x86/vdso/vdso32/
中有使用int 0x80
、sysenter
和syscall
的实现,内核选择合适的。
为了让用户空间知道有一个 vdso,以及它所在的位置,内核在辅助向量中设置 AT_SYSINFO
和 AT_SYSINFO_EHDR
条目(auxv
,第 4 个参数到 main()
,在 argc, argv, envp
之后,用于将一些信息从内核传递给新启动的进程)。 AT_SYSINFO_EHDR
指向vdso的ELF header,AT_SYSINFO
指向vsyscall实现:
$ LD_SHOW_AUXV=1 id # tell the dynamic linker ld.so to output auxv values
AT_SYSINFO: 0xb7fd4414
AT_SYSINFO_EHDR: 0xb7fd4000
[...]
glibc 使用此信息定位 vsyscall
。它将它存储到动态加载程序全局 _dl_sysinfo
,例如:
glibc-2.16.0/elf/dl-support.c:_dl_aux_init():
ifdef NEED_DL_SYSINFO
case AT_SYSINFO:
GL(dl_sysinfo) = av->a_un.a_val;
break;
#endif
#if defined NEED_DL_SYSINFO || defined NEED_DL_SYSINFO_DSO
case AT_SYSINFO_EHDR:
GL(dl_sysinfo_dso) = (void *) av->a_un.a_val;
break;
#endif
glibc-2.16.0/elf/dl-sysdep.c:_dl_sysdep_start()
glibc-2.16.0/elf/rtld.c:dl_main:
GLRO(dl_sysinfo) = GLRO(dl_sysinfo_dso)->e_entry + l->l_addr;
并且在 TCB(线程控制块)的 header 中的一个字段中:
glibc-2.16.0/nptl/sysdeps/i386/tls.h
_head->sysinfo = GLRO(dl_sysinfo)
如果内核是旧的并且不提供vdso,glibc 为_dl_sysinfo
提供默认实现:
.hidden _dl_sysinfo_int80:
int [=15=]x80
ret
当针对 glibc 编译程序时,根据情况,在调用系统调用的不同方式之间做出选择:
glibc-2.16.0/sysdeps/unix/sysv/linux/i386/sysdep.h:
/* The original calling convention for system calls on Linux/i386 is
to use int [=16=]x80. */
#ifdef I386_USE_SYSENTER
# ifdef SHARED
# define ENTER_KERNEL call *%gs:SYSINFO_OFFSET
# else
# define ENTER_KERNEL call *_dl_sysinfo
# endif
#else
# define ENTER_KERNEL int [=16=]x80
#endif
int 0x80
← 传统方式
call *%gs:offsetof(tcb_head_t, sysinfo)
← %gs
指向TCB,所以这里通过指向TCB 中存储的vsyscall的指针间接跳转
call *_dl_sysinfo
← 这个通过全局变量间接跳转
因此,在 x86 中:
system call
↓
int 0x80 / call *%gs:0x10 / call *_dl_sysinfo
│ │
╰─┬──────────┼─────────╮
↓ ↓ ↓
(in vdso) int 0x80 / sysenter / syscall
我想了解在 x86 中系统调用是如何进行的。我正在阅读 Smashing the stack for fun and profit。第7页给出了一个函数:
#include <stdio.h>
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
下面的函数给出了它的程序集转储:
函数 main 的汇编代码转储:
0x8000130 : pushl %ebp
0x8000131 : movl %esp,%ebp
0x8000133 : subl [=11=]x8,%esp
0x8000136 : movl [=11=]x80027b8,0xfffffff8(%ebp)
0x800013d : movl [=11=]x0,0xfffffffc(%ebp)
0x8000144 : pushl [=11=]x0
0x8000146 : leal 0xfffffff8(%ebp),%eax
0x8000149 : pushl %eax
0x800014a : movl 0xfffffff8(%ebp),%eax
0x800014d : pushl %eax
0x800014e : call 0x80002bc <__execve>
0x8000153 : addl [=11=]xc,%esp
0x8000156 : movl %ebp,%esp
0x8000158 : popl %ebp
0x8000159 : ret
函数 __execve:
的汇编代码转储0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
0x80002c0 <__execve+4>: movl [=12=]xb,%eax
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
0x80002ce <__execve+18>: int [=12=]x80
0x80002d0 <__execve+20>: movl %eax,%edx
0x80002d2 <__execve+22>: testl %edx,%edx
0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>
0x80002d6 <__execve+26>: negl %edx
0x80002d8 <__execve+28>: pushl %edx
0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location>
0x80002de <__execve+34>: popl %edx
0x80002df <__execve+35>: movl %edx,(%eax)
0x80002e1 <__execve+37>: movl [=12=]xffffffff,%eax
0x80002e6 <__execve+42>: popl %ebx
0x80002e7 <__execve+43>: movl %ebp,%esp
0x80002e9 <__execve+45>: popl %ebp
0x80002ea <__execve+46>: ret
0x80002eb <__execve+47>: nop
但是在我的机器上编写相同的代码并使用
进行编译gcc test.c -m32 -g -o test -fno-stack-protector -static
并使用
生成转储objdump -S test > test.dis
我得到以下主要的转储:
void main(){
8048e24: 55 push %ebp
8048e25: 89 e5 mov %esp,%ebp
8048e27: 83 e4 f0 and [=13=]xfffffff0,%esp
8048e2a: 83 ec 20 sub [=13=]x20,%esp
char *name[2];
name[0] = "/bin/sh";
8048e2d: c7 44 24 18 e8 de 0b movl [=13=]x80bdee8,0x18(%esp)
8048e34: 08
name[1] = NULL;
8048e35: c7 44 24 1c 00 00 00 movl [=13=]x0,0x1c(%esp)
8048e3c: 00
execve(name[0], name, NULL);
8048e3d: 8b 44 24 18 mov 0x18(%esp),%eax
8048e41: c7 44 24 08 00 00 00 movl [=13=]x0,0x8(%esp)
8048e48: 00
8048e49: 8d 54 24 18 lea 0x18(%esp),%edx
8048e4d: 89 54 24 04 mov %edx,0x4(%esp)
8048e51: 89 04 24 mov %eax,(%esp)
8048e54: e8 17 34 02 00 call 806c270 <__execve>
}
对于__execve:
0806c270 <__execve>:
806c270: 53 push %ebx
806c271: 8b 54 24 10 mov 0x10(%esp),%edx
806c275: 8b 4c 24 0c mov 0xc(%esp),%ecx
806c279: 8b 5c 24 08 mov 0x8(%esp),%ebx
806c27d: b8 0b 00 00 00 mov [=14=]xb,%eax
806c282: ff 15 f0 99 0e 08 call *0x80e99f0
806c288: 3d 00 f0 ff ff cmp [=14=]xfffff000,%eax
806c28d: 77 02 ja 806c291 <__execve+0x21>
806c28f: 5b pop %ebx
806c290: c3 ret
806c291: c7 c2 e8 ff ff ff mov [=14=]xffffffe8,%edx
806c297: f7 d8 neg %eax
806c299: 65 89 02 mov %eax,%gs:(%edx)
806c29c: 83 c8 ff or [=14=]xffffffff,%eax
806c29f: 5b pop %ebx
806c2a0: c3 ret
806c2a1: 66 90 xchg %ax,%ax
806c2a3: 66 90 xchg %ax,%ax
806c2a5: 66 90 xchg %ax,%ax
806c2a7: 66 90 xchg %ax,%ax
806c2a9: 66 90 xchg %ax,%ax
806c2ab: 66 90 xchg %ax,%ax
806c2ad: 66 90 xchg %ax,%ax
806c2af: 90 nop
我知道这篇文章很旧,所以它可能不完全符合当前的标准。事实上,我能够理解大部分差异。这是困扰我的事情:
据我所知:要进行 exec 系统调用,我需要将参数放入特定寄存器并调用指令
int 0x80
发送中断。我可以在文章中给出的转储中的地址 0x80002ce 看到这条指令。但是我在我的手册中找不到相同的说明。取而代之的是
call *0x80e99f0
并且地址 0x80e99f0 甚至不存在于我的转储中。我在这里错过了什么? 0x80e99f0之前的*有什么意义呢?地址 0x80e99f0 是否在 运行 时被动态加载?如果是这样,那么编译期间 -static 标志的用途是什么?我该怎么做才能使转储类似于文章中的转储?
我是 运行宁 64 位 ubuntu 14.04 英特尔处理器
在获得对带有 -DS 标志的 运行 objdump 的建议后进行编辑:
终于找到隐藏地址了:
080e99f0 <_dl_sysinfo>:
80e99f0: 70 ed jo 80e99df <_dl_load_lock+0x7>
80e99f2: 06 push %es
80e99f3: 08 b0 a6 09 08 07 or %dh,0x70809a6(%eax)
但仍然没有任何意义。
jo 80e99df 中的地址再次指向隐藏在这些行之间的内容:
080e99d8 <_dl_load_lock>:
...
80e99e4: 01 00 add %eax,(%eax)
...
从答案中可以明显看出,代码实际上跳转到内存位置 0x80e99f0 中的地址,最终指向 int [=17=]x80
指令。
尝试使用 objdump -DS
或 objdump -sS
在转储中包含地址 0x80e99f0。
本地示例:
0806bf70 <__execve>:
...
806bf82: ff 15 10 a3 0e 08 call *0x80ea310
地址 0x80ea310(显示为 objdump -sS
):
80ea310 10ea0608 60a60908 07000000 7f030000
10ea0608
是内存中的地址 0x806ea10 little-endian。
然后您会看到 _dl_sysinfo_int80
的地址位于此处:
0806ea10 <_dl_sysinfo_int80>:
806ea10: cd 80 int [=12=]x80
806ea12: c3 ret
它调用软件中断 0x80(执行系统调用)然后 returns 到调用者。
调用 *0x80ea310 因此实际上是在调用 0x806ea10(取消引用指针)
传统上,Linux 使用中断 0x80 来调用系统调用。从 PentiumPro 开始,有一种调用系统调用的替代方法:使用 SYSENTER 指令(AMD 也有自己的 SYSCALL 指令)。这是调用系统调用的更有效方法。
选择要使用的系统调用机制
linux 内核和 glibc 有一种机制可以在调用系统调用的不同方式之间进行选择。
内核为每个进程设置了一个虚拟共享库,称为VDSO(virtual dynamic shared object),你可以在cat /proc/<pid>/maps
的输出中看到:
$ cat /proc/self/maps
08048000-0804c000 r-xp 00000000 03:04 1553592 /bin/cat
0804c000-0804d000 rw-p 00003000 03:04 1553592 /bin/cat
[...]
b7ee8000-b7ee9000 r-xp b7ee8000 00:00 0 [vdso]
[...]
这个 vdso,除其他外,包含一个适当的系统调用调用序列,用于 CPU 在使用中,例如:
ffffe414 <__kernel_vsyscall>:
ffffe414: 51 push %ecx ; \
ffffe415: 52 push %edx ; > save registers
ffffe416: 55 push %ebp ; /
ffffe417: 89 e5 mov %esp,%ebp ; save stack pointer
ffffe419: 0f 34 sysenter ; invoke system call
ffffe41b: 90 nop
ffffe41c: 90 nop ; the kernel will usually
ffffe41d: 90 nop ; return to the insn just
ffffe41e: 90 nop ; past the jmp, but if the
ffffe41f: 90 nop ; system call was interrupted
ffffe420: 90 nop ; and needs to be restarted
ffffe421: 90 nop ; it will return to this jmp
ffffe422: eb f3 jmp ffffe417 <__kernel_vsyscall+0x3>
ffffe424: 5d pop %ebp ; \
ffffe425: 5a pop %edx ; > restore registers
ffffe426: 59 pop %ecx ; /
ffffe427: c3 ret ; return to caller
在arch/x86/vdso/vdso32/
中有使用int 0x80
、sysenter
和syscall
的实现,内核选择合适的。
为了让用户空间知道有一个 vdso,以及它所在的位置,内核在辅助向量中设置 AT_SYSINFO
和 AT_SYSINFO_EHDR
条目(auxv
,第 4 个参数到 main()
,在 argc, argv, envp
之后,用于将一些信息从内核传递给新启动的进程)。 AT_SYSINFO_EHDR
指向vdso的ELF header,AT_SYSINFO
指向vsyscall实现:
$ LD_SHOW_AUXV=1 id # tell the dynamic linker ld.so to output auxv values
AT_SYSINFO: 0xb7fd4414
AT_SYSINFO_EHDR: 0xb7fd4000
[...]
glibc 使用此信息定位 vsyscall
。它将它存储到动态加载程序全局 _dl_sysinfo
,例如:
glibc-2.16.0/elf/dl-support.c:_dl_aux_init():
ifdef NEED_DL_SYSINFO
case AT_SYSINFO:
GL(dl_sysinfo) = av->a_un.a_val;
break;
#endif
#if defined NEED_DL_SYSINFO || defined NEED_DL_SYSINFO_DSO
case AT_SYSINFO_EHDR:
GL(dl_sysinfo_dso) = (void *) av->a_un.a_val;
break;
#endif
glibc-2.16.0/elf/dl-sysdep.c:_dl_sysdep_start()
glibc-2.16.0/elf/rtld.c:dl_main:
GLRO(dl_sysinfo) = GLRO(dl_sysinfo_dso)->e_entry + l->l_addr;
并且在 TCB(线程控制块)的 header 中的一个字段中:
glibc-2.16.0/nptl/sysdeps/i386/tls.h
_head->sysinfo = GLRO(dl_sysinfo)
如果内核是旧的并且不提供vdso,glibc 为_dl_sysinfo
提供默认实现:
.hidden _dl_sysinfo_int80:
int [=15=]x80
ret
当针对 glibc 编译程序时,根据情况,在调用系统调用的不同方式之间做出选择:
glibc-2.16.0/sysdeps/unix/sysv/linux/i386/sysdep.h:
/* The original calling convention for system calls on Linux/i386 is
to use int [=16=]x80. */
#ifdef I386_USE_SYSENTER
# ifdef SHARED
# define ENTER_KERNEL call *%gs:SYSINFO_OFFSET
# else
# define ENTER_KERNEL call *_dl_sysinfo
# endif
#else
# define ENTER_KERNEL int [=16=]x80
#endif
int 0x80
← 传统方式call *%gs:offsetof(tcb_head_t, sysinfo)
←%gs
指向TCB,所以这里通过指向TCB 中存储的vsyscall的指针间接跳转
call *_dl_sysinfo
← 这个通过全局变量间接跳转
因此,在 x86 中:
system call
↓
int 0x80 / call *%gs:0x10 / call *_dl_sysinfo
│ │
╰─┬──────────┼─────────╮
↓ ↓ ↓
(in vdso) int 0x80 / sysenter / syscall