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