主頁 > .NET開發 > 編譯除錯 .NET Core 5.0 Preview 并分析 Span 的實作原理

編譯除錯 .NET Core 5.0 Preview 并分析 Span 的實作原理

2020-09-17 10:26:39 .NET開發

很久沒有寫過 .NET Core 相關的文章了,目前關店在家休息所以有些時間寫一篇新的??,這次的文章主要介紹如何在 Linux 上編譯除錯最新的 .NET Core 5.0 Preview 與簡單分析 Span 的實作原理,微軟從 .NET Core 5.0 開始把 GIT 倉庫 coreclr 與 corefx 合并移動到了 runtime 倉庫,原有倉庫僅用于維護 .NET Core 3.x,你可以從以下地址查看最新的源代碼:

https://github.com/dotnet/runtime

為了方便重現,接下來的編譯除錯會使用 docker 與 ubuntu 18.04 鏡像(盡管微軟提供了編譯專用的鏡像但并不適合除錯分析),步驟會與之前的博客介紹的 1.1,書籍介紹的 2.1 有一些不同,

如果你覺得閱讀這篇文章有困難,可以參考我之前發布的 .NET Core 源代碼分析系列或者書籍《.NET Core 底層入門》,書籍的購買鏈接在文章最后,

編譯 .NET Core 5.0 Preview

本文編譯的版本是 0d607a757372e3ecc8e942141d7f586a98694e42

創建 docker 容器

執行以下命令即可創建一個 ubuntu 18.04 的 docker 容器,注意創建時需要使用 --privileged 引數,否則無法使用 lldb 或者 gdb 除錯程式,

docker run -it --privileged ubuntu:18.04

安裝 cmake

.NET Core 5.0 要求的 cmake 版本非常高,我們需要添加第三方源來安裝新版本的 cmake:

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 相同:

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 源代碼并編譯

這個步驟也與之前的 .NET Core 相同,但因為 corefx 合并到了同一個倉庫中,執行以下步驟以后會同時編譯 corefx 的 dll 檔案,注意這個步驟編譯的是 Debug 版本的運行時,方便后面的除錯,

git clone https://github.com/dotnet/runtime
cd runtime
./build.sh

編譯完成后你可以在 artifacts 檔案夾下找到編譯結果,

使用 .NET Core 5.0 Preview 執行 Hello World 程式

接下來我們會看如何使用自己編譯的 .NET Core 執行一個 Hello World 程式,.NET Core 5.0 會同時編譯出 dotnet 程式,我們可以使用它代替 corerun 來簡化運行步驟(不需要像以前的版本一樣手動復制 corefx 的 dll或者設定 CORE_ROOT 環境變數),但因為 runtime 倉庫中不包括 sdk(sdk 在 sdk 倉庫中,這次懶得編譯),我們仍然需要另外安裝一個官方的 .NET Core 用于創建與編譯 Hello World 程式,

安裝官方的 .NET Core 3.1 SDK

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

創建與編譯 Hello World 程式

mkdir /console
cd /console
dotnet new console
dotnet build

執行 Hello World 程式

因為使用了 .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 輸出就代表執行成功了,

除錯 .NET Core 5.0 Preview

在 linux 上除錯 .NET Core 一般使用 lldb (gdb 也可以但是沒有 SOS 插件支持),SOS 插件的源代碼被搬到了 diagnostics 倉庫,所以我們還需要下載編譯這個倉庫的源代碼,

下載編譯 diagnostics 倉庫 (LLDB SOS 插件)

安裝 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 檔案,

使用 LLDB 除錯 .NET Core

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

Span 與 Memory 是微軟推出的,用于表示某段子內容的資料型別,它們的主要目的是為了減少記憶體分配與復制,例如取 "abcdefg" 的子字串 "def",傳統的方法 (Substring) 會分配一個長度為 3 的新字串然后復制 "def" 過去,但 Span 與 Memory 可以直接使用原有的物件、子內容的開始位置與子內容的長度來表示一段子內容,在其他語言中也有類似 Span 與 Memory 的概念,例如 go 中的 slice,c 中指標與長度的結合 (例如 struct char_view { char* ptr, size_t size; }),與 c++ 中的 string_viewspan 型別,

Span 與 Memory 的區別在于,Memory 是一個普通的型別,只保存 原有的物件子內容的開始地址子內容的長度,在記憶體中的表現可以參考下圖:

Memory 與很早就存在的 ArraySegment 實質上是一樣的,只是支持更多的型別,它們都不需要運行時或者編譯器的額外支持,

Span 則特殊很多,它保存了子內容的開始地址與長度(不保存原始物件的地址),使得它不需要計算開始地址并且允許指向托管物件以外的內容 (例如從 stackalloc 分配),Span 在記憶體中的表現可以參考下圖:

Span 是一個 ref struct 型別 (這個型別可以說是專門為 Span 發明的),ref struct 只能保存在于堆疊上或者作為其他 ref struct 的成員 (最終來說只能保存在于堆疊上),Span 只能存在于堆疊上主要有以下原因:

  • GC 處理 Span 物件的成本很高,所以不應該大范圍使用
  • Span 的讀寫是非原子的(兩個指標大小),如果允許在堆上就有可能被多個執行緒同時訪問
  • Span 可以由 stackalloc 生成,而 Span 自身并不會標記來源是托管物件還是堆疊空間

因為 Span 需要運行時的額外支持,在 .NET Framework 與 Mono 上使用的 Span (從 Nuget 包安裝的) 實際上與 Memory 一樣,只有在 .Net Core 上才有以上的特性,

此外,因為部分物件的內容不可修改 (例如 string),所以還有配套的 ReadOnlySpanReadOnlyMemory,它們除了在編譯器層面上限制修改以外,與原型別沒有什么區別,

除錯分析 Span 的實作原理

接下來我們可以除錯一個示例程式,簡單分析 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 查看生成的匯編代碼

編譯示例程式與執行 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 資訊中的各個 Slot

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=https://www.cnblogs.com/zkweb/p/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=https://www.cnblogs.com/zkweb/p/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

繼續執行 cp *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 物件時的處理

接下來我們看看 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 物件時的處理

接下來我們看看 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 (或者分離到另一個型別),所以微軟沒有選擇這么做,

參考鏈接

  • https://github.com/dotnet/runtime
  • https://github.com/dotnet/runtime/blob/master/docs/workflow/building/coreclr/linux-instructions.md
  • https://github.com/dotnet/runtime/blob/0d607a757372e3ecc8e942141d7f586a98694e42/src/libraries/System.Private.CoreLib/src/System/Span.cs
  • https://github.com/dotnet/runtime/blob/0d607a757372e3ecc8e942141d7f586a98694e42/src/libraries/System.Private.CoreLib/src/System/ReadOnlySpan.cs
  • https://github.com/dotnet/runtime/blob/0d607a757372e3ecc8e942141d7f586a98694e42/src/libraries/System.Private.CoreLib/src/System/Memory.cs
  • https://github.com/dotnet/runtime/blob/0d607a757372e3ecc8e942141d7f586a98694e42/src/libraries/System.Private.CoreLib/src/System/ReadOnlyMemory.cs
  • https://raw.githubusercontent.com/dotnet/runtime/0d607a757372e3ecc8e942141d7f586a98694e42/src/coreclr/src/gc/gc.cpp
  • https://github.com/dotnet/runtime/blob/0d607a757372e3ecc8e942141d7f586a98694e42/src/coreclr/src/vm/gcinfodecoder.cpp
  • https://github.com/dotnet/runtime/blob/0d607a757372e3ecc8e942141d7f586a98694e42/src/coreclr/src/inc/gcinfodecoder.h
  • https://docs.microsoft.com/en-us/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay
  • https://www.cnblogs.com/zkweb/p/6625049.html

寫在最后

在這里打個小廣告,我與檸檬??撰寫的書籍《.NET Core 底層入門》在一月份出版了,出版社是北京航空航天大學出版社,你可以查看以下網站,找到內容介紹與購買鏈接:

https://netcoreimpl.github.io

或者直接訪問京東的購買鏈接

https://item.jd.com/12796746.html

最后傳播一下正能量,最近這段時間大家都不容易,我目前也沒有收入來源,但我們仍然需要擺正心態,相信祖國,支持政府一同抗擊疫情,
中國加油????!武漢加油????! 國有戰,召必回,戰必勝????!

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/64900.html

標籤:.NET Core

上一篇:C#命名空間和作用域的一些問題請教

下一篇:Expression運算式樹的問題,標題不好描述,內有代碼截圖

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more