最近,我写了一些代码来比较像这样的指针:
if(p1+len < p2)
然而,有些工作人员说我应该这样写:
if(p2-p1 > len)
安全。
这里, p1 和 p2 是char *
指针, len 是一个整数。
我根本不知道。那是对的吗?
EDIT1:当然, p1 和 p2 指向同一个内存对象的指针。
EDIT2:就在一分钟之前,我在我的代码中找到了这个问题的 bogo (约3K行),因为len
太大了,p1+len
可以' t存储在4个字节的指针中,所以 p1 + len&lt; p2 是 true 。但实际上它不应该,所以我认为我们应该在某种情况下比较这样的指针:
if(p2 < p1 || (uint32_t)p2-p1 > (uint32_t)len)
答案 0 :(得分:23)
通常,只有当指针指向同一个内存对象的某些部分(或者超过对象末尾的一个位置)时,才能安全地比较指针。当p1
,p1 + len
和p2
都符合此规则时,您的if
- 测试都是等效的,因此您无需担心。另一方面,如果只知道p1
和p2
符合此规则,并且p1 + len
可能距离结尾太远,则只有if(p1-p2 > len)
是安全的。 (但是我无法想象你的情况。我假设p1
指向某个内存块的开头,而p1 + len
指向它结束后的位置,对吧?)
他们可能一直在考虑的是整数算术:如果i1 + i2
可能会溢出,但您知道i3 - i1
不会溢出,那么i1 + i2 < i3
可以回绕(如果它们是无符号整数)或触发未定义的行为(如果它们是有符号整数)或两者(如果你的系统恰好执行有符号整数溢出的回绕),而i3 - i1 > i2
则没有那个问题。
编辑添加:在评论中,您写道“len
是来自buff的值,因此它可能是任何内容”。在这种情况下,它们非常正确,p2 - p1 < len
更安全,因为p1 + len
可能无效。
答案 1 :(得分:12)
“未定义的行为”适用于此处。你不能比较两个指针,除非它们都指向同一个对象或指向该对象结束后的第一个元素。这是一个例子:
void func(int len)
{
char array[10];
char *p = &array[0], *q = &array[10];
if (p + len <= q)
puts("OK");
}
你可能会想到这样的功能:
// if (p + len <= q)
// if (array + 0 + len <= array + 10)
// if (0 + len <= 10)
// if (len <= 10)
void func(int len)
{
if (len <= 10)
puts("OK");
}
但是,编译器知道ptr <= q
对于ptr
的所有有效值都是正确的,因此它可能会优化函数:
void func(int len)
{
puts("OK");
}
快得多!但不是你想要的。
是的,野外存在编译器。
这是唯一安全的版本:减去指针并比较结果,不要比较指针。
if (p - q <= 10)
答案 2 :(得分:8)
从技术上讲,p1
和p2
必须指向同一个数组。如果它们不在同一个数组中,则行为未定义。
对于添加版本,len
的类型可以是任何整数类型。
对于差异版本,减法的结果为ptrdiff_t
,但任何整数类型都将被适当转换。
在这些约束中,你可以用任何一种方式编写代码;两者都不正确。在某种程度上,这取决于你正在解决的问题。如果问题是“数组的这两个元素是否超过len
个元素”,则减法是合适的。如果问题是'p2
与p1[len]
相同的元素(又名p1 + len
)',则添加是合适的。
实际上,在许多具有统一地址空间的机器上,你可以减去指向不同数组的指针,但是你可能会得到一些有趣的效果。例如,如果指针是某些结构类型的指针,而不是同一数组的部分,那么被视为字节地址的指针之间的差异可能不是结构大小的倍数。这可能会导致特殊问题。如果他们指向相同的数组,就不会有这样的问题 - 这就是限制到位的原因。
答案 3 :(得分:0)
现有答案显示为什么if (p2-p1 > len)
优于if (p1+len < p2)
,但仍然存在问题 - 如果p2
碰巧指向缓冲区中的p1
之前len
是无符号类型(例如size_t
),然后p2-p1
将为负数,但会转换为大的无符号值以与unsigned len进行比较,因此结果可能是是真的,这可能不是你想要的。
因此,为了完全安全,您实际上可能需要类似if (p1 <= p2 && p2 - p1 > len)
的内容。
答案 4 :(得分:0)
正如迪特里希已经说过的,比较不相关的指针是危险的,可以被视为未定义的行为。
鉴于两个指针在0到2GB的范围内(在32位Windows系统上),减去2个指针将得到介于-2 ^ 31和+ 2 ^ 31之间的值。这正是带符号的32位整数的域。所以在这种情况下,减去两个指针似乎是有意义的,因为结果总是在你期望的域内。
但是,如果在您的可执行文件中启用了LargeAddressAware标志(这是特定于Windows的,不了解Unix),那么您的应用程序将具有3GB的地址空间(当在32位Windows中运行时/ 3G标志)甚至4GB(在64位Windows系统上运行时)。 如果然后开始减去两个指针,结果可能会超出32位整数的域,并且您的比较将失败。
我认为这是地址空间最初划分为2GB的2个相等部分的原因之一,而LargeAddressAware标志仍然是可选的。但是,我的印象是当前的软件(你自己的软件和你正在使用的DLL)似乎非常安全(没有人再减去指针,不是吗?)并且我自己的应用程序默认启用了LargeAddressAware标志。
答案 5 :(得分:0)
表达式p1 + len < p2
会向下编译为类似p1 + sizeof(*p1)*len < p2
的形式,并且使用指向类型的大小进行缩放会导致指针溢出:
int *p1 = (int*)0xc0ffeec0ffee0000;
int *p2 = (int*)0xc0ffeec0ffee0400;
int len = 0x4000000000000000;
if(p1 + len < p2) {
printf("pwnd!\n");
}
当len
乘以int
的大小时,它溢出到0
,因此条件被评估为if(p1 + 0 < p2)
。显然,这是正确的,并且以下代码的长度值过高。
好吧,p2-p1 < len
呢?同样,溢出会杀死您:
char *p1 = (char*)0xa123456789012345;
char *p2 = (char*)0x0123456789012345;
int len = 1;
if(p2-p1 < len) {
printf("pwnd!\n");
}
在这种情况下,指针之间的差被评估为p2-p1 = 0xa000000000000000
,这被解释为 负有符号的值。因此,它比len
小,然后执行以下代码,而len
值太低(或指针差太大)。
我知道在受到攻击者控制的值的情况下唯一安全的方法是使用无符号算法:
if(p1 < p2 &&
((uintptr_t)p2 - (uintptr_t)p1)/sizeof(*p1) < (uintptr_t)len
) {
printf("safe\n");
}
p1 < p2
保证p2 - p1
不会产生真正的负值。第二子句执行p2 - p1 < len
的动作,同时以非UB方式强制使用无符号算术。即(uintptr_t)p2 - (uintptr_t)p1
精确给出了较大的p2
和较小的p1
之间的字节数,无论涉及的值如何。
当然,除非您知道需要防御确定的攻击者,否则您不希望在代码中看到此类比较。不幸的是,这是确保安全的唯一方法,并且如果您依靠问题中给出的任何一种形式,就容易受到攻击。