【发布时间】:2019-05-16 19:40:10
【问题描述】:
注意:删除了 printf 部分,因为它在另一篇文章中进行了解释。
我几年前从 K&R 第 2 版中学习了 C。我有一段时间没有使用 C,所以我决定浏览一本更现代的书。 “C How to Program, 8th Edition” 由 Deitel 和 Deitel(由 Pearson 于 2015 年出版)提供,具有 C99 和 C11。
与 K&R 不同,他负责安全工作。令我惊讶的一件事是整数溢出。他写道:
第 3.13 节安全 C 编程 • 添加整数会导致 值太大而无法存储在 int 变量中。这被称为 算术溢出并可能导致不可预测的运行时行为, 可能会使系统容易受到攻击。
他在另一个页面:
在执行之前确保 图 2.5 的第 18 行中的算术计算,它们将 不溢出。执行此操作的代码显示在 CERT 网站上 https://www.securecoding.cert.org — 只需搜索指南“INT32-C”。
如果您查看他们推荐的代码:
5.3.3.2 合规解决方案这种合规解决方案确保加法操作不会溢出,无论表示如何:
#include <limits.h>
void f(signed int si_a, signed int si_b)
{
signed int sum;
if (((si_b > 0) && (si_a > (INT_MAX - si_b))) ||
((si_b < 0) && (si_a < (INT_MIN - si_b)))) {
/* Handle error */
}
else {
sum = si_a + si_b;
}
/* ... */
}
我的理解是,尽管 unsigned int 行为是未定义的,但它始终是固定大小的位。在我的电脑上,它是一个 32 位的整数。所以在我的笔记本电脑上,我的 INT_MAX = 2147483647,如果我给它加 1,我得到 -2147483648。如果我继续向它添加一个,它最终会变为 0,然后回到 INT_MAX 并继续循环。我看不出有人使用我的代码如何攻击它?
要添加额外的代码,每次添加整数似乎都非常浪费,除非我确实需要注意可变性,而不仅仅是得到错误的结果。
编辑:由于下面的讨论,我将引用重新添加:
避免使用单一参数
printfs。一个这样的指导方针是避免使用 带有单个字符串参数的 printf。如果需要显示字符串 以换行符结束,使用puts函数,它显示 它的字符串参数后跟换行符。例如,在 图 2.1,第 8 行
printf( "Welcome to C!\n" );应该写成:puts( "Welcome to C!" );我们没有在前面的字符串中包含 \n 因为puts会自动添加它。如果需要显示字符串 没有终止换行符,使用 printf 和两个 arguments — "%s" 格式控制字符串和要显示的字符串。这 %s 转换说明符用于显示字符串。例如,在 图 2.3,第 8 行
printf( "Welcome " );应该写成:
printf( "%s", "Welcome " );虽然
printf在本章中写成 其实不是不安全的,这些变化是负责任的编码 将消除某些安全漏洞的做法,因为我们 深入了解 C。
【问题讨论】:
-
问题是未定义的行为是未定义的,有符号整数溢出并不总是环绕。编译器可以假设未定义的行为永远不会发生,因此他们可能会优化您的代码,使溢出导致可利用的行为,而不仅仅是环绕。你必须改掉对 UB 做出假设的习惯。
-
@curiousguy:为什么你认为使用
volatile会影响整数算术溢出的行为?您能否提供任何解释您的建议的权威参考资料? -
@curiousguy:我想我理解
volatile的作用——我一点也不明白为什么你认为它会对整数溢出的行为产生任何影响。 “全部”volatile确实是说编译器必须在源代码引用它时对 volatile 限定变量进行适当的引用,因此它不能(例如)优化读取,假设该值仍然与上次读取它的时间——这与读取值后如何评估整数表达式完全无关,这与溢出有关。 -
如果你想保证有符号整数的环绕行为,nemequ 建议将标志传递给你的特定编译器,强制它对有符号整数使用环绕语义是唯一可靠的方法。
-
@curiousguy 请先阅读并理解this article,然后再继续这个“更小、更快地实现 int ops”的口头禅)
标签: c