Linux系统调用与ptrace分析
概述
1
.
Linux
的系统结构
在Linux系统结构中,最核心的是计算机硬件,它提供对Linux软件的支持,靠近硬件的内层是Linux内核程序(即操作系统)。内核直接和硬件打交道是程序和硬件之间的接口或界面。它对一切外层程序提供公共服务,把外部程序同硬件隔离开。内核程序大致可分为文件系统管理,进程管理,内存管理等几部分。进程管理又分为低级进程管理和高级进程管理。低级进程管理主要包括:进程调度分配,控制占用处理器的程序和基本的进程通信。高级进程管理主要包括:进程的创建,终止,进程间通信,进程在内存和外存之间的转储,信号机构和进程间跟踪控制等。内核程序的外层是实用程序,内核提供对实用程序的支持,两层之间的界面是系统调用。内核外的实用程序通过系统调用来和内核打交道。实现的过程是通过一种特殊的指令(陷入指令)进入内核,然后转入相应的系统调用处理程序。这也是本文将主要讨论的问题。
2
.
80386
体系结构
80386的体系结构承认两类事件。
1.异常(exceptions)
2.中断(interrupts)
他们两都会引起“上下文转换”同时建立一个过程或任务,中断可以随时随地发生(包括在执行程序时)所以用来响应硬件信号。而异常则由指令内部错误引起。
每一个异常或中断都有一个唯一的标识符,在linux中被称为向量。
指令内部异常和NMI(不可屏蔽中断)的中断向量的范围从0—31。32-255的任何向量都可以用做
1.可屏蔽中断
2.编程(调试)异常
至于可屏蔽中断则取决于该系统的硬件配置。外部中断控制器在中断响应周期把中断向量放到总线上。
3
.
Linux
系统调用流程概述
Linux系统调用的流程非常简单,它由0x80号中断进入系统调用入口,通过使用系统调用表保存系统调用服务函数的入口地址来实现,本文首先分析一般Linux系统调用的流程,然后再分析Linux系统调用sys_ptrace().
一. Linux系统调用的流程分析
1.1 设定0x80号中断
系统启动后,先进行初始化,其中一部分重要的工作在start_kernel()函数(main.c中定义)中进行,在该函数中先做必要的初始化工作(setup_arch()与paging_init()),各种trap入口就在该函数中通过调用trap_init()(traps.c)被设置,其中与系统调用有关的是:set_system_gate(0x80,&system_call);
“set_system_gate()”是一宏,它在“system.h”中被定义:
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
……中断描述表结构(head.s)
其中“_set_gate()”也是在该文件中定义的宏:
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ __volatile__ ("movw %%dx,%%ax\n\t" \
"movw %2,%%dx\n\t" \
"movl %%eax,%0\n\t" \
"movl %%edx,%1" \
:"=m" (*((long *) (gate_addr))), \
"=m" (*(1+(long *) (gate_addr))) \
:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"d" ((char *) (addr)),"a" (KERNEL_CS << 16) \
:"ax","dx")
调用该宏,将使addr地址值置入gate_addr中的地址值所指向的内存单元中,以上过程,使中断向量描述表中的第128项(16进制第80项)保存了0x80号中断的中断服务程序,即system_call。
而中断描述表结构则定义在head.s中.
1.2 系统调用的数据结构
系统调用所用到的数据结构也非常简单,主要有两种,系统调用表和寄存器帧结构。
“entry.S”中定义了系统调用表,该表保存了Linux基于Intel x86系列体系结构的计算机的166个系统调用入口地址(其中3个保留,Linux开辟的系统调用表可容纳256项),其中每项都被说明成 long型。
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_setup) /* 0 */
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
…………
.long SYMBOL_NAME(sys_setitimer) /* 104 */
…………
.long SYMBOL_NAME(sys_select) /* 142*/
…………
.long 0,0
.long SYMBOL_NAME(sys_vm86) /* 166 */
.space (NR_syscalls-166)*4
NR_syscalls是在“sys.h”文件中定义的宏,表示x86微机上最多可容纳的系统调用个数。
#define NR_syscalls 256
在文件“ptrace.h”中定义了一种寄存器帧结构:pt_regs,该帧结构与系统调用时压入堆栈的寄存器的顺序保持一致,用来在系统调用时传递参数。
struct pt_regs {
long ebx;
long ecx;
long edx;
long esi;
long edi;
long ebp;
long eax;
int xds;
int xes;
long orig_eax;
long eip;
int xcs;
long eflags;
long esp;
int xss;
};
这样,如果pt_regs结构体的首地址(设为regs)是该帧的帧顶(栈顶),在entry.s中压入堆栈的一帧将和pt_regs结构体中的字段对应。
1.3 系统调用的入口
在头文件“unistd.h”中,定义了一系列的与系统调用有关的宏,包括系统调用序号,如:
#define __NR_exit
1
还定义了设置系统调用入口的宏,_syscallX(type,name, type1,arg1,type2,arg2……),其中X表示系统调用的参数个数,Linux定义的各种系统调用的参数个数不超过5个,因此,在该文件中,共定义了6个宏,“_syscallX”宏,分别对应X个参数,下面以X=2即两个参数为例,解释该宏:
#define _syscall2(type,name,type1,arg1,type2,arg2) \
type name(type1 arg1,type2 arg2) \
{ \
long __res; \
__asm__ volatile ("
int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
该宏的第一个参数是一类类型参数,它指明系统调用返回值的类型,第二个参数指明系统调用的名称。参数列表中若还有参数,则第2i个参数是系统调用函数的第i个参数的类型,第2i+1个参数是系统调用函数的第i个参数
该宏的主体部分是一内联汇编,在内联汇编中只有一条扩展汇编指令,即“int $0x80
”,该语句两个冒号后的语句设置输入和输出寄存器。
第一个冒号后的语句指明返回参数(即__res)使用eax寄存器。
第二个冒号后面指定接受输入的寄存器,“"0" (__NR_##name),”将参数name与“__NR_”串接起来,形成的标志符存入eax寄存器,作为区别系统调用类型的唯一参数,例如设置name为“ptrace”,那么,gcc编译器将把“__NR_”与之串接,被视为标志符“__NR_ptrace”,由于在文件“include/asm-i386/unistd.h”中已定义其为26,那么,传给eax的值将为26。
后面的语句将参数arg1,arg2分别传给寄存器ebx和ecx,在“_syscallX”宏中,有如下约定:
arg1值存入寄存器ebx;
arg2值存入寄存器ecx;
arg3值存入寄存器edx;
arg4值存入寄存器esi;
arg5值存入寄存器edi;
在该宏的最后,判断返回值“__res”是否合法,若为负数,表明在系统调用中出错,将其绝对值作为出错号赋给全局变量“errno”,并返回-1,否则返回“__res”。
该宏的唯一一条汇编指令“int $0x80
”使程序流程转入“system_call”。
1.4 转入system_call
system_call是在汇编语言文件“entry.S”中定义的一入口,在Linux中,所有的系统调用都是通过中断“int &0x80
”语句来实现的,因而,system_call是所有系统调用的入口。下面解释关于它的一些重要指令,以清晰它的流程:
1. 首先,pushl %eax,保存原来的eax寄存器,然后调用宏“SAVE_ALL”将现有通用寄存器保存,寄存器的保存不但避免影响原来的寄存器数据,而且提供了一种传递参数的方法。正如在2
.2节所指出的,这样保存的一帧寄存器,与该过程所要传递的pt_regs结构相对应。在该宏中,还使ds和es指向内核的数据段,使fs指向用户的数据段。
#define SAVE_ALL \
cld; \
push %gs; \
push %fs; \
push %es; \
push %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(KERNEL_DS),%edx; \
mov %dx,%ds; \
mov %dx,%es; \
movl $(USER_DS),%edx; \
mov %dx,%fs;
2. 语句“cmpl $(NR_syscalls),%eax”比较NR_syscalls与eax的大小,如果eax大于或等于NR_syscalls,表明指定的系统调用函数错误,“jae ret_from_sys_call”使系统调用直接返回。
3 . 流程进入ret_from_sys_call,该过程内处理一些系统调用返回前应该处理的事情,如检
测bottom half缓冲区,判断CPU是否需要重新调度等.
先注意全局变量intr_count,它虽然不是信号量,但也部分的具有了信号量的作用,表
示已有进程进入bottom_half,它在系统处理bottom_half时增1,则其为非零。
语句“cmpl $0,SYMBOL_NAME(intr_count)”就是进行上述判断,若非零,处理
bottom half 缓冲区。(“jne handle_bottom_half”)。
下面两条语句判断CPU是否需要重新调度:
cmpl $0,SYMBOL_NAME(need_resched)
jne reschedule
其中,need_resched是一全程量,它置位,表示CPU需要重新调度,程序转向过程reschedule,进而,转向schedule()函数,在该函数中,将其重新置零。
注意,handle_bottom_half和reschedule并不是必需的,只不过在系统运行过程中,随时都有可能出现需要处理bottom half缓冲区或重新调度CPU,放在系统调用返回前,有利于它们被及时处理。但这也说明,Linux 不是一个硬实时的操作系统,它可能会产生延误。
4. 如果eax小于NR_syscalls,system_call过程接下去执行语句:
movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax
该语句以
sys_call_table为基地址,eax寄存器中的内容(即系统调用的序号)乘以4为偏移量(因为long型为4字节),即得到所需调用的系统调用函数的入口地址,将其存入寄存器eax。
testl %eax,%eax
接着判断寄存器eax值是否为0,若是,表明出错,直接返回,je ret_from_sys_call。
#ifdef __SMP__
GET_PROCESSOR_OFFSET(%edx)
movl SYMBOL_NAME(current_set)(,%edx),%ebx
#else
movl SYMBOL_NAME(current_set),%ebx
以上语句首先判断是否为多处理器结构,若是,得到当前处理器的偏移值,当前的进
程控制块的指针为current_set[smp_processor_id()],否则,current_set[0]即为当前进程
控制块的指针,这样,ebx寄存器指向当前进程。
movl %db6,%edx
movl %edx,dbgreg6(%ebx)
以上两条语句用来保存当前调试信息,在进程控制块task_struct结构中,第8项是
debugreg[8],用来指示硬件调试信息。在entry.S中,定义了一系列宏作为偏移量,用
来得到当前进程的信息,它们是:
state
= 0
counter
= 4
priority
= 8
signal
= 12
blocked
= 16
flags
= 20
dbgreg6
= 52
dbgreg7
= 56
exec_domain
= 60
这样,在当前进程的task_struct结构中,保存了当前的调试信息。
5.语句“testb $0x20,flags(%ebx)”检测当前进程是否正跟踪系统调用,如果不是的话,直
接调用所选系统调用函数,相关语句为:
call *%eax
如判断当前进程正处于跟踪系统调用状态(current->flags&PF_TRACESYS==0),调用函
数体“syscall_trace()”(在ptrace.c中定义),使当前进程状态转为TASK_STOPPED,即
转入睡眠状态。
asmlinkage void syscall_trace(void)
{
if ((current->flags & (PF_PTRACED|PF_TRACESYS))
!= (PF_PTRACED|PF_TRACESYS))
return;
current->exit_code = SIGTRAP;
current->state = TASK_STOPPED;
notify_parent(current, SIGCHLD);
schedule();
if (current->exit_code) {
send_sig(current->exit_code, current, 1);
current->exit_code = 0;
}
}
然后从压入寄存器的堆栈中重新找到原来的eax值,再重新设置系统调用函数的偏移量,调用实现相应系统调用的函数,语句为:
call SYMBOL_NAME(syscall_trace)
movl ORIG_EAX(%esp),%eax
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
做完以上工作后,将返回值保存在eax寄存器中。
movl %eax,EAX(%esp)
最后进入ret_from_sys_call,作一些处理工作。
二. 系统调用实例分析:ptrace系统调用
2.1 跟踪及ptrace()简述
Linux提供的跟踪功能,即父进程对子进程的跟踪,使得父进程可以对自己的子进程进行监督与控制。具体包括读写子进程用户空间的程序,数据,或user结构中的变量,向它们发送软中断,以及命令它们自我终止等。系统提供了两种系统调用waitpid()和ptrace(),一实现跟踪功能。这种父子进程间的关联可由子进程发ptrace(0)请求,也可由父进程发attach请求来实现.
本文第二部分先利用第一部分的知识阐述ptrace系统调用的设置,然后再简单解释ptrace系统调用的流程。
2.2 预备知识
1. 标识”错误“的宏,定义在linux\include\asm_i386\errno.h
ptrace()中涉及下面几个宏:
#define
EPERM 1 /* 操作不被允许 */
#define
ESRCH 3 /* 不存在这样的进程 */
#define
EIO 5 /* I/O错误 */
2. 下面是一些用来帮助标识具体跟踪命令或状态的宏:
(定义在linux\include\linux\ptrace.h中)
#define PTRACE_TRACEME
0
/* 说明是子进程调用该程序,请求父进程跟踪 */
#define PTRACE_PEEKTEXT
1
/* 在指定的位置读一个字 */
#define PTRACE_PEEKDATA
2
#define PTRACE_PEEKUSR
3
/* 在USER结构指定的位置读一个字 */
#define PTRACE_POKETEXT
4
/* 在指定的位置写一个字 */
#define PTRACE_POKEDATA
5
#define PTRACE_POKEUSR
6
/* 在USER结构指定的位置写一个字 */
#define PTRACE_CONT
7
/*
子进程接受信号后,RESTART */
#define PTRACE_KILL
8
/* 终止子进程 */
#define PTRACE_SINGLESTEP
9
/* 置TRAP标志 */
#define PTRACE_ATTACH
0x10
#define PTRACE_DETACH
0x11
#define PTRACE_SYSCALL 24
(定义在linux\include\asm_i386\ptrace.h中)
#define PTRACE_GETREGS
12
/* 在子进程中获得所有的GP寄存器内容 */
#define PTRACE_SETREGS
13
/* 设置子进程中所有的的GP寄存器 */
#define PTRACE_GETFPREGS
14
/* Get the child FPU state. */
#define PTRACE_SETFPREGS
15
/* Set the child FPU state. */
3. 其他(待解释)
#define PAGE_SHIFT
12
#define PAGE_SIZE
(1UL << PAGE_SHIFT)
#define PAGE_MASK
(~(PAGE_SIZE-1))
#define VM_GROWSDOWN
0x0100
#define _NSIG
64
2.3 ptrace系统调用的设置
系统调用的设置均在文件“include/asm-i386/unistd.h”中进行.ptrace系统调用的对应函数带4个参数,因此该系统调用的设置应使用的宏应为:
_syscall4(int,ptrace,long,request,long,pid,long,addr,long,data)
(文件“include/asm-i386/unistd.h”中),
#define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \
type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
"d" ((long)(arg3)),"S" ((long)(arg4))); \
__syscall_return(type,__res); \
}
这样,在调用ptrace时,系统将调用宏指令_syscall4,进而,调用0x80号中断,寄存器eax中的值为__NR_ptrace, request值存入寄存器ebx;pid值存入寄存器ecx;addr值存入寄存器edx;data值存入寄存器esi;调用中断“int $0x80
”以后,在汇编过程“system_call”中,将通过eax中的值__NR_ptrace(即26)与4的乘积作为相对于系统调用表(sys_call_table)偏移量,找到入口:
.long SYMBOL_NAME(sys_ptrace)
于是,系统流程转向函数sys_ptrace()。sys_ptrace()是在文件“arch/i386/kernel/ptrace.c”中定义的,程序如下:
下面一节介绍该函数的大概流程。
2.4 ptrace()的流程
ptrace()函数一开始就“作最坏的打算”,将可能返回的值 ret初始值置为-EPERM,它意味着,操作被拒绝。然后,才进入主流程。
首先,函数根据request参数判断是否是子进程发出的要求,如果request为1,则表示是子进程要求父进程进行跟踪,再判断是否已被跟踪,如是则返回,此步可用指向当前进程的current的flags字段与PF_PTRACED进行求与,如结果非零表示子进程已被跟踪。如未被跟踪,则将PF_PTRACED与flags求并,ret置为0,最后返回。
if (request ==PTRACE_TRACEME) {
if (current->flags & PF_PTRACED)
goto out;
/* set the ptrace bit in the process flags. */
current->flags |= PF_PTRACED;
ret = 0;
goto out;
}
如request不为1,则表示父进程发给子进程的命令类型。
接下去如果子进程的pid等于1(1号进程是初始进程,是除0号进程外所有进程的祖先),则带出错信息返回。然后函数将从tasklist中找到子进程,先做好最坏的准备,即找不到pid所表示的子进程,将ret置为
-ESRCH,接着调用宏read_lock和read_unlock(定义在spinlock.h中)
#define read_lock(lock)
do { } while(0)
#define read_unlock(lock)
do { } while(0)
在它们中间调用find_task_by_pid(),根据进程的id号从hash表中找到指向子进程的指针,赋给child指针。如该指针为null,则带前设出错信息返回。
下面将判断是否是当前进程发出命令要求跟踪指定的进程
,为此函数有做好最坏的准备,将ret置为-EPERM。
接着函数将准备处理attach的请求
,此请求表示当前进程想跟踪指定的进程,以便后面各种命令的实现.若找到的进程就是当前进程,带设好的出错信息返回.而要想绑定一个进程,必须向它发送信号(signal),又因为除了内核和超级用户
,不是每一个进程都能向别的进程发送信号,而且一般的进程只能向同用户和同组的进程发送信号,所以如果当前进程不满足这些条件,或者子进程不能进行进程交换,并且当前进程所属用户也不是超级用户的话,那么该操作也将拒绝而返回.如果该进程正在被当前进程所跟踪,操作也将失败.这一系列的判断有下面的语句完成:
if ((!child->dumpable ||
(current->uid != child->euid) ||
(current->uid != child->suid) ||
(current->uid != child->uid) ||
(current->gid != child->egid) ||
(current->gid != child->sgid) ||
(current->gid != child->gid)) && !suser())
goto out;
if (child->flags & PF_PTRACED)
goto out;
接着将current的flags字段与PF_PTRACED求并
.如果当前进程是child进程的父进程
,则直接调用send_sig()函数
,向它发送SIGSTOP信号
,使子进程进入TASK_STOPPED状态,等待父进程的命令.父进程则返回准备下一次调用
.
send_sig(SIGSTOP, child, 1);
如果当前进程不是child的父进程
,而是属于超级用户,则需要修改进程的p_pptr字段
,以使当前进程为父进程,为对共享数据进行读写,须先保存必要的信息到flags中然后关中断
,以保证数据的一致性和完整性,修改完数据后从flags中恢复信息
.此工作有下面几个宏完成,分别定义在spin_lock.h,system.h中
.
#define save_flags(x) \
__asm__ __volatile__("pushfl ; popl %0":"=g" (x): /* no input */ :"memory")
#define restore_flags(x) \
__asm__ __volatile__("pushl %0 ; popfl": /* no output */ :"g" (x):"memory")
#define cli() __asm__ __volatile__ ("cli": : :"memory")
#define write_lock_irqsave(lock, flags) \
do { save_flags(flags); cli(); } while (0)
#define write_unlock_irqrestore(lock, flags) \
restore_flags(flags)
在REMOVE_LINKS和SET_LINKS两个宏中间
,将child的p_pptr指针指向当前进程
.然后就可以像父进程一样直接向child发信号了
.其中,REMOVE_LINKS和 SET_LINKS 定义在sched.h中,
REMOVE_LINKS宏把进程从上相链表中删除,并把连在其上的父兄进程指针移开。而SET_LINKS宏则重新设置该进程的相关指针。
如果收到的请求不是PTRACE_ATTACH,表示子进程已被当前进程绑定
,处于TASK_STOPPED状态,等待父进程接下来的命令,或者父进程将KILL子进程
.接着函数有将对子进程作一些判断,于是先将ret置为
-ESRCH,做好出错的准备.首先,如果该进程未被跟踪
则出错返回
,表明一个进程不能对任意的进程跟踪,子进程须先做请求,或者父进程先要提出跟踪的要求.如果子进程不处于TASK_STOPPED状态
,父进程也未发kill命令
,出错返回.如果child的父进程不是当前进程
,表明此跟踪乃子进程首先申请,但不应由当前进程处理,返回.
下面进入各种命令的具体实现部分
,由一个switch...case...组成
.共分下面几种情况处理:
1.PTRACE_PEEKTEXT;
2.PTRACE_PEEKDATA;
3.PTRACE_PEEKUSR;
4.PTRACE_POKETEXT:
5.PTRACE_POKEDATA:
6.PTRACE_POKEUSR:
7.PTRACE_SYSCALL:
8.PTRACE_CONT:
9.PTRACE_KILL:
10.PTRACE_SINGLESTEP:
11.PTRACE_DETACH:
12.PTRACE_GETREGS:
13.PTRACE_SETREGS:
14.PTRACE_GETFPREGS:
15.PTRACE_SETFPREGS:
16.default:
由于笔者时间有限
,将试着做一些简单的分析.
如果收到的要求是
PTRACE_PEEKDATA,即要求从指定的地址读数据。首先定义一个无符号长整型变量tmp,用来存放中间结果。然后如下调用函数:
ret = read_long(child, addr, &tmp);
该函数也定义在ptrace.h中,该函数首先调用find_extend_vma函数(ptrace.h),
这里需要讲一下vm_area_struct这个结构。当一个进程映像被执行的时候,可执行的进程映像内容须被引入到进程的虚地址空间去,任何与进程映像相联系的函数库也一样。可执行文件实际上并没有引入内存,相反,它仅仅只是被关联到进程的虚地址空间。这样,作为正运行的应用程序访问的程序的一部分,进程映像就通过可执行的映像引入到内存中。这个过程就叫内存映射。每一个进程的虚存由mm_struct数据结构代表,它包括了进程的映像的信息和一个指向一系列vm_area_struct结构的指针。每一个vm_area_struct结构描述一个虚存区的启始地址和结束地址,进程进入内存的权限,和一些相关的操作。
在find_extend_vma函数中调用find_vma函数,从进程的vm_area_struct中找到第一个满足addr<vm_end的虚存区,首先检查cache,然后再在双项链表中找到相应的虚存区.如果找不到则返回 NULL,找到,则返回一个满足条件的vm_area_struct指针。如果该指针为null,
返回null,如果它的vm_start<addr,则返回这个指针,下面的代码待解释:
if (!(vma->vm_flags & VM_GROWSDOWN))
return NULL;
if (vma->vm_end - addr > tsk->rlim[RLIMIT_STACK].rlim_cur)
return NULL;
如果都不是,则将vm_start和vm_offset重新设置,返回。如果返回值为null,则返回 -EIO给ret,这个例程将通过页表从进程区获得一个长整型数。注意:你必须自己检查该长整型是否在页的边界,并且在调用它前它已在task aera中,例程不会替你检查。通过下面的判断可知是否在边界:
if ((addr & ~PAGE_MASK) > PAGE_SIZE-sizeof(long))
如果不是则直接调用get_long()即可,不然则需用两个long来获得该数。
这个long存在tmp中,如果发现ret>=0,则调用put_user()宏。该宏允许设备在用户区写数据。注意,这个函数可能导致I/O冲突,如果被访问的内存已被换出,所以此时抢占可能发生。即使临界区已被cli()和sti()保护起来,也不要在临界区中使用这个函数,因为I/O冲突将破坏cli()/sti()对的完整性。如果你想到达用户空间内存,需在进入临界区前把它拷到内核区内存中。(ret为什么会大于零呢?)
如果请求是PTRACE_KILL,则首先判断子进程是否已为僵死状态,如是则直接退出。如否则调用一个内联函数wake_up_process(),置进程状态为运行态,然后将SIGKILL赋给child->exit_code,作为子进程强行退出时的出错代码。然后调用get_stack_long 和put_stack_long确保禁止单步跟踪,返回。(其中,对tss.esp0不了解,tss是一个thread_struct结构,也是task_struct中的一个字段)
如果请求是PTRACE_DETACH,则将断开父子进程的关联。首先做好最坏准备,置ret为-EIO。因为参数data将作为子进程强行退出时的出错代码,故如果大于64则出错返回,然后置flags的相关位,使子进程不被跟踪,接着唤醒子进程,将data赋给child->exit_ccode,安全的修改child的p_pptr指针使其指向起原始父进程,p_opptr.在通过调用get_stack_long和put_stack_long,确保单步中断被禁止。最后返回。
三. 总结
本实验报告组要有两部分组成,先分析了系统调用的陷入与返回过程,然后结合ptrace系统调用具体的分析了其中的流程.在分析过程中也遇到了不少的困难,比如,刚开始对那些内联的汇编语句不能理解,通过与同学的互相讨论,在结合Brennan UnderWood
“Brennan's Guide to Inline Assembly”,终于能看明白这些陌生的格式.在具体分析ptrace这个系统调用时,更是碰到了麻烦,再看了一本关于UNIX的相应的流程之后,对这个程序才有了些眉目.在UNIX SYSTEM V中,只有子进程能发ptrace(0)命令,要求父进程进行跟踪,而父进程则消极等待,在linux中,不仅支持了上面的功能,还允许父进程或超级用户主动提出要求,无疑大大方便了进程的监督与控制.然而由于时间和本人能力的关系,不能对其中所有的命令一一分析下来,即使分析过的,也还存在不少疑问,这也在分析过程中提过.但我仍感觉受益非浅,通过这学期操作系统课的学习,尤其是对linux系统的分析工作,我对计算机的认识又深了一层,
以前总认为能编出好的程序就足够了,可现在却觉得这还远远不够,因为任何程序的执行都依托于操作系统.我想利用寒假的时间对linux进行进一步研究, 也深深希望得到您的指点.
四. 附录
1
.参考资料
(1)Linux kernel 2.1.99
(2)李善平 《LINUX进程管理》
(3)David A Rusling “The Linux Kernel”
(4)Brennan UnderWood
“Brennan's Guide to Inline Assembly”