Linux提供了ptrace系统函数,使得父进程得以控制和监视其它进程。当使用ptrace跟踪子进程后,所有发给子进程的信号(除了SIGKILL)外,都会被父进程截获,而此时子进程阻塞,并且被标记为TASK_TRACED。父进程收到信号后,可以查看和修改子进程的内核映像和寄存器。父进程完成所要做的工作之后可以选择让子进程继续执行还是终止。
在i386体系中,应用程序的系统调用基本过程:将系统调用号放在eax寄存器,其他的函数形参依次放在ebx,ecx,edx,esi,edi中,
注意: stdin =0为标注输入流。stdout=1标注输出流(默认为屏幕) stderr=2标注错误输出流(默认为屏幕)
write系统调用 write(2,“hello”,5)
汇编以后:
movl $4,%eax
movl $2,%ebx
movl $hello,%ecx
movl $5,%edx
int $0x80
其中4是write的系统调用号。在/usr/include/asm/unistd.h中声明和定义。
下面的这个程序是利用ptrace跟踪系统调用和查看寄存器的值。
1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 8 int main() 9 { 10 pid_t child; 11 long orig_eax,eax;
12 long params[3]; 13 int status; 14 int insyscall=0; 15 struct user_regs_struct regs; 16 child=fork(); 17 if(-1==child) printf("fork error/n"); 18 else if(0==child) 19 { 20 ptrace(PTRACE_TRACEME,child,NULL,NULL); 21 execl("/bin/pwd","pwd",NULL); 22 } 23 else 24 { 25 wait(&status); 26 if(WIFEXITED(status)) return 0; 27 orig_eax=ptrace(PTRACE_PEEKUSER,child,4*ORIG_EAX,NULL); 28 printf("process executed syscall id=%ld/n",orig_eax); 29 ptrace(PTRACE_SYSCALL,child,NULL,NULL); 30 while(1) 31 { 32 wait(&status); 33 if(WIFEXITED(status)) break; 34 orig_eax=ptrace(PTRACE_PEEKUSER,child,4*ORIG_EAX,NULL); 35 if(0==insyscall) 36 { 37 insyscall=1; 38 orig_eax=ptrace(PTRACE_PEEKUSER,child,4*ORIG_EAX,NULL); 39 printf("process executed syscall id=%ld/n",orig_eax); 40 ptrace(PTRACE_GETREGS,child,NULL,®s); 41 printf("write called with ebx=%ld,ecx=%ld,edx=%ld/n",regs.ebx,regs.ecx,regs.edx); 42 } 43 else 44 { 45 eax=ptrace(PTRACE_PEEKUSER,child,4*EAX,NULL); 46 printf("syscall with return value = %ld/n",eax); 47 insyscall=0; 48 } 49 50 ptrace(PTRACE_SYSCALL,child,NULL,NULL); 51 } 52 } 53 } 54
在头文件/usr/include/asm/usr.h中,有关于struct user_regs_struct regs的原型声明如下:
struct user_regs_struct {
long ebx, ecx, edx, esi, edi, ebp, eax;
unsigned short ds, __ds, es, __es;
unsigned short fs, __fs, gs, __gs;
long orig_eax, eip;
unsigned short cs, __cs;
long eflags, esp;
unsigned short ss, __ss;
};
在该函数中,insyscall作为系统调用的标志。父进程fork了一个子进程,在子进程中用PTRACE_TRACEME作为第一个参数调用了ptrace函数,告知内核跟踪自己。在执行完execl函数后,父进程使用wait函数等待来自内核的通知。一旦得到这个通知,就开始跟踪子进程。
WIFEXITED(status)这个宏用来指出子进程是否正常退出,如果是,则返回非零值。
PTARCE_PEEKUSR作为ptrace的第一个参数,其含义为:ptrace(PTARCE_PEEKUSR,child,4*ORIG_EAX,NULL)从由child子进程的用户区addr=4*ORIG_EAX处读出数据并返回该数据。addr必须是字对齐的。
PTRACE_SYSCALL的含义是让子进程重新执行,但在下一次子进程进入系统调用和退出系统调用时停止。
这个程序编译输出的部分结果如下:
/home/estate66/ptrace
process executed syscall id=11
process executed syscall id=122
write called with ebx=-1074571892,ecx=-1074571492,edx=11595732
syscall with return value = 0
process executed syscall id=45
write called with ebx=0,ecx=11595732,edx=-1074571776
syscall with return value = 166211584
process executed syscall id=33
write called with ebx=11579169,ecx=4,edx=11595732
syscall with return value = -2
process executed syscall id=5
write called with ebx=11582367,ecx=0,edx=0
syscall with return value = 3
process executed syscall id=197
write called with ebx=3,ecx=-1074574172,edx=11595732
syscall with return value = 0
process executed syscall id=90
write called with ebx=-1074574204,ecx=2,edx=11595732
syscall with return value = -1208287232
process executed syscall id=6
write called with ebx=3,ecx=2,edx=11595732
syscall with return value = 0
process executed syscall id=5
write called with ebx=-1208195482,ecx=0,edx=0
第一行是我的当前工作目录,即在终端输入pwd看到的结果,后面的是显示该命令的系统调用过程。我们在终端输入strace pwd得到如下:
execve("/bin/pwd", ["pwd"], [/* 35 vars */]) = 0
uname({sys="Linux", node="localhost.localdomain", ...}) = 0
brk(0) = 0x97ad000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=103838, ...}) = 0
old_mmap(NULL, 103838, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7f86000
close(3) = 0
open("/lib/tls/libc.so.6", O_RDONLY) = 3
read(3, "/177ELF/1/1/1/0/0/0/0/0/0/0/0/0/3/0/3/0/1/0/0/0/320n/262/0004/0/0/0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1529136, ...}) = 0
old_mmap(0xb12000, 1227964, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb12000
old_mmap(0xc38000, 16384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x125000) = 0xc38000
old_mmap(0xc3c000, 7356, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xc3c000
close(3) = 0
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7f85000
mprotect(0xc38000, 8192, PROT_READ) = 0
mprotect(0xb0e000, 4096, PROT_READ) = 0
set_thread_area({entry_number:-1 -> 6, base_addr:0xb7f85aa0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
munmap(0xb7f86000, 103838) = 0
通过在系统调用头文件中对比发现:/usr/include/asm/unistd.h部分内容摘录如下:
#define __NR_execve 11
#define __NR_access 33
#define __NR_brk 45
#define __NR_open 5
#define __NR_mmap 90
#define __NR_fstat64 197
#define __NR_uname 122
#define __NR_close 6
上面只列出了部分的内容。 通过比对我们可以发现,ptrace返回的系统调用号和strace跟踪的shell命令pwd执行过程中所进行的系统调用是一致的。
execl函数调用号是11,执行该函数后就开始新的进程,因此该函数没有返回值。uname函数调用号是122,返回值为0,brk函数调用号 45,实现向内核中动态的扩展进程数据段的大小。包括余下的函数调用都是很吻合的。