好久沒有寫過 .NET Core 相關的文章了,目前關店在家休息因此有些時間寫一篇新的🤣。此次的文章主要介紹如何在 Linux 上編譯調試最新的 .NET Core 5.0 Preview 與簡單分析 Span 的實現原理。微軟從 .NET Core 5.0 開始把 GIT 倉庫 coreclr 與 corefx 合併移動到了 runtime 倉庫,原有倉庫僅用於維護 .NET Core 3.x,你能夠從如下地址查看最新的源代碼:html
https://github.com/dotnet/runtimelinux
爲了方便重現,接下來的編譯調試會使用 docker 與 ubuntu 18.04 鏡像(儘管微軟提供了編譯專用的鏡像但並不適合調試分析),步驟會與以前的博客介紹的 1.1,書籍介紹的 2.1 有一些不一樣。c++
若是你以爲閱讀這篇文章有困難,能夠參考我以前發佈的 .NET Core 源代碼分析系列或者書籍《.NET Core 底層入門》,書籍的購買連接在文章最後。git
本文編譯的版本是 0d607a757372e3ecc8e942141d7f586a98694e42github
執行如下命令便可建立一個 ubuntu 18.04 的 docker 容器,注意建立時須要使用 --privileged 參數,不然沒法使用 lldb 或者 gdb 調試程序。web
docker run -it --privileged ubuntu:18.04
.NET Core 5.0 要求的 cmake 版本很是高,咱們須要添加第三方源來安裝新版本的 cmake:docker
apt-get update apt-get install apt-transport-https ca-certificates gnupg software-properties-common wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | apt-key add - apt-add-repository 'deb https://apt.kitware.com/ubuntu/ bionic main' apt-get update
這個步驟與以前版本的 .NET Core 相同:json
apt-get install git wget locales locales-all vim apt-get install cmake llvm-3.9 clang-9 libunwind8 libunwind8-dev gettext libicu-dev liblttng-ust-dev libcurl4-openssl-dev libssl-dev libnuma-dev libkrb5-dev
這個步驟也與以前的 .NET Core 相同,但由於 corefx 合併到了同一個倉庫中,執行如下步驟之後會同時編譯 corefx 的 dll 文件。注意這個步驟編譯的是 Debug 版本的運行時,方便後面的調試。ubuntu
git clone https://github.com/dotnet/runtime cd runtime ./build.sh
編譯完成後你能夠在 artifacts 文件夾下找到編譯結果。vim
接下來咱們會看如何使用本身編譯的 .NET Core 執行一個 Hello World 程序,.NET Core 5.0 會同時編譯出 dotnet 程序,咱們可使用它代替 corerun 來簡化運行步驟(不須要像之前的版本同樣手動複製 corefx 的 dll或者設置 CORE_ROOT
環境變量)。但由於 runtime 倉庫中不包括 sdk(sdk 在 sdk 倉庫中,此次懶得編譯),咱們仍然須要另外安裝一個官方的 .NET Core 用於建立與編譯 Hello World 程序。
wget -q https://packages.microsoft.com/config/ubuntu/19.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb dpkg -i packages-microsoft-prod.deb apt-get update apt-get install dotnet-sdk-3.1
mkdir /console cd /console dotnet new console dotnet build
由於使用了 .NET Core 3.1 的 SDK 編譯,咱們還須要修改 程序名.runtimeconfig.json
中的運行時版本號,不然會出現版本號不一致而執行失敗的問題。
cd /console/bin/Debug/netcoreapp3.1 vi console.runtimeconfig.json
須要修改兩處:
runtimeOptions.tfm
修改到 netcoreapp5.0
runtimeOptions.framework.version
修改到 5.0.0
修改完之後使用如下命令便可執行:
/runtime/artifacts/bin/testhost/netcoreapp5.0-Linux-Debug-x64/dotnet console.dll
若是看到 Hello World 輸出就表明執行成功了。
在 linux 上調試 .NET Core 通常使用 lldb (gdb 也能夠可是沒有 SOS 插件支持),SOS 插件的源代碼被搬到了 diagnostics 倉庫,因此咱們還須要下載編譯這個倉庫的源代碼。
安裝 LLDB 與 LLDB 的開發文件:
apt-get install clang llvm lldb liblldb-3.9-dev
下載編譯 diagnostics 倉庫:
git clone https://github.com/dotnet/diagnostics cd diagnostics ./build.sh
編譯成功後你能夠在 /diagnostics/artifacts/bin/Linux.x64.Debug/libsosplugin.so
找到 SOS 插件的 dll 文件。
SOS 插件須要在執行到達 LoadLibraryExW 後才能夠正常使用,使用 LLDB 的 -o 參數能夠省略每次調試的時候都要作的準備工做:
cd /console/bin/Debug/netcoreapp3.1 lldb \ -o "plugin load /diagnostics/artifacts/bin/Linux.x64.Debug/libsosplugin.so" \ -o "process launch -s" \ -o "process handle -s false SIGUSR1 SIGUSR2" \ -o "b LoadLibraryExW" \ -o "c" \ -o "br del 1" \ -o "sos Help" \ /runtime/artifacts/bin/testhost/netcoreapp5.0-Linux-Debug-x64/dotnet console.dll
執行之後會停在 LoadLibraryExW 並打印出 SOS 插件的幫助,接下來咱們可使用 SOS 插件給託管函數下斷點:
sos bpmd console.dll console.Program.Main
而後使用 c 命令繼續執行程序,直到觸發斷點:
c
到達斷點(JIT 編譯後的託管函數 Main)之後咱們可使用 SOS 插件打印這個託管函數編譯出來的彙編內容:
sos u $rip
若是到此都沒有問題,那麼接下來咱們能夠開始分析 Span 的實現原理了。
Span 與 Memory 是微軟推出的,用於表示某段子內容的數據類型,它們的主要目的是爲了減小內存分配與複製,例如取 "abcdefg" 的子字符串 "def",傳統的方法 (Substring) 會分配一個長度爲 3 的新字符串而後複製 "def" 過去,但 Span 與 Memory 能夠直接使用原有的對象、子內容的開始位置與子內容的長度來表示一段子內容。在其餘語言中也有相似 Span 與 Memory 的概念,例如 go 中的 slice,c 中指針與長度的結合 (例如 struct char_view { char* ptr, size_t size; }
),與 c++ 中的 string_view
和 span
類型。
Span 與 Memory 的區別在於,Memory 是一個普通的類型,只保存 原有的對象
、子內容的開始地址
與 子內容的長度
,在內存中的表現能夠參考下圖:
Memory 與很早就存在的 ArraySegment 實質上是同樣的,只是支持更多的類型,它們都不須要運行時或者編譯器的額外支持。
Span 則特殊不少,它保存了子內容的開始地址與長度(不保存原始對象的地址),使得它不須要計算開始地址而且容許指向託管對象之外的內容 (例如從 stackalloc 分配)。Span 在內存中的表現能夠參考下圖:
Span 是一個 ref struct
類型 (這個類型能夠說是專門爲 Span 發明的),ref struct
只能保存在於棧上或者做爲其餘 ref struct
的成員 (最終來講只能保存在於棧上),Span 只能存在於棧上主要有如下緣由:
由於 Span 須要運行時的額外支持,在 .NET Framework 與 Mono 上使用的 Span (從 Nuget 包安裝的) 實際上與 Memory 同樣,只有在 .Net Core 上纔有以上的特性。
此外,由於部分對象的內容不可修改 (例如 string),因此還有配套的 ReadOnlySpan
與 ReadOnlyMemory
,它們除了在編譯器層面上限制修改之外,與原類型沒有什麼區別。
接下來咱們能夠調試一個示例程序,簡單分析 Span 在運行時中的實現原理 (此次分析不涉及到 JIT 部分,雖然 JIT 部分不多)。
如下是示例程序的代碼:
using System; namespace console { class Program { static void Main(string[] args) { Span<byte> span = new byte[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; span = span.Slice(5, 2); GC.Collect(); Console.WriteLine(span.Length); } } }
編譯示例程序與執行 LLDB 的命令請參考前面的內容,執行後可使用如下命令給託管函數 Main
下斷點而後執行到斷點,並查看彙編代碼:
sos bpmd console.dll console.Program.Main c sos u $rip
輸出以下:
(lldb) sos bpmd console.dll console.Program.Main Adding pending breakpoints... (lldb) c Process 6460 resuming JITTED console!console.Program.Main(System.String[]) Setting breakpoint: breakpoint set --address 0x00007FFF7BB352D0 [console.Program.Main(System.String[])] Process 6460 stopped * thread #1, name = 'dotnet', stop reason = breakpoint 3.1 frame #0: 0x00007fff7bb352d0 -> 0x7fff7bb352d0: pushq %rbp 0x7fff7bb352d1: pushq %r13 0x7fff7bb352d3: subq $0x48, %rsp 0x7fff7bb352d7: vzeroupper (lldb) sos u $rip Normal JIT generated code console.Program.Main(System.String[]) ilAddr is 00007FFFF18BB250 pImport is 00005576894771F0 Begin 00007FFF7BB352D0, size bc /console/Program.cs @ 9: >>> 00007fff7bb352d0 55 push rbp 00007fff7bb352d1 4155 push r13 00007fff7bb352d3 4883ec48 sub rsp, 0x48 00007fff7bb352d7 c5f877 vzeroupper 00007fff7bb352da 488d6c2450 lea rbp, [rsp + 0x50] 00007fff7bb352df 4c8bef mov r13, rdi 00007fff7bb352e2 488d7db0 lea rdi, [rbp - 0x50] 00007fff7bb352e6 b910000000 mov ecx, 0x10 00007fff7bb352eb 33c0 xor eax, eax 00007fff7bb352ed f3ab rep stosd dword ptr es:[rdi], eax 00007fff7bb352ef 498bfd mov rdi, r13 00007fff7bb352f2 48897df0 mov qword ptr [rbp - 0x10], rdi 00007fff7bb352f6 48bfe05fd87bff7f0000 movabs rdi, 0x7fff7bd85fe0 00007fff7bb35300 be0a000000 mov esi, 0xa 00007fff7bb35305 e8063fe079 call 0x7ffff5939210 (JitHelp: CORINFO_HELP_NEWARR_1_VC) 00007fff7bb3530a 488945d8 mov qword ptr [rbp - 0x28], rax 00007fff7bb3530e 48bf2894e07bff7f0000 movabs rdi, 0x7fff7be09428 00007fff7bb35318 e8b396e079 call 0x7ffff593e9d0 (JitHelp: CORINFO_HELP_FIELDDESC_TO_STUBRUNTIMEFIELD) 00007fff7bb3531d 488945d0 mov qword ptr [rbp - 0x30], rax 00007fff7bb35321 488b7dd8 mov rdi, qword ptr [rbp - 0x28] 00007fff7bb35325 488b75d0 mov rsi, qword ptr [rbp - 0x30] 00007fff7bb35329 e8829f307a call 0x7ffff5e3f2b0 (System.Runtime.CompilerServices.RuntimeHelpers.InitializeArray(System.Array, System.RuntimeFieldHandle), mdToken: 0000000006003730) 00007fff7bb3532e 488b7dd8 mov rdi, qword ptr [rbp - 0x28] 00007fff7bb35332 e8f9ecffff call 0x7fff7bb34030 (System.Span`1[[System.Byte, System.Private.CoreLib]].op_Implicit(Byte[]), mdToken: 00000000060012B1) 00007fff7bb35337 488945c0 mov qword ptr [rbp - 0x40], rax 00007fff7bb3533b 488955c8 mov qword ptr [rbp - 0x38], rdx 00007fff7bb3533f c5fa6f45c0 vmovdqu xmm0, xmmword ptr [rbp - 0x40] 00007fff7bb35344 c5fa7f45e0 vmovdqu xmmword ptr [rbp - 0x20], xmm0 /console/Program.cs @ 10: 00007fff7bb35349 488d7de0 lea rdi, [rbp - 0x20] 00007fff7bb3534d be05000000 mov esi, 0x5 00007fff7bb35352 ba02000000 mov edx, 0x2 00007fff7bb35357 e844edffff call 0x7fff7bb340a0 (System.Span`1[[System.Byte, System.Private.CoreLib]].Slice(Int32, Int32), mdToken: 00000000060012BE) 00007fff7bb3535c 488945b0 mov qword ptr [rbp - 0x50], rax 00007fff7bb35360 488955b8 mov qword ptr [rbp - 0x48], rdx 00007fff7bb35364 c5fa6f45b0 vmovdqu xmm0, xmmword ptr [rbp - 0x50] 00007fff7bb35369 c5fa7f45e0 vmovdqu xmmword ptr [rbp - 0x20], xmm0 /console/Program.cs @ 11: 00007fff7bb3536e e845b3ffff call 0x7fff7bb306b8 (System.GC.Collect(), mdToken: 0000000006000361) /console/Program.cs @ 12: 00007fff7bb35373 488d7de0 lea rdi, [rbp - 0x20] 00007fff7bb35377 e87cecffff call 0x7fff7bb33ff8 (System.Span`1[[System.Byte, System.Private.CoreLib]].get_Length(), mdToken: 00000000060012AC) 00007fff7bb3537c 8bf8 mov edi, eax 00007fff7bb3537e e8a5fcffff call 0x7fff7bb35028 (System.Console.WriteLine(Int32), mdToken: 0000000006000089) /console/Program.cs @ 13: 00007fff7bb35383 90 nop 00007fff7bb35384 488d65f8 lea rsp, [rbp - 0x8] 00007fff7bb35388 415d pop r13 00007fff7bb3538a 5d pop rbp 00007fff7bb3538b c3 ret
咱們能夠看到 00007fff7bb35305 處的指令從託管堆分配了數組,00007fff7bb35329 處的指令初始化了數組內容,00007fff7bb35332 處的指令生成了第一個 span 對象,00007fff7bb35357 處的指令生成了第二個 span 對象。你能夠從每一段彙編代碼上標記的文件名與行數找到對應的 C# 代碼。
接下來咱們會分析棧上的內容,包括數組的地址與 span 的內容等。
注意棧上會保存臨時變量和不使用的參數,這是由於以前的編譯沒有使用 Release 配置,你可使用 Release 配置編譯再按這裏的步驟試試有什麼不一樣 (可能會更難理解一些),使用 Release 配置時請關閉分層編譯,使用 export COMPlus_TieredCompilation=0
便可關閉。
首先咱們來看看分配數組以前棧上 (當前幀) 有什麼內容:
(lldb) b 0x00007fff7bb35305 Breakpoint 4: address = 0x00007fff7bb35305 # 分配數組的指令 (lldb) c Process 6460 resuming Process 6460 stopped * thread #1, name = 'dotnet', stop reason = breakpoint 4.1 frame #0: 0x00007fff7bb35305 -> 0x7fff7bb35305: callq 0x7ffff5939210 ; JIT_NewArr1VC_MP_FastPortable at jithelpers.cpp:2560 0x7fff7bb3530a: movq %rax, -0x28(%rbp) 0x7fff7bb3530e: movabsq $0x7fff7be09428, %rdi ; imm = 0x7FFF7BE09428 0x7fff7bb35318: callq 0x7ffff593e9d0 ; JIT_GetRuntimeFieldStub at jithelpers.cpp:3635 (lldb) p/x $rsp (unsigned long) $2 = 0x00007fffffffd220 # 棧頂 (lldb) p/x $rbp (unsigned long) $3 = 0x00007fffffffd270 # 幀底 (lldb) p $rbp - $rsp (unsigned long) $4 = 80 # 當前幀大小 (lldb) memory read -s 1 -c 80 0x00007fffffffd220 0x7fffffffd220: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ # 本地變量使用的空間 0x7fffffffd230: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ # 本地變量使用的空間 0x7fffffffd240: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ # 本地變量使用的空間 0x7fffffffd250: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ # 本地變量使用的空間 0x7fffffffd260: b0 d5 00 54 ff 7f 00 00 00 00 00 00 00 00 00 00 ...T............ # rbp-0x10 是 args 參數,rbp-0x8 是上一幀 r13 的值
接下來咱們看看原始數組的地址與數組的內容,數組的本地變量 (臨時變量) 會保存到 $rbp-0x28
,咱們能夠直接看這個地址中的內容。
(lldb) b 0x00007fff7bb3532e Breakpoint 5: address = 0x00007fff7bb3532e # 初始化數組後的指令 (lldb) c Process 6460 resuming Process 6460 stopped * thread #1, name = 'dotnet', stop reason = breakpoint 5.1 frame #0: 0x00007fff7bb3532e -> 0x7fff7bb3532e: movq -0x28(%rbp), %rdi 0x7fff7bb35332: callq 0x7fff7bb34030 0x7fff7bb35337: movq %rax, -0x40(%rbp) 0x7fff7bb3533b: movq %rdx, -0x38(%rbp) (lldb) p/x $rbp-0x28 (unsigned long) $6 = 0x00007fffffffd248 (lldb) memory read -s 1 -c 8 0x00007fffffffd248 0x7fffffffd248: 70 ed 00 54 ff 7f 00 00 p..T.... (lldb) dumpobj 7fff5400ed70 # SOS 插件提供的命令,用於輸出託管對象信息 Name: System.Byte[] MethodTable: 00007fff7bd85fe0 EEClass: 00007fff7bd85f30 Size: 34(0x22) bytes Array: Rank 1, Number of elements 10, Type Byte Content: .......... Fields: None (lldb) memory read -s 1 -c 26 0x7fff5400ed70 # 顯示數組對象的內容 0x7fff5400ed70: e0 5f d8 7b ff 7f 00 00 0a 00 00 00 00 00 00 00 ._.{............ # 0~8 是類型信息,8~16 是長度 0x7fff5400ed80: 01 02 03 04 05 06 07 08 09 0a .......... # 16~26 是數組內容
接下來咱們能夠繼續執行,而後看看各個 Span 的內容:
(lldb) b 0x00007fff7bb3536e Breakpoint 6: address = 0x00007fff7bb3536e (lldb) c Process 6460 resuming Process 6460 stopped * thread #1, name = 'dotnet', stop reason = breakpoint 6.1 frame #0: 0x00007fff7bb3536e -> 0x7fff7bb3536e: callq 0x7fff7bb306b8 0x7fff7bb35373: leaq -0x20(%rbp), %rdi 0x7fff7bb35377: callq 0x7fff7bb33ff8 0x7fff7bb3537c: movl %eax, %edi (lldb) memory read -s 1 -c 16 $rbp-0x40 0x7fffffffd230: 80 ed 00 54 ff 7f 00 00 0a 00 00 00 00 00 00 00 ...T............ # 第一個 span (臨時變量) 的開始地址與長度 (lldb) memory read -s 1 -c 16 $rbp-0x50 0x7fffffffd220: 85 ed 00 54 ff 7f 00 00 02 00 00 00 00 00 00 00 ...T............ # 第二個 span (臨時變量) 的開始地址與長度 (lldb) memory read -s 1 -c 16 $rbp-0x20 0x7fffffffd250: 85 ed 00 54 ff 7f 00 00 02 00 00 00 00 00 00 00 ...T............ # 本地變量 span 中的開始地址與長度
從輸出中咱們能夠看到,第一個 span 的地址是 0x7fff5400ed80,這恰好是數組地址 0x7fff5400ed70 加上類型信息 (8) 與長度 (8) 之後的值,
也就是數組的內容,使用如下命令能夠查看這個 span 指向的內容:
(lldb) memory read -s 1 -c 10 0x7fff5400ed80 0x7fff5400ed80: 01 02 03 04 05 06 07 08 09 0a ..........
而第二個 span 的地址 0x7fff5400ed85 則是第一個 span 的地址加 5,而且長度爲 2,使用如下命令能夠查看這個 span 指向的內容:
(lldb) memory read -s 1 -c 2 0x7fff5400ed85 0x7fff5400ed85: 06 07 ..
最後再看看棧上 (當前幀) 的內容:
(lldb) memory read -s 1 -c 80 0x00007fffffffd220 0x7fffffffd220: 85 ed 00 54 ff 7f 00 00 02 00 00 00 00 00 00 00 ...T............ # 本地變量 span 中的開始地址與長度 0x7fffffffd230: 80 ed 00 54 ff 7f 00 00 0a 00 00 00 00 00 00 00 ...T............ # 第一個 span (臨時變量) 的開始地址與長度 0x7fffffffd240: 98 ed 00 54 ff 7f 00 00 70 ed 00 54 ff 7f 00 00 ...T....p..T.... # 用於初始化數組的句柄,原始數組對象 (臨時變量) 0x7fffffffd250: 85 ed 00 54 ff 7f 00 00 02 00 00 00 00 00 00 00 ...T............ # 第二個 span (臨時變量) 的開始地址與長度 0x7fffffffd260: b0 d5 00 54 ff 7f 00 00 00 00 00 00 00 00 00 00 ...T............ # args 參數與上一幀 r13 的值
GC 信息是 .NET 運行時查找各個線程中託管函數的本地變量 (根對象) 時使用的信息,由於 GC 信息的編碼很是複雜,這裏不會介紹如何解碼 GC 信息,
而是下斷點來看各個 Slot 的內容,從掃描到標記的調用鏈跟蹤 (backtrace) 以下:
* frame #0: 0x00007ffff5cb0fcf libcoreclr.so`WKS::gc_heap::mark_object_simple(po=0x00007fffffffa460) at gc.cpp:19675 frame #1: 0x00007ffff5cb6fe8 libcoreclr.so`WKS::GCHeap::Promote(ppObject=0x00007fffffffd230, sc=0x00007fffffffc9c0, flags=1) at gc.cpp:36730 frame #2: 0x00007ffff5808fe8 libcoreclr.so`PromoteCarefully(fn=(libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) at gc.cpp:36666), ppObj=0x00007fffffffd230, sc=0x00007fffffffc9c0, flags=1)(Object**, ScanContext*, unsigned int), Object**, ScanContext*, unsigned int) at siginfo.cpp:4874 frame #3: 0x00007ffff5918c4a libcoreclr.so`GcEnumObject(pData=0x00007fffffffc710, pObj=0x00007fffffffd230, flags=1) at gcenv.ee.common.cpp:167 frame #4: 0x00007ffff5a87abc libcoreclr.so`GcInfoDecoder::ReportStackSlotToGC(this=0x00007fffffffab38, spOffset=-80, spBase=GC_FRAMEREG_REL, gcFlags=1, pRD=0x00007fffffffb5c0, flags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.cpp:1848 frame #5: 0x00007ffff5a88381 libcoreclr.so`GcInfoDecoder::ReportSlotToGC(this=0x00007fffffffab38, slotDecoder=0x00007fffffffa8d0, slotIndex=0, pRD=0x00007fffffffb5c0, reportScratchSlots=true, inputFlags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.h:679 frame #6: 0x00007ffff5a8666d libcoreclr.so`GcInfoDecoder::ReportUntrackedSlots(this=0x00007fffffffab38, slotDecoder=0x00007fffffffa8d0, pRD=0x00007fffffffb5c0, inputFlags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.cpp:1034 frame #7: 0x00007ffff5a85d28 libcoreclr.so`GcInfoDecoder::EnumerateLiveSlots(this=0x00007fffffffab38, pRD=0x00007fffffffb5c0, reportScratchSlots=false, inputFlags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.cpp:983 frame #8: 0x00007ffff570225a libcoreclr.so`EECodeManager::EnumGcRefs(this=0x0000555555822680, pRD=0x00007fffffffb5c0, pCodeInfo=0x00007fffffffb3f0, flags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710, relOffsetOverride=4294967295)(void*, OBJECTREF*, unsigned int), void*, unsigned int) at eetwain.cpp:5150 frame #9: 0x00007ffff5919462 libcoreclr.so`GcStackCrawlCallBack(pCF=0x00007fffffffb1c0, pData=0x00007fffffffc710) at gcenv.ee.common.cpp:283 frame #10: 0x00007ffff580e52f libcoreclr.so`Thread::MakeStackwalkerCallback(this=0x0000555555838aa0, pCF=0x00007fffffffb1c0, pCallback=(libcoreclr.so`GcStackCrawlCallBack(CrawlFrame*, void*) at gcenv.ee.common.cpp:201), pData=0x00007fffffffc710, uFramesProcessed=5)(CrawlFrame*, void*), void*, unsigned int) at stackwalk.cpp:886 frame #11: 0x00007ffff580e77b libcoreclr.so`Thread::StackWalkFramesEx(this=0x0000555555838aa0, pRD=0x00007fffffffb5c0, pCallback=(libcoreclr.so`GcStackCrawlCallBack(CrawlFrame*, void*) at gcenv.ee.common.cpp:201), pData=0x00007fffffffc710, flags=34048, pStartFrame=0x0000000000000000)(CrawlFrame*, void*), void*, unsigned int, Frame*) at stackwalk.cpp:966 frame #12: 0x00007ffff580f337 libcoreclr.so`Thread::StackWalkFrames(this=0x0000555555838aa0, pCallback=(libcoreclr.so`GcStackCrawlCallBack(CrawlFrame*, void*) at gcenv.ee.common.cpp:201), pData=0x00007fffffffc710, flags=34048, pStartFrame=0x0000000000000000)(CrawlFrame*, void*), void*, unsigned int, Frame*) at stackwalk.cpp:1049 frame #13: 0x00007ffff5ceeadb libcoreclr.so`ScanStackRoots(pThread=0x0000555555838aa0, fn=(libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) at gc.cpp:36666), sc=0x00007fffffffc9c0)(Object**, ScanContext*, unsigned int), ScanContext*) at gcenv.ee.cpp:146 frame #14: 0x00007ffff5cee7ab libcoreclr.so`GCToEEInterface::GcScanRoots(fn=(libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) at gc.cpp:36666), condemned=2, max_gen=2, sc=0x00007fffffffc9c0)(Object**, ScanContext*, unsigned int), int, int, ScanContext*) at gcenv.ee.cpp:182 frame #15: 0x00007ffff5cfa3d9 libcoreclr.so`GCScan::GcScanRoots(fn=(libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) at gc.cpp:36666), condemned=2, max_gen=2, sc=0x00007fffffffc9c0)(Object**, ScanContext*, unsigned int), int, int, ScanContext*) at gcscan.cpp:155 frame #16: 0x00007ffff5c9f701 libcoreclr.so`WKS::gc_heap::mark_phase(condemned_gen_number=2, mark_only_p=NO) at gc.cpp:21062 frame #17: 0x00007ffff5c9b479 libcoreclr.so`WKS::gc_heap::gc1() at gc.cpp:16713 frame #18: 0x00007ffff5cab832 libcoreclr.so`WKS::gc_heap::garbage_collect(n=2) at gc.cpp:18345 frame #19: 0x00007ffff5c90dea libcoreclr.so`WKS::GCHeap::GarbageCollectGeneration(this=0x0000555555793aa0, gen=2, reason=reason_induced) at gc.cpp:38188 frame #20: 0x00007ffff5cdd3bb libcoreclr.so`WKS::GCHeap::GarbageCollectTry(this=0x0000555555793aa0, generation=2, low_memory_p=NO, mode=2) at gc.cpp:37524 frame #21: 0x00007ffff5cde614 libcoreclr.so`WKS::GCHeap::GarbageCollect(this=0x0000555555793aa0, generation=2, low_memory_p=false, mode=2) at gc.cpp:37458 frame #22: 0x00007ffff58be151 libcoreclr.so`GCInterface::Collect(generation=-1, mode=2) at comutilnative.cpp:986 frame #23: 0x00007fff7bb55853 frame #24: 0x00007fff7bb55788 frame #25: 0x00007fff7bb553c3 frame #26: 0x00007ffff5a965f3 libcoreclr.so`CallDescrWorkerInternal at unixasmmacrosamd64.inc:862 frame #27: 0x00007ffff589cc9c libcoreclr.so`CallDescrWorkerWithHandler(pCallDescrData=0x00007fffffffd5a8, fCriticalCall=NO) at callhelpers.cpp:70 frame #28: 0x00007ffff589da1c libcoreclr.so`MethodDescCallSite::CallTargetWorker(this=0x00007fffffffd6e0, pArguments=0x00007fffffffd680, pReturnValue=0x0000000000000000, cbReturnValue=0) at callhelpers.cpp:546 frame #29: 0x00007ffff56ee983 libcoreclr.so`MethodDescCallSite::Call(this=0x00007fffffffd6e0, pArguments=0x00007fffffffd680) at callhelpers.h:459 frame #30: 0x00007ffff5ac1c64 libcoreclr.so`RunMainInternal(pParam=0x00007fffffffd950) at assembly.cpp:1487 frame #31: 0x00007ffff5ac1989 libcoreclr.so`RunMain(this=0x00007fffffffd858, pParam=0x00007fffffffd950)::$_1::operator()(Param*) const::'lambda'(Param*)::operator()(Param*) const at assembly.cpp:1559 frame #32: 0x00007ffff5abf1f9 libcoreclr.so`RunMain(this=0x00007fffffffd940, __EXparam=0x00007fffffffd950)::$_1::operator()(Param*) const at assembly.cpp:1561 frame #33: 0x00007ffff5abf019 libcoreclr.so`RunMain(pFD=0x00007fff7bd5c368, numSkipArgs=1, piRetVal=0x00007fffffffda4c, stringArgs=0x00007fffffffdf20) at assembly.cpp:1561 frame #34: 0x00007ffff5abf4a2 libcoreclr.so`Assembly::ExecuteMainMethod(this=0x00005555557d4d70, stringArgs=0x00007fffffffdf20, waitForOtherThreads=YES) at assembly.cpp:1671 frame #35: 0x00007ffff56e8a6b libcoreclr.so`CorHost2::ExecuteAssembly(this=0x000055555578eb40, dwAppDomainId=1, pwzAssemblyPath=u"/console/bin/Release/netcoreapp3.1/console.dll", argc=0, argv=0x0000000000000000, pReturnValue=0x00007fffffffe100) at corhost.cpp:460 frame #36: 0x00007ffff568822a libcoreclr.so`::coreclr_execute_assembly(hostHandle=0x000055555578eb40, domainId=1, argc=0, argv=0x0000000000000000, managedAssemblyPath="/console/bin/Release/netcoreapp3.1/console.dll", exitCode=0x00007fffffffe100) at unixinterface.cpp:407 frame #37: 0x00007ffff67dfd8a libhostpolicy.so`___lldb_unnamed_symbol100$$libhostpolicy.so + 810 frame #38: 0x00007ffff67e022d libhostpolicy.so`___lldb_unnamed_symbol101$$libhostpolicy.so + 45 frame #39: 0x00007ffff67e095b libhostpolicy.so`corehost_main + 203 frame #40: 0x00007ffff6a4b73c libhostfxr.so`___lldb_unnamed_symbol204$$libhostfxr.so + 1740 frame #41: 0x00007ffff6a49ea1 libhostfxr.so`___lldb_unnamed_symbol202$$libhostfxr.so + 641 frame #42: 0x00007ffff6a444f3 libhostfxr.so`hostfxr_main_startupinfo + 147 frame #43: 0x00005555555623b7 dotnet`___lldb_unnamed_symbol114$$dotnet + 791 frame #44: 0x0000555555562b90 dotnet`___lldb_unnamed_symbol115$$dotnet + 128 frame #45: 0x00007ffff6ca3b97 libc.so.6`__libc_start_main + 231 frame #46: 0x0000555555557810 dotnet`___lldb_unnamed_symbol9$$dotnet + 41
GcInfoDecoder::EnumerateLiveSlots
是枚舉 Slot 的函數,GcInfoDecoder::ReportSlotToGC
是處理各個 Slot 的函數 (包括寄存器與棧),GcInfoDecoder::ReportStackSlotToGC
是處理棧上 (引用類型或 ref 類型) 本地變量的函數。
咱們能夠在 這個位置 下斷點,而後查看解析出的各個 Slot 的信息:
(lldb) b gcinfodecoder.h:679 Breakpoint 8: where = libcoreclr.so`GcInfoDecoder::ReportSlotToGC(GcSlotDecoder&, unsigned int, REGDISPLAY*, bool, unsigned int, void (*)(void*, OBJECTREF*, unsigned int), void*) + 396 at gcinfodecoder.h:679, address = 0x00007ffff5a8836c (lldb) c Process 6460 resuming Process 6460 stopped * thread #1, name = 'dotnet', stop reason = breakpoint 8.1 frame #0: 0x00007ffff5a8836c libcoreclr.so`GcInfoDecoder::ReportSlotToGC(this=0x00007fffffffab28, slotDecoder=0x00007fffffffa8c0, slotIndex=0, pRD=0x00007fffffffb5b0, reportScratchSlots=true, inputFlags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc700)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.h:679 676 GcStackSlotBase spBase = pSlot->Slot.Stack.Base; 677 if( reportScratchSlots || !IsScratchStackSlot(spOffset, spBase, pRD) ) 678 { -> 679 ReportStackSlotToGC( 680 spOffset, 681 spBase, 682 pSlot->Flags, (lldb) p *pSlot (const GcSlotDesc) $12 = { Slot = { RegisterNumber = 4294967216 Stack = (SpOffset = -80, Base = GC_FRAMEREG_REL) } Flags = GC_SLOT_INTERIOR }
這個 Slot 表明 $rbp-80
($rbp-0x50
) 處有引用類型或 ref 類型的本地變量,在前面的內容中咱們已經知道 $rbp-0x50
儲存了第二個 span 對象,此外標誌 GC_SLOT_INTERIOR
表明本地變量是對象中間的內存地址,而不是對象開頭(對象頭以後類型信息以前)的內存地址,這個標誌會對 GC 標記與重定位對象產生很大的影響,微軟官方稱這樣的變量爲 Interior Pointer
。
繼續執行 c
與 p *pSlot
能夠看到其餘 Slot 的內容:
# $rbp-0x40, 即第一個 span 對象 (const GcSlotDesc) $13 = { Slot = { RegisterNumber = 4294967232 Stack = (SpOffset = -64, Base = GC_FRAMEREG_REL) } Flags = GC_SLOT_INTERIOR } # $rbp-0x20, 即本地變量 span (const GcSlotDesc) $14 = { Slot = { RegisterNumber = 4294967264 Stack = (SpOffset = -32, Base = GC_FRAMEREG_REL) } Flags = GC_SLOT_INTERIOR } # $rbp-0x30, 用於初始化數組的句柄 (const GcSlotDesc) $15 = { Slot = { RegisterNumber = 4294967248 Stack = (SpOffset = -48, Base = GC_FRAMEREG_REL) } Flags = GC_SLOT_BASE } # $rbp-0x28, 原始數組對象 (const GcSlotDesc) $16 = { Slot = { RegisterNumber = 4294967256 Stack = (SpOffset = -40, Base = GC_FRAMEREG_REL) } Flags = GC_SLOT_BASE } # $rbp-0x10, args 參數 (const GcSlotDesc) $17 = { Slot = { RegisterNumber = 4294967280 Stack = (SpOffset = -16, Base = GC_FRAMEREG_REL) } Flags = GC_SLOT_BASE }
標誌 GC_SLOT_BASE
表明是普通的引用類型變量,指向對象的開始地址。
接下來咱們看看 GC 掃描 Span 對象時會作什麼處理,儘管在上述例子中棧上保留了原始數組的地址,使用 Release 模式編譯時可能會出現不保留的狀況,所以 .NET Core 的運行時支持根據對象中間的地址找到對象的開始地址 (在前幾年已經實現了),從新運行程序並使用如下命令能夠給標記對象存活的函數下斷點:
(lldb) b GCHeap::Promote Breakpoint 10: 2 locations.
繼續執行到達斷點之後咱們能夠從 ppObject
獲得標記對象地址的地址,這裏的對象地址是第二個 span 對象中保存的開始地址,同時 flags 爲 1 即 GC_CALL_INTERIOR
表明地址爲對象中間的地址:
(lldb) b GCHeap::Promote Breakpoint 2: 2 locations. (lldb) c Process 6636 resuming Process 6636 stopped * thread #1, name = 'dotnet', stop reason = breakpoint 2.1 frame #0: 0x00007ffff5cb6dc3 libcoreclr.so`WKS::GCHeap::Promote(ppObject=0x00007fffffffd220, sc=0x00007fffffffc9b0, flags=1) at gc.cpp:36669 36666 { 36667 THREAD_NUMBER_FROM_CONTEXT; 36668 #ifndef MULTIPLE_HEAPS -> 36669 const int thread = 0; 36670 #endif //!MULTIPLE_HEAPS 36671 36672 uint8_t* o = (uint8_t*)*ppObject; (lldb) p/x *((long*)0x00007fffffffd220) (long) $0 = 0x00007fff5400ed85
由於地址在對象中間,.NET Core 運行時須要先找到對象的開始地址才能標記對象存活 (標記存活的位是類型信息的最低位),處理的代碼以下 (文件):
#ifdef INTERIOR_POINTERS if (flags & GC_CALL_INTERIOR) { if ((o < hp->gc_low) || (o >= hp->gc_high)) { return; } if ( (o = hp->find_object (o, hp->gc_low)) == 0) { return; } } #endif //INTERIOR_POINTERS
這裏會先判斷地址是否在託管堆中 (若是是 stackalloc 生成的就不在),而後使用 gc_heap::find_object
來找到對象的開始地址,find_object
會先找到中間地址在 Brick 表對應的 Brick,而後找到該 Brick 對應範圍中的第一個託管對象,而後一個個掃描託管對象判斷地址屬於哪一個託管對象,若是找到屬於的託管對象則使用該對象的開始地址,這是一個比較昂貴的操做。關於 Brick 表能夠參考我以前寫的文章。
接下來咱們看看 GC 是怎麼重定位 Span 對象的,先退出 LLDB 而後執行如下命令設置環境變量,這個環境變量能夠強制每次 GC 的時候都啓用壓縮:
export COMPlus_gcForceCompact=1
而後再執行 LLDB,給 GCHeap::Relocate
下斷點並執行到斷點:
(lldb) b GCHeap::Relocate Breakpoint 2: 2 locations. (lldb) c Process 6676 resuming Process 6676 stopped * thread #1, name = 'dotnet', stop reason = breakpoint 2.2 frame #0: 0x00007ffff5cb4633 libcoreclr.so`WKS::GCHeap::Relocate(ppObject=0x00007fffffffd220, sc=0x00007fffffffb810, flags=1) at gc.cpp:36741 36738 { 36739 UNREFERENCED_PARAMETER(sc); 36740 -> 36741 uint8_t* object = (uint8_t*)(Object*)(*ppObject); 36742 36743 THREAD_NUMBER_FROM_CONTEXT; 36744 (lldb) p/x *((long*)0x00007fffffffd220) (long) $0 = 0x00007fff5400ed85
一樣的,ppObject
是標記對象地址的地址,flags 爲 1 即 GC_CALL_INTERIOR
。具體處理代碼以下:
if ((flags & GC_CALL_INTERIOR) && gc_heap::settings.loh_compaction) { if (!((object >= hp->gc_low) && (object < hp->gc_high))) { return; } if (gc_heap::loh_object_p (object)) { pheader = hp->find_object (object, 0); if (pheader == 0) { return; } ptrdiff_t ref_offset = object - pheader; hp->relocate_address(&pheader THREAD_NUMBER_ARG); *ppObject = (Object*)(pheader + ref_offset); return; } } { pheader = object; hp->relocate_address(&pheader THREAD_NUMBER_ARG); *ppObject = (Object*)pheader; }
由於壓縮階段已經把對象內容移動了,重定位階段只須要修改地址到移動後的地址,無論地址是在對象開頭仍是在對象中間,
對於小對象並不須要檢查標記是否帶有 GC_CALL_INTERIOR
,直接找到對應的 Plug (relocate_address
會再次判斷地址是否在託管堆中),
獲取 Plug 中保存的偏移值,而後讓地址減去該偏移值便可。而大對象則須要使用 find_object
來先定位對象的開始地址,以提高處理效率。
至此咱們能夠發現,由於 .NET 能夠只根據 Span 找到原始對象並實現標記與重定位,因此 Span 原理上是能夠保存在堆上的,但這須要犧牲必定性能支持線程安全與放棄 stackalloc (或者分離到另外一個類型),因此微軟沒有選擇這麼作。
在這裏打個小廣告,我與檸檬🍋編寫的書籍《.NET Core 底層入門》在一月份出版了,出版社是北京航空航天大學出版社,你能夠查看如下網站,找到內容介紹與購買連接:
https://netcoreimpl.github.io
或者直接訪問京東的購買連接
https://item.jd.com/12796746.html
最後傳播一下正能量,最近這段時間你們都不容易,我目前也沒有收入來源,但咱們仍然須要擺正心態,相信祖國,支持政府一同抗擊疫情。 中國加油🇨🇳!武漢加油🇨🇳! 國有戰,召必回,戰必勝🇨🇳!