Potrzebuję funkcji, która (jak SecureZeroMemory z WinAPI) zawsze zeruje pamięć i nie jest zoptymalizowana, nawet jeśli kompilator uważa, że pamięć nie będzie już nigdy dostępna. Wydaje się, że jest to idealny kandydat na niestabilność. Ale mam pewne problemy z uruchomieniem tego z GCC. Oto przykładowa funkcja:
void volatileZeroMemory(volatile void* ptr, unsigned long long size)
{
volatile unsigned char* bytePtr = (volatile unsigned char*)ptr;
while (size--)
{
*bytePtr++ = 0;
}
}
Wystarczająco proste. Ale kod, który faktycznie generuje GCC, jeśli go wywołasz, różni się gwałtownie w zależności od wersji kompilatora i ilości bajtów, które faktycznie próbujesz wyzerować. https://godbolt.org/g/cMaQm2
- GCC 4.4.7 i 4.5.3 nigdy nie ignorują zmienności.
- GCC 4.6.4 i 4.7.3 ignorują zmienne dla tablic o rozmiarach 1, 2 i 4.
- GCC 4.8.1 do 4.9.2 ignoruje zmienne dla tablic o rozmiarach 1 i 2.
- GCC 5.1 do 5.3 ignoruje volatile dla tablic o rozmiarach 1, 2, 4, 8.
- GCC 6.1 po prostu ignoruje to dla dowolnego rozmiaru tablicy (dodatkowe punkty za spójność).
Każdy inny testowany przeze mnie kompilator (clang, icc, vc) generuje sklepy, których można by się spodziewać, z dowolną wersją kompilatora i dowolnym rozmiarem tablicy. W tym miejscu zastanawiam się, czy jest to (dość stary i poważny?) Błąd kompilatora GCC, czy też definicja niestabilności w standardzie jest tak nieprecyzyjna, że jest to faktycznie zgodne zachowanie, co zasadniczo uniemożliwia napisanie przenośnego. „SecureZeroMemory”?
Edycja: kilka interesujących obserwacji.
#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;
}
Możliwy zapis z callMeMaybe () spowoduje, że wszystkie wersje GCC z wyjątkiem 6.1 wygenerują oczekiwane magazyny. Komentowanie w obszarze pamięci spowoduje również, że GCC 6.1 wygeneruje magazyny, chociaż tylko w połączeniu z możliwym zapisem z callMeMaybe ().
Ktoś zasugerował również opróżnienie pamięci podręcznych. Firma Microsoft w ogóle nie próbuje opróżnić pamięci podręcznej w „SecureZeroMemory”. Pamięć podręczna prawdopodobnie i tak zostanie unieważniona dość szybko, więc prawdopodobnie nie będzie to wielka sprawa. Ponadto, jeśli inny program próbował sondować dane lub miał zostać zapisany w pliku stronicowania, zawsze będzie to wersja wyzerowana.
Istnieją również pewne obawy związane z używaniem memset () w GCC 6.1 w funkcji samodzielnej. Kompilator GCC 6.1 na Godbolt może być zepsuty, ponieważ GCC 6.1 wydaje się generować normalną pętlę (jak 5.3 robi na Godbolt) dla samodzielnej funkcji dla niektórych ludzi. (Przeczytaj komentarze do odpowiedzi Zwol.)
volatile
jest błędem, chyba że udowodniono inaczej. Ale najprawdopodobniej błąd.volatile
jest tak niedookreślony, że jest niebezpieczny - po prostu go nie używaj.volatile
w tym przypadku jest odpowiednie.memset
. Problem w tym, że kompilatory dokładnie wiedzą, comemset
robi.volatile
wskaźnik, do którego chcemy, aby wskaźnikvolatile
(nie obchodzi nas, czy++
jest ścisły, ale czy*p = 0
jest ścisły).Odpowiedzi:
Zachowanie GCC może być zgodne, a nawet jeśli tak nie jest, nie powinieneś polegać na
volatile
robieniu tego, co chcesz w takich przypadkach. Komitet C zaprojektowanyvolatile
dla rejestrów sprzętowych mapowanych w pamięci i dla zmiennych modyfikowanych podczas nieprawidłowego przepływu sterowania (np. Obsługi sygnałów isetjmp
). To jedyne rzeczy, na których można polegać. Używanie ogólnej adnotacji „nie optymalizuj tego” nie jest bezpieczne.W szczególności norma jest niejasna w kluczowym punkcie. (Przekonwertowałem twój kod na C; nie powinno być tutaj żadnych rozbieżności między C i C ++. Również ręcznie wykonałem wstawianie przed wątpliwą optymalizacją, aby pokazać co kompilator "widzi" w tym momencie .)
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; }
Pętla do czyszczenia pamięci uzyskuje dostęp
arr
za pośrednictwem lvalue kwalifikowanej ulotnie, alearr
sama nie jest zadeklarowanavolatile
. Dlatego przynajmniej prawdopodobnie kompilator C może wywnioskować, że zapasy utworzone przez pętlę są „martwe”, i całkowicie usunąć pętlę. W uzasadnieniu C znajduje się tekst, który sugeruje, że komitet chciał wymagać, aby te sklepy były konserwowane, ale sam standard w rzeczywistości nie nakłada takiego wymogu, jak go czytałem.Aby uzyskać więcej informacji na temat tego, czego standard wymaga, a czego nie, zobacz Dlaczego zmienna zmienna lokalna zmienna jest zoptymalizowana inaczej niż argument zmienny i dlaczego optymalizator generuje pętlę bez operacji na podstawie tego drugiego? , Czy dostęp do zadeklarowanego nieulotnego obiektu poprzez zmienne odniesienie / wskaźnik nadaje zmienne reguły tym dostępom? i błąd GCC 71793 .
Aby uzyskać więcej informacji na temat tego, o czym myśli komisja
volatile
, poszukaj w uzasadnieniu C99 słowa „niestabilny”. Artykuł Johna Regehra „ Volatiles are Miscompiled ” ilustruje szczegółowo, w jaki sposób oczekiwania programistówvolatile
mogą nie zostać spełnione przez kompilatory produkcyjne. Seria esejów zespołu LLVM „ Co każdy programista języka C powinien wiedzieć o niezdefiniowanym zachowaniu ” nie dotyczy konkretnie,volatile
ale pomoże ci zrozumieć, w jaki sposób i dlaczego współczesne kompilatory języka C nie są „przenośnymi asemblerami”.Do praktycznego pytania o to, jak zaimplementować funkcję, która robi to, co chciałeś
volatileZeroMemory
zrobić: Bez względu na to, czego wymaga lub miał wymagać norma, najrozsądniej byłoby założyć, że nie możesz tego użyćvolatile
. Jest to alternatywa, że można powołać się na pracy, ponieważ byłoby złamać zbyt wiele innych rzeczy, jeśli to nie działa: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) {}
Musisz jednak absolutnie upewnić się, że
memory_optimization_fence
nie jest to pod żadnym pozorem. Musi znajdować się we własnym pliku źródłowym i nie może podlegać optymalizacji czasu łącza.Istnieją inne opcje, opierające się na rozszerzeniach kompilatora, które mogą być użyteczne w pewnych okolicznościach i mogą generować ściślejszy kod (jedna z nich pojawiła się w poprzednim wydaniu tej odpowiedzi), ale żadna nie jest uniwersalna.
(Polecam wywołanie funkcji
explicit_bzero
, ponieważ jest ona dostępna pod tą nazwą w więcej niż jednej bibliotece C. Jest co najmniej czterech innych pretendentów do tej nazwy, ale każdy z nich został przyjęty tylko przez jedną bibliotekę C.)Powinieneś także wiedzieć, że nawet jeśli możesz to uruchomić, może to nie wystarczyć. W szczególności rozważ
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); }
Zakładając, że sprzęt z instrukcjami przyspieszania AES, jeśli
expand_key
iencrypt_with_ek
są wbudowane, kompilator może być w stanie zachowaćek
całość w pliku rejestru wektorowego - aż do wywołaniaexplicit_bzero
, które zmusza go do skopiowania poufnych danych na stos tylko w celu ich usunięcia, oraz gorzej, nie robi nic z klawiszami, które wciąż znajdują się w rejestrach wektorów!źródło
volatile
jako [...] Dlatego każde wyrażenie odnoszące się do takiego obiektu powinno być oceniane ściśle według reguł maszyny abstrakcyjnej, jak opisano w 5.1.2.3. Ponadto w każdym punkcie sekwencji wartość ostatnio zapisana w obiekcie będzie zgodna z wartością zalecaną przez maszynę abstrakcyjną , z wyjątkiem modyfikacji przez nieznane czynniki wspomniane wcześniej. To, co stanowi dostęp do obiektu, który ma typ ulotny-kwalifikowany, jest zdefiniowane przez implementację. ?volatile sig_atomic_t flag;
jest lotnym przedmiot .*(volatile char *)foo
jest jedynie dostępem poprzez zmienną kwalifikowaną l-wartość, a standard nie wymaga tego, aby mieć jakiekolwiek efekty specjalne.volatile
może być wystarczające, aby uczynić ją implementacją „zgodną”, ale to nie znaczy, że wystarczy, aby była „dobra” lub „użyteczna”. W przypadku wielu rodzajów programowania systemów należy to uznać za żałośnie ułomne w tym względzie.Do tego służy standardowa funkcja
memset_s
.Czy takie zachowanie jest zgodne z lotny, czy nie, to trochę trudno powiedzieć, a lotny został powiedział do od dawna boryka się z błędów.
Jedną z kwestii jest to, że specyfikacje mówią, że „Dostęp do obiektów ulotnych jest oceniany ściśle według reguł abstrakcyjnej maszyny”. Ale to odnosi się tylko do „obiektów ulotnych”, a nie dostępu do nieulotnego obiektu za pomocą wskaźnika, do którego dodano zmienne. Najwyraźniej więc jeśli kompilator może stwierdzić, że tak naprawdę nie uzyskujesz dostępu do obiektu ulotnego, nie jest wymagane traktowanie obiektu jako ulotnego.
źródło
memset_s
jako standard C11 to przesada. Jest częścią załącznika K, który jest opcjonalny w C11 (a zatem również opcjonalny w C ++). Zasadniczo wszyscy wdrażający, w tym Microsoft, którego pomysł był na pierwszym miejscu (!), Odmówili przyjęcia go; ostatnio słyszałem, że rozmawiali o złomowaniu go w C-next.size_t
mogą być. ABI Win64 nie jest zgodne z C90. To byłoby ... nie w porządku , ale nie straszne ... gdyby MSVC faktycznie odebrał rzeczy takie jak C99uintmax_t
i%zu
w odpowiednim czasie, ale tak się nie stało .)Oferuję tę wersję jako przenośny C ++ (chociaż semantyka jest nieco inna):
void volatileZeroMemory(volatile void* const ptr, unsigned long long size) { volatile unsigned char* bytePtr = new (ptr) volatile unsigned char[size]; while (size--) { *bytePtr++ = 0; } }
Teraz masz dostęp do zapisu do obiektu ulotnego , a nie tylko dostęp do obiektu nieulotnego utworzony za pomocą ulotnego widoku obiektu.
Różnica semantyczna polega na tym, że teraz formalnie kończy ona żywotność dowolnego obiektu (obiektów) zajmowanego w regionie pamięci, ponieważ pamięć została ponownie wykorzystana. Tak więc dostęp do obiektu po wyzerowaniu jego zawartości jest teraz z pewnością niezdefiniowanym zachowaniem (wcześniej w większości przypadków byłoby to niezdefiniowane zachowanie, ale z pewnością istniały pewne wyjątki).
Aby użyć tego zerowania w okresie istnienia obiektu zamiast na końcu, obiekt wywołujący powinien użyć umieszczenia,
new
aby ponownie umieścić nowe wystąpienie oryginalnego typu.Kod można skrócić (aczkolwiek mniej czytelny) za pomocą inicjalizacji wartości:
void volatileZeroMemory(volatile void* const ptr, unsigned long long size) { new (ptr) volatile unsigned char[size] (); }
w tym momencie jest to jednolinijka i ledwo gwarantuje w ogóle funkcję pomocniczą.
źródło
Powinno być możliwe napisanie przenośnej wersji funkcji przy użyciu nietrwałego obiektu po prawej stronie i zmuszając kompilator do zachowania magazynów w tablicy.
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
Obiekt jest zadeklarowanavolatile
, który zapewnia, kompilator może dokonać żadnych założeń co do jego wartości, mimo że zawsze ocenia jako zero.Ostatnie wyrażenie przypisania odczytuje zmienny indeks w tablicy i zapisuje wartość w zmiennym obiekcie. Ponieważ tego odczytu nie można zoptymalizować, zapewnia to, że kompilator musi wygenerować magazyny określone w pętli.
źródło
*ptr
podczas tej pętli, ani właściwie niczego ... po prostu zapętla. wtf, idzie mój mózg.edx
: Dostaję to:.L16: subq $1, %rax; movzbl -1(%rsp), %edx; jne .L16
volatile unsigned char const
bajtu wypełnienia ... nawet go nie czyta . Wygenerowane wbudowane wywołanievolatileFill()
to just[load RAX with sizeof] .L9: subq $1, %rax; jne .L9
. Dlaczego optymalizator (A) nie odczytuje ponownie bajtu wypełniania, a (B) zawraca sobie głowę zachowaniem pętli, w której nic nie robi?