编译调试 .NET Core 5.0 Preview 并分析 Span 的实现原理

很久没有写过 .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=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

继续执行 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.cnblogs.com/zkweb/p/12273151.html

时间: 2024-08-09 18:46:29

编译调试 .NET Core 5.0 Preview 并分析 Span 的实现原理的相关文章

用VS Code体验调试.NET Core 2.0 Preview (传统三层架构)

准备工作 VS Code下载地址:https://vscode.cdn.azure.cn/stable/379d2efb5539b09112c793d3d9a413017d736f89/VSCodeSetup-ia32-1.13.1.exe .NET Core 2.0 Preview下载地址:https://download.microsoft.com/download/6/1/B/61B3E81F-5509-48D2-BB4F-5189E23CD29A/dotnet-sdk-2.0.0-pre

.NET Core 3.0 Preview 6中对ASP.NET Core和Blazor的更新

原文:.NET Core 3.0 Preview 6中对ASP.NET Core和Blazor的更新 我们都知道在6月12日的时候微软发布了.NET Core 3.0的第6个预览版.针对.NET Core 3.0的发布我们国内的微软MVP-汪宇杰还发布的官翻版的博文进行了详细的介绍.具体的可以关注"汪宇杰博客"公众号,或者我的"DotNetCore实战"公众号然后在历史文章里面进行查阅.而我们这篇文章将会介绍本次更新中对ASP.NET Core和Blazor所做的更

使用VS Code从零开始开发调试.NET Core 1.0

使用VS Code 从零开始开发调试.NET Core 1.0. .NET Core 是一个开源的.跨平台的 .NET 实现. VS Code 全称是 Visual Studio Code,Visual Studio Code是一个轻量级的跨平台Web集成开发环境,可以运行在 Linux,Mac 和Windows下! 从零开始开发调试.NET Core 1.0,让你更好的了解.NET Core 应用程序. 本篇VS Code 开发教程是在windows做实际操作,但同样适用于其它系统.VS Co

VS Code开发调试.NET Core 2.0

VS Code开发调试.NET Core 2.0 使用VS Code 从零开始开发调试.NET Core 2.0.无需安装VS 2017 15.3+即可开发调试.NET Core 2.0应用. VS Code 全称是 Visual Studio Code,Visual Studio Code是一个轻量级的跨平台Web集成开发环境,可以运行在 Linux,Mac 和Windows下! 本篇为VS Code 开发调试.NET Core 2.0教程,在Windows下做实际操作,但同样适用于其它系统.

asp.net core 1.1 项目升级至 asp.net core 2.0 preview 2

这两天把一个 asp.net core 1.1 的项目迁移到了 asp.net core 2.0 preview 2 ,在这篇随笔中记录一下. 如果项目在有 global.json 文件,需要删除或修改为 .net 2.0 preview 2 的 sdk 版本号. 对于类库项目的 .csproj,需要把 TagetFramework 改为 netstandard2.0 ,比如 <PropertyGroup> <TargetFramework>netstandard1.6</T

.NET Core 3.0 Preview 5 亮点之一:发布单文件可执行程序

在阅读 Announcing .NET Core 3.0 Preview 5 时发现了 .NET Core 3.0 Preview 5 的一个新特性 —— Publishing Single EXEs ,可以通过 dotnet publish 命令将整个 .net core 应用发布为一个可执行文件.一个期待很久的特性,终于随着 .NET Core 3.0 Preview 5 来了,赶紧体验一下. 用 dotnet new mvc --no-https 创建一个 asp.net core 应用,

【译】宣告推出.NET Core 3.0 Preview 7(英雄的黎明)

今天,我们宣布推出.NET Core 3.0 Preview 7.我们已经从创建新特性阶段过渡到了完善版本阶段.对于接下来的预览版,我们将把重点放在质量(改进)上. 在Windows,macOS和Linux上下载.NET Core 3.0 Preview 7. .NET Core 3.0发行说明 API差异 GitHub发布 报告问题 ASP.NET Core和EF Core今天也在发布更新. Visual Studio用户需要Visual Studio 2019 16.3 Preview 1(

visual studio code调试.net core 2.0程序

最近在试.net 2.0 Preview时,在VS2017中相对麻烦,现说也怕把生产环境搞坏,于是就想在vs code下试试. 首在安装.net core 2.0的SDK和Runtime,这里下载 https://github.com/dotnet/core/blob/master/release-notes/download-archives/2.0.0-preview1-download.md 再安装vs code(我安装的是中文版),这里下载 https://code.visualstud

从ASP.NET Core 3.0 preview 特性,了解CLR的Garbage Collection

前言 在阅读这篇文章:Announcing Net Core 3 Preview3的时候,我看到了这样一个特性: Docker and cgroup memory Limits We concluded that the primary fix is to set a GC heap maximum significantly lower than the overall memory limit as a default behavior. In retrospect, this choice