<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/feeds/style.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>ralf-jung</title>
    <description>rssume processed feed for ralf-jung</description>
    <link>/feeds/ralf-jung</link>
    <atom:link href="/feeds/ralf-jung" rel="self" type="application/rss+xml"/>
    <lastBuildDate>Thu, 4 Jun 2026 09:59:18 +0000</lastBuildDate>
    <generator>rssume</generator>
    <item>
      <title>如何用“讲故事”模型将内联汇编融入 Rust</title>
      <category>rust</category>
      <description>[AI 摘要] 本文提出了“讲故事”模型，通过要求内联汇编提供对应的Rust代码描述来将其融入Rust的抽象机语义，以确保与优化和安全性兼容。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文提出了“讲故事”模型，通过要求内联汇编提供对应的Rust代码描述来将其融入Rust的抽象机语义，以确保与优化和安全性兼容。</div><p>Rust 抽象机中充满了在<a href="/blog/2020/12/14/provenance.html" rel="noopener noreferrer">实际硬件上不存在</a>的<a href="/blog/2019/07/14/uninit.html" rel="noopener noreferrer">奇妙细节</a>。不可避免地，每次讨论这些时，总会有人问：“但如果我使用内联汇编呢？那指针来源、未初始化内存、树形借用以及你们发明的这些实际不存在的‘有趣’东西会怎样？”</p>

<p>这是个好问题，但要正确回答需要一些功夫。在这篇文章中，我将通过提出一个<em>通用原则</em>来阐述当前对内联汇编如何融入 Rust 抽象机的思考：该原则解释了我们对纯 Rust 语义的任何决定如何影响内联汇编可以或不可以做什么。</p>



<p>请注意，我在这里讨论的所有内容同样适用于外部函数接口调用，就像适用于内联汇编一样。这两种机制本质上非常相似：它们都允许 Rust 代码调用非 Rust 编写的代码。<sup id="fnref:xlang"><a href="#fn:xlang" rel="noopener noreferrer">1</a></sup> 我不会在文章中反复重复“内联汇编或 FFI”，但每次我提到内联汇编时，也包含了 FFI。</p>

<p>首先，让我解释一下为什么有些事情是内联汇编从根本上就不允许做的。</p>

<h2 id="why-cant-inline-assembly-do-whatever-it-wants">为什么内联汇编不能为所欲为？</h2>

<p>人们喜欢认为内联汇编能让他们摆脱抽象机所有复杂的要求。不幸的是，那只是个空想。下面是一个示例来证明这一点：</p>

<div><div><pre><code><span>use</span> <span>std</span><span>::</span><span>arch</span><span>::</span><span>asm</span><span>;</span>

<span>#[inline(never)]</span>
<span>fn</span> <span>innocent</span><span>(</span><span>x</span><span>:</span> <span>&amp;</span><span>i32</span><span>)</span> <span>{</span> <span>unsafe</span> <span>{</span>
    <span>// 在 x 给出的地址存储 0。</span>
    <span>asm!</span><span>(</span>
        <span>"mov dword ptr [{x}], 0"</span><span>,</span>
        <span>x</span> <span>=</span> <span>in</span><span>(</span><span>reg</span><span>)</span> <span>x</span><span>,</span>
    <span>);</span>
<span>}</span> <span>}</span>

<span>fn</span> <span>main</span><span>()</span> <span>{</span>
    <span>let</span> <span>x</span> <span>=</span> <span>1</span><span>;</span>
    <span>innocent</span><span>(</span><span>&amp;</span><span>x</span><span>);</span>
    <span>assert!</span><span>(</span><span>x</span> <span>==</span> <span>1</span><span>);</span>
<span>}</span>
</code></pre></div></div>

<p>当编译器分析 <code>main</code> 时，它意识到只有一个共享引用被传递给 <code>innocent</code>。这意味着无论 <code>innocent</code> 做什么，它都无法改变存储在 <code>*x</code> 中的值。因此，这个断言可以被优化掉。</p>

<p>然而，<code>innocent</code> 确实写入了 <code>*x</code>！因此，优化改变了程序的行为。确实，这正是<a href="https://rust.godbolt.org/z/GG7YPsEcs" rel="noopener noreferrer">当前版本的 rustc 所发生的情况</a>：没有优化时，断言失败；但开启优化时，它通过。因此，要么优化是错误的，要么程序存在未定义行为（UB）。既然我们确实希望进行这种优化，那么我们只能选择第二种可能。<sup id="fnref:nondet"><a href="#fn:nondet" rel="noopener noreferrer">2</a></sup></p>

<p>然而，UB 从何而来？如果整个程序都是用 Rust 编写的，答案会是“别名模型”。栈借用（Stacked Borrows）和树形借用（Tree Borrows），以及任何其他为 Rust 考虑的值得考虑的别名模型，都会将通过从共享引用派生的指针进行写入视为 UB。但这次，程序的部分不是用 Rust 编写的，所以事情没那么简单。我们怎么能说内联汇编块违反了树形借用呢？它使用的语言甚至没有类似树形借用的东西。这就是本文剩余部分要讨论的内容。</p>

<p>我希望这个例子清楚地表明，我们<em>无法</em>让内联汇编完全忽略抽象机概念（如树形借用）。内联汇编块导致了 UB，我们只需要弄清楚它是如何以及为什么导致的——更重要的是，我们必须弄清楚人们如何确保他们的内联汇编块<em>不会</em>导致 UB。</p>

<h2 id="when-is-inline-assembly-compatible-with-optimizations">内联汇编何时与优化兼容？</h2>

<p>看起来我们现在必须定义一个能与汇编代码配合工作的树形借用版本。这将是一项不可能的任务（树形借用依赖于指针来源，这在汇编中并不存在）。<sup id="fnref:cheri"><a href="#fn:cheri" rel="noopener noreferrer">3</a></sup> 幸运的是，这也没必要。</p>

<p>相反，我们可以依托于已经存在的树形借用和抽象机的其余部分的定义。我们通过要求程序员用 Rust 术语来<em>讲述一个故事</em>，说明内联汇编块做了什么来实现这一点。<sup id="fnref:alice"><a href="#fn:alice" rel="noopener noreferrer">4</a></sup>（如果这听起来很奇怪，请听我解释。我将解释为什么这合理。）具体来说，对于每个内联汇编块，都必须有一个对应的 Rust 代码片段，在<strong><em>纯 Rust 代码可观察的状态方面</em></strong>做同样的事情。在推理整体程序的行为时，内联汇编块将被该“故事”代码替换。你不必实际编写这段代码；重要的是这段代码存在，并且能与周围的 Rust 代码讲述一个连贯的故事。</p>

<p>对于上面的例子，这立即解释了哪里出错了：该内联汇编块的故事代码必须类似于 <code>(x as *const i32 as *mut i32).write(0)</code>，如果我们用这段代码代替内联汇编块，我们立即就能看出（Miri 也可以确认）程序存在 UB。一个内联汇编块可以有多种可能的故事，只要找到<em>一个</em>让一切合理的即可，但在这种情况下，这是不可能的。</p>

<p>那么，稍微详细一点，以下是我认为适用于内联汇编的规则：</p>
<ol>
  <li>对于每个内联汇编块，选择一个“故事”：一段 Rust 代码，作为该汇编块在<em>抽象机状态方面</em>做了什么的代表。这段故事代码只能访问提供给该内联汇编块的数据（显式操作数和全局变量）。在抽象机层面推理程序的健全性和正确性时，我们假装故事代码被执行，而不是汇编代码。</li>
  <li>这段代码必须满足诸如 <code>readonly</code> 或 <code>nomem</code> 等属性强加给 asm 块的所有要求，并且尊重操作数约束，例如不修改 <code>in</code> 操作数。</li>
  <li>实际的汇编代码必须<em>细化</em>故事代码，即汇编代码对抽象机可观察状态（特别是操作数和全局变量）所做的任何事情，都必须是故事代码也能做到的事情。</li>
</ol>

<p>我应该声明，我没有一个能证明这种方法正确性的形式化理论。然而，我相当有信心，因为这种方法与我们证明优化（如上面例子中的优化）正确性的方法非常吻合：正确性论证的核心是一个证明，即<em>所有</em> Rust 代码都满足某些普遍属性。例如，我们可以形式化并证明这样一个主张：任何接受没有内部可变性的共享引用作为参数的 Rust 函数都不能写入该参数。这并非唯一的此类属性；事实上，此类属性的集合并未完全知晓：我们明天可能会发现所有 Rust 代码都遵循的一个新属性。关键在于，任何“对于所有 Rust 程序，……”形式的属性也必须适用于故事代码，因为它就是普通的 Rust 代码！最后，因为实际的汇编代码细化了故事代码，我们知道为了推理程序，我们可以假装实际上是故事代码被执行，然后在编译结束时用所需的汇编代码替换故事代码，而不会改变程序行为。</p>

<p>所以，这就是故事代码有效的原因。但是，这难道不是让内联汇编变得完全无用吗？毕竟，内联汇编的全部意义就在于做我在纯 Rust 中无法做到的事情！</p>

<h2 id="inline-assembly-stories-by-example">通过示例理解内联汇编故事</h2>

<p>为了说服你讲故事的方法是可行的，让我们考虑几个内联汇编使用的代表性示例，以及相应的故事可能是什么样子的。</p>

<h4 id="pure-instructions">纯指令</h4>

<p>最简单的情况是代码想要访问语言中未暴露的新硬件操作。例如，内联汇编块可能只包含一条指令，该指令返回寄存器中设置为 1 的位数。在这里，讲故事很简单：我们可以手动编写一些位操作代码来计算设置为 1 的位数。</p>

<h4 id="page-table-manipulation">页表操作</h4>

<p>那很简单，所以让我们加大难度，考虑一个操作页表的操作系统内核。Rust 没有页表的概念。这里的“故事”可能是什么样子？</p>

<p>答案是，Rust 有一些与向页表中放入新页面非常相似的东西——它叫做 <code>alloc</code>。它还有一些非常类似于移除页面（<code>dealloc</code>）和将页面移动到地址空间中不同位置（<code>realloc</code>）的东西。因此，操作系统内核要告诉编译器的故事是，操作页表实际上只是某种奇怪的内存分配器。</p>

<p>更具体一点，以一种与讲故事方法兼容的方式“分配”一个页面可能如下所示：</p>
<ul>
  <li>首先，一些 Rust 代码使用 volatile 加载和存储来执行实际的页表操作。<sup id="fnref:volatile"><a href="#fn:volatile" rel="noopener noreferrer">5</a></sup></li>
  <li>然后，一个 asm 块执行当前系统上所需的任何屏障，以确保更新的页表生效。</li>
  <li>接着，页面的地址被转换为指针（使用 <a href="https://doc.rust-lang.org/std/ptr/fn.with_exposed_provenance.html" rel="noopener noreferrer"><code>with_exposed_provenance</code></a>）。</li>
  <li>最后，Rust 代码可以使用该指针来访问新页面。</li>
</ul>

<p>这个 asm 块的故事是它执行了给定地址的内存分配，我们知道该地址未被分配。<sup id="fnref:alloc-control"><a href="#fn:alloc-control" rel="noopener noreferrer">6</a></sup> 这创建了一个代表新分配的新指针来源。该分配随后立即被故事代码<a href="https://doc.rust-lang.org/std/primitive.pointer.html#method.expose_provenance" rel="noopener noreferrer">暴露</a>。</p>

<p>即使在页表更改后不需要屏障的架构上，asm 块仍然至关重要：它防止编译器在页表操作期间重排对新页面的访问！使用 Rust 程序的常规规则，编译器无法弄清这里有任何依赖关系。因此，asm 块充当了编译器栅栏：就编译器而言，这个块可能确实调用了我们编造的“故事代码”，因此新的指针以及基于它的操作不能移动到 asm 块之前。</p>

<p>这就是为什么有时人们认为 asm 块是编译器栅栏：一个 asm 块代表了编译器不知道的某个任意的“故事代码”，因此编译器必须<em>好像</em>有某个任意代码在此执行一样来对待这段代码，这阻止了大多数重排序。但这里的重点在于<em>大多数</em>：如果编译器有额外的别名信息，例如来自 <code>&amp;mut</code> 类型，这能让编译器推理并在内存访问中重排序，即使跨越未知函数调用，因此也跨越内联汇编块。所以说 asm 块是阻止所有重排序的栅栏是错误的。以编译器栅栏的角度思考可以提供有用的直觉，但严谨的正确性论证需要更深入的细节。</p>

<p>这个故事中还有另一个注意事项：对于页表操作，不能只创建新的分配，也可以扩展现有分配。事实上，使用 <code>mmap</code>，从用户空间也可以做到同样的事情。结果证明<em>扩大</em>分配是无害的，因此这在 LLVM 中已<a href="https://github.com/llvm/llvm-project/pull/141338" rel="noopener noreferrer">正式认可</a>，我们应该找到在 Rust 端也公开这一点的方法。然而，<em>缩小</em>分配是有问题的——LLVM 可能合理进行的<a href="https://github.com/llvm/llvm-project/pull/141338" rel="noopener noreferrer">简单优化</a>会破坏缩小分配的代码！因此，需要进一步的工作来确保 Rust 代码（以及 C 和 C++ 代码）可以使用 <code>munmap</code> 而不会冒险导致错误编译。这就是为什么采取原则性的语言语义和正确性方法如此重要：否则，很容易错过这样的潜在问题。</p>

<h4 id="page-table-manipulation-ii-duplicating-pages">页表操作 II：复制页面</h4>

<p>接下来，让我们考虑另一个页表戏法：将单个物理内存页面映射到虚拟内存中的多个位置。这意味着该页面被“镜像”在多个地方，修改任何一个镜像都会更改所有镜像。首先，要注意这通常是不健全的。LLVM 会自由地假设 <code>ptr</code> 和 <code>ptr.wrapping_offset(4096)</code> 不别名，因此将相同内存映射到多个地方并自由访问所有地方可能导致微妙的错误编译。然而，有一种受限形式，我们可以使用内联汇编来构思一个符合抽象机的“故事”，因此是健全的。</p>

<p>关键的限制是程序一次只能使用此内存的“镜像”版本之一。更改哪个镜像是“活动的”需要一个显式屏障，并返回一个新的指针，该指针必须用于未来的访问。这个屏障可以是一个空的内联汇编块，只是返回未更改的指针，但我们附加给它的故事却非空：我们将说它的行为类似于 <code>realloc</code>，在逻辑上将分配从一个镜像移动到另一个。换句话说，就 Rust 抽象机而言，只有一个镜像版本的内存实际“存在”，切换到另一个意味着释放旧分配并创建新分配。关键在于，与 <code>realloc</code> 一样，在每次这样的切换后，所有指向该内存的旧指针都变为无效，而切换返回的新指针是访问该内存的唯一方式。<sup id="fnref:intptr"><a href="#fn:intptr" rel="noopener noreferrer">7</a></sup> 这些内联 asm 块还将阻止 LLVM 在不同的“镜像”访问之间重排序，从而避免了上述错误编译。换句话说，以一种让我们能够讲述恰当故事的方式修改我们的代码，也引入了足够的结构来防止优化器做它不应该做的事情。</p>

<p>这听起来可能有点牵强，但这种“纯逻辑的” <code>realloc</code> 确实出现在不止一种情况中；甚至有一个<a href="https://github.com/rust-lang/rfcs/pull/3700" rel="noopener noreferrer">正在讨论的 RFC</a> 提议将其添加到语言本身。</p>

<h4 id="non-temporal-stores">非时序存储</h4>

<p>前一个例子已经表明，有些硬件特性过于侵入性，无法在 Rust 这样的高级语言中自由使用。非时序存储是另一个例子。具体来说，我指的是 x86 上的“流式”存储操作（<code>_mm_stream_ps</code> 及类似指令）。这些操作的主要目的是避免用可能很快不会再被读取的数据塞满缓存，但它们也有一个不幸的副作用：破坏了 x86 通常的“全存储顺序”内存模型。这是个坏消息，因为程序其余部分的编译依赖于该内存模型。</p>

<p>为了解释这个问题，让我们考虑一个非时序存储的“故事”可能是什么。显而易见的选择是让它只是一个常规的写访问——毕竟缓存并未在抽象机中建模。不幸的是，这行不通。考虑流式存储后跟一个原子释放写入的情况。由于 x86 的全存储顺序模型，这会被编译成没有任何额外栅栏的普通写入指令。然而，流式存储实际上<em>确实</em>需要一个栅栏（<code>_mm_sfence</code>）来正确同步。因此，可以编写一个看似无数据竞争（根据故事）但实际有数据竞争的 Rust 程序。换句话说，违反了规则 3（内联 asm 块必须细化故事代码）。</p>

<p>对此的原则性修复是扩展 C++ 内存模型（Rust 共享该模型），加入非时序存储的概念，以便能够推理它们与并发程序中可能发生的所有其他事情的交互。这<a href="https://people.mpi-sws.org/~viktor/papers/oopsla2024-inline.pdf" rel="noopener noreferrer">是可能的</a>，但它需要重新证明编译器正确性结果，而且至少在该论文中采取的方法是针对特定架构的，无法扩展到 Rust 支持的众多架构。然而，有一个更简单的替代方案：我们可以尝试构思一个更复杂的故事，使得规则 3 不被违反。这正是当人们发现非时序存储相关问题时所做的事情。故事是，进行非时序存储对应于<em>生成一个线程</em>，该线程将异步执行实际的存储，而 <code>_mm_sfence</code> 对应于等待所有这些线程完成。这解释了为什么释放-获取同步会失败：同步会获取释放线程执行的所有写入，但流式存储在概念上是在另一个线程上发生的！这个新故事代码成为了更新后的 x86 流式存储<a href="https://doc.rust-lang.org/nightly/core/arch/x86/fn._mm_sfence.html" rel="noopener noreferrer">文档</a>的基础，代码本身甚至可以在<a href="https://github.com/rust-lang/rust/blob/cb3046e5f2f0736366c0fea4977a8df579d96311/library/stdarch/crates/core_arch/src/x86/sse.rs#L1456-L1481" rel="noopener noreferrer">代码注释中</a>找到。</p>

<p>有一个注意事项：我们选择的故事意味着执行流式存储的线程在 <code>_mm_sfence</code> 之前从该内存进行加载是未定义行为（UB），即使此操作在底层硬件上是定义良好的。这是我们为拥有一个关于使用流式存储的代码不会被错误编译的原则性论证所付出的代价。这个代价并不高：流式存储用于可能很快不会再被读取的数据，这就是它们的全部意义。我们在野外发现的所有流式存储示例都没有遇到这个限制的问题。<sup id="fnref:forgotten-fence"><a href="#fn:forgotten-fence" rel="noopener noreferrer">8</a></sup></p>

<h4 id="stack-painting">栈绘制</h4>

<p>内联汇编的另一个可能用途是使用栈绘制来测量程序的栈消耗量。这在 t-opsem Zulip 频道中被提出作为一个问题，我把它放在这里是因为它很好地展示了讲故事方法提供了多少自由度，以及它有哪些限制。</p>

<p>粗略地说，栈绘制意味着在程序开始之前，将稍后将成为栈的内存区域填充为固定的比特模式。之后，我们可以通过检查比特模式仍然完好的位置以及被覆盖的位置来测量程序的最大栈使用量。这可以使用直接读取栈的内联汇编代码来完成。</p>

<p>第一反应可能是说这显然是 UB：那块栈内存可能受到无别名约束（由于一个指向栈的可变引用）；你不能直接读取你没有权限读取的内存。然而，这预设了这个 asm 块的故事涉及读取内存。一个替代的故事是说这个 asm 块只是返回某个任意的、非确定性选择的值。这个故事的优点是，只要读取不陷入陷阱，根据我们的规则，故事总是正确的：无论汇编代码实际做什么，它肯定细化了返回一个任意值。然而，这个故事的缺点是，当推理我们的代码时，我们不能对读取的值做出<em>任何</em>假设！我们程序的正确性是在讲故事语义下定义的，即程序必须无论内联 asm 返回什么值都是正确的。这听起来可能是个问题，但在这个用例中，它实际上完全没问题：栈绘制反正只提供真实栈使用量的估计值。编译器不<em>保证</em>以这种方式产生的测量是大致准确的，但实验表明这在实践中效果很好。不准确的测量不会导致健全性或正确性问题，因此提供准确答案“只是”一个生活质量问题。</p>

<h4 id="floating-point-status-and-control-register">浮点状态和控制寄存器</h4>

<p>我最后想考虑的例子是浮点状态和控制寄存器。这是一个讲故事方法主要用来解释为什么使用这些寄存器是不可能或没有用处的例子。</p>

<p>程序员有时想读取状态寄存器以检查是否发生了浮点异常，并写入控制寄存器以调整舍入模式或浮点计算的其他方面。然而，实际支持这样的控制寄存器更改对优化来说是灾难性的：控制寄存器是全局（好吧，是线程局部的）状态，这意味着它会影响所有后续操作，直到寄存器再次被更改。这意味着为了优化任何可能需要舍入的浮点操作，编译器必须静态预测控制寄存器的值将是什么。这通常不太可能，因此编译器通常改为假设控制寄存器始终保持在其默认状态。（有时它们提供退出该假设的方法，但这很难做好，Rust 目前没有相关设施。）状态寄存器问题不那么明显，但请注意，如果我们说浮点操作可以修改状态寄存器，那么它就不再是一个纯操作，因此不能自由地重排序。为了让编译器能够对浮点操作进行像公共子表达式消除这样的基本优化，语言通常也认为状态寄存器是不可观察的。</p>

<p>这对读取/写入这些寄存器的内联汇编代码意味着什么？对于读取状态寄存器，这意味着故事代码无法说出这与实际的浮点操作有任何关系。抽象机中没有故事代码可以读取的浮点状态位，因此最好的故事是返回一个非确定性的值。这直接反映了编译器不会对程序在状态寄存器中观察到的值做任何保证这一事实，并且由于浮点操作可以任意重排序，这应该被相当字面地理解。</p>

<p>对于写入控制寄存器，根本没有可能的故事：没有任何 Rust 操作会改变后续浮点操作的舍入模式。因此，任何更改舍入模式的内联 asm 块都具有未定义行为（同样适用于其他改变 Rust 编译器使用的指令行为的标志，如将非规格化数刷新为零）。</p>

<p>虽然这听起来令人沮丧，但完全有可能编写一个内联 asm 块，它更改舍入模式，执行一些浮点操作，然后将其更改回来！这个块的故事代码可以使用软浮点库来执行与非默认舍入模式下完全相同的浮点操作。关键是，由于 asm 块整体上没有改变控制寄存器，故事代码甚至不需要担心那个寄存器。换句话说，有一个执行浮点操作的大 asm 块，使用非默认舍入模式，这是可以的。从优化的角度来看，这也说得通：没有风险将浮点操作移动到舍入模式不同的代码区域。</p>

<h2 id="conclusion">结论</h2>

<p>我希望这些例子有助于展示讲故事方法的灵活性和局限性。在许多情况下，无法构思出一个故事直接对应于潜在的错误编译。这很好！那些是我们<em>必须</em>规定为不正确的内联 asm 块。<sup id="fnref:noopt"><a href="#fn:noopt" rel="noopener noreferrer">9</a></sup> 然而，在某些情况下，没有明显的错误编译。而且确实，如果我们确切知道编译器依赖于哪些 Rust 程序的普遍属性，我们可以允许满足所有这些普遍属性的内联 asm 代码，即使它没有可以表示为 Rust 源代码的故事。不幸的是，这种方法要求我们承诺编译器可能使用的全部普遍属性集合。如果我们明天发现一个新的普遍属性，我们就不能使用它，因为可能有一个内联 asm 块不满足该普遍属性。</p>

<p>这就是为什么我建议采取保守的方法：只允许那些显然与所有实际 Rust 代码的普遍属性兼容的内联 asm 块，因为它们的故事可以表达为实际的 Rust 代码。如果我们想允许某个操作，而它目前没有有效的故事，我们只需<a href="https://github.com/rust-lang/rfcs/pull/3700" rel="noopener noreferrer">添加</a>一个<a href="https://github.com/rust-lang/rfcs/pull/3605" rel="noopener noreferrer">新的语言操作</a>，这相当于正式认可该操作是编译器将继续尊重的操作。</p>

<p>目前，我们没有关于内联 asm 块和 FFI 如何与 Rust 级别的 UB 交互的官方文档或指南，但正如文章顶部的 <code>innocent</code> 示例所示，我们不能让内联 asm 块像那样不受约束。讲故事的方法是我为填补这一空白而提出的建议。我计划最终将其作为内联汇编的官方规则提出。但在那之前，我想更确信这种方法真的能处理大多数现实场景。如果你有汇编块无法用讲故事解释的例子，但你确信它们是正确的，因此应该被支持，请告诉我们，可以在本文的即时<a href="https://www.reddit.com/r/rust/comments/1rshm93/how_to_use_storytelling_to_fit_inline_assembly/" rel="noopener noreferrer">讨论</a>中（或者如果你是在稍后阅读此内容）在<a href="https://rust-lang.zulipchat.com/#narrow/channel/136281-t-opsem" rel="noopener noreferrer">t-opsem Zulip 频道</a>中。</p>

<h4 id="footnotes">脚注</h4>
<div>
  <ol>
    <li id="fn:xlang">
      <p>FFI 有一个额外的复杂性，这在内联汇编中不会出现，那就是跨语言 LTO。那是另一回事，超出了本文的范围。 <a href="#fnref:xlang" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:nondet">
      <p>秘密的第三种选择是程序可能是非确定性的，允许两种行为，但这肯定不适用于此处。 <a href="#fnref:nondet" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:cheri">
      <p>我已经感觉到有些人想用 CHERI 作为明显的反例。CHERI 有能力性（capabilities），它们看起来和感觉有点像指针来源，但它们对于树形借用来说还不够精细，因此能力和来源仍然是不同的概念，不应混淆。 <a href="#fnref:cheri" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:alice">
      <p>感谢 Alice Ryhl 建议使用“讲故事”这个术语。 <a href="#fnref:alice" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:volatile">
      <p>为什么我坚持这里的 volatile 访问？因为如果你把页表放在一个普通的 Rust 分配中，对该页表的写入可能产生“有趣”的效果，而这与写入普通 Rust 分配时可能发生的情况并不真正对应。换句话说，我（还）没有构思出一个合适的故事来允许这些写入是非 volatile 的。 <a href="#fnref:volatile" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:alloc-control">
      <p>这假设我们细化了 Rust 中内存分配方式的规范，使得存在一些内存区域，“原生” Rust 分配（如栈和静态变量）不使用它们，而是完全由程序控制。如果语言唯一的分配操作是“在地址空间中任意位置的非确定性分配”，这个故事就行不通。 <a href="#fnref:alloc-control" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:intptr">
      <p>指向复制内存的长寿指针不太行，因为它们可能指向错误的副本。但如果可以避免这一点，那么你只需将它们存储为整数，并在每次访问时<a href="https://doc.rust-lang.org/std/ptr/fn.with_exposed_provenance.html" rel="noopener noreferrer">将它们转换为指针</a>；这避免了任何长寿的指针来源，从而防止编译器应用基于分配的常规推理来处理此内存。 <a href="#fnref:intptr" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:forgotten-fence">
      <p>我们发现的所有示例都忘记插入 <code>_mm_sfence</code>，这显然是不健全的。多亏了这个故事，我们现在清楚地知道<em>为什么</em>它是不健全的，即违反了 Rust 语言的哪条规则。 <a href="#fnref:forgotten-fence" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:noopt">
      <p>这假设我们不想牺牲这些优化。由于内联汇编可能隐藏在任何函数调用中，这通常会成为一种语言范围内的权衡：要么我们禁止此类内联 asm 块，要么我们不能在纯 Rust 代码中进行这种优化。 <a href="#fnref:noopt" rel="noopener noreferrer">↩</a></p>
    </li>
  </ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 16094 tokens</em></p>]]></content:encoded>
      <link>https://www.ralfj.de/blog/2026/03/13/inline-asm.html</link>
      <guid isPermaLink="false">https://www.ralfj.de/blog/2026/03/13/inline-asm.html</guid>
      <pubDate>Thu, 12 Mar 2026 23:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Miri（还有，我们发表了一篇关于Miri的论文！）有什么“新”内容？</title>
      <category>rust</category>
      <description>[AI 摘要] 本文更新了Miri未定义行为检测工具的最新进展，包括新增的垫片支持、诊断改进、性能优化、并发支持增强，并宣布相关论文被POPL 2026会议收录。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文更新了Miri未定义行为检测工具的最新进展，包括新增的垫片支持、诊断改进、性能优化、并发支持增强，并宣布相关论文被POPL 2026会议收录。</div><p>又到了写一篇“Miri近况如何”博文的时候了。
事实上，这确实<em>拖得太久了</em>，上一次<a href="/blog/2022/07/02/miri.html" rel="noopener noreferrer">更新</a>已经是三年前的事了（时间到底是什么东西？！），但确实越来越难找到时间写博客了，所以……我们就这样吧。
迟到总比不到好。:)</p>

<p>对于不熟悉的人，<a href="https://github.com/rust-lang/miri/" rel="noopener noreferrer">Miri</a> 是一个用于Rust的<a href="https://doc.rust-lang.org/reference/behavior-considered-undefined.html" rel="noopener noreferrer">未定义行为</a>测试工具。
这意味着它可以发现你unsafe代码中的bug，这些bug未能遵守诸如“所有访问必须对齐”、“可变引用绝不能别名”或“绝对不能有数据竞争”之类的要求。
Miri的成名之处在于它是一个实用工具，能够发现<em>所有确定性Rust程序中事实上的未定义行为</em>。
据我所知，没有其他免费工具能做到这一点——对任何语言来说都是如此。<sup id="fnref:relwork"><a href="#fn:relwork" rel="noopener noreferrer">1</a></sup></p>

<p>我们只能谈论<em>事实上的未定义行为</em>，因为Rust尚未稳定其未定义行为的定义。
为此，我们仔细检查编译器的行为，尽最大努力确保Rust程序<em>当前</em>可能遇到的所有未定义行为都被Miri捕获。
这意味着通过Miri测试的程序应该能在<em>当前</em>的编译器上正确编译，但同一个程序在未来可能会遭受未定义行为。
此外，如果Rust程序是<em>非确定性</em>的，意味着它可以有多种执行方式，而Miri只会执行一次。
你可以使用 <code>-Zmiri-many-seeds</code> 让Miri随机探索多种可能的执行，但总可能还有Miri尚未发现的执行路径。
这是所有测试工具的根本局限；你通常需要借助模型检查或演绎验证来克服。</p>

<p>要了解更多关于Miri的信息，你可以阅读这篇<a href="https://plf.inf.ethz.ch/research/popl26-miri.html" rel="noopener noreferrer"><strong>论文</strong></a>。
是的，我们有一篇论文！这是第一个新闻。
<em>《Miri：Rust实用未定义行为检测》</em>已被POPL 2026接收，这是编程语言基础研究领域最负盛名、竞争最激烈的会议之一。</p>

<p><strong>更新（2026-02-04）：</strong> 会议关于此论文的演讲<a href="https://www.youtube.com/watch?app=desktop&amp;v=9A8ZeDIStAs" rel="noopener noreferrer">录像现已上线</a>。</p>

<h2 id="miri-progress">Miri 进展</h2>

<p>论文就说到这。过去三年里Miri取得了哪些进展？
我们在此期间合并了超过1500个PR，不可能详述所有细节，但我会尽力概述总体趋势并指出一些重大事项。</p>

<h3 id="shims">垫片</h3>

<p>为Miri添加的新功能主要是为在Rust外部实现的函数添加垫片，因此这些函数无法被Miri直接执行。
这主要涉及操作系统API以及CPU厂商提供的内部函数。
以下列表尝试总结自上次更新以来为Miri添加的垫片：</p>

<ul>
  <li>极大扩展了Windows API垫片的支持，特别涵盖基本文件访问（由 @beepster4096, @CraftSpider 完成）。</li>
  <li>支持Unix（特别是Linux）上各种新的文件描述符类型，例如 <code>socketpair</code>（仅 <code>SOCK_STREAM</code>）、<code>pipe</code> 和 <code>eventfd</code>（由 @DebugSteven, @tiif, @RalfJung, @FrankReh 完成）。</li>
  <li>支持 Linux 的 <code>epoll</code>（由 @tiif 主导，并得到 @DebugSteven, @FrankReh, @RalfJung 的一些基础工作和扩展支持）。</li>
  <li>拓宽通用文件API支持（由 @Pointerbender, @Jefffrey, @tiif, @newpavlov 完成）。</li>
  <li>支持许多Intel厂商内部函数，涵盖从SSE2到AVX2（主要由 @eduardosm 完成，并得到 @TDecking, @Kixunil 的帮助）。感谢 @folkertdev，Miri甚至支持一些AVX-512内部函数，使其成为你可能无法在真实硬件上运行的代码的合适<a href="https://trifectatech.org/blog/emulating-avx-512-intrinsics-in-miri/" rel="noopener noreferrer">测试平台</a>。</li>
  <li>支持FreeBSD上的基本功能（由 @devnexen 和 @LorrensP-2158466 完成）。</li>
  <li>支持Illumos和Solaris上的基本功能（由 @devnexen 完成）。</li>
  <li>支持Android上的基本功能（由 @YohDeadfall 完成）。</li>
  <li>改进pthread同步操作的垫片（由 @Mandragorian, @LorrensP-2158466, @RalfJung 完成）。</li>
  <li>扩展了对macOS特定API的支持（由 @joboet 完成）。</li>
  <li>支持弱定义（由 @bjorn3 完成）。</li>
  <li>支持各种小型系统API（由 @folkertdev, @Mandragorian, @tgross35, @rayslava, @LorrensP-2158466, @YohDeadfall, @vishruth-thimmaiah, @saethlin, @RalfJung 完成）。</li>
  <li>支持在 <code>main</code> 之前执行的全局构造函数（由 @ibraheemdev 完成）。</li>
</ul>

<h3 id="diagnostics">诊断信息</h3>

<p>自上次博文以来，我们的诊断能力有了显著提高，这主要归功于 @saethlin。
例如，数据竞争错误现在会指出导致竞争的两个访问：</p>
<div><div><pre><code>error: Undefined Behavior: Data race detected between (1) non-atomic read on thread `unnamed-1` and (2) non-atomic write on thread `unnamed-2` at alloc87
  --&gt; tests/fail/data_race/read_write_race.rs:24:13
   |
24 | ...   *c.0 = 64; //~ ERROR: Data race detected between (1) non-atomic read on thread `unnamed-1` and (2) non-atomic write on thread ...
   |       ^^^^^^^^^ (2) just happened here
   |
help: and (1) occurred earlier here
  --&gt; tests/fail/data_race/read_write_race.rs:19:24
   |
19 |             let _val = *c.0;
   |                        ^^^^
</code></pre></div></div>
<p>释放后使用错误会显示指针指向的内存分配的创建和释放位置：</p>
<div><div><pre><code>error: Undefined Behavior: memory access failed: alloc194 has been freed, so this pointer is dangling
 --&gt; tests/fail/dangling_pointers/dangling_pointer_deref.rs:9:22
  |
9 |     let x = unsafe { *p }; //~ ERROR: has been freed
  |                      ^^ Undefined Behavior occurred here
  |
help: alloc194 was allocated here:
 --&gt; tests/fail/dangling_pointers/dangling_pointer_deref.rs:6:17
  |
6 |         let b = Box::new(42);
  |                 ^^^^^^^^^^^^
help: alloc194 was deallocated here:
 --&gt; tests/fail/dangling_pointers/dangling_pointer_deref.rs:8:5
  |
8 |     };
  |     ^
</code></pre></div></div>
<p>Stacked Borrows错误会显示相关指针的创建位置以及失效位置：</p>
<div><div><pre><code>error: Undefined Behavior: attempting a write access using &lt;254&gt; at alloc115[0x0], but that tag does not exist in the borrow stack for this location
 --&gt; tests/fail/stacked_borrows/illegal_write2.rs:8:14
  |
8 |     unsafe { *target2 = 13 }; //~ ERROR: /write access .* tag does not exist in the borrow stack/
  |              ^^^^^^^^^^^^^ this error occurs as part of an access at alloc115[0x0..0x4]
  |
help: &lt;254&gt; was created by a SharedReadWrite retag at offsets [0x0..0x4]
 --&gt; tests/fail/stacked_borrows/illegal_write2.rs:5:19
  |
5 |     let target2 = target as *mut _;
  |                   ^^^^^^
help: &lt;254&gt; was later invalidated at offsets [0x0..0x4] by a Unique retag
 --&gt; tests/fail/stacked_borrows/illegal_write2.rs:6:10
  |
6 |     drop(&amp;mut *target); // reborrow
  |          ^^^^^^^^^^^^
</code></pre></div></div>

<p>@Vanille-N 为Tree Borrows实现了类似的跟踪，因此其错误输出质量与Stacked Borrows相当。
@Zoxc 也贡献了改进涉及别名模型的数据竞争错误的逻辑。</p>

<h3 id="optimizations">性能优化</h3>

<p>Miri的速度仍然不算快，但一些性能工作显著提升了Miri的别名检查速度：</p>
<ul>
  <li>@saethlin 为Miri添加了指针标签的垃圾回收器，使得Stacked Borrows可以跳过大量与跟踪已不存在指针相关的工作。</li>
  <li>@JojoDeveloping 为Tree Borrows检查器添加了各种优化。</li>
</ul>

<h3 id="improved-concurrency-support">改进的并发支持</h3>

<p>Miri中的数据竞争检查器和弱内存支持最初基于一篇遵循C++11并发语义的论文。
然而，Rust指定使用C++20语义，这需要进行一些调整。
@cbeuw 完成了大部分工作，并得到 @SabrinaJewson 和 @michaliskok 的帮助。
（有关更多细节，请参见<a href="https://plf.inf.ethz.ch/research/popl26-miri.html" rel="noopener noreferrer">论文第4节</a>。）
作为撰写论文的一部分，我还在弱内存实现的核心部分发现并修复了两个缺陷。</p>

<p>此外，@geetanshjuneja 调整了Miri的调度器以实现完全非确定性，使得发现轮询调度不会出现的问题成为可能。
Furthermore, @pvdrz 在调度器中增加了完全“虚拟”计时支持，使Miri能够以完全确定性的方式支持单调时钟。
我还让Miri正确执行了只读内存中原子访问的限制（这在大多数情况下是禁止的，但有少数例外）。</p>

<p>最后，@Patrick-6 为将GenMC集成到Miri中奠定了基础。
GenMC是@michaliskok编写的弱内存模型检查器，这意味着它可以枚举并发程序的<em>所有</em>行为（只要程序没有无限循环）。
通过将它与Miri结合，我们可以对所有这些执行路径进行完整的未定义行为检查。
（我在引言中提到模型检查，确实是在为此铺垫。:))
目前，使用Miri+GenMC仍然是高度实验性的、速度缓慢的，并且需要自定义构建Miri，但第一步已经迈出，我对这种组合未来的潜力感到非常兴奋！</p>

<h3 id="invoking-native-code-from-miri">从Miri调用原生代码</h3>

<p>你知道Miri可以通过FFI执行从Rust调用的原生代码吗？
这种支持非常实验性且不完整，显然原生代码的运行没有任何未定义行为检查，但自上次更新以来取得了显著改进：</p>

<ul>
  <li>@Strophox 实现了与原生代码共享Rust分配内存的支持。</li>
  <li>@nia-e 添加了一些真正的魔法，让Miri能够相当精确地追踪原生代码访问了哪些内存，并利用这些信息改进Miri的未定义行为检查。我听说她计划未来让这个功能更加强大。:)</li>
</ul>

<h3 id="miscellaneous">其他杂项</h3>

<p>最后，我们有一些足够重要值得一提的贡献，但不符合上述任何类别：</p>

<ul>
  <li>内存泄漏检测器现在可以考虑主线程的线程本地存储（由 @max-heller 完成）。</li>
  <li>我们对文件描述符的表示变得更加灵活和可扩展（由 @Luv-Ray, @oli-obk, @RalfJung 完成）。</li>
  <li>Miri现在使某些浮点运算的精度具有非确定性，以捕获错误依赖精确或确定性结果的代码（由 @LorrensP-2158466 完成）。</li>
  <li>Miri非确定性地使 <code>read</code> 和 <code>write</code> 操作只处理缓冲区的一部分，以捕获错误依赖这些操作可靠立即完成的程序（由 @RalfJung 完成）。</li>
  <li>Tree Borrows现在默认以与Stacked Borrows相同的精度跟踪 <code>UnsafeCell</code>，当同一引用背后的其他字节被错误修改时捕获未定义行为（由 @yoctocell, @JojoDeveloping 完成）。</li>
  <li>Tree Borrows现在支持通配符来源，因此Miri可以检查使用整数到指针转换的程序，并仍然捕获涉及这些指针的一些错误（由 @royAmmerschuber 完成）。</li>
  <li>Miri可以检测与就地（“移动”）函数参数相关的未定义行为（由 @RalfJung 完成）。</li>
  <li>Miri支持精确性能分析，跟踪所有执行时间消耗在何处（由 @Stypox 完成）。</li>
  <li>Miri不再需要xargo，减少了设置工作量（由 @RalfJung 完成）。</li>
</ul>

<p>除此之外，还有大量的错误修复以及持续的重构和清理，以保持代码可维护性。
感谢所有贡献者。
如果你的名字应该在这个名单上，那很抱歉我忘了你。</p>

<h2 id="how-you-can-help">如何提供帮助</h2>

<p>如果你想帮助改进Miri，那太棒了！
<a href="https://github.com/rust-lang/miri/issues" rel="noopener noreferrer">问题跟踪器</a>是一个很好的起点；问题列表很短，你可以快速浏览一遍，看看是否有任何感兴趣的内容。
特别适合入门的问题标有绿色标签。
另一个好的起点是尝试实现缺失的功能，以让你的测试套件能够工作。
不过，你应该在一些更简单的项目中积累一些Rust经验后再处理Miri；Miri不是一个适合Rust初学者的好代码库。
然而，如果你已经了解Rust，Miri可能是一个有趣且有成就感的下一个挑战！
如果你需要任何指导，只需<a href="https://rust-lang.zulipchat.com/#narrow/stream/269128-miri" rel="noopener noreferrer">联系我们</a>。:)</p>

<p>我们目前在寻找愿意维护Miri对wasm目标支持的人。
wasm API与其他操作系统API差异很大，如果没有真正了解和理解wasm生态系统的人的帮助，维护这些垫片变得不可持续。
此外，我们也在寻找有Android经验的人来担任Miri的Android目标维护者。
这主要是指修复因标准库变更而出现的Android特定问题——这种情况应该很少，但发生时有一个可以联系的人会非常有用。
Android也几乎通过了我们的整个测试套件，你可以将其作为挑战来修复剩余的部分。</p>

<p>就到这里吧！
我对Miri的成就感到无比自豪，并深深感谢每一位帮助我们走到这里的人。
多亏了Miri以及其他基于Miri对未定义行为理解的工具，在大规模项目中避免unsafe代码中的未定义行为变得切实可行，同时也让我们有机会以满足现实世界unsafe代码需求的方式来定义未定义行为。
这比我想象的要成功得多，
如果没有整个优秀的Rust社区的支持，这是不可能实现的——我期待着接下来会发生什么。:D</p>
<div>
  <ol>
    <li id="fn:relwork">
      <p>请让我知道是否有这样的工具而我只是错过了！论文讨论了为什么sanitizers和valgrind虽然非常有用，但仍然会遗漏一些未定义行为。我只知道有一个商业工具能做出与Miri类似的声明，即“TrustInSoft Analyzer”。它需要许可证，所以我无法说明它对C标准的覆盖程度；特别是，比较GCC和clang认为什么是未定义行为与该工具认为什么是未定义行为会很有趣。在Miri中，我们花了很多时间与编译器团队讨论，以确保我们对什么是什么不是未定义行为有共同的理解。原则上，在C语言中这应该是不必要的，因为它有标准；但实践中，标准可能与程序员的预期以及编译器的实现<a href="https://dl.acm.org/doi/10.1145/2908080.2908081" rel="noopener noreferrer">存在很大差异</a>。<a href="#fnref:relwork" rel="noopener noreferrer">↩</a></p>
    </li>
  </ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 8942 tokens</em></p>]]></content:encoded>
      <link>https://www.ralfj.de/blog/2025/12/22/miri.html</link>
      <guid isPermaLink="false">https://www.ralfj.de/blog/2025/12/22/miri.html</guid>
      <pubDate>Sun, 21 Dec 2025 23:00:00 +0000</pubDate>
    </item>
    <item>
      <title>无线程安全，便无内存安全</title>
      <category>programming</category>
      <category>rust</category>
      <description>[AI 摘要] 本文论证了线程安全是真正内存安全的必要条件，并以Go语言为例，说明数据竞争如何导致内存破坏。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文论证了线程安全是真正内存安全的必要条件，并以Go语言为例，说明数据竞争如何导致内存破坏。</div><p>内存安全如今风头正劲。
但这个词究竟意味着什么？
事实证明，其定义可能比你想象的更难界定。
通常，人们用这个术语来指代那些确保程序中不存在释放后使用或越界内存访问的语言。
这常被视为与其他安全概念（如线程安全）的区别，后者指的是程序不存在某些并发错误。
然而，本文将论证这种区分意义不大，我们真正希望程序拥有的特性是<em>消除未定义行为</em>。</p>



<h2 id="breaking-memory-safety-with-a-data-race">通过数据竞争破坏内存安全</h2>

<p>我对安全概念被细分为内存安全、线程安全等细粒度类别存在一个主要疑问：一个线程不安全的语言无法以任何有意义的方式提供内存安全。
要理解我的意思，请看这个用Go编写的程序，根据<a href="https://en.wikipedia.org/wiki/Go_(programming_language)" rel="noopener noreferrer">维基百科</a>，Go是内存安全的：</p>
<div><div><pre><code><span>package</span> <span>main</span>

<span>// 仅作为一个任意接口，以便后续使用接口类型。</span>
<span>type</span> <span>Thing</span> <span>interface</span> <span>{</span>
    <span>get</span><span>()</span> <span>int</span>
<span>}</span>

<span>// 两种实现接口的类型，具有非常不同类型的字段。</span>
<span>type</span> <span>Int</span> <span>struct</span> <span>{</span>
    <span>val</span> <span>int</span>
<span>}</span>
<span>func</span> <span>(</span><span>s</span> <span>*</span><span>Int</span><span>)</span> <span>get</span><span>()</span> <span>int</span> <span>{</span>
    <span>return</span> <span>s</span><span>.</span><span>val</span>
<span>}</span>

<span>type</span> <span>Ptr</span> <span>struct</span> <span>{</span>
    <span>val</span> <span>*</span><span>int</span>
<span>}</span>
<span>func</span> <span>(</span><span>s</span> <span>*</span><span>Ptr</span><span>)</span> <span>get</span><span>()</span> <span>int</span> <span>{</span>
    <span>return</span> <span>*</span><span>s</span><span>.</span><span>val</span>
<span>}</span>

<span>// 一个接口类型的全局变量，我们将在指向 `Int` 和 `Ptr` 之间反复切换。</span>
<span>var</span> <span>globalVar</span> <span>Thing</span> <span>=</span> <span>&amp;</span><span>Int</span> <span>{</span> <span>val</span><span>:</span> <span>42</span> <span>}</span>

<span>// 反复调用全局变量的接口方法。</span>
<span>func</span> <span>repeat_get</span><span>()</span> <span>{</span>
    <span>for</span> <span>{</span>
        <span>x</span> <span>:=</span> <span>globalVar</span>
        <span>x</span><span>.</span><span>get</span><span>()</span>
    <span>}</span>
<span>}</span>

<span>// 反复更改全局变量的动态类型。</span>
<span>func</span> <span>repeat_swap</span><span>()</span> <span>{</span>
    <span>var</span> <span>myval</span> <span>=</span> <span>0</span>
    <span>for</span> <span>{</span>
        <span>globalVar</span> <span>=</span> <span>&amp;</span><span>Ptr</span> <span>{</span> <span>val</span><span>:</span> <span>&amp;</span><span>myval</span> <span>}</span>
        <span>globalVar</span> <span>=</span> <span>&amp;</span><span>Int</span> <span>{</span> <span>val</span><span>:</span> <span>42</span> <span>}</span>
    <span>}</span>
<span>}</span>

<span>func</span> <span>main</span><span>()</span> <span>{</span>
    <span>go</span> <span>repeat_get</span><span>()</span>
    <span>repeat_swap</span><span>()</span>
<span>}</span>
</code></pre></div></div>
<p>如果你运行这个程序（例如在<a href="https://go.dev/play/p/SC-o_Q8e-aK" rel="noopener noreferrer">Go playground</a>上），它很快就会崩溃：</p>
<div><div><pre><code>panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x2a pc=0x468863]
</code></pre></div></div>
<p>这是一个段错误（segfault），而非正常的Go panic，说明发生了严重的错误。
请注意，导致段错误的地址是<code>0x2a</code>，这是数字42的十六进制表示。
这是怎么回事？</p>

<p>这个例子利用了Go存储像<code>Thing</code>这样的接口类型值时，实际上是存储一个指向数据的指针和一个指向虚表（vtable）的指针对。
每次<code>repeat_swap</code>向<code>globalVar</code>存储新值时，它只是执行两次独立的存储操作来更新这两个指针。
因此在<code>repeat_get</code>中，当我们*在*那两次存储*之间*读取<code>globalVar</code>时，就存在一个小概率会得到一个指向<code>Int</code>数据但使用<code>Ptr</code>虚表的混合指针。
当这种情况发生时，我们将运行<code>Ptr</code>版本的<code>get</code>方法，该方法会将<code>Int</code>的<code>val</code>字段解引用为一个指针——因此程序访问地址42并崩溃。</p>

<p>很容易将这个例子改造成一个将整数转换为指针的函数，然后造成任意的内存损坏。</p>

<h2 id="what-about-other-languages">其他语言呢？</h2>

<p>此时你可能会想，这不是在许多语言中都存在的问题吗？
Java不也允许数据竞争吗？
是的，Java确实允许数据竞争，但Java开发者付出了大量努力以确保即使存在数据竞争的程序也完全定义良好且内存安全。
他们甚至为此开发了<a href="https://en.wikipedia.org/wiki/Java_memory_model" rel="noopener noreferrer">首个在工业中部署的并发内存模型</a>，这比C++11内存模型早了许多年。
所有这些工作的结果是，在并发的Java程序中，你可能会看到某些变量的意外过期值，例如在你期望引用已被正确初始化的地方出现一个空指针，但你*永远*无法真正破坏语言并解引用一个无效的悬垂指针，然后在地址<code>0x2a</code>处段错误。
从这个意义上说，所有Java程序都是线程安全的。<sup id="fnref:java-safe"><a href="#fn:java-safe" rel="noopener noreferrer">1</a></sup></p>

<p>通常，语言可以采取两种方案来确保并发不会破坏内存安全：</p>
<ul>
  <li>确保任意并发程序仍然遵守类型规则和关键的语言不变式。这代价高昂，因为它要求语言永不假设多字值的一致性，并限制编译器可以执行的优化。这是大多数语言采取的路线，从Java到C#、OCaml、JavaScript和WebAssembly。<sup id="fnref:multi-word"><a href="#fn:multi-word" rel="noopener noreferrer">2</a></sup></li>
  <li>拥有足够强大的类型系统以完全排除大多数访问上的数据竞争，并仅为一小部分内存访问承担安全处理竞争的成本。这是Rust首次实践的方法，Swift现在也通过其<a href="https://developer.apple.com/documentation/swift/adoptingswift6" rel="noopener noreferrer">“严格并发”</a>在采用此方法。</li>
</ul>

<p>遗憾的是，Go选择两者都不做。
这意味着严格来说，它不是一种内存安全的语言：该语言最多只能承诺，*如果*程序没有数据竞争（或者更具体地说，没有在接口、切片和映射等有问题的值上发生数据竞争），那么其内存访问永远不会出错。
公平地说，Go自带了开箱即用的数据竞争检测工具，可以快速发现我例子中的问题。
然而，在一个真实的程序中，这意味着你必须寄希望于你的测试套件能覆盖程序在实践中可能遇到的所有情况，而这*正是*强大的类型系统和静态安全保证旨在避免的那类问题。
因此，<a href="https://arxiv.org/pdf/2204.00764" rel="noopener noreferrer">数据竞争在Go中是个大问题</a>也就不足为奇了，并且有<a href="https://old.reddit.com/r/rust/comments/wbejky/a_succinct_comparison_of_memory_safety_in_rust_c/iid990t/?context=2" rel="noopener noreferrer">至少是传闻证据的实际内存安全违规</a>。
即使是经验丰富的Go程序员也并不总是意识到，你可以在不使用任何unsafe操作或利用任何编译器或语言bug的情况下破坏内存安全。
Go是一门*为*并发编程*而设计*的语言，所以人们并不期望存在这类隐患。
我认为这是一个有问题的盲点。</p>

<p>当然，正如语言设计中所有事情一样，这最终是一个权衡，Go的开发者们<a href="https://research.swtch.com/gorace" rel="noopener noreferrer">非常清楚这个问题</a>。<sup id="fnref:gosafe"><a href="#fn:gosafe" rel="noopener noreferrer">3</a></sup>
Go在这里做出了最简单的选择，这与该语言的整体设计完全一致。
这从根本上没有什么错。
然而，将Go归入与那些*确实*努力解决了数据竞争问题的语言<a href="https://www.memorysafety.org/docs/memory-safety/" rel="noopener noreferrer">同一个类别</a>，曲解了该语言的安全承诺。
<a href="https://go.dev/ref/mem" rel="noopener noreferrer">Go的内存模型文档</a>对此点也并未直言不讳：“非正式概述”强调“大多数竞争的结果是有限的”，并指出Go不同于“C和C++，其中任何存在竞争的程序的意义都是完全未定义的”。
你可以说这里的“大多数”是一个伏笔，但该节并未列出任何结果数量无限的情况，因此很容易错过这一点。
他们甚至声称Go“更像Java或JavaScript”，我认为这相当不公平，因为那些语言为实现它们所拥有的线程安全付出了巨大的努力。
只有<a href="https://go.dev/ref/mem#restrictions" rel="noopener noreferrer">后续一个小节</a>明确承认了Go中*某些*竞争*确实*具有完全未定义的行为（这与Java或JavaScript非常不同）。</p>

<h2 id="conclusion">结论</h2>

<p>我认为，人们在谈论内存安全时真正关心的特性是<em>程序无法破坏语言</em>。
所有这些由<a href="https://alexgaynor.net/2020/may/27/science-on-memory-unsafety-and-security/" rel="noopener noreferrer">内存安全违规导致的安全漏洞</a>，都是代码执行了在语言规范中甚至不可能的操作的情况，比如跳转到某个用户提供的数组并*将其作为汇编代码执行*。
我们通常用来形容破坏语言的程序的术语是<em>未定义行为</em>。
一旦你的程序有了UB，一切保证都将不复存在；攻击者随后是否能够控制这种UB如何显现并利用其获利，主要只是一个实现细节。<sup id="fnref:mitigations"><a href="#fn:mitigations" rel="noopener noreferrer">4</a></sup></p>

<p>在我看来，区分“安全”语言（程序不可能有UB）和“不安全”语言（程序可能有UB）有一条清晰的界限。
没有任何有意义的方式可以将此进一步细分为内存安全、线程安全、类型安全等等——你的程序有UB的*原因*并不重要，重要的是有UB的程序违背了语言本身的基本抽象，这是滋生漏洞的完美温床。
因此，如果一种语言不能系统地防止未定义行为，我们就不应称其为“内存安全”。</p>

<p>在实践中，安全性当然不是二元的，而是一个光谱，在这个光谱上，Go更接近典型的“安全”语言，而非C语言。
由数据竞争引起的UB，对于攻击者来说，其利用价值可能低于直接越界或释放后访问引起的UB，这是可信的。
但与此同时，我认为理解一种语言能可靠地提供哪些安全保证，以及权衡的模糊地带从何处开始，这很重要。
我的工作是<a href="https://research.ralfj.de/thesis.html" rel="noopener noreferrer"><em>证明</em></a>语言的安全声明，而对于Go，实际上能证明的东西并不多。
希望这篇文章能帮助你更好地理解不同语言所做选择的一些非平凡后果。<sup id="fnref:go"><a href="#fn:go" rel="noopener noreferrer">5</a></sup> :)</p>

<div>
  <ol>
    <li id="fn:java-safe">
      <p>Java程序员有时使用的“线程安全”和“内存安全”术语与C++或Rust程序员不同。从Rust的角度看，Java程序在设计上就是内存安全和线程安全的。Java程序员对此习以为常，以至于他们用同一个术语来指代更强的属性，例如没有“意外的”数据竞争或没有空指针异常。然而，这类bug无法因无效指针使用而导致段错误，因此这类问题与我的Go例子中的内存安全违规在性质上非常不同。就本文而言，我使用的是Rust和C++中的底层含义。 <a href="#fnref:java-safe" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:multi-word">
      <p>某些硬件支持大于指针大小的原子访问，这可用于确保多字值的一致性。然而，Go切片是三个指针大小的，据我所知，没有硬件支持*那么大*的原子访问。 <a href="#fnref:multi-word" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:gosafe">
      <p>我试图弄清楚Go开发者自己是否认为他们的语言是内存安全的，但未能得出确切的结论。Go网站没有对此事表态。在<a href="https://www.youtube.com/watch?v=rKnDgT73v8s&amp;t=463s" rel="noopener noreferrer">这个2009年的演讲</a>中，Rob Pike说内存安全是Go的目标，但在<a href="https://go.dev/talks/2012/splash.slide#49" rel="noopener noreferrer">这个2012年的幻灯片</a>中，他称该语言“不是纯粹的内存安全”，因为“共享是合法的”。 <a href="#fnref:gosafe" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:mitigations">
      <p>我意识到人们可以在这个“实现细节”上做*很多*事情；这基本上就是从基础的不可执行栈到花哨的控制流完整性等所有缓解技术所做的。但从原则和形式化的角度来看，这些都只是限制UB如何显现的各种形式。我们绝对应该继续这样做，但我们也应该尽一切可能防止UB的发生。 <a href="#fnref:mitigations" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:go">
      <p>如果你想知道我为什么在这里如此关注Go……嗯，我只是不知道还有其他任何声称是内存安全的语言，其内存安全却可以被数据竞争破坏。我最初想在多年前写这篇文章，当时Swift在这方面几乎和Go处于同一阵营，但Swift已经引入了“严格并发”，并加入了Rust的行列，成为那个使用花哨的类型系统技术处理并发问题的小圈子的一员。这很棒！不幸的是，对于Go，这意味着它是唯一一个我还能用来阐明观点的语言。本文并非意在抨击Go，而是旨在将语言中一个鲜为人知的弱点置于聚光灯下，因为我认为这是一个很有启发性的弱点。 <a href="#fnref:go" rel="noopener noreferrer">↩</a></p>
    </li>
  </ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 9531 tokens</em></p>]]></content:encoded>
      <link>https://www.ralfj.de/blog/2025/07/24/memory-safety.html</link>
      <guid isPermaLink="false">https://www.ralfj.de/blog/2025/07/24/memory-safety.html</guid>
      <pubDate>Wed, 23 Jul 2025 22:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Tree Borrows论文终于发表了</title>
      <category>research</category>
      <category>rust</category>
      <description>[AI 摘要] Tree Borrows论文已发表并获PLDI杰出论文奖，其形式化证明和广泛评估得到了认可。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> Tree Borrows论文已发表并获PLDI杰出论文奖，其形式化证明和广泛评估得到了认可。</div><p>经过数年的工作，我们的Tree Borrows论文最近终于在首尔举行的PLDI 2025大会上进行了展示。
与之前在这个博客和<a href="https://perso.crans.org/vanille/treebor/" rel="noopener noreferrer">Neven的网站</a>上提到的内容相比，Tree Borrows本身并没有太大变化。
我们利用所有额外的时间进行了<em>形式化证明</em>，以证明Tree Borrows确实允许我们期望从中获得的至少部分优化，并对crates.io上下载量最高的30,000个crate进行了广泛的Tree Borrows评估。
这一集实现、证明和评估于一体的综合成果给PLDI程序委员会留下了深刻印象，使我们获得了<em>杰出论文奖</em>。:-)
非常感谢Neven和Johannes的所有辛勤工作，并祝贺他们完成了一篇了不起的论文！</p>

<p>如果您想亲自查阅论文，所有内容均可<a href="https://plf.inf.ethz.ch/research/pldi25-tree-borrows.html" rel="noopener noreferrer">在开放获取下查看</a>。
Neven精彩的论文介绍演讲<a href="https://www.youtube.com/watch?v=CJi_Fcs4bak" rel="noopener noreferrer">可在此处找到</a>。</p><p><em>由 mimo-v2.5 模型翻译，花费 889 tokens</em></p>]]></content:encoded>
      <link>https://www.ralfj.de/blog/2025/07/07/tree-borrows-paper.html</link>
      <guid isPermaLink="false">https://www.ralfj.de/blog/2025/07/07/tree-borrows-paper.html</guid>
      <pubDate>Sun, 6 Jul 2025 22:00:00 +0000</pubDate>
    </item>
    <item>
      <title>MiniRust的当前状态</title>
      <category>research</category>
      <category>rust</category>
      <description>[AI 摘要] 该文介绍了演讲者在RustWeek上关于MiniRust作为unsafe Rust规约工具的当前进展的分享。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 该文介绍了演讲者在RustWeek上关于MiniRust作为unsafe Rust规约工具的当前进展的分享。</div><p>几周前，许多Rust爱好者在乌得勒支参加了RustWeek，我们都度过了愉快的时光。
作为活动的一部分，我做了一个题为“MiniRust：一个用于规约Rust的核心语言”的演讲，介绍了MiniRust的现状。
这是我在一个（满座的）电影院里第一次发表演讲；不幸的是，我的特效预算无法赶上通常在那里放映的节目水平。
不过，如果您想了解更多关于我如何规约unsafe Rust复杂细节的愿景，<a href="https://www.youtube.com/watch?v=yoeuW_dSe0o" rel="noopener noreferrer">请观看我的演讲视频</a>。 :)</p>

<p>感谢在场所有人的精彩聆听，也感谢组织者带来了精彩的一周和高质量的录像！</p><p><em>由 mimo-v2.5 模型翻译，花费 680 tokens</em></p>]]></content:encoded>
      <link>https://www.ralfj.de/blog/2025/07/02/minirust-talk.html</link>
      <guid isPermaLink="false">https://www.ralfj.de/blog/2025/07/02/minirust-talk.html</guid>
      <pubDate>Tue, 1 Jul 2025 22:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Rustlantis：基于随机化差异测试的Rust编译器模糊测试</title>
      <category>research</category>
      <category>rust</category>
      <description>[AI 摘要] 该论文介绍了通过随机生成MIR程序并跨后端/优化级别对比行为的方式，对Rust编译器进行模糊测试，从而发现了22个新缺陷。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 该论文介绍了通过随机生成MIR程序并跨后端/优化级别对比行为的方式，对Rust编译器进行模糊测试，从而发现了22个新缺陷。</div><p>我们团队产出的首篇论文最近在OOPSLA会议上发表。:)
这篇论文通过随机生成MIR程序，并确保这些程序在不同后端、不同优化级别以及Miri中行为一致，实现了对Rust编译器优化和代码生成阶段的模糊测试。
该工作的核心部分由Andy（王乾）在其<a href="https://ethz.ch/content/dam/ethz/special-interest/infk/inst-pls/plf-dam/documents/StudentProjects/MasterTheses/2023-Andy-Thesis.pdf" rel="noopener noreferrer">硕士论文</a>中完成。
这本已是一篇出色的论文，但Andy在开始全职工作后仍持续推进，最终成就了这篇非常优秀的论文。
他共计在Rust编译器中发现了22个新缺陷，其中12个位于已受到前人广泛模糊测试的LLVM后端。</p>

<p>要了解更多信息，请<a href="https://plf.inf.ethz.ch/research/oopsla24-rustlantis.html" rel="noopener noreferrer">查阅论文</a>或<a href="https://www.youtube.com/watch?v=kHYEHSHLffU&amp;t=20447s" rel="noopener noreferrer">观看Andy的演讲</a>（时间戳链接可能不稳定，若未自动跳转可手动定位至5小时40分处）。</p><p><em>由 mimo-v2.5 模型翻译，花费 988 tokens</em></p>]]></content:encoded>
      <link>https://www.ralfj.de/blog/2024/11/25/rustlantis.html</link>
      <guid isPermaLink="false">https://www.ralfj.de/blog/2024/11/25/rustlantis.html</guid>
      <pubDate>Sun, 24 Nov 2024 23:00:00 +0000</pubDate>
    </item>
    <item>
      <title>什么是位置表达式？</title>
      <category>programming</category>
      <category>rust</category>
      <description>[AI 摘要] 本文解释了 Rust 中位置表达式与值表达式的区别，以及隐式加载操作如何影响未定义行为。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文解释了 Rust 中位置表达式与值表达式的区别，以及隐式加载操作如何影响未定义行为。</div><p>Rust 语言中一个比较微妙的方面是，实际上存在两种表达式：<em>值表达式</em>和<em>位置表达式</em>。大多数时候，程序员不需要过多思考这种区别，因为 Rust 会在遇到一种表达式但期望另一种时，自动插入转换。然而，在编写 unsafe 代码时，正确理解这种表达式的二分法可能是必要的。请看以下<a href="https://play.rust-lang.org/?version=nightly&amp;mode=debug&amp;edition=2021&amp;gist=9a8802d20da16d6569510124c5827794" rel="noopener noreferrer">示例</a>：</p>

<div><div><pre><code><span>// 作为“packed”结构体，此类型的对齐要求为 1。</span>
<span>#[repr(packed)]</span>
<span>struct</span> <span>MyStruct</span> <span>{</span>
  <span>field</span><span>:</span> <span>i32</span>
<span>}</span>

<span>let</span> <span>x</span> <span>=</span> <span>MyStruct</span> <span>{</span> <span>field</span><span>:</span> <span>42</span> <span>};</span>
<span>let</span> <span>ptr</span> <span>=</span> <span>&amp;</span><span>raw</span> <span>const</span> <span>x</span><span>.field</span><span>;</span>
<span>// 这一行没问题。</span>
<span>let</span> <span>ptr_copy</span> <span>=</span> <span>&amp;</span><span>raw</span> <span>const</span> <span>*</span><span>ptr</span><span>;</span>
<span>// 但这一行有未定义行为 (UB)！</span>
<span>// `ptr` 是一个指向 `i32` 的指针，因此在内存访问时需要 4 字节对齐，</span>
<span>// 但 `x` 只有 1 字节对齐。</span>
<span>let</span> <span>val</span> <span>=</span> <span>*</span><span>ptr</span><span>;</span>
</code></pre></div></div>

<p>这里我使用了不稳定但<a href="https://github.com/rust-lang/rust/pull/127679" rel="noopener noreferrer">即将稳定</a>的“原始借用”运算符 <code>&amp;raw const</code>。你可能通过其稳定形式的宏 <code>ptr::addr_of!</code> 了解它，但 <code>&amp;</code> 语法使位置和值的交互更明确，因此我们在此使用它。</p>

<p>最后一行存在未定义行为（UB），因为 <code>ptr</code> 指向一个 packed 结构体的字段，其对齐不足。但为什么计算 <code>*ptr</code> 是 UB，而计算 <code>&amp;raw const *ptr</code> 却是正常的？一个表达式的求值应该先求值其子表达式，然后对结果进行某种操作。然而，<code>*ptr</code> 是 <code>&amp;raw const *ptr</code> 的子表达式，我们刚说 <code>*ptr</code> 是 UB，那么 <code>&amp;raw const *ptr</code> 不也应该 UB 吗？这就是本文讨论的主题。</p>



<p>（你可能在 C 和 C++ 中已经遇到过位置表达式和值表达式的区分，它们分别称为左值表达式和右值表达式。虽然基本的语法概念与 Rust 相同，但构成 UB 的具体场景不同，因此我们将完全聚焦于 Rust。）</p>

<h3 id="making-the-implicit-explicit">让隐式变得显式</h3>

<p>位置表达式和值表达式这种二分法之所以如此难以捉摸，主要原因是它完全是隐式的。因此，要理解上述代码中实际发生了什么，第一步是引入一些新语法，让我们能够在代码中明确这种隐式区分。</p>

<p>通常，我们可能会认为 Rust 表达式（的片段）的语法大致如下：</p>

<blockquote>
  <p><em>Expr</em> ::= <br>
&nbsp;&nbsp; <em>Literal</em> | <em>LocalVar</em> | <em>Expr</em> <code>+</code> <em>Expr</em> | <code>&amp;</code> <em>BorMod</em> <em>Expr</em> | <code>*</code> <em>Expr</em> | <br>
&nbsp;&nbsp; <em>Expr</em> <code>.</code> <em>Field</em> | <em>Expr</em> <code>=</code> <em>Expr</em> | … <br>
<em>BorMod</em> ::= <code>​</code> | <code>mut</code> | <code>raw</code> <code>const</code> | <code>raw</code> <code>mut</code> <br>
<em>Statement</em> ::= <br>
&nbsp;&nbsp; <code>let</code> <em>LocalVar</em> <code>=</code> <em>Expr</em> <code>;</code> | …</p>
</blockquote>

<p>这直接解释了为什么我们可以编写像 <code>*ptr = *other_ptr + my_var</code> 这样的表达式。</p>

<p>然而，要理解位置和值，考虑一个显式包含两种表达式的不同文法会更有启发性。我将首先给出文法，然后用一些例子进行解释：</p>

<blockquote>
  <p><em>ValueExpr</em> ::= <br>
&nbsp;&nbsp; <em>Literal</em> | <em>ValueExpr</em> <code>+</code> <em>ValueExpr</em> | <code>&amp;</code> <em>BorMod</em> <em>PlaceExpr</em> | <br>
&nbsp;&nbsp; <em>PlaceExpr</em> <code>=</code> <em>ValueExpr</em> | <code>load</code> <em>PlaceExpr</em> <br>
<em>PlaceExpr</em> ::= <br>
&nbsp;&nbsp; <em>LocalVar</em> | <code>*</code> <em>ValueExpr</em> | <em>PlaceExpr</em> <code>.</code> <em>Field</em> <br>
<em>Statement</em> ::= <br>
&nbsp;&nbsp; <code>let</code> <em>LocalVar</em> <code>=</code> <em>ValueExpr</em> <code>;</code> | …</p>
</blockquote>

<p><em>值表达式</em>是计算值的表达式：如 <code>5</code> 这样的字面量，像 <code>5 + 7</code> 这样的计算，也包括计算指针类型值的表达式如 <code>&amp;my_var</code>。然而，根据此文法，表达式 <code>my_var</code>（引用一个局部变量）<em>不是</em>值表达式，它是一个<em>位置表达式</em>。这是因为 <code>my_var</code> 实际上表示内存中的一个位置，可以对一个位置执行多种操作：可以加载该位置的内容（产生一个值），可以创建一个指向该位置的指针（也产生一个值，但根本不访问内存），或者可以将一个值存储到此位置（在 Rust 中产生 <code>()</code> 值，但更重要的是改变内存内容的副作用）。除了局部变量，位置表达式的另一个主要例子是 <code>*</code> 运算符的结果，它接受一个<em>值</em>（指针类型）并将其转换为一个位置。<sup id="fnref:deref"><a href="#fn:deref" rel="noopener noreferrer">1</a></sup> 此外，给定一个结构体类型的位置，我们可以使用字段投影来获取该字段的位置。</p>

<p>这可能听起来有些奇怪，因为这意味着 <code>let new_var = my_var;</code> 实际上在我们的文法中不是一个有效的语句！要接受此代码，Rust 编译器会自动将此语句转换为符合文法的形式，在需要的地方添加 <code>load</code>。<sup id="fnref:desugar"><a href="#fn:desugar" rel="noopener noreferrer">2</a></sup> <code>load</code> 接受一个位置，并如其名所示，执行从内存的加载以获取当前存储在该位置的值。因此，该语句的脱糖形式是 <code>let new_var = load my_var;</code>。</p>

<p>考虑一个更复杂的例子，上面提到的赋值表达式 <code>*ptr = *other_ptr + my_var</code> 脱糖为 <code>*(load ptr) = load *(load other_ptr) + load my_var</code>。这里有很多 <code>load</code> 表达式！说服自己相信它们都是必要的，才能使该项符合文法，这会很有启发性。特别是，<code>*</code> 作用于一个值表达式（因此我们需要 <code>load other_ptr</code> 来获取存储在此位置中的值），并产生一个位置表达式（因此我们需要再次 <code>load</code> 来获取一个可以与 <code>+</code> 一起使用值表达式）。然而，<code>=</code> 的左侧是一个位置表达式，所以我们不会在那里 <code>load</code> <code>*</code> 的结果。</p>

<p>由于 <code>load</code> 运算符是隐式引入的，它有时被称为“位置到值的强制转换”。理解位置到值的强制转换或 <code>load</code> 表达式在何处被引入，是理解本博文顶部示例的关键。所以让我们使用我们更明确的文法，再次编写该示例的相关部分：</p>
<div><div><pre><code><span>let</span> <span>ptr</span> <span>=</span> <span>&amp;</span><span>raw</span> <span>const</span> <span>x</span><span>.field</span><span>;</span>
<span>// 这一行没问题。</span>
<span>let</span> <span>ptr_copy</span> <span>=</span> <span>&amp;</span><span>raw</span> <span>const</span> <span>*</span><span>(</span><span>load</span> <span>ptr</span><span>);</span>
<span>// 但这一行有未定义行为 (UB)！</span>
<span>let</span> <span>val</span> <span>=</span> <span>load</span> <span>*</span><span>(</span><span>load</span> <span>ptr</span><span>);</span>
</code></pre></div></div>

<p>突然之间，为什么最后一行有 UB 而前一行没有就变得完全合理了！表达式 <code>&amp;raw const *(load ptr)</code> 仅仅是计算位置 <code>*(load ptr)</code> 而<em>从未加载其内容</em>，然后使用 <code>&amp;raw const</code> 将该位置转换为一个值。这一点值得重复：<code>*</code> 运算符，通常被称为“解引用指针”，<em>并不以任何方式访问内存</em>。它所做的只是接受一个指针类型的值，并将其转换为一个位置。这是一个纯粹的运算，永远不会失败。在最后一行，有一个额外的 <code>load</code> 应用于 <code>*</code> 的结果，而<em>那就是</em>发生内存访问的地方——并且在这种情况下发生了 UB，因为该位置对齐不足。</p>

<p>求值一个产生未对齐位置的位置表达式是完全合法的，然后将该未对齐位置转换为一个原始指针值也是合法的。通常，从 UB 的角度来说，你应该认为位置就像原始指针：没有要求它们指向有效的值，甚至指向现有的内存。<sup id="fnref:field"><a href="#fn:field" rel="noopener noreferrer">3</a></sup> 然而，从未对齐的位置<em>加载</em>（或存储）是非法的，这就是为什么 <code>load *(load ptr)</code> 是 UB。</p>

<p>换句话说，当 <code>*ptr</code> 被用作值表达式时（正如我们在示例中那样），它<em>不是</em> <code>&amp;raw const *ptr</code> 的子表达式，因为隐式的位置到值强制转换在 <code>*ptr</code> 周围添加了一个额外的 <code>load</code>，而这个 <code>load</code> 在 <code>&amp;raw const *ptr</code> 中并未添加。</p>

<h3 id="other-examples-of-place-expression-surprises">位置表达式导致的其他意外示例</h3>

<p>位置表达式可能导致意外行为的另一个主要例子是与 <code>_</code> 模式结合使用。例如：</p>
<div><div><pre><code><span>let</span> <span>ptr</span> <span>=</span> <span>std</span><span>::</span><span>ptr</span><span>::</span><span>null</span><span>::</span><span>&lt;</span><span>i32</span><span>&gt;</span><span>();</span>
<span>let</span> <span>_</span> <span>=</span> <span>*</span><span>ptr</span><span>;</span> <span>// 这没问题！</span>
<span>let</span> <span>_val</span> <span>=</span> <span>*</span><span>ptr</span><span>;</span> <span>// 这是 UB。</span>
</code></pre></div></div>

<p>请注意，上面的文法无法表示此程序：在 Rust 的完整文法中，<code>let</code> 语法类似于“<code>let</code> <em>Pattern</em> <code>=</code> <em>PlaceExpr</em> <code>;</code>”，然后模式脱糖决定如何处理该位置表达式。如果模式是绑定器（常见情况），会插入一个 <code>load</code> 来计算此绑定器所引用的局部变量的初始值。然而，如果模式是 <code>_</code>，则该位置表达式仍然会被求值——但其结果只是被丢弃。MIR 使用一个 <code>PlaceMention</code> 语句来表示这些语义。</p>

<p>特别地，这意味着 <code>_</code> 模式<em>不会</em>引起位置到值的强制转换！此代码相关部分的脱糖形式是：</p>
<div><div><pre><code><span>PlaceMention</span><span>(</span><span>*</span><span>(</span><span>load</span> <span>ptr</span><span>));</span> <span>// 这没问题！</span>
<span>let</span> <span>_val</span> <span>=</span> <span>load</span> <span>*</span><span>(</span><span>load</span> <span>ptr</span><span>);</span> <span>// 这是 UB。</span>
</code></pre></div></div>
<p>如你所见，第一行实际上并未从指针加载（唯一的 <code>load</code> 是为了从存储它的局部变量中加载指针本身）。当位置表达式与 <code>_</code> 模式一起使用时，不会构造任何值。相比之下，最后一行实际创建了一个新的局部变量，因此插入了一个位置到值的强制转换来计算该变量的初始值。</p>

<p><code>match</code> 语句也会发生同样的事情：</p>
<div><div><pre><code><span>let</span> <span>ptr</span> <span>=</span> <span>std</span><span>::</span><span>ptr</span><span>::</span><span>null</span><span>::</span><span>&lt;</span><span>i32</span><span>&gt;</span><span>();</span>
<span>match</span> <span>*</span><span>ptr</span> <span>{</span> <span>_</span> <span>=&gt;</span> <span>"happy"</span> <span>}</span> <span>// 这没问题！</span>
<span>match</span> <span>*</span><span>ptr</span> <span>{</span> <span>_val</span> <span>=&gt;</span> <span>"not happy"</span> <span>}</span> <span>// 这是 UB。</span>
</code></pre></div></div>
<p><code>match</code> 表达式的审查对象是一个位置表达式，如果模式是 <code>_</code>，则不会构造值。然而，当存在实际的绑定器时，会引入一个局部变量，并插入一个位置到值的强制转换来计算将要存储在该局部变量中的值。</p>

<p><strong>关于 <code>unsafe</code> 块的说明。</strong>请注意，将表达式包装在花括号中会强制它成为值表达式。这意味着 <code>unsafe { *ptr }</code> 总是从指针加载！换句话说：</p>
<div><div><pre><code><span>let</span> <span>ptr</span> <span>=</span> <span>std</span><span>::</span><span>ptr</span><span>::</span><span>null</span><span>::</span><span>&lt;</span><span>i32</span><span>&gt;</span><span>();</span>
<span>let</span> <span>_</span> <span>=</span> <span>*</span><span>ptr</span><span>;</span> <span>// 这没问题！</span>
<span>let</span> <span>_</span> <span>=</span> <span>unsafe</span> <span>{</span> <span>*</span><span>ptr</span> <span>};</span> <span>// 这是 UB。</span>
</code></pre></div></div>
<p>花括号强制产生值表达式的事实有时可能有用，但 <code>unsafe</code> 块具有这种行为确实相当不幸。</p>

<h3 id="are-there-also-value-to-place-coercions">是否也有从值到位置的强制转换？</h3>

<p>到目前为止，我们已经讨论了在期望值表达式的地方遇到位置表达式时会发生什么。但相反的情况呢？请考虑：</p>
<div><div><pre><code><span>let</span> <span>x</span> <span>=</span> <span>&amp;</span><span>mut</span> <span>15</span><span>;</span>
</code></pre></div></div>
<p>根据我们的文法，<code>&amp;</code>（在这种情况下带有 <code>mut</code> 修饰符）需要一个位置表达式，但 <code>15</code> 是一个值表达式。Rust 编译器如何能接受这样的代码？</p>

<p>在这种情况下，脱糖涉及引入新的“临时”局部变量：</p>
<div><div><pre><code><span>let</span> <span>mut</span> <span>_tmp</span> <span>=</span> <span>15</span><span>;</span>
<span>let</span> <span>x</span> <span>=</span> <span>&amp;</span><span>mut</span> <span>_tmp</span><span>;</span>
</code></pre></div></div>
<p>引入此临时变量的确切作用域由<a href="https://github.com/rust-lang/lang-team/blob/master/design-meeting-minutes/2023-03-15-temporary-lifetimes.md" rel="noopener noreferrer">非平凡的规则</a>定义，这超出了本博文的范围；关键点是这种转换再次使程序符合更明确的文法。</p>

<p>此规则有一个例外，即赋值运算符的左侧：如果你写类似 <code>15 = 12 + 19</code> 的东西，值 <code>15</code> 不会被转换为临时位置，程序会被拒绝。在这里引入临时变量不太可能产生有意义的结果，因此没有充分理由接受此类代码。</p>

<h3 id="conclusion">结论</h3>

<p>每当我们用在期望值的地方使用位置表达式，或者在期望位置的地方使用值表达式时，Rust 编译器会隐式地将我们的程序转换为符合上述文法的形式。如果你只编写安全代码，你几乎总是可以完全忘记这种转换。然而，如果你正在编写 unsafe 代码并想理解为什么一些程序有 UB 而另一些没有，理解到底发生了什么可能是至关重要的。如果你只能从本博文中记住一件事，那么请记住 <code>*</code> 解引用指针但<em>不加载内存</em>；相反，它所做的只是将指针转换为一个位置——是随后的隐式位置到值转换执行了实际的加载。我希望为这个隐式的 <code>load</code> 运算符命名可以帮助解开位置和值这个话题的神秘面纱。:)</p>
<div>
  <ol>
    <li id="fn:deref">
      <p><strong>更新 (2025-12-26)：</strong>如果你查看 <a href="https://doc.rust-lang.org/reference/expressions.html#r-expr.place-value.place-context" rel="noopener noreferrer">Rust 参考手册</a>，你可能会注意到它实际上说 <code>*</code> 接受一个<em>位置</em>表达式。这是一个相当特殊的设计选择，与实现 <code>Deref</code> trait 的自定义智能指针以及借用检查有关。事实证明，如果你只解引用位置，借用检查会更容易。然而，就操作语义而言，如果我们说 <code>*</code> 作用于值，整体画面会清晰得多。 <a href="#fnref:deref" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:desugar">
      <p>Rust 编译器实际上并不显式进行这样的脱糖，但这作为将程序编译为 MIR 形式的部分隐式发生。 <a href="#fnref:desugar" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:field">
      <p>然而，一个微妙之处在于，<em>位置表达式</em> <code>.</code> <em>Field</em> 表达式使用 <a href="https://doc.rust-lang.org/nightly/std/primitive.pointer.html#method.offset" rel="noopener noreferrer"><code>offset</code> 方法</a> 的规则执行<em>范围内</em>的指针算术。这是位置表达式关心是否指向现有内存的唯一情况。这很不幸，但优化极大地受益于此规则，并且自从引入 <code>offset_of!</code> 宏以来，unsafe 代码想要对悬空指针进行字段投影的情况应该极其罕见。 <a href="#fnref:field" rel="noopener noreferrer">↩</a></p>
    </li>
  </ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 14958 tokens</em></p>]]></content:encoded>
      <link>https://www.ralfj.de/blog/2024/08/14/places.html</link>
      <guid isPermaLink="false">https://www.ralfj.de/blog/2024/08/14/places.html</guid>
      <pubDate>Tue, 13 Aug 2024 22:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Google开源同行奖</title>
      <category>rust</category>
      <description>[AI 摘要] 作者收到谷歌开源同行奖后误以为是诈骗，确认真实后将奖金捐给了隐私保护组织。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 作者收到谷歌开源同行奖后误以为是诈骗，确认真实后将奖金捐给了隐私保护组织。</div><p>我们大家都习惯收到垃圾邮件，这些邮件据称来自谷歌，内容是“你赢了”，然后我只需发送所有个人数据到某个地方就能领取彩票奖金。最近当我收到一封关于谷歌“开源同行奖”计划的邮件时，我差点把它当作另一种垃圾邮件丢弃了。但事实证明，有时候这些邮件竟然是真的！与此同时，<a href="https://opensource.googleblog.com/2023/12/google-open-source-peer-bonus-program-announces-second-group-of-2023-winners.html" rel="noopener noreferrer">官方公告</a>已经发布，将我列为该奖金的获得者，以感谢我在Rust语言方面的工作。所以这一次，它确实不是垃圾邮件！</p>



<p>非常感谢谷歌的这项计划，提供了250美元的奖励；看到开源工作能得到这样的表彰，真是太棒了。我已经将这笔款项全额捐赠给了<a href="https://noyb.eu/en" rel="noopener noreferrer">noyb</a>，我相信他们会将这笔钱用于<a href="https://noyb.eu/en/noyb-win-first-major-fine-eu-1-million-using-google-analytics" rel="noopener noreferrer">正当事业</a>。</p>

<p><strong>更新（2024-01-07）：</strong>
事实上，这已经是我第二次获得谷歌开源同行奖了。第一次是在<a href="https://opensource.googleblog.com/2023/05/google-open-source-peer-bonus-program-announces-first-group-of-winners-2023.html" rel="noopener noreferrer">2023年上半年</a>。由于支付流程问题，那笔奖金延迟了一段时间才到账，但我现在可以确认它已经到达我的银行账户。我得找个合适的非营利组织捐赠这笔钱……或者也可能再次捐给noyb，我们拭目以待。
<strong>/更新</strong></p><p><em>由 mimo-v2.5 模型翻译，花费 1154 tokens</em></p>]]></content:encoded>
      <link>https://www.ralfj.de/blog/2023/12/27/open-source-peer-bonus.html</link>
      <guid isPermaLink="false">https://www.ralfj.de/blog/2023/12/27/open-source-peer-bonus.html</guid>
      <pubDate>Tue, 26 Dec 2023 23:00:00 +0000</pubDate>
    </item>
    <item>
      <title>谈论未定义行为、不安全 Rust 和 Miri</title>
      <category>rust</category>
      <description>[AI 摘要] 作者在苏黎世 Rust 聚会上讲解了未定义行为、不安全 Rust 和 Miri，并分享了录像。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 作者在苏黎世 Rust 聚会上讲解了未定义行为、不安全 Rust 和 Miri，并分享了录像。</div><p>我最近在苏黎世的本地 Rust 聚会上做了一次关于未定义行为、不安全 Rust 和 Miri 的演讲。
录像可以在<a href="https://www.youtube.com/watch?v=svR0p6fSUYY" rel="noopener noreferrer">这里</a>观看。
它针对的是熟悉 Rust 但对不安全代码的细节不熟悉的听众，所以我希望你们中很多人会喜欢它！
玩得开心。:)</p><p><em>由 mimo-v2.5 模型翻译，花费 1259 tokens</em></p>]]></content:encoded>
      <link>https://www.ralfj.de/blog/2023/06/13/undefined-behavior-talk.html</link>
      <guid isPermaLink="false">https://www.ralfj.de/blog/2023/06/13/undefined-behavior-talk.html</guid>
      <pubDate>Mon, 12 Jun 2023 22:00:00 +0000</pubDate>
    </item>
    <item>
      <title>从栈到树：Rust 的一种新别名模型</title>
      <category>research</category>
      <category>rust</category>
      <description>[AI 摘要] 树借用是 Rust 的一种新别名模型，旨在解决栈借用过早强制唯一性等主要问题，通过两阶段借用和延迟初始化等机制提供更多灵活性。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 树借用是 Rust 的一种新别名模型，旨在解决栈借用过早强制唯一性等主要问题，通过两阶段借用和延迟初始化等机制提供更多灵活性。</div><p>自去年秋天以来，<a href="https://perso.crans.org/vanille/" rel="noopener noreferrer">Neven</a> 一直在实习，为 Rust 开发一种新的别名模型：树借用（Tree Borrows）。等一下，你可能会说——Rust 不是已经有一个别名模型了吗？Ralf 不是总在谈论那个“栈借用（Stacked Borrows）”吗？确实有，但栈借用只是一个可能别名模型的提案——它<a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/133" rel="noopener noreferrer">存在</a><a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/134" rel="noopener noreferrer">着</a><a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/256" rel="noopener noreferrer">相当</a><a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/274" rel="noopener noreferrer">多</a><a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/276" rel="noopener noreferrer">的</a><a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/303" rel="noopener noreferrer">问题</a>。树借用旨在吸收从栈借用中学到的经验，构建一个问题更少的新模型，并做出一些不同的设计决策，以便我们在为 Rust 确定正式模型之前，了解这些模型可能涉及的权衡和微调。</p>

<p>Neven 在<a href="https://perso.crans.org/vanille/treebor/" rel="noopener noreferrer">他的博客</a>上撰写了一篇关于树借用的详细介绍，你应该先去阅读一下。他在最近的一次 RFMIG 会议上做了这个报告，所以你也可以<a href="https://www.youtube.com/watch?v=zQ76zLXesxA" rel="noopener noreferrer">观看他的演讲</a>。在本文中，我将重点介绍与栈借用的区别。我假设你已经了解栈借用，并想理解树借用带来了哪些变化以及原因。</p>



<p>作为简写，我有时会用 SB 代表栈借用，用 TB 代表树借用。</p>

<h2 id="two-phase-borrows">两阶段借用</h2>

<p>树借用的主要新奇之处在于它提供了对两阶段借用的恰当支持。两阶段借用是随着非词法生命周期（NLL）引入的一种机制，它允许接受如下代码：</p>

<div><div><pre><code><span>fn</span> <span>two_phase</span><span>(</span><span>mut</span> <span>x</span><span>:</span> <span>Vec</span><span>&lt;</span><span>usize</span><span>&gt;</span><span>)</span> <span>{</span>
    <span>x</span><span>.push</span><span>(</span><span>x</span><span>.len</span><span>());</span>
<span>}</span>
</code></pre></div></div>

<p>这段代码棘手的原因在于它的脱糖形式类似于这样：</p>

<div><div><pre><code><span>fn</span> <span>two_phase</span><span>(</span><span>mut</span> <span>x</span><span>:</span> <span>Vec</span><span>&lt;</span><span>usize</span><span>&gt;</span><span>)</span> <span>{</span>
    <span>let</span> <span>arg0</span> <span>=</span> <span>&amp;</span><span>mut</span> <span>x</span><span>;</span>
    <span>let</span> <span>arg1</span> <span>=</span> <span>Vec</span><span>::</span><span>len</span><span>(</span><span>&amp;</span><span>x</span><span>);</span>
    <span>Vec</span><span>::</span><span>push</span><span>(</span><span>arg0</span><span>,</span> <span>arg1</span><span>);</span>
<span>}</span>
</code></pre></div></div>

<p>这段代码显然违反了常规的借用检查规则，因为在调用 <code>x.len()</code> 时，<code>x</code> 已经被可变借给了 <code>arg0</code>！然而，编译器会接受这段代码。其工作原理是存储在 <code>arg0</code> 中的 <code>&amp;mut x</code> 被分成了两个阶段：在<em>保留</em>阶段，<code>x</code> 仍然可以通过其他引用被读取。只有当我们真正需要写入 <code>arg0</code>（或调用可能写入它的函数）时，该引用才会被“激活”，从此时起（直到借用生命周期结束），才不允许通过其他引用进行访问。更多细节，请参阅<a href="https://github.com/rust-lang/rfcs/blob/master/text/2025-nested-method-calls.md" rel="noopener noreferrer">RFC</a>和 <a href="https://rustc-dev-guide.rust-lang.org/borrow_check/two_phase_borrows.html" rel="noopener noreferrer">rustc-dev-guide 中关于两阶段借用的章节</a>。对于本博文，唯一相关的一点是：当为方法调用（如 <code>x.push(...)</code>）隐式发生借用时，Rust 会将其视为两阶段借用。当你在代码中显式写出 <code>&amp;mut</code> 时，它被视为没有“保留”阶段的常规可变引用。</p>

<p>对于别名模型而言，两阶段借用是一个大问题：当执行 <code>x.len()</code> 时，<code>arg0</code> 已经存在，而作为一个可变引用，它本不应该允许通过其他指针进行读取。因此，栈借用在此<a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/85" rel="noopener noreferrer">放弃了</a>，基本上将两阶段借用视为裸指针。这当然不能令人满意，因此对于树借用，我们正在添加对两阶段借用的恰当支持。更重要的是，我们将<em>所有</em>可变引用都视为两阶段借用：这比借用检查器允许的更为宽松，但让我们能够完全统一地处理可变引用。（这一点我们可能想要调整，但正如我们将很快看到的，这个决定实际上带来了一些主要的意外好处。）</p>

<p>这就是为什么我们首先需要一棵树：传递给 <code>Vec::len</code> 的 <code>arg0</code> 和引用都是 <code>x</code> 的子节点。栈不再足以表示这里的父子关系。一旦确定使用树，对两阶段借用的建模就相当直观了：它们以 <code>Reserved</code> 状态开始，容忍来自其他无关指针的读取。只有当引用（或其某个子节点）被首次写入时，其状态才转换为 <code>Active</code>，此时才不再接受来自其他无关指针的读取。（更多细节请参阅 Neven 的文章。特别要注意这里潜伏着一个令人不快的意外：如果涉及 <code>UnsafeCell</code>，那么一个保留的可变引用实际上必须容忍通过无关指针进行<em>修改</em>！换句话说，<code>&amp;mut T</code> 的别名规则现在受到了 <code>UnsafeCell</code> 存在的影响。我认为在引入两阶段借用时人们并未意识到这一点，但即使事后看来，替代方案是什么也不明确，似乎也很难避免。）</p>

<h2 id="delayed-uniqueness-of-mutable-references">可变引用唯一性的延迟</h2>

<p>栈借用问题最常见的来源之一是它<a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/133" rel="noopener noreferrer">过早强制执行可变引用唯一性</a>。例如，以下代码在栈借用下是非法的：</p>

<div><div><pre><code><span>let</span> <span>mut</span> <span>a</span> <span>=</span> <span>[</span><span>0</span><span>,</span> <span>1</span><span>];</span>
<span>let</span> <span>from</span> <span>=</span> <span>a</span><span>.as_ptr</span><span>();</span>
<span>let</span> <span>to</span> <span>=</span> <span>a</span><span>.as_mut_ptr</span><span>()</span><span>.add</span><span>(</span><span>1</span><span>);</span> <span>// `from` 在此失效</span>
<span>std</span><span>::</span><span>ptr</span><span>::</span><span>copy_nonoverlapping</span><span>(</span><span>from</span><span>,</span> <span>to</span><span>,</span> <span>1</span><span>);</span>
</code></pre></div></div>

<p>它非法的原因是 <code>as_mut_ptr</code> 接受 <code>&amp;mut self</code>，这断言了对整个数组的独占访问，因此使之前创建的 <code>from</code> 指针失效。然而，在树借用中，这个 <code>&amp;mut self</code> 是一个两阶段借用！<code>as_mut_ptr</code> 实际上不执行任何写操作，因此引用保持保留状态，从未被激活。这意味着 <code>from</code> 指针保持有效，整个程序是良定义的。对 <code>as_mut_ptr</code> 的调用被视为对 <code>*self</code> 的读取，但 <code>from</code>（以及从中派生的共享引用）完全可以通过无关指针进行读取。</p>

<p>碰巧的是，在栈借用中，交换 <code>from</code> 和 <code>to</code> 的行确实能使这段代码工作。然而，这并非出于好的原因：这是 SB 中相当非栈式规则的结果，该规则说在读取时，我们只是<em>禁用所有位于访问所用标签上方的 <code>Unique</code></em>，但我们保持从这些 <code>Unique</code> 指针派生的裸指针启用。基本上，裸指针可以比它们从中派生的可变引用存活更久，这非常违反直觉，并且可能对程序分析造成问题。使用 TB 时，交换后的程序仍然没问题，但原因不同：当 <code>to</code> 首先被创建时，它仍然是一个保留的两阶段借用。这意味着创建一个共享引用并从中派生 <code>from</code>（这相当于对 <code>self</code> 进行读取）是没问题的；保留的两阶段借用容忍通过无关指针进行读取。只有当 <code>to</code> 被写入时，它（或者更确切地说，从中创建的 <code>&amp;mut self</code>）才会变成一个需要唯一性的活动可变引用，但那是在 <code>as_ptr</code> 返回之后，因此不存在冲突的 <code>&amp;self</code> 引用。</p>

<p>事实证明，一致地使用两阶段借用让我们能够完全消除这个hacky的 SB 规则，并修复了 SB 下最常见的未定义行为来源之一。我完全没有预料到这一点，这是一个令人愉快的小意外。:)</p>

<p>但是请注意，以下程序在 SB 下是有效的，但在 TB 下是无效的：</p>

<div><div><pre><code><span>let</span> <span>mut</span> <span>a</span> <span>=</span> <span>[</span><span>0</span><span>,</span> <span>1</span><span>];</span>
<span>let</span> <span>to</span> <span>=</span> <span>a</span><span>.as_mut_ptr</span><span>()</span><span>.add</span><span>(</span><span>1</span><span>);</span>
<span>to</span><span>.write</span><span>(</span><span>0</span><span>);</span>
<span>let</span> <span>from</span> <span>=</span> <span>a</span><span>.as_ptr</span><span>();</span>
<span>std</span><span>::</span><span>ptr</span><span>::</span><span>copy_nonoverlapping</span><span>(</span><span>from</span><span>,</span> <span>to</span><span>,</span> <span>1</span><span>);</span>
</code></pre></div></div>

<p>这里，对 <code>to</code> 的写入激活了两阶段借用，因此强制执行唯一性。这意味着为 <code>as_ptr</code> 创建的 <code>&amp;self</code>（被认为读取整个 <code>self</code>）与 <code>to</code> 不兼容，因此当创建 <code>from</code> 时，<code>to</code> 失效了（好吧，它被设为只读）。到目前为止，我们没有证据表明这种模式在野外很常见。避免上述代码这类问题的方法是<em>在开始做任何事情之前设置好你所有的裸指针</em>。在 TB 下，调用接收引用的方法（如 <code>as_ptr</code> 和 <code>as_mut_ptr</code>）并使用它们返回的裸指针访问不相交的位置，即使这些引用重叠也是可以的，但你必须在第一次写入裸指针之前调用所有这些方法。一旦第一次写入发生，再创建引用可能导致别名违规。</p>

<h2 id="no-strict-confinement-of-the-accessible-memory-range">不要严格限制可访问的内存范围</h2>

<p>栈借用的另一个主要麻烦来源是<a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/134" rel="noopener noreferrer">限制裸指针只能访问其最初创建时的类型和可变性</a>。在 SB 下，当一个引用被转换为 <code>*mut T</code> 时，生成的裸指针被限制只能访问 <code>T</code> 所覆盖的内存。当人们对数组的某个元素（或结构体的某个字段）取裸指针，然后使用指针算术访问相邻元素时，这经常绊倒他们。此外，当一个引用被转换为 <code>*const T</code> 时，它实际上是只读的，即使该引用是可变的！许多人期望 <code>*const</code> 与 <code>*mut</code> 在别名方面没有区别，因此这是一个常见的困惑来源。</p>

<p>在 TB 下，我们通过不再对引用到裸指针的转换进行任何重标记（retagging）来解决这个问题。一个裸指针简单地使用其从中派生的父引用相同的标签，从而继承其可变性和它可以访问的地址范围。此外，引用并不严格受限于其类型描述的内存范围：当从一个父指针创建一个 <code>&amp;mut T</code>（或 <code>&amp;T</code>）时，我们最初记录允许新引用访问 <code>T</code> 描述的内存范围（我们认为这是对该内存范围的读取访问）。然而，我们也执行<em>延迟初始化</em>：当访问此初始范围之外的内存位置时，我们会检查父指针是否有权访问该位置，如果有，我们也会授予子节点相同的访问权限。此过程递归重复，直到找到具有足够访问权限的父节点，或到达树的根节点。</p>

<p>这意味着 TB 与<a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/243" rel="noopener noreferrer"><code>container_of</code>风格的指针算术</a>和<a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/276" rel="noopener noreferrer"><code>extern</code>类型</a>兼容，克服了 SB 的另外两个限制。</p>

<p>这也意味着以下代码在 TB 下变得合法：</p>

<div><div><pre><code><span>let</span> <span>mut</span> <span>x</span> <span>=</span> <span>0</span><span>;</span>
<span>let</span> <span>ptr</span> <span>=</span> <span>std</span><span>::</span><span>ptr</span><span>::</span><span>addr_of_mut!</span><span>(</span><span>x</span><span>);</span>
<span>x</span> <span>=</span> <span>1</span><span>;</span>
<span>ptr</span><span>.read</span><span>();</span>
</code></pre></div></div>

<p>在 SB 下，<code>ptr</code> 和对局部变量 <code>x</code> 的直接访问使用两个不同的标签，因此写入局部变量会使所有指向它的指针失效。在 TB 下，不再如此；直接对局部变量创建的裸指针被允许与对局部变量的直接访问任意别名。</p>

<p>可以说 TB 的行为更符合直觉，但它意味着我们不再能将写入局部变量作为所有可能别名已被失效的信号。然而，请注意，TB 仅在函数体中立即使用 <code>addr_of_mut</code>（或 <code>addr_of</code>）时才允许这样做！如果创建了引用 <code>&amp;mut x</code>，然后其他某个函数从中派生了一个裸指针，那么这些裸指针在下一次写入 <code>x</code> 时<em>确实会</em>失效。所以对我来说，这是一个完美的折衷：使用裸指针的代码未定义行为的风险更低，但不使用裸指针的代码（从语法上很容易看出）可以像 SB 一样进行优化。</p>

<p>请注意，TB 中的这整个方法依赖于 TB <em>不需要</em>上一节中提到的违反栈的 hack。如果 SB 中的裸指针只是继承了父标签，那么它们会与从中派生的唯一指针一起失效，从而禁止所有专门为支持这种 hack 而添加的代码。这意味着将这些改进移植回 SB 不太可能实现。</p>

<h2 id="unsafecell"><code>UnsafeCell</code></h2>

<p>对 <code>UnsafeCell</code> 的处理在 TB 中也发生了很大变化。首先，SB 的另一个<a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/303" rel="noopener noreferrer">主要问题</a>得到了修复：将 <code>&amp;i32</code> 转换为 <code>&amp;Cell&lt;i32&gt;</code> <em>然后从不写入它</em> 最终被允许了。这源于 TB 处理 <code>UnsafeCell</code> 所允许的别名的方式：它们被视为转换为裸指针，因此借用 <code>&amp;Cell&lt;i32&gt;</code> 只是继承父指针的标签（因此继承其权限）。</p>

<p>更具争议的是，TB 还改变了当 <code>&amp;T</code> 在 <code>T</code> 中某处涉及 <code>UnsafeCell</code> 时，事物变为只读的精确方式。特别是对于 <code>&amp;(i32, Cell&lt;i32&gt;)</code>，TB 允许修改<em>两个</em>字段，包括第一个是常规 <code>i32</code> 的字段，因为它只是将整个引用视为“允许别名”。<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>相比之下，SB 实际上搞清楚了前 4 个字节是只读的，只有最后 4 个字节允许通过别名指针进行修改。</p>

<p>做出这个设计决策的原因是，TB 的总体哲学是倾向于允许更多代码，拥有更少的未定义行为（这与我使用 SB 的方向相反）。这是一个有意识的选择，旨在用这两个模型探索尽可能多的设计空间。当然，我们想确保 TB 仍然允许所有预期的优化，并且仍然有足够的未定义行为来证明 rustc 生成的 LLVM IR 是合理的——这些是我们所需的最小未定义行为量的“下限”。事实证明，在这些约束下，我们可以用相当简单的方法支持 <code>UnsafeCell</code>：对于 <code>&amp;T</code> 的别名规则，只有 2 种情况。要么任何地方都没有 <code>UnsafeCell</code>，那么这个引用是只读的；否则，该引用允许别名。作为一个经常思考如何证明包含别名模型在内的完整 Rust 语义定理的人来说，这种方法看起来简单得令人愉悦。:)</p>

<p>我预计这个决定会有些争议，但我们收到的反对意见仍然出乎意料地多。好消息是，这远未板上钉钉：我们可以<a href="https://github.com/rust-lang/unsafe-code-guidelines/issues/403" rel="noopener noreferrer">更改 TB，使其更像 SB 那样处理 <code>UnsafeCell</code></a>。与之前描述的差异不同，这个选择完全独立于我们的其他设计选择。虽然我更喜欢 TB 的方法，但就目前的情况来看，我确实预计我们最终会采用类似 SB 的 <code>UnsafeCell</code> 处理方式。</p>

<h2 id="what-about-optimizations">那优化呢？</h2>

<p>我写了很多关于 TB 在哪些编码模式属于未定义行为方面与 SB 的不同之处。但硬币的另一面呢，优化？显然，由于 SB 有更多的未定义行为，我们不得不期望 TB 允许更少的优化。确实有一类主要的优化 TB 丧失了：推测性写入，即在以前不会写入此位置的代码路径中插入写入。这是一种强大的优化，我很高兴 SB 能做到这一点，但它也带来了巨大的代价：可变引用必须是“立即唯一”的。鉴于“过早强制唯一性”是多么常见的问题，我目前的倾向是我们可能宁愿让所有那些代码合法，也不愿允许推测性写入。我们仍然有关于读取的强大优化原则，并且当代码<em>确实</em>执行写入时，会产生更多优化，所以我的感觉是，坚持推测性写入有点过分了。</p>

<p>在另一方面，TB 实际上允许了一套关键的优化，而 SB 因意外而排除了这些优化：重排读取顺序！SB 的问题是，如果我们从“读取可变引用，然后读取共享引用”开始，然后重排为“读取共享引用，然后读取可变引用”，那么在新的程序中，读取共享引用可能会使可变引用失效——因此重排可能引入了未定义行为！这个优化无需特殊的别名模型就能实现，因此 SB 不允许它是一个相当尴尬的问题。如果不是因为上面多次提到的违反栈的 hack，我认为在 SB 中修复这个问题会相当容易，但唉，那个 hack 至关重要，如果我们移除它，太多现有代码将变成未定义行为。与此同时，TB 不需要这样的 hack，所以我们可以做正确的事（TM）：当进行读取时，相关的可变引用不会被完全禁用，它们只是被设为只读。这意味着“读取共享引用，然后读取可变引用”等同于“读取可变引用，然后读取共享引用”，因此优化得以保留。（一个结果是，重标签也可以彼此重排序，因为它们也充当读取。因此你设置各种指针的顺序无关紧要，直到你用其中一个进行第一次写入访问。）</p>

<h2 id="future-possibility-unique">未来的可能性：<code>Unique</code></h2>

<p>树借用为一个我们尚未实现但很兴奋去探索的扩展铺平了道路：赋予 <code>Unique</code> 含义。<code>Unique</code> 是 Rust 标准库中的一个私有类型，最初旨在表达 <code>noalias</code> 要求。然而，它从未真正连接到在 LLVM 层面发出该属性。<code>Unique</code> 主要在标准库的两个地方使用：<code>Box</code> 和 <code>Vec</code>。SB（和 TB）特殊处理 <code>Box</code>（与 rustc 本身匹配），但不处理 <code>Unique</code>，因此 <code>Vec</code> 不带有任何别名要求。而且 SB 的方法完全不适用于 <code>Vec</code>，因为我们实际上不知道这里要让多少内存唯一。然而，有了 TB，我们有了延迟初始化，所以我们不需要预先承诺一个内存范围——我们可以“在访问时”使其唯一。这意味着我们可以探索赋予 <code>Vec</code> 中的 <code>Unique</code> 含义。</p>

<p>现在，这可能实际上行不通。人们实际上确实对 <code>Vec</code> 进行了公然别名的事情，例如实现 arena。另一方面，<code>Vec</code> 的唯一性只会在它被移动或<em>按值传递</em>时出现，并且仅针对实际被访问的内存范围。因此这很可能与 arena 兼容。我认为最好的方法是在标志背后实现 <code>Unique</code> 语义并进行实验。如果成功了，我们甚至可能能够移除所有对 <code>Box</code> 的特殊处理，并依赖 <code>Box</code> 被定义为 <code>Unique</code> 上的新类型这一事实。这可能会略微降低优化潜力（<code>Box&lt;T&gt;</code> 已知指向至少与 <code>T</code> 一样大的内存范围，而 <code>Unique</code> 没有此信息），但让 <code>Box</code> 不那么魔法化是一个长期追求的目标，因此这可能是一个可以接受的权衡。</p>

<p>我应该注意到，有很多人认为 <code>Box</code> 和 <code>Vec</code> 都不应该有任何别名要求。我认为值得首先探索我们是否可以拥有足够轻量级的别名要求，使其与常见的编码模式兼容，但即使我们最终说 <code>Box</code> 和 <code>Vec</code> 的行为像裸指针，拥有 <code>Unique</code> 在我们的工具箱中并将其暴露给不安全代码作者以榨取最后一点性能仍然可能有用。</p>

<h2 id="conclusion">结论</h2>

<p>这些是栈借用和树借用之间的主要区别。正如你所看到的，几乎所有情况都是 TB 允许比 SB 更多的代码，确实 TB 修复了我认为 SB 的两个最大问题：可变引用的过早强制唯一性，以及将引用和裸指针限制在它们创建时的类型大小。这对不安全代码作者来说是个好消息！</p>

<p>TB <em>没有</em>改变的是“保护器”的存在，以确保某些引用在整个函数调用期间保持有效（无论它们是否再次被使用）；保护器对于证明我们想要发出的 LLVM <code>noalias</code> 注释绝对必要，它们也确实能实现一些比其他方式可能的更强的优化。我确实预计保护器将是树借用意外未定义行为的主要剩余来源，并且我认为我们在这里没有太多回旋余地，所以这可能只是一个我们需要告诉程序员调整代码，并投资于文档材料以使这个微妙问题广为人知的情况。</p>

<p>Neven 在 Miri 中实现了树借用，所以你可以通过设置 <code>MIRIFLAGS=-Zmiri-tree-borrows</code> 来玩一玩并检查你自己的代码。如果你遇到任何意外或疑虑，请告诉我们！<a href="https://rust-lang.zulipchat.com/#narrow/stream/136281-t-opsem" rel="noopener noreferrer">t-opsem Zulip</a> 和 <a href="https://github.com/rust-lang/unsafe-code-guidelines/" rel="noopener noreferrer">UCG issue tracker</a> 是提出此类问题的好地方。</p>

<p>以上就是我的全部内容，感谢阅读——并向 Neven 致意，他完成了所有实际工作（并在此博文中提供了反馈），监督这个项目非常有趣！记得阅读<a href="https://perso.crans.org/vanille/treebor/" rel="noopener noreferrer">他的文章</a>并<a href="https://www.youtube.com/watch?v=zQ76zLXesxA" rel="noopener noreferrer">观看他的演讲</a>。</p>
<div>
  <ol>
    <li id="fn:1">
      <p>这并不意味着我们祝福这种修改！这只是意味着编译器无法利用第一个字段的不可变性进行优化。基本上，该字段的不可变性变成了<a href="/blog/2018/08/22/two-kinds-of-invariants.html" rel="noopener noreferrer">安全不变量而非有效性不变量</a>：当你调用外部代码时，你仍然可以依赖它不修改该字段，但在你自己代码的私密性中，你是允许修改它的。更多背景信息，请参见<a href="https://www.reddit.com/r/rust/comments/13y8a9b/comment/jmlvgun/" rel="noopener noreferrer">我在这里的回复</a>。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩</a></p>
    </li>
  </ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 17145 tokens</em></p>]]></content:encoded>
      <link>https://www.ralfj.de/blog/2023/06/02/tree-borrows.html</link>
      <guid isPermaLink="false">https://www.ralfj.de/blog/2023/06/02/tree-borrows.html</guid>
      <pubDate>Thu, 1 Jun 2023 22:00:00 +0000</pubDate>
    </item>
    <item>
      <title>cargo careful：以额外的谨慎调试检查运行你的Rust代码</title>
      <category>rust</category>
      <description>[AI 摘要] 文章介绍了 cargo careful，一个运行 Rust 代码时增强调试检查以检测未定义行为的工具。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 文章介绍了 cargo careful，一个运行 Rust 代码时增强调试检查以检测未定义行为的工具。</div><p>你知道吗？标准库中充满了用户从未见过的有用检查。标准库中有许多调试断言，它们会检查诸如 <code>char::from_u32_unchecked</code> 是否被调用于有效的 <code>char</code>，<code>CStr::from_bytes_with_nul_unchecked</code> 是否没有内部空字节，或者指针函数如 <code>copy</code> 或 <code>copy_nonoverlapping</code> 是否被调用于适当对齐的非空（且不重叠）指针。然而，由 rustup 分发的常规标准库是在没有调试断言的情况下编译的，因此用户无法轻松受益于所有这些额外检查。</p>



<p><a href="https://github.com/RalfJung/cargo-careful" rel="noopener noreferrer"><code>cargo careful</code></a> 正是为了弥合这一差距而生：首次调用时，它会从源码构建带有调试断言的标准库，然后使用该标准库运行你的程序或测试套件。安装 <code>cargo careful</code> 就像 <code>cargo install cargo-careful</code> 一样简单，之后你可以执行 <code>cargo +nightly careful run</code>/<code>cargo +nightly careful test</code> 来以额外的调试检查运行你的二进制 crate 和测试套件。</p>

<p>这自然会比常规的调试或发布构建慢，但它比在 <a href="https://github.com/rust-lang/miri" rel="noopener noreferrer">Miri</a> 中执行你的程序快得多，并且仍然有助于发现一些未定义行为。与 Miri 不同，它完全兼容 FFI（尽管 FFI 边界后的代码完全未检查）。当然，Miri 更加彻底，<code>cargo careful</code> 会遗漏许多问题（例如，它无法检测越界指针算术——但它<em>确实</em>对 <code>get_unchecked</code> 切片访问执行边界检查）。</p>

<p>请注意，目前其中一些检查（特别是针对原始指针的方法）会导致程序通过 SIGILL 突然中止，而没有友好的错误消息或回溯。未来可能有方法改进这一点。与此同时，如果你有一些 <code>unsafe</code> 代码由于某些原因无法用 Miri 测试，可以试试 <a href="https://github.com/RalfJung/cargo-careful" rel="noopener noreferrer"><code>cargo careful</code></a> 并告诉我它的表现如何。:)</p>

<p><em>顺便说一下，我即将在<a href="/blog/2022/08/16/eth.html" rel="noopener noreferrer">苏黎世联邦理工学院担任教授</a>，因此如果你有兴趣作为硕士生、博士生或博士后与我一起研究编程语言理论，请<a href="https://research.ralfj.de/contact.html" rel="noopener noreferrer">联系我</a>！</em></p><p><em>由 mimo-v2.5 模型翻译，花费 3844 tokens</em></p>]]></content:encoded>
      <link>https://www.ralfj.de/blog/2022/09/26/cargo-careful.html</link>
      <guid isPermaLink="false">https://www.ralfj.de/blog/2022/09/26/cargo-careful.html</guid>
      <pubDate>Sun, 25 Sep 2022 22:00:00 +0000</pubDate>
    </item>
    <item>
      <title>新的开始</title>
      <category>research</category>
      <category>rust</category>
      <description>[AI 摘要] 文章宣布作者将于11月1日起在ETH苏黎世担任助理教授，并表达了激动、感激及对未来的期待。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 文章宣布作者将于11月1日起在ETH苏黎世担任助理教授，并表达了激动、感激及对未来的期待。</div><p>我有一些非常激动人心的消息要分享：从11月1日起，我将在ETH苏黎世担任助理教授！
成为教授本身就是一个梦想成真，而能在ETH苏黎世这样的地方当教授更是我从未敢梦想过的事。
我仍然不敢相信这真的发生了（我要当<em>教授</em>了？？？），但<a href="https://twitter.com/CSatETH/status/1548944615285350400" rel="noopener noreferrer">消息已经公布</a>，所以我想这是真的。:D</p>



<p>我感到既兴奋又恐惧，而且两者的程度差不多。
兴奋于所有新的可能性，期待与学生合作并激励下一代研究者；
恐惧于所有的责任，以及几个月后就得站在教室里讲课的前景。
但不知为何，其他人似乎都相信我能做到，所以我想我只好顺其自然，希望不会让他们失望……</p>

<p>我也深感谦卑，并永远感激能获得这个机会。
能在ETH这样的环境中工作是无法想象的特权，我不知道自己怎么如此幸运。
我可能用尽了今生所有的福报，我会尽我所能不辜负这份特权。
我深深感激所有与我共事过的人，首先当然是我的博士导师<a href="https://people.mpi-sws.org/~dreyer/" rel="noopener noreferrer">Derek Dreyer</a>。
但我也特别想感谢Rust社区，因为我认为如果没有Rust就不会有这一切——感谢<em>每一位</em>为这门语言做出贡献的人，我基本上是依托它建立自己的事业<sup id="fnref:rust"><a href="#fn:rust" rel="noopener noreferrer">1</a></sup>，特别感谢那些包容我对Rust如何处理不安全代码的想法、并帮助我塑造语言这一部分的人。</p>

<p>那么接下来呢？
我即将完成在MIT的博士后研究，搬回欧洲，然后于10月搬到苏黎世。
接着我就得弄明白当教授是怎么回事了。;)
我的首要任务是建立一个研究团队：“编程语言基础实验室”<sup id="fnref:lab"><a href="#fn:lab" rel="noopener noreferrer">2</a></sup>。
因此，如果你有兴趣攻读博士或从事博士后研究，研究，嗯，编程语言基础，特别是Rust的形式化基础，或者你是ETH的学生，对该领域有硕士论文兴趣——请<a href="https://research.ralfj.de/contact.html" rel="noopener noreferrer">联系我</a>！
我仍在摸索如何进行招聘和寻找合适的项目，但需要解决的开放问题和需要证明的定理并不少。:)</p>

<div>
  <ol>
    <li id="fn:rust">
      <p>在大家担心之前，我也有与Rust无关的<a href="https://iris-project.org/" rel="noopener noreferrer">想法</a>想探索。但Rust目前是我新研究问题的最大灵感来源，没有Rust，我认为我的研究不会像今天这样应用性强且有影响力，我相信这是ETH决定聘用我的关键因素。<a href="#fnref:rust" rel="noopener noreferrer">↩</a></p>
    </li>
    <li id="fn:lab">
      <p>是的，我有件实验袍。不过我通常不穿……如果你想看我穿，得请我喝点啤酒。<a href="#fnref:lab" rel="noopener noreferrer">↩</a></p>
    </li>
  </ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 2132 tokens</em></p>]]></content:encoded>
      <link>https://www.ralfj.de/blog/2022/08/16/eth.html</link>
      <guid isPermaLink="false">https://www.ralfj.de/blog/2022/08/16/eth.html</guid>
      <pubDate>Mon, 15 Aug 2022 22:00:00 +0000</pubDate>
    </item>
  </channel>
</rss>
