前言

好久没有动用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. 文盘Rust -- Mutex解决并发写文件乱序问题

    在实际开发过程中,我们可能会遇到并发写文件的场景,如果处理不当很可能出现文件内容乱序问题.下面我们通过一个示例程序描述这一过程并给出解决该问题的方法. use std::{ fs::{self, Fi ...

  2. css里的position的static|relative|absolute|fixed的区别

    前提:仅以div块为例,其它不清楚 注:1.下面截图看起来不是从页面左上角位置开始,是因为body元素有默认的margin,有边框 2.当position不是static时,top和bottom需要指 ...

  3. 为什么NoSQL不支持事务

    为什么NoSQL不支持事务 1. 背景 看书<Neo4j权威指南>的时候,发现个问题:日常的NoSQL都不支持事务(ACID). 2. 问题 事务对数据的存储过程是有利的,既然事情是有利的 ...

  4. 个人GAN训练的性能迭代

    使用GAN进行生成图片 损失函数的迭代 DCGAN->Wasserstein GAN-> Wasserstein GAN + Gradient Penalty Discriminator训 ...

  5. 如何使用io_uring构建快速响应的I/O密集型应用?

    本文分享自华为云社区<如何使用io_uring构建快速响应的I/O密集型应用>,作者: Lion Long . 当涉及构建快速响应的I/O密集型应用时,io_uring技术展现出了其卓越的 ...

  6. Linux 内核设备驱动程序的IO寄存器访问 (下)

    Linux 内核设备驱动程序通过 devm_regmap_init_mmio() 等函数获得 struct regmap 结构对象,该对象包含可用于访问设备寄存器的全部信息,包括定义访问操作如何执行的 ...

  7. 产品代码都给你看了,可别再说不会DDD(五):请求处理流程

    这是一个讲解DDD落地的文章系列,作者是<实现领域驱动设计>的译者滕云.本文章系列以一个真实的并已成功上线的软件项目--码如云(https://www.mryqr.com)为例,系统性地讲 ...

  8. BeanUtils.copyProperties:曾经是我的女神,现在是我的毒药。

    前言 BeanUtils.copyProperties十有八九是你这些年工作中用的很多的其中一个,不管是Apache的还是Spring的. 网上的解释浩如烟海,我这边用一个超简单的例子直观展示给你看. ...

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

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

  10. Strimzi Kafka Bridge(桥接)实战之三:自制sdk(golang版本)

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本文是<Strimzi Kafka B ...