为什么无符号整数容易出错?

问题描述 投票:57回答:8

我在看this videoBjarne Stroustrup说无符号整数容易出错并导致错误。所以,你应该只在你真正需要的时候使用它们。我还读过有关Stack Overflow的问题之一(但我不记得哪一个)使用无符号整数会导致安全漏洞。

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

c++ unsigned-integer
8个回答
45
投票

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

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

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

还有其他原因提到Danger – unsigned types used here!,在我看来,其中最强大的是签名和未签名之间的隐式类型转换。


33
投票

一个重要因素是它使循环逻辑变得更难:想象一下,你想迭代除了数组的最后一个元素(在现实世界中确实发生)。所以你写下你的功能:

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 < SIZE_T_MAX。灾难,UB,最有可能崩溃!

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

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


22
投票

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

#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进行比较,给出一个大的正数和一个令人惊讶的结果。


10
投票

虽然它可能只被视为现有答案的变体:参考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的错误传递值和非常大的数组分配。


3
投票

unsigned int的一个大问题是,如果从unsigned int 0中减去1,结果不是负数,结果不小于您开始使用的数字,但结果是最大可能的unsigned int值。

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

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

这就是使unsigned int容易出错的原因。当然unsigned int的工作原理与它的设计完全相同。如果你知道自己在做什么并且没有犯错,那绝对安全。但大多数人都会犯错误。

如果您使用的是良好的编译器,则打开编译器生成的所有警告,它会告诉您何时执行可能存在错误的危险事件。


1
投票

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

  1. 小于int的无符号类型(例如uint8)保持0..2ⁿ-1范围内的数字,并且使用它们的计算将根据整数运算的规则表现,只要它们不超过int类型的范围。根据现行规则,如果这样的计算超出了int的范围,编译器就可以用代码做任何它喜欢的事情,甚至可以否定时间和因果关系的规律(有些编译器会这样做!) ,即使计算结果将被分配回小于int的无符号类型。
  2. 无符号类型unsigned int和更大的保持成员的抽象包裹代数环的整数全等mod2ⁿ;这实际上意味着如果计算超出范围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 ++要定义新的无符号类型,这个问题可以彻底解决[例如, unum32_t和uwrap32_t]这样,无论unum32_t的大小如何,int总是表现为数字(如果int为32位,可能需要将减法或一元减法的右手操作提升为下一个更大的有符号类型或虽然wrap32_t总是表现为代数环的成员(即使int大于32位也阻止促销)。然而,在没有这种类型的情况下,编写既便携又干净的代码通常是不可能的,因为可移植代码通常需要在整个地方进行类型强制。


1
投票

C和C ++中的数字转换规则是拜占庭式的混乱。使用无符号类型比使用纯符号类型更大程度地暴露自己。

例如,两个变量之间进行比较的简单情况,一个是有符号的,另一个是无符号的。

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

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

  • 如果操作数大小大于或等于int的大小,则乘法将具有定义的环绕语义。
  • 如果操作数大小小于int但大于或等于int大小的一半,则可能存在未定义的行为。
  • 如果操作数大小小于int大小的一半,则乘法将产生数值正确的结果。

-2
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.