我试图弄清楚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结束:
相反,使用'优化构建'ON +(目标= x64或AnyCPU,'偏好32位'关闭(在64位CPU上)),TCE发生并且计数器一直保持旋转(好吧,它可以说是旋转每当它的值溢出时,。
但是我注意到StackOverflowException
案例中我无法解释的行为:它从未(?)发生在完全相同的堆栈深度。以下是几个32位运行的输出,Release build:
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.
和Debug build:
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(地址空间布局随机化)。
这是一种安全技术,通过随机化进程地址空间中的各种内容(包括堆栈位置以及显然其大小),使缓冲区溢出攻击很难找到(例如)特定系统调用的精确位置。 / p>
理论听起来不错。让我们付诸实践吧!
为了对此进行测试,我使用了专门用于该任务的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
答案 0 :(得分:51)
我认为可能ASLR正在工作。你可以关闭DEP来测试这个理论。
请参阅此处了解用于检查内存信息的C#实用程序类:https://stackoverflow.com/a/8716410/552139
顺便说一句,使用这个工具,我发现最大和最小堆栈大小之间的差异大约是2 KiB,这是半页。这很奇怪。
更新:好的,现在我知道我是对的。我对半页理论进行了跟进,发现这篇文档检查了Windows中的ASLR实现:http://www.symantec.com/avcenter/reference/Address_Space_Layout_Randomization.pdf
引用:
放置堆栈后,初始堆栈指针会更远 随机递减量随机化。初始偏移是 选择最多半页(2,048字节)
这就是你问题的答案。 ASLR随机取出初始堆栈的0到2048个字节。
答案 1 :(得分:-3)
将r.Next()
更改为r.Next(10)
。 StackOverflowException
应该出现在同一深度。
生成的字符串应使用相同的内存,因为它们具有相同的大小。 r.Next(10).ToString().Length == 1
始终。 r.Next().ToString().Length
是可变的。
如果您使用r.Next(100, 1000)