Мне нужна функция, которая (например, SecureZeroMemory из WinAPI) всегда обнуляет память и не оптимизируется, даже если компилятор считает, что после этого к памяти больше не будет доступа. Похоже, идеальный кандидат на волатильность. Но у меня возникли проблемы с тем, чтобы заставить это работать с GCC. Вот пример функции:
void volatileZeroMemory(volatile void* ptr, unsigned long long size)
{
volatile unsigned char* bytePtr = (volatile unsigned char*)ptr;
while (size--)
{
*bytePtr++ = 0;
}
}
Достаточно просто. Но код, который GCC фактически генерирует, если вы его вызываете, сильно зависит от версии компилятора и количества байтов, которое вы на самом деле пытаетесь обнулить. https://godbolt.org/g/cMaQm2
- GCC 4.4.7 и 4.5.3 никогда не игнорируют изменчивые.
- GCC 4.6.4 и 4.7.3 игнорируют изменчивые значения для массивов размером 1, 2 и 4.
- GCC с 4.8.1 по 4.9.2 игнорируют изменчивые значения для массивов размером 1 и 2.
- GCC с 5.1 по 5.3 игнорируют изменчивые значения для массивов размером 1, 2, 4, 8.
- GCC 6.1 просто игнорирует его для любого размера массива (бонусные баллы за согласованность).
Любой другой компилятор, который я тестировал (clang, icc, vc), генерирует ожидаемые хранилища с любой версией компилятора и любым размером массива. Итак, на данный момент мне интересно, является ли это (довольно старая и серьезная?) Ошибка компилятора GCC, или определение volatile в стандарте неточно указывает на то, что это действительно соответствующее поведение, что делает практически невозможным написать переносимый " SecureZeroMemory "?
Изменить: некоторые интересные наблюдения.
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <atomic>
void callMeMaybe(char* buf);
void volatileZeroMemory(volatile void* ptr, std::size_t size)
{
for (auto bytePtr = static_cast<volatile std::uint8_t*>(ptr); size-- > 0; )
{
*bytePtr++ = 0;
}
//std::atomic_thread_fence(std::memory_order_release);
}
std::size_t foo()
{
char arr[8];
callMeMaybe(arr);
volatileZeroMemory(arr, sizeof arr);
return sizeof arr;
}
Возможная запись из callMeMaybe () заставит все версии GCC, кроме 6.1, генерировать ожидаемые хранилища. Комментирование в заборе памяти также заставит GCC 6.1 генерировать хранилища, но только в сочетании с возможной записью из callMeMaybe ().
Кто-то тоже предлагал промывать кеши. Microsoft вообще не пытается очистить кеш в «SecureZeroMemory». Кеш, скорее всего, в любом случае будет недействителен довольно быстро, так что это, вероятно, не имеет большого значения. Кроме того, если другая программа пыталась проверить данные или если они собирались записать в файл подкачки, это всегда будет обнуленная версия.
Есть также некоторые опасения по поводу использования в GCC 6.1 memset () в автономной функции. Компилятор GCC 6.1 на godbolt может быть сломанной сборкой, поскольку GCC 6.1, кажется, генерирует нормальный цикл (как и 5.3 на godbolt) для автономной функции для некоторых людей. (Прочтите комментарии к ответу zwol.)
volatile
- это ошибка, если не доказано обратное. Но скорее всего баг.volatile
настолько недооценено, что может быть опасным - просто не используйте его.volatile
в данном случае подходит.memset
. Проблема в том, что компиляторы точно знают, чтоmemset
делают.volatile
указатель, на который нам нужен указательvolatile
(нас не волнует,++
строгий ли он, а*p = 0
строгий).Ответы:
Поведение GCC может соответствовать, и даже если это не так, вы не должны полагаться на
volatile
то, что хотите, в таких случаях. Комитет C разработанvolatile
для аппаратных регистров с отображением в память и для переменных, измененных во время аномального потока управления (например, обработчики сигналов иsetjmp
). Это единственное, для чего он надежен. Небезопасно использовать в качестве общей аннотации «не оптимизируйте это».В частности, в стандарте неясен ключевой момент. (Я преобразовал ваш код в C; здесь не должно быть никаких расхождений между C и C ++. Я также вручную выполнил встраивание, которое произошло бы до сомнительной оптимизации, чтобы показать, что компилятор «видит» в этот момент. .)
extern void use_arr(void *, size_t); void foo(void) { char arr[8]; use_arr(arr, sizeof arr); for (volatile char *p = (volatile char *)arr; p < (volatile char *)(arr + 8); p++) *p = 0; }
Цикл очистки памяти обращается
arr
через переменную lvalue, ноarr
сам не объявляетсяvolatile
. Поэтому, по крайней мере, возможно, компилятору C разрешено сделать вывод о том, что хранилища, созданные циклом, «мертвые», и полностью удалить цикл. В Обосновании C есть текст, который подразумевает, что комитет имел в виду требовать сохранения этих хранилищ, но, как я читал, сам стандарт на самом деле не требует этого требования.Для более подробного обсуждения того, что стандарт требует или не требует, см. Почему изменчивая локальная переменная оптимизируется иначе, чем изменчивый аргумент, и почему оптимизатор генерирует цикл без операции из последнего? , Присваивает ли доступ к объявленному энергонезависимому объекту через изменчивую ссылку / указатель изменчивые правила для указанных доступов? и ошибка GCC 71793 .
Чтобы узнать больше о том, что
volatile
было задумано комитетом , поищите в Обосновании C99 слово «изменчивый». В статье Джона Регера « Volatiles are Miscompiled » подробно показано, какvolatile
производственные компиляторы могут не удовлетворить ожидания программистов . Серия эссе команды LLVM « Что должен знать каждый программист на C о неопределенном поведении » не затрагивает конкретно,volatile
но поможет вам понять, как и почему современные компиляторы C не являются «портативными ассемблерами».К практическому вопросу о том, как реализовать функцию, которая выполняет то, что вы хотите
volatileZeroMemory
сделать: независимо от того, что требует стандарт или что он должен был требовать, было бы разумно предположить, что вы не можете использоватьvolatile
для этого. Там является альтернативой , которую можно полагаться на работу, так как это нарушило бы слишком много других вещей , если это не работа:extern void memory_optimization_fence(void *ptr, size_t size); inline void explicit_bzero(void *ptr, size_t size) { memset(ptr, 0, size); memory_optimization_fence(ptr, size); } /* in a separate source file */ void memory_optimization_fence(void *unused1, size_t unused2) {}
Однако вы должны быть абсолютно уверены, что
memory_optimization_fence
это не встроено ни при каких обстоятельствах. Он должен быть в собственном исходном файле и не должен подвергаться оптимизации во время компоновки.Существуют и другие варианты, основанные на расширениях компилятора, которые могут быть использованы при некоторых обстоятельствах и могут генерировать более жесткий код (один из них появился в предыдущей редакции этого ответа), но ни один из них не является универсальным.
(Я рекомендую вызвать функцию
explicit_bzero
, потому что она доступна под этим именем более чем в одной библиотеке C. Есть по крайней мере четыре других претендента на это имя, но каждый из них был принят только одной библиотекой C.)Вы также должны знать, что даже если вы сможете заставить это работать, этого может быть недостаточно. В частности, рассмотрим
struct aes_expanded_key { __uint128_t rndk[16]; }; void encrypt(const char *key, const char *iv, const char *in, char *out, size_t size) { aes_expanded_key ek; expand_key(key, ek); encrypt_with_ek(ek, iv, in, out, size); explicit_bzero(&ek, sizeof ek); }
Предполагая, что оборудование с инструкциями ускорения AES, если
expand_key
иencrypt_with_ek
встроены, компилятор может иметь возможностьek
полностью сохранить файл в векторном регистре - до вызоваexplicit_bzero
, который заставляет его копировать конфиденциальные данные в стек только для их стирания, и, хуже того, ни черта не делает с ключами, которые все еще находятся в векторных регистрах!источник
volatile
как [...] Следовательно, любое выражение, относящееся к такому объекту, должно оцениваться строго в соответствии с правилами абстрактной машины, как описано в 5.1.2.3. Более того, в каждой точке последовательности последнее сохраненное в объекте значение должно соответствовать значению, предписанному абстрактной машиной , за исключением случаев, когда оно изменено неизвестными факторами, упомянутыми ранее. То, что составляет доступ к объекту, имеющему тип с изменяемым типом, определяется реализацией. ?volatile sig_atomic_t flag;
летучий объект .*(volatile char *)foo
это просто доступ через переменную lvalue, и стандарт не требует от нее каких-либо специальных эффектов.volatile
может быть достаточной, чтобы сделать его «совместимой» реализацией, но это не значит, что этого достаточно, чтобы быть «хорошим» или «полезным». Для многих видов системного программирования его следует рассматривать как крайне несовершенный в этом отношении.Для этого и предназначена стандартная функция
memset_s
.Что касается ли это поведение с неустойчивыми в соответствии или нет, это немного трудно сказать, и летучий было сказал , что давно страдает от ошибок.
Одна из проблем заключается в том, что в спецификациях сказано: «Доступ к изменчивым объектам оценивается строго в соответствии с правилами абстрактной машины». Но это относится только к «изменчивым объектам», а не к доступу к энергонезависимому объекту через указатель, к которому добавлен изменчивый объект. Таким образом, очевидно, что если компилятор может сказать, что вы на самом деле не обращаетесь к изменчивому объекту, тогда в конце концов не требуется рассматривать этот объект как изменчивый.
источник
memset_s
как C11-стандарт - это преувеличение. Он является частью Приложения K, которое является необязательным в C11 (и, следовательно, также необязательно в C ++). Практически все разработчики, включая Microsoft, чья идея была в первую очередь (!), Отказались ее поддержать; последний раз я слышал, что они говорили о его утилизации в C-next.size_t
разрешено использовать базовый тип . Win64 ABI не соответствует C90. Это было бы ... не нормально , но не ужасно ... если бы MSVC фактически взяли C99 вещи , какuintmax_t
и%zu
своевременно, но они не делали ).Я предлагаю эту версию как переносимый C ++ (хотя семантика несколько отличается):
void volatileZeroMemory(volatile void* const ptr, unsigned long long size) { volatile unsigned char* bytePtr = new (ptr) volatile unsigned char[size]; while (size--) { *bytePtr++ = 0; } }
Теперь у вас есть доступ на запись к изменчивому объекту , а не просто доступ к энергонезависимому объекту, сделанный через изменчивое представление объекта.
Семантическая разница заключается в том, что теперь он формально завершает время жизни любого объекта (ов), занимающего область памяти, потому что память была повторно использована. Таким образом, доступ к объекту после обнуления его содержимого теперь определенно является неопределенным поведением (раньше в большинстве случаев это было бы неопределенным поведением, но, безусловно, существовали некоторые исключения).
Чтобы использовать это обнуление во время существования объекта, а не в конце, вызывающий должен использовать размещение,
new
чтобы снова вернуть новый экземпляр исходного типа.Код можно сделать короче (хотя и менее понятным) с помощью инициализации значения:
void volatileZeroMemory(volatile void* const ptr, unsigned long long size) { new (ptr) volatile unsigned char[size] (); }
и на данный момент это однострочник, и он практически не требует вспомогательной функции.
источник
Должна быть возможность написать переносимую версию функции, используя изменчивый объект с правой стороны и заставляя компилятор сохранять хранилища в массиве.
void volatileZeroMemory(void* ptr, unsigned long long size) { volatile unsigned char zero = 0; unsigned char* bytePtr = static_cast<unsigned char*>(ptr); while (size--) { *bytePtr++ = zero; } zero = static_cast<unsigned char*>(ptr)[zero]; }
Объявляется
zero
объект,volatile
который гарантирует, что компилятор не может делать никаких предположений о его значении, даже если он всегда оценивается как ноль.Последнее выражение присваивания считывается из изменчивого индекса в массиве и сохраняет значение в изменчивом объекте. Поскольку это чтение не может быть оптимизировано, оно гарантирует, что компилятор должен сгенерировать хранилища, указанные в цикле.
источник
*ptr
время этого цикла или вообще чего-либо ... просто зацикливается. черт возьми, вот и мой мозг.edx
: я получаю это:.L16: subq $1, %rax; movzbl -1(%rsp), %edx; jne .L16
volatile unsigned char const
байта заполнения ... он даже не читает его . Сгенерированный встроенный вызовvolatileFill()
- это просто[load RAX with sizeof] .L9: subq $1, %rax; jne .L9
. Почему оптимизатор (A) не перечитывает байт заполнения, а (B) заботится о сохранении цикла там, где он ничего не делает?