"Uçucu" tanımı bu kadar uçucu mu yoksa GCC bazı standart uyum sorunları mı yaşıyor?


89

Derleyici belleğe bundan sonra bir daha erişilemeyeceğini düşünse bile (WinAPI'den SecureZeroMemory gibi) belleği her zaman sıfırlayan ve optimize edilmeyen bir işleve ihtiyacım var. Uçucu olmak için mükemmel bir aday gibi görünüyor. Ancak bunu GCC ile çalıştırmakta bazı sorunlar yaşıyorum. İşte örnek bir işlev:

void volatileZeroMemory(volatile void* ptr, unsigned long long size)
{
    volatile unsigned char* bytePtr = (volatile unsigned char*)ptr;

    while (size--)
    {
        *bytePtr++ = 0;
    }
}

Yeterince basit. Ancak GCC'nin aslında onu çağırırsanız ürettiği kod, derleyici sürümüne ve aslında sıfırlamaya çalıştığınız bayt miktarına göre çılgınca değişir. https://godbolt.org/g/cMaQm2

  • GCC 4.4.7 ve 4.5.3 uçucuları asla göz ardı etmez.
  • GCC 4.6.4 ve 4.7.3, dizi boyutları 1, 2 ve 4 için uçucu yoksayar.
  • 4.9.2'ye kadar GCC 4.8.1, 1 ve 2 dizileri için uçucu göz ardı eder.
  • GCC 5.1, 5.3'e kadar 1, 2, 4, 8 dizi boyutları için uçucu yoksay.
  • GCC 6.1, herhangi bir dizi boyutu için onu yok sayar (tutarlılık için bonus puan).

Test ettiğim herhangi bir başka derleyici (clang, icc, vc), herhangi bir derleyici sürümü ve herhangi bir dizi boyutu ile birinin beklediği depoları oluşturur. Bu noktada merak ediyorum, bu (oldukça eski ve ciddi bir GCC derleyici hatası mı?) Yoksa standarttaki geçici tanımı, bunun gerçekte uyumlu bir davranış olduğu konusunda kesin değil, bu da taşınabilir bir yazmayı esasen imkansız hale getiriyor " SecureZeroMemory "işlevi?

Düzenleme: Bazı ilginç gözlemler.

#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 () 'den olası yazma, 6.1 dışındaki tüm GCC sürümlerinin beklenen depoları oluşturmasını sağlayacaktır. Bellek çitine yorum yapmak, yalnızca callMeMaybe () 'den olası yazma ile kombinasyon halinde olsa da, GCC 6.1'in depoları oluşturmasını da sağlayacaktır.

Birisi ayrıca önbellekleri temizlemeyi önerdi. Microsoft, yok değil "SecureZeroMemory" in hiç önbelleğini temizlemek için deneyin. Önbellek muhtemelen oldukça hızlı bir şekilde geçersiz kılınacak, bu yüzden bu büyük bir sorun değil. Ayrıca, başka bir program verileri araştırmaya çalışıyorsa veya sayfa dosyasına yazılacaksa, bu her zaman sıfırlanmış sürüm olacaktır.

Bağımsız işlevde memset () kullanan GCC 6.1 ile ilgili bazı endişeler de vardır. GCC 6.1 bazı insanlar için bağımsız işlev için normal bir döngü oluşturuyor gibi göründüğü için (5.3 Godbolt'ta olduğu gibi), godbolt üzerindeki GCC 6.1 derleyicisi bozuk bir yapı olabilir. (Zwol'un cevabının yorumlarını okuyun.)


4
volatileAksi ispatlanmadıkça IMHO kullanımı bir hatadır. Ama büyük olasılıkla bir hata. volatiletehlikeli olarak çok az tanımlanmıştır - sadece kullanmayın.
Jesper Juhl

20
@JesperJuhl: Hayır, volatilebu durumda uygun.
Dietrich Epp

9
@NathanOliver: Bu işe yaramaz çünkü derleyiciler ölü depoları kullanıyor olsalar bile optimize edebilirler memset. Sorun, derleyicilerin tam olarak ne yaptığını bilmeleridir memset.
Dietrich Epp

9
@PaulStelian: Bu bir volatileişaretçi yapar, biz bir işaretçi volatileisteriz ( ++katı olup olmadığı umurumuzda değil , ama *p = 0katı mı ).
Dietrich Epp

7
@JesperJuhl: Uçucu hakkında yeterince belirtilmemiş hiçbir şey yok.
GManNickG

Yanıtlar:


82

GCC'nin davranışı uygun olabilir ve öyle olmasa bile, bu volatilegibi durumlarda istediğinizi yapmaya güvenmemelisiniz . volatileHafıza eşlemeli donanım kayıtları ve anormal kontrol akışı sırasında değiştirilen değişkenler için tasarlanmış C komitesi (örn. Sinyal işleyicileri ve setjmp). Güvenilir olduğu tek şey bunlar. Genel bir "bunu optimize etmeyin" ek açıklaması olarak kullanmak güvenli değildir.

Özellikle, standart bir kilit noktada belirsizdir. (Kodunuzu C'ye çevirdim; burada C ile C ++ arasında herhangi bir sapma olmamalıdır . Ayrıca, derleyicinin o noktada ne "gördüğünü" göstermek için şüpheli optimizasyondan önce gerçekleşecek olan satır içi işlemi manuel olarak yaptım. .)

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;
}

Bellek temizleme döngüsü kere arruçucu nitelikli lvalue arasındadır, ancak arrkendisi bir değil beyanvolatile . Bu nedenle, C derleyicisinin, döngü tarafından yapılan depoların "ölü" olduğu sonucuna varmasına ve döngüyü tamamen silmesine en azından tartışmalı bir şekilde izin verilir. C Gerekçesinde, komitenin bu mağazaların korunmasını istediğini ima eden bir metin var , ancak standardın kendisi aslında okuduğum gibi bu gereksinimi yerine getirmiyor.

Standardın neyi gerektirip gerektirmediğiyle ilgili daha fazla tartışma için bkz. Uçucu bir yerel değişken neden geçici bir argümandan farklı şekilde optimize edilir ve optimizasyon aracı neden ikincisinden işlemsiz bir döngü oluşturur? , Bir erişim mu uçucu bir referans yoluyla bahsedilen erişimler üzerine / işaretçi kazandırır uçucu kuralları uçucu olmayan nesne ilan? , ve GCC bug 71793 .

Komitenin ne düşündüğü hakkında daha fazla bilgi volatileiçin, C99 Gerekçesinde "uçucu" kelimesini arayın . John Regehr'in " Uçucu Malzemeler Yanlış Derlenmiştir " başlıklı makalesi , programcıların beklentilerinin volatileüretim derleyicileri tarafından nasıl karşılanmayabileceğini ayrıntılı olarak göstermektedir . LLVM ekibinin " Her C Programcısının Tanımlanmamış Davranış Hakkında Bilmesi Gerekenler " adlı makale serisi, özellikle buna değinmez, volatileancak modern C derleyicilerinin nasıl ve neden "taşınabilir derleyiciler " olmadığını anlamanıza yardımcı olur .


Yapmak istediğiniz şeyi yapan bir işlevi nasıl uygulayacağınıza dair pratik soruya volatileZeroMemory: Standardın ne gerektirdiğine veya neyi gerektirdiğine bakılmaksızın, bunun volatileiçin kullanamayacağınızı varsaymak en akıllıca olacaktır . Orada ise o işi değildir verseydiniz çok fazla başka şeyler kıracak çünkü işe dayanıyordu edilebilir bir alternatif:

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) {}

Ancak, kesinlikle emin olmalısınız memory_optimization_fence hiçbir koşulda satır içi olmadığından . Kendi kaynak dosyasında olmalı ve bağlantı zamanı optimizasyonuna tabi tutulmamalıdır.

Bazı durumlarda kullanılabilen ve daha sıkı kod oluşturabilen derleyici uzantılarına dayanan başka seçenekler de vardır (bunlardan biri bu cevabın önceki sürümünde yer almıştır), ancak hiçbiri evrensel değildir.

(İşlevi aramanızı tavsiye ederim explicit_bzero , çünkü bu isim altında birden fazla C kütüphanesinde mevcuttur. İsim için en az dört başka yarışmacı vardır, ancak her biri yalnızca tek bir C kütüphanesi tarafından benimsenmiştir.)

Ayrıca şunu da bilmelisiniz ki, bunu çalıştırabilseniz bile yeterli olmayabilir. Özellikle düşünün

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 hızlandırma yönergelerine sahip donanım varsayılarak, eğer expand_keyve encrypt_with_eksatır içi ise, derleyici ektamamen vektör kayıt dosyasında tutabilir - çağrıya kadar explicit_bzero, hassas verileri sadece silmek için yığına kopyalamaya zorlar ve daha kötüsü, hala vektör yazmaçlarında duran anahtarlar hakkında lanet bir şey yapmaz!


6
Bu ilginç ... Komitenin yorumlarına bir referans görmek isterim.
Dietrich Epp

10
Nasıl arasında 6.7.3 (7) ile, bu köşeli 'in tanımı volatileolarak 5.1.2.3 tarif edildiği gibi [...] Bu nedenle bu tür bir nesneye atıfta herhangi bir ifade, özet makinenin kurallarına göre kesinlikle değerlendirilir. Ayrıca, her sıra noktasında, nesnede en son depolanan değer , daha önce bahsedilen bilinmeyen faktörler tarafından değiştirilenler dışında , soyut makine tarafından öngörülen değerle uyumlu olacaktır . Uçucu nitelikli türe sahip bir nesneye erişimi oluşturan şey, uygulama tanımlıdır. ?
Idonotexist

15
@IwillnotexistIdonotexist Bu pasajdaki anahtar kelime nesnedir . volatile sig_atomic_t flag;uçucu bir nesnedir . *(volatile char *)fooyalnızca geçici nitelikli bir değer üzerinden erişimdir ve standart, herhangi bir özel etkiye sahip olmasını gerektirmez.
zwol

3
Standart, bir şeyin "uyumlu" bir uygulama olması için hangi kriterleri karşılaması gerektiğini belirtir. Belirli bir platformdaki bir uygulamanın "iyi" bir uygulama veya "kullanılabilir" olması için hangi kriterleri karşılaması gerektiğini açıklamak için hiçbir çaba sarf etmez. GCC'nin yaklaşımı, volatileonu "uyumlu" bir uygulama haline getirmek için yeterli olabilir, ancak bu, "iyi" veya "yararlı" olması için yeterli olduğu anlamına gelmez. Pek çok sistem programlama türü için, bu bakımlardan ne yazık ki eksik olduğu kabul edilmelidir.
supercat

3
Cı-spec de yerine doğrudan bilgi vermez bir gerçek uygulama ihtiyacı değil değeri kullanılmaz olduğunu anlamak takdirde bir ifadenin bir kısmı değerlendirilmesi ve herhangi bir ihtiyaç duyulan yan etkiler (üretildiği" bir işlev arama veya uçucu bir nesneye ulaşmak neden olduğu dahil olmak üzere, ) . " (benimkini vurgula).
Johannes Schaub - litb

15

(WinAPI'den SecureZeroMemory gibi) hafızayı her zaman sıfırlayan ve optimize edilmeyen bir işleve ihtiyacım var,

Standart işlev memset_sbunun içindir.


Uçucu olan bu davranışın uygun olup olmadığına gelince, bunu söylemek biraz zor ve uçucunun uzun süredir böceklerle uğraştığı söyleniyor .

Bir sorun, teknik özelliklerin "Uçucu nesnelere erişim kesinlikle soyut makinenin kurallarına göre değerlendirildiğini" söylemesidir. Ancak bu, uçucu olmayan bir nesneye uçucu eklenmiş bir işaretçi aracılığıyla erişilmemesi nedeniyle yalnızca 'uçucu nesneler' anlamına gelir. Öyleyse görünüşe göre bir derleyici uçucu bir nesneye gerçekten erişemediğinizi söyleyebilirse, nesneyi sonuçta uçucu olarak ele almak gerekmez.


4
Not: Bu, C11 standardının bir parçasıdır ve henüz tüm alet zincirlerinde mevcut değildir.
Dietrich Epp

5
İlginç bir şekilde, bu işlevin C11 için standartlaştırıldığı, ancak C ++ 11, C ++ 14 veya C ++ 17 için standart olmadığı unutulmamalıdır. Teknik olarak C ++ için bir çözüm değil, ancak bunun pratik açıdan en iyi seçenek gibi göründüğüne katılıyorum. Bu noktada, GCC'nin davranışının uygun olup olmadığını merak ediyorum. Düzenleme: Aslında, VS 2015'te memset_s yok, bu yüzden henüz o kadar taşınabilir değil.
cooky451

2
@ cooky451 C ++ 17'nin C11 standart kitaplığını referans olarak çektiğini düşündüm (bkz. ikinci Misc).
nwp

14
Ayrıca, memset_sC11 standardı olarak tanımlamak bir abartıdır. C11'de isteğe bağlı (ve dolayısıyla C ++ 'da isteğe bağlı) Ek K'nin bir parçasıdır. Temelde , fikri ilk etapta olan Microsoft dahil (!) Tüm uygulayıcılar onu almayı reddetti; en son C-next'de hurdaya çıkarmaktan bahsettiklerini duydum.
zwol

8
@ cooky451 Bazı çevrelerde Microsoft, temelde herkesin itirazları üzerine bir şeyleri C standardına zorlamakla ve sonra da bunu kendi başına uygulamakla uğraşmamakla ünlüdür. (Bu en yaman örneği altında yatan tipi ne için kurallar C99 rahatlama olduğunu size_tolmayı izin verilir. Win64 ABI C90 ile değil conformant olduğunu. Bu ... olurdu değil Tamam , ama korkunç değil ... eğer MSVC aslında gibi C99 şeyler almış uintmax_tve %zuzamanında ama olmadı ).
Zwol

2

Bu sürümü taşınabilir C ++ olarak sunuyorum (anlambilim biraz farklı olsa da):

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

    while (size--)
    {
        *bytePtr++ = 0;
    }
}

Artık uçucu bir nesneye yazma erişiminiz var , yalnızca nesnenin uçucu bir görünümü aracılığıyla yapılan uçucu olmayan bir nesneye erişim değil.

Anlamsal fark, bellek bölgesini işgal eden nesnenin / nesnelerin artık yaşam süresini resmi olarak sona erdirmesidir, çünkü bellek yeniden kullanılmıştır. Dolayısıyla, içeriğini sıfırladıktan sonra nesneye erişim artık kesinlikle tanımlanmamış bir davranıştır (eskiden çoğu durumda tanımsız bir davranış olurdu, ancak bazı istisnalar kesinlikle mevcuttu).

Bu sıfırlamayı bir nesnenin sonunda değil ömrü boyunca kullanmak için, arayan kişi yerleştirmeyi newkullanarak orijinal türün yeni bir örneğini yeniden yerleştirmelidir.

Değer başlatma kullanılarak kod kısaltılabilir (daha az net olsa da):

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

ve bu noktada, tek satırlıktır ve neredeyse hiç yardımcı bir işlevi garanti etmez.


2
İşlev çalıştırıldıktan sonra nesneye erişim UB'yi çağırırsa, bu, bu tür erişimlerin nesnenin "temizlenmeden" önce tuttuğu değerleri verebileceği anlamına gelir. Nasıl bu güvenliğin tersi olmaz?
supercat

0

Sağ tarafta uçucu bir nesne kullanarak ve derleyiciyi depoları diziye korumaya zorlayarak işlevin taşınabilir bir sürümünü yazmak mümkün olmalıdır.

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];
}

zeroNesne bildirildi volatileolmasını sağlar derleyici her zaman sıfır olarak değerlendirir rağmen değeri hakkında hiçbir varsayımlar yapabilirsiniz.

Son atama ifadesi, dizideki geçici bir dizinden okur ve değeri uçucu bir nesnede depolar. Bu okuma optimize edilemediğinden, derleyicinin döngüde belirtilen depoları oluşturmasını sağlar.


1
Bu hiç çalışmıyor ... sadece üretilen koda bakın.
cooky451

1
Oluşturduğum ASM modumu daha iyi okuduktan sonra, işlev çağrısını satır içi yapıyor ve *ptrdöngüyü koruyor gibi görünüyor, ancak bu döngü sırasında herhangi bir depolama yapmıyor veya aslında hiçbir şey yapmıyor ... sadece döngü. wtf, beynim gidiyor.
underscore_d

3
@underscore_d Bunun nedeni, geçici olanın okunmasını korurken mağazayı optimize etmesidir.
D Krueger

1
Evet ve sonucu değişmeyen bir duruma düşürüyor edx: Bunu anlıyorum:.L16: subq $1, %rax; movzbl -1(%rsp), %edx; jne .L16
altçizgi_d

1
İşlevi rastgele bir volatile unsigned char constdoldurma baytı geçirmeye izin verecek şekilde değiştirirsem ... onu okumaz bile . Oluşturulan satır içi çağrı volatileFill()sadece [load RAX with sizeof] .L9: subq $1, %rax; jne .L9. İyileştirici (A) neden doldurma baytını yeniden okumuyor ve (B) döngüyü hiçbir şey yapmadığı yerde korumaya çalışıyor?
underscore_d
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.