Czy definicja "lotnego" jest taka lotna, czy GCC ma jakieś problemy ze standardową zgodnością?

Potrzebuję funkcji ,która (jak SecureZeroMemory z WinAPI) zawsze zeruje pamięć i nie jest optymalizowana, nawet jeśli kompilator myśli, że pamięć nigdy nie będzie dostępna ponownie po tym. Wygląda na idealnego kandydata do volatile. 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;
    }
}
Dość proste. Ale kod, który generuje GCC, jeśli go wywołasz, różni się szalenie z wersją kompilatora i ilością bajtów, które próbujesz zerować. https://godbolt.org/g/cMaQm2
  • GCC 4.4.7 i 4.5.3 nigdy nie ignoruj lotnych.
  • GCC 4.6.4 i 4.7.3 ignorują zmienność dla rozmiarów tablic 1, 2 i 4.
  • GCC 4.8.1 do 4.9.2 ignoruje zmienne dla tablic o rozmiarach 1 i 2.
  • GCC 5.1 until 5.3 ignoruj lotne dla tablic o rozmiarach 1, 2, 4, 8.
  • GCC 6.1 po prostu ignoruje go dla dowolnego rozmiaru tablicy (punkty bonusowe za spójność).

Każdy inny kompilator, który mam tested (clang, icc, vc) generuje sklepy, których można się spodziewać, z dowolną wersją kompilatora i dowolnym rozmiarem tablicy. Więc w tym momencie zastanawiam się, czy to jest (dość stare i poważne?) Błąd kompilatora GCC, czy definicja volatile w standardzie jest nieprecyzyjna, że jest to rzeczywiście zgodne zachowanie, co zasadniczo uniemożliwia napisanie przenośnej funkcji "SecureZeroMemory"?

Edit: kilka ciekawych spostrzeżeń.

#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 spraw, aby wszystkie wersje GCC z wyjątkiem 6.1 wygenerowały oczekiwane sklepy. Komentowanie w ogrodzeniu pamięci spowoduje również, że GCC 6.1 wygeneruje sklepy, chociaż tylko w połączeniu z możliwym zapisem z callMeMaybe ().

Ktoś zasugerował również, aby spłukać pamięć podręczną. Microsoft robi Nie próbuje opróżnić pamięć podręczną w ogóle w "SecureZeroMemory". pamięć podręczna prawdopodobnie zostanie szybko unieważniona, więc prawdopodobnie nie będzie to wielka sprawa. Również, jeśli inny program próbowało zbadać dane, lub gdyby miało być zapisane do pliku strony, zawsze byłaby to zerowana wersja.

Istnieją również pewne obawy dotyczące GCC 6.1 używającego memset () w funkcji autonomicznej. Kompilator gcc 6.1 na godbolcie może być zepsutą kompilacją, ponieważ GCC 6.1 wydaje się generować normalną pętlę (podobnie jak 5.3 na godbolcie) dla samodzielnej funkcji dla niektórych ludzi. (Czytaj komentarze do odpowiedzi Zwola.)

Author: cooky451, 2016-07-06

4 answers

Zachowanie GCC Może być zgodne, a nawet jeśli nie, nie powinieneś polegać na volatile, aby robić to, co chcesz w takich przypadkach. Komitet C zaprojektował volatile dla rejestrów sprzętowych mapowanych w pamięci oraz dla zmiennych modyfikowanych podczas nieprawidłowego przepływu sterowania(np. signal handlers i setjmp). to są jedyne rzeczy, dla których jest wiarygodny. nie jest bezpiecznie używać jako ogólnej adnotacji "nie Optymalizuj tego".

W szczególności, norma jest niejasna na klucz punkt. (Przekonwertowałem Twój kod na C; nie powinno być rozbieżności między C i c++ tutaj. Zrobiłem też ręcznie inlining, który wydarzyłby się 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 odczytu pamięci uzyskuje dostęp arr przez zmienną kwalifikowaną lvalue, ale arr sama w sobie jest , a nie zadeklarowana volatile. Dlatego przynajmniej możliwe jest, aby kompilator C wywnioskował, że sklepy wykonane przez pętle są "martwe" i całkowicie usunąć pętlę. W uzasadnieniu C jest tekst, który sugeruje, że komitet miał na myśli , aby wymagać zachowania tych sklepów, ale sam standard nie nakłada tego wymogu, jak to czytam.

Aby uzyskać więcej informacji na temat tego, czego standard wymaga, zobacz dlaczego zmienna lokalna jest zoptymalizowana inaczej niż zmienna zmienna zmienna i dlaczego optymalizator generuje pętlę no-op z Ostatnie?, czy dostęp do zadeklarowanego nieulotnego obiektu za pomocą lotnego odniesienia/wskaźnika nadaje lotne Zasady wspomnianym dostępom? i GCC bug 71793.

Więcej o tym, co myślał Komitet volatile był dla, szukaj C99 dla słowa "Lotny". Artykuł Johna Regehra "Volatiles are Miscompiled " szczegółowo ilustruje, w jaki sposób oczekiwania programistów wobec volatile mogą nie być spełnione przez kompilatory produkcyjne. Na Seria esejów zespołu LLVM "What Every C Programmer Should Know About Undefined Behavior " nie dotyka konkretnie volatile, ale pomoże Ci zrozumieć, w jaki sposób i dlaczego współczesne kompilatory C są, a nie "przenośnymi asemblerami".


Do praktycznego pytania o to, jak zaimplementować funkcję, która robi to, co chciałeś volatileZeroMemory: niezależnie od tego, czego standard wymaga lub miał wymagać, mądrzej byłoby założyć, że nie możesz użyć volatile dla to. Istnieje jest alternatywa, na której można polegać, aby działać, ponieważ zepsuje zbyt wiele innych rzeczy, jeśli nie zadział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 upewnić się, że nie jest ona w żadnych okolicznościach zamknięta. Musi znajdować się we własnym pliku źródłowym i nie może być poddawany 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ć ciaśniejszy kod (jeden z pojawiły się w poprzednim wydaniu tej odpowiedzi), ale żaden nie jest uniwersalny.

(zalecam wywołanie funkcji explicit_bzero, ponieważ jest ona dostępna pod tą nazwą w więcej niż jednej bibliotece C. Istnieje co najmniej czterech innych pretendentów do nazwy, ale każdy został przyjęty tylko przez jedną bibliotekę C.)

Powinieneś też wiedzieć, że nawet jeśli uda ci się to uruchomić, może to nie wystarczyć. W szczególności rozważmy
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 sprzęt z akceleracją AES instrukcje, jeśli expand_key i encrypt_with_ek są inline, kompilator może być w stanie zachować ek w całości w pliku rejestru wektorowego-aż do wywołania explicit_bzero, co zmusza go do skopiowania poufnych danych na stos tylko po to, aby je wymazać, a co gorsza, nie robi nic z kluczami, które wciąż znajdują się w rejestrach wektorowych!

 80
Author: zwol,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-02-24 20:04:45

Potrzebuję funkcji, która (jak SecureZeroMemory z WinAPI) zawsze zeruje pamięć i nie jest optymalizowana,

Do tego służy funkcja standardowa memset_s.


Jeśli chodzi o to, czy to zachowanie z volatile jest zgodne, czy nie, to trochę trudno powiedzieć, a volatile zostało powiedziane od dawna nękane błędami.

Jeden problem polega na tym, że specyfikacje mówią, że " dostęp do obiektów lotnych jest oceniany ściśle według Zasady maszyny abstrakcyjnej."Ale to odnosi się tylko do "obiektów lotnych", nie uzyskując dostępu do nieulotnego obiektu za pomocą wskaźnika, który dodał lotne. Więc najwyraźniej, jeśli kompilator może stwierdzić, że tak naprawdę nie uzyskujesz dostępu do obiektu volatile, to nie jest wymagane traktowanie obiektu jako volatile mimo wszystko.

 15
Author: bames53,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-06 18:25:22

Oferuję tę wersję jako portable C++ (choć semantyka jest subtelnie 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 lotnego obiektu , a nie tylko dostęp do nieulotnego obiektu dokonanego przez Lotny Widok obiektu.

Różnica semantyczna polega na tym, że obecnie formalnie kończy ona żywotność dowolnego obiektu(obiektów) zajmującego obszar pamięci, ponieważ pamięć została ponownie użyta. Tak więc dostęp do obiektu po zerowaniu jego zawartości jest teraz z pewnością niezdefiniowany zachowanie (dawniej w większości przypadków byłoby to zachowanie nieokreślone, ale na pewno istniały pewne wyjątki).

Aby użyć zerowania w czasie życia obiektu zamiast na końcu, wywołujący powinien użyć placetion new, aby ponownie umieścić nową instancję oryginalnego typu.

Kod może być krótszy (choć mniej przejrzysty), używając inicjalizacji wartości:

void volatileZeroMemory(volatile void* const ptr, unsigned long long size)
{
    new (ptr) volatile unsigned char[size] ();
}

I w tym momencie jest ona jednowierszowa i w ogóle nie gwarantuje funkcji pomocniczej.

 2
Author: Ben Voigt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-06 19:42:59

Powinno być możliwe napisanie przenośnej wersji funkcji poprzez użycie obiektu lotnego po prawej stronie i zmuszenie kompilatora do zachowania pamięci 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];
}

Obiekt zero jest zadeklarowany volatile, co zapewnia, że kompilator nie może przyjąć żadnych założeń co do swojej wartości, nawet jeśli zawsze jest oceniany jako zero.

Ostateczne wyrażenie przypisania odczytuje z indeksu lotnego w tablicy i przechowuje wartość w obiekcie lotnym. Ponieważ ten odczyt nie może być zoptymalizowany, zapewnia, że kompilator musi generować sklepy określone w pętli.

 0
Author: D Krueger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-07-06 22:19:37