【Windows】windows核心编程整理(下)
线程的堆栈
每当创建一个线程时,系统就会为线程的堆栈(每个线程有他自己的堆栈)保留一个堆栈空间区域,并将一些物理存储器提交给这个以保留的区域
当创建一个线程的堆栈时,系统将会保留一个链接程序的/STACK开关指明的地址空间区域
堆栈区域的最后一个页面始终被保留这,这样做的目地是为了防止不小心改写进程使用的其他数据
内存映射文件
与虚拟内存一样,内存映射文件可以用来保留一个地址空间的区域,并将物理存储器提交给该区域,他们之间的差别是,物理存储器来自一个已经位于磁盘上的文件,而不是系统的页文件,一旦该文件被映射,就可以访问他,就像整个文件已经加载内存一样
内存映射文件可以方便加载和执行.exe和dll文件,访问磁盘上的数据文件,不必对文件执行I/O操作,并且可以不必对文件内容进行缓存,可以使同一台计算机上运行的多个线程能够相互之间共享数据
当所有的.exe和DLL文件都被映射到进程的地址空间之后,系统就可以开始执行.exe文件的启动代码
全局数据和静态数据不能被同一个.exe或DLL文件的多个映像共享,这是个安全的默认设置
每个.exe或DLL文件的映像都是由许多节组成的,按照规定,每个标准节的名字均以圆点开头
之所有将变量放入他们自己的节中,最常见的原因也许是要在.exe或DLL文件的多个映像之间共享这些变量
仅仅告诉编译器将某些变量放入他们自己的节中,是不足以实现对这些变量的共享的,还必须告诉链接程序,某个节中的变量是需要加以共享的,若要进行这项操作,可以使用链接程序的命令行上的/section开关
操作系统使得内存能够将一个数据文件映射到进程的地址空间中,因此,对大量的数据进行操作是非常方便的
创建内存映射文件就像保留一个地址空间区域然后将物理存储器提交给该区域一样
不必一次性地映射整个文件,可以之将文件的一小部分映射到地址空间,被映射到进程的地址空间的这部分文件称为一个视图
当对内存映射文件进行操作时,通常要打开文件,创建文件映射对象,然后使用文件映射对象将文件的数据视图映射到进程的地址空间
系统允许你映射一个文件的相同数据的多个视图
WINDOWS不能保证这些不同的文件映射对象的视图具有相关性。他只能保证单个文化映射对象的多个视图具有相关性
只读文件不存在相关性的问题,因此他们可以作为很好的内存映射文件。内存映射文件绝不能应用于共享网络上的可写文件,因为系统无法保证数据视图的相关性
在链接表中,每个节点或元素均包含列表中的另一个元素的内存地址
在单个计算机上共享数据的最低层机制是内存映射文件
数据共享方法是通过让两个或多个进程映射同一个文件映射对象的视图来实现的,这意味这他们将共享物理存储器的同一个页面
注意:如果多个进程共享单个文件映射对象,那么所有进程必须使用相同的名字来表示该文件映射对象
当创建了文件映射对象并且将他的一个视图映射到进程的地址空间之后,就可以像使用任何内存区域那样使用他
堆栈
堆栈可以用来分配许多较小的数据块
堆栈的有点是,可以不考虑分配粒度和页面边界之类的问题,集中精力处理手头的任务。堆栈的缺点是,分配和释放内存块的速度比其他机制要慢,并且无法直接控制物理存储器的提交和回收
堆栈是保留的地址空间的一个区域
物理存储器总是从系统的页文件中分配的
许多WINDOWS函数要求进程使用其默认堆栈
系统必须保证在规定的时间内,每次只有一个线程能够分配和释放默认堆栈中的内存块
单个进程可以同时拥有若干个堆栈
每个堆栈均用他自己的堆栈句柄来标识,用于分配和释放堆栈中的内存块的所有堆栈函数都需要这个堆栈句柄作为其参数
通过在堆栈中分配同样大小的对象,就可以更加有效地管理堆栈
在进程完全终止运行之前,系统部允许进程的默认堆栈被撤销
DLL基础
动态链接库(DLL)一直是WINDOWS操作系统的基础,WINDOWS API中的所有函数都包含在DLL中
dll能够动态地装入进程的地址空间,因此应用程序能够在运行时确定需要执行什么操作,然后装入相应的代码,以便根据需要执行这些操作
dll可以用许多种编程语言来编写
对于一个dll来说,你必须设定该链接程序的/dll开关
在应用程序(或另一个DLL)能够调用DLL中的函数前,DLL文件映像必须映射到调用线程的地址空间中
一旦DLL的文件映像被映射到调用进程的地址空间中,DLL的函数就可以供进程中运行的所有线程使用
系统并不跟踪DLL中的函数保留该区域的情况
可执行文件的全局变量和静态变量不能被同一个可执行文件的多个运行实例共享
单个地址空间是由一个可执行模块和若干个dll模块组成的
创建DLL模块的步骤:(见文档)
一旦DLl和可执行模块创建完成,一个进程就可以执行
运行可执行模块时操作系统的加载程序将执行的操作步骤:(见文档)
一旦可执行模块和所有DLL模块被映射到进程的地址空间中,进程的主线程就可以启动运行,同时应用程序也可以启动运行
相对虚拟地址(RVA)
当创建可执行源代码文件时,必须加上DLL的头文件
当一个可执行文件被启动时,操作系统加载程序将为该进程创建虚拟地址空间,然后,加载程序将可执行模块映射到进程的地址空间中
动态链接完成,进程的主线程就开始执行,应用程序也就开始运行了
DLL的高级操作技术
无论何时,进程中的线程都可以决定将一个DLL映射到进程的地址空间
系统为每个进程维护了一个DLl的使用计数,就是说如果两个进程中的两个线程同时调用一个函数,这个DLL将被映射到两个线程的地址空间,这样,两个进程的DLL使用计数都是1
一旦DLL模块被显示加载,线程就必须获取他要引用的符号的地址
强调:Microsoft非常反对使用序号
当一个新线程创建时,系统将分配进程的地址空间,然后将.exe文件映像和所有需要的DLL文件映像映射到进程的地址空间中
DLL能够阻止进程终止运行
系统是顺序调用DLL的DLlmain函数的
延迟加载的DLL能够更容易地完成分开加载各个DLL
函数转发器是DLL的输出节中的一个项目,用于将对一个函数的调用转至另一个DLL中的另一个函数
操作系统提供的某些DLL得到了特殊的处理,这些DLL称为已知的DLL。他们与其他DLL基本相同,但是操作系统总是在同一个目录中查找他们,以便对它们进行加载操作
为了强制加载程序总是首先查找应用程序的目录,要做的工作就是在应用程序的目录中放入一个文件,该文件的内容可以忽略,但是该文件必须称为appname.lacal
每个可执行模块和DLL模块都有一个首选的基地址,用于标识模块应该映射到的进程地址空间中的理想内存地址
可以创建一个不包含移位节的可执行模块或DLL模块,当创建该模块时,你可以将/FIXED开关传递给链接程序。使用这个开关,能够使模块变得比较小一些,但是这意味着模块不能被改变位置。如果模块不能加载到他的首选基地址,那么他就根本无法加载
始终都应该从高位内存地址开始加载DLL,然后逐步向下加载到低位内存地址,以减少地址空间中出现的碎片
首选基地址必须始终从分配粒度边界开始
模块的移位是非常重要的,他能够大大提高整个系统的运行性能
可以将模块绑定起来,使应用程序能够更快的进行初始化,并且使用较少的存储器,绑定一个模块时,可以为该模块的输入节配备所有的输入符号的虚拟地址
当进程初始化时,需要的DLL实际上加载到了他们的首选基地址中
插入DLL和挂接API
每个进程都有他的自己的私有地址空间。当使用指针来引用内存时,指针的值将引用你自己进程的地址空间中的一个内存地址。你的进程不能创建一个其引用属于另一个进程的内地指针
独立的地址空间对于编程人员和用户来说都是非常有利的。对于编程人员来说,系统更容易捕获随意的内存读取和写入操作
你的DLl只会映射到使用user32.dll的进程中。所有基于GUI的应用程序均使用use32.dll,不过大多数基于CUI的应用程序并不使用他
可以使用挂钩将DLL插入进程的地址空间
函数转发器是你能够非常容易地挂接某些函数,但是应该避免使用这种方法,因为他不具备版本升级的能力
调试程序能够对被调试的进程执行特殊的操作,调试程序可以强制将某些代码插入被调试进程的地址空间中,然后使被调试进程的主线程执行该代码
将DLL插入进程的地址空间是确定进程运行状况的一种很好的方法,但是仅仅插入DLL无法提供足够的信息,人们常常需要知道某个进程中的线程究竟是如何调用各个函数的,也可能需要修改WINDOWS函数的功能
挂钩函数就是通常说的替换函数
模块的输入节包含一组该模块运行时需要的DLL,另外,他还包含模块从每个DLL输入的符号列表。当模块调用一个输入函数时,线程时间上要从模块的输入节中捕获需要的输入函数的地址,然后转移到该地址
硬件输入模型和局部输入状态
微软设计输入模型的一个主要目标就是为了保证一个线程的动作不要对其他线程的动作产生不好的影响
当系统初始化时,要建立一个特殊的线程,即原始输入线程
RIT:原始输入线程
RIT只是确定是哪一个窗口在鼠标光标之下
在任何给定的时刻,只有一个线程同RIT连接,这个线程称为前景线程,因为他建立了正在与用户交互的窗口,并且这个线程的窗口相对于其他线程所建立的窗口来说处在画面中的前景
当一个用户在系统上登录时,WINDOWS explorer进程让一个线程建立相应的任务栏和桌面。这个线程连接到RIT
RIT还要负责处理特殊的键组合
当用户按了某个特殊的键组合时,RIT激活选定的窗口,并将窗口的线程连接到RIT,WINDOWS也提供激活窗口的功能,是窗口的线程连接到RIT
RIT使用户的键盘输入流向一个线程的虚拟输入队列,而不是流向一个窗口。RIT将键盘事件放入线程的虚拟输入队列时不用涉及具体的窗口
关于键盘管理和局部输入状态,其他的内容是同步键状态数组。每个线程的局部输入状态变量都包含一个同步键状态数组,但所有的线程要共享一个同步键状态数组。这些数组反映了在任何给定时刻所有键的状态
如果用户想激活一个其他线程所建立的窗口,系统自动向设置捕获的线程发送鼠标按钮按下和鼠标按钮放开的消息。然后系统更新线程的局部输入状态变量,指出该线程不再具有鼠标捕获
因为每个线程都有自己的局部输入状态环境,并且在必要时每个线程可以连接到RIT或从RIT断开
当将两个线程的输入都挂接在一起时,就使线程共享单一的虚拟输入队列和同一组局部输入状态变量,但线程仍然使用自己的等级消息队列、发送消息队列、应答消息队列和唤醒标志
窗口消息
当一个线程调用一个函数来建立某个对象时,则该对象就归这个线程的进程所有了
建立窗口的线程必须是为窗口处理所有消息的线程
要保证每个线程运行在一个环境中,在这个环境中每个线程都相信自己是唯一运行的线程。更确切地说,每个线程必须有完全不受其他线程影响的消息队列。而且,每个线程必须有一个模拟环境,使线程可以维持他自己的键盘焦点、窗口激活、鼠标捕获等概念
当线程有了与之相联系的threadinfo结构时,线程就有了自己的消息队列集合
发送线程不是运行在接受进程的地址空间中,因此不能访问相应窗口过程的代码和数据,实际上,发送线程要挂起,而由另外的线程处理消息
当接收线程等待消息时,系统从发送消息队列中取出第一个消息并调用适当的窗口过程来处理消息
在发送的消息处理之后,窗口过程的返回值被登记到发送线程的应答消息队列中,发送线程现在被唤醒,取出包含在应答消息队列中的返回值,这个返回值就是调用sendmessage的返回值,这时,发送线程继续正常执行
处理线程之间发送消息的方法可以造成线程挂起(hang)
一个线程在等待发送的消息返回时可以被中断,以便处理另一个发送来的消息,可以阻止
发送给窗口的大多数消息是用于通知的目地,也就是,发送消息是因为窗口需要知道某个窗台已经发生变化,在程序能够继续执行之前,窗口要做某种处理
有时候,你可能想知道究竟是在处理线程间的消息发送,还是在处理线程内的消息发送
当一个线程调用getmessage或waitmessage,但是没有对这个线程或这个线程所建立窗口的消息时,系统可以挂起这个线程,这样就不再给它分配CPU时间
当一个线程调用getmessage或peekmessage时,系统必须检查线程的队列状态标志的情况,并确定应该处理哪个消息
当一个进程要向另一个进程的窗口发送一些数据时,必须先初始化copydatastruct结构
当你注册一个新的窗口类时,必须将负责为这个类处理消息的窗口过程的地址告诉系统
告诉系统一个窗口过程是要求ansi字符串还是unicode字符串,实际上取决于注册窗口类时所使用的函数
本文出自 “成鹏致远” 博客,请务必保留此出处http://infohacker.blog.51cto.com/6751239/1171414