Использование летучих дважды в R-значении

Заявление:

volatile unsigned char * volatile p = (volatile unsigned char * volatile)v;

Генерирует предупреждение C4197 в MSVC v14.1:

Предупреждение C4197: "volatile unsigned char * volatile": волатильность верхнего уровня в листинге игнорируется

В стандарте 2011 C (раздел [N1570] 6.7.3 4.) говорится: "Свойства, связанные с квалифицированными типами, имеют смысл только для выражений, которые являются значениями l", таким образом, волатильность верхнего уровня в этом приведении игнорируется и генерирует это предупреждение.

Автор этого кода заявляет, что он не нарушает стандарт C и необходим для предотвращения некоторых оптимизаций GCC. Он иллюстрирует проблему с кодом по адресу: https://godbolt.org/g/xP4eGz

#include <stddef.h>

static void memset_s(void * v, size_t n) {
  volatile unsigned char * p = (volatile unsigned char *)v;
  for(size_t i = 0; i < n; ++i) {
    p[i] = 0;
  }
}

void f1() {
  unsigned char x[4];
  memset_s(x, sizeof x);
}

static void memset_s_volatile_pnt(void * v, size_t n) {
  volatile unsigned char * volatile p = (volatile unsigned char * volatile)v;
  for(size_t i = 0; i < n; ++i) {
    p[i] = 0;
  }
}

void f1_volatile_pnt() {
  unsigned char x[4];
  memset_s_volatile_pnt(x, sizeof x);
}

... где он показывает, что функция f1() ничего не компилирует (просто инструкция ret), но f1_volatile_pnt() компилируется в инструкции, которые выполняют задание.

ВОПРОС: Есть ли способ правильно написать этот код, чтобы он был правильно скомпилирован GCC и в соответствии со стандартом 2011 C (раздел [N1570] 6.7.3 4.), поэтому он не генерирует предупреждение с MSVC и ICC?... без #ifdef...

В контексте этой проблемы см.: https://github.com/jedisct1/libsodium/issues/687

Ответ 1

Заключение

Сделать код volatile unsigned char * volatile p = (volatile unsigned char * volatile) v; скомпилировать на C или в C++ без предупреждений и при сохранении намерений авторов удалить вторую volatile в составе:

volatile unsigned char * volatile p = (volatile unsigned char *) v;

Присвоение в C не требуется, но в вопросе спрашивается, что код можно компилировать без предупреждения в MSVC, который компилируется как C++, а не C, поэтому приведение требуется. Только в C, если утверждение может быть (предполагается, что v является void * или совместим с типом p):

volatile unsigned char * volatile p = v;

Почему квалифицировать указатель как изменчивый

Исходный источник содержит этот код:

volatile unsigned char *volatile pnt_ =
    (volatile unsigned char *volatile) pnt;
size_t i = (size_t) 0U;

while (i < len) {
    pnt_[i++] = 0U;

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

Предположим, у нас есть некоторый буфер x который является массивом unsigned char. Если x были определены с volatile, это изменчивый объект, и компилятор всегда реализует на него записи; он никогда не удаляет их во время оптимизации.

С другой стороны, если x не определяется с volatile, но мы помещаем его адрес в указатель p который имеет pointer to volatile unsigned char типа pointer to volatile unsigned char, что происходит, когда мы пишем *p = 0? Как указывает R.., если компилятор может видеть, что p указывает на x, он знает, что изменяемый объект не является изменчивым, и поэтому компилятор не обязан фактически записывать в память, если он в противном случае может оптимизировать назначение. Это связано с тем, что стандарт C определяет volatile с точки зрения доступа к изменчивым объектам, а не просто для доступа к памяти через указатель, который имеет тип "указатель на летучее что-то".

Чтобы гарантировать, что компилятор действительно пишет x, автор этого кода объявляет p неустойчивым. Это означает, что в *p = 0 компилятор не может знать, что p указывает на x. Компилятор должен загрузить значение p из любой памяти, которую он назначил для p; он должен предположить, что p может измениться от значения, которое указано в x.

Кроме того, когда p объявлен volatile unsigned char *volatile p, компилятор должен предположить, что место, на которое указывает p является изменчивым. (Технически, когда он загружает значение p, он может исследовать его, обнаруживать, что на самом деле он указывает на x или какую-то другую память, которая, как известно, нестабильна, а затем воспринимает ее как энергонезависимую. Но это было бы необычайным усилием компилятором, и мы можем предположить, что этого не происходит.)

Поэтому, если бы код был:

volatile unsigned char *pnt_ = pnt;
size_t i = (size_t) 0U;

while (i < len) {
    pnt_[i++] = 0U;

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

volatile unsigned char *volatile pnt_ = pnt;
size_t i = (size_t) 0U;

while (i < len) {
    pnt_[i++] = 0U;

то на каждой итерации цикла компилятор должен:

  • Загрузите pnt_ из выделенной для него памяти.
  • Вычислите адрес получателя.
  • Записывать нуль на этот адрес (если компилятор не переходит к чрезвычайной проблеме определения адреса, является нелетучим).

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

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

Значение ценности

Рассмотрим определение:

volatile unsigned char * volatile p = (volatile unsigned char * volatile) v;

Мы видели выше, что определение p как volatile unsigned char * volatile необходимо для достижения цели авторов, хотя это неудачная попытка обхода недостатков в C. Однако, как насчет трансляции (volatile unsigned char * volatile).

Во-первых, бросок не нужен, так как значение v будет автоматически преобразовано в тип p. Чтобы избежать предупреждения в MSVC, бросок можно просто удалить, оставив определение в виде volatile unsigned char * volatile p = v; ,

Учитывая, что приведение происходит, вопрос задает вопрос о том, имеет ли второй volatile смысл. В стандарте C явно говорится: "Свойства, связанные с квалифицированными типами, имеют смысл только для выражений, которые являются lvalues". (C 2011 [N1570] 6.7.3 4.)

volatile означает, что что-то неизвестное компилятору может изменить значение объекта. Например, если в программе есть volatile int a, это означает, что объект, идентифицированный a может быть изменен некоторыми способами, не известными компилятору. Он может быть изменен с помощью специального оборудования на компьютере, отладчиком, операционной системой или другими средствами.

volatile изменяет объект. Объектом является область хранения данных в памяти, которая может представлять значения.

В выражениях мы имеем значения. Например, некоторые значения int равны 3, 5 или -1. Значения не могут быть неустойчивыми. Они не хранятся в памяти; они являются абстрактными математическими значениями. Число 3 никогда не может измениться; это всегда 3.

Приведение (volatile unsigned char * volatile) говорит, чтобы заставить что-то быть volatile указателем на volatile unsigned char. Хорошо указывать на volatile unsigned char -a указатель указывает на что-то в памяти. Но что значит быть изменчивым указателем? Указатель - это просто значение; это адрес. Значения не имеют памяти, они не являются объектами, поэтому они не могут быть неустойчивыми. Таким образом, вторая volatile в трансляции (volatile unsigned char * volatile) не действует в стандарте C. Соответствует C-коду, но квалификатор не действует.

Ответ 2

Принципиально нет способа выразить то, что автор хочет выразить. Некоторые версии компилятора корректно оптимизируют первую версию кода, потому что базовый объект unsigned char x[4] не является изменчивым; доступ к нему через указатель к летучести не волшебным образом делает его изменчивым.

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

Безопасная очистка памяти не выражается в C; ему нужны расширения на уровне компилятора/языка. Лучший способ сделать это в C + POSIX - это просто обработка конфиденциальных данных в отдельном процессе, время жизни которого ограничено продолжительностью чувствительных данных и полагаться на границы защиты памяти, чтобы гарантировать, что он никогда не течет где-либо еще.

Если вы просто хотите избавиться от предупреждения, решение легко. Просто измените:

volatile unsigned char * volatile p = (volatile unsigned char * volatile)v;

чтобы:

volatile unsigned char * volatile p = (volatile unsigned char *)v;