分配超大堆栈结构是不确定的行为?

时间:2014-04-17 19:19:32

标签: c++ c language-lawyer undefined-behavior

这是一个C规范问题。

我们都知道这是合法的C,并且应该可以在任何平台上正常运行:

/* Stupid way to count the length of a number */
int count_len(int val) {
    char buf[256];
    return sprintf(buf, "%d", val);
}

但这几乎可以保证崩溃:

/* Stupid way to count the length of a number */
int count_len(int val) {
    char buf[256000000];
    return sprintf(buf, "%d", val);
}

不同之处在于后一个程序会破坏堆栈并且可能会崩溃。但是,纯粹在语义上,它与以前的程序没有任何不同。

根据C规范,后一个程序实际上是未定义的行为吗?如果是这样,它与前者的区别是什么?如果没有,那么C规范中的说法是否可以使符合要求的实现崩溃?

(如果这在C89 / C99 / C11 / C ++ *之间有所不同,那么这也很有趣。)

4 个答案:

答案 0 :(得分:4)

C(899911)的语言标准以带有此措辞的范围部分开头(也可在某些C ++,C#,Fortran和Pascal标准中找到):< / p>

本国际标准未指明

  • 程序及其数据的大小或复杂程度,超出任何特定数据处理系统的容量或特定处理器的容量;
  • 能够支持符合要求的实施的数据处理系统的所有最低要求。

gcc编译器确实提供了an option to check for stack overflow at runtime

21.1堆栈溢出检查

  

对于大多数操作系统,gcc默认不执行堆栈溢出检查。这意味着如果主环境任务或某个其他任务超出可用堆栈空间,则会发生不可预测的行为。大多数本机系统通过在每个任务堆栈的末尾添加一个保护页面来提供某种程度的保护。这种机制通常不足以正确处理堆栈溢出情况,因为大的局部变量可能会“跳转”到防护页面之上。此外,当命中保护页面时,堆栈上可能没有任何空间用于执行异常传播代码。启用堆栈检查可避免此类情况。   要激活堆栈检查,请使用gcc选项-fstack-check编译所有单元。例如:

gcc -c -fstack-check package1.adb
  

使用此选项编译的单元将生成额外的指令,以检查堆栈的任何使用(用于过程调用或声明块中的局部变量)都不会超过可用的堆栈空间。如果超出空间,则引发Storage_Error异常。

在C99标准化过程中尝试make a stronger statement within the standard,虽然规模和复杂性超出了标准的范围,但实施者有责任记录限制。

理由是

  

一致性的定义一直是C的问题   标准,由一位作者描述为&#34;甚至不是橡胶牙齿,更多   像橡胶口香糖&#34;。虽然C9X与C89相比有所改进,   许多问题仍然存在。

     

本文提出了一些改进,虽然不完美,但希望改进   情况。

建议将以下措辞列入第5.2.4.1节

    如果程序或其数据的大小或复杂性超出实施能力,
  • 翻译或执行可能会失败。
  • 实施应记录一种方法,以确定正确计划的规模或复杂程度是否超出或可能超过实施能力。

      

    5.2.4.1。实现始终可以自由地声明给定的程序   太大或太复杂,无法翻译或执行。但是,要停下来   这是一种声称符合性的方法,同时不提供有用的设施   无论如何,实施者必须提供一种方法来确定是否   计划可能会超出限制。这个方法不一定是完美的,所以   只要它在谨慎方面犯错误。   一种方法是使用一个转换值的公式   作为变量的数量,比如说,编译器的内存量   需要。同样,如果堆栈空间有限制,则公式需要   仅显示如何确定每个函数调用的堆栈要求   (假设这是堆栈分配的唯一位置)并且无需工作   通过每一个可能的执行路径(这是不可能的)   面对递归)。编译器甚至可以有一个输出a的模式   程序中每个函数的值。

  •   
  拟议的措辞未达到C99标准,因此该领域仍然不在标准范围内。 C99的5.2.4.1节确实列出了这些限制

     

实现应能够翻译和执行至少一个包含以下每个限制的至少一个实例的程序:

  • 127个嵌套级别的块
  • 63条件包含的嵌套级别
  • 在声明中修改算术,结构,联合或不完整类型的12个指针,数组和函数声明符(以任意组合)
  • 完整声明符中的括号声明符的嵌套级别
  • 完整表达式中括号内表达式的嵌套级别
  • 内部标识符或宏名称中的63个重要初始字符(每个通用字符名称或扩展源字符被视为单个字符)
  • 外部标识符中的31个重要的初始字符(指定短标识符0000FFFF或更少的每个通用字符名称被认为是6个字符,指定短标识符00010000或更多的每个通用字符名称被认为是10个字符,并且每个扩展源字符被认为是与相应的通用字符名称相同的字符数(如果有的话)
  • 一个翻译单元中的4095个外部标识符
  • 在一个块中声明了块范围的511个标识符
  • 在一个预处理翻译单元中同时定义的4095个宏标识符
  • 一个函数定义中的127个参数
  • 一个函数调用中的127个参数
  • 一个宏定义中的127个参数
  • 一次宏调用中的127个参数
  • 逻辑源行中的4095个字符
  • 字符串文字或宽字符串文字中的4095个字符(连接后)
  • 对象中的65535个字节(仅限托管环境中)
  • #included files的15个嵌套级别
  • 开关语句的1023个案例标签(不包括任何嵌套的switch语句的案例标签)
  • 单一结构或工会中的1023名成员
  • 单个枚举中的1023枚举常量
  • 单个struct-declaration-list中的63个嵌套结构或联合定义

答案 1 :(得分:3)

在C ++中,附件B表明对象的最大大小是特定于实现的有限数。这往往会限制具有自动存储类的数组。

但是,我没有看到调用堆栈上所有自动变量累积的空间,这是应该触发堆栈溢出的地方。我也没有看到附件B中的递归限制,尽管这是密切相关的。

答案 2 :(得分:3)

C标准对与堆栈溢出有关的所有问题都保持沉默。这有点奇怪,因为它几乎在C编程的每个角落都很有声音。 AFAIK没有规定必须有一定数量的自动存储,并且无法检测或恢复可用于自动存储的空间。假设抽象机器具有无限量的自动存储。

答案 3 :(得分:3)

我认为遗漏是不明确的 - 如果 250,000,000字节的本地对象实际上超出了实现的容量。

引用2011 ISO C标准,第1节(范围),第2段:

  

本国际标准未指明
  [...]
   - 程序的大小或复杂性及其数据将超过任何程序的容量   特定数据处理系统或特定处理器的容量

因此,该标准明确承认程序可能超出实施的能力。

我认为我们可以安全地假设超出实现容量的程序不需要表现得与不执行程序的行为相同;否则就没有必要提及它。

由于标准中没有任何内容定义此类程序的行为,因此行为未定义。这由第4节(符合性)第2段规定:

  

[...]
在此以其他方式表示未定义的行为   国际标准中的“未定义行为”或由   省略任何明确的行为定义。没有   这三者之间的重点不同;他们都描述了“行为   这是未定义的“。

当然,大多数现代计算机上的实现都可以轻松分配2.5亿字节的内存;例如,这只是我打字的计算机上可用内存的一小部分。但是许多操作系统对程序可以分配的堆栈空间量设置了相当低的限制。

(顺便说一句,我假设问题中的代码是一个实际调用函数的完整程序的片段。就目前而言,代码没有行为,因为没有调用count_len,也没有main函数。我通常不会提到这一点,但你 使用“language-lawyer”标签。)

任何认为行为未定义的人都应该解释(a)为什么程序崩溃不会使实现不符合要求,或者(b)程序崩溃如何在定义的行为范围内(甚至如果它是实现定义的或未指定的。)