<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>

          記一次有教益的內(nèi)存碎片轉(zhuǎn)儲(chǔ)文件分析經(jīng)歷

          共 9120字,需瀏覽 19分鐘

           ·

          2022-07-26 09:50

          前言

          其實(shí),這篇文章早在 2021 年就完成了初稿,后面一直沒(méi)來(lái)得及完善(各種加班各種忙),所以一直沒(méi)來(lái)得及整理發(fā)布。而且,我從這個(gè)案例里學(xué)到的東西太多了,很多內(nèi)容并沒(méi)有體現(xiàn)在本篇文章中,后續(xù)有機(jī)會(huì)一定會(huì)再寫(xiě)文章分享。話不多說(shuō),一起來(lái)看正文吧。

          緣起

          前一陣子,有朋友在微信上發(fā)了一段 windbg 的輸出信息,大概內(nèi)容如下:

          This dump file has an exception of interest stored in it. The stored exception information can be accessed via .ecxr. (fd0.9bc): Security check failure or stack buffer overrun - code c0000409 (first/second chance not available) For analysis of this file, run !analyze -v

          ... 省略 N 行

          看樣子像棧相關(guān)的問(wèn)題。于是簡(jiǎn)單跟朋友說(shuō)了下我的猜想,有可能是棧破壞了。沒(méi)想到,遠(yuǎn)不是這么簡(jiǎn)單。

          說(shuō)明: 之所以一定要寫(xiě)這篇總結(jié),是因?yàn)槲以诜治鲞^(guò)程中犯了很多想當(dāng)然的錯(cuò)誤,記錄下來(lái),以后不要再犯。

          另有隱情

          過(guò)了一會(huì),朋友發(fā)了一段更貼近真相的錯(cuò)誤提示,并且發(fā)送了更詳細(xì)的語(yǔ)音描述。

          FAILURE_ID_HASH_STRING: um:fail_fast_fatal_app_exit_c0000409_server.exe!std::_xbad_alloc

          這個(gè)提示跟最開(kāi)始的提示風(fēng)馬牛不相及。一個(gè)是棧相關(guān)的問(wèn)題,一個(gè)是內(nèi)存分配的問(wèn)題。

          說(shuō)明: 經(jīng)常分析轉(zhuǎn)儲(chǔ)文件的小伙伴兒應(yīng)該都知道,直接打開(kāi)轉(zhuǎn)儲(chǔ)文件時(shí),windbg 給出的提示有可能是不準(zhǔn)確的,如果想獲取最準(zhǔn)確的信息,最好通過(guò) .cxr 切換上下文,然后再執(zhí)行 k 系列命令查看。但是,很多時(shí)候直接執(zhí)行 !analayze -v 就能拿到正確信息。正式分析之前,不妨先試試 !analyze -v

          又跟朋友又聊了幾個(gè)相關(guān)問(wèn)題,得到了更多的關(guān)鍵信息。比如,

          • 之前解決過(guò)類(lèi)似的內(nèi)存泄漏問(wèn)題,當(dāng)時(shí)抓的 dump3GB 多。

          • 這次抓取的 dump 只有 905MB,但是確實(shí)是 full dump

            說(shuō)明: 內(nèi)存相關(guān)問(wèn)題,最好抓一個(gè) full dump,否則分析到一半,由于轉(zhuǎn)儲(chǔ)文件缺少關(guān)鍵信息,沒(méi)法繼續(xù)確認(rèn),就太尷尬了。

          • 程序是 32 位的,并且開(kāi)啟了大地址。

            說(shuō)明: 64 位程序的虛擬地址空間比 32 位程序大多了,不太容易在短時(shí)間內(nèi)看出問(wèn)題。

          • 前幾次出問(wèn)題時(shí)都是運(yùn)行了很久才出問(wèn)題,這次只運(yùn)行了 7 個(gè)小時(shí)左右就出問(wèn)題了。

          • 幾次出問(wèn)題時(shí),都是在內(nèi)存比較緊張的時(shí)候。

          聊了一會(huì),問(wèn)朋友是否方便發(fā)送完整的 !analyze -v 結(jié)果。沒(méi)想到,朋友除了發(fā)送 !analyze -v 的分析結(jié)果外,還特別貼心的發(fā)送了 轉(zhuǎn)儲(chǔ)文件和對(duì)應(yīng)的符號(hào)文件。必須為朋友點(diǎn)贊,看來(lái)沒(méi)少分析轉(zhuǎn)儲(chǔ)文件。

          查看分析結(jié)果

          由于當(dāng)時(shí)在北京出差的路上,于是在地鐵里用手機(jī)查看了一下 !analyze -v 的分析結(jié)果(旁邊的小哥哥小姐姐會(huì)不會(huì)以為我在看小說(shuō)?),跟朋友說(shuō)的一樣,打開(kāi)轉(zhuǎn)儲(chǔ)文件后,windbg 給出的錯(cuò)誤提示確實(shí)是棧相關(guān)的,但是 !analyze -v 卻指向了內(nèi)存分配相關(guān)的問(wèn)題。

          這里簡(jiǎn)單摘錄幾個(gè)關(guān)鍵的信息:

          1. 轉(zhuǎn)儲(chǔ)文件中的 comment
          Comment: '
          *** "D:\software\Procdump_installer\procdump.exe" -accepteula -ma -j "D:\software\Procdump_installer\dumps" 4048 384 00990000
          *** Just-In-Time debugger. PID: 4048 Event Handle: 384 JIT Context: .jdinfo 0x990000'

          說(shuō)明此 dump 是注冊(cè)為 JIT debuggerprocdump 在程序異常的時(shí)候抓取的。

          1. 運(yùn)行環(huán)境
          Windows 10 Version 14393 MP (2 procs) Free x86 compatible
          Product: Server, suite: TerminalServer DataCenter SingleUserTS
          10.0.14393.2430 (rs1_release_inmarket_aim.180806-1810)
          Debug session time: Wed Aug 18 20:32:43.000 2021 (UTC + 8:00)
          System Uptime: 40 days 1:00:55.704
          Process Uptime: 0 days 7:00:35.000

          win10 服務(wù)器系統(tǒng),系統(tǒng)運(yùn)行時(shí)間是 40 days 1:00:55.704 ,程序運(yùn)行時(shí)間是 7:00:35.000。跟朋友的描述是一致的。

          1. 上下文相關(guān)信息
          CONTEXT:  0f77ef58 -- (.cxr 0xf77ef58)
          eax=0f77f3b8 ebx=0f77f468 ecx=00000003 edx=00000000 esi=00eceba8 edi=00ef7dd4
          eip=76b6c232 esp=0f77f3b8 ebp=0f77f414 iopl=0         nv up ei pl nz ac pe nc
          cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000216
          KERNELBASE!RaiseException+0x62:
          76b6c232 8b4c2454        mov     ecx,dword ptr [esp+54h] ss:002b:0f77f40c=93fc57d7
          Resetting default scope

          FAULTING_IP: 
          Server!abort+28 [d:\th\minkernel\crts\ucrt\src\appcrt\startup\abort.cpp @ 77]
          00ea0d4f cd29            int     29h

          EXCEPTION_RECORD:  0f77ef08 -- (.exr 0xf77ef08)
          ExceptionAddress: 76b6c232 (KERNELBASE!RaiseException+0x00000062)
             ExceptionCode: e06d7363 (C++ EH exception)
            ExceptionFlags: 00000001
          NumberParameters: 3
             Parameter[0]: 19930520
             Parameter[1]: 0f77f468
             Parameter[2]: 00ef7dd4
            pExceptionObject: 0f77f468
            _s_ThrowInfo    : 00ef7dd4
            Type            : class std::bad_alloc
            Type            : class std::exception

          可以使用 .cxr 0xf77ef58 切換上下文,使用 .exr 0xf77ef08 查看異常信息。

          1. 調(diào)用棧

            windbg-analyze-v-callstack

            注意:上圖中紅色部分,表示正在處理 std::_Xbad_alloc 異常。黃色部分是一個(gè)警告,意思是說(shuō)下面的調(diào)用棧可能不準(zhǔn)確。

          看樣子,極有可能是內(nèi)存分配的問(wèn)題了。當(dāng)時(shí)想著一定要找個(gè)時(shí)間分析一下。過(guò)了兩三天,終于有了一點(diǎn)兒時(shí)間(今年加班是真的多),折騰完調(diào)試環(huán)境(主要是加載調(diào)試符號(hào),從微軟服務(wù)器下載符號(hào)有時(shí)候需要梯子),就可以開(kāi)始分析了。

          初次分析

          加載好符號(hào)后,根據(jù) !analyze -v 的結(jié)果,先執(zhí)行 .cxr 0xf77ef58,切換上下文,然后執(zhí)行 kp 查看調(diào)用棧(p 表示顯示參數(shù),應(yīng)該是 parameter 的縮寫(xiě))。如下圖:

          append-string-call-stack

          從整個(gè)調(diào)用棧來(lái)看,是在拼接兩個(gè)字符串時(shí),需要分配  0x8e52d 字節(jié)的空間,但是失敗了,拋出了 std::_Xbad_alloc 異常。

          說(shuō)明: 看到 std::_Xbad_alloc 時(shí),腦子里自然而然地想到了兩種可能:

          1. 分配的內(nèi)存超級(jí)大(本例不屬于這種情況)
          2. 內(nèi)存不夠用了,沒(méi)有一塊內(nèi)存可以滿足本次的分配請(qǐng)求。

          之前遇到的 std::_Xbad_alloc 異常都是嘗試分配的內(nèi)存超級(jí)大(非常典型的是在反序列化 vector, string 的時(shí)候,由于內(nèi)存錯(cuò)位導(dǎo)致嘗試分配超大內(nèi)存),但是本次的分配請(qǐng)求看上去相對(duì)合理。

          既然是跟內(nèi)存分配相關(guān)的問(wèn)題(而且是標(biāo)準(zhǔn)庫(kù)中涉及到的內(nèi)存分配),很可能跟堆有關(guān),可以查看堆相關(guān)的信息。在 windbg 中執(zhí)行 !heap -s 命令,查看堆概要信息。

          heap-s

          看到上圖中的輸出結(jié)果,我很(草)快(率)得(湊)出了一個(gè)結(jié)論。

          草率了

          我犯的第一個(gè)超級(jí)愚蠢的錯(cuò)誤是:一看 Free 這一列顯示的值是 334147。而 0x8e52d 的十進(jìn)制值是 582957(可以在 windbg 中通過(guò) .formats 0x8e52d 方便的顯示出來(lái)),就跟朋友說(shuō)是堆空間不足導(dǎo)致的,建議從內(nèi)存泄漏這個(gè)角度排查一下。做出這個(gè)結(jié)論的依據(jù)是:Free 一列給出的值比要分配的值"小"。

          為什么草率了呢?

          首先,我忽略了 Free 的單位,在 Free 的下方明顯的寫(xiě)著 (k),說(shuō)明這一列是按 KB 計(jì)算的 。

          其次,這一列給出的數(shù)不是 10 進(jìn)制的,如果仔細(xì)看其它相關(guān)信息,可以推斷出這里的值是以 16 進(jìn)制表示的,比如 Lock cont. 的值是 1b61,明顯是 16 進(jìn)制。

          既然是分配一個(gè)正常大小的內(nèi)存塊失敗了,那肯定是堆里沒(méi)有能滿足請(qǐng)求大小的空閑堆塊了。

          如果能找出堆中空閑的堆塊,而且每個(gè)堆塊的大小都比本次分配請(qǐng)求的大小(0x8e52d )要小,說(shuō)明沒(méi)有一個(gè)空閑堆塊能滿足本次的分配請(qǐng)求,那么拋出 std::_Xbad_alloc 就很正常了。

          接著分析

          可以通過(guò) !heap -a 打印出所有信息。在 windbg 中輸入 !heap -a 00a80000,因?yàn)檩敵鼋Y(jié)果太多了,在執(zhí)行此命令前,先執(zhí)行 .logopen d:\heap.txt,這樣輸出結(jié)果會(huì)同時(shí)保存到日志文件 d:\heap.txt 中,執(zhí)行完需要的命令后,可以通過(guò)執(zhí)行 .logclose 關(guān)閉日志文件。

          接下來(lái),從輸出信息中查找空閑堆塊。

          我不會(huì)告訴你,最開(kāi)始找空閑堆塊的時(shí)候我是傻傻的到每一個(gè) Segment 里面找里面包含的 free 堆塊(flags100)。

          下圖是 Sement27 中的部分 Free 堆塊截圖:

          view-free-heap-entry-in-segment

          由于實(shí)在是太多了,找到一半的時(shí)候真的是找(累)不(成)動(dòng)(狗)了!于是停下來(lái)開(kāi)始思考:

          • windbg 難道沒(méi)有直接查找空閑堆塊的命令嗎?

          • 有插件可以做這事嗎?

          • 自己寫(xiě)個(gè)腳本(程序)整理下輸出結(jié)果?

          • 想個(gè)辦法過(guò)濾一下空閑塊,然后再排序?

          最后一個(gè)想法簡(jiǎn)單易行:把所有包含 [100] 的行找出來(lái),然后保存為 .csv 文件。按 [100] 前面的字段(當(dāng)前堆塊的大小)降序排列,就可以很快找出最大空閑堆塊了。

          整個(gè)過(guò)程參考下面視頻:

          最后發(fā)現(xiàn)最大空閑堆塊的大小是 0x7f000,比 0x8e52d 要小。

          意外發(fā)現(xiàn)

          在查找過(guò)程中偶然發(fā)現(xiàn)了一個(gè)有意思的現(xiàn)象,同一個(gè)地址會(huì)出現(xiàn)兩次,比如 03c8da98 這個(gè)地址:

          same-address-showup-twice

          仔細(xì)一看,所有的空閑堆塊會(huì)被單獨(dú)整理出來(lái),放到 FreeList 下。趕緊通過(guò) .hh !heap 查看幫助手冊(cè),關(guān)鍵部分截圖如下:

          原來(lái)可以通過(guò) !heap -f heap_address  直接顯示所有的空閑堆塊。于是在 windbg 中輸入 !heap -f 00a80000 驗(yàn)證一下,果然可以列出所有空閑堆塊。

          view-all-free-heap-entry

          而且,空閑塊是按大小排序的,找到最后一個(gè)空閑塊,就找到了最大的空閑塊(大小是 0x7f000),比上面的方法簡(jiǎn)單太多了。

          又草率了

          查找完所有空閑堆塊后發(fā)現(xiàn),沒(méi)有一個(gè)空閑堆塊的大小比 0x8e52d 還要大。于是,趕緊跟朋友說(shuō)上次分析的結(jié)果應(yīng)該沒(méi)錯(cuò),但是判斷依據(jù)不對(duì)。告訴朋友可以在 windbg 中執(zhí)行!heap -a a80000 ,然后查找 FreeList 對(duì)應(yīng)的記錄,就可以確定沒(méi)有一塊空閑堆塊滿足本次的分配請(qǐng)求了。

          跟朋友說(shuō)完之后長(zhǎng)出了一口氣,心想這次肯定穩(wěn)了,應(yīng)該沒(méi)問(wèn)題了。

          不對(duì)勁兒

          過(guò)了一兩天,忙完手頭工作后,又想起這件事,隱約感到有些不對(duì)勁。趕緊翻開(kāi)《軟件調(diào)試》第一版查找關(guān)于堆的介紹,在第 2323.4.1 節(jié) 655 頁(yè)中有非常清晰的描述,摘錄如下:

          HEAP_ENTRY 結(jié)構(gòu)的前兩個(gè)字節(jié)是以分配粒度表示的堆塊大小。分配粒度通常為 8,這意味著每個(gè)堆塊的最大值是 216 次方乘以 8,即 0x10000 * 8 = 0x80000 = 524288 字節(jié) = 512 KB,因?yàn)槊總€(gè)堆塊至少要有 8 字節(jié)的管理信息,因此應(yīng)用程序可以使用的最大堆塊便是 0x80000-8=0x7FFF8,這也正是 SDK 文檔中所給出的數(shù)值(位于 HeapCreate 函數(shù) dwMaximumSize 參數(shù)的說(shuō)明中)。不過(guò)這并不意味著不可以從 Win32 堆上分配到更大的內(nèi)存塊。當(dāng)一個(gè)應(yīng)用程序要分配大于 512KB 的堆塊時(shí),如果堆標(biāo)志包含 HEAP_GROWABLE(2),那么堆管理器便會(huì)直接調(diào)用 ZwAllocateVirtualMemory 來(lái)滿足這次分配,并把分得地址記錄在 HEAP 結(jié)構(gòu)的 VirtualAllocatedBlocks 所指向的鏈表中。

          有了理論依據(jù),實(shí)際用 windbg 查看一下。該看哪些內(nèi)容呢?

          1. 堆是否是可增長(zhǎng)的?

            ntdll!_HEAP 中的 Flags 字段如果為 2 則表示堆是可以自動(dòng)增長(zhǎng)的。

          2. 閾值是多少?本次請(qǐng)求是否超出了閾值?

            閾值是由 VirtualMemoryThreshold 中的值乘以分配粒度計(jì)算得到的。

            說(shuō)明: 分配粒度與 _HEAP_ENTRY 的大小一致,32 位程序是 8 字節(jié), 64 位程序是 16 字節(jié)。可以在 !heap -f 00a80000 的輸出結(jié)果中看到,Granularity8 bytes

          windbg 中輸入如下命令 dt _HEAP -y Flags VirtualMemoryThreshold a80000 即可查看這兩個(gè)關(guān)鍵的信息,如下圖:

          view-flags-and-virtualmemorythreshold

          可以確認(rèn)堆是可增長(zhǎng)的,而且閾值是 0xfe00 * 8 = 0x7f000。本次請(qǐng)求大小( 0x8e52d )超出了閾值,會(huì)直接通過(guò) ZwAllocVirtualMemory 進(jìn)行分配。

          說(shuō)明: 空閑列表中的最大空閑塊的尺寸也是 0x7f000。前面做了這么多無(wú)用功,真是浪費(fèi)時(shí)間。

          既然請(qǐng)求大小超出了閾值,接下來(lái)的任務(wù)是找到整個(gè)內(nèi)存空間中最大空閑區(qū)域,看看其是否能滿足本次分配請(qǐng)求(應(yīng)該是不滿足,要不然也不會(huì)拋出異常了)。

          查找空閑區(qū)域

          相信,很多小伙伴兒都知道使用 !address 查看地址信息。我經(jīng)常在排查內(nèi)存訪問(wèn)異常的時(shí)候,通過(guò)該命令查看某個(gè)地址的詳細(xì)信息。比如,下圖是通過(guò) !address 0x12345678 查看到的關(guān)于地址 0x12345678 的相關(guān)信息:

          address-0x12345678

          另外一個(gè)非常有用的命令是  !address -summary, 可以查看地址空間概要信息。

          address-summary

          通過(guò)查看 Usage Summary 的數(shù)據(jù)可以發(fā)現(xiàn),在整個(gè)內(nèi)存空間中,堆空間大小是 3.930GB,占比 98.25%。通過(guò)查看 State Summary 的數(shù)據(jù)可知,保留( MEM_RESERVE )大小是 3.105GB,提交(MEM_COMMIT)大小是 910.848MB。通過(guò)查看 Largest Region by Usage 可以發(fā)現(xiàn),最大的空閑內(nèi)存大小是 0x1c000,要比本次申請(qǐng)的大小 0x8e52d 小很多。

          內(nèi)存嚴(yán)重碎片化

          說(shuō)實(shí)話,剛開(kāi)始我是有點(diǎn)不敢相信 !address -summary 給出的結(jié)果:最大的空閑空間居然只有區(qū)區(qū)的 112KB

          繼續(xù)使用 !address -f:Free -o:csv 命令打印出所有的空閑內(nèi)存。整理后的結(jié)果如下圖所示:

          -o:csv 表示以逗號(hào)分隔的 csv 格式輸出,這樣可以把結(jié)果直接粘貼到 csv 文件中。

          view-largest-free-region-in-process

          可以看出,跟 !address -summary 給出的結(jié)論是一致的。最大空閑區(qū)域的大小是 0n114688(十六進(jìn)制對(duì)應(yīng)的數(shù)值是 0x1c000)。空閑內(nèi)存的總大小是 0n5791744,只有區(qū)區(qū)的 5656KB(也就是 5.23MB)。碎片真的是太嚴(yán)重了。

          再回過(guò)頭仔細(xì)看 !heap -s 的輸出結(jié)果。其中有如下提示:

          Virtual address fragmentation 77 % (1713 uncommited ranges)

          意思是說(shuō)虛擬地址空間的碎片率已經(jīng)高達(dá) 77% 。看來(lái)這次的崩潰確實(shí)是由內(nèi)存碎片導(dǎo)致的。

          總結(jié)

          之前只是理論上知道如果碎片太嚴(yán)重可能導(dǎo)致分配失敗,沒(méi)想到這次真遇到了。剛開(kāi)始給出的結(jié)論雖然正確,但是理由太牽強(qiáng),站不住腳。

          • 使用 windbg 分析轉(zhuǎn)儲(chǔ)文件,先執(zhí)行 !analyze -v,很多情況下問(wèn)題就解決了。
          • 發(fā)生異常的時(shí)候會(huì)保存線程上下文和異常相關(guān)信息。異常信息可以通過(guò) .exr address 查看,線程上下文可以通過(guò) .cxr address 查看并切換。
          • windbg 中可以通過(guò) .hh command 查看對(duì)應(yīng)命令的幫助。
          • windbg 中的 !heap 擴(kuò)展命令是查看堆信息的好幫手,!heap -s 可以給出堆的概要信息。!heap -f heap_address 可以顯示所有空閑堆塊,!heap -a heap_address 可以顯示所有堆塊。
          • !address 命令相當(dāng)強(qiáng)大。!address address 可以查看指定內(nèi)存地址的信息,!address -summary 可以給出一個(gè)概要信息,!address -f:Type 可以給出特定的內(nèi)存區(qū)域,甚至后面還可以跟命令。
          • 堆中分配內(nèi)存時(shí),如果堆大小可以增長(zhǎng)并且申請(qǐng)的大小超出閾值(VirtualMemoryThreshold * Granularity),會(huì)直接使用 ZwAllocVirtualMemory 進(jìn)行分配。

          參考資料

          《軟件調(diào)試》第一版

          windbg 幫助文檔

          未完待續(xù)

          后面又跟朋友折騰了一些其它問(wèn)題,頗有收獲,希望能有時(shí)間做個(gè)總結(jié)。

          • 為什么 32 位程序可以使用的內(nèi)存空間是 4GB?高 2GB 應(yīng)該是系統(tǒng)使用的才對(duì)?用戶態(tài)代碼也可以使用?

          • 如果一條命令輸出結(jié)果太多,直接顯示的話太慢,怎么辦?

          • 轉(zhuǎn)儲(chǔ)文件中包含了一些珍貴的數(shù)據(jù)(比如某些充值記錄),如何找出來(lái)?

          • ...


          瀏覽 66
          點(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>
                  sese亚洲 | 亚洲视频中文字幕在线播放 | 青草网视频 | 高清无码-熊猫成人网 | 国产福利1000 |