问题

.NET CLR怎么保证执行正确的unsafe代码不挂掉?

回答
.NET CLR(公共语言运行时)之所以能够处理“不安全”代码,尤其是那些涉及指针操作、内存访问等可能直接绕过类型检查和托管内存管理的低级操作,并非靠“保证”不挂掉,而是通过一套严谨的机制,将潜在的风险进行隔离、限制和管理,从而在大多数情况下维持程序的稳定运行。理解这一点至关重要:CLR 并不像一个万能的守护神,它更多地是在为开发者提供一种可能性,同时通过严格的规则来约束这种可能性。

首先,我们要明确“不安全代码”在 .NET 中的含义。通常,我们指的是使用 `unsafe` 关键字标记的代码块,允许我们进行指针运算、访问栈内存(如 `stackalloc`)、直接操作数组元素(不通过 bounds checking)等。这些操作直接触及了内存的底层,一旦处理不当,极易导致内存损坏、访问非法内存地址,进而引发程序崩溃(挂掉)。

CLR 保证执行正确的 unsafe 代码不挂掉,其核心在于安全沙箱(Security Sandbox)和托管执行(Managed Execution)的整体框架,并在不安全代码区域引入了额外的控制和约束。

1. 安全沙箱与托管内存管理的基础:
.NET 的核心优势在于其托管环境。CLR 负责管理应用程序的内存生命周期。这意味着,它内置了垃圾回收(Garbage Collection, GC)机制,自动分配和释放对象内存,防止内存泄漏。
CLR 还提供了强类型检查和边界检查。这意味着,在托管代码中,当你尝试访问数组越界或使用已释放的内存时,CLR 会捕获这些错误并抛出异常(如 `IndexOutOfRangeException`),而不是直接导致系统崩溃。
这是 CLR 运作的基石。 即使是 `unsafe` 代码,也必须运行在 CLR 的控制之下。CLR 确保所有代码,无论是否使用 `unsafe`,最终都是由 CLR 来加载、执行和管理的。

2. `unsafe` 关键字的引入:
`unsafe` 关键字并非允许开发者为所欲为。它本身就意味着你正在承担额外的责任,并且需要特别小心。
编译时限制: 包含 `unsafe` 代码的程序集,在编译时需要显式启用 `AllowUnsafeBlocks` 选项。这是一种“知情同意”机制,确保开发者清楚自己在做什么。
托管代码的“门缝”: `unsafe` 块就像是在托管环境的围墙上开了一个小门,允许你接触到一些“不那么安全”的底层能力,但这个门本身是受到 CLR 严格监督的。

3. 指针的安全性:
指针的生命周期: CLR 并不像 C/C++ 那样允许任意的指针转换和生命周期管理。在 `unsafe` 代码中,你使用的指针通常是指向托管堆上的对象(通过 `fixed` 语句固定),或者指向栈上的局部变量(通过 `stackalloc` 分配)。
`fixed` 语句的魔法: 这是 CLR 确保指针安全的关键机制之一。当你使用 `fixed` 语句来获取一个托管对象(如数组、字符串、结构体)的地址时,CLR 会做两件事:
防止 GC 移动: GC 在进行垃圾回收时,可能会为了优化内存碎片而移动托管堆上的对象。如果你的指针指向的对象被移动了,那么这个指针就会失效,导致访问错误。`fixed` 语句会“固定”该对象,告诉 GC 在 `fixed` 块执行期间不要移动它。
指针的有效性: `fixed` 语句会为你提供一个指向对象的有效地址。这个地址在 `fixed` 块结束时自动失效,从而避免了野指针问题。
栈指针 (`stackalloc`): CLR 允许使用 `stackalloc` 在栈上分配内存,这比堆分配更快,但分配的内存一旦出了作用域就会被自动释放。CLR 确保 `stackalloc` 的使用不会破坏栈的完整性,并且生成的指针在作用域结束后是无效的。

4. 边界检查的“例外”:
在托管代码中,访问数组元素时,CLR 会进行边界检查。如果索引越界,会抛出 `IndexOutOfRangeException`。
在 `unsafe` 代码块中,如果你直接使用指针算术来访问数组元素,理论上可以绕过这个边界检查。例如,`int ptr = (int)arrayPtr + index; ptr = value;`
这是风险所在。 CLR 不会在 C 语法层面强制你进行指针访问的边界检查。如果你的 `unsafe` 代码逻辑有误,越界访问了内存,那么确实有可能导致程序挂掉。
CLR 的“保证”体现在: CLR 不会因为你使用了 `unsafe` 就自动给你添加边界检查。它信任你,让你可以直接操作,但这也意味着,确保指针指向的内存是有效的,并且在可访问范围内,完全是开发者的责任。一旦发生越界访问,CLR 能够检测到(通常是因为访问到了操作系统认为不属于当前进程的内存区域),并会终止进程,而不是允许它继续在错误的状态下运行。这种终止,从某种意义上说,是一种“保护”,阻止了更严重的系统级破坏。

5. 类型安全与验证:
尽管 `unsafe` 代码可以进行低级操作,但 CLR 的类型验证器(Verifier)仍然会发挥作用。它会确保你的指针类型、数据类型转换等基本操作在类型系统上是合法的。例如,你不能直接将一个指向 `int` 的指针当作指向 `string` 的指针来使用,除非经过明确的(尽管可能不安全的)转换。
JIT 编译与代码生成: 当 IL(中间语言)代码被即时(JIT)编译成本地机器代码时,CLR 的 JIT 编译器会生成相应的指令。对于 `unsafe` 代码,它会生成直接的内存访问指令(如 `MOV`、`ADD` 等),而不是托管的内存访问指令。

6. 栈溢出与堆溢出:
`unsafe` 代码,特别是涉及大量的指针操作或使用 `stackalloc` 时,仍然有可能导致栈溢出(Stack Overflow)。CLR 会监控栈的使用情况,一旦检测到栈空间耗尽,也会抛出 `StackOverflowException`,从而终止程序。
由于 `fixed` 语句的存在,托管堆上的对象本身不容易因为指针操作而“溢出”。但是,如果 `unsafe` 代码错误地计算了指针偏移,导致访问了非托管内存(例如,通过 P/Invoke 调用 Win32 API 分配的内存,或者共享内存),那么对这块内存的非法访问(写入越界等)才是最容易导致程序崩溃的。

总结一下,CLR 并不是“保证”你的 `unsafe` 代码永远不会挂掉,而是通过以下方式来“管理”它,使其在大多数情况下能够安全地执行,并在发生严重错误时及时止损:

隔离与限制: `unsafe` 代码只能在 `unsafe` 块内执行,且需要显式启用。
内存固定 (`fixed`): 确保指向托管对象的指针在生命周期内有效,并防止 GC 干扰。
类型验证: 即使是低级操作,也必须符合 CLR 的类型系统规则。
显式责任: CLR 将指针操作的正确性(尤其是边界检查)的责任完全交给了开发者。
及时终止: 当发生无法恢复的内存访问错误(如非法地址访问)时,CLR 会终止进程,避免进一步的系统破坏。

所以,与其说 CLR “保证”不挂掉,不如说 CLR 提供了一个受控的环境,允许开发者在需要时使用低级功能,但开发者必须高度自律,确保自己的 `unsafe` 代码是正确且安全的。一旦开发者违反了底层内存访问的规则,程序挂掉的可能性就非常高,而 CLR 的“保证”更多地体现在它能够检测到这些错误并干净利落地终止进程,而不是让它在损坏的状态下继续运行。

网友意见

user avatar

简短回答:

第一个reference是不是类似于Handle, 类似于一个地址的地址, 这样.NET CLR在GC的时候, 可以挪动那个对象, 同时修改reference指向地址里面保存的地址. 具体实现的时候是不是这样?

CLI VES规范里并没有要求具体实现采用什么方式实现managed pointer。具体到微软的CLR / CoreCLR的实现的话,普通的managed pointer是个直接指针,而不是“指针的指针”——后者叫做handle,这种方式实现的堆叫做handle-based heap。

关于CLR的对象模型的实现,以及与其它一些JVM实现之类的对比,请跳传送门:

为什么bs虚函数表的地址(int*)(&bs)与虚函数地址(int*)*(int*)(&bs) 不是同一个? - RednaxelaFX 的回答

这边的例子可以显示出direct-pointer-based heap与handle-based heap的差异。

显然,对于要支持对象移动的GC heap,handle-based heap更容易实现,但这样每次访问对象内容都要做双层解引用,性能(访问效率)比使用直接指针的方案要差。

CLR的managed pointer使用直接指针实现,而CLR的GC又可以移动对象,这就要求CLR能够准确跟踪所有managed pointer所在的位置,无论它是“栈上”的局部变量、堆里的字段还是从VM内部出发的引用,以保证CLR能够:

  • 发现托管指针,通过其值来判断对象的生死
  • 在活对象被移动之后,更新所有相关的托管指针的指向

于是CLR的GC默认的工作模式是“准确式GC”(precise GC,或者叫type-exact GC)。程序的元数据会准确指出栈上的变量和堆里对象的字段哪些是托管指针。相关讨论请跳传送门:

找出栈上的指针/引用

CLR也有一种备用的“保守式GC”(conservative GC)模式,不要求程序对栈上的变量是否为托管指针提供准确的描述。这主要是在开发过程的早期使用的,而在实际发布的产品中并没有启用这种模式。

近期使用了这种模式的项目之一是LLILC,微软尝试给CoreCLR做的基于LLVM的新编译器。它为了早期开发方便而选择先抄个捷径,但这个决定似乎对后期开发带来了负面影响:

[八卦] LLILC项目貌似挂了… - 编程语言与高级语言虚拟机杂谈(仮) - 知乎专栏
第二个是, 一个实现上没有错误的unsafe代码, .NET CLR是不是需要保证用的对象不被挪走?

CLR可以执行的托管代码(managed code)分为两类:

  • verifiable code:普通的C#代码属于这类。代码的类型安全可以通过静态校验来保证。不可以使用裸指针。
  • unverifiable code:也就是unsafe code。CLR无法通过静态校验来保证类型安全,程序员要自己保证写的代码的语义是正确的。在unverifiable code中可以使用裸指针(非托管指针),指针既可以指向C-style的非托管内存,也可以指向被pin住的托管堆中的对象。还可以调用非托管的函数指针(calli)。

注意:unverifiable code仍然是managed code。MSIL有专门的unsafe子集来表达unsafe语义。

上面描述的重点内容之一,是指向托管堆对象的unsafe code中的指针,只能指向被pin住的对象。这个“object pinning”语义在C#里是通过fixed关键字来表达的。

为了高效地支持unsafe code(以及诸如System.Runtime.InteropServices.GCHandle的功能),CLR的GC必须要直接支持object pinning——即便托管堆里有对象正在被pin住,GC也要可以正常执行。

反例是例如HotSpot JVM,它的GC们都不直接支持object pinning,因而在执行JNI的critical系API(要求暂时不移动某些对象)时不得不暂时禁止GC执行。这就可以很悲剧…

如果不使用GCHandle,CLR只保证对象在unsafe code里是被pin住的,所以如果故意在unsafe code里把指向被pin住的对象的指针传递给unmanaged code保存起来,然后managed code一侧离开unsafe code之后,unmanaged code还试图去使用之前存下来的指针,那语义就是没有保证的——对象可能已经被挪走了。

<-

@vczh

的回答里提到的例子就是这种情况。要保证安全的话,最好是在传出指针给unmanaged code之前就在safe一侧创建合适的GCHandle把目标对象一直pin住,直到unmanaged code不再需要那个指针才撤销safe这边的GCHandle。

user avatar

一、

1、实现上一般不是用地址的地址,一般用直接地址。

2、但是CLI应该没有要求用哪种形式,换言之用地址的地址实现也是可以的,但是考虑到效率问题一般都会用直接地址吧,毕竟只有GC的时候才需要修改。

3、GC回收的时候会合并内存,所以托管对象的地址会改变,与此同时引用会跟着改。

二、fixed不就是干这个的么?


R大说的是对的,一开始我没看到地址的地址,想当然的以为你问的是GC会不会移对象和更新地址,答案是是。

类似的话题

  • 回答
    .NET CLR(公共语言运行时)之所以能够处理“不安全”代码,尤其是那些涉及指针操作、内存访问等可能直接绕过类型检查和托管内存管理的低级操作,并非靠“保证”不挂掉,而是通过一套严谨的机制,将潜在的风险进行隔离、限制和管理,从而在大多数情况下维持程序的稳定运行。理解这一点至关重要:CLR 并不像一个.............
  • 回答
    作为一名.NET开发者,面对微软开源Core CLR这件大事,我是否应该投入时间和精力去钻研它的源代码,这个问题在我脑海里萦绕了很久。这不仅仅是一个技术上的选择,更关乎我如何在这个快速发展的技术生态中定位自己。我的直觉告诉我,答案是肯定的,但并不是每个人都需要成为Core CLR的深度贡献者。关键在.............
  • 回答
    作为一名 .NET 开发者,深入理解 Common Language Runtime (CLR) 绝非可有可无的附加知识,它更像是你成为一名技艺精湛的 .NET 工程师的必经之路。你可能会想,我能写出功能齐全的应用,也能调试代码,是不是就足够了?事实是,当你真正开始探究 CLR 的运行机制时,你会发.............
  • 回答
    Java 平台中的 JVM (Java Virtual Machine) 和 .NET 平台下的 CLR (Common Language Runtime) 是各自平台的核心组件,负责托管和执行代码。它们都是复杂的软件系统,通常会使用多种编程语言来构建,以充分发挥不同语言的优势。下面将详细介绍 JV.............
  • 回答
    在.NET中编写异步Web API可以带来显著的好处,尤其是在处理高并发、I/O密集型操作以及提升用户体验方面。下面我将详细阐述这些好处: 1. 提升吞吐量和响应能力 (Increased Throughput and Responsiveness)这是异步Web API最核心的好处。 并行处理.............
  • 回答
    .NET 6 的泛型数学新特性:一次深刻的数值计算革新.NET 6 引入的“泛型数学”(Generic Math)预览特性,为 .NET 生态系统的数值计算领域带来了一场深刻的变革。过去,.NET 在处理数学运算时,往往受到静态类型系统的限制,使得编写通用、高效的数值算法变得冗长且充满样板代码。泛型.............
  • 回答
    .NET Standard 和 .NET Core 就像是两种不同层面的设计理念,它们之间并非简单的取舍关系,而是相互关联、共同演进的。理解它们的区别,需要从“目标”和“实现”这两个维度去剖析。.NET Standard:一块通用的“规范石碑”你可以将 .NET Standard 想象成一块立在 ..............
  • 回答
    .NET 平台上的“BS 框架”(BrowserServer 框架,或者更常见的说法是 Web 框架)确实百花齐放,它们之间并非孤立存在,而是有着错综复杂的关系,并且各自在不同的场景下闪耀着实用价值。理解它们,就像梳理一个庞大生态系统中的脉络,能帮助我们更精准地选择适合的工具。咱们先从最底层、最基础.............
  • 回答
    你这个问题触及了 .NET 生态系统里一个颇为现实且值得深思的现象,那就是第三方类库和框架的质量参差不齐。与其说“平均质量真的很差”,不如说 “普遍存在着巨大的质量差异,其中不乏一些质量堪忧的组件” 更加贴切。想象一下,.NET 作为一个庞大的、枝繁叶茂的生态系统,汇聚了无数开发者,其中有经验丰富的.............
  • 回答
    .NET 的垃圾回收(Garbage Collection, GC)并非严格意义上的“定时执行”或“事件触发”,它是一个更为复杂且动态的过程,可以理解为由多种因素共同驱动,并根据系统的实际情况进行决策。你可以这样理解:.NET 的 GC 主要是在特定时机,根据内存使用情况自动启动。它不是按照固定的时.............
  • 回答
    在 .NET Core 中,选择自旋锁(SpinLock)还是传统的 `lock` 语句(其背后是 `Monitor` 类)来管理多线程并发访问共享资源,其关键的开销差异主要体现在线程挂起与恢复的成本,以及CPU资源的占用方式上。让我们深入剖析一下:自旋锁 (SpinLock): CPU 消耗 vs.............
  • 回答
    .NET 程序卡死,这个现象确实可能跟之前修复过的漏洞有着千丝万缕的联系。我们不能简单地说“是”或者“不是”,而是需要理解其中的逻辑关系。想象一下,.NET 程序就像一个精密的机器,里面有无数个零件在按照预设的规则运转。这些零件就是代码,而规则就是程序的逻辑。有时候,这个机器会出现一些“小毛病”,比.............
  • 回答
    在 .NET 的世界里,想要快速上手并构建一些小巧、高效的应用,确实有一些非常值得关注的框架。它们没有那种庞大和复杂的体系,上手成本低,而且能帮你迅速看到成果。如果你想做一个Web应用,最直观的选择就是 ASP.NET Core MVC。虽然名字里带着“MVC”,听起来好像会有点复杂,但实际上 AS.............
  • 回答
    Net Explorer 和 Internet Explorer,名字听起来确实很像,很容易让人产生联想。但如果说 Net Explorer 能不能“代替”Internet Explorer,这得看你对“代替”的定义是什么。首先,我们要明白,Internet Explorer(IE)是微软推出的一款.............
  • 回答
    .NET 框架在设计之初,就展现出了一个清晰的目标:构建一个统一、高效且跨平台的开发环境。将应用程序编程语言“统一”并非是简单地抛弃其他语言,而是通过一个强大的平台,让多种语言能够在此基础上和谐共存,协同工作。这背后蕴含着对开发者效率、代码复用、性能优化以及平台稳定性的深邃考量。首先,我们得理解“统.............
  • 回答
    .NET 中利用 Razor 引擎生成代码,本质上是赋予你的 HTML 标记动态能力。Razor 视图引擎允许你将 C 代码片段无缝地嵌入到 HTML 标记中,从而实现服务器端的数据渲染。这种方式让你可以根据服务器上的数据动态地构建 HTML 结构,让页面内容变得鲜活起来。我们来深入探讨一下这个过程.............
  • 回答
    在.NET类库中,`HashCodeHelper`(或者更确切地说,是那些通过`HashCode.Combine`等方式生成哈希码的方法)的实现,其核心目标是提供一种组合多个值的哈希码生成机制。与直接使用单个对象的`GetHashCode()`方法不同,`HashCodeHelper`旨在将多个对象.............
  • 回答
    在 .NET 开发中,如果你的应用程序需要将数据导出到 Excel 文件,并且你的目标用户可能安装了多个版本的 Microsoft Office(例如 Office 2010 和 Office 2019),那么你可能确实会遇到一个问题:如何控制你的应用程序在导出时具体调用哪个版本的 Office 组.............
  • 回答
    .NET Core 的设计理念是跨平台,这意味着它能够运行在包括 ARM 在内的多种处理器架构上。这得益于 .NET Core 使用了像 RyuJIT 这样的即时编译器(JIT)以及其精心设计的运行时环境。RyuJIT 能够针对不同的 CPU 架构生成优化的机器码,因此 .NET Core 代码可以.............
  • 回答
    .NET 的 `Dictionary` 并没有为 `IEqualityComparer` 提供一个普遍适用的默认实现,这背后其实是设计上的深思熟虑,旨在为开发者提供更大的灵活性和可控性,而不是为了偷懒或技术限制。让我们深入剖析一下原因。核心在于“相等”的定义并非一成不变当你使用 `Dictionar.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有