【问题标题】:Why are unsigned integers error prone?为什么无符号整数容易出错?
【发布时间】:2015-08-04 09:19:13
【问题描述】:

我在看this videoBjarne Stroustrup 表示 unsigned ints 容易出错并导致错误。因此,您应该只在真正需要它们时才使用它们。我还阅读了有关 StackOverflow 的一个问题(但我不记得是哪一个),使用 unsigned ints 会导致安全错误。

它们如何导致安全漏洞?有人可以通过给出一个合适的例子来清楚地解释它吗?

【问题讨论】:

  • 我强烈主张使用无符号类型。如果你得到错误的循环条件,你就是一个糟糕的开发者。让它与无符号整数一起工作是非常简单的数学运算,对我来说,数量是无符号的感觉更自然
  • 问题是大多数开发人员都很糟糕......
  • 他们当然可以放大一个错误。考虑奖励一个人 $2^32-1$ 美分的 VLT。 thestar.com/news/ontario/2009/03/18/… 当然,有符号数也有类似的问题,最小的距离最大的只有一个,但由于我们经常在 0 附近玩,所以无符号数更接近悬崖边缘。
  • 有符号整数也容易出错。当移动“字节”值产生奇怪的结果时,我花了一个小时在 Java 中调试一个问题。这是由于促销和标志延期。我宁愿两者兼得,并为工作选择正确的类型。
  • @MattiVirkkunen:除了有符号和无符号之外,我宁愿拥有具有显式包装语义、显式检查语义、松散 mod 2ⁿ 语义和溢出等于 UB 语义的类型。分离出不同种类的整数可以编写出比目前可用的类型和与之相关的规则更可移植、更健壮和更优化的代码[在许多情况下需要更小的有符号类型来使用干净的包装语义,但允许对较小的无符号类型进行数学运算以生成未定义的行为]。

标签: c++ unsigned-integer


【解决方案1】:

一个重要因素是它使循环逻辑更难:想象一下,您想要遍历数组中除最后一个元素之外的所有元素(这在现实世界中确实发生过)。所以你写你的函数:

void fun (const std::vector<int> &vec) {
    for (std::size_t i = 0; i < vec.size() - 1; ++i)
        do_something(vec[i]);
}

看起来不错,不是吗?它甚至可以以非常高的警告级别干净地编译! (Live) 所以你把它放在你的代码中,所有的测试都运行顺利,你就忘了它。

现在,稍后,有人过来将一个空的vector 传递给您的函数。现在有了一个有符号整数,你希望你会注意到sign-compare compiler warning,引入了适当的演员表,并且一开始就没有发布错误的代码。

但是在您使用无符号整数的实现中,您进行了换行并且循环条件变为i &lt; SIZE_T_MAX。灾难,UB,很可能崩溃!

我想知道它们是如何导致安全漏洞的?

这也是一个安全问题,特别是buffer overflow。一种可能利用这一点的方法是do_something 会做一些攻击者可以观察到的事情。他们可能能够找到do_something 的输入内容,这样攻击者不应该能够访问的数据就会从你的内存中泄露出去。这将是类似于Heartbleed bug 的场景。 (感谢棘轮怪胎在comment 中指出这一点。)

【讨论】:

  • 我一直对这个所谓的反例感到不安。确实,仅通过近视的代码,您会认为有符号整数在这里更好。但是,这忽略了更大的算法问题:该算法显然希望特别对待范围的最后一个元素。因此,这个算法应该有某种先决条件或分支,以确保范围最后一个元素!有了这样的分支,无符号整数就可以正常工作了。
  • 为什么每个人都必须在这里使用减法?为什么不for (std::size_t i = 0; i + 1 &lt; vec.size(); ++i)
  • @SiyuanRen 我用减法因为它是错误的。这个问题和答案的重点是突出潜在的错误。没有人试图争辩说这些错误是不可修复或不可避免的。我只是争辩说这样的事情可能会发生,而且会很糟糕。所以是的,你可以使用你的代码,然后有正确的代码。关键是一个可以(有点容易)弄错(就像我在回答中故意做的那样)。
  • 再次,这是糟糕的代码。不错的变量类型。不成立。整数不容易出错。 编程容易出错。
  • @fyngyrz:恕我直言,unsigned int 在想要执行模运算的情况下是一种非常好的变量类型,但它在语义上是不合适的 [不是“坏” ] 在一个表示数量的情况下键入。
【解决方案2】:

一个可能的方面是无符号整数可能会导致循环中有些难以发现的问题,因为下溢会导致大量数字。我数不清(即使是无符号整数!)我做了多少次这个错误的变种

for(size_t i = foo.size(); i >= 0; --i)
    ...

请注意,根据定义,i &gt;= 0 始终为真。 (首先导致这种情况的原因是,如果 i 已签名,编译器将警告 size_tsize_t 可能溢出)。

Danger – unsigned types used here! 提到了其他原因,在我看来,其中最强烈的原因是有符号和无符号之间的隐式类型转换。

【讨论】:

  • 我会接受这个答案,因为这是编译器唯一不会发出警告的答案
  • @AndyT 获得更好的编译器。 coliru.stacked-crooked.com/a/c79fc9148dfb5f3f
  • @AndyT 顺便说一句,与上面不同,我的示例实际上没有收到警告。 :)
  • 使用时间operator--&gt; (go down to):for (size_t i = sz; i --&gt; 0;) ...sz-1 迭代到0
  • 这并没有说明无符号整数存在问题。这表明代码本身存在问题。提倡避免使用适合工作的工具,因为它们可能会被糟糕地使用,这对任何人都没有任何好处。只是不要用得不好。
【解决方案3】:

我不会为了回答问题而观看视频,但一个问题是,如果混合使用有符号值和无符号值,可能会发生令人困惑的转换。例如:

#include <iostream>

int main() {
    unsigned n = 42;
    int i = -42;
    if (i < n) {
        std::cout << "All is well\n";
    } else {
        std::cout << "ARITHMETIC IS BROKEN!\n";
    }
}

升级规则意味着将i转换为unsigned进行比较,给出一个很大的正数和一个令人惊讶的结果。

【讨论】:

  • 否决票的任何理由?如果有错误,我想更正答案。
  • 没有投反对票,只是一个猜测:如果你的编译器允许你这样做,那么你编译的警告标志太少了
  • @example - 你的编译器必须让你这样做;代码格式正确,含义明确。当然,警告可能有助于发现逻辑错误,但这不是编译器的主要职责。
  • 通过在unsigned n=2; int i=-1, j=1; 之间进行比较可以使结果更有趣,然后会发现n &lt; ii &lt; jj &lt; n 都是正确的。
  • 文本应为“C++ IS BROKEN”。 @PeteBecker 说“它的含义很明确”;形式上这是对的,但这个定义在数学上是荒谬的。如果要生成整数结果,则很难避免将 i 转换为 unsigned,但对于比较而言,正确定义语言是微不足道的。甚至 COBOL had has On size error,但 C(++) 只是给了你足够的绳索让你上吊!在 VMS 上,DEC C(不知道 ++)警告有符号/无符号比较/赋值,也非常正确(考虑到损坏的语言),
【解决方案4】:

unsigned int 最大的问题是,如果你从 unsigned int 0 中减去 1,结果不是负数,结果不小于你开始时的数字,但结果是可能的最大数无符号整数值。

unsigned int x = 0;
unsigned int y = x - 1;

if (y > x) printf ("What a surprise! \n");

这就是导致 unsigned int 容易出错的原因。当然 unsigned int 完全按照它的设计工作。如果您知道自己在做什么并且不犯错误,那绝对是安全的。但大多数人都会犯错误。

如果你使用了一个好的编译器,你打开编译器产生的所有警告,它会告诉你什么时候你做了很可能是错误的危险事情。

【讨论】:

  • 一个更糟糕的问题是,给定的 uint32_t x,y,z; 表达式,如 x-y &gt; z,在 32 位和 64 位系统上的含义将截然不同。
  • @supercat afaict 它在 LP32、LP64 和 LLP64 系统上会有相同的结果。只有 ILP64 系统会有所不同。
  • @plugwash:我应该澄清一下——int 是 64 位的系统。恕我直言,标准将受益于定义非促进类型,其行为在所有接受使用它们的代码的编译器上都是一致的。使用wrap32_t 的操作应该尽可能产生该类型的结果,或者完全拒绝编译(例如,因为编译器不支持所需的语义,或者因为例如代码试图添加wrap16_twrap32_t一起——一个不可能产生满足两个约束的结果的动作。
【解决方案5】:

除了无符号类型的范围/扭曲问题。混合使用无符号和有符号整数类型会影响处理器的重大性能问题。少于浮点转换,但很多忽略这一点。此外,编译器可以对值进行范围检查并更改进一步检查的行为。

【讨论】:

  • 您能否详细说明哪些重要的性能问题,并给出示例代码?
  • 如果您将unsigned 转换为int 或反之亦然,则二进制表示可以准确识别。因此,当您将一个转换为另一个时,CPU 不会产生任何开销。
  • (假设 C++ 实现使用二进制补码表示负整数)
  • @example 二进制布局不一样。无符号值占据所有位空间(8,16,32,64),但有符号具有符号的最高有效位,将值空间减少 1 位。在 SIMD 指令的情况下,没有人在一条指令中对两种类型都执行计算。发生饱和转换,即性能下降。
【解决方案6】:

虽然它可能只被视为现有答案的变体:参考 Scott Meyers 的 "Signed and unsigned types in interfaces," C++ Report, September 1995,在 接口 中避免无符号类型尤为重要。

问题在于无法检测到接口的客户端可能犯的某些错误(如果他们可以犯,他们犯)。

这里给出的例子是:

template <class T>
  class Array {
  public:
      Array(unsigned int size);
  ...

以及此类的可能实例化

int f(); // f and g are functions that return
int g(); // ints; what they do is unimportant
Array<double> a(f()-g()); // array size is f()-g()

f()g() 返回的值的差异可能是负数,原因有很多。 Array 类的构造函数将接收此差异作为隐式转换为 unsigned 的值。因此,作为Array 类的实现者,我们无法区分-1 的错误传递值和非常大的数组分配。

【讨论】:

  • 对于引用或值难道不是同样的论点吗?显然有人可能错误地将空指针传递给Array&lt;double&gt;(*ptrToSize)
  • @josefx :你可以检查一下。 assert(ptr != nullptr) 可能就足够了。像assert(size &lt; theSizeThatIsLikelyToBeAllocated) 这样的东西不起作用。当然,仍然可以将 API 与签名类型一起滥用。它更难,并且可以覆盖最可能的错误(由隐式转换等原因引起)。
【解决方案7】:

无符号整数类型的问题在于,根据它们的大小,它们可能代表两种不同的事物之一:

  1. 小于int(例如uint8)的无符号类型将数字保持在0..2ⁿ-1范围内,并且使用它们的计算将根据整数算术规则进行,前提是它们不要超出int 类型的范围。根据目前的规则,如果这样的计算超出了int的范围,编译器可以对代码做任何它喜欢的事情,甚至可以否定时间和因果律(一些编译器会这样做!),即使计算结果将被分配回小于int 的无符号类型。
  2. 无符号类型unsigned int 和更大的持有整数全等模2ⁿ 的抽象环绕代数环的成员;这实际上意味着如果计算超出范围 0..2ⁿ-1,系统将添加或减去 2ⁿ 所需的任何倍数以使值回到范围内。

因此,给定uint32_t x=1, y=2;,表达式x-y 可能具有两种含义之一,具体取决于int 是否大于32 位。

  1. 如果int 大于32 位,则表达式将从数字1 中减去数字2,得到数字-1。请注意,虽然uint32_t 类型的变量不能保持值 -1 而不管int 的大小如何,存储任一 -1 都会导致此类变量保持 0xFFFFFFFF,但除非或直到该值被强制为一个无符号类型,它的行为类似于有符号数量 -1。
  2. 如果int 是32 位或更小,则表达式将产生uint32_t 值,当添加到uint32_t 值2 时,将产生uint32_t 值1(即uint32_t 值0xFFFFFFFF )。

恕我直言,如果 C 和 C++ 定义新的无符号类型 [e.g. unum32_t 和 uwrap32_t] 这样unum32_t 将始终表现为一个数字,而不管int 的大小(如果@ 987654340@ 是 32 位或更小),而 wrap32_t 始终表现为代数环的成员(即使 int 大于 32 位也会阻止提升)。然而,在没有此类类型的情况下,通常不可能编写既可移植又干净的代码,因为可移植代码通常需要到处进行类型强制。

【讨论】:

  • 一个完全令人困惑的答案。你是说无符号整数的包装和提升规则取决于它们的大小以及“base”int 的大小?
  • @MartinBa:是的,他就是这么说的。既然你理解了,我想这并不令人困惑,但可能会让一些人感到惊讶 :-) 小于 int 的整数类型是完整的 PITA,尤其是无符号的。
  • @MartinBa:答案令人困惑,因为基本规则是。我在前几点上添加了一些内容;有帮助吗?
  • @MartinBa:小于int 的无符号类型将被提升为有符号 int,只要对其执行任何计算。在常见的 32 位机器上,这在 uint8_tuint16_t 类型中最为常见。当无符号值表示数量时,升级到int 通常很有用,但在它们表示应该包装的东西的情况下可能是灾难性的。请注意,给定uint16_t x=65533; x*=x; 的编译器,用于unsigned int 为16 位或大于32 位的系统,必须设置x=9,但在unsigned 为17 到32 位的系统上...
  • @MartinBa:虽然您是正确的,实际上几乎所有的实现都提供了“包装带符号的 int”选项,但存在一些弱点:(1)C 程序没有标准的方法可以请求这样的语义,或者如果编译器不能提供它们,则拒绝编译; (2) 要求整数值(无论是有符号还是无符号)包装会排除许多通常有用的优化(尽管有时是灾难性的)。我真的很想看到 C 提供各种不同类型的整数,它们选择了不同的语义来提供许多良好的优化机会......
【解决方案8】:

C 和 C++ 中的数字转换规则是拜占庭式的混乱。与使用纯有符号类型相比,使用无符号类型更容易让自己陷入混乱。

以两个变量之间比较的简单情况为例,一个有符号,另一个无符号。

  • 如果两个操作数都小于 int,那么它们都将被转换为 int,并且比较将给出正确的数值结果。
  • 如果无符号操作数小于有符号操作数,则两者都将转换为有符号操作数的类型,并且比较将给出正确的数值结果。
  • 如果无符号操作数的大小大于或等于有符号操作数,并且大小也大于或等于 int,则两者都将转换为无符号操作数的类型。如果有符号操作数的值小于零,这将导致数值不正确。

再举一个例子,考虑将两个相同大小的无符号整数相乘。

  • 如果操作数大小大于或等于 int 的大小,则乘法将具有定义的环绕语义。
  • 如果操作数大小小于 int 但大于或等于 int 大小的一半,则可能会出现未定义的行为。
  • 如果操作数大小小于 int 大小的一半,则乘法将产生正确的数值结果。将此结果分配回原始无符号类型的变量将产生已定义的环绕语义。

【讨论】:

    猜你喜欢
    • 2022-01-20
    • 1970-01-01
    • 1970-01-01
    • 2012-11-16
    • 1970-01-01
    • 2016-02-17
    • 2011-08-10
    • 2018-10-17
    相关资源
    最近更新 更多