Неопределенное поведение из указателя math в массиве C++
Почему выход этой программы равен 4
?
#include <iostream>
int main()
{
short A[] = {1, 2, 3, 4, 5, 6};
std::cout << *(short*)((char*)A + 7) << std::endl;
return 0;
}
С моей точки зрения, в x86 маленькой системе endian, где char имеет 1 байт и короткие 2 байта, вывод должен быть 0x0500
, потому что данные в массиве A
являются паром в шестнадцатеричном виде:
01 00 02 00 03 00 04 00 05 00 06 00
Мы перемещаемся с начала на 7 байт вперед, а затем читаем 2 байта. Что мне не хватает?
Ответы
Ответ 1
Вы нарушаете строгие правила псевдонимов. Вы не можете просто читать на полпути в объект и притворяться, что объект все сам по себе. Вы не можете изобретать гипотетические объекты, используя байтовые смещения, подобные этому. GCC полностью в пределах своих прав делать сумасшедшие sh, как возвращаться во времени и убивать Элвиса Пресли, когда вы передаете ему свою программу.
То, что вам разрешено делать, - это проверять и манипулировать байтами, которые составляют произвольный объект, с использованием 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
switch), созданный объект неправильно выровнен для любой текущей архитектуры основного потока. short
юридически не может жить в этом нечетном месте в памяти †, поэтому вдвойне не может претендовать есть один там. Просто нет способа обойти, как неопределенное поведение исходного кода; на самом деле, если вы передадите GCC -fsanitize=undefined
switch, он скажет вам столько же.
† Я несколько упрощенно.
Ответ 2
Программа имеет неопределенное поведение из-за того, что вы неправильно указали указатель на (short*)
. Это нарушает правила в 6.3.2.3 p6 в C11, что не имеет ничего общего со строгим псевдонимом, как утверждается в других ответах:
Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если результирующий указатель неправильно выровнен для ссылочного типа, поведение не определено.
В [expr.static.cast] p13 C++ говорится, что преобразование неизмененного char*
в short*
дает неопределенное значение указателя, которое может быть недопустимым указателем, который не может быть разыменован.
Правильный способ проверки байтов через char*
не отбрасывать на short*
и притворяться, что есть short
адрес, где short
не может жить.
Ответ 3
Это, возможно, ошибка в GCC.
Во-первых, следует отметить, что ваш код вызывает неопределенное поведение из-за нарушения правил строгой псевдонимы.
С учетом сказанного, вот почему я считаю это ошибкой:
-
Такое же выражение, когда оно сначала назначается промежуточному short
или short *
, вызывает ожидаемое поведение. Это только при передаче выражения непосредственно как аргумент функции, проявляется ли неожиданное поведение.
-
Это происходит даже при компиляции с -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 <[email protected]>
return 0;
4005bd: mov eax,0x0
}
4005c2: leave
4005c3: ret
- Я получаю тот же результат с GCC 4.9.4 и GCC 5.5.0 от Docker hub