• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            loop_in_codes

            低調(diào)做技術(shù)__歡迎移步我的獨(dú)立博客 codemaro.com 微博 kevinlynx

            記一次堆棧平衡錯(cuò)誤

            最近在一個(gè)使用Visual Studio開(kāi)發(fā)的C++程序中,出現(xiàn)了如下錯(cuò)誤:

            Run-Time Check Failure #0 - The value of ESP was not properly saved across a function call. This is usually a result of calling a function declared with one calling convention with a function pointer declared with a different calling convention.

            這個(gè)錯(cuò)誤主要指的就是函數(shù)調(diào)用堆棧不平衡。在C/C++程序中,調(diào)用一個(gè)函數(shù)前會(huì)保存當(dāng)前堆棧信息,目標(biāo)函數(shù)返回后會(huì)把堆棧恢復(fù)到調(diào)用前的狀態(tài)。函數(shù)的參數(shù)、局部變量會(huì)影響堆棧。而函數(shù)堆棧不平衡,一般是因?yàn)楹瘮?shù)調(diào)用方式和目標(biāo)函數(shù)定義方式不一致導(dǎo)致,例如:

            void __stdcall func(int a) {
            }
            
            int main(int argc, char* argv[]) {
                typedef void (*funcptr)(int);
                funcptr ptr = (funcptr) func;
                ptr(1); // 返回后導(dǎo)致堆棧不平衡
                return 0;
            }
            

            __stdcall修飾的函數(shù),其函數(shù)參數(shù)的出棧由被調(diào)用者自己完成,而__cdecl,也就是C/C++函數(shù)的默認(rèn)調(diào)用約定,則是調(diào)用者完成參數(shù)出棧。

            Visual Studio在debug模式下會(huì)在我們的代碼中加入不少檢查代碼,例如以上代碼對(duì)應(yīng)的匯編中,就會(huì)增加一個(gè)檢查堆棧是否平衡的函數(shù)調(diào)用,當(dāng)出現(xiàn)問(wèn)題時(shí),就會(huì)出現(xiàn)提示Run-Time Check Failure...這樣的錯(cuò)誤對(duì)話框:

            call dword ptr [ptr]  ; ptr(1)
            add  esp,4  ; cdecl方式,調(diào)用者清除參數(shù)
            cmp  esi,esp  
            call @ILT+1345(__RTC_CheckEsp) (0B01546h) ; 檢查堆棧是否平衡
            

            但是我們的程序不是這種低級(jí)錯(cuò)誤。我們調(diào)用的函數(shù)是放在dll中的,調(diào)用約定顯示定義為__stdcall,函數(shù)聲明和實(shí)現(xiàn)一致。大致的結(jié)構(gòu)如下:

            IParser *parser = CreateParser();
            parser->Begin();
            ...
            ...
            parser->End();
            parser->Release(); // 返回后導(dǎo)致堆棧不平衡
            

            IParser的實(shí)現(xiàn)在一個(gè)dll里,這反而是一個(gè)誤導(dǎo)人的信息。parser->Release返回后,堆棧不平衡,并且僅僅少了一個(gè)字節(jié)。一個(gè)字節(jié)怎么來(lái)的?

            解決這個(gè)問(wèn)題主要的手段就是跟反匯編,在關(guān)鍵位置查看寄存器和堆棧的內(nèi)容。編譯器生成的代碼是正確的,而我們自己的代碼乍看上去也沒(méi)問(wèn)題。最后甚至使用最傻逼的調(diào)試手段–逐行語(yǔ)句注釋查錯(cuò)。

            具體查錯(cuò)過(guò)程就不細(xì)說(shuō)了。解決問(wèn)題往往需要更多的冷靜,和清晰的思路。最終我使用的方法是,在進(jìn)入Release之前記錄堆棧指針的值,堆棧指針的值會(huì)被壓入堆棧,以在函數(shù)返回后從堆棧彈出,恢復(fù)堆棧指針。Release的實(shí)現(xiàn)很簡(jiǎn)單,就是刪除一個(gè)Parser這個(gè)對(duì)象,但這個(gè)對(duì)象的析構(gòu)會(huì)導(dǎo)致很多其他對(duì)象被析構(gòu)。我就逐層地檢查,是在哪個(gè)函數(shù)里改變了堆棧里的內(nèi)容。

            理論上,函數(shù)本身是操作不到調(diào)用者的堆棧的。而現(xiàn)在看來(lái),確實(shí)是被調(diào)用函數(shù),也就是Release改寫(xiě)了調(diào)用者的堆棧內(nèi)容。要改變堆棧的內(nèi)容,只有通過(guò)局部變量的地址才能做到。

            最終,我發(fā)現(xiàn)在調(diào)用完以下函數(shù)后,我跟蹤的堆棧地址內(nèi)容發(fā)生了改變:

            call llvm::RefCountedBase<clang::TargetOptions>::Release (10331117h)
            

            因?yàn)樽⒁獾?code>TargetOptions這個(gè)字眼,想起了在parser->Begin里有涉及到這個(gè)類的使用,類似于:

            TargetOptions TO;
            ...
            TargetInfo *TI = TargetInfo::CreateTargetInfo(m_inst.getDiagnostics(), TO);
            

            這部分初始化代碼,是直接從網(wǎng)上復(fù)制的,因?yàn)椴⒉挥绊懼饕壿嫞詮膩?lái)沒(méi)對(duì)這塊代碼深究。查看CreateTargetInfo的源碼,發(fā)現(xiàn)這個(gè)函數(shù)將TO這個(gè)局部變量的地址保存了下來(lái)

            而在Release中,則會(huì)對(duì)這個(gè)保存的臨時(shí)變量進(jìn)行刪除操作,形如:

            void Delete() const {
              assert (ref_cnt > 0 && "Reference count is already zero.");
              if (--ref_cnt == 0) delete static_cast<const Derived*>(this);
            }
            

            但是,問(wèn)題并不在于對(duì)一個(gè)局部變量地址進(jìn)行deletedelete在調(diào)試模式下是做了內(nèi)存檢測(cè)的,那會(huì)導(dǎo)致一種斷言。

            TargetOptions包含了ref_cnt這個(gè)成員。當(dāng)出了Begin作用域后,parser保存的TargetOptions的地址,指向的內(nèi)容(堆棧)發(fā)生了改變,也就是ref_cnt這個(gè)成員變量的值不再正常。由于一些巧合,主要是代碼中各個(gè)局部變量、函數(shù)調(diào)用順序、函數(shù)參數(shù)個(gè)數(shù)(曾嘗試去除Begin的參數(shù),可以避免錯(cuò)誤提示),導(dǎo)致在調(diào)用Release前堆棧指針恰好等于之前保存的TargetOptions的地址。注意,之前保存的TargetOptions的地址,和調(diào)用Release前的堆棧指針值相同了。

            而在TargetOptionsDelete函數(shù)中,進(jìn)行了--ref_cnt,這個(gè)變量是TargetOptions的第一個(gè)成員,它的減1,也就導(dǎo)致了堆棧內(nèi)容的改變。

            至此,整個(gè)來(lái)龍去脈算是摸清。

            posted on 2013-08-15 23:01 Kevin Lynx 閱讀(5773) 評(píng)論(1)  編輯 收藏 引用 所屬分類: c/c++

            評(píng)論

            # re: 記一次堆棧平衡錯(cuò)誤 2013-08-16 12:05 小二郎健康燈

            終于找到一個(gè)專業(yè)的C++博客了  回復(fù)  更多評(píng)論   

            亚洲国产另类久久久精品黑人| 久久精品无码专区免费青青| 香蕉久久夜色精品国产尤物 | 亚洲狠狠婷婷综合久久蜜芽| 亚洲国产精品热久久| 久久久久亚洲精品中文字幕| 久久久久AV综合网成人| 久久久久九九精品影院| 一级a性色生活片久久无| 久久久久亚洲AV无码观看| 国内精品久久久久久99蜜桃| 久久综合九色综合欧美狠狠| 久久人妻少妇嫩草AV无码专区| 久久久久免费精品国产| 国产精品99久久不卡| 亚洲日本va午夜中文字幕久久| 久久狠狠高潮亚洲精品| 精品蜜臀久久久久99网站| 精品久久人人爽天天玩人人妻 | 久久久久亚洲av无码专区喷水 | 久久夜色精品国产亚洲| 一本久久a久久精品综合夜夜 | 国产69精品久久久久9999APGF | 久久免费看黄a级毛片| 亚洲国产精品婷婷久久| 久久精品国产99国产精品澳门| 理论片午午伦夜理片久久| 久久久久亚洲AV片无码下载蜜桃 | 亚洲AV乱码久久精品蜜桃| 久久免费大片| 久久精品国产亚洲av麻豆色欲 | 久久国产视频网| 亚洲女久久久噜噜噜熟女| 99久久人妻无码精品系列| 精品水蜜桃久久久久久久| 国产精品久久新婚兰兰| 久久99久久99小草精品免视看| 久久精品亚洲AV久久久无码| 少妇久久久久久被弄到高潮| 国产精品久久亚洲不卡动漫| 亚洲欧美日韩精品久久|