我试图弄清楚C#编译器如何处理尾部调用。
(答案:不是。但是64位JIT将执行TCE(尾部调用消除)。有限制条件。)
因此,我使用递归调用编写了一个小型测试,该测试打印了在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中的新功能,也是我第一次看到它。更多内容请点击此处。)
- 代码中一些看似无害的分支(请参阅注释为“ else”的分支)。
相反,使用“优化构建”为ON +(目标= x64或“首选32位”为OFF(在64位CPU上)为AnyCPU),TCE发生并且计数器一直持续旋转(好的,可以说每次值溢出时它都会旋转)。)。
但是我注意到在这种StackOverflowException
情况下无法解释的行为:它永远不会在完全相同的堆栈深度下发生?以下是一些32位运行版本Release的输出:
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.
堆栈大小是恒定的(默认为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或“增强缓解经验工具包”。它允许在系统级或进程级设置ASLR标志(以及更多)。
(还有我没有尝试过的系统范围的注册表黑客替代方法)
为了验证该工具的有效性,我还发现Process Explorer在该过程的“属性”页面中适当报告了ASLR标志的状态。直到今天才见到它:)
从理论上讲,EMET可以为单个进程(重新)设置ASLR标志。实际上,它似乎没有任何改变(参见上图)。
但是,我在整个系统上禁用了ASLR,并且(稍后重新启动)我最终可以确认确实如此,因此SO异常现在始终在相同的堆栈深度发生。
奖金
与ASLR相关的较旧的新闻:Chrome是如何伪装的