[英]How to compare C pointers?
最近,我写了一些代码来比较像这样的指针:
if(p1+len < p2)
但是,有些工作人员说我应该这样写:
if(p2-p1 > len)
为了安全起见。 这里, p1和p2是char *
指针, len是整数。 我对此一无所知。是吗?
EDIT1:当然, p1和p2在乞讨时指向同一个内存对象。
EDIT2:就在一分钟之前,我在我的代码中找到了这个问题的bogo (约3K行),因为len
太大了, p1+len
不能存储在4个字节的指针中,所以p1 + len <p2为真但实际上它不应该,所以我认为我们应该在某些情况下比较像这样的指针:
if(p2 < p1 || (uint32_t)p2-p1 > (uint32_t)len)
通常,只有指针指向同一个内存对象的部分(或者超过对象末尾的一个位置)时,才能安全地比较指针。 当p1
, p1 + len
和p2
都符合此规则时,两个if
-tests都是等效的,所以你不必担心。 另一方面,如果只知道p1
和p2
符合这个规则,并且p1 + len
可能在结束之后太远,那么只有if(p2-p1 > len)
是安全的。 (但我无法想象你的情况。我假设p1
指向某个内存块的开头,而p1 + len
指向它结束后的位置,对吧?)
他们可能一直在考虑的是整数运算:如果i1 + i2
可能会溢出,但你知道i3 - i1
不会,那么i1 + i2 < i3
可以回绕(如果它们是无符号整数)或者触发未定义的行为(如果它们是有符号整数)或两者(如果你的系统恰好执行有符号整数溢出的环绕),而i3 - i1 > i2
将不会有这个问题。
编辑添加:在评论中,你写“ len
是来自buff的值,所以它可能是任何东西”。 在这种情况下,它们是完全正确的,并且p2 - p1 > len
更安全,因为p1 + len
可能无效。
“未定义的行为”适用于此处。 你不能比较两个指针,除非它们都指向同一个对象或指向该对象结束后的第一个元素。 这是一个例子:
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)
从技术上讲, p1
和p2
必须是指向同一个数组的指针。 如果它们不在同一个数组中,则行为未定义。
对于加法版本, len
的类型可以是任何整数类型。
对于差异版本,减法的结果是ptrdiff_t
,但任何整数类型都将被适当地转换。
在这些约束中,您可以以任何方式编写代码; 两者都不正确。 在某种程度上,这取决于你正在解决的问题。 如果问题是“数组的这两个元素是否比len
元素分开”,则减法是合适的。 如果问题是' p2
与p1[len]
(又名p1 + len
)'相同,则添加是合适的。
实际上,在许多具有统一地址空间的机器上,你可以减去指向不同数组的指针,但你可能会得到一些有趣的效果。 例如,如果指针是某些结构类型的指针,而不是同一数组的部分,那么被视为字节地址的指针之间的差异可能不是结构大小的倍数。 这可能会导致特殊问题。 如果他们指向相同的数组,就不会有这样的问题 - 这就是限制到位的原因。
现有的答案显示了为什么if (p2-p1 > len)
优于if (p1+len < p2)
,但仍然存在问题 - 如果p2
碰巧指向缓冲区中的p1
,而len
是无符号类型(例如size_t
),那么p2-p1
将为负数,但会转换为大的无符号值,以便与unsigned len进行比较,因此结果可能为true,这可能不是您想要的。
所以你可能真的需要像if (p1 <= p2 && p2 - p1 > len)
才能完全安全。
正如迪特里希已经说过的,比较不相关的指针是危险的,并且可以被认为是不确定的行为。
假设两个指针在0到2GB的范围内(在32位Windows系统上),减去2个指针将得到介于-2 ^ 31和+ 2 ^ 31之间的值。 这正是带符号的32位整数的域。 因此,在这种情况下,减去两个指针似乎是有意义的,因为结果将始终在您期望的域内。
但是,如果在您的可执行文件中启用了LargeAddressAware标志(这是特定于Windows的,不了解Unix),那么您的应用程序将具有3GB的地址空间(当在具有/ 3G标志的32位Windows中运行时)甚至4GB(在64位Windows系统上运行时)。 如果然后开始减去两个指针,结果可能在32位整数的域之外,并且您的比较将失败。
我认为这是地址空间最初划分为2GB的2个相等部分的原因之一,而LargeAddressAware标志仍然是可选的。 但是,我的印象是当前的软件(你自己的软件和你正在使用的DLL)似乎非常安全(没有人再减去指针,不是吗?)并且我自己的应用程序默认启用了LargeAddressAware标志。
表达式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
之间的字节数,无论所涉及的值如何。
当然,除非您知道需要为确定的攻击者辩护,否则您不希望在代码中看到此类比较。 不幸的是,这是保证安全的唯一方法,如果您依赖于问题中给出的任何一种形式,那么您就会受到攻击。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.