我试图弄清楚C#编译器是如何处理尾部调用的.
(回答:They're not.,但64bit 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"的分支).
相反,在+(Target=x64或任何关闭了'prefere 32bit'的CPU(在64位CPU上))上使用'Optimize build'(优化构建),TCE就会发生,计数器会一直旋转(好吧,每次值溢出时,它都会旋转down).
But I noticed a behaviour I can't explain在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提出了P/Invoke、互操作和可能的非确定性锁定的问题.
所以我将代码简化为:
class Program
{
static void Main(string[] args)
{
Rec();
}
static int sz = 0;
static void Rec()
{
sz++;
Rec();
}
}
我在没有调试器的情况下,在Release/32bit/Optimization上运行了它.当程序崩溃时,我连接调试器并判断计数器的值.
而且它在几次运行中都不是相同的.(或者我的测试有缺陷.)
使现代化: Closure
正如fejesjoco所建议的,我研究了ASLR(地址空间布局随机化).
这是一种安全技术,使得缓冲区溢出攻击很难找到(例如)特定的系统调用,通过随机化进程地址空间中的各种内容,包括堆栈位置和它的大小.
The theory sounds good. Let's put it into practice!
为了测试这一点,我使用了特定于该任务的Microsoft工具:EMET or The Enhanced Mitigation Experience Toolkit.它允许在系统或进程级别设置ASLR标志(以及更多).
(还有一款system-wide, registry hacking alternative我没试过)
为了验证工具的有效性,我还发现Process Explorer在流程的"属性"页面中正式报告了ASLR标志的状态.直到今天才看到:)
理论上,EMET可以(重新)为单个进程设置ASLR标志.实际上,它似乎没有改变任何事情(见上图).
然而,我为整个系统禁用了ASLR,并且(一次重启之后)我最终可以验证,确实,SO异常现在总是在相同的堆栈深度发生.
prize
与ASLR相关的旧新闻:How Chrome got pwned