为什么无符号整数容易出错?
我正在看这个video 。 Bjarne Stroustrup说无符号整数很容易出错并导致错误。 所以,你只有在真正需要的时候才能使用它们。 我也读过Stack Overflow的一个问题(但我不记得是哪一个),使用unsigned int可能导致安全漏洞。
他们如何导致安全漏洞? 有人可以通过一个合适的例子清楚地解释它吗?
一个可能的方面是无符号整数可能会导致循环中出现难以辨认的问题,因为下溢导致大量数据。 我无法计数(即使是一个无符号整数!)多less次,我做了这个bug的变种
for(size_t i = foo.size(); i >= 0; --i) ...
请注意,根据定义, i >= 0
始终为真。 (这首先是因为如果i
签了名,编译器会警告size()
的size_t
可能会溢出)。
还有其他原因提到危险 – 这里使用的无符号types! 在我看来,其中最强的是signed和unsigned之间的隐式types转换。
一个重要的原因是它使得循环逻辑变得更难:想象一下你想遍历一个数组的最后一个元素(这在现实世界中是发生的)。 所以你写你的function:
void fun (const std::vector<int> &vec) { for (std::size_t i = 0; i < vec.size() - 1; ++i) do_something(vec[i]); }
看起来不错,不是吗? 它甚至编译干净的非常高的警告水平! ( Live )所以你把它放在你的代码中,所有的testing运行顺利,你忘了它。
现在,稍后,有人向你的函数传递一个空的vector
。 现在用一个有符号的整数,你希望会注意到符号比较编译器的警告 ,介绍了适当的强制转换,并没有公布错误的代码。
但是在你用无符号整数实现的时候,你换行和循环条件变成i < SIZE_T_MAX
。 灾难,UB和最有可能的崩溃!
我想知道他们如何导致安全漏洞?
这也是一个安全问题,特别是它是一个缓冲区溢出 。 一种可能的利用方法是,如果do_something
会做一些攻击者可以观察到的事情。 他可能能够finddo_something
input内容,而攻击者不应该访问的数据将从内存中泄漏出去。 这将是一个类似于Heartbleed错误的场景。 (感谢棘手怪胎在他的评论中指出)
我不会仅仅是为了回答一个问题而观看video,但是一个问题是混合有符号值和无符号值会导致混乱的转换。 例如:
#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
的比较,给出了一个很大的正数和令人惊讶的结果。
虽然它可能只被认为是现有答案的一个变种:参考1995年9月由Scott Meyers撰写的C ++ Report中的“接口中有符号和无符号types” ,避免接口中的无符号types特别重要。
问题是,检测接口的客户端可能产生的某些错误是不可能的(如果他们能做到的话,他们会做出这些错误)。
这里给出的例子是:
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
的错误传递值和非常大的数组分配。
无符号int的大问题是,如果从无符号整数0中减去1,则结果不是负数,结果不会小于开始使用的数字,但结果是最大可能的无符号整数值。
unsigned int x = 0; unsigned int y = x - 1; if (y > x) printf ("What a surprise! \n");
这就是使得unsigned int容易出错的原因。 当然,无符号整数的工作原理与其devise的一样。 如果你知道自己在做什么,不犯错误,这是绝对安全的。 但大多数人犯错误。
如果你使用的是一个好的编译器,你打开编译器产生的所有警告,它会告诉你什么时候你做了可能是错误的危险的事情。
无符号整数types的问题是,根据它们的大小,它们可能代表两种不同的东西之一:
- 小于
int
无符号types(例如uint8
)保存的数字范围为0..2ⁿ-1,并且使用它们的计算将根据整数运算的规则进行操作,前提是它们不超过int
types的范围。 按照现在的规则,如果这样的计算超出了int
的范围,编译器就可以做任何它喜欢的代码,甚至可以去否定时间和因果关系的规律(一些编译器会这么做!) ,即使计算结果将被分配回小于int
的无符号types。 - 无符号types
unsigned int
和更大的保持成员的整数代表环绕代数环一致mod2ⁿ; 这实际上意味着如果计算超出范围0..2ⁿ-1,则系统将增加或减less2ⁿ所需的任何倍数,以使该值返回到范围内。
因此,给定uint32_t x=1, y=2;
expression式xy
可以具有两个含义之一,取决于int
是否大于32位。
- 如果
int
大于32位,则expression式将从数字1中减去数字2,产生数字-1。 请注意,虽然types为uint32_t
的variables不能保存值-1,而不考虑int
的大小,并且存储-1会导致这样的variables保持为0xFFFFFFFF,但除非或直到该值被强制为无符号types将performance为签名数量-1。 - 如果
int
为32位或更小,则expression式将产生一个uint32_t
值,该值在添加到uint32_t
值2时将产生uint32_t
值1(即,uint32_t
值0xFFFFFFFF)。
恕我直言,这个问题可以得到干净的解决,如果C和C ++定义新的无符号types[例如unum32_t和uwrap32_t],使得一个unum32_t
总是performance为一个数字,不pipeint
的大小(可能需要右手操作如果int
为32位或更小,则减法或一元减去被提升到下一个更大的带符号types),而wrap32_t
将总是作为代数环的成员(即使int
大于32位阻止促销)。 然而,在没有这种types的情况下,编写既便携又干净的代码通常是不可能的,因为可移植代码经常需要types强制。
C和C ++中的数字转换规则是拜占庭式的混乱。 使用无符号types比使用纯粹签名的types更大程度地暴露自己的混乱。
举个例子,比较两个variables之间的简单情况,一个是有符号的,另一个是无符号的。
- 如果两个操作数都小于int,则它们都将被转换为int,并且比较将给出数值上正确的结果。
- 如果无符号操作数小于有符号操作数,则两者都将转换为有符号操作数的types,比较将给出数值上正确的结果。
- 如果无符号操作数的大小大于或等于有符号操作数,且大于或等于int大小,则两者都将转换为无符号操作数的types。 如果有符号操作数的值小于零,则会导致数值不正确的结果。
再举一个例子,考虑乘以两个相同大小的无符号整数。
- 如果操作数大小大于或等于int的大小,则乘法将定义环绕语义。
- 如果操作数大小小于int的大小,那么可能会出现未定义的行为。
除了无符号types的范围/扭曲问题。 混合使用无符号和有符号整数types会影响处理器的显着性能问题。 less于浮点投射,但相当多的要忽略这一点。 此外,编译器可能会对该值进行范围检查并更改进一步检查的行为。