首页 文章

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

提问于
浏览
57

我在看this video . Bjarne Stroustrupunsigned ints 容易出错并导致错误 . 所以,你应该只在你真正需要的时候使用它们 . 我记得哪一个使用_2583169可以导致安全漏洞 .

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

8 回答

  • 33

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

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

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

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

  • 22

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

    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 . )

  • 10

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

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

  • 3

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

    问题是,无法检测到界面客户端可能产生的某些错误(如果他们能够制作它们,他们就会制作它们) .

    给出的例子是:

    模板<class T>
    class Array {
    上市:
    数组(unsigned int size);
    ...

    以及此类的可能实例化

    int f(); // f和g是返回的函数
    int g(); //整数;他们所做的并不重要
    数组<double> a(f() - g()); //数组大小是f() - g()

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

  • 45

    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

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

    • 小于 int 的无符号类型(例如 uint8 )保持0..2ⁿ-1范围内的数字,并且使用它们的计算将按照整数运算的规则运行,前提是它们不超出 int 类型的范围 . 根据现行规则,如果这样的计算超出了 int 的范围,编译器就可以用代码做任何喜欢的事情,甚至可以否定时间和因果关系的规律(有些编译器会这样做!) ,即使计算结果将被分配回小于 int 的无符号类型 .

    • 无符号类型 unsigned int 和更大的保持成员的抽象包裹代数环的整数全等mod2ⁿ;这实际上意味着如果计算超出范围0..2ⁿ-1,系统将添加或减去2 ^所需的任何倍数,以使值回到范围内 .

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

    • 如果 int 大于32位,则表达式将从数字1中减去数字2,得到数字-1 . 请注意,虽然 uint32_t 类型的变量不能保持值-1,无论 int 的大小如何,并且存储-1将导致这样的变量保持0xFFFFFFFF,但除非或直到该值被强制转换为无符号类型将表现得像签名数量-1 .

    • 如果 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位也阻止促销) . 然而,在没有这种类型的情况下,编写既便携又干净的代码通常是不可能的,因为可移植代码通常需要在整个地方进行类型强制 .

  • -2

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

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

    • 如果两个操作数小于int,则它们都将转换为int,并且比较将给出数值正确的结果 .

    • 如果无符号操作数小于带符号操作数,则两者都将转换为带符号操作数的类型,并且比较将给出数值正确的结果 .

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

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

    • 如果操作数大小大于或等于int的大小,则乘法将具有定义的环绕语义 .

    • 如果操作数大小小于int但大于或等于int大小的一半,则可能存在未定义的行为 .

    • 如果操作数大小小于int的一半,则乘法将产生数值正确的结果 .

  • 1

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

相关问题