【问题标题】:Confusion with system call与系统调用混淆
【发布时间】:2016-04-16 22:23:56
【问题描述】:

我试图了解如何在 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 $0x8,%esp
0x8000136 : movl $0x80027b8,0xfffffff8(%ebp)
0x800013d : movl $0x0,0xfffffffc(%ebp)
0x8000144 : pushl $0x0
0x8000146 : leal 0xfffffff8(%ebp),%eax
0x8000149 : pushl %eax
0x800014a : movl 0xfffffff8(%ebp),%eax
0x800014d : pushl %eax
0x800014e : call 0x80002bc <__execve> 
0x8000153 : addl $0xc,%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 $0xb,%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 $0x80
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 $0xffffffff,%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    $0xfffffff0,%esp
 8048e2a:       83 ec 20                sub    $0x20,%esp
        char *name[2];
        name[0] = "/bin/sh";
 8048e2d:       c7 44 24 18 e8 de 0b    movl   $0x80bdee8,0x18(%esp)
 8048e34:       08
        name[1] = NULL;
 8048e35:       c7 44 24 1c 00 00 00    movl   $0x0,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   $0x0,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    $0xb,%eax
 806c282:       ff 15 f0 99 0e 08       call   *0x80e99f0
 806c288:       3d 00 f0 ff ff          cmp    $0xfffff000,%eax
 806c28d:       77 02                   ja     806c291 <__execve+0x21>
 806c28f:       5b                      pop    %ebx
 806c290:       c3                      ret
 806c291:       c7 c2 e8 ff ff ff       mov    $0xffffffe8,%edx
 806c297:       f7 d8                   neg    %eax
 806c299:       65 89 02                mov    %eax,%gs:(%edx)
 806c29c:       83 c8 ff                or     $0xffffffff,%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 看到这条指令。但我找不到相同的指令。代替它,我找到了

调用 *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 $0x80 指令。

【问题讨论】:

  • 您必须将 80e99f0 处的数据解释为 data,而不是说明。你从中得到70 ed 06 08,即内存地址806ed70
  • 你为什么定义void main()而不是正确的int main(void)
  • @KeithThompson 我没想到。在原始文章中,仅限void main()。这对我的问题有影响吗?
  • 这可能与您的问题无关。但是void main()是不可移植的,没有理由使用它而不是正确便携的int main(void)

标签: c assembly linker exec system-calls


【解决方案1】:

传统上,Linux 使用中断 0x80 来调用系统调用。自 PentiumPro 以来,有另一种调用系统调用的方法:使用 SYSENTER 指令(AMD 也有自己的 SYSCALL 指令)。这是调用系统调用的更有效方式。

选择使用哪种系统调用机制

linux 内核和 glibc 有一种机制可以在调用系统调用的不同方式之间进行选择。

内核为每个进程建立一个虚拟共享库,称为VDSO(virtual dynamic shared object),可以在cat /proc/&lt;pid&gt;/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 0x80sysentersyscall的实现,内核会选择合适的。

为了让用户空间知道有一个 vdso 以及它所在的位置,内核在辅助向量中设置了 AT_SYSINFOAT_SYSINFO_EHDR 条目(auxvmain() 的第四个参数,在 @987654332 之后@,用于将一些信息从内核传递给新启动的进程)。 AT_SYSINFO_EHDR指向vdso的ELF头,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(线程控制块)头部的一个字段中:

glibc-2.16.0/nptl/sysdeps/i386/tls.h

_head->sysinfo = GLRO(dl_sysinfo)

如果内核较旧且不提供 vdso,则 glibc 为 _dl_sysinfo 提供默认实现:

.hidden _dl_sysinfo_int80:
int $0x80
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 $0x80.  */
#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 $0x80
#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

【讨论】:

    【解决方案2】:

    尝试使用objdump -DSobjdump -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    $0x80
     806ea12:       c3                      ret    
    

    它调用软件中断0x80(执行系统调用)然后返回给调用者。

    因此,调用 *0x80ea310 实际上是在调用 0x806ea10(取消引用指针)

    【讨论】:

    • 我根据您的建议编辑了这个问题。我还是没有任何意义..
    • 谢谢。我明白你想说的话。为什么 gcc 做这一切的并发症。有任何想法吗? -D 标志对 objdump 命令究竟做了什么。
    • 这是标准链接过程,取消引用链接表中的地址,该地址填充在可执行文件 linkage 上,而不是在编译目标文件时。
    • 如果我在填写适当的寄存器后立即致电int $0x80。它还会起作用吗?我觉得应该
    • 您确实可以将806bf82 的内容更改为cd 80 90 90 90 90 (int $0x80 nop nop nop nop),它仍然可以工作
    猜你喜欢
    • 2011-06-10
    • 1970-01-01
    • 2012-01-20
    • 2021-07-30
    • 1970-01-01
    • 2012-03-04
    • 1970-01-01
    • 2019-10-01
    • 1970-01-01
    相关资源
    最近更新 更多