用无符号“负”数递减指针

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 longunsigned 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】:

看看你的“godb​​olt”反汇编,区别很容易。您正在为本机 64 位、unsigned int 32 位和 unsigned long 64 位的平台进行编译。也就是说,数学本身是模 2^64,它完全匹配匹配 unsigned long 的行为。但是对于unsigned int,需要一条额外的指令。这是一个微妙的MOV 指令,从 32 位寄存器到自身 (!)。这个指令的原因是什么?它清除了 64 位结果的高 32 位,这是“模 2^32”行为所需要的。

这很有效,也很聪明。对于表现出未定义行为的代码,它可能会产生意想不到的结果,但无论如何您都不应该对这些情况抱有期望。

【讨论】:

以上是关于用无符号“负”数递减指针的主要内容,如果未能解决你的问题,请参考以下文章

Swift中的无符号整数到负整数

Java负整数的左移右移无符号右移

分析轮子- 十进制整数怎么变成无符号二进制的整数的

波形格式 - 负数据大小

将无符号整数存储在指针中

6.3 40亿个非负整数中找到没出现的数