摘要: David LeBlanc 重新探讨了 SafeInt 类,并且讨论了自从他的上一篇有关该主题的文章发布以来的相关更新。
注 — Michael Howard我的好友和合著者 David LeBlanc 在整型溢出安全漏洞方面所做的研究可能超过了世界上的任何一个人。在本文中,他针对这一非常严重的安全缺陷提出了更多的问题,并且发表了更多的真知灼见。
SafeInt 的一些历史
在过去一年左右的时间里,整型已经继续向我们显示了一些新的花招。例如,请观察下面这个有趣的程序错误:
template <typename T>
unsigned _int64 AbsVal(T t)
{
if(t < 0)
return (unsigned _int64)-t;
return t;
}
程序错误位于何处?请回想一下,在我原来的文章 Integer Handling with the C++ SafeInt Class 中,我曾经指出,几乎每个运算符都可以将结果类型强制转换为与开始时使用的类型不同的类型,而一元求反运算符也不例外。对于任何小于 32 位的整数或有符号的 32 位整数,–t 的返回类型都是 int。
如果传入的值是最小的 8 位有符号整数,即 -128,则写下 –t 等效于写下 -(int)t。让我们逐步对其进行演算,看一看发生了什么:
t = -128 = 0x80
(int)t = -128 = 0xffffff80
-(int)t = 0x00000080 = 128
到现在为止,一切都还不错。但是,如果输入类型是最小的 int 类型,那么会怎样呢?隐式类型转换结果为 int,但对最小的整数结果求反会得到相同的值,而且更糟糕的是,它仍然是负数!让我们考察一下所发生的事情:
t = (int)0x80000000
-t = (int)0x80000000
(unsigned _int64)-t = (unsigned _int64)(_int64)-t
= (unsigned _int64)0xffffffff80000000
= 0xffffffff80000000
我们需要的答案是 0x80000000,但那不是我们得到的结果。该示例说明了隐式类型转换和从有符号类型到无符号类型的转换是如何联合起来导致开发人员创建有问题的代码的。
除了发现几个新的程序错误以外,该 SafeInt 更新还包含一些改进:
• |
更好地支持使用 Win32 异常以及创建自己的异常处理机制。 |
• |
提供所有基本类型的类型转换运算符。这意味着您可以将 SafeInt 传递到函数调用中,该调用将执行正确的操作。 |
• |
提供更好的内联,因为现在移除了空构造函数。有关详细信息,请阅读头文件注释。 |
• |
用 IntTraits 类替换了 MaxInt、MinInt 和其他几个函数。由此可以产生好得多的调试性能。 |
• |
使用函数调用来处理异常,而不是在代码内部引发异常。这提供了更多的灵活性,并且可以产生更少的优化代码。 |
• |
重写了二进制运算符,以便更好地处理极端情况。 |
• |
解决了在求模运算符的输入值为负数时,返回值的符号不正确的情况。 |
• |
对于 U op SafeInt <T>.的情形,解决了除法和求模运算符中的大量极端情况程序错误。 |
• |
添加了对指针运算的有限支持。 |
• |
添加了对类型 U op= SafeInt<T>. 的运算的支持。这是 SafeInt 应该具有传播性这一规则的一个例外。 |
最大的更改是,该类已经在很大程度上进行了重写,以便利用局部模板特殊化。局部模板特殊化是这样一种功能:我们可以使用它让编译器选取正确的代码集以在编译时运行,而不是在我们的代码中包含大量编译时常量。Visual Studio 7.0 和更高版本编译器具有对局部模板特殊化的支持。让我们以我在本文开头说明的程序错误为例。我们希望做的第一件事情是创建一个包含枚举的类,其中,该枚举的值是在编译时解析的。我们将在下面的 AbsValueMethod 类中执行此操作。
template <typename T>
class AbsValueMethod
{
public:
enum
{
method = (T)-1 > 0 ? 0 : //all unsigned cases
sizeof(T) == 4 ? 1 : //problem case of signed int32
2; //all other signed ints
};
};
正如您可以看到的那样,方法的值可以设置为 0、1 或 2,具体取决于我们碰到的是容易的无符号情形、有问题的有符号 32 位整数情形,还是问题较轻的任何其他大小整数情形。现在,让我们考察一下如何使用该类:
//forward declaration
template <typename T, int> class AbsValueHelper;
//easy case of unsigned
template <typename T> class AbsHelper template <typename T, 0>
{
public:
static unsigned _int64 Abs(T t)
{
return (unsigned _int64)t;
}
};
//specialized version for 32-bit ints
template <typename T> class AbsHelper template <typename T, 1>
{
public:
static unsigned _int64 Abs(T t)
{
if(t < 0)
return (unsigned _int64)(unsigned _int32)-t;
return (unsigned _int64)t;
}
};
//all other signed ints
template <typename T> class AbsHelper template <typename T, 2>
{
public:
static unsigned _int64 Abs(T t)
{
if(t < 0)
return (unsigned _int64)-t;
return (unsigned _int64)t;
}
};
正如您可以看到的那样,我们现在拥有的类恰好包含每个类型的正确代码,并且我们不会浪费时间来计算调试代码中的编译时常量。当您单步执行每个函数时,正在发生什么也是显而易见的,并且该函数中不再充斥大量错综复杂的编译时条件。最后,我们得到如下正确函数。
template <typename T>
unsigned _int64 AbsVal(T t)
{
return AbsHelper<T, AbsValueMethod<T>::method>::Abs(t);
}
小结
我们已经讨论了自从我的上一篇文章发布以来对 SafeInt 类进行的更新。我要感谢 Hannes Reuscher、Chris White 和 Atin Bansal 帮助我使该类变得更好,尤其要感谢 Atin 编写了一个非常可靠的测试工具来验证该类的正确性。
(转自:MSDN)(编辑:诚心帮大家)
|