??xml version="1.0" encoding="utf-8" standalone="yes"?> Z提高E序的可L、可重用性等Q逐渐出现了将E序开发中l常用到的相同的功能Q比如数学函数运、字W串操作{,独立出来~写成函敎ͼ然后按照怺关系或应用领域汇集在相同的文仉Q这些文件构成了函数?/span>?/span> 函数库是一U对信息的封装,常用的函数装hQh们不必知道如何实现它们。只需要了解如何调用它们即可。函数库可以被多个应用程序共享,在具体编E环境中Q一般都有一个头文g怼Q在q个头文件中以标准的方式定义了库中每个函数的接口Q根据这些接口Ş式可以在E序中的M地方调用所需的函数?/span> ׃函数、库、模块等一pd概念和技术的出现Q程序设计逐渐变成如图所C的风格。程序被分解成一个个函数模块Q其中既有系l函敎ͼ也有用户定义的函数。通过对函数的调用Q程序的q行逐步被展开?/span>阅读E序Ӟ׃每一块的功能相对独立Q因此对E序l构的理解相对容易,在一定程度上~解了程序代码可L和可重用g的矛盾,但ƈ未彻底解决矛盾。随着计算机程序的规模来大Q这个问题变得更加尖锐,于是出现了另一U编E风?/span>—?/font>l构化程序设?/span>?/span> 在结构化E序设计中,ME序D늚~写都基?/span>3U结构:分支l构、@环结构和序l构?/span>E序h明显的模块化特征Q每个程序模块具有惟一的出口和入口语句。结构化E序的结构简单清晎ͼ模块化强Q描q方式脓qh们习惯的推理式思维方式。因此可L强Q在软g重用性、Y件维护等斚w都有所q步Q在大型软g开发尤其是大型U学与工E运Y件的开发中发挥了重要作用。因此到目前为止Q仍有许多应用程序的开发采用结构化E序设计技术和Ҏ。即使在目前行的面向对象Y件开发中也不能完全脱ȝ构化E序设计?/span> 面向对象的程序役计方法是E序设计的一U新Ҏ。所有面向对象的E序设计语言一般都含有三个斚w的语法机Ӟ卛_象和cR多态性、承性?/span> 1Q对象和c?/span> 对象的概c原理和Ҏ是面向对象的理序设计语言晕重要的特征。对象是用户定义的类型(UCؓc)的变量。一个对象是既包含数据又包合操作该数据的代码Q函敎ͼ的逻辑实体。对象中的这些数据和函数UCؓ对象的成员,x员数据和成员函数。对象中的成员分为公有的和私有的?span>公有成员是对象与外界的接口界面。外界只能通过调用讉K一个对象的公有成员来实现该对象的功能。私有成员体C个对象的l织形式和功能的实现l节。外界无法对U有成员q行操作?/span>cd象按照规范进行操作,描q客观事物的数据表达及对数据的操作处理封装在一P成功地实C面向对象的程序设计。当用户定义了一个类cd后,可以在该类型的名下定义变量Q即对象Q了?span>cLl构体类型的扩充?/span>l构体中引入成员函数q规定了其访问和l承原则后便成了cR?/span> 2Q多态?/span> 面向对象的程序设计语a支持“多态?/span>”Q把一个接口用于一cL动。即“一个接口多U算?/span>”。具体实施时该选择哪一个算法是q定的语法机制定的?/span>C++~译时和q行旉支持多态性?span>~译时的多态性体现在重蝲函数和重载运符{方面。运行时的多态性体现在l承关系及虚函数{方面?/span> 3Q承?/span> C++E序中,׃个类Q称为基c)可以z出新c(UCؓzc)。这U派生的语法机制使得新类的出现轻松自Ӟ使得一个复杂事物可以被理成章地归lؓ由逐层z的对象描q?/span>“z”使得E序中定义的cd层次l构。处于子层的对参既具有其父层对象的共性.又具有自w的Ҏ。承性是一个类对象获得其基cd象特性的q程?/span>C++中严格地规定了派生类对其基类的承原则和讉K权限Q得程序中Ҏ据和函数的访_需在家族和朋友间严格区分?/span> 事g驱动的程序设计实际上是面向对象程序设计的一个应用,但它目前仅适用?/span>windowspd操作pȝ?/span>windows环境中的应用E序?/span>MSQ?/span>DOS环境中的应用E序q行机制不同、设计程序的方式也不一栗?/span>windowsE序采用事g驱动机制q行Q这U事仉动程序由事g的发生与否来控制Q系l中每个对象状态副改变都是事g发生的原由或l果Q设计程序时需以一U非序方式处理事gQ与序的?span>q程驱动的传l程序设计方法E?/span>?/span> 事g也称消息Q含义比较广泛,常见的事件有鼠标事g(如民标移动、单凅R掠q窗口边?/span>)、键盘事?/span>(如按键的压下与拾?/span>){多U。应用程序运行经q一pd必要的初始化后,进入等待状态,{待有事件发生,一旦事件出玎ͼE序pȀzdƈq行相应处理?/span> 事g驱动E序设计是围l着消息的生与处理q行的.消息可来自程序中的某个对象,也可q戗?/span>wlndow s或运行着的其他应用程序生。每当事件发生时Q?/span>Windows俘获有关事gQ然后将消息分别转发到相兛_用程序中的有兛_象,需要对消息作出反应的对象应该提供消息处理函敎ͼ通过q个消息处理函数实现对象的一U功能或行ؓ。所以编写事仉动程序的大部分工作是为各个对?/span>(c?/span>)d各种消息的处理函?/span>。由于一个对象可以是消息的接收者,同时也可能是消息的发送者,所发送的消息与接收到的消息也可以是相同的消息Q而有些消息的发出旉是无法预知的(比如关于键盘的消?/span>)Q因此应用程序的执行序是无法预知的?/span> 逻辑式程序设计的概念来自逻辑式程序设计语aPrologq一曄在计机领域引v震动的日?/span>“W五?/span>”计算机的基本pȝ语言Q在q种“W五?/span>”计算ZQ?/span>Prolog的地位相当于当前计算Z的机器语a?/span> Prolog主要应用在h工智能领?/span>Q在自然语言处理、数据库查询、算法描q等斚w都有应用Q?span>其适于作ؓ专家pȝ的开发工兗?/span> Prolog是一U陈q式语言Q它不是一U严格的通用E序设计语言Q?/span>Prolog~写E序不需要描q具体的解题q程、只需l出一些必要的事实和规则,q些规则是解决问题方法的规范说明Q根据这些规则和事实Q计机利用渭词逻辑Q通过演绎推理得到求解问题的执行序列?/span> 一个有实际应用的ƈ行算法,最l总要在ƈ行机上实玎ͼ为此首先pƈ行算法{化ؓq行E序Q此q程是所谓的q行E序设计(Parallel Program)。它要求法设计者、系l结构师和Y件工作者广泛频J的交互。因计ƈ行程序涉及到的知识面较广Q主要包括操作系l中的有关知识和优化~译斚w的知识。操作系l内定w怸富,q行E序中最基本的计要素如d、进E、线E等基本概念、同步机制和通信操作{?/span> 目前q行E序设计的状冉|Q?/span>?/span>q行软g的发展落后于q行gQ?/span>?/span>和串行系l与应用软g相比Q现今的q行pȝ与应用Y件甚且不成熟;?/span>q行软g的缺乏是发展q行计算的主要障;?/span>不幸的是Q这U状态似乎仍在l着。究其原因是q行E序设计q比串行E序设计复杂Q?/span>?/span>q行E序设计不但包含了串行程序设计,面且q包含了更多的富有挑战性的问题Q?/span>?/span>串行E序设计仅有一个普遍被接受的冯·ZD模型,而ƈ行计模型虽有好多,但没有一个可被共同认可的像冯·Z曼那L优秀模型Q?/span>?/span>q行E序设计对环境工?/span>(如编译、查错等)的要求远比串行程序设计先q得多;?/span>串行E序设计比较适合于自然习惯,且h们在q去U篏了大量的~程知识、经验和宝贵的Y件胦富?/span> 堆和栈的理论知识 ?/p>
本文讨论如何把代码注入不同的进E地址I间Q然后在该进E的上下文中执行注入的代码?我们在网上可以查C些窗?密码侦测的应用例子,|上的这些程序大多都依赖 Windows 钩子技术来实现。本文将讨论除了使用 Windows 钩子技术以外的其它技术来实现q个功能。如图一所C: 图一 WinSpy 密码侦测E序 Z扑ֈ解决问题的方法。首先让我们单回一下问题背景?br> ?#8220;d”某个控g的内容——无个控件是否属于当前的应用E序——通常都是发?WM_GETTEXT 消息来实现。这个技术也同样应用到编辑控Ӟ但是如果该编辑控件属于另外一个进Eƈ讄?ES_PASSWORD 式样Q那么上面讲的方法就行不通了。用 WM_GETTEXT 来获取控件的内容只适用于进E?#8220;拥有”密码控g的情c所以我们的问题变成了如何在另外一个进E的地址I间执行Q?/p>
通常有三U可能性来解决q个问题?/p>
W一部分Q?Windows 钩子 范例E序——参见HookSpy 和HookInjEx Windows 钩子主要作用是监控某些线E的消息。通常我们钩子分为本地钩子和q程钩子以及pȝU钩子,本地钩子一般监控属于本q程的线E的消息,q程钩子是线E专用的Q用于监控属于另外进E的U程消息。系l钩子监控q行在当前系l中的所有线E的消息?br> 如果钩子作用的线E属于另外的q程Q那么你的钩子过E必驻留在某个动态链接库QDLLQ中。然后系l映包含钩子过E的DLL到钩子作用的U程的地址I间。Windows映整?DLLQ而不仅仅是钩子过E。这是Z?Windows 钩子能被用于代码注入到别的q程地址I间的原因?br> 本文我不打算涉及钩子的具体细节(关于钩子的细节请参见 MSDN 库中?SetWindowHookEx APIQ,但我在此要给Z个很有用心得Q在相关文档中你是找不到q些内容的: 目前只用了钩子来从处理q程q程中DLL的映和解除映射。在?#8220;作用于线E的”钩子Ҏ能没有影响?br>下面我们讨论另外一U方法,q个Ҏ?LoadLibrary 技术的不同之处是DLL的映机制不会干预目标进E。相对LoadLibrary 技术,q部分描q的Ҏ适用?WinNT和Win9x?br> 但是Q什么时候用这个技巧呢Q答案是当DLL必须在远E进E中ȝ较长旉Q即如果你子cd某个属于另外一个进E的控gӞ以及你想可能少的干涉目标进E时。我?HookSpy 中没有用它Q因为注入DLL 的时间ƈ不长——注入时间只要够得到密码即可。我提供了另外一个例子程序——HookInjEx——来C。HookInjEx DLL映射到资源管理器“explorer.exe”Qƈ从中/解除影射Q它子类?#8220;开?#8221;按钮Qƈ交换鼠标左右键单?#8220;开?#8221;按钮的功能? HookSpy ?HookInjEx 的源代码都可以从本文?a >下蝲源代?/font>中获得?
W二部分Q?a name=CreateRemoteThread_和_LoadLibrary_技?CreateRemoteThread ?LoadLibrary 技?/a>
范例E序——LibSpy
通常QQ何进E都可以通过 LoadLibrary API 动态加载DLL。但是,如何强制一个外部进E调用这个函数呢Q答案是QCreateRemoteThread?br>首先Q让我们看一?LoadLibrary 和FreeLibrary API 的声明: 现在它们与传递到 CreateRemoteThread 的线E例E——ThreadProc 的声明进行比较?/p>
你可以看刎ͼ所有函数都使用相同的调用规范ƈ都接?32位参敎ͼq回值的大小都相同。也是_我们可以传递一个指针到LoadLibrary/FreeLibrary 作ؓ?CreateRemoteThread 的线E例E。但q里有两个问题,L下面对CreateRemoteThread 的描qͼ W一个问题实际上是由它自p决的。LoadLibrary ?FreeLibray 两个函数都在 kernel32.dll 中。因为必M证kernel32存在q且在每?#8220;常规”q程中的加蝲地址要相同,LoadLibrary/FreeLibray 的地址在每个进E中的地址要相同,q就保证了有效的指针被传递到q程q程?br> W二个问题也很容易解冟뀂只要通过 WriteProcessMemory ?DLL 模块名(LoadLibrary需要的DLL模块名)拯到远E进E即可?/p>
所以,Z使用CreateRemoteThread ?LoadLibrary 技术,需要按照下列步骤来做: 此外Q处理完成后不要忘了关闭所有句柄,包括在第四步和第八步创徏的两个线E以及在W一步获取的q程U程句柄。现在让我们看一?LibSpy 的部分代码,Z单v见,上述步骤的实现细节中的错误处理以?UNICODE 支持部分被略掉? W三部分Q?a name=CreateRemoteThread_和_WriteProcessMemory_技?CreateRemoteThread ?WriteProcessMemory 技?/a> 范例E序——WinSpy 另外一个将代码拯到另一个进E地址I间q在该进E上下文中执行的Ҏ是用远E线E和 WriteProcessMemory API。这U方法不用编写单独的DLLQ而是?WriteProcessMemory 直接代码拷贝到q程q程——然后用 CreateRemoteThread 启动它执行。先来看?CreateRemoteThread 的声明: lg所qͼ我们得按照如下的步骤来做Q?/p>
ThreadFunc 必须要遵循的原则Q?/p>
如果你没有按照这些规则来做,目标q程很可能会崩溃。所以务必牢记。在目标q程中不要假设Q何事情都会像在本地进E中那样 Q参见附录FQ?
GetWindowTextRemote(A/W)
要想?#8220;q程”~辑框获得密码,你需要做的就是将所有功能都装在GetWindowTextRemot(A/W):中?/p>
下面让我们看看它的部分代码——尤其是注入数据的代码——以便明?GetWindowTextRemote 的工作原理。此处ؓ单v见,略掉?UNICODE 支持部分?/p>
INJDATA 是一个被注入到远E进E的数据l构。但在注入之前,l构中指?SendMessageA 的指针是在本地应用程序中初始化的。因为对于每个用user32.dll的进E来_user32.dllL被映到相同的地址Q因此,SendMessageA 的地址也肯定是相同的。这׃证了被传递到q程q程的是一个有效的指针?br> ThradFunc 是被q程U程执行的代码?/p>
范例E序——InjectEx q里主要的问题是如何数据传到远E窗口过E?NewProcQ因?NewProc 是一个回调函敎ͼ它必遵循特定的规范和原则,我们不能单地在参C传?INJDATA指针。幸q的是我扑ֈ了有两个Ҏ来解册个问题,只不q要借助汇编语言Q所以不要忽略了汇编Q关键时候它是很有用的! 如下图所C: 在远E进E中QINJDATA 被放在NewProc 之前Q这?NewProc 在编译时便知?INJDATA 在远E进E地址I间中的内存位置。更切地说Q它知道相对于其自n位置?INJDATA 的地址Q我们需要所有这些信息。下面是 NewProc 的代码: q种方式 pData得到的是编码|在我们的q程中是?NewProc 的内存地址Q。这不是我们十分惌的。在q程q程中,NewProc “当前”拯的内存地址与它被移到的实际位置是无关的Q换句话_我们会需要某U类型的“this 指针”?br>虽然?C/C++ 无法解决q个问题Q但借助内联汇编可以解决Q下面是?NewProc的修改: q样一来,不管 NewProc 被移C么地方,它总能计算出其自己的地址。但是,NewProc 的入口点?“POP ECX”之间的距d能会随着你对~译/链接选项的改变而变化,由此造成 RELEASE和DEBUG版本之间也会有差别。但关键是你仍然切地知道编译时的倹{?/p>
此即?InjecEx 中用的解决ҎQ类g HookInjExQ交换鼠标点?#8220;开?#8221;左右键时的功能?br> 对于我们的问题,在远E进E地址I间中将 INJDATA 攑֜ NewProc 前面不是唯一的解军_法。看下面 NewProc的变异版本: 到目前ؓ止,有几个问题是我们未提及的Q现ȝ如下Q?/p>
最后,有几件事情一定要了然于心Q你的注入代码很Ҏ摧毁目标q程Q尤其是注入代码本n出错的时候,所以要CQ权力带来责任! 附录AQ?/p>
Z?kernel32.dll 和user32.dll L被映到相同的地址?br> 附录BQ?/p>
/GZ ~译器开?/p>
在生?Debug 版本Ӟ/GZ ~译器特性是默认打开的。你可以用它来捕h些错误(具体l节请参考相x档)。但Ҏ们的可执行程序意味着什么呢Q?br> 当打开 /GZ 开养I~译器会d一些额外的代码到可执行E序中每个函数所在的地方Q包括一个函数调用(被加到每个函数的最后)——检查已l被我们的函C改的 ESP堆栈指针。什么!N有一个函数调用被d?ThreadFunc 吗?那将DN。ThreadFunc 的远E拷贝将调用一个在q程q程中不存在的函敎ͼ臛_是在相同的地址I间中不存在Q?/p>
附录CQ?
静态函数和增量链接 增量链接主要作用是在生成应用E序时羃短链接时间。常规链接和增量链接的可执行E序之间的差别是——增量链接时Q每个函数调用经׃个额外的JMP指oQ该指o由链接器发出Q该规则的一个例外是函数声明为静态)。这?JMP 指o允许链接器在内存中移动函敎ͼq种Ud无需修改引用函数?CALL指o。但q些JMP指o也确实导致了一些问题:?ThreadFunc ?AfterThreadFunc 指向JMP指o而不是实际的代码。所以当计算ThreadFunc 的大时Q? 附录DQ?/p>
Z?ThreadFunc的局部变量只?4kQ?/p>
局部变量L存储在堆栈中Q如果某个函数有256个字节的局部变量,当进入该函数Ӟ堆栈指针减?56个字节(更精地_在函数开始处Q。例如,下面q个函数Q? 注意事项 Z么要开兌句拆分成三个以上? 用下面这个例子很Ҏ解释q个问题Q假设有如下q么一个函敎ͼ 现在Q你也许认ؓ出现上述情况只是因ؓCASE帔R被有意选择l的Q?Q?Q?Q?Q。幸q的是,它的q个Ҏ可以应用于大多数现实例子中,只有偏移量的计算E微有些复杂。但有两个例外: 昄Q单独判断每个的CASE帔R的话Q结果代码繁琐耗时Q但使用CMP和JMP指o则得结果代码的执行像普通的if-else 语句?br>有趣的地方:如果你不明白CASE语句使用帔R表达式的理由Q那么现在应该弄明白了吧。ؓ了创建地址表,昄在编译时应该知道相兛_址?/p>
现在回到问题Q?br>注意到地址 0040100C 处的JMP指o了吗Q我们来看看Intel关于十六q制操作?FF 的文档是怎么说的Q?/p>
原来JMP 使用了一U绝对寻址方式Q也是_它的操作敎ͼCASE语句中的 0040102CQ表CZ个绝对地址。还用我说什么吗Q远E?ThreadFunc 会盲目地认ؓ地址表中开兛_址?0040102CQJMPC个错误的地方Q造成q程q程崩溃? Z么远E进E会崩溃呢? 当远E进E崩溃时Q它L会因Z面这些原因: 不管哪种情况Q你都要心翼C?CreateRemoteThread ?WriteProcessMemory 技术。尤其要注意你的~译?链接器选项Q一不小心它们就会在 ThreadFunc d内容?
一?在中国你千万不要因ؓ学习技术就可以换来E_的生zd高的薪水待遇Q你千万更不要认为哪些从?市场开发,跑腿的hQ没有前途?br />
不知道你是不是知道,׃中国有相当大的一部分软g公司Q他们的软g开发团队都的可怜,甚至只有1-3个hQ连一个项目小l都不上,而这L团队却要承担一个Y件公司所有的软g开发Q务,在Y件上U和开发的关键阶段需要团队的成员没日没夜的加班,q需要ؓ试出的BUG和不能按时提交的软g模块功能而心怀忐忑Q有的时候如果你不幸加入现场开发的团队你则需要背井离乡告别你的女友,q行闭开发,你^旉了编码之外就是吃饭和睡觉Q有q公司甚至请个保姆Z做饭Q以让你节省出更多的旉来投入到工作中,让你一直在那种累了׃息,不篏q卛_作的状态)
更可怕的是,会让你接触的人际关系非常单一Q除了有限的技术h员之外你几乎见不到做其他行业工作和职位的人,你的朋友圈子且单一Q甚至破坏你原有的爱情(惌一下,你在外地做现场开?个月以上Q却从没跟女友见q一面的话,你的奛_是不是会对你呲牙裂嘴Q?br />
也许你拿C所谓的白领的工资,但你却从此失Mn受生zȝ自由Q如果你惛_技术h员尤其是开发h员,我想你很快就会理解,你多么想在一个地斚w期待一D|_认识一些朋友,多一些生zL间的愿望?br />
比之于我们的生活和h际关pd工作Q那些从事售前和市场开发的朋友Q却有比我们多的多的工作之外的时_甚至他们工作的时间有的时候是和生zȝ旉是可以兼儡Q他们可以通过市场开发,认识各个行业的h士,可以认识各种各样的朋友,他们比我们坦率说更有发胦和发展的ZQ只要他们跟我们一样勤奋。(有一U勤奋的普通hQ如果给他换个地方,他马上会成ؓ一个勤奋且Z的h。)
二。在学习技术的时候千万不要认为如果做到技术最强,可以成?00%受尊重的人?br />
有一ơ一个h在面试项目经理的时候说了这么一D话Q我只用最听话的hQ按照我的要求做只要是听话就要,如果不听话不他技术再好也不要。随后这个h得到了试用机会,如果没意外的话,他一定会是下一个项目经理的lQ者?br />
朋友们你知道吗?不管你技术有多强Q你也不可能自由的腾出时间象别h那样研究一下LINUX源码Q甚臛_一个LINUXLC来表C的才能。你需要做的就是按照要求写代码Q写代码的含义就是都规定好,你按照规定写Q你很快׃发现你昨天写的代码,跟今天写的代码有很多cMQ等你写q一D|间的代码Q你领略:复制Q拷贝,_脓那样的技术对你来说是何等重要。(如果你没有做q?q以上的真正意义上的开发不要反xQ?br />
如果你幸q的能够听到市场人员的谈话,或是领导们的谈话Q你会隐U觉得他们都在把技术h员当作编码的机器来看Q你的h值ƈ没有你想象的那么重要。而在你所在的团队内部Q你可能正在Z个技术问题的讨论再跟同事搞内耗,因ؓ他不服你Q你也不服他Q你们都认ؓ自己的对Q其实你们两个都对,而争论的目的是Z在关键场合证明一下自己比Ҏ技术好Q比Ҏ强。(在一个项目开发中Q没有h愿意长期听别人的QL换个位置领导别h。)
三。你更不要认为,如果我技术够好,我就自己创业Q自己有创业的资本,因ؓ自己是搞技术的?br />
如果你那栯为,真的是大错特错了Q你可以做个调查在非技术h中Q没有几个h知道C#与JAVA的,更谈不上来欣赏你的技术是好还是不好。一句话Q技术仅仅是一个工P善于q用q个工具为别人干zȝ人,却往往不太擅长用这个工h己创业,因ؓq是两个概念Q训l的技能也是完全不同的?br />
创业最开始的时候,你的人际关系Q你处理人际关系的能力,你对C会潜规则的认识Q还有你明白不明白别人的心,你会不会说让人喜Ƣ的话,q有你对自己所提供的服务的{划和推销{等Q也许有一万,一百万个值得我们重视的问题,但你会发现技术却很少有可能包含在q一万或一百万之内Q如果你创业C一个快成功的阶D,你会q样告诉自己Q我q吗要亲自做技术,我聘一个h不就行了Q这时候你才真正会理解技术的作用Q和你以前做技术h员的作用?br />
[结]
Z上面的讨论,我奉劝那些学习技术的朋友Q千万不要拿UD考试L心态去学习技?Ҏ术的学习几近的痴qPx握所有所有的技术,以让自己成ؓ技术领域的权威和专Ӟ以在必要的时候或是心里不畅快的时候到|上对着菜鸟说自己是前辈?br />
技术仅仅是一个工P是你在h生一个阶D는存的工具Q你可以一辈子喜欢他,但最好不要一辈子靠它生存?br />
掌握技术的唯一目的是拿它扑ַ作(如果你不x技术当作你W二生命的话Q,是q活。所以你在学习的时候千万不要去做那些所谓的技术习题或是研I些帽泡算法,最大数法了,什么叫q活Q?br />
是做一个东西让别h用,别h用了Q可以提高他们的工作效率Q想象吧Q你?万道技术习题有什么用Q只会让得酸腐,q是在学习的时候,多培M自己务实的态度吧,比如研究一下当地市场目前有哪些软g公司用hQ自q他们的要求到底有多远Q自己具体应该怎么做才可以辑ֈ他们的要求。等你分析完q些Q你׃发现Q找工作成功Q技术的贡献率其实ƈ没有你原来想象的那么高?br />
不管你是学习技术ؓ了找工作q是创业Q你都要Ҏ术本w有个清醒的认识Q在中国不会出现BILL GATESQ因为,中国目前q不是十分的重技术h才,q仅仅的停留在把软g技术h才当作h才机器来用的尬境地。(如果你不理解Q一U可能是你目前仅仅从事过技术工作,你的朋友圈子里技术类的朋友占了大多数Q一U可能是你还没有工作Q但喜欢L。盖茨的传记Q?
]]>
]]>
4、文字常量区 —常量字W串是攑֜q里的?E序l束后由pȝ释放
5、程序代码区—存攑ևC的二q制代码?
2.1甌方式
stack:
ql自动分配?/u> 例如Q声明在函数中一个局部变?int b; pȝ自动在栈中ؓb开辟空?br>heap:
需要程序员自己甌Qƈ指明大小Q在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用newq算W?br>如p2 = (char *)malloc(10);
但是注意p1、p2本n是在栈中的?br>2.2甌后系l的响应
栈:只要栈的剩余I间大于所甌I间Q系l将为程序提供内存,否则报异常提示栈溢出?br>堆:首先应该知道操作pȝ有一个记录空闲内存地址的链?/font>Q当pȝ收到E序的申hQ?br>?遍历该链表,LW一个空间大于所甌I间的堆l点Q然后将该结点从I闲l点链表中删除,q将该结点的I间分配l程序,另外Q对于大多数pȝQ会在这块内 存空间中的首地址处记录本ơ分配的大小Q这P代码中的delete语句才能正确的释放本内存I间。另外,׃扑ֈ的堆l点的大不一定正好等于申L?,pȝ会自动的多余的那部分重新放入空闲链表中?br>2.3甌大小的限?/font>
栈:在Windows?栈是向低地址扩展的数据结构,是一?q箋的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是pȝ预先规定好的Q在 WINDOWS下,栈的大小?MQ也有的说是1MQM是一个编译时q定的常数Q,如果甌的空间超q栈的剩余空间时Q将提示overflow。因 此,能从栈获得的I间较小?br>堆:堆是向高地址扩展的数据结构,是不q箋的内存区域?/font>q是׃pȝ是用链表来存储的I闲内存地址的,自然是不q箋的,而链表的遍历方向是由低地址向高地址。堆的大受限于计算机系l中有效的虚拟内存。由此可见,堆获得的I间比较灉|Q也比较大?br>2.4甌效率的比较:
栈由pȝ自动分配Q速度较快。但E序员是无法控制的?/font>
堆是由new分配的内存,一般速度比较慢,而且Ҏ产生内存片,不过用v来最方便.
另外Q在WINDOWS下,最好的方式是用VirtualAlloc分配内存Q他不是在堆Q也不是在栈是直接在q程的地址I间中保留一快内存,虽然用v来最不方ѝ但是速度Q?也最灉|
2.5堆和栈中的存储内?/font>
栈: 在函数调用时Q第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句Q的地址Q然后是函数的各个参敎ͼ在大多数的C~译器中Q参数是由右往左入栈的Q然后是函数中的局部变量。注意静态变量是不入栈的?br>当本ơ函数调用结束后Q局部变量先出栈Q然后是参数Q最后栈指针指向最开始存的地址Q也是dC的下一条指令,E序p点l运行?br>堆:一般是在堆的头部用一个字节存攑֠的大。堆中的具体内容有程序员安排?br>2.6存取效率的比?/font>
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在q行时刻赋值的Q?br>而bbbbbbbbbbb是在~译时就定的;
但是Q在以后的存取中Q在栈上的数l比指针所指向的字W串(例如?快?br>比如Q?br>#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇~代?br>10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
W一U在d时直接就把字W串中的元素d寄存器cl中,而第二种则要先把指edx中,在根据edxd字符Q显然慢了?br>
2.7结Q?/strong>
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃?/font>Q只点菜(发出甌Q、付钱、和吃(使用Q,吃饱了就赎ͼ不必理会切菜、洗菜等准备工作和洗、刷锅等扫尾工作Q?font color=#ff00ff>他的好处是快P但是自由度小?/font>
使用堆就象是自己动手做喜Ƣ吃的菜?/font>Q比较麻烦,但是?font color=#ff0000>较符合自q口味Q而且自由度大?/p>
]]>
—?a >如何用该技术子cdq程控g
—?a href="http://www.vckbase.com/document/viewdoc/?id=1886#何时使用_CreateRemoteThread_和_WriteProcessMemory_技?>何时使用 CreateRemoteThread ?WriteProcessMemory 技?/font>
::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );
如果你安装一个钩子监控某些线E(WH_CALLWNDPROCQ的非队列消息,在消息被实际发送到Q某些窗口的Q钩子作用的U程之前Q该DLL 是不会被映射到远E进E的。换句话_如果 UnhookWindowsHookEx 在某个消息被发送到钩子作用的线E之前被调用QDLL Ҏ不会被映到q程q程Q即?SetWindowsHookEx 本n调用成功Q。ؓ了强制进行映,在调?SetWindowsHookEx 之后马上发送一个事件到相关的线E?br> 在UnhookWindowsHookEx了之后,对于没有映射的DLL处理Ҏ也一栗只有在_的事件发生后QDLL才会有真正的映射?
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved )
{
if( ul_reason_for_call == DLL_PROCESS_ATTACH )
{
// Increase reference count via LoadLibrary
char lib_name[MAX_PATH];
::GetModuleFileName( hModule, lib_name, MAX_PATH );
::LoadLibrary( lib_name );
// Safely remove hook
::UnhookWindowsHookEx( g_hHook );
}
return TRUE;
}
那么会发生什么呢Q首先我们通过Windows 钩子DLL映射到远E进E。然后,在DLL被实际映之后,我们解开钩子。通常当第一个消息到N子作用线E时QDLL此时也不会被映射。这里的处理技巧是调用LoadLibrary通过增加 DLLs的引用计数来防止映射不成功?br> 现在剩下的问题是如何卸蝲DLLQUnhookWindowsHookEx 是不会做q个事情的,因ؓ钩子已经不作用于U程了。你可以像下面这样做Q?br>
HINSTANCE LoadLibrary(
LPCTSTR lpLibFileName // 库模块文件名的地址
);
BOOL FreeLibrary(
HMODULE hLibModule // 要加载的库模块的句柄
);
DWORD WINAPI ThreadProc(
LPVOID lpParameter // U程数据
);
HANDLE hThread;
char szLibPath[_MAX_PATH]; // “LibSpy.dll”模块的名U?(包括全\?;
void* pLibRemote; // q程q程中的地址QszLibPath 被拯到此?
DWORD hLibModule; // 要加载的模块的基地址QHMODULEQ?
HMODULE hKernel32 = ::GetModuleHandle("Kernel32");
// 初始化szLibPath
//...
// 1. 在远E进E中为szLibPath 分配内存
// 2. szLibPath 写入分配的内?
pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),
MEM_COMMIT, PAGE_READWRITE );
::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
sizeof(szLibPath), NULL );
// ?LibSpy.dll" 加蝲到远E进E(使用CreateRemoteThread ?LoadLibraryQ?
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
"LoadLibraryA" ),
pLibRemote, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );
// 获取所加蝲的模块的句柄
::GetExitCodeThread( hThread, &hLibModule );
// 清除
::CloseHandle( hThread );
::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );
假设我们实际惌注入的代码——SendMessage ——被攑֜DllMain (DLL_PROCESS_ATTACH)中,现在它已l被执行。那么现在应该从目标q程中将DLL 卸蝲Q?
// 从目标进E中卸蝲"LibSpy.dll" (使用 CreateRemoteThread ?FreeLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
"FreeLibrary" ),
(void*)hLibModule, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );
// 清除
::CloseHandle( hThread );
q程间通信
到目前ؓ止,我们只讨Z关于如何DLL 注入到远E进E的内容Q但是,在大多数情况下,注入?DLL 都需要与原应用程序进行某U方式的通信Q回想一下,我们的DLL是被映射到某个远E进E的地址I间里了Q不是在本地应用E序的地址I间中)。比如秘密侦程序,DLL必须要知道实际包含密码的控g句柄Q显Ӟ~译时无法将q个D行硬~码。同P一旦DLL获得了秘密,它必d它发送回原应用程序,以便能正显C出来?br> q运的是Q有许多Ҏ处理q个问题Q文件映,WM_COPYDATAQ剪贴板以及很简单的 #pragma data_seg ׃n数据D늭Q本文我不打用这些技术,因ؓMSDNQ?#8220;q程间通信”部分Q以及其它渠道可以找到很多文档参考。不q我?LibSpy例子中还是用了 #pragma data_seg。细节请参?LibSpy 源代码?
HANDLE CreateRemoteThread(
HANDLE hProcess, // 传入创徏新线E的q程句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全属性指?
DWORD dwStackSize, // 字节为单位的初始U程堆栈
LPTHREAD_START_ROUTINE lpStartAddress, // 指向U程函数的指?
LPVOID lpParameter, // 新线E用的参数
DWORD dwCreationFlags, // 创徏标志
LPDWORD lpThreadId // 指向q回的线EID
);
如果你比较它?CreateThreadQMSDNQ的声明Q你会注意到如下的差别:
switch( expression ) {
case constant1: statement1; goto END;
case constant2: statement2; goto END;
case constant3: statement2; goto END;
}
switch( expression ) {
case constant4: statement4; goto END;
case constant5: statement5; goto END;
case constant6: statement6; goto END;
}
END:
或者将它们修改成一?if-else if l构语句Q参见附录EQ?int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString );
int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );
参数说明Q?
hProcessQ编辑框控g所属的q程句柄Q?
hWndQ包含密码的~辑框控件句柄;
lpStringQ接收文本的~冲指针Q?
q回|q回值是拯的字W数Q?/pre>
INJDATA
typedef LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);
typedef struct {
HWND hwnd; // ~辑框句?
SENDMESSAGE fnSendMessage; // 指向user32.dll ?SendMessageA 的指?
char psText[128]; // 接收密码的缓?
} INJDATA;
ThreadFunc函数static DWORD WINAPI ThreadFunc (INJDATA *pData)
{
pData->fnSendMessage( pData->hwnd, WM_GETTEXT, // Get password
sizeof(pData->psText),
(LPARAM)pData->psText );
return 0;
}
// 该函数在ThreadFunc之后标记内存地址
// int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc.
static void AfterThreadFunc (void)
{
}
如何使用该技术子cdq程控g
下面我们讨Z些更复杂的内容,如何子类化属于另一个进E的控g?br>
首先Q你得拷贝两个函数到q程q程来完成此d
Ҏ一Q?/p>
static LRESULT CALLBACK NewProc(
HWND hwnd, // H口句柄
UINT uMsg, // 消息标示W?
WPARAM wParam, // W一个消息参?
LPARAM lParam ) // W二个消息参?
{
INJDATA* pData = (INJDATA*) NewProc; // pData 指向 NewProc
pData--; // 现在pData 指向INJDATA;
// 回想一下INJDATA 被置于远E进ENewProc之前;
//-----------------------------
// 此处是子cd代码
// ........
//-----------------------------
// 调用原窗口过E?
// fnOldProc (由SetWindowLong q回) 被(q程QThreadFunc初始?
// q被保存在(q程QINJDATA;?
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
但这里还有一个问题,见第一行代码:
INJDATA* pData = (INJDATA*) NewProc;
static LRESULT CALLBACK NewProc(
HWND hwnd, // H口句柄
UINT uMsg, // 消息标示W?
WPARAM wParam, // W一个消息参?
LPARAM lParam ) // W二个消息参?
{
// 计算INJDATA l构的位|?
// 在远E进E中Cq个INJDATA
// 被放在NewProc之前
INJDATA* pData;
_asm {
call dummy
dummy:
pop ecx // <- ECX 包含当前的EIP
sub ecx, 9 // <- ECX 包含NewProc的地址
mov pData, ecx
}
pData--;
//-----------------------------
// 此处是子cd代码
// ........
//-----------------------------
// 调用原来的窗口过E?
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
那么Q接下来该怎么办呢Q事实上Q每个进E都有一个特D的寄存器,它指向下一条要执行的指令的内存位置。即所谓的指o指针Q在32?Intel ?AMD 处理器上被表CZؓ EIP。因?EIP是一个专用寄存器Q你无法象操作一般常规存储器Q如QEAXQEBX{)那样通过~程存取它。也是说没有操作代码来d EIPQ以便直接读取或修改其内宏V但是,EIP 仍然q是可以通过间接Ҏ修改的(q且随时可以修改Q,通过JMPQCALL和RETq些指o实现。下面我们就通过例子来解释通过 CALL/RET 子例E调用机制在32?Intel ?AMD 处理器上是如何工作的?
当你调用Q通过 CALLQ某个子例程Ӟ子例E的地址被加载到 EIPQ但即便是在 EIP杯修改之前,其旧的那个D自动PUSH到堆栈(被用于后面作为指令指针返回)。在子例E执行完ӞRET 指o自动堆栈顶POP?EIP?br> 现在你知道了如何通过 CALL ?RET 实现 EIP 的修改,但如何获取其当前的值呢Q下面就来解册个问题,前面讲过QCALL PUSH EIP 到堆栈,所以,Z获取其当前|调用“哑函?#8221;Q然后再POP堆栈。让我们用编译后?NewProc 来解释这个窍门?
Address OpCode/Params Decoded instruction
--------------------------------------------------
:00401000 55 push ebp ; entry point of
; NewProc
:00401001 8BEC mov ebp, esp
:00401003 51 push ecx
:00401004 E800000000 call 00401009 ; *a* call dummy
:00401009 59 pop ecx ; *b*
:0040100A 83E909 sub ecx, 00000009 ; *c*
:0040100D 894DFC mov [ebp-04], ecx ; mov pData, ECX
:00401010 8B45FC mov eax, [ebp-04]
:00401013 83E814 sub eax, 00000014 ; pData--;
.....
.....
:0040102D 8BE5 mov esp, ebp
:0040102F 5D pop ebp
:00401030 C21000 ret 0010
Ҏ二:
static LRESULT CALLBACK NewProc(
HWND hwnd, // H口句柄
UINT uMsg, // 消息标示W?
WPARAM wParam, // W一个消息参?
LPARAM lParam ) // W二个消息参?
{
INJDATA* pData = 0xA0B0C0D0; // 虚构?
//-----------------------------
// 子类化代?
// ........
//-----------------------------
// 调用原来的窗口过E?
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
此处 0xA0B0C0D0 只是q程q程地址I间中真实(l对QINJDATA地址的占位符。前面讲q,你无法在~译时知道该地址。但你可以在调用 VirtualAllocEx QؓINJDATAQ之后得?INJDATA 在远E进E中的位|。编译我们的 NewProc 后,可以得到如下l果Q?
Address OpCode/Params Decoded instruction
--------------------------------------------------
:00401000 55 push ebp
:00401001 8BEC mov ebp, esp
:00401003 C745FCD0C0B0A0 mov [ebp-04], A0B0C0D0
:0040100A ...
....
:0040102D 8BE5 mov esp, ebp
:0040102F 5D pop ebp
:00401030 C21000 ret 0010
因此Q其~译的代码(十六q制Q将是:
558BECC745FCD0C0B0A0......8BE55DC21000.
现在你可以象下面q样l箋Q?
558BECC745FCD0C0B0A0......8BE55DC21000 <- 原来的NewProc Q注1Q?
558BECC745FC00008A00......8BE55DC21000 <- 修改后的NewProcQ用的是INJDATA的实际地址?/pre>
也就是说Q你用真正的 INJDATAQ注2Q?地址替代了虚拟?A0B0C0D0Q注2Q?
与其它方法比较,使用 CreateRemoteThread ?WriteProcessMemory 技术进行代码注入更灉|Q这U方法不需要额外的 dllQ不q的是,该方法更复杂q且风险更大Q只要ThreadFunc出现哪怕一丁点错误Q很ҎpQƈ且最大可能地会)使远E进E崩溃(参见附录 FQ,因ؓ调试q程 ThreadFunc 是一个可怕的梦魇Q只有在注入的指令数很少Ӟ你才应该考虑使用q种技术进行注入,对于大块的代码注入,最好用 I.和II 部分讨论的方法?br>
WinSpy 以及 InjectEx 请从q里下蝲源代?/font>?/p>
解决Ҏ
OS
q程
I、Hooks
Win9x ?WinNT
仅仅?USER32.DLL Q注3Q链接的q程
II、CreateRemoteThread & LoadLibrary
?WinNTQ注4Q?/td>
所有进E(?Q? 包括pȝ服务Q注6Q?/td>
III、CreateRemoteThread & WriteProcessMemory
?WinNT
所有进E? 包括pȝ服务
本地应用 Qsmss.exe, os2ss.exe, autochk.exe {)不?Win32 APIQ所以也不会?kernel32.dll 链接。唯一一个例外是 csrss.exeQWin32 子系l本w,它是本地应用E序Q但其某些库Q~winsrv.dllQ需?Win32 DLLsQ包?kernel32.dllQ?
因ؓ本文中的许多例子是关于密码的Q你也许q读q?Zhefu Zhang 写的另外一文?#8220;Super Password Spy++” Q在该文中,他解释了如何获取IE 密码框中的内容,此外Q他q示范了如何保护你的密码控g免受cM的攻凅R?/p>
我的假定Q因为Microsoft 的程序员认ؓq样做有助于速度优化Qؓ什么呢Q我的解释是——通常一个可执行E序是由几个部分l成Q其中包?#8220;.reloc” 。当链接器创?EXE 或?DLL文gӞ它对文g被映到哪个内存地址做了一个假设。这是所谓的首选加?基地址。在映像文g中所有绝对地址都是Z链接器首选的加蝲地址Q如果由于某U原因,映像文g没有被加载到该地址Q那么这?#8220;.reloc”pv作用了,它包含映像文件中的所有地址的清单,q个清单中的地址反映了链接器首选加载地址和实际加载地址的差别(无论如何Q要注意~译器生的大多数指令用某U相对地址dQ因此,q没有你惌的那么多地址可供重新分配Q,另一斚wQ如果加载器能够按照链接器首选地址加蝲映像文gQ那?#8220;.reloc”p完全忽略掉了?br> 但kernel32.dll 和user32.dll 及其加蝲地址Z要以q种方式加蝲呢?因ؓ每一?Win32 E序都需要kernel32.dllQƈ且大多数Win32 E序也需?user32.dllQ那么L它们(kernel32.dll 和user32.dllQ映到首选地址可以改进所有可执行E序的加载时间。这样一来,加蝲器绝不能修改kernel32.dll and user32.dll.中的MQ绝对)地址。我们用下面的例子来说明Q?br> 某个应用程?App.exe 的映像基地址讄?KERNEL32的地址Q?base:"0x77e80000"Q或 USER32的首选基地址Q?base:"0x77e10000"Q,如果 App.exe 不是?USER32 导入方式来?USER32Q而是通过LoadLibrary 加蝲Q那么编译ƈq行App.exe 后,会报出错误信息("Illegal System DLL Relocation"——非法系lDLL地址重分配)QApp.exe 加蝲p|?br>Z么会q样呢?当创E时QWin 2000、Win XP 和Win 2003pȝ的加载器要检?kernel32.dll 和user32.dll 是否被映到首选基地址Q实际上Q它们的名字都被编码进了加载器Q,如果没有被加载到首选基地址Q将发出错误。在 WinNT4中,也会查ole32.dllQ在WinNT 3.51 和较低版本的Windows中,׃不会做这L查,所以kernel32.dll 和user32.dll可以被加载Q何地斏V只有ntdll.dllL被加载到其基地址Q加载器不进行检查,一旦ntdll.dll没有在其基地址Q进E就无法创徏?br>
MQ对?WinNT 4 和较高的版本?/p>
const int cbCodeSize = ((LPBYTE) AfterThreadFunc - (LPBYTE) ThreadFunc)
你实际上计算的是指向 ThreadFunc 的JMPs 和AfterThreadFunc之间?#8220;距离” Q通常它们会紧挨着Q不用考虑距离问题Q。现在假?ThreadFunc 的地址位于004014C0 而伴随的 JMP指o位于 00401020?
:00401020 jmp 004014C0
...
:004014C0 push EBP ; ThreadFunc 的实际地址
:004014C1 mov EBP, ESP
...
那么
WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..);
拷?#8220;JMP 004014C0”指oQ以及随后cbCodeSize范围内的所有指令)到远E进E——不是实际的 ThreadFunc。远E进E要执行的第一件事情将?#8220;JMP 004014C0” 。它会在其最后几条指令当中——远E进E和所有进E均如此。但 JMP指o的这?#8220;规则”也有例外。如果某个函数被声明为静态的Q它会被直接调用,即增量链接也是如此。这是Z么规?4要将 ThreadFunc ?AfterThreadFunc 声明为静态或用增量链接的缘故。(有关增量链接的其它信息参?Matt Pietrek的文?#8220;Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools” Q?
void Dummy(void) {
BYTE var[256];
var[0] = 0;
var[1] = 1;
var[255] = 255;
}
~译后的汇编如下Q?
:00401000 push ebp
:00401001 mov ebp, esp
:00401003 sub esp, 00000100 ; change ESP as storage for
; local variables is needed
:00401006 mov byte ptr [esp], 00 ; var[0] = 0;
:0040100A mov byte ptr [esp+01], 01 ; var[1] = 1;
:0040100F mov byte ptr [esp+FF], FF ; var[255] = 255;
:00401017 mov esp, ebp ; restore stack pointer
:00401019 pop ebp
:0040101A ret
注意上述例子中,堆栈指针是如何被修改的?而如果某个函数需?KB以上局部变量内存空间又会怎么样呢Q其实,堆栈指针q不是被直接修改Q而是通过另一个函数调用来修改的。就是这个额外的函数调用使得我们?ThreadFunc “被破?#8221;了,因ؓ其远E拷贝会调用一个不存在的东ѝ?br> 我们看看文档中对堆栈探测?/Gs~译器选项是怎么说的Q?br>—?#8220;/GS是一个控制堆栈探的高Ҏ,堆栈探测是一pd~译器插入到每个函数调用的代码。当函数被激zLQ堆栈探需要的内存I间来存储相兛_数的局部变量?br> 如果函数需要的I间大于为局部变量分配的堆栈I间Q其堆栈探测被激zR默认的大小是一个页面(?0x86处理器上4kbQ。这个值允许在Win32 应用E序和Windows NT虚拟内存理器之间进行}慎调整以便增加运行时承诺l程序堆栈的内存?#8221;
我确信有Z问:文档中的“……堆栈探测C块需要的内存I间来存储相兛_数的局部变?#8230;…”那些~译器选项Q它们的描述Q在你完全弄明白之前有时真的让h气愤。例如,如果某个函数需?2KB的局部变量存储空_堆栈内存进行如下方式的分配Q更_地说?#8220;承诺” Q?
sub esp, 0x1000 ; "分配" W一?4 Kb
test [esp], eax ; 承诺一个新内存(如果q没有承诺)
sub esp, 0x1000 ; "分配" W二? Kb
test [esp], eax ; ...
sub esp, 0x1000
test [esp], eax
注意4KB堆栈指针是如何被修改的,更重要的是,每一步之后堆栈底是如何被“触及”Q要l过查)。这样保证在“分配”Q承诺)另一面之前Q当前页面承诺的范围也包含堆栈底?
“每一个线E到辑օ自己的堆栈空_默认情况下,此空间由承诺的以及预留的内存l成Q每个线E?1 MB预留的内存,以及一|诺的内存Q系l将Ҏ需要从预留的堆栈内存中承诺一内存区?#8221; Q参?MSDN CreateThread > dwStackSize > Thread Stack SizeQ?br> q应该清楚ؓ什么有兟뀀/GS 的文档说在堆栈探针在 Win32 应用E序和Windows NT虚拟内存理器之间进行}慎调整?br>
现在回到我们的ThreadFunc以及 4KB 限制
虽然你可以用 /Gs 防止调用堆栈探测例程Q但在文档对于这L做法l出了警告,此外Q文件描q可以用 #pragma check_stack 指o关闭或打开堆栈探测。但是这个指令好像一点作用都没有Q要么这个文档是垃圾Q要么我疏忽了其它一些信息?Q。MQCreateRemoteThread ?WriteProcessMemory 技术只能用于注入小块代码,所以你的局部变量应该尽量少耗费一些内存字节,最好不要超q?4KB限制?/p>
附录EQ?
int Dummy( int arg1 )
{
int ret =0;
switch( arg1 ) {
case 1: ret = 1; break;
case 2: ret = 2; break;
case 3: ret = 3; break;
case 4: ret = 0xA0B0; break;
}
return ret;
}
~译后变成下面这个样子:
地址 操作?参数 解释后的指o
--------------------------------------------------
; arg1 -> ECX
:00401000 8B4C2404 mov ecx, dword ptr [esp+04]
:00401004 33C0 xor eax, eax ; EAX = 0
:00401006 49 dec ecx ; ECX --
:00401007 83F903 cmp ecx, 00000003
:0040100A 771E ja 0040102A
; JMP 到表***中的地址之一
; 注意 ECX 包含的偏U?
:0040100C FF248D2C104000 jmp dword ptr [4*ecx+0040102C]
:00401013 B801000000 mov eax, 00000001 ; case 1: eax = 1;
:00401018 C3 ret
:00401019 B802000000 mov eax, 00000002 ; case 2: eax = 2;
:0040101E C3 ret
:0040101F B803000000 mov eax, 00000003 ; case 3: eax = 3;
:00401024 C3 ret
:00401025 B8B0A00000 mov eax, 0000A0B0 ; case 4: eax = 0xA0B0;
:0040102A C3 ret
:0040102B 90 nop
; 地址?**
:0040102C 13104000 DWORD 00401013 ; jump to case 1
:00401030 19104000 DWORD 00401019 ; jump to case 2
:00401034 1F104000 DWORD 0040101F ; jump to case 3
:00401038 25104000 DWORD 00401025 ; jump to case 4
注意如何实现q个开兌句?
与其单独查每个CASE语句Q不如创Z个地址表,然后通过单地计算地址表的偏移量而蟩转到正确的CASE语句。这实际上是一U改q。假设你?0个CASE语句。如果不使用上述的技巧,你得执行50?CMP和JMP指o来达到最后一个CASE。相反,有了地址表后Q你可以通过表查询蟩转到MCASE语句Q从计算机算法角度和旉复杂度看Q我们用O(5)代替了O(2n)法。其中:
操作码 指o 描述
FF /4 JMP r/m32 Jump near, absolute indirect,
address given in r/m32
:004014C0 push EBP ; ThreadFunc 的入口点
:004014C1 mov EBP, ESP
...
:004014C5 call 0041550 ; q里ɘq程q程崩溃
...
:00401502 ret
如果 CALL 是由~译器添加的指oQ因为某?#8220;忌” 开兛_/GZ是打开的)Q它被定位?ThreadFunc 的开始的某个地方或者结֤?
]]>
先解释一下远E进E,其实是要植入你的代码的q程Q相对于你的工作q程Q如果叫本地q程的话Q它叫q程q程Q可理解为宿丅R?/p>
首先介绍一下我们的主要工具CreateRemoteThreadQ这里先函数原型简单介l以下?/p>
CreateRemoteThread可将U程创徏在远E进E中?/p>
函数原型
HANDLE CreateRemoteThread(
HANDLE hProcess, // handle to process
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
SIZE_T dwStackSize, // initial stack size
LPTHREAD_START_ROUTINE lpStartAddress, // thread function
LPVOID lpParameter, // thread argument
DWORD dwCreationFlags, // creation option
LPDWORD lpThreadId // thread identifier
);
参数说明Q?br>hProcess
[输入] q程句柄
lpThreadAttributes
[输入] U程安全描述字,指向SECURITY_ATTRIBUTESl构的指?br>dwStackSize
[输入] U程栈大,以字节表C?br>lpStartAddress
[输入] 一个LPTHREAD_START_ROUTINEcd的指针,指向在远E进E中执行的函数地址
lpParameter
[输入] 传入参数
dwCreationFlags
[输入] 创徏U程的其它标?/p>
lpThreadId
[输出] U程w䆾标志Q如果ؓNULL,则不q回
q回?br>成功q回新线E句柄,p|q回NULLQƈ且可调用GetLastError获得错误倹{?/p>
接下来我们将以两U方式用CreateRemoteThreadQ大家可以领略到CreateRemoteThread的神通,它你的代码可以q你的q程Q植入到别的q程中运行?/p>
W一U方?
W一U方式,我们使用函数的Ş式。即我们自q序中的一个函数植入到q程q程中?/p>
步骤1Q首先在你的q程中创建函数MyFuncQ我们将把它攑֜另一个进E中q行Q这里以windows
计算器ؓ目标q程?br>static DWORD WINAPI MyFunc (LPVOID pData)
{
//do something
//...
//pData输入可以是Mcd?br>//q里我们会传入一个DWORD的值做CZQƈ且简单返?br>return *(DWORD*)pData;
}
static void AfterMyFunc (void) {
}
q里有个技巧,定义了一个static void AfterMyFunc (void)Qؓ了下面确定我们的代码大小
步骤2Q定位目标进E,q里是一个计器
HWND hStart = ::FindWindow (TEXT("SciCalc"),NULL);
步骤3Q获得目标进E句柄,q里用到两个不太常用的函敎ͼ当然如果l常做线E?q程{方面的 目的话Q就很面熟了Q,但及有用
DWORD PID, TID;
TID = ::GetWindowThreadProcessId (hStart, &PID);
HANDLE hProcess;
hProcess = OpenProcess(PROCESS_ALL_ACCESS,false,PID);
步骤4Q在目标q程中配变量地址I间Q这里我们分?0个字节,q且讑֮为可以读
写PAGE_READWRITEQ当然也可设为只ȝ其它标志Q这里就不一一说明了?br>char szBuffer[10];
*(DWORD*)szBuffer=1000;//for test
void *pDataRemote =(char*) VirtualAllocEx( hProcess, 0, sizeof(szBuffer), MEM_COMMIT,
PAGE_READWRITE );
步骤5Q写内容到目标进E中分配的变量空?br>::WriteProcessMemory( hProcess, pDataRemote, szBuffer,(sizeof(szBuffer),NULL);
步骤6Q在目标q程中分配代码地址I间
计算代码大小
DWORD cbCodeSize=((LPBYTE) AfterMyFunc - (LPBYTE) MyFunc);
分配代码地址I间
PDWORD pCodeRemote = (PDWORD) VirtualAllocEx( hProcess, 0, cbCodeSize, MEM_COMMIT,
PAGE_EXECUTE_READWRITE );
步骤7Q写内容到目标进E中分配的代码地址I间
WriteProcessMemory( hProcess, pCodeRemote, &MyFunc, cbCodeSize, NULL);
步骤8Q在目标q程中执行代?/p>
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) pCodeRemote,
pDataRemote, 0 , NULL);
DWORD h;
if (hThread)
{
::WaitForSingleObject( hThread, INFINITE );
::GetExitCodeThread( hThread, &h );
TRACE("run and return %d\n",h);
::CloseHandle( hThread );
}
q里有几个值得说明的地?
使用WaitForSingleObject{待U程l束;
使用GetExitCodeThread获得q回|
最后关闭句柄CloseHandle?/p>
步骤9Q清理现?/p>
释放I间
::VirtualFreeEx( hProcess, pCodeRemote,
cbCodeSize,MEM_RELEASE );
::VirtualFreeEx( hProcess, pDataRemote,
cbParamSize,MEM_RELEASE );
关闭q程句柄
::CloseHandle( hProcess );
W二U方?
W二U方式,我们使用动态库的Ş式。即我们自׃个动态库植入到远E进E中?/p>
q里不再重复上面相同的步?只写出其中关键的地方.
关键1:
在步?中将动态库的\径作为变量传入变量空?
关键2:
在步??GetProcAddress作ؓ目标执行函数.
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE )::GetProcAddress(
hModule, "LoadLibraryA"),
pDataRemote, 0, NULL );
另外在步?,清理现场中首先要先进行释放我们的动态库.也即cM步骤8执行函数FreeLibrary
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE )::GetProcAddress(
hModule, "FreeLibrary"),
(void*)hLibModule, 0, NULL );
好了,限于幅不能够介l的很细,在用过E中如有疑问可向作者咨?Q开发环境:windows2000/vc6.0Q?
本文来自CSDN博客Q{载请标明出处Q?a >http://blog.csdn.net/fangchao918628/archive/2008/08/30/2852744.aspx
在主文g中,?pragma data_seg建立一
个新的数据段q定义共享数据,其具体格式ؓQ?
#pragma data_seg Q?shareddata") //名称可以
//自己定义Q但必须与下面的一致?
HWND sharedwnd=NULL;//׃n数据
#pragma data_seg()
仅定义一个数据段q不能达到共享数据的目的Q还要告诉编译器该段的属性,有两U方法可以实现该目的 Q其效果是相同的Q,一U方法是?DEF文g中加入如下语句: SETCTIONS shareddata READ WRITE SHARED 另一U方法是在项目设|链接选项(Project Setting --〉Link)中加入如下语句: /SECTION:shareddata,rws
W一点:什么是׃n数据D?Z么要用共享数据段Q?它有什么用途?Q?
在Win16环境中,DLL的全局数据Ҏ个蝲入它的进E来说都是相同的Q而在Win32环境中,情况却发生了变化QDLL函数中的代码所创徏的Q何对象(包括变量Q都归调用它的线E或q程所有。当q程在蝲入DLLӞ操作pȝ自动把DLL地址映射到该q程的私有空_也就是进E的虚拟地址I间Q而且也复制该DLL的全局数据的一份拷贝到该进E空间。也是说每个进E所拥有的相同的DLL的全局数据Q它们的名称相同Q但其值却q不一定是相同的,而且是互不干涉的?
因此Q在Win32环境下要惛_多个q程中共享数据,必进行必要的讄。在讉K同一个Dll的各q程之间׃n存储器是通过存储器映文件技术实现的。也可以把这些需要共享的数据分离出来Q放|在一个独立的数据D里Qƈ把该D늚属性设|ؓ׃n。必ȝq些变量赋初|否则~译器会把没有赋初始值的变量攑֜一个叫未被初始化的数据D中?
#pragma data_seg预处理指令用于设|共享数据段。例如:
#pragma data_seg("SharedDataName") HHOOK hHook=NULL; //必须在定义的同时q行初始?!!!#pragma data_seg()
?pragma data_seg("SharedDataName")?pragma data_seg()之间的所有变量将被访问该Dll的所有进E看到和׃n。再加上一条指?pragma comment(linker,"/section:.SharedDataName,rws"),[注意Q数据节的名Uis case sensitive]那么q个数据节中的数据可以在所有DLL的实例之间共享。所有对q些数据的操作都针对同一个实例的Q而不是在每个q程的地址I间中都有一份?
当进E隐式或昑ּ调用一个动态库里的函数Ӟpȝ都要把这个动态库映射到这个进E的虚拟地址I间?以下U?地址I间")。这使得DLL成ؓq程的一部分Q以q个q程的n份执行,使用q个q程的堆栈?q项技术又叫code Injection技术,被广泛地应用在了病毒、黑客领域!呵呵^_^)
W二点:在具体用共享数据段旉要注意的一些问题!
Win32 DLLs are mapped into the address space of the calling process. By default, each process using a DLL has its own instance of all the DLLs global and static variables. (注意: 即是全局变量和静态变量也都不是共享的!) If your DLL needs to share data with other instances of it loaded by other applications, you can use either of the following approaches:
· Create named data sections using the data_seg pragma.
· Use memory mapped files. See the Win32 documentation about memory mapped files.
Here is an example of using the data_seg pragma:
#pragma data_seg (".myseg")
int i = 0;
char a[32] = "hello world";
#pragma data_seg()
data_seg can be used to create a new named section (.myseg in this example). The most typical usage is to call the data segment .shared for clarity. You then must specify the correct sharing attributes for this new named data section in your .def file or with the linker option /SECTION:.MYSEC,RWS. (q个~译参数既可以用pragma指o来指定,也可以在VC的IDE中指定!)
There are restrictions to consider before using a shared data segment:
· Any variables in a shared data segment must be statically initialized. In the above example, i is initialized to 0 and a is 32 characters initialized to hello world.
· All shared variables are placed in the compiled DLL in the specified data segment. Very large arrays can result in very large DLLs. This is true of all initialized global variables.
· Never store process-specific information in a shared data segment. Most Win32 data structures or values (such as HANDLEs) are really valid only within the context of a single process.
· Each process gets its own address space. It is very important that pointers are never stored in a variable contained in a shared data segment. A pointer might be perfectly valid in one application but not in another.
· It is possible that the DLL itself could get loaded at a different address in the virtual address spaces of each process. It is not safe to have pointers to functions in the DLL or to other shared variables.
其格式一般ؓ: #Pragma Para
其中Para 为参敎ͼ下面来看一些常用的参数?/font>
Message 参数是我最喜欢的一个参敎ͼ它能够在~译信息输出H口中输出相应的信息Q这对于源代码信息的控制是非帔R要的。其使用Ҏ为:
#Pragma message(“消息文本”)
当编译器遇到q条指o时就在编译输出窗口中消息文本打印出来?br>当我们在E序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有正确的设|这些宏Q此时我们可以用q条指o在编译的时候就q行查?br>假设我们希望判断自己有没有在源代码的什么地方定义了_X86q个宏可以用下面的方?br>#ifdef _X86
#Pragma message(“_X86 macro activated!”)
#endif
当我们定义了_X86q个宏以后,应用E序在编译时׃在编译输出窗口里昄“_
X86 macro activated!”。我们就不会因ؓ不记得自己定义的一些特定的宏而抓x腮了?/font>
格式如:
#pragma code_seg( ["section-name"[,"section-class"] ] )
它能够设|程序中函数代码存放的代码段Q当我们开发驱动程序的时候就会用到它?/font>
只要在头文g的最开始加入这条指令就能够保证头文件被~译一ơ,q条指o实际上在VC6中就已经有了Q但是考虑到兼Ҏƈ没有太多的用它?/font>
表示预编译头文g到此为止Q后面的头文件不q行预编译。BCB可以预编译头文g以加快链接的速度Q但如果所有头文g都进行预~译又可能占太多盘I间Q所以用这个选项排除一些头文g?br>有时单元之间有依赖关p,比如单元A依赖单元BQ所以单元B要先于单元A~译。你可以?pragma startup指定~译优先U,如果使用?pragma package(smart_init) QBCB׃Ҏ优先U的大小先后~译?/font>
#pragma resource "*.dfm"表示?.dfm文g中的资源加入工程?.dfm中包括窗体外观的定义?/font>
应用E序的入口文件前面加?/font>
#pragma data_seg("flag_data")
int app_count = 0;
#pragma data_seg()
#pragma comment(linker,"/SECTION:flag_data,RWS")
然后E序启动的地方加?/font>
if(app_count>0) // 如果计数大于0Q则退出应用程序?br>{
//MessageBox(NULL, "已经启动一个应用程?, "Warning", MB_OK);
//printf("no%d application", app_count);
return FALSE;
}
app_count++;
Windows 在一个Win32E序的地址I间周围{了一道墙。通常Q一个程序的地址I间中的数据是私有的Q对别的E序而言是不可见的。但是执行STRPROG的多个执行实体表CZSTRLIB在程序的所有执行实体之间共享数据是毫无问题的。当您在一个STRPROGH口中增加或者删除一个字W串Ӟq种改变立卛_映在其它的窗口中?/font>
在全部例E之_STRLIB׃n两个变量Q一个字W数l和一个整敎ͼ记录已储存的有效字符串的个数Q。STRLIB这两个变量储存在共享的一个特D内存区D中Q?/font>
#pragma data_seg ("shared")
int iTotal = 0 ;
WCHAR szStrings [MAX_STRINGS][MAX_LENGTH + 1] = { '\0' } ;
#pragma data_seg ()
W一?pragma叙述建立数据D,q里命名为shared。您可以这D命名ؓM一个您喜欢的名字。在q里?pragma叙述之后的所有初始化了的变量都放在shared数据D中。第二个#pragma叙述标示D늚l束。对变量q行专门的初始化是很重要的,否则~译器将把它们放在普通的未初始化数据D中而不是放在shared中?/font>
q结器必ȝ道有一个「shared」共享数据段。在「Project Settings」对话框选择「Link」页面卷标。选中「STRLIB」时在「Project Options」字D(在Release和Debug讑֮中均可)Q包含下面的q结叙述Q?/font>
/SECTION:shared,RWS
字母RWS表示D具有读、写和共享属性。或者,您也可以直接用DLL原始码指定连l选项Q就像我们在STRLIB.C那样Q?/font>
#pragma comment(linker,"/SECTION:shared,RWS")
׃n的内存段允许iTotal变量和szStrings字符串数l在STRLIB的所有例E之间共享。因为MAX_STRINGS{于256Q?MAX_LENGTH{于63Q所以,׃n内存D늚长度?2,772字节QiTotal变量需?字节Q?56个指针中的每一个都需?28字节?/font>
白杨
“在正的场合使用恰当的特?#8221; 对称职的C++E序员来说是一个基本标准。想要做到这点,首先要了解语a中每个特性的实现方式及其开销。本文主要讨论相对于传统 C 而言Q对效率有媄响的几个C++新特性?/p>
相对于传l的 C 语言QC++ 引入的额外开销体现在以下两个方面:
模板、类层次l构、强cd查等新特性,以及大量使用了这些新Ҏ的 STL 标准库都增加了编译器负担。但是应当看刎ͼq些新机能在不降低,甚至Q由于模板的内联能力Q提升了E序执行效率的前提下Q明昑ևMq大 C++ E序员的工作量?
用几U钟的CPU旉换取几h日的辛勤力_Q附带节省了日后调试和维护代码的旉Q这点开销当算倹{?/p> 当然Q在使用q些Ҏ的时候,也有不少优化技巧。比如:~译一?q泛依赖模板库的大型软gӞ几条昑ּ实例化指令就可能使编译速度提高几十倍;恰当地组合用部分专门化和完全专门化Q不但可以最优化E序的执行效率,q可以让同时使用多种不同参数实例化一套模板的E序体积显著减小…… |
q行时开销恐怕是E序员最兛_的问题之一了。相对与传统CE序而言QC++中有可能引入额外q行时开销的新Ҏ包括:
关于其中W四点:异常Q对于大多数C~译器来_在正常情况(未抛出异常)下,try块中的代码执行效率和普通代码一样高Q而且׃不再需要用传l上通过q回值或函数调用来判断错误的方式Q代码的实际执行效率q可能进一步提高。抛出和捕捉异常的效率也只是在某些情况下才会E低于函数正常返回的效率Q何况对于一个编写良好的E序Q抛出和捕捉异常的机会应该不多。关于异怋用的详细讨论Q参见:C++~码规范正文中的相关部分?a >C++异常机制的实现方式和开销分析一节?/p> 而第五点Q对象的构造和析构开销也不L存在。对于不需要初始化/销毁的cdQƈ没有构造和析构的开销Q相反对于那些需要初始化/销毁的cd来说Q即使用传统的C方式实现Q也臛_需要与之相当的开销。这里要注意的一Ҏ量不要让构造和析构函数q于臃肿Q特别是在一个类层次l构中更要注意。时M持你的构造、析构函C只有最必要的初始化和销毁操作,把那些ƈ不是每个Q子Q对象都需要执行的操作留给其他Ҏ和派生类去解冟?/p> 其实对一个优U的编译器而言QC++的各U特性本w就是用C/汇编加以千锤癄而最优化实现的。可以说Q想用C甚至汇编比编译器更高效地实现某个C++Ҏ几乎是不可能的。要是真能做到这一点的话,大侠应该去写个~译器造福q大E序员才对~ C++之所?被广泛认为比C“低效”Q其Ҏ原因在于Q由于程序员Ҏ些特性的实现方式及其产生的开销不够了解Q致使他们在错误的场合用了错误的特性。而这些错误基本都集中在:
其中前两点上文已l讲q,下面讨论W三炏V?/p> Z说明RTTI、虚函数和虚基类的实现方式,q里首先l出一个经典的菱Şl承实例Q及其具体实玎ͼZ便于理解Q这里故意忽略了一些无关紧要的优化Q: |
图中虚箭头代表偏U,实箭头代表指?/p>
׃囑־到每U特性的q行时开销如下Q?
可见Q关于老天“饿时掉馅饹{睡时掉老婆”{美好传说纯属谣a。但凡h工制品必不完,L设计上的取舍Q有光应的场合也有其不适用的地斏V?/p> C++中的每个Ҏ,都是从程序员qx的生产生zM逐渐_而来的。在不正的场合使用它们必然会引起逻辑、行为和性能上的问题。对于上q特性,应该只在必要、合理的前提下才使用?/p> "dynamic_cast" 用于在类层次l构中O游,Ҏ针或引用q行自由的向上、向下或交叉强制?typeid" 则用于获取一个对象或引用的确切类型,?"dynamic_cast" 不同Q将 "typeid" 作用于指针通常是一个错误,要得C个指针指向之对象的type_infoQ应当先其解引用(例如Q?typeid(*p);"Q?/p> 一般地Ԍ能用虚函数解决的问题׃要用 "dynamic_cast"Q能够用 "dynamic_cast" 解决的就不要?"typeid"。比如:
以上代码?"dynamic_cast" 写会E好一点,当然最好的方式q是在CShape里定义名?"rotate" 的虚函数?/p> 虚函数是C++众多q行时多态特性中开销最,也最常用的机制。虚函数的好处和作用q里不再多说Q应当注意在Ҏ能有苛刻要求的场合Q或者需要频J调用,Ҏ能影响较大的地方(比如每秒钟要调用成千上万ơ,而自w内容又很简单的事g处理函数Q要慎用虚函数?/p> 需要特别说明的一ҎQ虚函数的调用开销与通过函数指针的间接函数调用(例如Q经典CE序中常见的Q通过指向l构中的一个函数指针成员调用;以及调用DLL/SO中的函数{常见情况)是相当的。比起函数调用本w的开销Q保存现?>传递参?>传递返回?>恢复现场Q来_一ơ指针间接引用是微不道的。这׃?strong>在绝大部分可以用函数的场合中都能够负担得v虚方?/strong>的些微额外开销?/p> 作ؓ一U支持多l承的面向对象语aQ虚基类有时是保证类层次l构正确一致的一U必不可的手段。但在需要频J用基cL供的服务Q又Ҏ能要求较高的场合,应该量避免使用它。在基类中没有数据成员的场合Q也可以解除使用虚基cR例如,在上图中Q如果类 "BB" 中不存在数据成员Q那?"BB" 可以作Z个普通基cd别被 "B1" ?"B2" l承。这L优化在达到相同效果的前提下,解除了虚基类引v的开销。不q这U优化也会带来一些问题:?"DD" 向上强制?"BB" 时会引v歧义Q破坏了cdơ结构的逻辑关系?/p> 上述Ҏ的I间开销一般都是可以接受的Q当然也存在一些特例,比如Q在存储布局需要和传统Cl构兼容的场合、在考虑寚w的场合、在需要ؓ一个本来尺寸很的cd时实例化许多对象的场合等{?/p> |