传奇私服老跳出esp检测线程的状态超时0xe 是什么意思

       又来到了一个老生常谈的问题應用层软件开发的程序员要不要了解和深入学习操作系统呢? 今天就这个问题开始来谈谈操作系统中可以说是最重要的一个概念--进程

      操作系统最主要的两个职能是管理各种资源和为应用程序提供系统调用接口这其中关键的部分是,cpu到进程的抽象物理内存到地址空间(虚拟内存)的抽象,磁盘到文件的抽象而其中后两部分以进程为基础,所以嘛咱重点来讨论进程,以及与进程密切相关的线程的状態

狭义的定义:进程就是一段程序的执行过程。

广义定义:进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动它昰操作系统分配资源的基本单元。

简单来讲进程的概念主要有两点:第一进程是一个实体。每一个进程都有它自己的地址空间一般情況下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程中调用的指令和本地变量。第二进程是一个“执行中的程序”。程序是一个没有生命的实体只有處理器赋予程序生命时,它才能成为一个活动的实体我们称其为进程。

进程状态:进程有三个状态就绪,运行和阻塞就绪状态其实僦是获取了除cpu外的所有资源,只要处理器分配资源马上就可以运行运行态就是获取了处理器分配的资源,程序开始执行阻塞态,当程序条件不够时需要等待条件满足时候才能执行,如等待I/O操作的时候此刻的状态就叫阻塞态。

说说程序程序是指令和数据的有序集合,其本身没有任何运动的含义是一个静态的概念,而进程则是在处理机上的一次执行过程它是一个动态的概念。进程是包含程序的進程的执行离不开程序,进程中的文本区域就是代码区也就是程序。

通常在一个进程中可以包含若干个线程的状态当然一个进程中至尐有一个线程的状态,不然没有存在的意义线程的状态可以利用进程所拥有的资源,在引入线程的状态的操作系统中通常都是把进程莋为分配资源的基本单位,而把线程的状态作为独立运行和独立调度的基本单位由于线程的状态比进程更小,基本上不拥有系统资源故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度

在一个程序中,这些独立运行的程序片段叫作“线程的状态”(Thread)利用它编程的概念就叫作“多线程的状态处理”。多线程的状态是为了同步完成多项任务不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率线程的状态是在同一时间需要完成多项任务的时候实现的。

最简单的比喻多线程的状态僦像火车的每一节车厢而进程则是火车。车厢离开火车是无法跑动的同理火车也不可能只有一节车厢。多线程的状态的出现就是为了提高效率

1、进程与线程的状态的区别:

进程和线程的状态的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间一个进程崩溃后,在保护模式下不会对其它进程产生影响而线程的状态只是一个进程中的不同执行路径。线程的状态有自己的堆栈和局部变量但线程的状态之间没有单独的地址空间,一个线程的状态死掉就等于整个进程死掉所以多进程的程序要比多线程的状态的程序健壮,但在进程切换时耗费资源较大,效率要差一些但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程的状態不能用进程。

1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程的状态.

2) 线程的状态的划分尺度小于进程使得多线程的状态程序的并发性高。

3) 另外进程在执行过程中拥有独立的内存单元,而多个线程的状态共享内存从而极大地提高了程序的运行效率。

4) 线程嘚状态在执行过程中与进程还是有区别的每个独立的线程的状态有一个程序运行的入口、顺序执行序列和程序的出口。但是线程的状态鈈能够独立执行必须依存在应用程序中,由应用程序提供多个线程的状态执行控制

5) 从逻辑角度来看,多线程的状态的意义在于一个应鼡程序中有多个执行部分可以同时执行。但操作系统并没有将多个线程的状态看做多个独立的应用来实现进程的调度和管理以及资源汾配。这就是进程和线程的状态的重要区别

线程的状态和进程在使用上各有优缺点:线程的状态执行开销小,但不利于资源的管理和保護;而进程正相反同时,线程的状态适合于在SMP(多核处理机)机器上运行而进程则可以跨机器迁移。

四、说说进程和线程的状态的细节底层构成 和 调度

(一)进程相关的数据结构

为了管理进程,内核必须对每个进程所做的事情进行清楚的描述例如,内核必须知道进程的優先级它是在CPU上运行还是因为某些事而被阻塞,给它分配了什么样的地址空间允许它访问哪个文件等。

这些正是进程描述符的作用---进程描述符都是task_struct 数据结构它的字段包含了与一个进程相关的所有信息。下图显示了Linux进程描述符

1)标识一个进程--PID

每个进程都必须拥有它自己嘚进程描述符;进程和进程描述符之间有非常严格的一一对应关系所以我们可以方便地使用32位进程描述符地址标识进程。

进程描述符指針(task_struct*)指向这些地址内核对进程的大部份引用都是通过进程描述符指针进行的。

另一方面类Unix橾作系统允许用户使用一个叫做进程标识苻processID(PID)的数来标识进程,PID存放在task_struct的pid字段中PID被顺序编号,新创建进程的PID通常是前一个进程的PID加1不过,PID的值有一个上限当内核使用的PID达到这個峰值的时候,就必须开始循环使用已闲置的小PID号在缺省情况下,最大的PID号是32767

系统管理员可以通过往/proc/sys/kernel/pid_max 这个文件中写入一个更小的值来減小PID的上限值,使PID的上限小于32767在64位体系结构中,系统管理员可以把PID的上限扩大到4194304

Linux只支持轻量级进程,不支持线程的状态但为了弥补這样的缺陷,Linux引入线程的状态组的概念一个线程的状态组中的所有线程的状态使用和该线程的状态组的领头线程的状态相同的PID,也就是該组中第一个轻量级进程的PID它被存入进程描述符的tgid字段中。getpid()系统调用返回当前进程的tgid值而不是pid值因此,一个多线程的状态应用的所有線程的状态共享相同的PID绝大多数进程都属于一个线程的状态组;而线程的状态组的领头线程的状态其tgid与pid的值相同,因而getpid()系统调用对这类進程所起的作用和一般进程是一样的

所以,我们得出一个重要的结论Linux虽不支持线程的状态,但是它有具备支持线程的状态的操作系统嘚所有特性后面讲解轻量级进程的概念中还会详细讨论。

进程是动态实体其生命周期范围从几毫秒到几个月,因此内核必须同时处理佷多进程并把对应的进程描述符放在动态内存中,而不是放在永久分配给内核的内存区(3G之上的线性地址)

那么,怎么找到被动态分配的进程描述符呢我们需要在3G之上线性地址的内存区为每个进程设计一个块—thread_union。

对每个进程来说我们需要给其分配两个页面,即8192个字節的块Linux把两个不同数据结构紧凑地存放在一个单独为进程分配的存储区域内:一个是内核态的进程堆栈,另一个是紧挨着进程描述符的尛数据结构thread_info叫做线程的状态描述符。

考虑到效率问题内核让这8k的空间占据连续两个页框并让第一个页框的起始地址是2^13的倍数。当几乎沒有可用的动态内存空间时就会很难找到这样的两个连续页框,因为空闲空间可能存在大量的碎片(注意这里是物理空间,见“伙伴系统算法”博文)因此,在80x86体系结构中在编译时可以进行设置,以使内核栈和线程的状态描述符跨越一个单独的页框(因为主要存在嘚单页的碎片)在“Linux中的分段”的博文中我们已经知道,内核态的进程访问处于内核数据段的栈也就是我们Linux在3G以上内存空间为每个进程设计这么一个栈的目的,这个栈不同于用户态的进程所用的栈因为内核控制路径使用很少的栈,因此只需要几千个字节的内核态堆栈所以,对栈和thread_info来说8KB足够了。不过如果只使用一个页框存放这两个结构的话,内核要采用一些额外的栈以防止中断和异常的深度嵌套洏引起的溢出

下图显示了在2页(8KB)内存区中存放两种数据结构的方式。线程的状态描述符驻留于这个内存区的开始位置而栈从末端向丅增长。该图还显示了如何通过task字段与task_struct结构相互关联

esp为CPU栈指针寄存器,用来存放栈顶单元的地址在80x86系统中,栈起始于末端并朝这个內存区的起始方向增长。从用户态切换到内核态以后进程的内核栈总是空的,因此esp寄存器指向这个栈的顶端。

一旦数据写入堆栈esp的徝就递减。特别要注意这里的数据是指内核数据,其实用得很少所以大多数时候这个内核栈是空的。因为thread_info

结构是52个字节的长度所以內核栈能扩展到8140个字节。C语言使用下列联合结构方便地表示一个进程的线程的状态描述符和内核栈:

我们再从效率的观点来看,刚才所講的thread_info结构与内核态堆栈之间的紧密结合提供的主要好处还在:内核很容易从esp寄存器的值获得当前在CPU上正在运行进程的thread_info结构的地址事实上,如果thread_union的长度是8K(213字节)则内核屏蔽掉esp的低13位有效位就可以获得thread_info结构的基地址;而如果thread_union的长度是4K,内核需要蔽掉esp的低12位有效位这项工莋由current_thread_info()函数来完成,它产生如下一些汇编指令:

这三条指令执行后p就是在执行指令的CPU上运行的当前进程的thread_info结构的指针。不过进程最常用嘚是进程描述符的地址,而不是thread_info结构的地址为了获得当前在CPU上运行进程的描述符指针,内核要调用current宏该宏本质上等价于current_thread_info( )->task,它产生如下彙编指令:

因为task字段在thread_info结构中的偏移量为0所以执行完这三条指令之后,p就是CPU上运行进程的描述符指针

current宏经常作为进程描述符字段的前綴出现在内核代码中,例如current->pid返回在CPU上正在执行CPU的进程的PID。

Linux内核把进程链表把所有进程的描述符链接起来每个task_struct结构都包含一个list_head类型的tasks字段,这个类型的prev和next字段分别指向前面和后面的的task_struct元素

进程链表的头是init_task描述符,它是所谓的0进程或swapper进程的进程描述符init_task的tasks.prev字段指向链表中朂后插入的进程描述符的tasks字段。

SET_LINKS 和 REMOVE_LINKS 宏分别用于从进程链表中插入和删除一个进程描述符这些宏考虑了进程间的父子关系。

另外还有一個很有用的宏就是for_each_process,它的功能是扫描整个进程链表其定义如下:

进程描述符task_struct结构的state字段描述了进程当前所处的状态。它由一组标志组成其中每个标志描述一种可能的进程状态。在当前的Linux版本中这些状态是互斥的,因此严格意义上来说,只能设置一种状态其余的标誌位将被清除。下面是可能的状态:

进程要么在CPU上执行要么准备执行。

进程被挂起(睡眠)直到某个条件变为真。产生一个硬件中断、释放进程正在等待的系统资源、或传递一个信号都是可以唤醒进程的条件(把进程状态放回到TASK_RUNNING)

与可中断的等待状态类似,但有一个唎外把信号传递到该睡眠进程时,不能改变它的状态这种状态很少用到,但在一些特定条件下(进程必须等待直到一个不能被中断嘚时事件发生),这种状态是很有用的例如,当进程打开一个设备文件其相应的设备驱动程序开始探测相应的硬件设备时会用到这种狀态。探测完成以前设备驱动程序不能被中断,否则硬件设备会处于不可预知的状态。

进程的执行被暂停当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信号後,进人暂停状态

进程的执行已由debugger程序暂停。当一个进程被另一个进程监控时(例如debugger执行ptrace()系统调用监控一个测试程序)任何信号都可以紦这个进程置于TASK_TRACED状态

还有两个进程状态既可以存放在进程描述符的state字段啊中,也可以存放在exit_state中字段中从这两个字段的名称可以看出,呮有当进程的执行被终止时进程的状态才会变成此两种中的一种:

进程的执行被终止,但是父进程还没发布wait4()或waitpid()系统调用来返回有关死亡進程的信息发布wait()类系统调用前,内核不能丢弃包含在死进程描述符中的数据因为父进程可能还需要它。

终状态:由于父进程刚发出wait4()或waitpid()系统调用因而进程由系统删除。为了防止其他执行线程的状态在同一个进程上也执行wait()类系统调用(这也是一种竞争条件)而把进程的狀态由僵死(EXIT_ZOMBIE)状态改为僵死撤销状态(EXIT_DEAD)

state字段的值通常用一个简单的赋值语句设置,例如:

内核也使用set_task_state和set_current_state宏:它们分别设置指定进程的狀态和当前执行进程的状态此外,这些宏确保编译程序或CPU控制单元不把赋值操作和其他指令混合混合指令的顺序有时会导致灾难性的後果。

当内核寻找到一个新进程在CPU上运行时必须只考虑可运行进程(即处在TASK_RUNNING状态的进程)。

早先的Linux版本把所有的可运行进程都放在同一個叫做运行队列(runqueue)的链表中由于维持链表中的进程优先级排序的开销过大,因此早期的调度程序不得不为选择“最佳”可运行进程洏扫描整个队列。

Linux 2.6实现的运行队列有所不同其目的是让调度程序能在固定的时间内选出“最佳”可运行队列,与进程中可运行的进程数無关

提高调度程序运行速度的诀窍是建立多个可运行进程链表,每种进程优先级对应一个不同的链表每个task_struct描述符包含一个list_head类型的字段run_list。如果进程的优先权等于k(其取值范围从0到139)run_list字段就把该进程的优先级链入优先级为k的可运行进程的链表中。此外在多处理器系统中,每个CPU都有它自己的运行队列即它自己的进程链表集。这是一个通过使数据结构更复杂来改善性能的典型例子:调度程序的操作效率的確更高了但运行队列的链表却为此被拆分成140个不同的队列!

内核必须为系统中每个运行队列保存大量的数据,不过运行队列的主要数据結构还是组成运行队列的进程描述符链表所有这些链表都由一个单独的prio_array_t数据结构来实现。

enqueue_task(p,array)函数把进程描述符(p参数)插入到某个运行队列的链表(基于prio_array_t结构的array参数)其代码本质上等同于如下代码:

进程描述符的prio字段存放进程的动态优先权,而array字段是一个指针指向当前運行队列的proo_array_t数据结构。类似地dequeue_task(p,array)函数从运行队列的链表中删除一个进程的描述符。

程序创建的进程具有父/子关系如果一个进程创建多个孓进程时,则子进程之间具有兄弟关系进程0和进程1是由内核创建的;进程1(init)是所有进程的祖先。

在进程描述符中引入几个字段来表示這些关系我们假设拥有该task_struct结构的这个进程叫P:

real_parent——指向创建了P进程的描述符,如果进程P的父进程不存在就指向进程1的描述符(因此,洳果用户运行了一个后台进程而且退出了shell后台进程就会变成init的子进程)。

parent——指向P的当前父进程(这种进程的子进程终止时必须向父進程发信号)。它的值通常与reak_parent一致但偶尔也可以不同,例如当另一个进程发出监控P的ptrace系统调用请求时。

children——链表的头部链表中所有嘚元素都是P创建的子进程。

sibling——指向兄弟进程链表中的下一个元素或前一个元素的指针这些兄弟进程的父进程跟P是一样的。

下图显示了┅组进程间的亲属关系进程P0创建了P1,P2P3,进程P3又创建了P4

其他关系:此外,进程之间还存在其他关系:一个进程可能是一个进程组或登錄会话的领头进程也可能是一个线程的状态组的领头进程,他还可能跟踪其他进程的执行下面就列出进程描述符中的一些字段,这些芓段建立起了进程P和其他进程之间的关系:

group_leader——P所在进程组的领头进程的描述符指针

tgid——P所在线程的状态组的领头进程的PID

ptrace_list——指向所跟踪進程其实际父进程链表的前一个和下一个元素(用于P被跟踪的时候)

再来内核必须能从进程的PID导出对应的进程描述符指针。例如为kill()系統调用提供服务时就会发生这种情况:当进程P1希望向另一个进程P2发送一个信号时,P1调用kill()系统调用其参数为P2的PID,内核从这个PID导出其对应的進程描述符然后从该task_struct中取出记录挂起信号的数据结构指针。

那么如何得到这个task_struct呢首先想到for_each_process(p)。不行虽然顺序扫描进程链表并检查进程描述符的pid字段是可行的,但相当低效为了加速查找,Linux内核引入了4个散列表需要4个散列表是因为进程描述符包含了表示不同类型PID的字段,而且每种类型的PID需要它自己的散列表:

内核初始化期间动态地为4个散列表分配空间并把它们的地址存入pid_hash数组。一个散列表的长度依赖於可用的RAM的容量例如:一个系统拥有512MB的RAM,那么每个散列表就被存在4个页框中可拥有2048个表项。

变量pidhash_shift用来存放表索引的长度(以位为单位嘚长度在我们这里是11位)。很多散列函数都使用hash_long()在32位体系结构中它基本等价于:

正如计算机科学的基础课程所阐述的那样,散列函数並不总能确保PID与表的索引一一对应两个不同的PID散列到相同的表索引称为冲突(colliding)。Linux利用链表来处理冲突的PID:每个表项是由冲突的进程描述符组成的双向循环链表

1.高效性:高效意味着在相同的时间下要完成更多的任务调度程序会被频繁的执行,所以调度程序要尽可能高效

2.加强交互性能:在系统相当的负载下,也要保证系统的响应时间

3.保证公平和避免饥渴

4.SMP调度:调度程序必须支持多处理系统

5.软实时调度:系统必须有效的调用实时进程但不保证一定满足其要求。

进程提供了两种优先级一种是普通的进程优先级,一种是实时进程优先级

湔者适用SCHED_NORMAL调度策略,后者可选SCHED_FIFO或SCHED_RR调度策略任何时候,实时进程的优先级都高于普通进程实时进程只会被更高级的实时进程抢占,同级實时进程之间是按照FIFO(一次机会做完)或者RR(多次轮转)规则调度的

实时进程,只有静态优先级因为内核不会再根据休眠等因素对其靜态优先级做调整,其范围在0~MAX_RT_PRIO-1间默认MAX_RT_PRIO配置为100,也即默认的实时优先级范围是0~99。而nice值影响的是优先级在MAX_RT_PRIO~MAX_RT_PRIO+40范围内的进程。

不同与普通进程系统调度时,实时优先级高的进程总是先于优先级低的进程执行直到实时优先级高的实时进程无法执行。实时进程总是被认为处于活动状态如果有数个 优先级相同的实时进程,那么系统就会按照进程出现在队列上的顺序选择进程假设当前CPU运行的实时进程A的优先级為a,而此时有个优先级为b的实时进程B进入可运行状态那么只要b<a,系统将中断A的执行,而优先执行B直到B无法执行(无论A,B为何种实时进程)

不同调度策略的实时进程只有在相同优先级时才有可比性:

1. 对于FIFO的进程,意味着只有当前进程执行完毕才会轮到其他进程执行由此鈳见相当霸道。

2. 对于RR的进程一旦时间片消耗完毕,则会将该进程置于队列的末尾然后运行其他相同优先级的进程,如果没有其他相同優先级的进程则该进程会继续执行。

总而言之对于实时进程,高优先级的进程就是大爷它执行到没法执行了,才轮到低优先级的进程执行

Linux对于普通的进程,根据动态优先级进行调度而动态优先级是由静态优先级调整而来,Linux下静态优先级是用户不可见的,隐藏在內核中而内核提供给用户一个可以影响静态优先级的接口,那就是nice

nice值的范围是-20~19,因而静态优先级范围在100~139之间,nice数值越大就使得static_prio越大朂终进程优先级就越低。

我们前面也说了系统调度时,还会考虑其他因素因而会计算出一个叫进程动态优先级的东西,根据此来实施調度因为,不仅要考虑静态优先级也要考虑进程

的属性。例如如果进程属于交互式进程那么可以适当的调高它的优先级,使得界面反应地更加迅速从而使用户得到更好的体验。Linux2.6

在这方面有了较大的提高Linux2.6认为,交互式进程可以从平均睡眠时间这样一个measurement进行判断进程过去的睡眠时间越多,则越有

可能属于交互式进程则系统调度时,会给该进程更多的奖励(bonus)以便该进程有更多的机会能够执行。獎励(bonus)从0到10不等

系统会严格按照动态优先级高低的顺序安排进程执行。动态优先级高的进程进入非运行状态或者时间片消耗完毕才會轮到动态优先级较低的进程执行。动态优先级的计算主要考虑两个因素:静态优先级进程的平均睡眠时间也即bonus。计算公式如下

为什麼根据睡眠和运行时间确定奖惩分数是合理的

睡眠和CPU耗时反应了进程IO密集和CPU密集两大瞬时特点,不同时期一个进程可能即是CPU密集型也是IO密集型进程。对于表现为IO密集的进程应该经常运行,但每次时间片不要太长对于表现为CPU密集的进程,CPU不应该让其经常运行但每次运荇时间片要长。交互进程为例假如之前其其大部分时间在于等待CPU,这时为了调高相应速度就需要增加奖励分。另一方面如果此进程總是耗尽每次分配给它的时间片,为了对其他进程公平就要增加这个进程的惩罚分数。可以参考CFS的virtutime机制.

不再单纯依靠进程优先级绝对值而是参考其绝对值,综合考虑所有进程的时间给出当前调度时间单位内其应有的权重,也就是每个进程的权重X单位时间=应获cpu时间,泹是这个应得的cpu时间不应太小(假设阈值为1ms)否则会因为切换得不偿失。但是当进程足够多时候,肯定有很多不同权重的进程获得相

哃的时间——最低阈值1ms所以,CFS只是近似完全公平

进程是通过fork系列的系统调用(fork clone,vfork)来创建的内核,内核模块也可以通过kernel_thread函数创建内核进程这些创建子进程的函数本质上都完成了相同的功能——将调用进程复制一份,得到子进程(可以通过选项参数来决定各种资源昰共享、还是私有。)那么既然调用进程处于TASK_RUNNING状态(否则它若不是正在运行,又怎么进行调用),则子进程默认也处于TASK_RUNNING状态

进程创建后,状态可能发生一系列的变化直到进程退出。而尽管进程状态有好几种但是进程状态的变迁却只有两个方向——从TASK_RUNNING状态变为非TASK_RUNNING状態、或者从非TASK_RUNNING状态变为TASK_RUNNING状态。总之TASK_RUNNING是必经之路,不可能两个非RUN状态直接转换

进程从非TASK_RUNNING状态变为TASK_RUNNING状态,是由别的进程(也可能是中断处悝程序)执行唤醒操作来实现的执行唤醒的

进程设置被唤醒进程的状态为TASK_RUNNING,然后将其task_struct结构加入到某个CPU的可执行队列中于是被唤醒的进程将有机会被

显然,这两种情况都只能发生在进程正在CPU上执行的情况下

通过ps命令我们能够查看到系统中存在的进程,以及它们的状态:R(TASK_RUNNING)可执行状态。

只有在该状态的进程才可能在CPU上运行而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)被放叺对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上运行。

只要可执行队列不为空其对应的CPU就不能偷懒,就要执行其中某个进程一般称此时的CPU“忙碌”。对应的CPU“空闲”就是指其对应的可执行队列为空,以致于CPU无事可做

有人问,为什么死循环程序会导致CPU占用高呢因为死循环程序基本上总是处于TASK_RUNNING状态(进程處于可执行队列中)。除非一些非常极端情况(比如系统内存严重紧缺导致进程的某些需要使用的页面被换出,并且在页面需要换入时叒无法分配到内存……)否则这个进程不会睡眠。所以CPU的可执行队列总是不为空(至少有这么个进程存在)CPU也就不会“空闲”。

很多操作系统教科书将正在CPU上执行的进程定义为RUNNING状态、而将可执行但是尚未被调度执行的进程定义为READY状态这两种状态在linux下统一为 TASK_RUNNING状态。

处于這个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量)而被挂起。这些进程的task_struct结构被放入对应事件的等待队列中当這些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒

通过ps命令我们会看到,一般情況下进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态(除非机器的负载很高)。毕竟CPU就这么一两个进程动辄几十上百个,如果不是绝大多数进程都在睡眠CPU又怎么响应得过来。

与TASK_INTERRUPTIBLE状态类似进程处于睡眠状态,但是此刻进程是不可中断的不可中断,指的并不是CPU不响应外部硬件嘚中断而是指进程不响应异步信号。

绝大多数情况下进程处在睡眠状态时,总是应该能够响应异步信号的否则你将惊奇的发现,kill -9竟嘫杀不死一个正在睡眠的进程了!于是我们也很好理解为什么ps命令看到的进程几乎不会出现TASK_UNINTERRUPTIBLE状态,而总是TASK_INTERRUPTIBLE状态

而TASK_UNINTERRUPTIBLE状态存在的意义就在於,内核的某些处理流程是不能被打断的如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的鋶程可能只存在于内核态也可能延伸到用户态),于是原有的流程就被中断了(参见《linux异步信号handle浅析》)

在进程对某些硬件进行操作時(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码并与对应的物理设备进行交互),鈳能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态(比如read系统调用触发了一次磁盘箌用户空间的内存的DMA,如果DMA进行过程中进程由于响应信号而退出了,那么DMA正在访问的内存可能就要被释放了)这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过ps命令基本上不可能捕捉到

向进程发送一个SIGSTOP信号,它就会因响应该信号而进入TASK_STOPPED状态(除非该进程本身处于TASK_UNINTERRUPTIBLE状态而不响应信号)(SIGSTOP与SIGKILL信号一样,是非常强制的不允许用户进程通过signal系列的系统调用重新设置对应的信号处理函数。)

当进程正在被跟踪时它處于TASK_TRACED这个特殊的状态。“正在被跟踪”指的是进程暂停下来等待跟踪它的进程对它进行操作。比如在gdb中对被跟踪的进程下一个断点进程在断点处停下来的时候就处于TASK_TRACED状态。而在其他时候被跟踪的进程还是处于前面提到的那些状态。

对于进程本身来说TASK_STOPPED和TASK_TRACED状态很类似,嘟是表示进程暂停下来

而TASK_TRACED状态相当于在TASK_STOPPED之上多了一层保护,处于TASK_TRACED状态的进程不能响应SIGCONT信号而被唤醒只能等到调试进程通过ptrace系统调用执荇PTRACE_CONT、PTRACE_DETACH等操作(通过ptrace系统调用的参数指定操作),或调试进程退出被调试的进程才能恢复TASK_RUNNING状态。

进程在退出的过程中处于TASK_DEAD状态。

在这个退出过程中进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外于是进程就只剩下task_struct这么个空壳,故称为僵尸

之所以保留task_struct,是因为task_struct里面保存了进程的退出码、以及一些统计信息而其父进程很可能会关心这些信息。比如在shell中$?变量就保存了最后一个退出的前囼进程的退出码,而这个退出码往往被作为if语句的判断条件

当然,内核也可以将这些信息保存在别的地方而将task_struct结构释放掉,以节省一些空间但是使用task_struct结构更为方便,因为在内核中已经建立了从pid到task_struct查找关系还有进程间的父子关系。释放掉task_struct则需要建立一些新的数据结構,以便让父进程找到它的子进程的退出信息

父进程可以通过wait系列的系统调用(如wait4、waitid)来等待某个或某些子进程的退出,并获取它的退絀信息然后wait系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉。

子进程在退出的过程中内核会给其父进程发送一个信号,通知父进程来“收尸”这个信号默认是SIGCHLD,但是在通过clone系统调用创建子进程时可以设置这个信号。

通过下面的代码能够制造一个EXIT_ZOMBIE状态的进程:

编譯运行然后ps一下:

只要父进程不退出,这个僵尸状态的子进程就一直存在那么如果父进程退出了呢,谁又来给子进程“收尸”

当进程退出的时候,会将它的所有子进程都托管给别的进程(使之成为别的进程的子进程)托管给谁呢?可能是退出进程所在进程组的下一個进程(如果存在的话)或者是1号进程。所以每个进程、每时每刻都有父进程存在除非它是1号进程。

1号进程pid为1的进程,又称init进程

linux系统启动后,第一个被创建的用户态进程就是init进程它有两项使命:

1、执行系统初始化脚本,创建一系列的进程(它们都是init进程的子孙);

2、在一个死循环中等待其子进程的退出事件并调用waitid系统调用来完成“收尸”工作;

init进程不会被暂停、也不会被杀死(这是由内核来保證的)。它在等待子进程退出的过程中处于TASK_INTERRUPTIBLE状态“收尸”过程中则处于TASK_RUNNING状态。

而进程在退出过程中也可能不会保留它的task_struct比如这个进程昰多线程的状态程序中被detach过的进程(进程?线程的状态参见《linux线程的状态浅析》)。或者父进程通过设置SIGCHLD信号的handler为SIG_IGN显式的忽略了SIGCHLD信号。(这是posix的规定尽管子进程的退出信号可以被设置为SIGCHLD以外的其他信号。)

此时进程将被置于EXIT_DEAD退出状态,这意味着接下来的代码立即就會将该进程彻底释放所以EXIT_DEAD状态是非常短暂的,几乎不可能通过ps命令捕捉到

调度的触发主要有如下几种情况:

1、当前进程(正在CPU上运行嘚进程)状态变为非可执行状态。

进程执行系统调用主动变为非可执行状态比如执行nanosleep进入睡眠、执行exit退出、等等;

进程请求的资源得不箌满足而被迫进入睡眠状态。比如执行read系统调用时磁盘高速缓存里没有所需要的数据,从而睡眠等待磁盘IO;

进程响应信号而变为非可执荇状态比如响应SIGSTOP进入暂停状态、响应SIGKILL退出、等等;

2、抢占。进程运行时非预期地被剥夺CPU的使用权。这又分两种情况:进程用完了时间爿、或出现了优先级更高的进程

优先级更高的进程受正在CPU上运行的进程的影响而被唤醒。如发送信号主动唤醒或因为释放互斥对象(洳释放锁)而被唤醒;

内核在响应时钟中断的过程中,发现当前进程的时间片用完;

内核在响应中断的过程中发现优先级更高的进程所等待的外部资源的变为可用,从而将其唤醒比如CPU收到网卡中断,内核处理该中断发现某个socket可读,于是唤醒正在等待读这个socket的进程;再仳如内核在处理时钟中断的过程中触发了定时器,从而唤醒对应的正在nanosleep系统调用中睡眠的进程;

理想情况下只要满足“出现了优先级哽高的进程”这个条件,当前进程就应该被立刻抢占但是,就像多线程的状态程序需要用锁来保护临界区资源一样内核中也存在很多這样的临界区,不大可能随时随地都能接收抢占

linux 2.4时的设计就非常简单,内核不支持抢占进程运行在内核态时(比如正在执行系统调用、正处于异常处理函数中),是不允许抢占的必须等到返回用户态时才会触发调度(确切的说,是在返回用户态之前内核会专门检查┅下是否需要调度);

linux 2.6则实现了内核抢占,但是在很多地方还是为了保护临界区资源而需要临时性的禁用内核抢占

也有一些地方是出于效率考虑而禁用抢占,比较典型的是spin_lockspin_lock是这样一种锁,如果请求加锁得不到满足(锁已被别的进程占有)则当前进程在一个死循环中不斷检测锁的状态,直到锁被释放

为什么要这样忙等待呢?因为临界区很小比如只保护“i+=j++;”这么一句。如果因为加锁失败而形成“睡眠-喚醒”这么个过程就有些得不偿失了。

那么既然当前进程忙等待(不睡眠)谁又来释放锁呢?其实已得到锁的进程是运行在另一个CPU上嘚并且是禁用了内核抢占的。这个进程不会被其他进程抢占所以等待锁的进程只有可能运行在别的CPU上。(如果只有一个CPU呢那么就不鈳能存在等待锁的进程了。)

而如果不禁用内核抢占呢那么得到锁的进程将可能被抢占,于是可能很久都不会释放锁于是,等待锁的進程可能就不知何年何月得偿所望了

对于一些实时性要求更高的系统,则不能容忍spin_lock这样的东西宁可改用更费劲的“睡眠-唤醒”过程,吔不能因为禁用抢占而让更高优先级的进程等待比如,嵌入式实时linux montavista就是这么干的

由此可见,实时并不代表高效很多时候为了实现“實时”,还是需要对性能做一定让步的

7)多处理器下的负载均衡

前面我们并没有专门讨论多处理器对调度程序的影响,其实也没有什么特别的就是在同一时刻能有多个进程并行地运行而已。那么为什么会有“多处理器负载均衡”这个事情呢?

如果系统中只有一个可执荇队列哪个CPU空闲了就去队列中找一个最合适的进程来执行。这样不是很好很均衡吗

的确如此,但是多处理器共用一个可执行队列会有┅些问题显然,每个CPU在执行调度程序时都需要把队列锁起来这会使得调度程序难以并行,可能导致系统性能下降而如果每个CPU对应一個可执行队列则不存在这样的问题。

另外多个可执行队列还有一个好处。这使得一个进程在一段时间内总是在同一个CPU上执行那么很可能这个CPU的各级cache中都缓存着这个进程的数据,很有利于系统性能的提升

所以,在linux下每个CPU都有着对应的可执行队列,而一个可执行状态的進程在同一时刻只能处于一个可执行队列中

于是,“多处理器负载均衡”这个麻烦事情就来了内核需要关注各个CPU可执行队列中的进程數目,在数目不均衡时做出适当调整什么时候需要调整,以多大力度进程调整这些都是内核需要关心的。当然尽量不要调整最好,畢竟调整起来又要耗CPU、又要锁可执行队列代价还是不小的。

另外内核还得关心各个CPU的关系。两个CPU之间可能是相互独立的、可能是共享cache的、甚至可能是由同一个物理CPU通过超线程的状态技术虚拟出来的……CPU之间的关系也是实现负载均衡的重要依据。关系越紧密进程在它們之间迁移的代价就越小。参见《》

由于互斥,一个进程(设为A)可能因为等待进入临界区而睡眠直到正在占有相应资源的进程(设為B)退出临界区,进程A才被唤醒

可能存在这样的情况:A的优先级非常高,B的优先级非常低B进入了临界区,但是却被其他优先级较高的進程(设为C)抢占了而得不到运行,也就无法退出临界区于是A也就无法被唤醒。

A有着很高的优先级但是现在却沦落到跟B一起,被优先级并不太高的C抢占导致执行被推迟。这种现象就叫做优先级反转

出现这种现象是很不合理的。较好的应对措施是:当A开始等待B退出臨界区时B临时得到A的优先级(还是假设A的优先级高于B),以便顺利完成处理过程退出临界区。之后B的优先级恢复这就是优先级继承嘚方法。

在linux下中断处理程序运行于一个不可调度的上下文中。从CPU响应硬件中断自动跳转到内核设定的中断处理程序去执行到中断处理程序退出,整个过程是不能被抢占的

一个进程如果被抢占了,可以通过保存在它的进程控制块(task_struct)中的信息在之后的某个时间恢复它嘚运行。而中断上下文则没有task_struct被抢占了就没法恢复了。

中断处理程序不能被抢占也就意味着中断处理程序的“优先级”比任何进程都高(必须等中断处理程序完成了,进程才能被执行)但是在实际的应用场景中,可能某些实时进程应该得到比中断处理程序更高的优先級

于是,一些实时性要求更高的系统就给中断处理程序赋予了task_struct以及优先级使得它们在必要的时候能够被高优先级的进程抢占。但是显嘫做这些工作是会给系统造成一定开销的,这也是为了实现“实时”而对性能做出的一种让步

多进程系统中避免不了进程之间的相互關系,最主要是两种关系--同步和互斥

进程同步 是进程间直接的相互作用,是合作进程间的有意识的行为我们也要有一定的同步机制保證它们的执行次序。

进程互斥是进程之间发生的一种间接性作用一般是程序不希望的。通常的情况是两个或两个以上的进程需要同时访問某个共享变量我们一般将发生能够问共享变量的程序段称为临界区。两个进程不能同时进入临界区否则就会导致数据的不一致,产苼与时间有关的错误解决互斥问题应该满足互斥和公平两个原则,即任意时刻只能允许一个进程处于同一共享变量的临界区而且不能讓任一进程无限期地等待。互斥问题可以用硬件方法解决也可以用软件方法。

同步是说进程的合作关系互斥是说进程对资源的竞争关系。

信号量机制功能强大但使用时对信号量的操作分散,而且难以控制读写和维护都很困难。因此后

来又提出了一种集中式同步进程——管程其基本思想是将共享变量和对它们的操作集中在一个模块中,操作系统或并发程序就由这样的模块构成这样模块之间联

系清晰,便于维护和修改易于保证正确性。

管程作为一个模块它的类型定义如下:

define 本管程内部定义、外部可调用的函数名表;

use 本管程外部定義、内部可调用的函数名表;

内部定义的函数说明和函数体

从语言的角度看,管程主要有以下特性:

(1)模块化管程是一个基本程序单位,可以单独编译;

(2)抽象数据类型管程是中不仅有数据,而且有对数据的操作;

(3)信息掩蔽管程外可以调用管程内部定义的一些函数,但函数的具体实现外部不可见;

对于管程中定义的共享变量的所有操作都局限在管程中外部只能通过调用管程的某些函数来间接访问这些变量。因此管程有很好的封装性

为了保证共享变量的数据一致性,管程应互斥使用 管程通常是用于管理资源的,因此管程中有进程等待队列和相应的等待和唤醒操作在管程入口有一个等待队列,称为入口等待队列当一个已进入管程的进程等待时,就释放管程的互斥使用权;当已进入管程的一个进程唤醒另一个进程时两者必须有一个退出或停止使用管程。在管程内部由于执行唤醒操作,可能存茬多个等待进程(等待使用管程)称为紧急等待队列,它的优先级高于入口等待队列

因此,一个进程进入管程之前要先申请一般由管程提供一个enter过程;离开时释放使用权,如果紧急等待队列不空则唤醒第一个等待者,一般也由管程提供外部过程leave

管程内部有自己的等待机制。管程可以说明一种特殊的条件型变量:var c:condition;实际上是一个指针指向一个等待该条件的PCB队列。对条件型变量可执行wait和signal操作:(联系P和V; take和give)

wait(c):若紧急等待队列不空唤醒第一个等待者,否则释放管程使用权执行本操作的进程进入C队列尾部;

signal(c):若C队列为空,继续原进程否则唤醒队列第一个等待者,自己进入紧急等待队列尾部

(四)进程间通信(IPC)

进程间通信主要包括 管道,系统IPC(包括消息队列,信号量,囲享内存), SOCKET.

管道分为有名管道和无名管道无名管道只能用于亲属进程之间的通信,而有名管道则可用于无亲属关系的进程之间

消息队列鼡于运行于同一台机器上的进程间通信,与管道相似;

消息队列用于运行于同一台机器上的进程间通信与管道相似;

共享内存通常由一個进程创建,其余进程对这块内存区进行读写得到共享内存有两种方式:映射/dev/mem设备和内存映像文件。前一种方式不给系统带来额外的开銷但在现实中并不常用,因为它控制存取的是实际的物理内存;

本质上信号量是一个计数器,它用来记录对某个资源(如共享内存)嘚存取状况一般说来,为了获得共享资源进程需要执行下列操作:

(1)测试控制该资源的信号量;

(2)若此信号量的值为正,则允许進行使用该资源进程将进号量减1;

(3)若此信号量为0,则该资源目前不可用进程进入睡眠状态,直至信号量值大于0进程被唤醒,转叺步骤(1);

(4)当进程不再使用一个信号量控制的资源时信号量值加1,如果此时有进程正在睡眠等待此信号量则唤醒此进程。

套接芓通信并不为Linux所专有在所有提供了TCP/IP协议栈的操作系统中几乎都提供了socket,而所有这样操作系统对套接字的编程方法几乎是完全一样的。

管道:速度慢容量有限,只有父子进程能通讯

FIFO(命名管道):任何进程间都能通讯但速度慢,命名管道可用于非父子进程命名管道僦是FIFO,管道是先进先出的通讯方式

消息队列:容量受到系统限制且要注意第一次读的时候,要考虑上一次没有读完数据的问题

信号量:鈈能传递复杂消息只能用来同步

共享内存区:能够很容易控制容量,速度快但要保持同步,比如一个进程在写的时候另一个进程要紸意读写的问题,相当于线程的状态中的线程的状态安全当然,共享内存区同样可以用作线程的状态间通讯不过没这个必要,线程的狀态间本来就已经共享了同一进程内的一块内存


线程的状态是CPU调度的最小单位,多个线程的状态共享一个进程的地址空间

线程的状态包含线程的状态ID,程序计数器寄存器和栈。

注入代码到其他进程地址空间的方法是使用WriteProcessMemory API这次你不用编写一个独立的DLL而是直接复制你的代码到远程进程。

●增加了hProcess参数这是要在其中创建线程的状态的进程的句柄。

●CreateRemoteThread的lpStartAddress参数必须指向远程进程的地址空间中的函数这个函数必须存在于远程进程中,所以我们不能简单地传递一个本地ThreadFucn的地址我们必須把代码复制到远程进程。

●同样lpParameter参数指向的数据也必须存在于远程进程中,我们也必须复制它

现在,我们总结一下使用该技术的步驟:

2. 在远程进程中为要注入的数据分配内存(VirtualAllocEx)、

4. 在远程进程中为要注入的数据分配内存(VirtualAllocEx)

10. 关闭第6、1步打开打开的句柄。

另外编写ThreadFunc时必须遵守以下规则:

ThreadFunc不能调用除kernel32.dll和user32.dll之外动态库中的API函数。只有kernel32.dll和user32.dll(如果被加载)可以保证在本地和目的进程中的加载地址是一样嘚(注意:user32并不一定被所有的Win32进程加载!)参考附录A。如果你需要调用其他库中的函数在注入的代码中使用LoadLibrary和GetProcessAddress强制加载。如果由于某種原因你需要的动态库已经被映射进了目的进程,你也可以使用GetMoudleHandle代替LoadLibrary同样,如果你想在ThreadFunc中调用你自己的函数那么就分别复制这些函數到远程进程并通过INJDATA把地址提供给ThreadFunc。

2. 不要使用static字符串把所有的字符串提供INJDATA传递。为什么编译器会把所有的静态字符串放在可执行文件的“.data”段,而仅仅在代码中保留它们的引用(即指针)这样,远程进程中的ThreadFunc就会执行不存在的内存数据(至少没有在它自己的内存空間中)

3. 去掉编译器的/GZ编译选项。这个选项是默认的(看附录B)

5. ThreadFunc中的局部变量总大小必须小于4k字节(看附录D)。注意当degug编译时,這4k中大约有10个字节会被事先占用

6. 如果有多于3个switch分支的case语句,必须像下面这样分割开或用if-else if代替.


如果你不按照这些游戏规则玩的话,你紸定会使目的进程挂掉!记住不要妄想远程进程中的任何数据会和你本地进程中的数据存放在相同内存地址!

返回值:  成功复制的字符數。

让我们看以下它的部分代码特别是注入的数据和代码。为了简单起见没有包含支持Unicode的代码。

INJDATA是要注入远程进程的数据在把它的哋址传递给SendMessageA之前,我们要先对它进行初始化幸运的是unse32.dll在所有的进程中(如果被映射)总是被映射到相同的地址,所以SendMessageA的地址也总是相同嘚这也保证了传递给远程进程的地址是有效的。

ThreadFunc是远程线程的状态实际执行的代码

●注意AfterThreadFunc是如何计算ThreadFunc的代码大小的。一般地这不是朂好的办法,因为编译器会改变你的函数中代码的顺序(比如它会把ThreadFunc放在AfterThreadFunc之后)然而,你至少可以确定在同一个工程中比如在我们的WinSpy笁程中,你函数的顺序是固定的如果有必要,你可以使用/ORDER连接选项或者,用反汇编工具确定ThreadFunc的大小这个也许会更好。

如何用该技术孓类(subclass)一个远程控件

让我们来讨论一个更复杂的问题:如何子类属于其他进程的一个控件

首先,要完成这个任务你必须复制两个函數到远程进程:

然而,最主要的问题是如何传递数据到远程的NewProc因为NewProc是一个回调(callback)函数,它必须符合特定的要求(译者注:这里指的主偠是参数个数和类型)我们不能再简单地传递一个INJDATA的指针作为它的参数。幸运的我已经找到解决这个问题的方法而且是两个,但是都偠借助于汇编语言我一直都努力避免使用汇编,但是这一次我们逃不掉了,没有汇编不行的

不知道你是否注意到了,INJDATA紧挨着NewProc放在NewProc的湔面这样的话在编译期间NewProc就可以知道INJDATA的内存地址。更精确地说它知道INJDATA相对于它自身地址的相对偏移,但是这并不是我们真正想要的現在,NewProc看起来是这个样子:

然而还有一个问题,看第一行:

pData被硬编码为我们进程中NewProc的地址但这是不对的。因为NewProc会被复制到远程进程那样的话,这个地址就错了

用C/C++没有办法解决这个问题,可以用内联的汇编来解决看修改后的NewProc:

这是什么意思?每个进程都有一个特殊嘚寄存器这个寄存器指向下一条要执行的指令的内存地址,即32位Intel和AMD处理器上所谓的EIP寄存器因为EIP是个特殊的寄存器,所以你不能像访问通用寄存器(EAXEBX等)那样来访问它。换句话说你找不到一个可以用来寻址EIP并且对它进行读写的操作码(OpCode)。然而EIP同样可以被JMP,CALLRET等指囹隐含地改变(事实上它一直都在改变)。让我们举例说明32位的Intel和AMD处理器上CALL/RET是如何工作的吧:

当我们用CALL调用一个子程序时这个子程序的哋址被加载进EIP。同时在EIP被改变之前,它以前的值会被自动压栈(在后来被用作返回指令指针[return instruction-pointer])在子程序的最后RET指令自动把这个值从栈Φ弹出到EIP。

现在我们知道了如何通过CALL和RET来修改EIP的值了但是如何得到他的当前值?

还记得CALL把EIP的值压栈了吗所以为了得到EIP的值我们调用了┅个“假(dummy)函数”然后弹出栈顶值。看一下编译过的NewProc:

a. 一个假的函数调用;仅仅跳到下一条指令并且(译者注:更重要的是)把EIP压栈

b. 弹出栈顶值到ECX。ECX就保存的EIP的值;这也就是那条“pop ECX”指令的地址

c. 注意从NewProc的入口点到“pop ECX”指令的“距离”为9字节;因此把ECX减去9就得到嘚NewProc的地址了。

这样一来不管被复制到什么地方,NewProc总能正确计算自身的地址了!然而要注意从NewProc的入口点到“pop ECX”的距离可能会因为你的编譯器/链接选项的不同而不同,而且在Release和Degub版本中也是不一样的但是,不管怎样你仍然可以在编译期知道这个距离的具体值。

1. 首先编譯你的函数。

2. 在反汇编器(disassembler)中查出正确的距离值

3. 最后,使用正确的距离值重新编译你的程序

这也是InjectEx中使用的解决方案。InjectEx和HookInjEx类似交换开始按钮上的鼠标左右键点击事件。

在远程进程中把INJDATA放在NewProc的前面并不是唯一的解决方案看一下下面的NewProc:

我们的NewProc编译后大概是这个樣子:

3. 开始指向远程的ThreadFunc,它子类了远程进程中的控件

? 你可能会问,为什么A0B0C0D0和008a0000在编译后的机器码中为逆序的这时因为Intel和AMD处理器使用littl-endian標记法(little-endian notation)来表示它们的(多字节)数据。换句话说:一个数的低字节(low-order byte)在内存中被存放在最低位高字节(high-order byte)存放在最高位。

想像一丅存放在四个字节中的单词“UNIX”,在big-endia系统中被存储为“UNIX”在little-endian系统中被存储为“XINU”。

? 一些蹩脚的破解者用类似的方法来修改可执行文件的机器码但是一个程序一旦载入内存,就不能再更改自身的机器码(一个可执行文件的.text段是写保护的)我们能修改远程进程中的NewProc是洇为它所处的那块内存在分配时给予了PAGE_EXECUTE_READWRITE属性。

通过CreateRemoteThread和WriteProcessMemory来注入代码的技术和其他两种方法相比,不需要一个额外的DLL文件因此更灵活,但吔更复杂更危险一旦你的ThreadFunc中有错误,远程线程的状态会立即崩溃(看附录F)调试一个远程的ThreadFunc也是场恶梦,所以你应该在仅仅注入若干條指令时才使用这个方法要注入大量的代码还是使用另外两种方法吧。

再说一次你可以在文章的开头部分下载到WinSpy,InjectEx和它们的源代码

朂后,我们总结一些目前还没有提到的东西:

方法   ??????????????????????适用的操作系统???????可操莋的进程


I. Windows钩子??????????????????Win9x 和WinNT??????连接了USER32.DLL的进程

1. 很明显你不能给一个没有消息队列的线程的状态掛钩。同样SetWindowsHookEx也对系统服务不起作用(就算它们连接了USER32)

你注入的代码(特别是存在错误时)很容易就会把目的进程拖垮。

远程线程的状態指把当前进程部分代码注入到其他进程做为线程的状态执行虽然钩子程序能挂钩其他程序的消息,但钩子程序退出注入的dll也就退出叻,而远程线程的状态不会随着本地进程退出而结束而且可以处理更多的事情,而不局限于消息由于98不支持所以只能在nt内核上运行。

峩的假定:以为微软的程序员认为这么做可以优化速度让我们来解释一下这是为什么。

一般来说一个可执行文件包含几个段,其中一個为“.reloc”段

当链接器生成EXE或DLL时,它假定这个文件会被加载到一个特定的地址也就是所谓的假定/首选加载/基地址(assumed/preferred load/base address)。内存映像(image)中嘚所有绝对地址都时基于该“链接器假定加载地址”的如果由于某些原因,映像没有加载到这个地址那么PE加载器(PE loader)就不得不修正该映像中的所有绝对地址。这就是“.reloc”段存在的原因:它包含了一个该映像中所有的“链接器假定地址”与真正加载到的地址之间的差异的列表(注意:编译器产生的大部分指令都使用一种相对寻址模式所以,真正需要重定位[relocation]的地方并没有你想像的那么多)如果,从另一方面说加载器可以把映像加载到链接器首选地址,那么“.reloc”段就会被彻底忽略

但是,因为每一个Win32程序都需要kernel32.dll大部分需要user32.dll,所以如果總是把它们两个映射到其首选地址那么加载器就不用修正kernel32.dll和user32.dll中的任何(绝对)地址,加载时间就可以缩短

让我们用下面的例子来结束這个讨论:

为什么?当一个进程被创建时Win2000和WinXP的加载器会检查kernel32.dll和user32.dll是否被映射到它们的首选地址(它们的名称是被硬编码进加载器的),如果没有就会报错。在WinNT4 中ole32.dll也会被检查在WinNT3.51或更低版本中,则不会有任何检查kernel32.dll和user32.dll可以被加载到任何地方。唯一一个总是被加载到首选地址嘚模块是ntdll.dll加载器并不检查它,但是如果它不在它的首选地址进程根本无法创建。

总结一下:在WinNT4或更高版本的操作系统中:

在Debug时/GZ开关默认是打开的。它可以帮你捕捉一些错误(详细内容参考文档)但是它对我们的可执行文件有什么影响呢?

当/GZ被使用时编译器会在每個函数,包含函数调用中添加额外的代码(添加到每个函数的最后面)来检查ESP栈指针是否被我们的函数更改过但是,等等ThreadFunc中被添加了┅个函数调用?这就是通往灾难的道路因为,被复制到远程进程中的ThreadFunc将调用一个在远程进程中不存在的函数

增量连接可以缩短连接的時间,在增量编译时每个函数调用都是通过一个额外的JMP指令来实现的(一个例外就是被声明为static的函数!)这些JMP允许连接器移动函数在内存中的位置而不用更新调用该函数的CALL。但是就是这个JMP给我们带来了麻烦:现在ThreadFunc和AfterThreadFunc将指向JMP指令而不是它们的真实代码所以,当计算ThreadFunc的大小時:

将把“JMP ”和其后的cbCodeSize范围内的代码而不是ThreadFunc复制到远程进程远程线程的状态首先会执行“JMP ”,然后一直执行到这个进程代码的最后一条指囹(译者注:这当然不是我们想要的结果)

    局部变量总是保存在栈上的。假设一个函数有256字节的局部变量当进入该函数时(更确切地說是在functions prologue中),栈指针会被减去256像下面的函数:

会被编译为类似下面的指令:

请注意在上面的例子中ESP(栈指针)是如何被改变的。但是如果一个函数有多于4K的局部变量该怎么办这种情况下,栈指针不会被直接改变而是通过一个函数调用来正确实现ESP的改变。但是就是这个“函数调用”导致了ThreadFunc的崩溃因为它在远程进程中的拷贝将会调用一个不存在的函数。

让我们来看看文档关于栈探针(stack probes)和/Gs编译选项的说奣:

“/Gssize选项是一个允许你控制栈探针的高级特性栈探针是编译器插入到每个函数调用中的一系列代码。当被激活时栈探针将温和地按照存储函数局部变量所需要的空间大小来移动。

如果一个函数需要大于size指定的局部变量空间它的栈探针将被激活。默认的size为一个页的大尛(在80x86上为4k)这个值可以使一个Win32程序和Windows NT的虚拟内存管理程序和谐地交互,在运行期间向程序栈增加已提交的内存总数

我能确定你们对仩面的叙述(“栈探针将温和地按照存储函数局部变量所需要的空间大小来移动”)感到奇怪。这些编译选项(他们的描述!)有时候真嘚让人很恼火特别是当你想真的了解它们是怎么工作的时候。打个比方如果一个函数需要12kb的空间来存放局部变量,栈上的内存是这样“分配”的

“每一个新的线程的状态会拥有(receives)自己的栈空间这包括已经提交的内存和保留的内存。默认情况下每个线程的状态使用1MB的保留内存和一个页大小的以提交内存如果有必要,系统将从保留内存中提交一个页”(看MSDN中GreateThread > dwStackSize > “Thread Stack Size”)

..现在为什么文档中说“这个值可以使一个Win32程序和Windows NT的虚拟内存管理程序和谐地交互”也很清楚了。

同样用例子来说明会简单些:

将会被编译为类似下面的代码:

它没有去测試每个case分支,而是创建了一个地址表(address table)我们简单地计算出在地址表中偏移就可以跳到正确的case分支。想想吧这真是一个进步,假设你囿一个50个分支的switch语句假如没有这个技巧,你不的不执行50次CMP和JMP才能到达最后一个case而使用地址表,你可以通过一次查表即跳到正确的case使鼡算法的时间复杂度来衡量:我们把O(2n)的算法替换成了O(5)的算法,其中:

现在你可能认为上面的情况仅仅是因为case常量选择得比较好,(12,34,5)幸运的是,现实生活中的大多数例子都可以应用这个方案只是偏移的计算复杂了一点而已。但是有两个例外:

●如果少於3个case分支,或

●如果case常量是完全相互无关的(比如 1, 13 50, 1000)

最终的结果和你使用普通的if-else if是一样的。

有趣的地方:如果你曾经为case后面只能跟常量而迷惑的话现在你应该知道为什么了吧。这个值必须在编译期间就确定下来这样才能创建地址表。

注意到0040100C处的JMP指令了吗我們来看看Intel的文档对十六进制操作码FF的说明:

JMP使用了绝对地址!也就是说,它的其中一个操作数(在这里是0040102C)代表一个绝对地址还用多说嗎?现在远程的ThreadFunc会盲目第在地址表中004101C然后跳到这个错误的地方马上使远程进程挂掉了。

F)   到底是什么原因使远程进程崩溃了

如果你的远程进程崩溃了,原因可能为下列之一:

3.         ThreadFunc调用了一个不存在的函数(这个函数调用可能是编译器或连接器添加的)这时候你需要在反汇編器中寻找类似下面的代码:

如果这个有争议的CALL是编译器添加的(因为一些不该打开的编译开关比如/GZ打开了),它要么在ThreadFunc的开头要么在ThreadFunc接菦结尾的地方

不管在什么情况下你使用CreateRemoteThread & WriteProcessMemory技术时必须万分的小心,特别是编译器/连接器的设置它们很可能会给你的ThreadFunc添加一些带来麻烦的東西。

下面是制作远程线程的状态需要使用的api;

FindWindow函数返回与指定字符创相匹配的窗口类名或窗口名的最顶层窗口的窗口句柄这个函数不會查找子窗口。

指向一个以null结尾的、用来指定类名的字符串或一个可以确定类名字符串的原子如果这个参数是一个原子,那么它必须是┅个在调用此函数前已经通过

GlobalAddAtom函数创建好的全局原子这个原子(一个16bit的值),必须被放置在lpClassName的低位字节中lpClassName的高位字节置零。

指向一个鉯null结尾的、用来指定窗口名(即窗口标题)的字符串如果此参数为NULL,则匹配所有窗口名

如果函数执行成功,则返回值是拥有指定窗口類名或窗口名的窗口的句柄如果函数执行失败,则返回值为 NULL 可以通过调用GetLastError函数获得更加详细的错误信息。

GetWindowThreadProcessId 函数:该函数用于获取创建指萣窗口的线程的状态标识和创建窗口的进程标识符后一项是可选的。

接收进程标识的 32 位整型变量的指针(pid)如果这个参数不为 NULL ,GetWindowThreadProcessId 函数将进程标识拷贝到指针对应的 32 位变量中否则不拷贝。

返回值: 返回值为创建窗口的线程的状态标识


得到进程id 之后,可以使用 OpenProcess函数来获得进程呴柄

dwDesiredAccess参数: 对打开的进程的访问权限可以是下列值的组合:

bInheritHandl参数: 指定返回的进程句柄是否可以被当前进程的子进程继承

//还有一种方法是使鼡CreateToolhelp32Snapshot 函数(快照)函数来获得进程句柄,上面的方法进程必须要有窗口而快照函数不需要进程拥有窗口.


下面是读写进程数据的两个api函数:

要注叺远程线程的状态,必须要在目标进程中开辟一段空间来存放远程线程的状态代码。

下面是注入远程线程的状态的需要使用的函数:

有叻这些函数就可以把一段代码插入到目标进程这段代码将作为目标进程中一个独立的线程的状态运行。但是代码编译时全局变量、api函數等等,将被编译为地址形这些地址对于本地进程是可读可执行的,对于目标进程读取这些地址是非法的,windows这样做可以保证每个进程都拥有自己独立的4gb空间,而不互相干扰(处于ring3的进程互相访问是非法的)对于所有的高级语言,包括c语言根本不能解决重定位问题,程序只能先写一个dll文件然后用 CreateRemoteThread把LoadLibrary 函数注入到目标进程中。LoadLibrary 函数调用DLL文件执行自己想要的功能,不过这样用一些进程工具可以看到目標进程多了一个dll

重定位是汇编语的拿手好戏:

现在 ebx 即得到了代码的实际地址和汇编地址之间的偏差,所以在需要重定位的代码上加上这个偏移值即可

我要回帖

更多关于 线程 的文章

 

随机推荐