Неопределенное поведение из указателя 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.

Во-первых, следует отметить, что ваш код вызывает неопределенное поведение из-за нарушения правил строгой псевдонимы.

С учетом сказанного, вот почему я считаю это ошибкой:

  1. Такое же выражение, когда оно сначала назначается промежуточному short или short *, вызывает ожидаемое поведение. Это только при передаче выражения непосредственно как аргумент функции, проявляется ли неожиданное поведение.

  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 <[email protected]>

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

  • Я получаю тот же результат с GCC 4.9.4 и GCC 5.5.0 от Docker hub