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

Undefined behavior from pointer math on a C++ array

本文关键字:未定义 数组 指针 C++      更新时间:2023-10-16

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

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

据我了解,在 x86 小端系统中,char 有 1 个字节,短 2 个字节,输出应该是0x0500的,因为数组A中的数据在十六进制中休耕:

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

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

您在此处违反了严格的别名规则。你不能只读到一个对象的一半,然后假装它是一个单独的对象。你不能像这样使用字节偏移量来发明假设的对象。海湾合作委员会完全有权做疯狂的事情,比如回到过去并谋杀猫王,当你把它交给你的程序时。

您可以做的是使用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

(现场演示)

但是你不能只是"编造"原始集合中不存在的对象。

此外,即使你有一个编译器可以被告知忽略这个问题(例如,使用 GCC 的-fno-strict-aliasing开关),虚构的对象也没有针对任何当前的主流架构正确对齐†,short不能合法地存在于记忆中的那个奇数位置,所以你不能加倍假装那里有一个。只是没有办法绕过原始代码的行为是多么未定义;事实上,如果你通过GCC的-fsanitize=undefined开关,它会告诉你很多。

我正在简化一点。

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

指向对象类型的指针

可以转换为指向其他对象类型的指针。如果生成的指针未与引用的类型正确对齐,则行为未定义。

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

检查字节的正确方法是通过char*,而不是通过转换回short*并假装short无法驻留的地址有short

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

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

话虽如此,这就是为什么我认为它是一个错误:

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

  2. 即使使用-O0 -fno-strict-aliasing编译也会发生这种情况。

我用 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 集线器使用 GCC 4.9.4 和 GCC 5.5.0 得到相同的结果