acm-header
登录

ACM通信

实践

未初始化的读取


未初始化的读取、插图

图片来源:Moviestore / Rex / Shutterstock

回到顶部

大多数开发人员都知道在C中读取未初始化的变量是一个缺陷,但有些人还是这么做了——例如,为了创建熵。在C标准的当前版本(C11)中,读取未初始化的对象时会发生什么还没有解决。3.在计划中的C2X标准修订版中,已经提出了各种解决这些问题的建议。因此,这是了解现有行为以及对C语言的发展产生影响的标准修订建议的好时机。鉴于在C11中未初始化的读取行为是不确定的,谨慎要求从代码中消除未初始化的读取。

本文描述了对象初始化不确定的值,陷阱表示然后检查示例程序,说明这些概念对程序行为的影响。

回到顶部

初始化

要理解读取未初始化对象的行为,必须了解如何以及何时初始化对象。

声明其标识符时不带链接(文件作用域对象默认具有内部链接)且不带存储类说明符static的对象自动存储时间。对象的初始值为不确定的。如果为对象指定了初始化,则在每次执行块时到达声明或复合文字时执行初始化;否则,该值在每次到达声明时都变成不确定的。

C11标准第6.7.9小节第10段4描述具有静态或线程存储持续时间的对象如何初始化:

如果没有显式初始化具有自动存储持续时间的对象,则其值为不确定值。如果没有显式初始化具有静态或线程存储持续时间的对象,则:

  • 如果它有指针类型,则初始化为空指针;
  • 如果它具有算术类型,则初始化为(正的或无符号的)零;
  • 如果它是一个聚合,每个成员都根据这些规则初始化(递归地),任何填充都初始化为零位;
  • 如果它是一个联合,则根据这些规则初始化第一个命名成员(递归地),并初始化任何填充为零位。

许多动态分配函数不初始化内存。例如,malloc函数为对象分配空间,该对象的大小由参数指定且值不确定。为realloc函数中,新对象中超过旧对象大小的任何字节都具有不确定的值。

回到顶部

不确定的值

在所有情况下,未初始化的对象都具有不确定值。C标准规定an不确定的值可以是未指定的值或陷阱表示形式。一个未指定的值是相关类型的有效值,而C标准对在任何实例中选择哪个值没有要求。“在任何情况下”这个词都不清楚。这个词实例在英语中被定义为“任何事情的一个案例或事件”,但从上下文看不清楚发生了什么。最明显的解释是,发生是一个读取。9一个陷阱表示是不需要表示对象类型值的对象表示形式。注意,未指定的值不能是trap表示形式。

如果对象的存储值具有trap表示形式,并且由没有字符类型的左值表达式读取,则该行为是未定义的。因此,可以为自动变量分配陷阱表示,而不会导致未定义的行为,但只有在其中存储了适当的值,才能读取该变量的值。

附件J.2“未定义行为”不完整地总结了行为在以下情况下是未定义的:

  • trap表示由没有字符类型的左值表达式读取。
  • 具有自动存储持续时间的对象的值在不确定时使用。

第二个未定义行为更为普遍(至少对于具有自动存储时间的对象而言),因为不确定值包括所有未指定的值和trap表示。这(不正确地)意味着从具有分配、静态或线程存储持续时间的对象中读取不确定值是定义良好的行为,除非trap表示是由没有字符类型的左值表达式读取的。

根据WG14现任召集人David Keaton的说法,在C语言中,读取任何存储时间的不确定值都是隐含的未定义行为,附件J.2中的描述(非规范性的)是不完整的。未定义行为的修订定义可以表述为“当对象的值不确定时读取它”。

不幸的是,对于未初始化的读,委员会或更广泛的社区没有达成共识。Memarian和Sewell在323位C专家中进行了一项调查,以了解他们对系统软件在实践中所依赖的属性的看法,以及当前的实现提供了什么。5调查收集了以下对“是否读取未初始化的变量或结构成员(使用当前主流编译器)”问题的回答:

  • 未定义的行为?139例(43%)
  • 要让包含这个值的表达式的结果变得不可预测吗?42 (13%)
  • 要给出一个任意且不稳定的值(如果您再次读取,可能会得到一个不同的值)?21 (6%)
  • 要给出一个任意但稳定的值(如果再次读取,则会得到相同的值)?112例(35%)

回到顶部

陷阱表示

陷阱表示并不总是被很好地理解,即使是专业的C程序员和编译器作者。6一个陷阱表示是不需要表示对象类型值的对象表示形式。获取陷阱表示可能可以执行trap,但不需要执行。在C语言中执行一个trap将中断程序的执行,直到不再执行进一步的操作。

Trap表示被引入到C语言中,以帮助调试。未初始化的对象可以被分配一个trap表示,这样未初始化的读就会被捕获,从而在开发过程中被程序员检测到。一些编译器编写者倾向于完全消除陷阱表示,并简单地使任何未初始化的读取未定义的行为——理论是,为什么要因为明显的错误代码而阻止编译器优化呢?相反的观点是,为什么要优化明显有问题的代码,而不简单地发出致命的诊断呢?

无符号整数类型。C标准规定,对于除。以外的无符号整数类型无符号字符,一个对象表示分为位值而且填充位(填充位是可选的)。无符号整数类型使用纯二进制表示,称为值表示,但是没有指定任何填充位的值。根据C标准,填充位的某些组合可能会生成陷阱表示—例如,如果一个填充位是a校验位。

奇偶校验位对一组二进制值进行校验,计算方法是:1的个数加上奇偶校验位应该总是偶数(偶尔也应该总是奇数)。早期的计算机有时需要使用奇偶RAM,奇偶校验不能被禁用。从历史上看,错误的内存是相对常见的,而明显的奇偶校验错误也并不罕见。从那时起,由于简单的奇偶校验RAM已经不再使用,错误变得不那么明显了。错误现在是不可见的,因为它们没有被检测到,或者用ECC(错误纠正码)RAM不可见地纠正它们。ECC内存可以检测和纠正最常见的内部数据损坏类型。现代RAM被认为是可靠的,有很多理由,而错误检测RAM在非关键应用中已经很大程度上不再使用。奇偶校验位和ECC位被内存处理单元看到,但程序员看不见。

除了作为溢出等异常条件的一部分以外,对已知值的算术操作不会生成trap表示,而且这对于无符号类型来说不会发生。所有其他填充位的组合都是由值位指定的值的可选对象表示形式。陷阱表示的读取具有未定义的行为。但是,目前还没有任何已知的体系结构为存储在内存中的任何类型的无符号整数实现trap表示_Bool.因此,大多数无符号整数类型的trap表示都是C标准中过时的特性。

_Bool类型是无符号类型的一个特例,它在许多体系结构上具有实际的内存可表示的陷阱表示。值的类型_Bool通常占用一个字节。该字节中除0或1以外的值都是trap表示。因此,一个实现可以假定一个字节读取_Bool对象产生0或1的值,并基于该假设进行优化。GCC (GNU Compiler Collection)就是以这种方式执行的一个例子。

因为将任何非零值转换为类型_Bool结果为值1,类型双关需要创建类型的对象_Bool它包含不表示任何类型值的确定位模式_Bool(因此是当前标准中的陷阱表示)。

未定义的行为可能发生在可能进行优化的推论中。考虑以下代码,例如:

_Bool a, b, c, d, e;
开关(a | (b < < 1) | (c d < < < < 2) | (3) | (e < < 4))

值范围传播可以推断switch参数在0到31的范围内,并在产生表跳转时使用该推断,因此,如果其中一个值超出了范围,那么就会跳转到任意地址,因此switch参数也超出了该范围。没有现有的实现被证明完全忽略表跳转的范围测试。GCC将优化默认情况,并跳转到一个超出范围的参数的其他情况。然而,C标准允许省略范围测试,也可能允许定义的实现省略__STDC __可分析的___。

考虑以下代码:

无符号字符f (
无符号怕羞的
) {
_Bool;/未初始化/ unsigned char x[2] = {0,0};x[一]= 1;

在本例中,可能写入到x(一个)是否会导致一个没有定义的实现的越界存储__STDC __可分析的___。

带符号整数类型。对于有符号整数类型,对象表示的位分为三组:值位、填充位和符号位。填充位不需要;签署了字符特别是不能有填充位。如果符号位为零,则不影响结果值。

C标准支持有符号整数值的三种表示:符号和大小、1的补数和2的补数。实现可以自由选择使用哪种表示,尽管two的补语是最常见的。C标准还规定,对于符号和幅度以及2的补数,带符号位1和所有值位0的值可以是陷阱表示或正常值。对于补位,带符号位1和所有值位1的值可以是陷阱表示或正常值。在符号、大小和补数的情况下,如果这种表示是一个正常值,则称为负零。对于two的补变量,这是该类型的最小(最负)值。

大多数two的补充实现将所有表示都视为正常值。同样,大多数符号大小和补码实现都将负零视为正常值。C标准委员会无法识别任何将这些表示作为陷阱值的当前实现,因此这可能是C标准中未使用和过时的特性。

指针类型。整数可以转换为任何指针类型。结果是实现定义的,可能没有正确对齐,可能没有指向引用类型的实体,并且可能是一个陷阱表示。用于将指针转换为整数和将整数转换为指针的映射函数的目的是与执行环境的寻址结构一致。

浮动点类型。IEC 605592需要两种nan(不是数字):安静和信号。C标准委员会只采用了安静的nan。它没有采用信号nan,因为它认为它们的效用对于支持它们所需的工作来说太有限了。7

IEC 60559浮点标准指定了安静的和有信号的nan,但是这些术语也可以应用于一些非IEC 60559实现。例如,VAX保留操作数和Cray不定数是信号nan。在IEC 60559标准算法中,触发信令NaN参数的操作通常返回一个安静的NaN结果,前提是不捕捉陷阱。对信令nan的完全支持意味着可重新启动的陷阱,例如IEC 60559浮点标准中指定的可选陷阱。C标准支持安静nan的主要实用程序“处理其他棘手的情况,例如为0.0/0.0提供默认值”,如IEC 60559所述。

nan的其他应用可能被证明是有用的。NaN的可用部分被用来编码辅助信息——例如,关于NaN起源的信息。信号nan可能是填充未初始化存储的候选对象,其可用部分可以区分未初始化的浮动对象。IEC 60559信令nan和trap处理程序可能提供维护诊断信息或实现特殊算术的钩子。

然而,C语言对信号nan或可在nan中编码的辅助信息的支持是有问题的。不同的实现对Trap的处理差异很大。实现机制可能以神秘的方式触发或无法触发信号nan。IEC 60559浮点标准建议nan进行传播,但它并不要求这样做,也不是所有的实现都这样做。此外,浮点标准无法通过格式转换指定nan的内容。使信号nan可预测会带来优化限制,这些限制超过了预期的好处。由于这些原因,C标准既没有定义信号NaN的行为,也没有规定NaN意义的解释。

x86扩展精度格式是一种80位格式,首次在Intel 8087数学协处理器中实现,所有基于x86设计的处理器都支持该格式,其中包含浮点单元。伪无穷、伪零、伪nan、非正态和伪正态都是陷阱表示。

Itanium cpu有一个NaT(不是一个东西)标志对于每个整数寄存器。NaT标志用于控制投机性执行,并且可能停留在使用前未正确初始化的寄存器中。一个8位的值可能有多达257个不同的值:0-255和NaT值。然而,C99显式禁止为无符号字符.在C语言中,NaT标志不是一种trap表示,因为trap表示是一种对象表示,而对象是执行环境中数据存储的区域,而不是寄存器标志。8

没有将Itanium的NaT标志归类为陷阱,而是在C11第6.3.2.1节第2段中添加了以下语言来解释NaT标志的可能性:


要理解读取未初始化对象的行为,必须了解如何以及何时初始化对象。


如果左值指定了一个具有自动存储持续时间的对象,该对象可以用寄存器存储类声明(从未取过它的地址),并且该对象未初始化(没有使用初始化式声明且在使用之前没有对其进行赋值),则该行为是未定义的。

这句话被添加到C11中,以支持Itanium NaT标志,使编译器开发人员可以在所有实现中将适用的未初始化读视为未定义的行为。这种未定义的行为甚至适用于对类型对象的直接读取无符号字符.的无符号字符Type在标准中通常具有特殊的地位,即存储在非位域对象中的值可以复制到Type对象中unsigned char [n](例如,通过memcpy),其中n是该类型对象的大小。

回到顶部

示例程序

前面对陷阱表示的回顾清楚地说明了无符号字符类型是最有趣的情况。考虑以下代码:

无符号字符f (
无符号字符y
) {
无符号字符x [1];/单位/
If (x[0] > 10)
返回y / x [0];
其他的
返回10;

无符号字符数组x具有自动存储持续时间,因此未初始化。因为它被声明为数组,所以取了x的地址,这意味着读取是已定义的行为。虽然编译器可以避免接受地址,但它不能将代码的语义从未指定的值更改为未定义的行为。因此,编译器不允许将此代码转换为可能执行陷阱的指令。的对象无符号字符类型保证没有trap值。定义这个示例中的read是因为它来自类型的对象无符号字符而且已知是由记忆支持的。但是,不清楚读取的是哪个值以及该值是否稳定。从这个角度来看,可以认为这种行为是隐式未定义的。至少,该标准是不明确的,可能是矛盾的。

缺陷报告# 45111处理未初始化自动变量的不稳定性问题。建议的委员会对该缺陷报告的回应声明,对不确定值执行的任何操作将导致不确定值。库函数在使用不确定值时将显示未定义的行为。然而,还不清楚y/x[0]是否会导致陷阱。根据委员会对缺陷报告#451的回应,对于所有没有trap表示形式的类型,未初始化的值可能会改变其值,从而允许符合要求的实现打印两个不同的值。

考虑以下代码:

空白f(空白){
无符号字符x [1];/uninit/
x [0] ^ = x [0];
printf (" % d \ n”,x [0]);
printf (" % d \ n”,x [0]);
返回;

在本例中,无符号Char数组x故意未初始化,但不能包含trap表示形式,因为它有字符类型。因此,该值既是不确定值,又是未指定的值。位独占OR操作将在初始化值上产生零,将产生一个不确定的结果,该结果可能为零,也可能不为零。优化编译器有权删除此代码,因为它具有未定义的行为。这两个printf调用显示未定义的行为,因此可能执行任何操作,包括为x[0]打印两个不同的值。

在OpenSSL、DragonFly BSD、OpenBSD和其他地方,未初始化的内存被用作随机数生成器的熵源。10但是,如果访问不确定值是未定义的行为,编译器可能会优化这些表达式,从而得到可预测的值。1

回到顶部

结论

与未初始化读相关的行为是C标准委员会需要在标准的下一个修订版(C2X)中解决的一个尚未解决的问题。一个简单的解决方案是完全消除陷阱表示,并简单地声明读取不确定值是未定义的行为。这将大大简化标准(这本身是有价值的),并为编译器开发人员提供他们想要优化代码的所有自由度。截然相反的解决方案是为未初始化的读取定义完全具体的语义,这样的读取保证提供内存的实际内容。

最有可能的是,将确定一些中间立场,允许编译器优化,但不消除对程序员的所有保证。一种可能是引入一种不稳定的价值这将允许未初始化的对象更改值,而不要求这是未定义的行为。

Trap表示是一个奇怪的现象,因为它们被引入是为了帮助诊断未初始化的读取,但现在受到安全和安全团体的怀疑,他们担心与读取Trap值相关的未定义行为被传递给不确定值的读取。

ACM队列的q戳相关文章
queue.acm.org

通过针眼学习一门语言
Roberto Ierusalimschy等人。
http://queue.acm.org/detail.cfm?id=1983083

跨语言互操作性的挑战
David Chisnall
http://queue.acm.org/detail.cfm?id=2543971

回到顶部

参考文献

1.Debian安全咨询。DSA-1571-1 OpenSSL -可预测随机数生成器,2008;http://www.debian.org/security/2008/dsa-1571

2.独立选举委员会。用于微处理器系统的二进制浮点算法(60559:1989)。

3.ISO / IEC。程序设计语言- c,第三版(ISO/IEC 8999:2011)。瑞士日内瓦。

4.克雷伯斯,维迪克,F. N1793: C11中不定值的稳定性;http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1793.pdf

5.Memarian, K.和Sewell, P.澄清C内存对象模型,2016 (WG14 N2012的修订版)。剑桥大学;http://www.cl.cam.ac.uk/~pes20/cerberus/notes64-wg14.html#clarifying-the-c-memory-object-model-uninitialised-values

6.梅马里安,K.,休厄尔,P. C在实践中是什么?2015(2016)更新。(Cerberus调查v2):回应分析(2014)-附评论;https://www.cl.cam.ac.uk/~pes20/cerberus/notes50-survey-discussion.html

7.开放标准。信令nan的可选支持,2003;http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1011.htm

8.彼得森,R.缺陷报告#338。C99似乎将不确定值排除在未初始化寄存器之外。开放标准,2007;http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_338.htm

9.澄清未指明的价值。开放标准,2016;http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2042.pdf

10.多或少的随机性;http://kqueue.org/blog/2012/06/25/more-randomness-or-less/

11.Wiedijk, F.和Krebbers, R.缺陷报告#451。未初始化自动变量的不稳定性。开放标准,2013;http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_451.htm

回到顶部

作者

罗伯特·c·Seacord是NCC集团的首席安全顾问,在那里他与软件开发人员和软件开发组织合作,在部署之前消除编码错误导致的漏洞。


版权归所有者/作者所有。授权ACM出版权利。

数字图书馆是由计算机协会出版的。版权所有©2017 ACM, Inc.


没有发现记录

登录为完全访问
»忘记密码? »创建ACM Web帐号
文章内容:
Baidu
map