一:背景
1. 講故事
前些天有位朋友找到我,說他的程序跑著跑著就崩潰了,讓我看下怎么回事,其實沒怎么回事,抓它的 crash dump 就好,具體怎么抓也是被問到的一個高頻問題,這里再補一下鏈接: [.NET程序崩潰了怎么抓 Dump ? 我總結(jié)了三種方案] https://www.cnblogs.com/huangxincheng/p/14811953.html ,采用第二種 AEDebug 的形式抓取即可。
二:Windbg 分析
1. 崩潰原因是什么
如果dump中塞了異常,用 windbg 打開的時候會有一個提示 This dump file has an exception of interest stored in it
,輸出如下:
************* Path validation summary **************
Response Time (ms) Location
Deferred SRV*C:\mysymbols*http://msdl.microsoft.com/download/symbols
Symbol search path is: SRV*C:\mysymbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
Windows 7 Version 7601 (Service Pack 1) MP (4 procs) Free x64
Product: Server, suite: Enterprise TerminalServer SingleUserTS
Debug session time: Wed Jun 14 13:34:49.000 2023 (UTC + 8:00)
System Uptime: 0 days 3:28:04.223
Process Uptime: 0 days 0:00:14.000
................................................................
................................................................
......................................................
This dump file has an exception of interest stored in it.
The stored exception information can be accessed via .ecxr.
(9e4.bc4): Stack overflow - code c00000fd (first/second chance not available)
For analysis of this file, run !analyze -v
clr!SlowAllocateString+0x11:
000007fe`f9236451 48c785b0fffffffeffffff mov qword ptr [rbp-50h],0FFFFFFFFFFFFFFFEh ss:00000000`123d5fd0=0000000000000000
從卦中看當(dāng)前有一個 Stack overflow - code c00000fd
異常,說實話好久都沒看到 棧溢出
了,甚是想念,既然說棧溢出了,那就看下異常前是個啥情況,使用 .excr
即可。
0:028> .excr;k
rax=00000000123d6048 rbx=00000000123d5d70 rcx=0000000000000001
rdx=0000000000000001 rsi=0000000000000000 rdi=00000000123d5880
rip=000007fef9236451 rsp=00000000123d5fb0 rbp=00000000123d6020
r8=00000000ffffffff r9=0000000000000000 r10=00000000123d618e
r11=0000000000000000 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000001
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010200
clr!SlowAllocateString+0x11:
000007fe`f9236451 48c785b0fffffffeffffff mov qword ptr [rbp-50h],0FFFFFFFFFFFFFFFEh ss:00000000`123d5fd0=0000000000000000
*** Stack trace for last set context - .thread/.cxr resets it
# Child-SP RetAddr Call Site
00 00000000`123d5fb0 000007fe`f920a5bd clr!SlowAllocateString+0x11
01 00000000`123d6050 000007fe`f920a9c7 clr!StringObject::NewString+0x25
02 00000000`123d6080 000007fe`f920a80d clr!Int32ToDecStr+0xdf
03 00000000`123d6320 000007fe`9ab3bb72 clr!COMNumber::FormatInt32+0x10d
04 00000000`123d65f0 000007fe`9ab33e04 0x000007fe`9ab3bb72
05 00000000`123d6630 000007fe`9ab3be52 0x000007fe`9ab33e04
06 00000000`123d6720 000007fe`9ab3bd2a 0x000007fe`9ab3be52
07 00000000`123d6790 000007fe`9ab33e35 0x000007fe`9ab3bd2a
08 00000000`123d67f0 000007fe`9ab3be52 0x000007fe`9ab33e35
09 00000000`123d68e0 000007fe`9ab3bd2a 0x000007fe`9ab3be52
...
ff 00000000`123df860 000007fe`9ab3bd2a 0x000007fe`9ab3be52
從卦中看,當(dāng)前默認的 255 個棧幀全部被打滿,看樣子是無限死循環(huán)了,為了能看到托管部分我們改用 !clrstack
命令。
0:028> !clrstack
OS Thread Id: 0xbc4 (28)
Child SP IP Call Site
00000000123d63b8 000007fef9236451 [HelperMethodFrame_PROTECTOBJ: 00000000123d63b8] System.Number.FormatInt32(Int32, System.String, System.Globalization.NumberFormatInfo)
00000000123d65f0 000007fe9ab3bb72 xxx_symbol01.xxx_symbol09.xxx_symbol00(Byte[])
00000000123d6630 000007fe9ab33e04 xxx_symbol01.xxx_symbol09.xxx_symbol00(Byte[], Int64, Int64, Boolean)
00000000123d6720 000007fe9ab3be52 xxx_symbol01.xxx_symbol09.xxx_symbol00(Int32, Int32)
00000000123d6790 000007fe9ab3bd2a xxx_symbol01.xxx_symbol09.xxx_symbol00(Byte[], Boolean)
00000000123d67f0 000007fe9ab33e35 xxx_symbol01.xxx_symbol09.xxx_symbol00(Byte[], Int64, Int64, Boolean)
00000000123d68e0 000007fe9ab3be52 xxx_symbol01.xxx_symbol09.xxx_symbol00(Int32, Int32)
00000000123d6950 000007fe9ab3bd2a xxx_symbol01.xxx_symbol09.xxx_symbol00(Byte[], Boolean)
00000000123d69b0 000007fe9ab33e35 xxx_symbol01.xxx_symbol09.xxx_symbol00(Byte[], Int64, Int64, Boolean)
00000000123d6aa0 000007fe9ab3be52 xxx_symbol01.xxx_symbol09.xxx_symbol00(Int32, Int32)
00000000123d6b10 000007fe9ab3bd2a xxx_symbol01.xxx_symbol09.xxx_symbol00(Byte[], Boolean)
00000000123d6b70 000007fe9ab33e35 xxx_symbol01.xxx_symbol09.xxx_symbol00(Byte[], Int64, Int64, Boolean)
00000000123d6c60 000007fe9ab3be52 xxx_symbol01.xxx_symbol09.xxx_symbol00(Int32, Int32)
00000000123d6cd0 000007fe9ab3bd2a xxx_symbol01.xxx_symbol09.xxx_symbol00(Byte[], Boolean)
00000000123d6d30 000007fe9ab33e35 xxx_symbol01.xxx_symbol09.xxx_symbol00(Byte[], Int64, Int64, Boolean)
00000000123d6e20 000007fe9ab3be52 xxx_symbol01.xxx_symbol09.xxx_symbol00(Int32, Int32)
00000000123d6e90 000007fe9ab3bd2a xxx_symbol01.xxx_symbol09.xxx_symbol00(Byte[], Boolean)
....
000000001244db60 000007fe9ab31f0e xxx.PDFFile.xxx_symbol00(System.String, System.IO.Stream, Byte[])
000000001244dbc0 000007fe9ab318e5 xxx.xxx.Convertxxxx(System.IO.Stream, Int32, Int32, System.Drawing.Imaging.ImageFormat, Int32)
從卦中信息看,是代碼用 Convertxxxx
調(diào)用了一個第三方庫,在這個庫中出現(xiàn)了死遞歸。
按理說不管外界給了什么參數(shù)下去,都不應(yīng)該用死遞歸的方式來呈現(xiàn),所以這類問題可以歸于 SDK 的bug,接下來我們的研究方向就是看下這個 SDK 是何方神圣?
[assembly: AssemblyCopyright("? 2008 O2 Solutions")]
[assembly: AssemblyProduct("PDFxxx4NET")]
[assembly: AssemblyCompany("O2 Solutions (http://www.xxx.com/)")]
[assembly: AssemblyTrademark("PDFxxx4NET is a trademark of O2 Solutions")]
[assembly: AllowPartiallyTrustedCallers]
[assembly: AssemblyTitle("Print and convert PDF files to images.")]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyDescription("Component for rendering pdf files on .NET platform")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyInformationalVersion("2.0.1")]
[assembly: AssemblyKeyName("")]
[assembly: AssemblyDelaySign(false)]
[assembly: CompilationRelaxations(8)]
[assembly: AssemblyVersion("2.0.1.0")]
從卦中看還是 2008 年寫的 2.0.1
版本,而官網(wǎng)早已出了 2023 年版本,也就是說 15年都沒有更新,也是厲害,截圖如下:
到這里就可以給到朋友答案了,讓他看下能否把 PDFRender4NET
升級到最新版本,按理說應(yīng)該就沒有問題了。
2. 為什么會棧溢出
心細的朋友可能會有一個疑問,既然都棧溢出了,按理說異常碼應(yīng)該是 c0000005
(訪問違例),怎么會是 c00000fd
呢?
這是一個非常好的問題,要理解為什么是 c00000fd
而不是 c0000005
,需要你對棧的布局有一個比較清晰的理解,為了方便講述,以當(dāng)前的 w3wp 來繪制一張圖。
畫完這張圖肯定有朋友會提幾個反對意見:
1) 線程棧不是 1M 嗎? 怎么會是 512k 呢?
這里要說的是 1M 并不是什么公理,可以在 PE 頭上隨便設(shè)定的,截圖如下:
2)PAGE_GUARD 不是 1個內(nèi)存頁嗎?
很多教科書都是按 1個內(nèi)存頁 講述的,但這也不是定死的,也可能是多個內(nèi)存頁,比如 2個,5個,要想驗證很簡單,用 !address -f:Stack
觀察下便知。
0:121> !address -f:Stack
BaseAddress EndAddress+1 RegionSize Type State Protect Usage
--------------------------------------------------------------------------------------------------------------------------
0`001f0000 0`00266000 0`00076000 MEM_PRIVATE MEM_RESERVE Stack [~0; 9e4.e30]
0`00266000 0`00268000 0`00002000 MEM_PRIVATE MEM_COMMIT PAGE_READWRITE | PAGE_GUARD Stack [~0; 9e4.e30]
0`00268000 0`00270000 0`00008000 MEM_PRIVATE MEM_COMMIT PAGE_READWRITE Stack [~0; 9e4.e30]
...
0`15710000 0`15788000 0`00078000 MEM_PRIVATE MEM_RESERVE Stack [~139; 9e4.14ac]
0`15788000 0`1578d000 0`00005000 MEM_PRIVATE MEM_COMMIT PAGE_READWRITE | PAGE_GUARD Stack [~139; 9e4.14ac]
0`1578d000 0`15790000 0`00003000 MEM_PRIVATE MEM_COMMIT PAGE_READWRITE Stack [~139; 9e4.14ac]
接下來我們聊一下什么是 PAGE_GUARD
,從名字上看就是 哨兵頁
,說白一點就是 Windows 做 棧伸展
的一種系統(tǒng)機制,當(dāng) rsp 訪問到這個區(qū)域時會引發(fā)系統(tǒng)的 頁中斷
進而 COMMIT 更多內(nèi)存頁,新的 Commit 頁會被 哨兵
侵占,同時也會讓渡 RSP 所占的內(nèi)存頁給程序使用,這是一種良性機制,一旦 哨兵
無法侵占更多新的 COMMIT 頁時,也就表示??臻g已經(jīng)到位了,這時候會將自身的 PAGE_GUARD
標簽去掉,表示它的使命已完成,如果此時 RSP 訪問到了這個彌留的 哨兵區(qū)
,就會拋出 c00000fd
異常,這種異常只是表示 RSP 進入了 哨兵區(qū)
,不代表??臻g
真的用完了,所以這就是不拋 c0000005
的真正原因,畫個簡圖如下:
說了這么說,如何去驗證呢?非常簡單,我們提取出 StackLimit, StackBase, RSP
即可。
0:028> r rsp
rsp=00000000123d5fb0
0:028> !teb
TEB at 000007fffff70000
ExceptionList: 0000000000000000
StackBase: 0000000012450000
StackLimit: 00000000123d1000
0:028> !address -f:Stack
BaseAddress EndAddress+1 RegionSize Type State Protect Usage
--------------------------------------------------------------------------------------------------------------------------
0`123d0000 0`123d1000 0`00001000 MEM_PRIVATE MEM_RESERVE Stack [~28; 9e4.bc4]
0`123d1000 0`12450000 0`0007f000 MEM_PRIVATE MEM_COMMIT PAGE_READWRITE Stack [~28; 9e4.bc4]
從卦中看,當(dāng)前 哨兵區(qū) = StackLimit ~ StackLimit+0x5000 = 00000000123d1000 ~ 00000000123d6000
,然后看下 rsp=00000000123d5fb0
果然是在這個范圍內(nèi),在一些低級語言中還可以繼續(xù)放任 棧溢出
異常,繼續(xù)讓程序跑,當(dāng)代碼跑到圖中的 MEM_RESERVE
區(qū)時這就是貨真價實的 c0000005
訪問違例。文章來源:http://www.zghlxwxcb.cn/news/detail-499333.html
三:總結(jié)
這次崩潰事故主要還是第三方的SDK代碼不健壯導(dǎo)致的 死遞歸
拖累程序崩潰,解決辦法很簡單,升級升級再升級,如果還有問題建議提交官方或者使用其他替代品,如果官方解決問題不活躍,你還敢用嗎?文章來源地址http://www.zghlxwxcb.cn/news/detail-499333.html

到了這里,關(guān)于記一次 .NET 某企業(yè)內(nèi)部系統(tǒng) 崩潰分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!