【发布时间】:2013-12-13 06:54:16
【问题描述】:
我试图弄清楚 C# 编译器如何处理尾调用。
(答案:They're not. 但 64 位 JIT(s) 将执行 TCE(尾调用消除)。Restrictions apply.)
所以我使用递归调用编写了一个小测试,它打印了在StackOverflowException 终止进程之前它被调用了多少次。
class Program
{
static void Main(string[] args)
{
Rec();
}
static int sz = 0;
static Random r = new Random();
static void Rec()
{
sz++;
//uncomment for faster, more imprecise runs
//if (sz % 100 == 0)
{
//some code to keep this method from being inlined
var zz = r.Next();
Console.Write("{0} Random: {1}\r", sz, zz);
}
//uncommenting this stops TCE from happening
//else
//{
// Console.Write("{0}\r", sz);
//}
Rec();
}
马上,程序以 SO Exception 结束:
- “优化构建”关闭(调试或发布)
- 目标:x86
- 目标:AnyCPU + “首选 32 位”(这是 VS 2012 中的新功能,我第一次看到它。More here。)
- 代码中有一些看似无害的分支(请参阅注释的“else”分支)。
相反,使用 'Optimize build' ON +(Target = x64 或 AnyCPU with 'Prefer 32bit' OFF(在 64 位 CPU 上)),TCE 发生并且计数器永远旋转(好吧,它可以说是旋转 down 每次它的值溢出)。
但我注意到StackOverflowException 案例中的一种我无法解释的行为:它从来没有(?)发生在完全相同相同的堆栈深度。以下是一些 32 位运行的输出,发布版本:
51600 Random: 1778264579
Process is terminated due to StackOverflowException.
51599 Random: 1515673450
Process is terminated due to StackOverflowException.
51602 Random: 1567871768
Process is terminated due to StackOverflowException.
51535 Random: 2760045665
Process is terminated due to StackOverflowException.
并调试构建:
28641 Random: 4435795885
Process is terminated due to StackOverflowException.
28641 Random: 4873901326 //never say never
Process is terminated due to StackOverflowException.
28623 Random: 7255802746
Process is terminated due to StackOverflowException.
28669 Random: 1613806023
Process is terminated due to StackOverflowException.
堆栈大小是恒定的 (defaults to 1 MB)。堆栈帧的大小是恒定的。
那么,当StackOverflowException 命中时,什么可以解释堆栈深度的(有时不是微不足道的)变化?
更新
Hans Passant 提出了Console.WriteLine 涉及 P/Invoke、互操作和可能的非确定性锁定的问题。
所以我将代码简化为:
class Program
{
static void Main(string[] args)
{
Rec();
}
static int sz = 0;
static void Rec()
{
sz++;
Rec();
}
}
我在没有调试器的情况下在 Release/32bit/Optimization ON 中运行它。当程序崩溃时,我附加调试器并检查计数器的值。
而且它仍然在几次运行中都不相同。 (或者我的测试有缺陷。)
更新:关闭
按照 fejesjoco 的建议,我研究了 ASLR(地址空间布局随机化)。
这是一种安全技术,通过随机化进程地址空间中的各种事物,包括堆栈位置,显然,它的大小,缓冲区溢出攻击很难找到(例如)特定系统调用的精确位置。
这个理论听起来不错。让我们付诸实践吧!
为了对此进行测试,我使用了一个特定于该任务的 Microsoft 工具:EMET or The Enhanced Mitigation Experience Toolkit。它允许在系统或进程级别设置 ASLR 标志(以及更多)。
(还有一个system-wide, registry hacking alternative没试过)
为了验证该工具的有效性,我还发现Process Explorer 在进程的“属性”页面中适时报告了ASLR 标志的状态。直到今天才看到:)
理论上,EMET 可以(重新)为单个进程设置 ASLR 标志。实际上,它似乎并没有改变任何东西(见上图)。
但是,我为整个系统禁用了 ASLR,并且(稍后重新启动)我终于可以验证确实,SO 异常现在总是发生在相同的堆栈深度。
奖金
与 ASLR 相关,在较早的新闻中:How Chrome got pwned
【问题讨论】:
-
我已经编辑了你的标题。请参阅“Should questions include “tags” in their titles?”,其中的共识是“不,他们不应该”。
-
仅供参考:刚刚尝试不使用
Random,仅打印sz。也会发生同样的情况。 -
我想知道有什么技术可以判断 JIT 是否内联了方法调用。
-
@CristiDiaconescu 在 JIT 编译代码之后在 Visual Studio 中附加一个调试器(通过下拉菜单
Debug->Attach to process或在代码中放置Debugger.Attach())然后转到下拉菜单Debug->Windows->Disassembly查看 JIT 创建的机器代码。请记住,无论您是否附加了调试器,JIT 编译代码的方式都会有所不同,因此请务必在未附加调试器的情况下启动它。 -
+1 用于发布实际上与 StackOverflow 主题相关的问题。可笑的是,有多少人发布根本与堆栈溢出无关的问题!
标签: c# .net stack-overflow jit tail-recursion