用无符号“负”数递减指针
Posted
技术标签:
【中文标题】用无符号“负”数递减指针【英文标题】:Decrementing a pointer with an unsigned "negative" number 【发布时间】:2017-07-19 12:58:01 【问题描述】:以下示例在调用look_back_1() 或look_back_2() 时会崩溃。 原因:当对无符号变量取反时,结果应保持无符号。
#include <stdio.h>
int look_back_1(int *arr, unsigned int nmElems, unsigned long dist)
int *elem = arr + nmElems;
elem += -dist;
return (*elem);
int look_back_2(int *arr, unsigned int nmElems, unsigned int dist)
int *elem = arr + nmElems;
elem += -dist;
return (*elem);
int main(int argc, char **argv)
int arr[100] = 0, ;
printf("1. %d\n", look_back_1(arr, 100, 1)); // <NEEDS TO CRASH, BUT WORKS????>>
printf("2. %d\n", look_back_2(arr, 100, 1)); // <<CRASH!!!!!>>
在执行数组越界访问时,GCC 4.5 会在每个函数调用中崩溃。 对于这两种情况,编译器都会发出 NEG 操作码。
GCC 6.1 或 Clang 只会在调用 int 版本时崩溃。 但是当它们为 unsigned long 版本发出 SUB 操作码时,它们都避免了崩溃。
他们可以这样做吗?
【问题讨论】:
没有“正确崩溃”之类的东西。 - 你所拥有的是未定义的行为。 编译器应该永远不会崩溃,而且它不太可能崩溃。 “崩溃”是什么意思?添加不应该失败。但是,您正在使用该函数来访问数组的边界。这可能导致程序在运行时崩溃。打印地址以了解灾难发生的原因。 为了充分理解,了解您的代码是否编译为 64 位可能很重要。您使用的是 64 位操作系统吗?编译命令行是否有“-m32”?unsigned long
和 unsigned int
的大小是多少?你可以edit你的问题来提及这些细节。
我想知道您是否了解一元 operator-
对 unsigned
值的作用。它定义明确,不会产生负值。
你是在问是否允许编译器创建一个不会崩溃的程序?如果是这样,答案总是肯定的。未定义的行为可以做任何事情,包括不崩溃。
【参考方案1】:
[编辑] 这是对先前版本问题的回答,它显示了使用参数dist==1
调用这些函数时的实际问题
-(unsigned long)1
定义明确并环绕。只是ULONG_MAX
。出于同样的原因,-(unsigned int)
是 UINT_MAX
。
数组边界外的指针算术会导致未定义行为,因此 GCC 完全可以忽略这种可能性。例如,他们可以将 x64 上的指针视为带有回绕的 64 位整数。将 64 位 ULONG_MAX
添加到带有回绕的 64 位指针只会将指针减少 -1,这就是回绕的工作原理。添加 32 位 UINT_MAX
点在您的 int[100]
附近。
因此,您看到的行为是未定义行为的一种完全有效的结果。然而,它是完全不可靠的。优化器可能知道您添加的元素数不能超过数组中允许的最大元素数(对于 64 位平台上的 4 字节整数为 2^62),并从那里做出假设。
【讨论】:
请忽略我提供的未定义示例。您能解释一下编译为导入函数时两个函数中未定义的内容吗? godbolt.org/g/gxNN1W @Tal 将 UINT_MAX 添加到指针是未定义的,除非该指针指向具有 UINT_MAX 元素的数组。你的指针没有。 @Tal:这个问题没有意义。 “未定义的行为”是 C++ 标准中的一个术语。 “导入函数”不是。一般来说,该标准定义了与给定特定输入的整个程序相关的术语,尽管有一部分程序将表现出与输入无关的未定义行为。 我希望现在更清楚我不是在谈论“未定义的行为” @tal 它仍然是未定义的行为。您将 X 添加到指向具有 Y 元素的数组的指针,其中 X > Y + 1。这是未定义的行为。 C11 标准 §6.5.6 第 8 点。【参考方案2】:看看你的“godbolt”反汇编,区别很容易。您正在为本机 64 位、unsigned int
32 位和 unsigned long
64 位的平台进行编译。也就是说,数学本身是模 2^64,它完全匹配匹配 unsigned long
的行为。但是对于unsigned int
,需要一条额外的指令。这是一个微妙的MOV
指令,从 32 位寄存器到自身 (!)。这个指令的原因是什么?它清除了 64 位结果的高 32 位,这是“模 2^32”行为所需要的。
这很有效,也很聪明。对于表现出未定义行为的代码,它可能会产生意想不到的结果,但无论如何您都不应该对这些情况抱有期望。
【讨论】:
以上是关于用无符号“负”数递减指针的主要内容,如果未能解决你的问题,请参考以下文章