为啥递归调用会导致不同堆栈深度的 ***?
Posted
技术标签:
【中文标题】为啥递归调用会导致不同堆栈深度的 ***?【英文标题】:Why does a recursive call cause *** at different stack depths?为什么递归调用会导致不同堆栈深度的 ***? 【发布时间】:2013-12-13 06:54:16 【问题描述】:我试图弄清楚 C# 编译器如何处理尾调用。
(答案:They're not. 但 64 位 JIT(s) 将执行 TCE(尾调用消除)。Restrictions apply.)
所以我使用递归调用编写了一个小测试,它打印了在***Exception
终止进程之前它被调用了多少次。
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 每次它的值溢出)。
但我注意到***Exception
案例中的一种我无法解释的行为:它从来没有(?)发生在完全相同相同的堆栈深度。以下是一些 32 位运行的输出,发布版本:
51600 Random: 1778264579
Process is terminated due to ***Exception.
51599 Random: 1515673450
Process is terminated due to ***Exception.
51602 Random: 1567871768
Process is terminated due to ***Exception.
51535 Random: 2760045665
Process is terminated due to ***Exception.
并调试构建:
28641 Random: 4435795885
Process is terminated due to ***Exception.
28641 Random: 4873901326 //never say never
Process is terminated due to ***Exception.
28623 Random: 7255802746
Process is terminated due to ***Exception.
28669 Random: 1613806023
Process is terminated due to ***Exception.
堆栈大小是恒定的 (defaults to 1 MB)。堆栈帧的大小是恒定的。
那么,当***Exception
命中时,什么可以解释堆栈深度的(有时不是微不足道的)变化?
更新
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 用于发布实际上与 *** 主题相关的问题。可笑的是,有多少人发布根本与堆栈溢出无关的问题!
【参考方案1】:
我想可能是ASLR 在工作。你可以关闭 DEP 来测试这个理论。
查看这里的 C# 实用程序类来检查内存信息:https://***.com/a/8716410/552139
顺便说一句,使用这个工具,我发现最大和最小堆栈大小之间的差异在 2 KiB 左右,也就是半页。这很奇怪。
更新:好的,现在我知道我是对的。我跟进了半页理论,发现了这个检查 Windows 中的 ASLR 实现的文档:http://www.symantec.com/avcenter/reference/Address_Space_Layout_Randomization.pdf
引用:
一旦堆栈被放置,初始堆栈指针将进一步 通过随机递减量随机化。初始偏移量为 选择为最多半页(2,048 字节)
这就是您问题的答案。 ASLR 随机取走初始堆栈的 0 到 2048 个字节。
【讨论】:
以前从未听说过 ASLR。到目前为止 +1 - 我喜欢学习新事物。明天测试。 @Hans:赛门铁克的研究准确地说,堆栈被随机偏移,最多半页,因此有效地减小了大小。 好的,我更喜欢你的解释。【参考方案2】:将r.Next()
更改为r.Next(10)
。 ***Exception
s 应该出现在相同的深度。
生成的字符串应该消耗相同的内存,因为它们具有相同的大小。 r.Next(10).ToString().Length == 1
总是。 r.Next().ToString().Length
是可变的。
如果您使用r.Next(100, 1000)
,同样适用
【讨论】:
不,它停在不同的深度。即使您完全删除随机数。 它适用于我(在 DEBUG 和 RELEASE 模式下)。 (XP SP3 - VS 2K8 - .NET 3.5) 虽然说得通,但对我来说不是(Win7 64 SP1,VS 2010,.net 4.5) 在 XP 中不是随机的,因为它没有 ASLR。 @AhmedKRAIEM,你确定它对随机种子也不起作用吗?所谓的字符串长度差异不应影响堆栈。毕竟,堆栈只保存(相同数量和大小)指针,指向在堆上分配的字符串。 (此外,在关闭 ASLR 后,它对我的两种方式都有效)以上是关于为啥递归调用会导致不同堆栈深度的 ***?的主要内容,如果未能解决你的问题,请参考以下文章