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 中的一个错误。
首先,请注意,由于违反了严格的别名规则,您的代码正在调用未定义的行为。
话虽如此,这就是我将其视为错误的原因:
相同的表达式,当第一次分配给中间 short
或 short *
时,会导致预期的行为。只有当直接将表达式作为函数参数传递时,才会出现意外行为。
即使用-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
活不下去了。
为什么这个程序的输出是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 中的一个错误。
首先,请注意,由于违反了严格的别名规则,您的代码正在调用未定义的行为。
话虽如此,这就是我将其视为错误的原因:
相同的表达式,当第一次分配给中间
short
或short *
时,会导致预期的行为。只有当直接将表达式作为函数参数传递时,才会出现意外行为。即使用
-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
活不下去了。