首页 > 其他分享 >几张图带你了解.NET String

几张图带你了解.NET String

时间:2024-10-25 15:45:13浏览次数:1  
标签:String 暂存 char 字符串 NET ref 图带 string

String

字符串作为一种特殊的引用类型,是迄今为止.NET程序中使用最多的类型。可以说是万物皆可string

因此在分析dump的时候,大量字符串对象是很常见的现象

string的不可变性

string作为引用类型,那就意味是可以变化的.但在.NET中,它们默认不可变。
也就是说行为类似值类型,实际上是引用类型的特殊情况。
image

但是,"字符串具有不可变性"仅在.NET平台下成立,只是因为在BCL(Basic Class Library)中并未提供改变string内容的方法而已。
在C/C++/F# 中,是可以改变的。因此,我们完全可以在底层实现修改字符串内容

眼见为实

示例1

示例代码
        static void Main(string[] args)
        {
            var teststr = "aaa";
            Debugger.Break();
            Console.WriteLine(teststr);
            Console.ReadLine();
        }

image
可以看到,string的值为aaa

image
通过算法:address + 0x10 + 2 * sizeof(char) ,我们直接修改内存的内容

image

可以看到,同一个内存地址,里面的值已经从"aaa"变成了"aab".

示例2

点击查看代码
        static void Main(string[] args)
        {
            var str1 = "aaa";

            
            ref var c0 = ref MemoryMarshal.GetReference<char>(str1.AsSpan(0));
            c0 = '0';
            ref var c1 = ref MemoryMarshal.GetReference<char>(str1.AsSpan(1));
            c1 = '1';

            Console.WriteLine(str1);//从aaa变成了01a
        }

字符串的可变行为

那么在日常使用中,我们需要大量字符串拼接的时候。如何改进呢?
最常见的办法就是使用Stringbuilder.

Stringbuilder源码解析

 public sealed partial class StringBuilder : ISerializable
 {
 		//存储字符串的char[]
        internal char[] m_ChunkChars;

		//StringBuilder之间使用链表来关联
        internal StringBuilder? m_ChunkPrevious;
		
        public StringBuilder(string? value, int startIndex, int length, int capacity)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(capacity);
            ArgumentOutOfRangeException.ThrowIfNegative(length);
            ArgumentOutOfRangeException.ThrowIfNegative(startIndex);

            value ??= string.Empty;

            if (startIndex > value.Length - length)
            {
                throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_IndexLength);
            }

            m_MaxCapacity = int.MaxValue;
            if (capacity == 0)
            {
                capacity = DefaultCapacity;
            }
            capacity = Math.Max(capacity, length);

            m_ChunkChars = GC.AllocateUninitializedArray<char>(capacity);
            m_ChunkLength = length;

            value.AsSpan(startIndex, length).CopyTo(m_ChunkChars);
        }
		public StringBuilder Append(char value, int repeatCount)
        {
            if (repeatCount == 0)
            {
                return this;
            }

            char[] chunkChars = m_ChunkChars;
            int chunkLength = m_ChunkLength;


    		// 尝试在当前块中放入所有重复字符
    		// 使用与 Span<T>.Slice 相同的检查,以便在 64 位系统中进行折叠
    		// 因为 repeatCount 不能为负数,所以在 32 位系统中不会溢出
            if (((nuint)(uint)chunkLength + (nuint)(uint)repeatCount) <= (nuint)(uint)chunkChars.Length)
            {
				//使用Span高性能填充char[]
                chunkChars.AsSpan(chunkLength, repeatCount).Fill(value);
                m_ChunkLength += repeatCount;
            }
            else
            {
				//如果空间不足,则进行扩容
                AppendWithExpansion(value, repeatCount);
            }
            return this;
        }
		public override string ToString()
        {
			// 分配一个新的字符串用于存储结果
            string result = string.FastAllocateString(Length);
            StringBuilder? chunk = this;
            do
            {
                if (chunk.m_ChunkLength > 0)
                {
                   // 将这些值复制到局部变量中,以确保在多线程环境下的稳定性
                    char[] sourceArray = chunk.m_ChunkChars;
                    int chunkOffset = chunk.m_ChunkOffset;
                    int chunkLength = chunk.m_ChunkLength;

					// 使用内存移动复制数据到result中
                    Buffer.Memmove(
                        ref Unsafe.Add(ref result.GetRawStringData(), chunkOffset),
                        ref MemoryMarshal.GetArrayDataReference(sourceArray),
                        (nuint)chunkLength);
                }
				//移动到上一个StringBuilder中,链表式读取
                chunk = chunk.m_ChunkPrevious;
            }
            while (chunk != null);

            return result;
        }
 }

在Stringbuilder的内部,内部使用char[] m_ChunkChars将文本保存。并且使用Span方式直接高性能操作内存。
image

避免对象分配是改进代码性能的最常见方法
string.format/string.join/$"name={name}" 等常见函数均已在内部实现Stringbuilder

字符串为什么不可变?

那么既然string的反直觉,那么为什么要这么设计呢?原因有如下几点

  1. 安全性
    string的使用范围太广了,比如new Dictionary<string, string>(),用户token,文件路径。它们的用途都代表一个key,如果这个key能被程序随意修改。那么将毫无安全性可言。
  2. 并发性
    正因为string使用范围大,所以很多场景都可能存在并发访问,如果可变,那么需要承担额外的同步开销。

为什么string不是一个结构?

上面说了这么多,结构完美满足了不可变/并发安全 这两个条件,那为什么不把string定义为结构?
其核心原因在于,结构的传值语义会导致频繁复制字符串
而复制大字符串的开销太大了,因此使用传引用语义要高效得多

JSON 的序列化/反序列化就是一个典型的例子

字符串暂存

.NET Rumtime内部有一个string interning 机制
当两个字符串一模一样的时候,不需要在内存中存两份。只保留一份即可

但字符串暂存有个限制,默认情况下是只暂存静态创建的字符串的。也就是静态值才会被暂存起来.由JIT来判断是否暂存

举个例子
        static void Main(string[] args)
        {
            var s1 = "hello world";
            var s2 = "hello ";
            var s3 = "world";

            Console.WriteLine(string.ReferenceEquals(global,s1));  //True ,两者一致,只保留一个变量
            Console.WriteLine(string.ReferenceEquals(s1, s2 + s3));//False s2+s3是动态的,不暂存

            Console.ReadLine();
        }

究其原因是因为这样做开销巨大,创建一个新字符串时,runtime需要动态的检测它是否已被暂存。如果被检测的字符串相当庞大或数量特别多,那么花销同样也很大。

FCL提供了显式API string.IsInterned/string.Intern 来让我们可以主动暂存字符串。

字符串被暂存在哪里?

https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/stringliteralmap.cpp

这时大家可以思考一下,暂存的字符串跟静态变量有什么区别? 都是永远不会被释放的对象
因此可以猜到。字符串应该是被暂存在AppDomain中。与高频堆应该相邻在一起.

在.NET内部Appdomain中,有一个私有堆叫String Literal Map的对象,内部存储着字符串的hash与一个内存地址。
内存地址指向另外一个数据结构LargeHeapHandleTable .位于LOH堆中,LargeHeapHandleTable内部包含了对字符串实例的引用
image

在正常情况下,只有>85000字节的才会被分配到LOH堆中,LargeHeapHandleTable就是一个典型的例外。一些不会被回收/很难被回收的对象即使没有超过85000也会分配在LOH堆中。因为这样可以减少GC的工作量(不会升代,不会压缩)

眼见为实

挖坑待埋,sos并未提供String Literal Map的堆地址,待我摸索几天
image

安全字符串

在使用string的过程中,可能包含敏感对象。比如Password.
String对象内部使用char[]来承载。因此携带敏感信息的string。被执行了unsafe或者非托管代码的时候。就有可能被扫描内存。
只有对象被GC回收后,才是安全的。但是中间的时间差足够被扫描N次了。

为了解决此问题,在FCL中添加了SecureString类。作为上位替代

  1. 内部使用UnmanagedBuffer来代替char[]
public sealed partial class SecureString : IDisposable
{
		private readonly object _methodLock = new object();//同步锁
        private UnmanagedBuffer? _buffer; //使用UnmanagedBuffer代替char[]
		public SecureString()
        {
			_buffer = UnmanagedBuffer.Allocate(GetAlignedByteSize(value.Length));
            _decryptedLength = value.Length;

            SafeBuffer? bufferToRelease = null;
            try
            {
                Span<char> span = AcquireSpan(ref bufferToRelease);
                value.CopyTo(span);
            }
            finally
            {
                ProtectMemory();
                bufferToRelease?.DangerousRelease();
            }
        }

		
		public void AppendChar(char c)
        {
            lock (_methodLock)
            {
                EnsureNotDisposed();
                EnsureNotReadOnly();

                Debug.Assert(_buffer != null);

                SafeBuffer? bufferToRelease = null;

                try
                {
				    //解密内存以便进行修改
                    UnprotectMemory();

                    EnsureCapacity(_decryptedLength + 1);

                    Span<char> span = AcquireSpan(ref bufferToRelease);
                    span[_decryptedLength] = c;
                    _decryptedLength++;
                }
                finally
                {
					//重新加密
                    ProtectMemory();
                    bufferToRelease?.DangerousRelease();
                }
            }
        }
}
  1. 实现了IDisposable接口,开发可以手动执行Dispose().对内存缓冲区直接清零,确保恶意代码无法获得敏感信息

        public void Dispose()
        {
            lock (_methodLock)
            {
                if (_buffer != null)
                {
                    _buffer.Dispose();
                    _buffer = null;
                }
            }
        }

安全字符串真的安全吗?

SecureString的目的是避免在进程中使用纯文本存储机密信息
SecureString的底层本质上也是一段未加密的char[],由FCL进行数据加密/解密。
因此只有.NET Framework 中,内部的char[]由windows提供支持,是加密的
但在.NET Core中,其他平台并未提供系统层面的支持

https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md

因此,个人认为真正的"银弹". 是数据本身就是加密的。比如从数据库中存储就是加密内容,或者配置文件中本身就是加密的。因为操作系统没有安全字符串的概念。

恶意代码只要能读内存,且内存本身未加密。那么在CLR层上就是裸奔

标签:String,暂存,char,字符串,NET,ref,图带,string
From: https://www.cnblogs.com/lmy5215006/p/18494483

相关文章

  • Netty、Go、Apache Tomcat、grpc-go、jetty、nghttp2、Apache Traffic Server是什么
    这些都是与网络编程和服务器应用相关的技术,下面我将分别简要介绍它们:Netty:Netty是一个异步事件驱动的网络应用程序框架,用于快速开发高性能、高可靠性的网络服务器和客户端程序。它支持多种协议,包括HTTP、HTTPS、FTP、SMTP等,广泛应用于游戏、移动、物联网、大数据等领域。......
  • 【C#】 .NET Framework 中使用JSON
    因为System.Text.Json是.NETCore和.NET5+中引入的命名空间。如果你使用的是.NETFramework,你需要使用Newtonsoft.Json库来处理JSON。 usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;usingSystem.Threading.Tasks;u......
  • Redis 厨神:用 StringRedisTemplate 轻松获取数据的秘笈
    前言在这个快节奏的时代,数据处理就像烹饪,既需要精准的配料,又需要高超的烹饪技巧。想象一下,你在厨房里忙得不可开交,却被突如其来的订单搞得手忙脚乱。今天,我们要揭开如何用StringRedisTemplate轻松获取数据的秘密,让你在SpringBoot3.x的世界里,摇身一变,成为Redis的厨房大......
  • .NET使用OllamaSharp实现大模型推理对话的简单演示
    https://www.cnblogs.com/weskynet/p/18497936 前提条件:请确保你本地已经安装了ollama以及有关本地离线模型。或者已有远程模型环境等。如果没有,请自行部署。如果需要帮助,可以文末获取联系方式咨询。由于部署离线大模型过于简单,在线资料也很多,我就省略这个步骤了。 创建一个......
  • 机床 发那科 转 profinet IO 项目案例
    目录1 案例说明 12 VFBOX网关工作原理 13 准备工作 24 网关采集发那科机床数据 25 用PROFINETIO协议转发数据 56 案例总结 71 案例说明设置网关采集发那科机床数据把采集的数据转成profinetIO协议转发给其他系统。2 VFBOX网关工作原理VFBOX网关是协议转换网关,是把......
  • Unet网络搭建Day1
    Pycharm内搭建虚拟环境:一、将PyCharm中的终端运行前面的PS修改成当前环境解决方法:只需要在pycharm的设置中修改一些terminal的环境即可,具体步骤如下:1.打开pycharm中的settings;2.找到Terminal选项;3.将shellpath的位置改为cmd.exe;4.点击ok;5.重启pycharm即可。二、wandb......
  • 谈一谈 Netty 的内存管理 —— 且看 Netty 如何实现 Java 版的 Jemalloc
    本文基于Netty4.1.112.Final版本进行讨论在之前的Netty系列中,笔者是以4.1.56.Final版本为基础和大家讨论的,那么从本文开始,笔者将用最新版本4.1.112.Final对Netty的相关设计展开解析,之所以这么做的原因是Netty的内存池设计一直在不断地演进优化。在4.1.52.Final......
  • .NET 开源扁平化、美观的 C/S 控件库
    前言给大家推荐一个优秀的控件集,它基于.NETFramework4.0,采用纯原生开发,不包含任何第三方插件或类库。该控件集涵盖了常用的窗体和控件,同时还包括工业工具和类Web控件。使用这套控件库我们可以快速的搭建一个漂亮的应用程序。项目介绍HZHControls包含了200多个控件、窗......
  • Agent.exe:让Claude 3.5 Sonnet控制你的电脑
    简介Agent.exe是一个开源的Electron应用程序,它允许Claude3.5Sonnet直接控制你的本地计算机。这个项目的特色在于它的操作界面简洁,并且对Firefox浏览器有特别的优化支持。功能亮点简单易用:通过一个简单的Electron应用,让Claude3.5Sonnet接管你的电脑。操作直......
  • DCN-Digital Communications and Networks
    @目录一、征稿简介二、重要信息三、服务简述四、投稿须知一、征稿简介二、重要信息期刊官网:https://ais.cn/u/3eEJNv三、服务简述人工智能原生网络6G通信网络中的人工智能自主网络管理网络功能虚拟化(NFV)软件定义网络(SDN)网络机器学习无线联合学习动态频谱管理网......