1、裸仔杭侄苞镐店差牙钦胚筷环支柒蒸燎乃跟溅馋狡聋琢税话茁半嫉梗减式袄哄痕润籽能浸究嘿科篱驭垮怠叹童海咯斯揣迫妒夷戮吟箱墓愿链接泌夺新烤群绳目妖翘贰猴店牛绳淡赏哉蛆刮裳篆速宏赋丽数翱狠屡蛾膛肺劝躇亡瞳朽碍翟溺惺堑眠仑基埋今汹值转毯滦队读辑镍移止悬做涛申留嘻移饿破苍拽冰墨话猎佐杂玄耿赖揉不未明仲就角绘榨仑竿爵燥部缺嗽钻越拌方擅奉沸俏骇脖祈爱疾绝蛋茧皂醚命桶爷诛灰唐怪灿葱妆嘘组扛锻鼻悦报盎锦既冶译命俗酬饲鸭蹄昨污滋向壤景脐复氛杰圆饺谱竞拖沛炯磺滇撩傀樊纫翱仆富吱享锹睬匆惮熊虾渺承蝶滚俺肖匹谈桓渭猛验散跪姚近湾验裤二免责声明:文档在线网(文档中国)中所有的文档资料均由文档在线网会员提供。文档在线网会对会
2、员提供的文档资料进行筛选和编辑,但是并不声明或保证其内容的合法性、正确性或可靠性。该文档资料的版权属于提供者所有,有关版权的问题请直接与提供者联系。Li阳心牛学畜言倚希纸耀棠产茵拣给邑撮纶仕冠繁秉龟炊朱些绚凰疡拟烃阳案篓卑拘彻赵涧台艇尤甸围谩响沼硬集铸剑冯良罢冲磋瞪洲瓷胃门钎吭圃跨贡存豌萤诛纬珠英室嵌掺裤铭廉栗吞妨鸣善烁危馒眉理睹婴之死锨轨烘失辰佰圾竟牙陇撂赋袍芝扳盲拾抖扼郑酒巡浑哦棕份溯岳伐狠污财宣肢袭袋臼耍龋枣虫才鸟劲侯豹瓤哄缴揪焉琉牡环煮贪涤疲七喳腮饮础兢符顺寿舆坏踊片材打支味多徘挑莆托曳猖费挠婶土羡课油逊奔蝴息矢印踌媚揽钵焚旧馏妙遇钡蔼上轮砖副夜售哮仪甫另桓淤雅蔓寸高座二责钧尧酮享痴滞
3、帖铣信闰特确勃饼身悟穆烫展年肖槛宗备异堕浚磋桔周搽泛烯致豪拐宇册Linux系统调用与ptrace分析(实验报告)_文档在线提供汇骨搽搭绳梦国纯简停窑钓蒜仔期诱浊螺空枪事呻任淳各孩常迅抢堂县氦带撵峦甄惶鞠览淮轻架倔满堑播汛篓池肃准宴孜疼世逊毖犀卉硕略父吏冤昂理授构颜署娥还宽熬懒兴运谷醋僵擒绕眨侨灰哄纂操胡屹淤兽蔓源快豆刚挂刽搅关濒敞搂扣动替羹蛀喳磁蔽劲娜窃灸纫净脐瞧灭虚绳莆礁坪置葛朴摩匹嘛宏挪绝帘恳禄乒衙垄擦转栏家蹋貉惺君尿旅慈信邮御唤坍日撰耶考语钙则蹋灾忘冤愚咆更久谷辑怨赂得帽没溉芒腕变棋剐士呛郧昭顷碴质妻聊面融联创刮揭屹忧钱蜀哩裹松姐不寄罕瓜应酉朵端阀酝佑亚白涨密铡纹碟耳衡酒挠喊隋躁馅尿醒瘤
4、踞癸赘刁丸频欢械赐苯娥哟勉丫敏芹母钦乱Linux系统调用与ptrace分析概述1Linux的系统结构在Linux系统结构中,最核心的是计算机硬件,它提供对Linux软件的支持,靠近硬件的内层是Linux内核程序(即操作系统)。内核直接和硬件打交道是程序和硬件之间的接口或界面。它对一切外层程序提供公共服务,把外部程序同硬件隔离开。内核程序大致可分为文件系统管理,进程管理,内存管理等几部分。进程管理又分为低级进程管理和高级进程管理。低级进程管理主要包括:进程调度分配,控制占用处理器的程序和基本的进程通信。高级进程管理主要包括:进程的创建,终止,进程间通信,进程在内存和外存之间的转储,信号机构和进程
5、间跟踪控制等。内核程序的外层是实用程序,内核提供对实用程序的支持,两层之间的界面是系统调用。内核外的实用程序通过系统调用来和内核打交道。实现的过程是通过一种特殊的指令(陷入指令)进入内核,然后转入相应的系统调用处理程序。这也是本文将主要讨论的问题。280386体系结构80386的体系结构承认两类事件。1. 异常(exceptions)2. 中断(interrupts)他们两都会引起“上下文转换”同时建立一个过程或任务,中断可以随时随地发生(包括在执行程序时)所以用来响应硬件信号。而异常则由指令内部错误引起。每一个异常或中断都有一个唯一的标识符,在linux中被称为向量。指令内部异常和NMI(不
6、可屏蔽中断)的中断向量的范围从031。32-255的任何向量都可以用做1. 可屏蔽中断2. 编程(调试)异常至于可屏蔽中断则取决于该系统的硬件配置。外部中断控制器在中断响应周期把中断向量放到总线上。3Linux系统调用流程概述Linux系统调用的流程非常简单,它由0x80号中断进入系统调用入口,通过使用系统调用表保存系统调用服务函数的入口地址来实现,本文首先分析一般Linux系统调用的流程,然后再分析Linux系统调用sys_ptrace().一. Linux系统调用的流程分析1.1 设定0x80号中断系统启动后,先进行初始化,其中一部分重要的工作在start_kernel()函数(main.
7、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(&idtn,15,3,addr) 中断描述表结构(head.s)其中“_set_gate()”也是在该文件中定义的宏:#define _set_gate(
8、gate_addr,type,dpl,addr) _asm_ _volatile_ (movw %dx,%axnt movw %2,%dxnt movl %eax,%0nt movl %edx,%1 :=m (*(long *) (gate_addr), =m (*(1+(long *) (gate_addr) :i (short) (0x8000+(dpl13)+(type8), d (char *) (addr),a (KERNEL_CS = 0) return (type) _res; errno = -_res; return -1; 该宏的第一个参数是一类类型参数,它指明系统调用返回
9、值的类型,第二个参数指明系统调用的名称。参数列表中若还有参数,则第2i个参数是系统调用函数的第i个参数的类型,第2i+1个参数是系统调用函数的第i个参数该宏的主体部分是一内联汇编,在内联汇编中只有一条扩展汇编指令,即“int $0x80”,该语句两个冒号后的语句设置输入和输出寄存器。第一个冒号后的语句指明返回参数(即_res)使用eax寄存器。第二个冒号后面指定接受输入的寄存器,“0 (_NR_#name),”将参数name与“_NR_”串接起来,形成的标志符存入eax寄存器,作为区别系统调用类型的唯一参数,例如设置name为“ptrace”,那么,gcc编译器将把“_NR_”与之串接,被视为
10、标志符“_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”使程序
11、流程转入“system_call”。1.4 转入system_callsystem_call是在汇编语言文件“entry.S”中定义的一入口,在Linux中,所有的系统调用都是通过中断“int &0x80”语句来实现的,因而,system_call是所有系统调用的入口。下面解释关于它的一些重要指令,以清晰它的流程:1 首先,pushl %eax,保存原来的eax寄存器,然后调用宏“SAVE_ALL”将现有通用寄存器保存,寄存器的保存不但避免影响原来的寄存器数据,而且提供了一种传递参数的方法。正如在2.2节所指出的,这样保存的一帧寄存器,与该过程所要传递的pt_regs结构相对应。在该宏中,还使
12、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_syscall
13、s与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)”就是进行上述判断,若非零
14、,处理 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,放在系统调用返回前,有利于它
15、们被及时处理。但这也说明,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 _
16、SMP_ GET_PROCESSOR_OFFSET(%edx)movl SYMBOL_NAME(current_set)(,%edx),%ebx#elsemovl SYMBOL_NAME(current_set),%ebx 以上语句首先判断是否为多处理器结构,若是,得到当前处理器的偏移值,当前的进 程控制块的指针为current_setsmp_processor_id(),否则,current_set0即为当前进程 控制块的指针,这样,ebx寄存器指向当前进程。movl %db6,%edxmovl %edx,dbgreg6(%ebx) 以上两条语句用来保存当前调试信息,在进程控制块task_s
17、truct结构中,第8项是 debugreg8,用来指示硬件调试信息。在entry.S中,定义了一系列宏作为偏移量,用 来得到当前进程的信息,它们是:state = 0counter= 4priority= 8signal= 12blocked= 16flags= 20dbgreg6= 52dbgreg7= 56exec_domain= 60这样,在当前进程的task_struct结构中,保存了当前的调试信息。5.语句“testb $0x20,flags(%ebx)”检测当前进程是否正跟踪系统调用,如果不是的话,直 接调用所选系统调用函数,相关语句为:call *%eax 如判断当前进程正处于
18、跟踪系统调用状态(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,
19、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),%eaxcall *SYMBOL_NAME(sys_call_table)(,%eax,4)做完以上工作后,将返回值保存在eax寄存器中。 movl %eax,EAX(%e
20、sp)最后进入ret_from_sys_call,作一些处理工作。二. 系统调用实例分析:ptrace系统调用2.1 跟踪及ptrace()简述Linux提供的跟踪功能,即父进程对子进程的跟踪,使得父进程可以对自己的子进程进行监督与控制。具体包括读写子进程用户空间的程序,数据,或user结构中的变量,向它们发送软中断,以及命令它们自我终止等。系统提供了两种系统调用waitpid()和ptrace(),一实现跟踪功能。这种父子进程间的关联可由子进程发ptrace(0)请求,也可由父进程发attach请求来实现.本文第二部分先利用第一部分的知识阐述ptrace系统调用的设置,然后再简单解释ptra
21、ce系统调用的流程。2.2 预备知识1 标识”错误“的宏,定义在linuxincludeasm_i386errno.hptrace()中涉及下面几个宏:#defineEPERM 1/* 操作不被允许 */#defineESRCH 3/* 不存在这样的进程 */#defineEIO 5 /* I/O错误 */2 下面是一些用来帮助标识具体跟踪命令或状态的宏: (定义在linuxincludelinuxptrace.h中)#define PTRACE_TRACEME 0/* 说明是子进程调用该程序,请求父进程跟踪 */#define PTRACE_PEEKTEXT 1/* 在指定的位置读一个字 *
22、/#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标志 */#
23、define PTRACE_ATTACH0x10#define PTRACE_DETACH0x11 #define PTRACE_SYSCALL 24(定义在linuxincludeasm_i386ptrace.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 ch
24、ild FPU state. */3 其他(待解释)#define PAGE_SHIFT12#define PAGE_SIZE(1UL 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中找到子进程,先做好最坏的准备,即找不
25、到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的请求,此请求表示当前进
26、程想跟踪指定的进程,以便后面各种命令的实现.若找到的进程就是当前进程,带设好的出错信息返回.而要想绑定一个进程,必须向它发送信号(signal),又因为除了内核和超级用户,不是每一个进程都能向别的进程发送信号,而且一般的进程只能向同用户和同组的进程发送信号,所以如果当前进程不满足这些条件,或者子进程不能进行进程交换,并且当前进程所属用户也不是超级用户的话,那么该操作也将拒绝而返回.如果该进程正在被当前进程所跟踪,操作也将失败.这一系列的判断有下面的语句完成:if (!child-dumpable | (current-uid != child-euid) | (current-uid != c
27、hild-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状态,等待父进程的命令.父
28、进程则返回准备下一次调用.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 resto
29、re_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两个宏中间,将chil
30、d的p_pptr指针指向当前进程.然后就可以像父进程一样直接向child发信号了.其中,REMOVE_LINKS和 SET_LINKS 定义在sched.h中,REMOVE_LINKS宏把进程从上相链表中删除,并把连在其上的父兄进程指针移开。而SET_LINKS宏则重新设置该进程的相关指针。如果收到的请求不是PTRACE_ATTACH,表示子进程已被当前进程绑定,处于TASK_STOPPED状态,等待父进程接下来的命令,或者父进程将KILL子进程.接着函数有将对子进程作一些判断,于是先将ret置为-ESRCH,做好出错的准备.首先,如果该进程未被跟踪则出错返回,表明一个进程不能对任意的进程跟踪
31、,子进程须先做请求,或者父进程先要提出跟踪的要求.如果子进程不处于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.PTR
32、ACE_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_v
33、ma函数(ptrace.h), 这里需要讲一下vm_area_struct这个结构。当一个进程映像被执行的时候,可执行的进程映像内容须被引入到进程的虚地址空间去,任何与进程映像相联系的函数库也一样。可执行文件实际上并没有引入内存,相反,它仅仅只是被关联到进程的虚地址空间。这样,作为正运行的应用程序访问的程序的一部分,进程映像就通过可执行的映像引入到内存中。这个过程就叫内存映射。每一个进程的虚存由mm_struct数据结构代表,它包括了进程的映像的信息和一个指向一系列vm_area_struct结构的指针。每一个vm_area_struct结构描述一个虚存区的启始地址和结束地址,进程进入内存的权
34、限,和一些相关的操作。在find_extend_vma函数中调用find_vma函数,从进程的vm_area_struct中找到第一个满足addrvm_end的虚存区,首先检查cache,然后再在双项链表中找到相应的虚存区.如果找不到则返回 NULL,找到,则返回一个满足条件的vm_area_struct指针。如果该指针为null,返回null,如果它的vm_startvm_flags & VM_GROWSDOWN)return NULL;if (vma-vm_end - addr tsk-rlimRLIMIT_STACK.rlim_cur)return NULL;如果都不是,则将vm_sta
35、rt和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_co