Windows堆栈限制检查回顾,后续
摘要
Raymond Chen跟进了他之前关于ARM64堆栈限制检查的文章,指出了堆栈探测函数中x15寄存器的非常规使用细节,并比较了多个架构的寄存器使用。
<p><a href="https://aarongiles.com/"> Aaron Giles</a> 参与了将Windows移植到ARM32和AArch64的工作,他<a href="https://bsky.app/profile/did:plc:rozrlqzq7umwvv5etd7slmxz/post/3mhk2isogwc2q?ref_src=embed">指出</a>我之前的<a title="Windows stack limit checking retrospective: arm64, also known as AArch64" href="https://devblogs.microsoft.com/oldnewthing/20260320-00/?p=112154">ARM64堆栈限制检查回顾</a>中遗漏了一个细节:</p>
<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:rozrlqzq7umwvv5etd7slmxz/app.bsky.feed.post/3mhk2isogwc2q" data-bluesky-cid="bafyreihi4tton7infgddx7csbqjm5xljewdwpfeoi27narlqdbfghlsp2a" data-bluesky-embed-color-mode="system">
<p lang="en">每隔一段时间,Raymond Chen就会发表一系列架构对比文章,我就能看到(经过转述的)自己很久以前写的代码。他关于我们为何传递stack size/16的解释是正确的,但让我惊讶的是他没有提到非常规的x15使用。</p>
<p>— Aaron Giles (<a href="https://bsky.app/profile/did:plc:rozrlqzq7umwvv5etd7slmxz?ref_src=embed">@aarongiles.com</a>) <a href="https://bsky.app/profile/did:plc:rozrlqzq7umwvv5etd7slmxz/post/3mhk2isogwc2q?ref_src=embed"> 2026年3月20日 晚上8:08</a></p></blockquote>
<p><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script></p>
<p>我猜“非常规的x15使用”指的是:“为什么参数是通过<code>x15</code>寄存器传递的?AArch64调用约定将第一个参数放在<code>x0</code>寄存器中,那么这个参数不应该是<code>x0</code>寄存器吗?”</p>
<p>我觉得这太明显了,所以认为不值得提及。</p>
<p>需要执行堆栈探测的函数陷入了一个两难境地:它有自己的入站参数,其中一些可能通过寄存器传递。如果堆栈大小参数像普通参数一样传递给堆栈探测函数,那么调用函数必须将原始的入站参数保存到某处。但它不能将它们保存到堆栈上,因为在使用堆栈之前必须先进行堆栈探测。</p>
<p>解决方案是为堆栈探测函数提供自定义调用约定,将其限制在未用于接收入站参数的临时寄存器。</p>
<table style="border-collapse: collapse;" border="1" cellspacing="0" cellpadding="3">
<tbody>
<tr>
<th>架构</th>
<th>用于参数</th>
<th>分配大小</th>
<th>同时修改</th>
</tr>
<tr>
<td>8086</td>
<td> </td>
<td><tt>ax</tt></td>
<td><tt>bx</tt>, <tt>dx</tt></td>
</tr>
<tr>
<td>x86-32</td>
<td><tt>ecx</tt></td>
<td><tt>eax</tt></td>
<td> </td>
</tr>
<tr>
<td>MIPS</td>
<td><tt>a0</tt>…<tt>a3</tt></td>
<td><tt>t8</tt></td>
<td> </td>
</tr>
<tr>
<td>PowerPC</td>
<td><tt>r3</tt>…<tt>r10</tt></td>
<td><tt>r12</tt></td>
<td><tt>r0</tt>, <tt>r11</tt></td>
</tr>
<tr>
<td>Alpha AXP</td>
<td><tt>a0</tt>…<tt>a5</tt></td>
<td><tt>t12</tt></td>
<td><tt>t8</tt>, <tt>t9</tt>, <tt>t10</tt></td>
</tr>
<tr>
<td>x86-64</td>
<td><tt>rcx</tt>, <tt>rdx</tt>, <tt>r8</tt>, <tt>r9</tt></td>
<td><tt>rax</tt></td>
<td><tt>r10</tt>, <tt>r11</tt></td>
</tr>
<tr>
<td>AArch64</td>
<td><tt>x0</tt>…<tt>x7</tt></td>
<td><tt>x15</tt></td>
<td><tt>x16</tt>, <tt>x17</tt></td>
</tr>
</tbody>
</table>
<p>处理器架构的调用约定会将某些寄存器指定为“超易失”,通常是那些保留给汇编临时变量或用于模块间函数调用的寄存器。这些寄存器非常适合堆栈探测函数使用,因为它们绝对不可能用于普通参数传递。</p>
<p>例如,PowerPC使用<tt>r11</tt>,AArch64使用<tt>r16</tt>和<tt>r17</tt>,它们都可用于函数胶水桩。其他机会则被忽略了:MIPS和Alpha AXP本可以使用<tt>at</tt>,不过我能理解他们可能想避免使用它,因为汇编器在汇编伪指令时可能会隐式地使用它们。</p>
<p>本文章<a href="https://devblogs.microsoft.com/oldnewthing/20260617-00/?p=112436">Windows堆栈限制检查回顾,后续</a>首发于<a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>。</p>
查看缓存全文
缓存时间: 2026/06/18 12:56
# Windows 堆栈限制检查回顾,后续篇 - 《老调新谈》
来源:https://devblogs.microsoft.com/oldnewthing/20260617-00?p=112436
Aaron Giles (https://aarongiles.com/) 曾参与将 Windows 移植到 ARM32 和 AArch64 的工作。他指出了我在关于 arm64 堆栈限制检查的回顾(https://devblogs.microsoft.com/oldnewthing/20260320-00/?p=112154)中遗漏的一个细节(https://bsky.app/profile/did:plc:rozrlqzq7umwvv5etd7slmxz/post/3mhk2isogwc2q?ref_src=embed):
> 每隔一段时间,Raymond Chen 就会写一篇架构比较系列文章,而我也总能看到(改述版)一些我很久以前写过的代码。他关于我们为什么传递 stack size/16 的解释是正确的,但令我惊讶的是他没有提及那个非常规的 x15 用法。—— Aaron Giles (@aarongiles.com (https://bsky.app/profile/did:plc:rozrlqzq7umwvv5etd7slmxz?ref_src=embed)),2026年3月20日晚上8:08 (https://bsky.app/profile/did:plc:rozrlqzq7umwvv5etd7slmxz/post/3mhk2isogwc2q?ref_src=embed)
我猜Aaron所说的“非常规x15用法”是指:“为什么参数是通过 `x15` 寄存器传递的?AArch64 调用约定规定第一个参数应通过 `x0` 寄存器传递,所以这个参数不是应该放在 `x0` 里吗?”
对我来说这太明显了,以至于我觉得不值得一提。
需要进行堆栈探测的函数处境有点尴尬:它拥有入站参数,其中一些可能通过寄存器传递。如果堆栈大小参数像普通参数一样传递给堆栈探测函数,那么调用函数就必须将其原始的入站参数暂存到其他地方。但它不能将这些参数保存到堆栈上,因为在使用堆栈之前必须先进行堆栈探测。
解决方案是给堆栈探测函数一个自定义的调用约定,将其限制在那些不用于接收入站参数的临时寄存器中。
| 架构 | 用于参数的寄存器 | 分配大小的寄存器 | 同时被修改的寄存器 |
|------|----------------|------------------|-------------------|
| 8086 | ax | bx | dx |
| x86-32 | ecx | eax | |
| MIPS | a0...a3 | t8 | |
| PowerPC | r3...r10 | r12 | r0, r11 |
| Alpha AXP | a0...a5 | t12 | t8, t9, t10 |
| x86-64 | rcx, rdx, r8, r9 | rax | r10, r11 |
| AArch64 | x0...x7 | x15 | x16, x17 |
处理器架构的调用约定将某些寄存器指定为“超易失”寄存器,这些寄存器通常保留给汇编器临时变量或用于模块间的函数调用。这些寄存器非常适合堆栈探测函数使用,因为它们不可能用于常规参数传递。
例如,PowerPC 使用 r11,AArch64 使用 r16 和 r17,所有这些寄存器都可用于函数胶合存根。其他架构则错失了机会:MIPS 和 Alpha AXP 本可以使用 `at`,不过我明白为什么他们可能不想使用它们,因为汇编器在汇编伪指令时可能会隐式使用这些寄存器。
### 分类
### 主题
## 作者
Raymond Chen
Raymond 参与 Windows 的发展已有30多年。2003年,他创办了一个名为《老调新谈》的网站,其受欢迎程度远超他最狂野的想象,这一发展至今仍让他感到不安。该网站还衍生出了一本书,巧合的是书名也是《老调新谈》(Addison Wesley 2007)。他偶尔会出现在 Windows Dev Docs Twitter 账号上,讲述一些毫无实用信息的故事。
相似文章
SBCL: 终极汇编代码面包板 (2014)
一篇技术博客文章,探讨如何使用SBCL作为汇编代码的面包板,重点介绍基于堆栈的虚拟机技术,如旋转堆栈和高效的原语操作分发,并引用了F18处理器和x87堆栈。
Linux/x86-64上使用内存间接调用(徒劳?)的系统调用检测,第一部分
一篇技术博文,讨论在Linux/x86-64上检测系统调用的技术,包括指令双关、E9Patch、zpoline以及短指令修补的挑战。
编写可移植的ARM64汇编代码
一份关于编写可在Apple Darwin和Linux/BSD系统间移植的ARM64汇编代码的指南,涵盖ABI、符号命名和向量助记符的差异。
Intel 8087浮点芯片的堆栈电路逆向工程
本文详细介绍了对Intel 8087浮点协处理器堆栈电路的逆向工程,解释了该芯片基于堆栈的寄存器架构和微码ROM如何实现快速浮点运算。
关于WebAssembly作为栈机器的思考
这篇博客文章回应了关于WebAssembly不是纯栈机器的说法,通过讨论其带局部变量的设计并与Forth进行比较,论证它仍然符合栈机器的定义,并且其类似寄存器的局部变量提高了可读性和性能。