PTRACE函数代码分析.docx
《PTRACE函数代码分析.docx》由会员分享,可在线阅读,更多相关《PTRACE函数代码分析.docx(56页珍藏版)》请在冰豆网上搜索。
PTRACE函数代码分析
一函数说明
1.函数使用说明
名字
ptrace–进程跟踪
形式
#include
intptrace(intrequest,intpid,intaddr,intdata);
描述
Ptrace提供了一种父进程可以控制子进程运行,并可以检查和改变它的核心image。
它主要用于实现断点调试。
一个被跟踪的进程运行中,直到发生一个信号。
则进程被中止,并且通知其父进程。
在进程中止的状态下,进程的内存空间可以被读写。
父进程还可以使子进程继续执行,并选择是否是否忽略引起中止的信号。
Request参数决定了系统调用的功能:
PTRACE_TRACEME
本进程被其父进程所跟踪。
其父进程应该希望跟踪子进程。
PTRACE_PEEKTEXT,PTRACE_PEEKDATA
从内存地址中读取一个字节,内存地址由addr给出。
PTRACE_PEEKUSR
从USER区域中读取一个字节,偏移量为addr。
PTRACE_POKETEXT,PTRACE_POKEDATA
往内存地址中写入一个字节。
内存地址由addr给出。
PTRACE_POKEUSR
往USER区域中写入一个字节。
偏移量为addr。
PTRACE_SYSCALL,PTRACE_CONT
重新运行。
PTRACE_KILL
杀掉子进程,使它退出。
PTRACE_SINGLESTEP
设置单步执行标志
PTRACE_ATTACH
跟踪指定pid进程。
PTRACE_DETACH
结束跟踪
Intel386特有:
PTRACE_GETREGS
读取寄存器
PTRACE_SETREGS
设置寄存器
PTRACE_GETFPREGS
读取浮点寄存器
PTRACE_SETFPREGS
设置浮点寄存器
init进程不可以使用此函数
返回值
成功返回0。
错误返回-1。
errno被设置。
错误
EPERM
特殊进程不可以被跟踪或进程已经被跟踪。
ESRCH
指定的进程不存在
EIO
请求非法
2.功能详细描述
1)PTRACE_TRACEME
形式:
ptrace(PTRACE_TRACEME,0,0,0)
描述:
本进程被其父进程所跟踪。
其父进程应该希望跟踪子进程。
2)PTRACE_PEEKTEXT,PTRACE_PEEKDATA
形式:
ptrace(PTRACE_PEEKTEXT,pid,addr,data)
ptrace(PTRACE_PEEKDATA,pid,addr,data)
描述:
从内存地址中读取一个字节,pid表示被跟踪的子进程,内存地址由addr给出,data为用户变量地址用于返回读到的数据。
在Linux(i386)中用户代码段与用户数据段重合所以读取代码段和数据段数据处理是一样的。
3)PTRACE_POKETEXT,PTRACE_POKEDATA
形式:
ptrace(PTRACE_POKETEXT,pid,addr,data)
ptrace(PTRACE_POKEDATA,pid,addr,data)
描述:
往内存地址中写入一个字节。
pid表示被跟踪的子进程,内存地址由addr给出,data为所要写入的数据。
4)PTRACE_PEEKUSR
形式:
ptrace(PTRACE_PEEKUSR,pid,addr,data)
描述:
从USER区域中读取一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为用户变量地址用于返回读到的数据。
USER结构为core文件的前面一部分,它描述了进程中止时的一些状态,如:
寄存器值,代码、数据段大小,代码、数据段开始地址等。
在Linux(i386)中通过PTRACE_PEEKUSER和PTRACE_POKEUSR可以访问USER结构的数据有寄存器和调试寄存器。
5)PTRACE_POKEUSR
形式:
ptrace(PTRACE_POKEUSR,pid,addr,data)
描述:
往USER区域中写入一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为需写入的数据。
6)PTRACE_CONT
形式:
ptrace(PTRACE_CONT,pid,0,signal)
描述:
继续执行。
pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。
7)PTRACE_SYSCALL
形式:
ptrace(PTRACE_SYS,pid,0,signal)
描述:
继续执行。
pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。
与PTRACE_CONT不同的是进行系统调用跟踪。
在被跟踪进程继续运行直到调用系统调用开始或结束时,被跟踪进程被中止,并通知父进程。
8)PTRACE_KILL
形式:
ptrace(PTRACE_KILL,pid)
描述:
杀掉子进程,使它退出。
pid表示被跟踪的子进程。
9)PTRACE_SINGLESTEP
形式:
ptrace(PTRACE_KILL,pid,0,signle)
描述:
设置单步执行标志,单步执行一条指令。
pid表示被跟踪的子进程。
signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。
当被跟踪进程单步执行完一个指令后,被跟踪进程被中止,并通知父进程。
10)PTRACE_ATTACH
形式:
ptrace(PTRACE_ATTACH,pid)
描述:
跟踪指定pid进程。
pid表示被跟踪进程。
被跟踪进程将成为当前进程的子进程,并进入中止状态。
11)PTRACE_DETACH
形式:
ptrace(PTRACE_DETACH,pid)
描述:
结束跟踪。
pid表示被跟踪的子进程。
结束跟踪后被跟踪进程将继续执行。
12)PTRACE_GETREGS
形式:
ptrace(PTRACE_GETREGS,pid,0,data)
描述:
读取寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。
此功能将读取所有17个基本寄存器的值。
13)PTRACE_SETREGS
形式:
ptrace(PTRACE_SETREGS,pid,0,data)
描述:
设置寄存器值,pid表示被跟踪的子进程,data为用户数据地址。
此功能将设置所有17个基本寄存器的值。
14)PTRACE_GETFPREGS
形式:
ptrace(PTRACE_GETFPREGS,pid,0,data)
描述:
读取浮点寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。
此功能将读取所有浮点协处理器387的所有寄存器的值。
15)PTRACE_SETFPREGS
形式:
ptrace(PTRACE_SETREGS,pid,0,data)
描述:
设置浮点寄存器值,pid表示被跟踪的子进程,data为用户数据地址。
此功能将设置所有浮点协处理器387的所有寄存器的值。
二80386的调试设施
80386提供的调试设施包括:
●一字节的陷阱指令
●单步指令
●断点检测
●任务切换时的自陷
1.调试断点
断点设施是80386为调试程序提供的最重要的功能。
一个断点,允许编程人员对特定的线性地址设置特定的条件;当程序访问到该线性地址并满足特定的条件时,即跳转到异常处理程序。
80386可支持同时设置四个断点条件,编程人员可在程序中的四个位置设置条件,使其转向异常处理程序。
这四个断点的每一个断点,都可以是如下三种不同类型的任何一种:
只在指令地址与断点地址一致时,断点有效。
数据写入地址与断点地址一致时,断点有效。
数据读出地址或数据写入地址与断点地址一致时,断点有效。
16)调试寄存器
为支持提供四个调试断点,在80386中增加了八个寄存器,编号为DR0至DR7。
这八个寄存器中由四个用于断点,两个用于控制,另两个保留未用。
对这八个寄存器的访问,只能在0级特权级进行。
在其它任何特权级对这八个寄存器中的任意一个寄存器进行读或写访问,都将产生无效操作码异常。
此外,这八个寄存器还可用DR6及DR7中的BD位和GD位进行进一步的保护,使其即使是在0级也不能进行读出或写入。
对这些寄存器的访问使用通常的MOV指令:
MOVregDri
该指令将调试寄存器i中的内容读至通用寄存器reg中;
MOVDrireg
该指令将通用寄存器reg中的内容写至调试寄存器i中。
此处i的取值可以为0至7中的任意值。
图表示了这八个调试寄存器。
这些寄存器的功能如下:
DR0—DR3寄存器DR0—DR3包含有与四个断点条件的每一个相联系的线性地址(断点条件则在DR7中)。
因为这里使用的是线性地址,所以,断点设施的操作,无论分页机制是否启用,都是相同的。
DR4—DR5保留。
DR6DR6是调试状态寄存器。
当一个调试异常产生时,处理器设置DR6的相应位,用于指示调试异常发生的原因,帮助调试异常处理程序分析、判断,以及作出相应处理。
DR7DR7是调试控制寄存器。
分别对应四个断点寄存器的控制位,对断点的启用及断点类型的选择进行控制。
所有断点寄存器的保护也在此寄存器中规定。
DR6各位的功能
B0—B3当断点线性地址寄存器规定的条件被检测到时,将对应的B0—B3位置1。
置位B0—B3与断点条件是否被启用无关。
即B0—B3的某位被置1,并不表示要进行对应的断点异常处理。
BD如下一条指令要对八个调试寄存器之一进行读或写时,则在指令的边界BD位置1。
在一条指令内,每当即将读写调试寄存器时,也BD位置1。
BD位置1与DR7中GD位启用与否无关。
BS如果单步异常发生时,BS位被置1。
单步条件由EFLAGS寄存器中的TF位启用。
如果程序由于单步条件进入调试处理程序,则BS位被置1。
与DR6中的其它位不同的是,BS位只在单步陷阱实际发生时才置位,而不是检测到单步条件就置位。
BTBT位对任务切换导致TSS中的调试陷阱位被启用而造成的调试异常,指示其原因。
对这一条件,在DR7中没有启用位。
DR6中的各个标志位,在处理机的各种清除操作中不受影响,因此,调试异常处理程序在运行以前,应清除DR6,以避免下一次检测到异常条件时,受到原来的DR6中状态位的影响。
DR7各位的功能
LENLEN为一个两位的字段,用以指示断点的长度。
每一断点寄存器对应一个这样的字段,所以共有四个这样的字段分别对应四个断点寄存器。
LEN的四种译码状态对应的断点长度如下
LEN
说明
00
断点为一字节
01
断点为两字节
10
保留
11
断点为四字节
这里,如果断点是多字节长度,则必须按对应多字节边界进行对齐。
如果对应断点是一个指令地址,则LEN必须为00
RWERWE也是两位的字段,用以指示引起断点异常的访问类型。
共有四个RWE字段分别对应四个断点寄存器,RWE的四种译码状态对应的访问类型如下
RWE
说明
00
指令
01
数据写
10
保留
11
数据读和写
GE/LEGE/LE为分别指示准确的全局/局部数据断点。
如果GE或LE被置位,则处理器将放慢执行速度,使得数据断点准确地把产生断点的指令报告出来。
如果这些位没有置位,则处理器在执行数据写的指令接近执行结束稍前一点报告断点条件。
建议读者每当启用数据断点时,启用LE或GE。
降低处理机执行速度除稍微降低一点性能以外,不会引起别的问题。
但是,对速度要求严格的代码区域除外。
这时,必须禁用GE及LE,并且必须容许某些不太精确的调试异常报告。
L0—L3/G0—G3L0—L3及G0—G3位分别为四个断点寄存器的局部及全局启用信号。
如果有任一个局部或全局启用位被置位,则由对应断点寄存器DRi规定的断点被启用。
GDGD位启用调试寄存器保护条件。
注意,处理程序在每次转入调试异常处理程序入口处清除GD位,从而使处理程序可以不受限制地访问调试寄存器。
前述的各个L位(即LE,L0—L3)是有关任务的局部位,使调试条件只在特定的任务启用。
而各个G位(即GD,G0—G3)是全局的,调试条件对系统中的所有任务皆有效。
在每次任务切换时,处理器都要清除L位。
17)断点地址识别
LEN字段及断点线性地址的组合,规定调试异常检查的四个线性地址的范围。
上面已经提到,断点线性地址必须对齐于LEN规定的多字节长度的相应长度边界。
事实上,处理器在检查断点时,根据LEN规定的长度,忽略线性地址的相应低位。
例如,当LEN=11时,线性地址的最低两位被忽略,即把线性地址最低两位视为00,因而按四字节边界对齐。
而当LEN=01时,线性地址的最低位被忽略,即把线性地址的最低位视为0,因而按两字节边界对齐。
对于由断点线性地址及LEN规定的地址范围内类型正确的任何字节的访问都产生异常,数据的访问及指令的取出,都要按所有四个断点地址范围进行检查。
如果断点地址范围的任何字节匹配,访问的类型也匹配,则断点异常被报告。
下表给出了识别数据断点的几个离子,这里假设所有断点被启用,而且设置了正确的访问类型。
地址
长度
长度
断点
寄存器内容
DR0
DR1
DR2
DR3
00FF02
00CC32
0D0004
01FF00
1
2
4
4
Len=00
Len=01
Len=11
Len=11
引起异常的
访问
00FF02
00CC33
0D0007
00FEFF
01FF00
01FF03
1
1
2
4
4
4
B0=1
B1=1
B2=1
B0=1
B3=1
B3=1
不引起异常
的访问
00FF01
00FF00
00CC34
01FEFF
0D0000
1
2
1
1
4
18)代码断点与数据断点的比较
指令访问断点与数据访问断点之间有如下几点区别:
1.在RWE字段的设置不同。
指令断点,RWE=0;数据断点,RWE≠0。
2.LEN的设置不同。
指令断点的长度只能是00即一字节;数据断点的长度可以是1、2、4字节。
由于很多指令的长度超过一字节(事实上,指令长度为1—15字节),所以指令断点必须设置在指令的第一个字节。
3.指令断点属故障类型,数据断点属陷阱类型。
指令断点故障在指令执行前被检查及报告。
数据断点陷阱则在对断点位置进行读和写之后才被报告。
可以看出,数据断点陷阱对断点数据未进行写保护。
要保护断点的数据不会被写入操作破坏,必须先在调试处理程序中保存断点原来数据的拷贝。
由于指令断点在指令执行之前被报告,因此,很明显,对该指令不能简单的重新执行。
因为每一次新的执行都简单地重复产生故障,所以,如果调试处理程序不禁用断点,则这种故障就会形成无限地循环。
为解决这一问题,就需用到80386中EFLAGS的RF位。
当RF位置位时,任何指令断点都被忽略,因此,在RF位保持为置位状态时,指令断点将不再起作用。
但RF位的置位状态不会长久保持。
事实上,处理器的内部逻辑保证,在任何一条指令成功完成后,都将RF位清零,因此,RF位的置位状态最多只保持一条指令的时间。
也就是说,在RF位置位后的下一条指令,指令断点不起作用,这样只要在重新执行指令之前,将RF置1,即可保证该指令断点不会形成无限循环,而且,也不影响紧接的下一条指令也设置指令断点。
RF位的置位,不是用某一个操作直接将EFLAGS的RF位置1来完成。
每当进入一个故障处理程序,处理器保存中断现场时,需把断点等信息压栈。
当把EFLAGS寄存器压栈时,推入栈中的EFLAGS的RF位是1,因此用IRET指令推出故障处理程序时,从栈中弹出的EFLAGS寄存器标志位中的RF为1,从而将RF位置位。
2.TSS中的调度陷阱
每当通过TSS发生任务切换时,TSS中的T位使调试处理程序被调用,这就为调试程序管理某些任务的活动提供了一种方便的方法。
DR6中的BT位指示对该位的检测,DR7中对该位没有特别的启用位。
如果调试处理程序是通过任务门使用的,则不能设置对应TSS的调试陷阱位。
否则,将发生调试处理程序的无限循环。
3.INT3
一个断点指令提供调试程序的另一种方法。
按这种方法,要求作为断点指令的第一个字节用INT3指令替代。
因此,程序执行到预先需要的断点处遇到断点指令,并进入INT3处理程序。
在一些使用INT3显然不足的地方还需使用断点寄存器。
这样的情况有:
1.由ROM提供的代码中,不可能插入INT3指令。
2.由于使用了INT3,原来的程序代码被修改,使执行此代码的其它任务也被中断。
3.INT3不能执行数据断点。
在另外一些情况下,使用INT3则很有用:
1.单步及断点设施仅仅进入调试程序,而对调试处理程序的调试,INT3则是唯一方便的方法。
2.代码中可以插入任意数量的INT3指令,而断点设施只能提供最多四个断点。
3.早期86系列的各种型号处理器,没有80386提供的断点设施,INT3指令在这些处理器中是执行任何断点的唯一方法。
概括地说,除了某些特别情况之外,建议使用INT3指令在代码中执行断点,保留断点寄存器用于数据断点。
4.程序的步进执行
单步功能对程序调试者来说,是一个方便的调试手段。
通过一条一条地执行指令,对操作数据、操作指令及操作结果地观察和分析,可以帮助调试人员判断出执行某一指令时,是否发生了硬件错误,或是否软件逻辑错误。
80386的单步功能通过陷阱来实现。
单步陷阱在EFLAGS寄存器中的TF位置位时启用。
在一条指令开始执行时,如果有TF=1,则在指令执行的末尾产生调试异常,并进入调试处理程序。
在这里,“指令开始执行时,TF=1”这一条件是重要的。
有此条件的限制,使TF位置位1的指令不会产生单步陷阱。
每次产生单步陷阱之后,在进入调试处理程序之前要将TF位清除。
此外,在处理中断或异常时,也清除TF位。
如果外部中断与单步中断同时发生,则单步中断被优先处理,并清除TF。
在调试处理程序第一条指令执行之前,如仍有悬挂的中断请求,则响应并处理中断。
因此,中断处理是在没有单步启用的情况下完成的。
如果希望在中断处理程序中使用单步功能。
则需先把中断处理程序的第一条指令设置为断点,当程序运行到断点处停下来之后,再启用单步功能。
三代码分析
在Linux核心源代码中,与完成ptrace功能相关的代码有:
●sys_ptrace函数,完成ptrace系统调用的代码。
●为完成sys_ptrace功能所需调用的一些辅助函数,寄存器读写函数和内存读写函数。
●信号处理函数中,对被调试进程的处理(中止其运行、继续运行)。
●syscall_trace函数,完成了系统调用调试下的处理。
●调试陷阱处理(异常1处理),完成单步执行和断点中断处理。
●execve系统调用中对被调试进程装入后中止的实现。
1.sys_ptrace函数
ptrace系统调用在核心对应的处理函数为sys_ptrace()(/linux/arch/i386/kernel/ptrace.c)。
sys_ptrace函数完成了ptrace系统调用功能。
ptrace函数的总体流程如下:
程序和说明注释如下:
asmlinkageintsys_ptrace(longrequest,longpid,longaddr,longdata)
{
structtask_struct*child;
structuser*dummy=NULL;
unsignedlongflags;
inti,ret;
lock_kernel();
ret=-EPERM;
if(request==PTRACE_TRACEME){
。
。
。
PTRACE_TRACEME处理
}
ret=-ESRCH;
read_lock(&tasklist_lock);
child=find_task_by_pid(pid);/*查找task结构*/
read_unlock(&tasklist_lock);
if(!
child)/*没有找到task结构,所名给定pid错误*/
gotoout;
ret=-EPERM;
if(pid==1)/*init进程不能调试*/
gotoout;
if(request==PTRACE_ATTACH){
。
。
。
PTRACE_ATTACH处理
}
ret=-ESRCH;
if(!
(child->flags&PF_PTRACED))/*进程没有被跟踪,不能执行其它功能*/
gotoout;
if(child->state!
=TASK_STOPPED){
if(request!
=PTRACE_KILL)/*除PTRACE_KILL外的其它功能要求*/
gotoout;/*要求进程状态为TASK_STOPPED*/
}
if(child->p_pptr!
=current)/*被跟踪进程要求为当前进程的子进程*/
gotoout;
switch(request){
casePTRACE_PEEKTEXT:
casePTRACE_PEEKDATA:
{
。
。
。
PTRACE_PEEKTEXT,PTRACE_PEEKDATA处理
}
casePTRACE_PEEKUSR:
{
。
。
。
PTRACE_PEEKUSR处理
}
casePTRACE_POKETEXT:
casePTRACE_POKEDATA:
{
。
。
。
PTRACE_POKETEXT,PTRACE_POKEDATA处理
}
casePTRACE_POKEUSR:
{
。
。
。
PTRACE_POKEUSR处理
}
casePTRACE_SYSCALL:
casePTRACE_CONT:
。
。
。
PTRACE_SYSCALL,PTRACE_CONT处理
}
casePTRACE_KILL:
{
。
。
。
PTRACE_KILL处理
}
casePTRACE_SINGLESTEP:
{
。
。
。
PTRACE_SINGLESTEP处理
}
casePTRACE_DETACH:
。
。
。
PTRACE_DETACH处理
}
casePTRACE_GETREGS:
。
。
。
PTRACE_GETREGS处理
};
casePTRACE_SETREGS:
。
。
。
PTRACE_SETREGS处理
};
casePTRACE_GETFPREGS:
。
。
。
PTRACE_GETFPREGS处理
};
casePTRACE_SETFPREGS:
。
。
。
PTRACE_SETFPREGS处理
};
default:
ret=-EIO;
go