Ответ 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-коду, но квалификатор не действует.