Всегда ли (p + x) -x приводит к p для указателя p и целому числу x в gcc linux x86-64 C ++ - PullRequest
0 голосов
/ 03 марта 2019

Предположим, у нас есть:

char* p;
int   x;

Как недавно обсуждалось в другом вопросе , арифметика , включая операции сравнения с недействительными указателями, может привести к непредвиденному поведению в gcc linux x86-64 C ++.Этот новый вопрос конкретно относится к выражению (p+x)-x: может ли оно генерировать неожиданное поведение (т. Е. Результат не будет p) в любой существующей версии GCC, работающей на Linux x86-64?

Обратите внимание, что этот вопроспочти арифметика указателя;нет абсолютно никакого намерения получить доступ к местоположению, обозначенному *(p+x), что, очевидно, было бы вообще непредсказуемым.

Практический интерес здесь представляет собой ненулевые массивы .Обратите внимание, что (p+x) и вычитание на x происходят в разных местах кода в этих приложениях.

Если можно показать, что последние версии GCC на x86-64 никогда не генерируют неожиданное поведение для (p+x)-x, тогдаэти версии могут быть сертифицированы для ненулевых массивов, и будущие версии, генерирующие непредвиденное поведение, могут быть изменены или настроены для поддержки этой сертификации.

ОБНОВЛЕНИЕ

ДляВ практическом случае, описанном выше, мы также можем предположить, что p сам по себе является действительным указателем, а p != NULL.

Ответы [ 3 ]

0 голосов
/ 03 марта 2019

Вы не понимаете, что такое "неопределенное поведение", и я не могу винить вас, учитывая, что это часто плохо объясняется.Вот как стандарт определяет неопределенное поведение, раздел 3.27 в intro.defs:

поведение, для которого этот документ не предъявляет никаких требований

Вот и все.Ни меньше, ни больше.Стандарт можно представить как ряд ограничений, которым должны следовать поставщики компиляторов при создании допустимых программ.Когда есть неопределенное поведение, все ставки выключены.

Некоторые люди говорят, что неопределенное поведение может привести к тому, что ваша программа порождает драконов или переформатирует ваш жесткий диск, но я считаю, что это немного бессмысленно.Более реалистично, что-то вроде прохождения границ массива может привести к ошибке сегмента (из-за запуска ошибки страницы).Иногда неопределенное поведение позволяет компиляторам выполнять оптимизацию, которая может неожиданно изменить поведение вашей программы, поскольку нет ничего, говорящего о том, что компилятор не может .

Дело в том, что компиляторы не "генерируют"неопределенное поведение ".Неопределенное поведение существует в вашей программе.

Я имел в виду, что если у GCC есть замечательная функция (в частности, математика для недействительных указателей), которая в данный момент не названа, мы можем дать ейимя, а затем потребовать его в будущих версиях.

Тогда это будет нестандартное расширение, и можно ожидать, что оно будет задокументировано.Я также очень сомневаюсь, что такая функция будет востребована, учитывая, что она не только позволит людям писать небезопасный код, но и будет чрезвычайно сложно создавать переносимые программы для.

0 голосов
/ 04 марта 2019

Да, в частности, для gcc5.x и более поздних версий это конкретное выражение оптимизируется очень рано до p, даже если оптимизация отключена, независимо от возможного времени выполнения UB.

Это происходит даже со статическимпостоянный размер массива и времени компиляции.gcc -fsanitize=undefined также не вставляет никаких инструментов для его поиска.Также нет предупреждений на -Wall -Wextra -Wpedantic

int *add(int *p, long long x) {
    return (p+x) - x;
}

int *visible_UB(void) {
    static int arr[100];
    return (arr+200) - 200;
}

Использование gcc -dump-tree-original для выгрузки своего внутреннего представления программной логики до любых проходов оптимизации показывает, что эта оптимизация произошла еще до этого в gcc5.x и новее .(И происходит даже в -O0).

;; Function int* add(int*, long long int) (null)
;; enabled by -tree-original

return <retval> = p;


;; Function int* visible_UB() (null)
;; enabled by -tree-original
{
  static int arr[100];

    static int arr[100];
  return <retval> = (int *) &arr;
}

Это из проводника компилятора Godbolt с gcc8.3 с -O0.

x86-64выход asm просто:

; g++8.3 -O0 
add(int*, long long):
    mov     QWORD PTR [rsp-8], rdi
    mov     QWORD PTR [rsp-16], rsi    # spill args
    mov     rax, QWORD PTR [rsp-8]     # reload only the pointer
    ret
visible_UB():
    mov     eax, OFFSET FLAT:_ZZ10visible_UBvE3arr
    ret

-O3 выход, конечно, просто mov rax, rdi


gcc4.9 и более ранние версииэта оптимизация в последующем проходе, а не в -O0: дамп дерева все еще включает в себя вычитание, а асм x86-64 равен

# g++4.9.4 -O0
add(int*, long long):
    mov     QWORD PTR [rsp-8], rdi
    mov     QWORD PTR [rsp-16], rsi
    mov     rax, QWORD PTR [rsp-16]
    lea     rdx, [0+rax*4]            # RDX = x*4 = x*sizeof(int)
    mov     rax, QWORD PTR [rsp-16]
    sal     rax, 2
    neg     rax                       # RAX = -(x*4)
    add     rdx, rax                  # RDX = x*4 + (-(x*4)) = 0
    mov     rax, QWORD PTR [rsp-8]
    add     rax, rdx                  # p += x + (-x)
    ret

visible_UB():       # but constants still optimize away at -O0
    mov     eax, OFFSET FLAT:_ZZ10visible_UBvE3arr
    ret

Это совпадает с -fdump-tree-originalвывод:

return <retval> = p + ((sizetype) ((long unsigned int) x * 4) + -(sizetype) ((long unsigned int) x * 4));

Если x*4 переполнится, вы все равно получите правильный ответ.На практике я не могу придумать, как написать функцию, которая бы приводила к тому, что UB вызывает заметное изменение поведения.


Как часть более крупной функции, компиляторразрешено выводить некоторую информацию о диапазоне, например, p[x] является частью того же объекта, что и p[0], так что чтение памяти между / вне этого допускается и не будет вызывать ошибки.например, разрешить автоматическую векторизацию цикла поиска.

Но я сомневаюсь, что gcc даже ищет это, не говоря уже об этом.

(Обратите внимание, что заголовок вашего вопроса был связан с таргетингом на gccx86-64 в Linux, не о том, безопасны ли подобные вещи в gcc, например, если сделано в отдельных инструкциях. Я имею в виду, да, возможно, безопасно на практике, но не будет оптимизировано почти сразу после разбора.определенно не о C ++ в целом.)


Я настоятельно рекомендую не делать это.Используйте uintptr_t для хранения значений, похожих на указатели, которые не являются действительными указателями. Как вы делаете в обновлениях к своему ответу на расширение C ++ gcc для ненулевого распределения указателей массива? .

0 голосов
/ 03 марта 2019

Вот список расширений gcc.https://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html

Существует расширение для арифметики указателей.Gcc позволяет выполнять арифметику указателей на пустых указателях.(Не о том расширении, о котором вы спрашиваете.)

Таким образом, gcc рассматривает поведение запрашиваемой арифметики указателя как неопределенное при тех же условиях, что описаны в стандарте языка.

Вы можете просмотреть там и посмотреть, есть ли что-то, что я пропустил, которое имеет отношение к вашему вопросу.

...