最近在項目中,發現在使用Qt4.8.5 提供的QWebView與網頁交互的時候,
m_pWebView->page()->mainFrame()->evaluateJavaScript(tmp);
QtWebKitd4.dll模塊偶爾會出現崩潰,如圖
中斷查看調用堆棧(加載QtWebkitd4.pdb 才可看到正確的堆棧信息)
最后停止在 QT StackBounds::checkConsistency。從堆棧類名跟函數名看出,可能是跟堆棧相關,嘗試看看源文件,找到函數定義
函數很短,跟具體業務邏輯沒什么關系,可以得出release模式函數直接返回,debug模式下,在棧上創建一個對象來獲取此時棧指針大小,assert根據堆棧增長方向,
檢查此時棧指針跟 m_origin 和m_bound的關系。 從名字推測是與棧基址跟棧邊界比較。繼續看代碼,怎么給這兩個賦值的。
在X86CPU配合MSVC編譯器的平台下,棧基址 m_origin 通過FS寄存器中保存的 NT_TIB 線程信息塊中得到當前線程的棧的基址,沒有問題
那么棧邊界呢m_bound ?
通過源碼的注釋,似乎想通過NT_TIB獲得,但沒這樣做(后面驗證,此方法得到的棧邊界不可靠,只能獲得已提交棧大小。qt5.4中提供新的方式獲取,后面修改也是基於此)。
繼續看看 QT是怎么做的。
轉到函數定義:
QT把棧邊界的大小固定為512kB,顯然不適用所有平台,源碼的注釋也給出了說明 this code unsafely guesses stack sizes!,WINDOWS、WINCE等平台下,may be work wrong。
明知不可行但還是設置了固定值,而且是全平台通用的一個值,想想我們在以往的項目中是不是也做過類似的妥協呢?
可見實現Qt4.8.5時還是比較匆忙,並不是一個穩定的版本。
后面再看qt5.4時(中間哪個版本開始修改的,這個沒有關注。。),全平台都是代碼獲取,感覺很可靠,至少是在window平台。(Qt團隊效率還是可以的!)
問題原因大致定位了,但是是哪里導致棧空間被大量使用了呢?
猜測1:QtWebKit內部調用,消耗了大量棧空間。
驗證: 1.1 新建一個工程,用QWebView加載網頁 m_pWebView->load(QUrl("xxx"));
1.2 注冊js調用對象 m_pWebView->page()->mainFrame()->addToJavaScriptWindowObject("servers", this);
1.3 聲明接口供js調用
int JS2QT::MainCall(QString szCallOperate,QString szExternData)
{
….
m_pWebView->page()->mainFrame()->evaluateJavaScript(tmp);
}
1.4 在執行js之前查看堆棧ebp(棧基址) 與esp(棧頂指針)。 當前消耗 ebp-esp 才幾k,屬於正常現象。
1.5 執行js,正常。
猜測2:項目工程在進入QWebView調用之前=的調用鏈就已經消耗了大量棧空間。
驗證: 在進入MainCall之前下斷點,觀察到進入MainCall之后, ebp-esp 瞬間消耗了 850KB以上的棧空間(默認1MB),
執行js,出現中斷。850KB 早已超過512KB, debug模式下QtWebKit只要執行stackCheck,必然assert。
相同情況relese不會崩潰,正好驗證了之前的源碼在relese模式下不檢查stack。
結果: 查看實際項目中MainCall的實現,該函數內部確實聲明了大量的臨時對象,消耗了大量的棧空間。
修改意見:
|
主工程(生成exe的工程)屬性 |
QtWebKit |
Debug |
項目屬性-》鏈接器-》系統 修改堆棧保留大小(推薦2097152) 其他默認0 |
|
Release |
項目屬性-》鏈接器-》系統 修改堆棧保留大小(推薦2097152) 其他默認0 |
Release模式下QtWebKit不對堆棧使用做檢查。一旦發生棧空間不夠,直接崩潰。 |
Q&A
- 同樣操作為什么debug模式必崩潰,但是release模式不會崩潰?
答:因為QtWebkit StackBounds類負責做棧邊界檢查的時候,認為棧的大小固定在512kB,而主線程的默認棧 1MB,當主線程使用超過512kB的棧空間時,QtWebkit必崩,
但在release模式下,QtWebkit不做棧檢查,只要主線程使用棧不超過1MB,程序就不會崩潰。
- 為什么跟js做一些交互的時候,程序會崩潰?
答:使用QWebView內核,與js交互都是通過我們項目中的 xxx::MainCall 完成分發的,MainCall中聲明的各種數組消耗了大量的棧空間,
目前來看已使用850kB左右,此時函數調用繼續發生,堆棧進一步被消耗,當某些操作需要消耗大一點棧空間的時候,此時就會發生崩潰,而如果崩潰在
Vs編譯的庫(不主動做棧檢查,不主動產生中斷),會友好提示 stackOverflow,崩潰在其他庫(不主動做棧檢查,不主動產生中斷),就會顯得莫名其妙了吧。
!!隱藏的問題,雖然擴大默認棧大小,可以解決問題,但是,改變默認棧大小帶來的問題?
- 如果最后我們的工程生成的是 xxx.exe 以進程提供服務,那么我們設置的默認堆棧大小會起到作用。
- 如果我們工程生成的是 xxx.dll 或 xxx.ocx。我們的服務是被IE(其他進程)加載,主線程的堆棧是由加載進程決定的,我們工程設置的大堆棧將不起作用。(解決方法:修改IE默認堆棧大小字段,利用PE工具很方便)
總之,問題的根源在於,一個函數中大量使用堆棧資源,勢必不是良好的程序設計風格,就目前及以后會出現的問題,提兩點自己的建議
- 一個函數不要太長,應按照實際業務分發處理,多加些函數負責不同的操作;同時一個函數內部不要消耗太多的棧空間,這樣有可能導致后的函數調用時,stackOverflow。
- 使用標准庫容器來管理大量臨時對象(容器對象在棧上分配空間,容器中的內容在堆上分配,堆的釋放由標准庫負責,有一定的可靠性).
附:手動修改QtWebKitd4.dll文件,改變QtWebkit 設置的固定棧大小。
下斷點觀察 m_bound的指令地址
指令地址 0x10EC3636 查看模塊加載地址:0x10000000 則文件偏移地址 0x00EC3626
用二進制編輯器打開QtWebKit4d.dll (debug才需修改) 找到0x00EC3626 或直接搜內容 2D00000800
2D 00 00 08 00 對應匯編指令 sub eax 80000h 注意為小端字節序
保存即可。
替換QtWebkitd4.dll 斷點查看
修改成功