Главная » Хабрахабр » [Перевод] Обнаруживаем целочисленные константные выражения в макросе [вместе с Линусом]

[Перевод] Обнаруживаем целочисленные константные выражения в макросе [вместе с Линусом]

Вашему вниманию предлагается перевод недавнего письма по поводу неоднозначной идеи из рассылки Linux Kernel Mailing List, вызвавшей традиционную реакцию Линуса Торвальдса. Необходимые для понимания пояснения предоставлены в конце поста.

Письмо

Отправитель: Мартин Уэкер
Дата: Tue, 20 Mar 2018 22:13:35 +0000
Тема: Обнаружение целочисленных константных выражений в макросе

Здравствуй Линус,

У меня появилась идея:

Тест для целочисленных константных выражений, который возвращает само целочисленное константное выражение (integer constant expression, ICE), которое должно подходить для передачи в __builtin_choose_expr, и выглядит следующим образом:

#define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))

Кстати, в этом выражении само x не вычисляется в gcc, хотя это и не гарантируется стандартом (я не проверял этот факт в старых версиях gcc.)

Ответ Линуса Торвальдса

Отправитель: Линус Торвальдс <>
Дата: Tue, 20 Mar 2018 16:08:30 -0700
Тема: Re: Обнаружение целочисленных константных выражений в макросе

On Tue, Mar 20, 2018 at 3:13 PM, Мартин Уэкер
<Martin.Uecker@med.uni-goettingen.de> написал:
У меня появилась идея:

Нет, это не «идея».
Это либо работа гения, либо напрочь больного на голову.
До конца пока не уверен, поэтому не могу сказать с точностью.

Тест для целочисленных константных выражений, который возвращает само целочисленное константное выражение, которое должно подходить для передачи в __builtin_choose_expr, и выглядит следующим образом:

#define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))

ОК, здесь я вижу, что (void *)((x)*0l)) становится NULL когда x – это ICE. Хорошо. С константой мы имеем:

sizeof( 1 ? NULL : (int *) 1)

и правило здесь следующее — если одна из сторон тернарного оператора с указателями является NULL, то её конечный результат — это другой тип (int *).

Так что да, выражение выше возвращает sizeof(int).

И если оно не ICE, то первый указатель всё ещё типа (void*), но он не NULL.

И да, правила приведения типов для тернарного оператора с двумя указателями, каждый из которых не является NULL, различные — поэтому теперь оно возвращает "void *".

Итак, теперь конечный результат — это (sizeof(*(void *)(x)), что в gcc как правило отличается от int.

Итак, здесь я наблюдаю две проблемы:

  • "sizeof(*(void *)1)" не обязательно строго определено. Для gcc это 1. Это может стать причиной предупреждений (warnings).
  • это поломает мозг каждому, кому на глаза попадется данное выражение.

Однако, обе эти проблемы могут не иметь особого значения, и всё это может быть нормой.

Кстати, в этом выражении само x не вычисляется в gcc, хотя это и не гарантируется стандартом (я не проверял этого в старых версиях gcc.)

О, как по мне, стандартом именно что гарантируется, что оператор sizeof() не вычисляет значение аргумента, только его тип.

Я в восторге от вашего по-настоящему удивительного и отвратительного «хака». Он представляет собой самое настоящее произведение искусства.

Я уверен, что это не будет работать или вызовет предупреждения по разным причинам, но
это по-прежнему просто прекрасно.

Линус

Давайте постараемся разобраться в том, что происходит в данном коде.

#define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))

Мы определяем макрос ICE_P(x). P — это, согласно правилам именования, лисповатый предикат. ICE обозначает целочисленное константное выражение. Мы хотим вернуть true, если x — это целочисленное константное выражение, и false — в другом случае.

Это выражение будет true, если правая часть сравнения равна sizeof(int). Попробуем развернуть её.

sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1))

Это выражение возвращает размер типа, на который указывает тернарное выражение. Копаем глубже.

1 ? ((void*)((x) * 0l)) : (int*)1

Понятное дело, левая часть всегда возвращается, поскольку 1 — это всегда true. Как разъясняет Линус, когда x — это ICE, левая сторона становится NULL. Получается, у нас есть два возможных варианта:

Когда x это ICE: 1 ? ((void*)(NULL)) : (int*)1
Когда x это не ICE: 1 ? ((void*)(NOT-NULL)) : (int*)1

Единственная разница состоит в том, является ли void* слева NULL или нет.

Если оно NULL (x — это ICE), выражение возвращает тип int*
Если оно не NULL (x — это не ICE), выражение возвращает void*

По сути, тернарное выражение может превратить NULL void * в int *, но когда void * — не NULL, вместо этого превратит int * в void *. Теперь мы можем вернуться к оригинальному выражению, и мы получаем следующее:

Если x это ICE: sizeof(int) == sizeof(*(int *))
Когда x это не ICE: sizeof(int) == sizeof(*(void *))

Разыменование void * не является валидной операцией, но sizeof — это магия, оно полностью вычисляется во время компиляции. В gcc код sizeof(*(void *)) даёт 1.

Вот пример кода, позволяющий протестировать данный макрос, icep.c:

/* компилируем и запускаем: gcc icep.c -o icep && ./icep ожидаемый вывод: $ gcc icep.c -o icep && ./icep ICE_P(1): 1 ICE_P('c'): 1 ICE_P(rand()): 0
*/ #include <stdio.h>
#include <stdlib.h> #define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))
#define CHECK(x) printf("ICE_P(%s): %d\n", #x, ICE_P(x)) int main()
{ CHECK(1); CHECK('c'); CHECK(rand()); return 0;
}

Дополнительное объяснение

Ключевое выражение здесь — это всего лишь x * 0. Если x — это целочисленная константа, компилятор может произвести вычисление, и целое на ноль — это ноль. Если x — это не целочисленная константа, то компилятор не может выполнить это вычисление, и неизвестно, является ли оно нулем. Этот результат приводится к «пустому» указателю (void pointer). Вот как мы узнаем, NULL или нет (поскольку void pointer к нулю — это определение NULL).

Еще один ключ к пониманию этого выражения — это тип a ? b : c. Понятно, что b и c могут иметь различные типы, и в этом случае, компилятор должен выяснить «общий» тип этих выражений. Здесь c — это явно указатель на int. Но NULL совместим с другими типами указателей. Так что если b — это NULL, тогда общий тип — это int*, поскольку он описывает оба выражения. Однако, если статически неизвестно, является ли b NULL, то единственным типом, который подходит void* и int* — это void*.

Это приводит нас к тому, что мы делаем sizeof(*(void*)), когда x — это не целочисленное константное выражение, и sizeof(*(int*)), когда x — это оно самое.


x

Ещё Hi-Tech Интересное!

Погружение в AD: разбираем продвинутые атаки на Microsoft Active Directory и способы их детекта

Изображение: Pexels Участники рассказывают о новых векторах и своих изобретениях, но не забывают и о советах, как можно их обнаружить и предотвратить. За последние четыре года ни один Black Hat или DEF CON не обошелся без докладов на тему атак ...

На все компьютеры в России хотят предустанавливать российские антивирусы

В правительство РФ внесён национальный проект «Цифровая экономика», в паспорте которого указано интересное предложение от Министерство цифрового развития, связи и массовых коммуникаций России: законодательно обеспечить предустановку отечественных антивирусных программ на все персональные компьютеры, ввозимые и создаваемые на территории РФ, начиная ...