Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
前一段時(shí)間在折騰拆分 rc 的問(wèn)題,已經(jīng)把遇到的問(wèn)題整理成文了。感興趣的小伙伴兒可以參考這里,這里 和 這里。本以為不會(huì)有問(wèn)題了,后續(xù)流程就請(qǐng)其它同事幫忙處理了,沒(méi)想到在拆分實(shí)際項(xiàng)目時(shí)遇到了一個(gè)非常奇怪的鏈接問(wèn)題。
本文總結(jié)了使用 process monitor 監(jiān)聽進(jìn)程創(chuàng)建,查看進(jìn)程參數(shù)、使用 gflags 設(shè)置 Image File Excution Options、使用 IDA 靜態(tài)分析相關(guān)函數(shù)的業(yè)務(wù)邏輯以及使用 windbg 進(jìn)行動(dòng)態(tài)調(diào)試的整個(gè)過(guò)程。我認(rèn)為這是一個(gè)由不良的編程習(xí)慣與 crt 的限制共同導(dǎo)致的問(wèn)題。快來(lái)一起看看吧。
前些日子,在家隔離辦公的某日中午,收到同事發(fā)來(lái)的信息說(shuō) rc 拆分的編譯問(wèn)題已經(jīng)解決了,但是遇到了鏈接錯(cuò)誤,還發(fā)送了鏈接錯(cuò)誤的截圖,并且給出了一個(gè)解決方案。
嘗試把 .rc 文件排除幾十個(gè)就鏈接過(guò)去了。
聽到這個(gè)問(wèn)題的時(shí)候,我懷疑是不是哪里操作有問(wèn)題。從錯(cuò)誤提示看是 無(wú)法打開 xxx.res 進(jìn)行讀取,所以第一感覺(jué)是文件路徑不對(duì)。于是趕緊跟同事聊了一下,同事覺(jué)得是 vs 的限制,可能這個(gè)限制數(shù)量是 512 。
但是我從沒(méi)聽過(guò)同一個(gè)工程中的 .rc 文件有數(shù)量限制,不管怎樣,還是建個(gè)簡(jiǎn)單的工程驗(yàn)證下吧。
帶著懷疑 + 好奇的心態(tài),我快速新建了一個(gè) MFC 對(duì)話框工程。然后在 vs 中不斷復(fù)制默認(rèn)對(duì)話框(大概復(fù)制了600 個(gè),已經(jīng)比同事所說(shuō)的 512 上限要多了,如果有問(wèn)題應(yīng)該能重現(xiàn)了),然后使用工具把每個(gè)對(duì)話框拆分成獨(dú)立的 .rc 文件并添加到工程文件中。保存好工程后,開始編譯。等待一段時(shí)間后,果然報(bào)錯(cuò)了,錯(cuò)誤截圖如下:
從錯(cuò)誤提示看,處理 dialog_testmultiplerccompile_dialog507.rc 文件的時(shí)候報(bào)錯(cuò)了。按照同事說(shuō)的,刪除若干個(gè) .rc 文件,只保留 500 個(gè),再次編譯,沒(méi)有報(bào)錯(cuò)。
看來(lái),在同一個(gè)工程中包含太多 .rc 文件真可能有問(wèn)題。難道真有限制?為什么會(huì)做這種限制呢?不管為什么要做限制,我需要找到一個(gè)解決方案。
開始深入調(diào)查前,先看看報(bào)錯(cuò)信息。
之前遇到過(guò)錯(cuò)誤 LINK : fatal error LNK1123: 轉(zhuǎn)換到 COFF 期間失敗: 文件無(wú)效或損壞,是由于 link.exe 與 cvtres.exe 的版本不一樣導(dǎo)致的。這次報(bào)錯(cuò)不是這個(gè)原因。通過(guò) process monitor 看,這兩個(gè)程序的路徑是一樣的。
再看錯(cuò)誤 error CVT1101: 無(wú)法打開“dialog_testmultiplerccompile_dialog507.res”進(jìn)行讀取。猜測(cè)是在讀取這個(gè)文件的時(shí)候發(fā)生了錯(cuò)誤,可以在 process monitor 中查看相關(guān)事件。
在 process monitor 中根據(jù)路徑名進(jìn)行過(guò)濾。如果路徑以 dialog_testmultiplerccompile_dialog507.res 結(jié)尾則包含,如下圖:
沒(méi)想到一條記錄都沒(méi)有,一片空白。這是怎么回事?說(shuō)實(shí)話,我有點(diǎn)不知所措,看來(lái)只能硬著頭皮調(diào)試 + 用 IDA 逆向了。在調(diào)試之前,先用 IDA 看看有沒(méi)有什么發(fā)現(xiàn)。
使用 ida32 打開 cvtres.exe,IDA 會(huì)提示是否查找符號(hào)(真是一個(gè)好消息),當(dāng)然選擇是。等待 IDA 分析完成后,在左側(cè)的 Function window 中找到 _main,雙擊查看反匯編代碼,直接在反匯編窗口按 F5,查看偽代碼( IDA 的 F5 真香!)。
大概瀏覽后,基本明白了 main() 函數(shù)的整體流程。首先,解析傳入的參數(shù),確定第一個(gè)文件在參數(shù)列表中的索引位置。然后,從此索引開始循環(huán)調(diào)用 ReadResFile() 讀取每個(gè)文件,讀取完所有的文件后統(tǒng)一調(diào)用 CvtRes() 函數(shù)進(jìn)行轉(zhuǎn)換。
下圖是在 IDA 中對(duì) main() 函數(shù)使用 F5 獲得的偽代碼的后半部分。
其中的 CvtRes() 函數(shù)應(yīng)該是轉(zhuǎn)換的主要函數(shù),非常值得懷疑。迫不及待的啟動(dòng) windbg 準(zhǔn)備調(diào)試,但是 cvtres.exe 是被 link.exe 調(diào)用的,該如何調(diào)試呢?
如果 cvtres.exe 啟動(dòng)的時(shí)候,能夠自動(dòng)中斷到調(diào)試器中,就可以方便的調(diào)試了。之前在 全局變量初始化順序探究 中介紹過(guò)使用 gflags 進(jìn)行設(shè)置的方法。
根據(jù)之前調(diào)試 cl.exe 的經(jīng)驗(yàn),如果長(zhǎng)時(shí)間中斷到調(diào)試器中,調(diào)用者會(huì)重新啟動(dòng) cl.exe。猜想這里也會(huì)有類似的邏輯。為了避免這種問(wèn)題,需要根據(jù) link.exe 啟動(dòng) cvtres.exe 的參數(shù)手動(dòng)運(yùn)行 cvtres.exe。
可以通過(guò) process monitor 很快找出 cvtres.exe 需要的參數(shù)。經(jīng)過(guò)簡(jiǎn)單觀察,發(fā)現(xiàn)傳遞給 cvtres.exe 的參數(shù)比較簡(jiǎn)單直接,而且根據(jù) cvtres.exe /? 提供的幫助信息,可以很快確定各個(gè)參數(shù)的意義。
于是很快寫出了一個(gè)批處理腳本,如下圖:
沒(méi)想到,雙擊腳本運(yùn)行的時(shí)候,出現(xiàn)了如下錯(cuò)誤:
提示找不到 cvtres.exe。看來(lái)需要使用完整路徑。正確的腳本如下:
說(shuō)明: 為了避免命令行參數(shù)過(guò)長(zhǎng),我特意簡(jiǎn)化了 .res 文件名,之前的名字太長(zhǎng)了。而且經(jīng)過(guò)測(cè)試,打開 510.res 的時(shí)候就能重現(xiàn),沒(méi)必要準(zhǔn)備 600 多個(gè) .res 進(jìn)行測(cè)試,這里只準(zhǔn)備了 511 個(gè) .res 文件進(jìn)行測(cè)試。
雙擊腳本啟動(dòng) cvtres.exe,立刻就中斷到了 windbg 中。
在 windbg 中執(zhí)行 x cvtres!*main 即可找到入口函數(shù),輸入 bp cvtres!wmain 即可在 wmain() 函數(shù)入口處設(shè)置好斷點(diǎn)。
同理,執(zhí)行 x cvtres!*CvtRes 即可找到 cvtres!CvtRes() 函數(shù),輸入 bp cvtres!CvtRes 即可在 CvtRes() 函數(shù)入口處設(shè)置好斷點(diǎn)。
設(shè)置好斷點(diǎn)后,輸入 g 讓程序跑起來(lái),可以發(fā)現(xiàn) wmain() 函數(shù)內(nèi)的斷點(diǎn)命中了,但是 CvtRes() 函數(shù)內(nèi)的斷點(diǎn)并沒(méi)有命中,進(jìn)程直接退出了。
有些出乎意料,居然不是在 CvtRes() 函數(shù)里出的錯(cuò)。沒(méi)(有)關(guān)(點(diǎn))系(懵),繼續(xù)挖掘有效信息。
雖然進(jìn)程退出了,但是依然可以通過(guò) k 系列命令查看調(diào)用棧,在 windbg 中輸入 kp,如下圖:
上圖中紅色高亮部分就是關(guān)鍵調(diào)用棧。從上圖還可以得到一個(gè)非常有用的信息 —— exit code 的值是 1。可以猜測(cè),link.exe 就是根據(jù) cvtres.exe 的返回值來(lái)判斷其是否執(zhí)行成功的。
調(diào)用棧中的 OurFileOpen() 函數(shù),應(yīng)該是負(fù)責(zé)打開文件的函數(shù)。在繼續(xù)調(diào)試之前,先在 IDA 中看看 OurFileOpen() 函數(shù)的實(shí)現(xiàn)。
雙擊 OurFileOpen,當(dāng)然是直接查看 F5 的結(jié)果啦,有細(xì)節(jié)需要確認(rèn)再看反匯編代碼。
可以看到這個(gè)函數(shù)實(shí)現(xiàn)的非常簡(jiǎn)單,就是調(diào)用 _wfsopen(),如果失敗(result==0)那么調(diào)用 ErrorPrint() 打印錯(cuò)誤信息。如果 open_mode(第二個(gè)參數(shù))是 0,那么傳遞給 ErrorPrint() 的第一個(gè)參數(shù)是 1101,否則是 1108。
而調(diào)用 OurFileOpen 時(shí)傳遞的第二個(gè)參數(shù)是通過(guò) edx 傳遞的,對(duì)應(yīng)的值是 0,所以如果出錯(cuò),那么會(huì)傳遞 1101。
說(shuō)實(shí)話,看到 OurOpenFile() 函數(shù)中的 1101 ,我太激動(dòng)了,因?yàn)樵?/span>vs 中看到的錯(cuò)誤提示是 error CVT1101: 無(wú)法打開“xxx.res”進(jìn)行讀取。為了進(jìn)一步確認(rèn)猜想,在 IDA 中查看 ErrorPrint() 函數(shù)的反匯編代碼,如下圖:
從上方紅色高亮語(yǔ)句 CVTRES: fatal error CVT%04u: 基本可以確定猜測(cè)是正確的。從上圖底部的紅色高亮區(qū)域還可以知道該函數(shù)內(nèi)部確實(shí)會(huì)調(diào)用 exit(1) 來(lái)結(jié)束進(jìn)程。
接下來(lái)需要調(diào)查的問(wèn)題是 _wfsopen 為什么失敗了?
在 windbg 中輸入 .restart 重啟目標(biāo)程序,輸入 bp MSVCR120!_wfsopen,然后執(zhí)行 g 命令。因?yàn)橐呀?jīng)設(shè)置好了符號(hào)查找路徑,所以 windbg 自動(dòng)打開了對(duì)應(yīng)的源碼文件。
這個(gè)函數(shù)雖然很簡(jiǎn)單,加上注釋不到 50 行。但是會(huì)被調(diào)用很多次,根據(jù)經(jīng)驗(yàn),前面的 500 多次調(diào)用都沒(méi)有問(wèn)題,在嘗試打開 510.res 的時(shí)候會(huì)有問(wèn)題,所以設(shè)置一個(gè)條件斷點(diǎn)非常有必要。
簡(jiǎn)單查看反匯編代碼發(fā)現(xiàn),_wfsopen() 函數(shù)的第一個(gè)參數(shù)是通過(guò) ecx 傳遞的,可以設(shè)置如下的條件斷點(diǎn)(真是燒腦還不好理解,我不會(huì)告訴你,我嘗試了很久才寫出了下面這段蹩腳的腳本):
bp MSVCR120!_wfsopen "aS /mu $myFileName @ecx; .block {.echo $myFileName; r @$t0=$spat(@\"$myFileName\", @\"*510.res\"); .if(1==$t0){.echo **** bang ****} .else{ gc;} };"
耐心等待一會(huì)就中斷下來(lái)了,如下圖:
單步走兩步,發(fā)現(xiàn)是 _getstream() 出錯(cuò)了。
輸入 .restart 重啟目標(biāo)程序,并且設(shè)置好條件斷點(diǎn),重新運(yùn)行程序,當(dāng)中斷到 _wfsopen() 函數(shù)后,單步步入到 _getstream() 函數(shù)中。
可以看到 _getstream() 函數(shù)邏輯也不復(fù)雜,根據(jù)注釋可以很簡(jiǎn)單的理解此函數(shù)的邏輯 —— 從 __piob 中(大小是 _nstream,通過(guò) dt _nstream 可知其大小是 512)找到一條可用的記錄項(xiàng)。判斷一條記錄項(xiàng)是否可用的標(biāo)準(zhǔn)是 __piob[i]==NULL ,或者 !inuse( (FILE *)__piob[i] ) && !str_locked( (FILE *)__piob[i] )。
直接在函數(shù)末尾加好斷點(diǎn),g 起來(lái),發(fā)現(xiàn)確實(shí)沒(méi)有找到一條可用的記錄項(xiàng)。
至此,我大概明白了整個(gè)過(guò)程。cvtres.exe 在 main() 函數(shù)中會(huì)循環(huán)調(diào)用 ReadResFile() 函數(shù)(內(nèi)部會(huì)調(diào)用 _wfsopen())讀取所有的 .res 文件,但是讀取完一個(gè) .res 文件后,并沒(méi)有關(guān)閉,當(dāng)打開一定數(shù)量的文件后會(huì)導(dǎo)致 __piob 被占滿。再嘗試打開一個(gè)文件的時(shí)候就報(bào)錯(cuò)了。
看來(lái),crt 還有最大打開文件數(shù)的限制,趕緊 google 搜索是否有什么設(shè)置可以調(diào)整最大文件打開數(shù)量。
在 google 中輸入 crt max open file 找到了幾個(gè)相關(guān)的網(wǎng)址。
雖然可以通過(guò) _setmaxstdio() 調(diào)整 crt 的最大文件打開數(shù),但是好像不能通過(guò)修改配置文件或者修改注冊(cè)表的方式調(diào)整。
說(shuō)實(shí)話,第一次分析到這個(gè)結(jié)果的時(shí)候我是有些不信的。于是我再三確認(rèn)了 ReadResFile() 函數(shù)內(nèi)部確實(shí)沒(méi)有關(guān)閉文件的操作。難道有什么特殊的理由不關(guān)閉打開的文件?但是我實(shí)在想不出有什么理由。所以我覺(jué)得這是一個(gè) bug,于是我在微軟官方論壇上發(fā)了一個(gè)帖子,希望能得到一些回復(fù)。
帖子地址是 https://docs.microsoft.com/en-us/answers/questions/709392/cvt1101-can39t-open-xxxres-for-reading.html
目前只有一位網(wǎng)友回復(fù)(另外一個(gè)是我自己),為了方便大家閱讀,截圖如下:
雖然到現(xiàn)在還沒(méi)收到官方的確認(rèn)回復(fù),不過(guò)我依然認(rèn)為這是一個(gè) bug,而不是 feature。
既然沒(méi)有設(shè)置選項(xiàng)或者配置文件可以簡(jiǎn)單的調(diào)整最大文件打開數(shù)量,對(duì) cvtres.exe 打補(bǔ)丁又不太現(xiàn)實(shí)(每臺(tái)機(jī)器上都要做處理),等待微軟修復(fù)這個(gè)問(wèn)題也不現(xiàn)實(shí)(遠(yuǎn)水解不了近渴)。所以我們的解決方案是通過(guò)合并一些 .rc 以減少工程中的 .rc 文件數(shù)量來(lái)規(guī)避這個(gè)問(wèn)題。
雖然問(wèn)題已經(jīng)調(diào)查清楚了,但是還有幾個(gè)問(wèn)題值得探究。
由于本篇已經(jīng)太長(zhǎng)了,下一篇文章中繼續(xù)把殘留的這幾個(gè)問(wèn)題解答。
https://stackoverflow.com/questions/61581826/visual-studio-2019-cvt1101-lnk1123-fatal-error
https://docs.microsoft.com/en-us/cpp/build/reference/dot-res-files-as-linker-input?view=msvc-170
https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio?view=msvc-170
vs2013 自帶的 crt 源碼
多人都說(shuō)HTML是一門很簡(jiǎn)單的語(yǔ)言,看看書,看看視頻就能讀懂。但是,如果你完全沒(méi)有接觸過(guò),就想通過(guò)看一遍教程,背背標(biāo)簽,想要完全了解HTML,真的有點(diǎn)太天真了。
HTML中文“超文本標(biāo)記語(yǔ)言”,英文名叫HTML沒(méi)有變量,沒(méi)有循環(huán),沒(méi)有函數(shù),只是單純的一門靜態(tài)語(yǔ)言而已。你可以用來(lái)描述靜態(tài)的東西,比如標(biāo)題、段落、圖片。
1)HTML通常被稱為靜態(tài)網(wǎng)頁(yè)。
2)HTML的一些標(biāo)簽代碼規(guī)則將內(nèi)容呈現(xiàn)在瀏覽器中所需的風(fēng)格。
3)HTML可以使用記事本創(chuàng)建,并以.html為擴(kuò)展名保存。
打開瀏覽器,例如打開百度的首頁(yè)
這個(gè)頁(yè)面非常的簡(jiǎn)潔,但是包含了很多內(nèi)容,有文字、圖片、動(dòng)畫、超鏈接等一系列HTML頁(yè)面所能夠包含的元素。什么意思,也就是說(shuō),HTML頁(yè)面就是能夠包含文本、圖像、聲音、超鏈接等內(nèi)容的集合,然后通過(guò)瀏覽器對(duì)這些元素進(jìn)行渲染,就呈現(xiàn)出多彩的頁(yè)面。
打開頁(yè)面的審查元素(快捷鍵是【F12】),就能夠看到構(gòu)成HTML頁(yè)面的所有元素,當(dāng)我們?cè)趯戫?yè)面,對(duì)頁(yè)面進(jìn)行調(diào)試的時(shí)候,也是通過(guò)審查元素,在這個(gè)窗口里面檢測(cè)問(wèn)題,所以審查的方法一定要掌握。
一個(gè)HTML頁(yè)面最基本框架。
優(yōu)點(diǎn)
易于使用,松散的語(yǔ)法(雖然,過(guò)于靈活的將不符合標(biāo)準(zhǔn)),HTML還允許使用模板,這使設(shè)計(jì)網(wǎng)頁(yè)變得容易、對(duì)Web設(shè)計(jì)領(lǐng)域的初學(xué)者來(lái)說(shuō)非常有用。幾乎所有瀏覽器都支持該功能。被廣泛使用的; 建立在幾乎所有網(wǎng)站上。與XML語(yǔ)法非常相似,后者已越來(lái)越多地用于數(shù)據(jù)存儲(chǔ)。免費(fèi)-無(wú)需購(gòu)買任何軟件,即使對(duì)于新手程序員而言,都易于學(xué)習(xí)和編碼。
缺點(diǎn)
由于它是一種靜態(tài)語(yǔ)言,它不能單獨(dú)產(chǎn)生動(dòng)態(tài)輸出。有時(shí),HTML文檔的結(jié)構(gòu)難以掌握。程序錯(cuò)誤可能會(huì)導(dǎo)致高昂的代價(jià)。它只能創(chuàng)建靜態(tài)頁(yè)面和普通頁(yè)面,因此如果我們需要?jiǎng)討B(tài)頁(yè)面,則HTML無(wú)效。需要編寫大量代碼來(lái)制作簡(jiǎn)單的網(wǎng)頁(yè)。您必須跟上已棄用的標(biāo)記,并確保不要使用它們,因?yàn)槌霈F(xiàn)了另一種與HTML兼容的語(yǔ)言代替了標(biāo)記的原始工作。因此需要學(xué)習(xí)其他語(yǔ)言(大多數(shù)情況下是CSS)HTML提供的安全功能受到限制。
在了解這么多之后,一定想要自己寫個(gè)HTML頁(yè)面試試手,那么HTML頁(yè)面怎么寫呢,用什么工具來(lái)寫呢?在電腦上建立一個(gè)hello.txt的文件,將下面的代碼粘貼復(fù)制進(jìn)去保存 。
然后將后綴名修改為.html,用瀏覽器在頁(yè)面上看到hello world字樣的輸出,這就是第一個(gè)HTML頁(yè)面。
今天我們就先分享到這里啦,趕快去練練手吧~(私信我有免費(fèi)IT課程可以領(lǐng)取喲)
tmlAgilityPack 是一個(gè) HTML 解析庫(kù),用于 .NET 平臺(tái)。它允許開發(fā)者以類似于解析 XML 的方式,輕松地解析和操作 HTML 文檔。這個(gè)庫(kù)特別適合處理非標(biāo)準(zhǔn)的 HTML,例如那些格式不正確或包含錯(cuò)誤的 HTML 文檔。
從原理上說(shuō),解析是一個(gè) CPU 密集型操作。在計(jì)算資源充裕的情況下,使用多線程并行可以加快處理速度。
以下代碼展示了兩個(gè)場(chǎng)景:
使用一個(gè)線程解析 1000 個(gè)頁(yè)面
使用 8 個(gè)線程解析 1000 個(gè)頁(yè)面(總量 1000 個(gè),測(cè)試機(jī)器上的 CPU 有 8 個(gè)內(nèi)核)。
string html=File.ReadAllText("PATH");
//One thread
for (int i=0; i < 1000; i++)
new HtmlDocument().LoadHtml(html);
//Several threads
Parallel.For(0, 1000, (int i)=> new HtmlDocument().LoadHtml(html));
然而實(shí)際的情況是:盡管多線程版本消耗了 2 ~ 3 倍的 CPU,但所花費(fèi)的時(shí)間大致相同。而且 CPU 占用率一直維持在 30% 以下。即便更換了要處理的頁(yè)面,或者內(nèi)核數(shù)量更多的電腦,情況都差不多。
開啟更多的線程并不會(huì)提升處理的速度,這讓我開始懷疑是不是存在鎖的問(wèn)題。遺憾的是沒(méi)有在源代碼中找到 lock ,但是發(fā)現(xiàn)了一個(gè) Issues:
https://github.com/zzzprojects/html-agility-pack/issues/191
在使用 Profiler 工具對(duì)多線程程序進(jìn)行分析之后,發(fā)現(xiàn)程序可能存在內(nèi)存瓶頸。根據(jù)他的觀察,有大約 50% 的 CPU 時(shí)間耗費(fèi)在了內(nèi)存分配上。
這和使用的 GC 類型有關(guān),向 App.config 增加以下代碼可以解決該問(wèn)題:
<runtime>
<gcServer enabled="true"/>
<gcConcurrent enabled="false" />
</runtime>
我的程序是一個(gè)使用 .NET 8.0 框架的控制臺(tái),增加 App.config 文件之后并沒(méi)有效果。于是,我找到了微軟的官方文檔:
https://learn.microsoft.com/zh-cn/dotnet/core/runtime-config/garbage-collector
根據(jù)文檔所述,可以通過(guò)環(huán)境變量、runtimeconfig.json 文件或項(xiàng)目文件來(lái)指定程序使用 Server 版本。我選擇修改項(xiàng)目文件:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
</Project>
問(wèn)題得以解決:處理速度快了不少,CPU 占用維持在了 70% 左右。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。