strace如何获得系统调用相关信息

一、问题的引出

对于很多的Linux下程序,我们有时候并不像详细的知道它执行的每一条指令或者,或者我们不想(或者不能)进行源代码级的调试,而只实现想大致看一下某个程序它执行了哪些核心的API调用,从而判断出程序执行的关键路径。此时使用strace是一个不错的选择,它可以不间断的执行完一个子程序,从而可以知道一个程序大致的运行框架。

二、实现方法

从strace的实现来看,它可以显示出系统调用的系统的调用号、系统调用的参数、系统调用的返回值等信息。这些我们就可以看一下他是如何获得这些信息的。

我们知道,在Linux系统中,不同的体系结构使用的系统调用方法可能并不相同。例如386通常是用的是eax表示系统调用号,然后使用ebx、ecx、edx、esi、edi、ebp共六个寄存器来保存系统调用的参数;而对于PowerPC来说,它通过r0保存系统调用号,而通过r3到r9保存系统调用的参数。所以strace如果想要知道这些信息,核心还是要知道这些寄存器组的信息。

对于i386来说,系统调用并没有一个专门的ABI规定,因为通常的386的处理器都是通过堆栈来传递寄存器的,而显然用户态和内核态不能通过这种方式来进行寄存器传递,所以这个规定不存在和用户态程序兼容的问题,只要内核和用户态规定好就可以了。虽然在用户态程序使用寄存器的顺序是eax、ecx、edx,但是系统调用为了一个统一的实现方法,它并没有也不需要遵守这个ABI规定,所以在这个386的系统调用中

1、系统调用寄存器使用

glibc-2.7\nptl\sysdeps\unix\sysv\linux\i386\sysdep-cancel.h
# undef PSEUDO
# define PSEUDO(name, syscall_name, args)          \
  .text;              \
  ENTRY (name)              \
    cmpl $0, %gs:MULTIPLE_THREADS_OFFSET;          \
    jne L(pseudo_cancel);            \
  .type __##syscall_name##_nocancel,@function;          \
  .globl __##syscall_name##_nocancel;           \
  __##syscall_name##_nocancel:            \
    DO_CALL (syscall_name, args);           \
    cmpl $-4095, %eax;             \
    jae SYSCALL_ERROR_LABEL;            \
    ret;              \
  .size __##syscall_name##_nocancel,.-__##syscall_name##_nocancel;       \
  L(pseudo_cancel):             \
    CENABLE              \
    SAVE_OLDTYPE_##args             \
    PUSHCARGS_##args             \
    DOCARGS_##args             \
    movl $SYS_ify (syscall_name), %eax;           \
    ENTER_KERNEL;             \
    POPCARGS_##args;             \
    POPSTATE_##args             \
    cmpl $-4095, %eax;             \
    jae SYSCALL_ERROR_LABEL;            \
  L(pseudo_end):

glibc-2.7\sysdeps\unix\sysv\linux\i386\sysdep.h
#undef DO_CALL
#define DO_CALL(syscall_name, args)                 \
    PUSHARGS_##args             \ 这里的pushargs只是保存调用者需要保存的寄存器,也就是eax、ecx和edx。这里用户态的API使用的寄存器和内核态顺序不同。用户态调用者需要保存的是EAX、ECX和EDX,剩余的EBX、ESI、EDI这些寄存器都是由被调用者保存的。这一点和系统调用使用的寄存器号不同
    DOARGS_##args             这里才是真正的向系统的调用构造参数的过程,也就是这里操作的寄存器才是内核使用的寄存器,所以,我们看系统调用号中寄存器使用的顺序应该从这个DOARGS系列中找这个顺序
    movl $SYS_ify (syscall_name), %eax;           \
    ENTER_KERNEL             \
    POPARGS_##args


#define PUSHARGS_0 /* No arguments to push.  */
#define DOARGS_0 /* No arguments to frob.  */
#define POPARGS_0 /* No arguments to pop.  */
#define _PUSHARGS_0 /* No arguments to push.  */
#define _DOARGS_0(n) /* No arguments to frob.  */
#define _POPARGS_0 /* No arguments to pop.  */

#define PUSHARGS_1 movl %ebx, %edx; L(SAVEBX1): PUSHARGS_0将ebx的值保存在挥发寄存器EDX中
#define DOARGS_1 _DOARGS_1 (4)
#define POPARGS_1 POPARGS_0; movl %edx, %ebx; L(RESTBX1):
#define _PUSHARGS_1 pushl %ebx; cfi_adjust_cfa_offset (4); \
   cfi_rel_offset (ebx, 0); L(PUSHBX1): _PUSHARGS_0这里保存了ebx的原始值,当参数大于2的时候
#define _DOARGS_1(n) movl n(%esp), %ebx; _DOARGS_0(n-4)
#define _POPARGS_1 _POPARGS_0; popl %ebx; cfi_adjust_cfa_offset (-4); \
   cfi_restore (ebx); L(POPBX1):

#define PUSHARGS_2 PUSHARGS_1
#define DOARGS_2 _DOARGS_2 (8)
#define POPARGS_2 POPARGS_1
#define _PUSHARGS_2 _PUSHARGS_1
#define _DOARGS_2(n) movl n(%esp), %ecx; _DOARGS_1 (n-4)
#define _POPARGS_2 _POPARGS_1

#define PUSHARGS_3 _PUSHARGS_2
#define DOARGS_3 _DOARGS_3 (16)
#define POPARGS_3 _POPARGS_3
#define _PUSHARGS_3 _PUSHARGS_2
#define _DOARGS_3(n) movl n(%esp), %edx; _DOARGS_2 (n-4)
#define _POPARGS_3 _POPARGS_2

#define PUSHARGS_4 _PUSHARGS_4
#define DOARGS_4 _DOARGS_4 (24)
#define POPARGS_4 _POPARGS_4
#define _PUSHARGS_4 pushl %esi; cfi_adjust_cfa_offset (4); \直到使用到第三个寄存器的时候,才开始真正的保存寄存器,因为ECX、EDX都是调用者保存的寄存器
   cfi_rel_offset (esi, 0); L(PUSHSI1): _PUSHARGS_3
#define _DOARGS_4(n) movl n(%esp), %esi; _DOARGS_3 (n-4)
#define _POPARGS_4 _POPARGS_3; popl %esi; cfi_adjust_cfa_offset (-4); \
   cfi_restore (esi); L(POPSI1):

#define PUSHARGS_5 _PUSHARGS_5
#define DOARGS_5 _DOARGS_5 (32)
#define POPARGS_5 _POPARGS_5
#define _PUSHARGS_5 pushl %edi; cfi_adjust_cfa_offset (4); \
   cfi_rel_offset (edi, 0); L(PUSHDI1): _PUSHARGS_4
#define _DOARGS_5(n) movl n(%esp), %edi; _DOARGS_4 (n-4)
#define _POPARGS_5 _POPARGS_4; popl %edi; cfi_adjust_cfa_offset (-4); \
   cfi_restore (edi); L(POPDI1):

#define PUSHARGS_6 _PUSHARGS_6
#define DOARGS_6 _DOARGS_6 (40)
#define POPARGS_6 _POPARGS_6
#define _PUSHARGS_6 pushl %ebp; cfi_adjust_cfa_offset (4); \
   cfi_rel_offset (ebp, 0); L(PUSHBP1): _PUSHARGS_5
#define _DOARGS_6(n) movl n(%esp), %ebp; _DOARGS_5 (n-4)
#define _POPARGS_6 _POPARGS_5; popl %ebp; cfi_adjust_cfa_offset (-4); \
   cfi_restore (ebp); L(POPBP1):

2、通知内核监控系统调用

在linux-2.6.21\arch\i386\kernel\ptrace.c中

long arch_ptrace(struct task_struct *child, long request, long addr, long data)
 case PTRACE_SYSEMU: /* continue and stop at next syscall, which will not be executed */
 case PTRACE_SYSCALL: /* continue and stop at next (return from) syscall */
 case PTRACE_CONT: /* restart after signal. */
  ret = -EIO;
  if (!valid_signal(data))
   break;
  if (request == PTRACE_SYSEMU) {
   set_tsk_thread_flag(child, TIF_SYSCALL_EMU);
   clear_tsk_thread_flag(child, TIF_SYSCALL_TRACE);
  } else if (request == PTRACE_SYSCALL) {
   set_tsk_thread_flag(child, TIF_SYSCALL_TRACE);
   clear_tsk_thread_flag(child, TIF_SYSCALL_EMU);这两个的区别在于一个会在系统调用结束的时候通知调试器,另一个不会,但是它们都会在系统调用进入的时候通知调试器
  } else {
   clear_tsk_thread_flag(child, TIF_SYSCALL_EMU);
   clear_tsk_thread_flag(child, TIF_SYSCALL_TRACE);
  }
  child->exit_code = data;
  /* make sure the single step bit is not set. */
  clear_singlestep(child);
  wake_up_process(child);
  ret = 0;
  break;

3、strace设置系统调用跟踪标志及判断

strace-4.5.8\process.c

internal_clone(tcp)

 if (ptrace(PTRACE_SYSCALL, pid, (char *) 1, 0) < 0) {

从而在系统调用开始和结束的时候收到通知。当strace收到通知之后,它就可以通过系统调用获得这个被调试线程的寄存器组,也就是信号处理函数中的pt_regs结构。

在内核的linux-2.6.21\arch\i386\kernel\entry.S文件中,大家可以搜索这两个标志,它们分别是在系统调用执行之前和之后执行的一个判断,然后如果标志置位,那么执行do_syscall_trace,这个函数位于linux-2.6.21\arch\i386\kernel\ptrace.c中,它通知调试器系统调用的发生

ptrace_notify(SIGTRAP | ((current->ptrace & PT_TRACESYSGOOD) ? 0x80:0));

4、系统调用的判断

在pt_regs结构中中,我们可以知道系统中的所有的寄存器组,而内核和用户态的寄存器传递同样是通过这种方式来实现的。正如上面说所说,其中的EAX保存系统调用号,而EBX、ECX等依次保存系统调用的参数,所以这样我们就可以判断出系统调用的各个参数。然后就是系统调用的返回值,这个需要在系统调用返回的时候从eax寄存器中获得,所以也不成问题。在多线程的情况下,我想我们需要记录系统调用号还有线程号,从而和系统调用匹配。

在strace的strace-4.5.8\syscall.c中:

get_scno(tcp)

#elif defined (POWERPC)
 if (upeek(pid, sizeof(unsigned long)*PT_R0, &scno) < 0)
  return -1;
 if (!(tcp->flags & TCB_INSYSCALL)) {
  /* Check if we return from execve. */
  if (scno == 0 && (tcp->flags & TCB_WAITEXECVE)) {
   tcp->flags &= ~TCB_WAITEXECVE;
   return 0;
  }
 }
#elif defined (I386)
 if (upeek(pid, 4*ORIG_EAX, &scno) < 0)
  return -1;

可以看到,其中获得系统调用号的时候根据各个体系结构的不同而不同,其中PowerPC使用r0寄存器,而386则使用ORIG_EAX寄存器,都是标准的系统系统调用号获得方法。

posted on 2019-03-06 20:31  tsecer  阅读(545)  评论(0编辑  收藏  举报

导航