前言

好久没有动用LLDB了,这种未来的下一代高性能调试器应该是用在Linux内核系统的Arm64/Riscv64/X64系统指令集上的,LLDB Debug .NET有点杀鸡用牛刀。本篇通过它来看下FOH也就是.NET8里面优化字符串,为了提高其性能增加的FOH堆分配过程。关于FOH可以参考:.NET8极致性能优化Non-GC Heap

详细

来看一个简单的例子:

public static string GetPrefix() => "https://";
static void Main(string[] args)
{
GetPrefix();
GC.Collect();
Console.ReadLine();
}

函数GetPrefix里面的字符串“https://”就是被分配到FOH堆里面的,如何验证呢?

首先通过LLDB把CLR运行到托管Main入口

(lldb) b RunMainInternal
Breakpoint 7: where = libcoreclr.so`RunMainInternal(Param*) at assembly.cpp:1257, address = 0x00007ffff6d43930
(lldb) r
Process 2697 launched: '/home/tang/opt/dotnet/debug_clr/clrrun' (x86_64)
Process 2697 stopped
* thread #1, name = 'clrrun', stop reason = breakpoint 6.1 7.1
frame #0: 0x00007ffff6d43930 libcoreclr.so`RunMainInternal(pParam=0x00007ffff7faaab6) at assembly.cpp:1257
1254 } param;
1255
1256 static void RunMainInternal(Param* pParam)
-> 1257 {
1258 MethodDescCallSite threadStart(pParam->pFD);
1259
1260 PTRARRAYREF StrArgArray = NULL;
(lldb)

然后把其运行到JIT前置入口

(lldb) b PreStubWorker
Breakpoint 8: where = libcoreclr.so`::PreStubWorker(TransitionBlock *, MethodDesc *) at prestub.cpp:1865, address = 0x00007ffff6ee6c10
(lldb) c
Process 2697 resuming
Process 2697 stopped
* thread #1, name = 'clrrun', stop reason = breakpoint 8.1
frame #0: 0x00007ffff6ee6c10 libcoreclr.so`::PreStubWorker(pTransitionBlock=0x00000000ffffcb38, pMD=0x0000000155608c70) at prestub.cpp:1865
1862 // returns a pointer to the new code for the prestub's convenience.
1863 //=============================================================================
1864 extern "C" PCODE STDCALL PreStubWorker(TransitionBlock* pTransitionBlock, MethodDesc* pMD)
-> 1865 {
1866 PCODE pbRetVal = NULL;
1867
1868 BEGIN_PRESERVE_LAST_ERROR;

此时可以看下当前JIT编译的函数是谁,这里需要先n命令单步一下

(lldb) n
Process 2697 stopped
* thread #1, name = 'clrrun', stop reason = step over
frame #0: 0x00007ffff6ee6c36 libcoreclr.so`::PreStubWorker(pTransitionBlock=0x00007fffffffc648, pMD=0x00007fff78f56b70) at prestub.cpp:1866:11
1863 //=============================================================================
1864 extern "C" PCODE STDCALL PreStubWorker(TransitionBlock* pTransitionBlock, MethodDesc* pMD)
1865 {
-> 1866 PCODE pbRetVal = NULL;
1867
1868 BEGIN_PRESERVE_LAST_ERROR;

然后通过微软提供的sos.dll Dump下当前的函数描述结构体MethodDesc,pMD是传过来的函数参数,也即是MethodDesc的变量

(lldb) sos dumpmd pMD
Method Name: ConsoleApp1.Test+Program.Main(System.String[])
Class: 00007fff78f97530
MethodTable: 00007fff78f56c08
mdToken: 0000000006000008
Module: 00007fff78f542d0
IsJitted: no
Current CodeAddr: ffffffffffffffff
Version History:
ILCodeVersion: 0000000000000000
ReJIT ID: 0
IL Addr: 00007ffff7faa2aa
CodeAddr: 0000000000000000 (MinOptJitted)
NativeCodeVersion: 0000000000000000

可以清晰的看到Method Name就是ConsoleApp1.Test+Program.Main,OK这一步确定了,我们下面继续寻找字符串分配到FOH,首先删掉前面所有的断点

(lldb) br del
About to delete all breakpoints, do you want to do that?: [Y/n] y
All breakpoints removed. (3 breakpoints)

在TryAllocateObject 函数上下断,它是分配托管内存的函数

(lldb) b TryAllocateObject
Breakpoint 9: 2 locations.

运行到此处

(lldb) c
Process 2697 resuming
Process 2697 stopped
* thread #1, name = 'clrrun', stop reason = breakpoint 9.1
frame #0: 0x00007ffff70300c0 libcoreclr.so`FrozenObjectHeapManager::TryAllocateObject(this=0x00007fffffff80b8, type=0x00000008017fa948, objectSize=140737488322759, publish=false) at frozenobjectheap.cpp:22
19 // May return nullptr if object is too large (larger than FOH_COMMIT_SIZE)
20 // in such cases caller is responsible to find a more appropriate heap to allocate it
21 Object* FrozenObjectHeapManager::TryAllocateObject(PTR_MethodTable type, size_t objectSize, bool publish)
-> 22 {
23 CONTRACTL
24 {
25 THROWS;

这个函数就是字符串分配到FOH堆的地方,通过它的函数所在的类名即可看出FrozenObjectHeapManager,但是我们依然还是需要验证下。继续n单步这个函数的返回的地方,也就是如下代码:

 202
-> 203 return object;

此时的这个object变量就是示例里面字符串的“https://”的对象地址,看下它的地址值

(lldb) p/x object
(Object *) $14 = 0x00007fffe6bff8c0

记住这个值:0x00007fffe6bff8c0,后面会把它和GC堆的范围进行一个比较。如果它不在GC堆范围,说明.NET8的字符串确实分配在了FOH堆里面。

我们继续单步向下,运行到这个对象被赋值字符串的地方

STRINGREF AllocateStringObject(EEStringData *pStringData, bool preferFrozenObjHeap, bool* pIsFrozen)
{
//此处省略
memcpyNoGCRefs(strDest, pStringData->GetStringBuffer(), cCount*sizeof(WCHAR)); //此处省略
}

然后看下它的内存:

(lldb) memory re 0x00007fffe6bff8c0
0x7fffe6bff8c0: 68 00 74 00 74 00 70 00 73 00 3a 00 2f 00 2f 00 h.t.t.p.s.:././.
0x7fffe6bff8dc: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

它确实是https字符串的对象地址。没有问题。

避免干扰,此时我们再次删除所有断点

(lldb) br del
About to delete all breakpoints, do you want to do that?: [Y/n] y
All breakpoints removed. (1 breakpoint)

然后在函数is_in_find_object_range处下断,它是在GC回收的时候,判断当前的对象地址是否在GC堆里面,如果是则进行对象标记,如果不是直接返回。可以通过这个获取GC堆的范围,运行到此处

(lldb) b is_in_find_object_range
(lldb) c
Process 2697 resuming
Process 2697 stopped
* thread #1, name = 'clrrun', stop reason = breakpoint 13.8
frame #0: 0x00007ffff72d881d libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) [inlined] WKS::gc_heap::is_in_find_object_range(o=0x0000000000000000) at gc.cpp:7906:11
7903 inline
7904 bool gc_heap::is_in_find_object_range (uint8_t* o)
7905 {
-> 7906 if (o == nullptr)
7907 {
7908 return false;
7909 }

单步n

(lldb) n
Process 2697 stopped
* thread #1, name = 'clrrun', stop reason = step over
frame #0: 0x00007ffff72d8831 libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) [inlined] WKS::gc_heap::is_in_find_object_range(o="@\x9b\xd6x\xff\U0000007f") at gc.cpp:7911:14
7908 return false;
7909 }
7910 #if defined(USE_REGIONS) && defined(FEATURE_CONSERVATIVE_GC)
-> 7911 return ((o >= g_gc_lowest_address) && (o < bookkeeping_covered_committed));
7912 #else //USE_REGIONS && FEATURE_CONSERVATIVE_GC
7913 if ((o >= g_gc_lowest_address) && (o < g_gc_highest_address))
7914 {

注意,此时我们看到了GC堆的一个范围,也就是变量g_gc_lowest_address和变量g_gc_highest_address,看下它们的地址范围

(lldb) p/x g_gc_lowest_address
(uint8_t *) $17 = 0x00007fbf68000000 ""
(lldb) p/x g_gc_highest_address
(uint8_t *) $18 = 0x00007fff68000000 "0"

上面很明显了,GC堆的范围起始地址:0x00007fbf68000000 ,结束地址:0x00007fff68000000 。而字符串“https://”的对象地址是0x00007fffe6bff8c0,很明显它不在GC堆的范围内。

以上通过分配一个字符串到FOH堆,后调用一个GC.Collect()查看GC堆的范围,对FOH对象地址和GC堆范围进行一个判断,为一个非常简单的FOH字符串分配验证。

欢迎加入C#12/.NET8最新技术交流群

结尾

作者:jianghupt

原文:.NET8顶级调试lldb观察FOH堆字符串分配

公众号:jianghupt,文章首发,欢迎关注

.NET8顶级调试lldb观察FOH堆字符串分配的更多相关文章

  1. windows程序员进阶系列:《软件调试》之Win32堆

     win32堆及内部结构 Windows在创建一个新的进程时会为该进程创建第一个堆,被称为进程的默认堆.默认堆的句柄会被保存在进程环境块_PEB的ProcessHeap字段中. 要获得_PEB的地址, ...

  2. windows程序员进阶系列:《软件调试》之Win32堆的调试支持

    Win32堆的调试支持 为了帮助程序员及时发现堆中的问题,堆管理器提供了以下功能来辅助调试. 1:堆尾检查(Heap Tail Check) HTC,在堆尾添加额外的标记信息,用于检测堆块是否溢出. ...

  3. java字符串池和字符串堆内存分配

    1. String str=new String("abc")和String str="abc"的字符串“abc”都是存放在堆中,而不是存在 栈中. 2. 其实 ...

  4. Java中的对象都是在堆上分配的吗?

    作者:LittleMagic https://www.jianshu.com/p/8377e09971b8 为了防止歧义,可以换个说法: Java对象实例和数组元素都是在堆上分配内存的吗? 答:不一定 ...

  5. 如何限制一个类只在堆上分配和栈上分配(StackOnly HeapOnly)

    [本文链接] http://www.cnblogs.com/hellogiser/p/stackonly-heaponly.html [题目] 如何限制一个类只在堆上分配和栈上分配? [代码]  C+ ...

  6. Unix系统编程()在堆上分配内存

    在堆上分配内存:malloc和free 一般情况下,C程序使用malloc函数族在堆上分配和释放内存.较之brk和sbrk,这些函数具备不少优点: 属于C语言标准的一部分 更易于在多线程程序中使用 接 ...

  7. JVM系列(1)- JVM常见参数及堆内存分配

    常见参数配置 基于JDK1.6 -XX:+PrintGC 每次触发GC的时候打印相关日志 -XX:+UseSerialGC 串行回收模式 -XX:+PrintGCDetails 打印更详细的GC日志 ...

  8. new 的对象如何不分配在堆而分配在栈上(方法逃逸等)

    当能够明确对象不会发生逃逸时,就可以对这个对象做一个优化,不将其分配到堆上,而是直接分配到栈上,这样在方法结束时,这个对象就会随着方法的出栈而销毁,这样就可以减少垃圾回收的压力. 如方法逃逸. 逃逸分 ...

  9. Java中对象并不是都在堆上分配内存的

    转(https://blog.51cto.com/13906751/2153924) 前段时间,给星球的球友们专门码了一篇文章<深入分析Java的编译原理>,其中深入的介绍了Java中的j ...

  10. 【性能优化】面试官:Java中的对象都是在堆上分配的吗?

    写在前面 从开始学习Java的时候,我们就接触了这样一种观点:Java中的对象是在堆上创建的,对象的引用是放在栈里的,那这个观点就真的是正确的吗?如果是正确的,那么,面试官为啥会问:"Jav ...

随机推荐

  1. 【opencv】传统目标检测:Haar检测器实现人脸检测

    传统目标分类器主要包括Viola Jones Detector.HOG Detector.DPM Detector,本文主要介绍VJ检测器,在VJ检测器基础上发展出了Haar检测器,Haar检测器也是 ...

  2. [信友队图灵杯中级组-D]基础循环结构练习题

    2023-5-13 题目 题目传送门 难度&重要性(1~10):6.5 题目来源 信友队图灵杯 题目算法 构造 解题思路 我们可以知道,在一开始我们得到的 \(a\) 数组是 \(1,2,3, ...

  3. 修改内置框架css 样式

    <style scoped> 1 <style scoped> 2 .info /deep/ .video{ // info 外层便签 /deep/ 可以理解为连接桥 .vid ...

  4. 我的 Kafka 旅程 - 基于账号密码的 SASL+PLAIN 认证授权 · 配置 · 创建账号 · 用户授权 · .NET接入

    本文基于 Kafka 3.0+ 的 KRaft 模式来阐述 默认的 Kafka 不受认证约束,可不用账号就可以连接到服务,也就是默认的 PLAIN 方式,不需要认证:配置了 SASL 认证之后,连接K ...

  5. 如何使用Java + React计算个人所得税?

    前言 在报表数据处理中,Excel公式拥有强大而多样的功能,广泛应用于各个业务领域.无论是投资收益计算.财务报表编制还是保险收益估算,Excel公式都扮演着不可或缺的角色.传统的做法是直接依赖Exce ...

  6. MySQL允许远程登录的授权方法

    泛授权方式 数据库本地直接登录上数据库: mysql -h localhost -u root 然后执行以下命令,授权完后直接就可以远程连接上.mysql>GRANT ALL PRIVILEGE ...

  7. springboot、jvm调优(设置运行的参数)

    1.工具 jdk自带的工具位置: 找到窗口->应用程序 2.问题和方式 在SpringBoot项目中,调优主要通过配置文件和配置JVM的参数的方式进行. 2.1 springboot修改配置文件 ...

  8. Mysql进击篇-存储引擎、索引、sql优化、视图、锁、innoDb、管理

    1.存储引擎 (1)连接层 最上层是一些客户端和连接服务,主要完成一些类似于连接处理,授权认证.以及相关的安全方案,服务器也会为安全接入的每个客户端验证它所具有的操作权限 (2)服务层 第二层架构主要 ...

  9. Flask框架——Flask脚本、flask知识点补充

    文章目录 Flask_脚本 1 集成Python shell 1.1 flask-script的用法: 1.1.1 实例:flask-script的简单实现 1.1.1命令添加方式: 第一种(无参命令 ...

  10. 重写equals方法的注意事项

    重写equals方法的注意事项   一. 在重写equals方法时,要注意满足离散数学上的特性1   自反性:对任意引用值X,x.equals(x)的返回值一定为true.2   对称性:对于任何引用 ...