<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          C++常見的三種內(nèi)存破壞的場(chǎng)景和分析

          共 866字,需瀏覽 2分鐘

           ·

          2021-10-22 10:20

          有一定C++開發(fā)經(jīng)驗(yàn)的同學(xué)大多數(shù)踩過內(nèi)存破壞的坑,有這么幾種現(xiàn)象:
          1. 比如某個(gè)變量整形,在程序中只可能初始化或者賦值為1或者2, 但是在使用的時(shí)候卻發(fā)現(xiàn)其為0或者其他的情況。對(duì)于其他類型,比如字符串等,可能出現(xiàn)了一種出乎意料的值!
          2. 程序在堆上申請(qǐng)內(nèi)存或者釋放內(nèi)存的時(shí)候,在內(nèi)存充足的情況下,居然出現(xiàn)了堆錯(cuò)誤。

          當(dāng)出現(xiàn)以上場(chǎng)景的時(shí)候,你該思考一下,是不是出現(xiàn)了內(nèi)存破壞的情況了。而本文主要通過展示和分析常見的三種內(nèi)存破壞導(dǎo)致覆蓋相鄰變量的場(chǎng)景,讓讀者在碰到類似的場(chǎng)景,不至于束手無策。而對(duì)于堆上的內(nèi)存破壞,很常見并且棘手的場(chǎng)景,本人將在后續(xù)的文章和大家分享。


          1. 內(nèi)存破壞之強(qiáng)制類型轉(zhuǎn)換

          大家都知道不匹配的類型強(qiáng)制轉(zhuǎn)換會(huì)帶來一些bug,比如intunsigned int互相轉(zhuǎn)換,又或者int__int64強(qiáng)行轉(zhuǎn)換。是不是每次當(dāng)讀起這類文章起來如雷貫耳,但是當(dāng)自己去寫代碼的時(shí)候還是容易犯錯(cuò)?這也就是為什么C++容易寫出的原因,明知可能有錯(cuò),還難以避免。這往往是因?yàn)檎鎸?shí)的項(xiàng)目中復(fù)雜程度,往往讓人容易忽略這些細(xì)節(jié)。
          不少老的工程代碼還是采用VC6編譯,為了安全問題或者使用C++新特性需要將VC6升級(jí)到更新的Visual Studio。接下來要介紹的一個(gè)樣例程序,就是隱藏于代碼中的一個(gè)問題,如果從VC6升級(jí)到VS2017的時(shí)候會(huì)帶來問題嗎?可以先找找看:
          #include #include 
          class DemoClass{public: DemoClass() : m_bInit(true), m_tRecordTime(0) { time((time_t *)(&m_tRecordTime)); };
          void DoSomething(){ if (m_bInit) std::cout << "Do Task!" << std::endl; }
          private: int m_tRecordTime; bool m_bInit;};
          int main(){ DemoClass testObj; testObj.DoSomething(); return 0;}
          Do Task!這個(gè)字符串會(huì)不會(huì)打印出來呢? 可以發(fā)現(xiàn)這段程序在VC6中可以打印出來,但是在VS2017中卻打印不出來了。那是因?yàn)槿缦略?
          函數(shù)原型time_t time( time_t *destTime );,在VC6time_t默認(rèn)是32位,而在VS2017中默認(rèn)是64位。早期程序以為32位中表達(dá)最大的時(shí)間是2038年,那時(shí)候完全夠用,但隨著計(jì)算機(jī)本身的發(fā)展64位逐漸成為主流time_t在最新的編譯器中也默認(rèn)采用64位,這樣時(shí)間完全夠用以億年為單位了,那時(shí)候計(jì)算機(jī)發(fā)展超出我們想象了。

          程序的問題所在m_tRecordTime采用的是int類型,默認(rèn)為32位,那么其地址作為time_t time( time_t *destTime );函數(shù)實(shí)參后,在VC6time_t本身為32位自然也不會(huì)出錯(cuò),但是在VS2017中因?yàn)?code style="box-sizing: border-box;font-family: "Source Code Pro", "DejaVu Sans Mono", "Ubuntu Mono", "Anonymous Pro", "Droid Sans Mono", Menlo, Monaco, Consolas, Inconsolata, Courier, monospace, "PingFang SC", "Microsoft YaHei", sans-serif;font-size: 14px;background-color: rgb(249, 242, 244);border-radius: 2px;padding: 2px 4px;line-height: 22px;color: rgb(199, 37, 78);">time_t為64位,則time((time_t *)(&m_tRecordTime));后寫入了一個(gè)64位的值。結(jié)合下圖,看下這個(gè)對(duì)象的內(nèi)存布局,m_bInit的值將會(huì)被覆蓋,而這里原先的m_bInit的值為1,被覆蓋為0,從而導(dǎo)致內(nèi)存破壞,導(dǎo)致程序執(zhí)行意想不到的結(jié)果。這里只是不輸出,那在真實(shí)程序中,可能會(huì)導(dǎo)致某個(gè)邏輯錯(cuò)亂,發(fā)生嚴(yán)重的問題

          這個(gè)問題修改自然比較簡(jiǎn)單,將m_tRecordTime定義為time_t類型就可以了。
          如果有類似的問題發(fā)生的時(shí)候,比如這個(gè)變量的可疑的發(fā)生了不該有的變化的時(shí)候,你可以查看下這個(gè)變量定義的附近是否有內(nèi)存的操作可能產(chǎn)生溢出,找到問題所在。因?yàn)閮?nèi)存上溢的比較多,一般可以查看下定義在當(dāng)前出現(xiàn)問題的變量的低地址出的變量操作,是否存在可疑的地方。
          最后,針對(duì)這種場(chǎng)景,我們是不是也可以得到一些收獲呢,個(gè)人總結(jié)如下兩點(diǎn):
          1. 在定義類型的時(shí)候,盡量和原始類型一致,比如這里的time_t有些程序員可能慣性的認(rèn)為就是32位,那就定義一個(gè)時(shí)間戳的時(shí)候就定義為int了,而我們要做的應(yīng)該是和原始類型匹配(也就是函數(shù)的輸入類型),將其定義為time_t,于此類似的還有size_t等,這樣可以避免未來在數(shù)據(jù)集變化或者做平臺(tái)遷移的時(shí)候造成不必要的麻煩。
          2. 在有一些復(fù)雜的場(chǎng)景的下,也許你不得不做類型轉(zhuǎn)換,而這個(gè)時(shí)候就格外的需要注意或者了解清楚,轉(zhuǎn)換帶來的情況和后果,保持警惕,否則就可能是一個(gè)潛在的bug。這和開車一樣,當(dāng)你開車的時(shí)候如果看到前方車輛忽然產(chǎn)生一個(gè)不合常理的變道行為,首先要做的不是噴那輛車,而是集中注意力,看看是否更前方有障礙物或者事故放生,做出相應(yīng)的反應(yīng)。


          2. 字符串拷貝溢出

          這種情況應(yīng)該是最常見了,我們來看一看樣例程序:
          #include #define BUFER_SIZE_STR_1 5#define BUFER_SIZE_STR_2 8class DemoClass{public:  void DoSomething(){    strcpy(m_str1, "Hi Coder!");    std::cout << m_str1 << std::endl;    std::cout << m_str2 << std::endl;  }
          private: char m_str1[BUFER_SIZE_STR_1] = { 0 }; char m_str2[BUFER_SIZE_STR_2] = { 0 };};
          int main(){ DemoClass testObj; testObj.DoSomething(); return 0;}

          這種情況下肉眼可以分析的,輸出結(jié)果為:

          m_str1的空間為5,但是Hi Coder!包含\010個(gè)字符,在調(diào)用strcpy(m_str1, "Hi Coder!");的時(shí)候超過了m_str1的空間,于是覆蓋了m_str2的內(nèi)存,從而導(dǎo)致內(nèi)存破壞。內(nèi)存溢出這種尤其字符串溢出,程序崩潰可能是小事兒,如果是一個(gè)廣為流傳的軟件,那么就很有可能會(huì)被黑客所利用。比如看看這篇文章,就講解了一種利用內(nèi)存溢出的場(chǎng)景<<棧上內(nèi)存溢出漏洞利用之Return Address>>

          這種字符串場(chǎng)景如何分析呢,如果程序崩潰了,可以收集Dump先看看被覆蓋的地方是什么樣的字符串,然后聯(lián)想看看自己的程序哪里有可能對(duì)這個(gè)字符串的操作,從而找到原因。別小看這種方法,簡(jiǎn)單粗暴很有用,曾經(jīng)就用這種方式分析過Linux驅(qū)動(dòng)模塊的內(nèi)存泄露問題。
          那如果還找不到問題呢?如果問題還能重現(xiàn),那還是有調(diào)試手法的,下一節(jié)將會(huì)進(jìn)行講解。當(dāng)然最差最差的還是不要放棄代碼審查。尤其在這個(gè)內(nèi)存被破壞的附近的邏輯。
          對(duì)于這種場(chǎng)景的建議,比較簡(jiǎn)單就是使用微軟安全函數(shù)strcpy_s,注意這里雖然列出了返回值errno_t不過對(duì)于微軟的實(shí)現(xiàn)來說,如果是目標(biāo)內(nèi)存空間不夠的情況下,在Relase版本下會(huì)調(diào)用TerminateProcess, 并且要注意的是這個(gè)時(shí)候抓Dump有時(shí)候并不是完整的Dump,可以采用這篇文章的方法<<程序Crash了卻無法捕獲正確的函數(shù)調(diào)用棧?>>。至于微軟為什么要這樣做,有可能是安全的考慮比崩潰優(yōu)先級(jí)更高,于是在內(nèi)存溢出不夠的時(shí)候,直接讓程序結(jié)束。
          errno_t strcpy_s(
          char *dest,
          rsize_t dest_size,
          const char *src
          );

          3. 隨機(jī)性的內(nèi)存被修改

          這一個(gè)一聽都快崩潰了,C++的坑能不能少一點(diǎn)呢。但是確實(shí)是會(huì)有各種各樣的場(chǎng)景讓你落入坑內(nèi)。上一節(jié)的程序我稍作修改:
          #include #define BUFER_SIZE_STR_1 5#define BUFER_SIZE_STR_2 8
          class DemoClass{public: void DoSomething(){ strcpy_s(m_str2, BUFER_SIZE_STR_2, "Coder"); strcpy_s(m_str1, BUFER_SIZE_STR_1, "Test");
          //Notice this line: m_str1[BUFER_SIZE_STR_2 - 1] = '\0';
          std::cout << m_str1 << std::endl; std::cout << m_str2 << std::endl; }
          private: char m_str1[BUFER_SIZE_STR_1] = { 0 }; char m_str2[BUFER_SIZE_STR_2] = { 0 };};
          int main(){ DemoClass testObj; testObj.DoSomething(); return 0;}

          程序本意是m_str2賦值為Coder,?m_str1賦值為Test, 在編程中很多字符串拷貝或者操作中有些是在字符串末尾補(bǔ)\0有的可能不補(bǔ)\0, 而在本例中實(shí)際上strcpy_s會(huì)自動(dòng)補(bǔ)0,但是有的程序員防止萬一,字符串靠背后,在數(shù)組的最后一位設(shè)置為’\0’。這種有時(shí)候就變成了好心辦壞事。比如這里的m_str1[BUFER_SIZE_STR_2 - 1] = '\0';?,大家注意到?jīng)],這里應(yīng)該改寫為m_str1[BUFER_SIZE_STR_1 - 1] = '\0';?,也就是說程序員可能拷貝代碼或者不小心寫錯(cuò)了BUFER_SIZE_STR_2BUFER_SIZE_STR_1因?yàn)閮烧吆瓴畈欢?。只要是人寫代碼,就有可能會(huì)犯這種錯(cuò)誤。這個(gè)程序的輸出變?yōu)?

          這個(gè)程序是比較簡(jiǎn)單,一目了然,但是在大型程序中呢,這個(gè)數(shù)組的位置跳躍的訪問到了其他變量的位置,你首先得判斷這個(gè)被跳躍式修改的變量,是不是程序本意造成的,因?yàn)榛旌狭诉@么多的猜想,可能會(huì)導(dǎo)致分析變的異常復(fù)雜。那么有什么好的方法嗎?只要程序能偶爾重現(xiàn)這個(gè)問題,那就是有方法的。

          通過Windbg調(diào)試命令ba可以在指定的內(nèi)存地址做操作的時(shí)候進(jìn)入斷點(diǎn)。假設(shè)目前已經(jīng)知道m_str2的第四個(gè)字符,總是被某個(gè)地方誤寫,那么我們可以在這個(gè)地址處設(shè)置一個(gè)ba命令: 當(dāng)寫的這個(gè)內(nèi)存地址的時(shí)候進(jìn)入斷點(diǎn)。不過這樣還是有個(gè)問題,那就是程序中有可能有很多次對(duì)這塊內(nèi)存的寫操作,有時(shí)候是正常的寫操作,如果一直進(jìn)入斷點(diǎn),人工分析將會(huì)非常累,不現(xiàn)實(shí)。
          這個(gè)時(shí)候有個(gè)方法,同時(shí)也是一個(gè)workaround,就是當(dāng)你還沒找到程序出錯(cuò)的根本原因的時(shí)候在被誤踩的內(nèi)存前面加上一個(gè)足夠大的不使用的空間。比如下面的代碼, m_str2總是被誤寫,于是在m_str2的前面加上一個(gè)100個(gè)字節(jié)的不使用的內(nèi)存m_strUnused(因?yàn)橐话愠绦騼?nèi)存溢出是上溢,當(dāng)然也可以在m_str2的后面同樣加上)。這樣我們被踩的內(nèi)存就很容易落在m_strUnused空間里面了,這個(gè)時(shí)候我們?cè)谄淇臻g里設(shè)置寫內(nèi)存操作的斷點(diǎn),就容易捕獲到問題所在了。
          #include #define BUFER_SIZE_STR_1 5#define BUFER_SIZE_STR_2 8#define BUFFER_SIZE_UNUSED 100class DemoClass{public:  void DoSomething(){    strcpy_s(m_str2, BUFER_SIZE_STR_2, "Coder");    strcpy_s(m_str1, BUFER_SIZE_STR_1, "Test");
          //Notice this line: m_str1[BUFER_SIZE_STR_2 - 1] = '\0';
          std::cout << m_str1 << std::endl; std::cout << m_str2 << std::endl; }
          private: char m_str1[BUFER_SIZE_STR_1] = { 0 }; char m_strUnused[BUFFER_SIZE_UNUSED] = { 0 }; char m_str2[BUFER_SIZE_STR_2] = { 0 };};
          int main(){ DemoClass testObj; testObj.DoSomething(); return 0;}
          下面完整的展示一下分析過程:
          第一步?用Windbg啟動(dòng)(有的情況下可能是Attach,根據(jù)情況而定)到調(diào)試進(jìn)程,設(shè)置main的斷點(diǎn)
          0:000> bp ObjectMemberBufferOverFllow!main*** WARNING: Unable to verify checksum for ObjectMemberBufferOverFllow.exe0:000> gBreakpoint 0 hiteax=010964c0 ebx=00e66000 ecx=00000000 edx=00000000 esi=75aae0b0 edi=0109b390eip=003a1700 esp=00defa00 ebp=00defa44 iopl=0         nv up ei pl nz na pe nccs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206ObjectMemberBufferOverFllow!main:003a1700 55              push    ebp
          第二步?使用p命令單步執(zhí)行代碼到testObj.DoSomething();
          第三步?找到testObj的地址為00def984
          0:000> dv /t /v00def984          class DemoClass testObj = class DemoClass
          第四步?設(shè)置斷點(diǎn)到testObj相對(duì)偏移的位置,這個(gè)位置即&m_str1+BUFER_SIZE_STR_2 - 1?=?&m_str1+7。并且繼續(xù)執(zhí)行代碼:
          0:000> ba w1 00def984+70:000> g
          第五步?你會(huì)發(fā)現(xiàn)程序運(yùn)行進(jìn)入斷點(diǎn),這個(gè)時(shí)候查看對(duì)應(yīng)的函數(shù)調(diào)用棧即可。這個(gè)斷點(diǎn)不一定在一個(gè)非常精確的位置,但是當(dāng)你按照函數(shù)調(diào)用棧去閱讀附近的代碼,便比較容易找出問題所在了。
          0:000> k # ChildEBP RetAddr  00 00def97c 003a1720 ObjectMemberBufferOverFllow!DemoClass::DoSomething+0x41 [......\strcpybufferoverflow.cpp @ 16]01 00def9fc 003a1906 ObjectMemberBufferOverFllow!main+0x20 [......\strcpybufferoverflow.cpp @ 30]02 (Inline) -------- ObjectMemberBufferOverFllow!invoke_main+0x1c [d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78]03 00defa44 75818494 ObjectMemberBufferOverFllow!__scrt_common_main_seh+0xfa [d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]04 00defa58 770a40e8 KERNEL32!BaseThreadInitThunk+0x2405 00defaa0 770a40b8 ntdll!__RtlUserThreadStart+0x2f06 00defab0 00000000 ntdll!_RtlUserThreadStart+0x1b

          總結(jié)

          以上對(duì)三種內(nèi)存破壞場(chǎng)景做了分析,在實(shí)際應(yīng)用中將會(huì)變的更加復(fù)雜。在寫代碼的時(shí)候要注意避開其中的坑,有個(gè)叫做墨菲定律,你感覺可能會(huì)出問題的地方,那它一定會(huì)在某個(gè)時(shí)刻出現(xiàn),當(dāng)你對(duì)某個(gè)地方有所疑慮的時(shí)候一定要多加考慮,否則這個(gè)坑可能查找的時(shí)間,比寫代碼的時(shí)間要長的許多,更可怕的是可能會(huì)帶來意想不到的后果。
          同樣的分析問題要保持足夠的耐心,相信真相總會(huì)出現(xiàn),這樣的底氣也是來自于自己平時(shí)不斷的學(xué)習(xí)和實(shí)踐。
          內(nèi)存破壞問題不區(qū)分棧上還是堆上,我們?cè)诋a(chǎn)品中離不開使用堆開間,而且由多個(gè)模塊核心功能模塊組成,而這些模塊通常是公用一個(gè)進(jìn)程默認(rèn)堆的。所以也有人推薦在這些關(guān)鍵模塊中,各自創(chuàng)建一個(gè)獨(dú)立的堆,從而降低一個(gè)堆內(nèi)存的使用對(duì)另一個(gè)堆中內(nèi)存的影響。雖然不是完全隔離,但是也是一個(gè)聊勝于無的操作了。
          對(duì)于堆內(nèi)存破壞的處理,往往會(huì)伴隨著這種現(xiàn)象:“為什么在我機(jī)器上跑的好好的,你的機(jī)器上不行”, “為什么機(jī)器重啟后問題就不再出現(xiàn)了?” 等等。本人將在后續(xù)文章中和大家分享如何確認(rèn)堆破壞以及如何查找堆破壞的罪魁禍?zhǔn)住?/span>
          瀏覽 70
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91视频在线免费观看 | 欧美日韩中文字幕视频 | 色多多视频在线观看 | 无码AV在线免费观看 | 色免男免费香蕉视频 |