zoukankan      html  css  js  c++  java
  • 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 ptlsysdepsunixsysvlinuxi386sysdep-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.7sysdepsunixsysvlinuxi386sysdep.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.21archi386kernelptrace.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.8process.c

    internal_clone(tcp)

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

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

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

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

    4、系统调用的判断

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

    在strace的strace-4.5.8syscall.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寄存器,都是标准的系统系统调用号获得方法。

  • 相关阅读:
    ZUCC2129 The Tree of Power(树形DP)
    ZUCC Flower Name(01字典树)
    JDBC 测试01
    CF1355E Restorer Distance(三分)
    CF1352E Special Permutation(桶排序+前缀和)
    CF1350E Orac and Game of Life(BFS)
    CF1350D Orac and Medians(找规律)
    Python机器学习(五十七)SciPy 积分
    Python机器学习(五十六)SciPy fftpack(傅里叶变换)
    Python机器学习(五十五)SciPy 常量
  • 原文地址:https://www.cnblogs.com/tsecer/p/10485785.html
Copyright © 2011-2022 走看看