我试图弄清楚 C # 编译器是如何处理尾部调用的。
(答案: 他们不是。但是 64位 JIT (s)会做 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 结束:
相反,使用“优化构建”ON + (Target = x64或 AnyCPU 与“偏好32位”关闭(在64位 CPU 上)) ,TCE 发生,计数器永远不停地旋转(好吧,它可以说旋转 放下每次其值溢出)。
但是我注意到在 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.
堆栈大小不变(默认值为1MB)。堆栈帧的大小不变。
那么,什么可以解释当 StackOverflowException
到达时堆栈深度(有时是非平凡的)的变化呢?
Hans Passant 提出了 Console.WriteLine
涉及 P/Invoke、互操作和可能的非确定性锁的问题。
所以我把代码简化成这样:
class Program
{
static void Main(string[] args)
{
Rec();
}
static int sz = 0;
static void Rec()
{
sz++;
Rec();
}
}
我在没有调试器的情况下在32位发布/优化运行它。当程序崩溃时,我附加调试器并检查计数器的值。
而且它的 还是在几次运行中都不一样。(或者我的测试有缺陷。)
按照 fejesjoco 的建议,我查了一下 ASLR (位址空间配置随机载入)。
这是一种安全技术,通过随机化进程地址空间中的各种东西,包括堆栈位置和显然的大小,使得缓冲区溢出攻击很难找到(例如)特定系统调用的精确位置。
这个理论听起来不错。让我们把它付诸实践吧!
为了测试这一点,我使用了一个特定于此任务的 Microsoft 工具: EMET 或增强缓解经验工具包。它允许在系统或进程级别设置 ASLR 标志(以及更多)。
(还有一个我没有试过的 系统范围的注册表黑客替代方案)
为了验证该工具的有效性,我还发现 进程资源管理器在过程的“属性”页面中适时地报告了 ASLR 标志的状态。直到今天才知道
理论上,EMET 可以(重新)为单个进程设置 ASLR 标志。实际上,它似乎没有改变任何东西(见上图)。
然而,我为整个系统禁用了 ASLR,(一次重启后)我终于可以确认,SO 异常现在总是发生在相同的堆栈深度。
与 ASLR 相关,旧新闻: Chrome 是怎么被打败的