编译调试 .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_view 和 span 类型。

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),所以还有配套的 ReadOnlySpan 与 ReadOnlyMemory,它们除了在编译器层面上限制修改以外,与原类型没有什么区别。

调试分析 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.1frame #0: 0x00007fff7bb352d0
->  0x7fff7bb352d0: pushq  %rbp0x7fff7bb352d1: pushq  %r130x7fff7bb352d3: subq   $0x48, %rsp0x7fff7bb352d7: 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.1frame #0: 0x00007fff7bb35305
->  0x7fff7bb35305: callq  0x7ffff5939210            ; JIT_NewArr1VC_MP_FastPortable at jithelpers.cpp:25600x7fff7bb3530a: movq   %rax, -0x28(%rbp)0x7fff7bb3530e: movabsq $0x7fff7be09428, %rdi     ; imm = 0x7FFF7BE094280x7fff7bb35318: 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.1frame #0: 0x00007fff7bb3532e
->  0x7fff7bb3532e: movq   -0x28(%rbp), %rdi0x7fff7bb35332: callq  0x7fff7bb340300x7fff7bb35337: 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.1frame #0: 0x00007fff7bb3536e
->  0x7fff7bb3536e: callq  0x7fff7bb306b80x7fff7bb35373: leaq   -0x20(%rbp), %rdi0x7fff7bb35377: callq  0x7fff7bb33ff80x7fff7bb3537c: 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:19675frame #1: 0x00007ffff5cb6fe8 libcoreclr.so`WKS::GCHeap::Promote(ppObject=0x00007fffffffd230, sc=0x00007fffffffc9c0, flags=1) at gc.cpp:36730frame #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:4874frame #3: 0x00007ffff5918c4a libcoreclr.so`GcEnumObject(pData=0x00007fffffffc710, pObj=0x00007fffffffd230, flags=1) at gcenv.ee.common.cpp:167frame #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:1848frame #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:679frame #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:1034frame #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:983frame #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:5150frame #9: 0x00007ffff5919462 libcoreclr.so`GcStackCrawlCallBack(pCF=0x00007fffffffb1c0, pData=0x00007fffffffc710) at gcenv.ee.common.cpp:283frame #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:886frame #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:966frame #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:1049frame #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:146frame #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:182frame #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:155frame #16: 0x00007ffff5c9f701 libcoreclr.so`WKS::gc_heap::mark_phase(condemned_gen_number=2, mark_only_p=NO) at gc.cpp:21062frame #17: 0x00007ffff5c9b479 libcoreclr.so`WKS::gc_heap::gc1() at gc.cpp:16713frame #18: 0x00007ffff5cab832 libcoreclr.so`WKS::gc_heap::garbage_collect(n=2) at gc.cpp:18345frame #19: 0x00007ffff5c90dea libcoreclr.so`WKS::GCHeap::GarbageCollectGeneration(this=0x0000555555793aa0, gen=2, reason=reason_induced) at gc.cpp:38188frame #20: 0x00007ffff5cdd3bb libcoreclr.so`WKS::GCHeap::GarbageCollectTry(this=0x0000555555793aa0, generation=2, low_memory_p=NO, mode=2) at gc.cpp:37524frame #21: 0x00007ffff5cde614 libcoreclr.so`WKS::GCHeap::GarbageCollect(this=0x0000555555793aa0, generation=2, low_memory_p=false, mode=2) at gc.cpp:37458frame #22: 0x00007ffff58be151 libcoreclr.so`GCInterface::Collect(generation=-1, mode=2) at comutilnative.cpp:986frame #23: 0x00007fff7bb55853frame #24: 0x00007fff7bb55788frame #25: 0x00007fff7bb553c3frame #26: 0x00007ffff5a965f3 libcoreclr.so`CallDescrWorkerInternal at unixasmmacrosamd64.inc:862frame #27: 0x00007ffff589cc9c libcoreclr.so`CallDescrWorkerWithHandler(pCallDescrData=0x00007fffffffd5a8, fCriticalCall=NO) at callhelpers.cpp:70frame #28: 0x00007ffff589da1c libcoreclr.so`MethodDescCallSite::CallTargetWorker(this=0x00007fffffffd6e0, pArguments=0x00007fffffffd680, pReturnValue=0x0000000000000000, cbReturnValue=0) at callhelpers.cpp:546frame #29: 0x00007ffff56ee983 libcoreclr.so`MethodDescCallSite::Call(this=0x00007fffffffd6e0, pArguments=0x00007fffffffd680) at callhelpers.h:459frame #30: 0x00007ffff5ac1c64 libcoreclr.so`RunMainInternal(pParam=0x00007fffffffd950) at assembly.cpp:1487frame #31: 0x00007ffff5ac1989 libcoreclr.so`RunMain(this=0x00007fffffffd858, pParam=0x00007fffffffd950)::$_1::operator()(Param*) const::'lambda'(Param*)::operator()(Param*) const at assembly.cpp:1559frame #32: 0x00007ffff5abf1f9 libcoreclr.so`RunMain(this=0x00007fffffffd940, __EXparam=0x00007fffffffd950)::$_1::operator()(Param*) const at assembly.cpp:1561frame #33: 0x00007ffff5abf019 libcoreclr.so`RunMain(pFD=0x00007fff7bd5c368, numSkipArgs=1, piRetVal=0x00007fffffffda4c, stringArgs=0x00007fffffffdf20) at assembly.cpp:1561frame #34: 0x00007ffff5abf4a2 libcoreclr.so`Assembly::ExecuteMainMethod(this=0x00005555557d4d70, stringArgs=0x00007fffffffdf20, waitForOtherThreads=YES) at assembly.cpp:1671frame #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:460frame #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:407frame #37: 0x00007ffff67dfd8a libhostpolicy.so`___lldb_unnamed_symbol100$$libhostpolicy.so + 810frame #38: 0x00007ffff67e022d libhostpolicy.so`___lldb_unnamed_symbol101$$libhostpolicy.so + 45frame #39: 0x00007ffff67e095b libhostpolicy.so`corehost_main + 203frame #40: 0x00007ffff6a4b73c libhostfxr.so`___lldb_unnamed_symbol204$$libhostfxr.so + 1740frame #41: 0x00007ffff6a49ea1 libhostfxr.so`___lldb_unnamed_symbol202$$libhostfxr.so + 641frame #42: 0x00007ffff6a444f3 libhostfxr.so`hostfxr_main_startupinfo + 147frame #43: 0x00005555555623b7 dotnet`___lldb_unnamed_symbol114$$dotnet + 791frame #44: 0x0000555555562b90 dotnet`___lldb_unnamed_symbol115$$dotnet + 128frame #45: 0x00007ffff6ca3b97 libc.so.6`__libc_start_main + 231frame #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.1frame #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:679676              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 = 4294967216Stack = (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 = 4294967232Stack = (SpOffset = -64, Base = GC_FRAMEREG_REL)}Flags = GC_SLOT_INTERIOR
}
# $rbp-0x20, 即本地变量 span
(const GcSlotDesc) $14 = {Slot = {RegisterNumber = 4294967264Stack = (SpOffset = -32, Base = GC_FRAMEREG_REL)}Flags = GC_SLOT_INTERIOR
}
# $rbp-0x30, 用于初始化数组的句柄
(const GcSlotDesc) $15 = {Slot = {RegisterNumber = 4294967248Stack = (SpOffset = -48, Base = GC_FRAMEREG_REL)}Flags = GC_SLOT_BASE
}
# $rbp-0x28, 原始数组对象
(const GcSlotDesc) $16 = {Slot = {RegisterNumber = 4294967256Stack = (SpOffset = -40, Base = GC_FRAMEREG_REL)}Flags = GC_SLOT_BASE
}
# $rbp-0x10, args 参数
(const GcSlotDesc) $17 = {Slot = {RegisterNumber = 4294967280Stack = (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.1frame #0: 0x00007ffff5cb6dc3 libcoreclr.so`WKS::GCHeap::Promote(ppObject=0x00007fffffffd220, sc=0x00007fffffffc9b0, flags=1) at gc.cpp:3666936666    {36667        THREAD_NUMBER_FROM_CONTEXT;36668    #ifndef MULTIPLE_HEAPS
-> 36669        const int thread = 0;36670    #endif //!MULTIPLE_HEAPS3667136672        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.2frame #0: 0x00007ffff5cb4633 libcoreclr.so`WKS::GCHeap::Relocate(ppObject=0x00007fffffffd220, sc=0x00007fffffffb810, flags=1) at gc.cpp:3674136738    {36739        UNREFERENCED_PARAMETER(sc);36740
-> 36741        uint8_t* object = (uint8_t*)(Object*)(*ppObject);3674236743        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

最后传播一下正能量,最近这段时间大家都不容易,我目前也没有收入来源,但我们仍然需要摆正心态,相信祖国,支持政府一同抗击疫情。
中国加油????????!武汉加油????????!国有战,召必回,战必胜????????!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/311976.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构----归并排序

数据结构----归并排序 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> #define N 100 void guiBingSort(int a[], int l, int h,int length) {//归并排序int mid (l h) / 2;int* b (int *)malloc(N*sizeof(int));i…

利用obfuscar对.NET应用进行混淆

背景发布客户端程序产品时&#xff0c;免不了会遇到一些怀有恶意或有强烈学习欲望的用户尝试对程序进行反编译。对于一些编译成本地指令的程序&#xff08;如C、C&#xff09;&#xff0c;编译后可读性低&#xff0c;反编译和破解成本较高&#xff0c;不需要对代码进行太高强度…

数据结构---基数排序

数据结构—基数排序 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> int getNumberBit(int number) {//获取数字的位数int x number,count0;if (x 0)return 1;while (x ! 0) {count;x / 10;}return count; } int g…

C# 版本 疫情传播仿真程序

前言前一阵子看到有人制作了《疫情传播仿真程序》&#xff0c;是用 Java做的。里面根据多种实际情况&#xff0c;如居民移动意愿、医护能力、病毒传播能力&#xff0c;来模拟疫情的发展。看完之后&#xff0c;我暗暗称奇&#xff0c;特别是结合一些视频和照片&#xff0c;确实做…

jmeter 加密解密_使用Jmeter对SHA1加密接口进行性能测试

机会只留给那些有准备的人改变能改变的&#xff0c;接受不能改变的&#xff0c;就是进步性能测试过程中&#xff0c;有时候会遇到需要对信息头进行加密鉴权&#xff0c;下面我就来介绍如何针对SHA1加密鉴权开发性能测试脚本 1、首先了解原理&#xff0c;就是需要对如下三个参数…

word List 06

word List 06 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;

面对疫情,在家办公的程序员如何突围

作者&#xff1a;陌北有棵树&#xff0c;架构师社区合伙人很多程序员朋友都已经开始了在家办公的生活&#xff0c;第一天办公&#xff0c;你的远程工具还流畅吗&#xff0c;视频会议换了几个软件&#xff1f;当然这些都是外在因素&#xff0c;尤其对于程序员来说&#xff0c;解…

[蓝桥杯2018初赛]全球变暖-dfs,bfs,连通块

解题思路: bfs:遍历所有未遍历过的陆地&#xff0c;通过bfs计算出当前位置连通陆地的数量cnt&#xff0c;以及被淹没陆地的数量bound,若cnt bound表示完整淹没的一个岛屿 dfs:将连通块全部标记&#xff0c;如果这个连通块全部都会淹没&#xff0c;则答案1&#xff0c;如果这个…

latex 参考文献显示问号_回「LaTeX 的罪与罚」

原文链接&#xff1a;LaTeX 的罪与罚 - 朴素的贝叶斯的文章 - 知乎作为 LaTeX 开发者&#xff0c;看到这种嘲讽自然是非常 angry 的。本来并不想趟这个混水&#xff0c;然而眼见着赞数一天天涨上去&#xff0c;还居然进了精华区&#xff0c;实在忍不住只好注册了贵乎来说几句。…

疫情之下,使用FRP实现内网穿透,远程连接公司电脑进行办公

当前情况下&#xff0c;经常会有需要到公司电脑进行一些操作&#xff0c;比如连接内网OA&#xff0c;数据库或者提交文档。为了减少外出&#xff0c;将使用frp进行内网穿透的方法进行一个说明。前提条件1. 一台拥有公网 IP 的设备(如果没有&#xff0c;服务器可以使用https://d…

ad中电容用什么封装_二极管在电路中到底做什么用的

所有的电子电路中基本上都会用到二极管&#xff0c;它的特性也是非常之多&#xff0c;最主要就是单方向导电性&#xff0c;(单向导电性的两根引脚之间的电阻分为正向电阻和反向电阻两种)。人们利用这些不同特性构成各种具体的应用电路&#xff0c;分析不同电路中的二极管工作原…

数据结构---邻接矩阵的DFS

数据结构—邻接矩阵的DFS 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> #define N 100 #define elemType int //const int MAX_INT (1 << 31) - 1; //const int MAX_INT 0X7fffffff; #define INF (((uns…

.NET Core 如何判断程序是否在远程桌面(RDP)下运行

点击上方蓝字关注“汪宇杰博客”导语由于疫情的关系&#xff0c;很久没发文章了。今天终于稳定下来在家办公&#xff0c;抽空分享一个刚学会的技巧。最近在家办公的程序员可能避免不了要用远程桌面&#xff0c;那么问题来了&#xff0c;你的 .NET Core 程序有没有办法知道自己是…

[蓝桥杯2018初赛]方格计数-巧妙枚举,找规,数论

解题思路&#xff1a; 枚举第一象限的所有点&#xff0c;判断是否在圆内&#xff0c;最后结果*4 我们用下面的程序&#xff0c;来算一个半径为2的圆&#xff0c;其实我们第一象限算的就是那个绿点&#xff0c;然后类比到半径5000. 代码如下&#xff1a; #include <iostre…

ipa在线安装搭建_三种越狱工具安装方法

从 iOS 9.2 开始&#xff0c;苹果越狱进入了半越狱时代&#xff0c;也就是重启手机之后需要重新进入越狱工具激活越狱环境&#xff0c;以下是三种常用的越狱工具安装方法&#xff1a;方法一&#xff1a;自签名下载大胡子签名工具&#xff1a;Cydia Impactor下载地址&#xff1a…

《ASP.NET Core 微服务实战》-- 读书笔记(第7章)

第 7 章 开发 ASP.NET Core Web 应用ASP.NET Core 基础在本章&#xff0c;我们将从一个命令行应用开始&#xff0c;并且在不借助任何模板&#xff0c;脚手架和向导的情况下&#xff0c;最终得到一个功能完整的 Web 应用GitHub链接&#xff1a;https://github.com/microservices…

git为私有仓库设置密码_dnf仓库密码设置不跳出 dnf仓库密码设置流程

部分玩家想要设置游戏中仓库密码但是自己不小心点击了不再提醒后不再弹出了&#xff0c;那么怎么办呢&#xff0c;其实不用着急&#xff0c;下面带大家了解一下如何设置设置仓库锁密码&#xff0c;感兴趣的玩家可以玩下看看哦。dnf仓库密码设置不跳出在游戏中玩家每天第一次登陆…

word List 07

word List 07 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;

TIOBE 2月编程语言排行榜新鲜出炉!C# 获3.08%增长率!

编程C# 刚经历了一次TIOBE考试&#xff0c;2020年2月&#xff0c;它拿到的最新分数是第5名。老牌编程语言Java独占鳌头&#xff0c;C语言紧随其后。诚然&#xff0c;工程师们对C# 的青睐&#xff0c;还没能影响到大多数企业&#xff0c;但增长率3.08%的势头&#xff0c;可凭此预…

自动生成sqlserver增删改成_如何批量生成证书证件-可变条码-可变图片-可变数据-快速教程...

如何生成可变图像&#xff1f;如何生成可变文本&#xff1f;如何生成可变条码&#xff1f;如何使打印作业自动化&#xff1f;从多页设计中选取页使用几乎所有数据库程序中的数据库信息通过“拖放”创建个性化打印分享快速教程。1&#xff1a;批量自动化化印刷详细步骤如下&…