C++ 数组上指针数学的未定义行为

Undefined behavior from pointer math on a C++ array

为什么这个程序的输出是4

#include <iostream>

int main()
{
    short A[] = {1, 2, 3, 4, 5, 6};
    std::cout << *(short*)((char*)A + 7) << std::endl;
    return 0;
}

根据我的理解,在x86 little endian系统上,其中char有1个字节,short有2个字节,输出应该是0x0500,因为数组A中的数据是闲置的十六进制:

01 00 02 00 03 00 04 00 05 00 06 00

我们从头开始向前移动7个字节,然后读取2个字节。我错过了什么?

这可以说是 GCC 中的一个错误。

首先,请注意,由于违反了严格的别名规则,您的代码正在调用未定义的行为。

话虽如此,这就是我将其视为错误的原因:

  1. 相同的表达式,当第一次分配给中间 shortshort * 时,会导致预期的行为。只有当直接将表达式作为函数参数传递时,才会出现意外行为。

  2. 即使用-O0 -fno-strict-aliasing编译也会出现。

我 re-wrote 你的 C 代码,以消除任何 C++ 疯狂的可能性。你的问题 标记 c 毕竟!我添加了 pshort 函数以确保不涉及变量性质 printf

#include <stdio.h>

static void pshort(short val)
{
    printf("0x%hx ", val);
}

int main(void)
{
    short A[] = {1, 2, 3, 4, 5, 6};

#define EXP ((short*)((char*)A + 7))

    short *p = EXP;
    short q = *EXP;

    pshort(*p);
    pshort(q);
    pshort(*EXP);
    printf("\n");

    return 0;
}

gcc (GCC) 7.3.1 20180130 (Red Hat 7.3.1-2)编译后:

gcc -O0 -fno-strict-aliasing -g -Wall -Werror  endian.c

输出:

0x500 0x500 0x4

当表达式直接用作参数时,GCC 似乎实际上生成了不同的代码,即使我显然使用了相同的表达式(EXP ).

倾销 objdump -Mintel -S --no-show-raw-insn endian:

int main(void)
{
  40054d:   push   rbp
  40054e:   mov    rbp,rsp
  400551:   sub    rsp,0x20
    short A[] = {1, 2, 3, 4, 5, 6};
  400555:   mov    WORD PTR [rbp-0x16],0x1
  40055b:   mov    WORD PTR [rbp-0x14],0x2
  400561:   mov    WORD PTR [rbp-0x12],0x3
  400567:   mov    WORD PTR [rbp-0x10],0x4
  40056d:   mov    WORD PTR [rbp-0xe],0x5
  400573:   mov    WORD PTR [rbp-0xc],0x6

#define EXP ((short*)((char*)A + 7))

    short *p = EXP;
  400579:   lea    rax,[rbp-0x16]             ; [rbp-0x16] is A
  40057d:   add    rax,0x7
  400581:   mov    QWORD PTR [rbp-0x8],rax    ; [rbp-0x08] is p
    short q = *EXP;
  400585:   movzx  eax,WORD PTR [rbp-0xf]     ; [rbp-0xf] is A plus 7 bytes
  400589:   mov    WORD PTR [rbp-0xa],ax      ; [rbp-0xa] is q

    pshort(*p);
  40058d:   mov    rax,QWORD PTR [rbp-0x8]    ; [rbp-0x08] is p
  400591:   movzx  eax,WORD PTR [rax]         ; *p
  400594:   cwde   
  400595:   mov    edi,eax
  400597:   call   400527 <pshort>
    pshort(q);
  40059c:   movsx  eax,WORD PTR [rbp-0xa]      ; [rbp-0xa] is q
  4005a0:   mov    edi,eax
  4005a2:   call   400527 <pshort>
    pshort(*EXP);
  4005a7:   movzx  eax,WORD PTR [rbp-0x10]    ; [rbp-0x10] is A plus 6 bytes ********
  4005ab:   cwde   
  4005ac:   mov    edi,eax
  4005ae:   call   400527 <pshort>
    printf("\n");
  4005b3:   mov    edi,0xa
  4005b8:   call   400430 <putchar@plt>

    return 0;
  4005bd:   mov    eax,0x0
}
  4005c2:   leave  
  4005c3:   ret

  • 我从 Docker hub
  • 使用 GCC 4.9.4 和 GCC 5.5.0 得到相同的结果

您在这里违反了严格的别名规则。您不能只将 half-way 读入一个对象并假装它本身就是一个对象。您不能像这样使用字节偏移来发明假设的对象。当你把你的程序交给 GCC 时,GCC 完全有权利去做疯狂的事情!比如回到过去并谋杀猫王。

允许做的是使用 char* 检查和操作构成任意对象的字节。使用该特权:

#include <iostream>
#include <algorithm>

int main()
{
    short A[] = {1, 2, 3, 4, 5, 6};

    short B;
    std::copy(
       (char*)A + 7,
       (char*)A + 7 + sizeof(short),
       (char*)&B
    );
    std::cout << std::showbase << std::hex << B << std::endl;
}

// Output: 0x500

(live demo)

但您不能只 "make up" 原始集合中的 non-existent 个对象。

此外,即使您的编译器可以被告知忽略此问题(例如使用 GCC 的 -fno-strict-aliasing 开关),made-up 对象也未正确 对齐 适用于任何当前的主流架构。 short 不能合法地居住在内存中的那个 odd-numbered 位置 ,所以你不能假装那里有一个。根本没有办法解决原始代码行为的未定义程度;事实上,如果你通过 GCC -fsanitize=undefined 开关,它会告诉你同样多的信息。

我简化了一点

由于将错误对齐的指针投射到 (short*),该程序具有未定义的行为。这违反了 C11 中 6.3.2.3 p6 中的规则,这与其他答案中声称的严格别名无关:

A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned for the referenced type, the behavior is undefined.

在[expr.static.cast] p13 C++中说,将未对齐的char*转换为short*会给出一个未指定的指针值,这可能是一个无效指针,不能被取消引用.

检查字节的正确方法是通过 char*,而不是通过转换回 short* 并假装在 short 的地址处有一个 short活不下去了。