dump net core lldb 分析

原文https://www.cnblogs.com/calvinK/p/9274239.html

centos7 lldb 調試netcore應用的內存泄漏和死循環示例(dump文件調試)

 

寫個demo來玩一玩linux平臺下使用lldb加載sos來調試netcore應用。
當然,在真實的產線環境中需要分析的數據和難度遠遠高於demo所示,所以demo的作用也僅僅只能起到介紹工具的作用。
通常正常情況下,分析個幾天才能得出一個結論的的結果都還是比較令人開心的!,很多時候分析來分析去也搞不出個所以然,也是很正常的(當然,也是自己學藝不精(^_^))
在linux平臺下的sos調試遠沒有在windows下面用windbg來得舒服,該有的命令很多都沒有。
微軟爸爸還要加油努力啊!如果能做到linux下的dmp能在windows下面用windbg之類的工具那就爽翻了,哈哈,當然不可能,臆想一下下拉。

lldb工具的安裝,linux下netcore如何生成dump文件,查看下文
centos7使用lldb調試netcore應用轉儲dump文件

圖片有點多,文章有點長,來一個大綱先

  • 準備DEMO程序的代碼
  • 生成待調試分析的dump文件
  • 目前linux下sos支持的命令
  • 模擬分析內存泄漏
  • 內存泄漏調試分析結論
  • 內存泄漏分析疑問一
  • 內存泄漏分析疑問二
  • 死循環調試分析
  • 內存泄漏調試分析結論

準備DEMO程序的代碼

廢話不多說,先上demo程序代碼。代碼超級簡單,模擬內存泄漏就簡單的往一個靜態list裏面每次插入1M的byte[];死循環則就是一個while(true);
PS:話說markdown插入代碼能不能有收起,展開功能呢。那就爽歪歪拉 @dudu

namespace linxu_dump_lldb.Controllers
{
    class env
    {
        public static bool cpu_flag;
        public static bool setcpu_flag(bool flag) => cpu_flag = flag;
        public static bool getcpu_flag() => cpu_flag;
        public static List<byte[]> memory = new List<byte[]>();
    }
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        public string index() =>(GC.GetTotalMemory(false) / 1024.0 / 1024).ToString("0.00M");
        [HttpGet]
        public void begin_cpu()
        {
            env.setcpu_flag(true);
            Task.Run(() => {while (env.getcpu_flag()){}});
        }
        [HttpGet]
        public void begin_memory()
        {
            var size_1m = 1 * 1024 * 1024;
            for (int i = 0; i < 100; i++)  env.memory.Add(new byte[size_1m]);
        }
        [HttpGet]
        public void end_cpu() => env.setcpu_flag(false);
        [HttpGet]
        public void end_memory()
        {
            env.memory.Clear();
            GC.Collect();
        }}}

  

生成待調試分析的dump文件

生成模擬內存泄漏的dump

請求接口begin_memory來個幾次後,然後通過createdump工具生成dump包,執行了4-5次begin_memory,也就是加了大約400-500M的byte[]放到靜態變量中

生成死循環的dump包

請求接口begin_cpu開始異步任務進入死循環,然後通過createdump工具生成dump包

目前linux下sos支持的命令

當前dotnet版本2.1.1。如下圖所示支持,sos支持的命令,缺少幾個比較有用的命令:ProcInfo ,ObjSize ,SyncBlk,其他缺少的趕腳也用不太上。最最重要的是gdb,lldb的調試命令不熟悉,或者說找不到windbg所對應命令還是蠻難受的,需要進一步認真學習才行...

模擬分析內存泄漏

命令走一個,進入lldb。

/usr/local/llvm-3.9.0/bin/lldb dotnet -c /opt/dump_file/memory_dump -o "plugin load /usr/share/dotnet/shared/Microsoft.NETCore.App/2.1.1/libsosplugin.so"

 

dumpheap -stat 分析先走一波。對堆上面的對象進行統計

大於2kb的對象看一看

圖上反饋byte[]數組對象佔的內存最大,而且是遠超其他類型的,因此可以判定應該是byte[]在代碼的某個地方沒有釋放。進去跟進去即可。
真實情況項目情況很可能是佔用內存最大,對象最多的string對象。分析起來真的有時候看運氣,憑經驗!...(^_^)
dumpheap -mt addr(byte[]數組的MT地址) 過濾看看類型是byte[]的都有那些對象。


看上去特徵特別明顯,全是大小爲1048600的bte[]對象。接下來隨便找一個看看具體對象的數據是什麼
dumpobj addr(對象地址);查看對象的基本結構

內存數據看上去全是 00 00 00。可以說是一個默認的byte[]對象。可以在進入查看一下
sos DumpArray -start 0 -length 10 00007fd5febff9d8(對象地址)
查看數據對象,上一張圖上我們能看到數組的lenght有1048576個,所以加上-start,-length參數,只查看最前面10個對象。不然刷屏得刷死咯。
在接着使用
sos DumpVC(查看值類型命令) 00007fd611151460(數組元素類型的mt地址) 00007fd5febff9e9(數組元素對象的地址)
a 如下圖所示,每個數組元素的類型都是byte,他們的value都是0;

接下來,我們在看看這些個對象的gcroot對象是誰,也就是說這些個對象到底由誰持有
gcroot addr(對象地址)

在挨個看一看,能發現我們的這個list對象lenth有400個,_version=501;這是因爲我clear過一次,所以。clear+1,add([100])個數組,所以400+100+1=501;
如果這是時候有一個objsize命令可以使用,我們就能計算出來這個list是一個400M的醜陋大對象。可惜linux下面木有。

那就只能用查看數據的方法看看這個數組的具體詳情拉。
sos DumpArray -details(可以把每個對象的基本結構都打印出來),能看到他的每一個元素都有1M(size:1048600(0x100018) bytes)大小

內存泄漏調試分析結論

上圖種gcroot有3個結果。
第一個,用DumpArray查看後發現,應該是一個系統的靜態對象,裏面存儲都是context之類的東西。
第二個,就是我們的問題list對象。即List<byte[]>
第三個,是第二個list對象的items。
所以問題就出在我們這個靜態的 list對象上了,那從代碼上搜索一下就比較容易發現我們的List<byte[]>在哪裏了。

疑問一


上圖種是書籍Pro .Net Performance: Optimize Your C# Applications第98頁的一個列子,可惜沒有搞懂他的這個地址怎麼出來的,能直接拉出來堆棧信息...

疑問二

按理來說1M應該等於1048576,那爲什麼這裏顯示是1048600呢,多餘的24byte是啥玩意呢?
dumpobj查看byte[]對象信息
dumpmt查看byte[]類型的mt信息
x addr(對象地址,x命令是lldb的命令,用戶查看地址處的內存數據。可以使用 -c 24指定需要查看多少位數據)

x addr 前16位數據小紅框標記,最後8位小紅框標記。中間的則是1M的01。01:byte數據,代碼直接賦值。

for (int i = 0; i < 100; i++)
{
    var x = new byte[size_1m];
    for (int j = 0; j < x.Length; j++) x[j] = 1;
    env.memory.Add(x);
}

 



但是這24位數據內存結構爲何這麼組織,以及具體的含義就不是特別清楚了,有待考證!!!
學藝不精!,準備回家看看C#本質論有沒有說到這部分內容...或者哪位大哥可以說清楚一下,不勝感激!!!
google搜索的時候發現 Pro .Net Performance: Optimize Your C# Applications,這本書很屌啊!!!,絕壁值得一看,就是英文不行,求中文版啊!!!,好想吐槽一下國內的垃圾編輯或作者,好的書一本都不翻譯,垃圾玩意全翻譯過來。
http://codingsight.com/precise-computation-of-clr-object-size/

https://stackoverflow.com/questions/38056513/why-does-windbg-show-system-int32-variables-as-24-bytes

死循環調試分析

clrthreads -live 先看看還在運行的線程有那些。然後通過thread select 線程編號(lldb命令)。來切換到當前線程。線程編號不是列表種的id字段,而是最前面一行的id。lldb 可以通過thread list命令來列舉所有線程。


剩下的工作就是體力活動拉,一個一個看,一個一個分析。
比如,我們切換到線程3看一看他當前的堆棧信息
clrstack命令可以查看當前線程在託管代碼種的堆棧信息。
dumstack則可以看到非託管代碼種的堆棧信息
thread backtrace lldb查看堆棧信息的命令。


線程3,能看到當前棧在非託管代碼中(libcoreclr.so!TwoWayPipe::WaitForConnection),看方法名字也能猜到幹嘛的,不太像我們的目標。
另外,linux下面
ps -T -p 32728 命令可以查看到進行下線程的基本情況
top -H -p 32728 更happy。
所以在排查高cpu問題的時候能提供許多便利性,反而比內存問題要來得方便很多。(圖中的pid等數據不是一致性的。因爲在寫blog的時候圖片是多次截取的。)


所以在dump包的時候可以記錄下來高cpu的線程id,然後通過thread select 找到對應的線程編號。在然後直接切換過去看一看就完事拉。
所以 thread select 30
clrstack看一看,嗯!當前線程在 linxu_dump_lldb.Controllers.ValuesController+<>c.<begin_cpu>b__1_0() [C:\Users\czd89\source\repos\ConsoleApp4\linxu_dump_lldb\Controllers\ValuesController.cs @ 31]。

看一看當前棧上面都有一些上面參數
CLRStack [-a] [-l] [-p];-p:看參數,-l:看局部變量,-a:=-l+-p;


當然,我們的代碼是異步的,也沒有捕獲任何action裏面的變量,所以這裏的這個參數,以及參數裏面的屬性啥都沒有。
從dll反編譯代碼也能和我們lldb看到的東西一一對以上。

內存泄漏調試分析結論

到這裏,問題就很明顯能看出來了,當然主要還是我們的DEMO是最簡單的。還是開篇說過的那句話:通常正常情況下,分析個幾天才能得出一個結論的的結果都還是比較令人開心的!,很多時候分析來分析去也搞不出個所以然,也是很正常的(當然,也是自己學藝不精(^_^),當自勉!)
還能看一看具體方法的彙編代碼等信息。

參考資料:
https://docs.microsoft.com/en-us/dotnet/framework/tools/sos-dll-sos-debugging-extension
https://github.com/dotnet/coreclr/blob/master/Documentation/building/debugging-instructions.md
https://lldb.llvm.org/tutorial.html
https://stackoverflow.com/questions/38056513/why-does-windbg-show-system-int32-variables-as-24-bytes
http://codingsight.com/precise-computation-of-clr-object-size/
https://zhuanlan.zhihu.com/p/20838172
https://blog.csdn.net/inuyashaw/article/details/55095545

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章