Saya memerlukan fungsi yang (seperti SecureZeroMemory dari WinAPI) selalu memiliki memori nol dan tidak dapat dioptimalkan, bahkan jika kompiler berpikir bahwa memori tidak akan pernah diakses lagi setelah itu. Sepertinya kandidat yang sempurna untuk volatile. Namun saya mengalami beberapa masalah saat membuatnya berfungsi dengan GCC. Berikut ini contoh fungsinya:
void volatileZeroMemory(volatile void* ptr, unsigned long long size)
{
volatile unsigned char* bytePtr = (volatile unsigned char*)ptr;
while (size--)
{
*bytePtr++ = 0;
}
}
Cukup sederhana. Tetapi kode yang sebenarnya dihasilkan GCC jika Anda memanggilnya sangat bervariasi dengan versi kompilator dan jumlah byte yang sebenarnya Anda coba nol. https://godbolt.org/g/cMaQm2
- GCC 4.4.7 dan 4.5.3 tidak pernah mengabaikan volatile.
- GCC 4.6.4 dan 4.7.3 mengabaikan volatile untuk ukuran array 1, 2, dan 4.
- GCC 4.8.1 hingga 4.9.2 mengabaikan volatile untuk ukuran array 1 dan 2.
- GCC 5.1 hingga 5.3 mengabaikan volatile untuk ukuran array 1, 2, 4, 8.
- GCC 6.1 mengabaikannya untuk ukuran larik apa pun (poin bonus untuk konsistensi).
Kompiler lain yang telah saya uji (clang, icc, vc) menghasilkan penyimpanan yang diharapkan, dengan versi kompiler dan ukuran array apa pun. Jadi pada titik ini saya bertanya-tanya, apakah ini bug compiler GCC (cukup lama dan parah?), Atau apakah definisi volatile dalam standar tidak tepat bahwa ini sebenarnya merupakan perilaku yang sesuai, sehingga pada dasarnya tidak mungkin untuk menulis portabel "" Fungsi SecureZeroMemory?
Edit: Beberapa pengamatan menarik.
#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;
}
Kemungkinan penulisan dari callMeMaybe () akan membuat semua versi GCC kecuali 6.1 menghasilkan penyimpanan yang diharapkan. Mengomentari di pagar memori juga akan membuat GCC 6.1 menghasilkan penyimpanan, meskipun hanya dikombinasikan dengan kemungkinan penulisan dari callMeMaybe ().
Seseorang juga menyarankan untuk membersihkan cache. Microsoft tidak mencoba membersihkan cache sama sekali di "SecureZeroMemory". Cache kemungkinan besar akan dinonaktifkan dengan cukup cepat, jadi ini mungkin bukan masalah besar. Juga, jika program lain mencoba untuk menyelidiki data, atau jika itu akan ditulis ke file halaman, itu akan selalu menjadi versi nol.
Ada juga beberapa kekhawatiran tentang GCC 6.1 yang menggunakan memset () dalam fungsi standalone. Kompiler GCC 6.1 pada godbolt mungkin merupakan build yang rusak, karena GCC 6.1 tampaknya menghasilkan loop normal (seperti yang dilakukan 5.3 pada godbolt) untuk fungsi mandiri bagi sebagian orang. (Baca komentar jawaban zwol.)
volatile
adalah bug kecuali terbukti sebaliknya. Tapi kemungkinan besar bug.volatile
sangat tidak ditentukan sehingga berbahaya - jangan gunakan saja.volatile
tepat dalam kasus ini.memset
. Masalahnya adalah kompiler tahu persis apa yangmemset
dilakukannya.volatile
pointer, kami ingin sebuah pointer kevolatile
(kami tidak peduli apakah++
ketat, tetapi apakah*p = 0
ketat).Jawaban:
Perilaku GCC mungkin sesuai, dan meskipun tidak, Anda tidak boleh mengandalkan
volatile
untuk melakukan apa yang Anda inginkan dalam kasus seperti ini. Komite C dirancangvolatile
untuk register perangkat keras yang dipetakan dengan memori dan untuk variabel yang dimodifikasi selama aliran kontrol abnormal (misalnya penangan sinyal dansetjmp
). Itu adalah satu-satunya hal yang dapat diandalkan. Tidak aman digunakan sebagai anotasi umum "jangan optimalkan ini".Secara khusus, standarnya tidak jelas tentang poin kunci. (Saya telah mengubah kode Anda menjadi C; seharusnya tidak ada perbedaan antara C dan C ++ di sini. Saya juga telah melakukan inlining secara manual yang akan terjadi sebelum pengoptimalan yang dipertanyakan, untuk menunjukkan apa yang "dilihat" oleh compiler pada saat itu .)
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; }
Loop pembersihan memori mengakses
arr
melalui nilai l yang memenuhi syarat volatile, tetapiarr
itu sendiri tidak dideklarasikanvolatile
. Oleh karena itu, setidaknya boleh dibilang kompiler C dapat menyimpulkan bahwa penyimpanan yang dibuat oleh loop "mati", dan menghapus loop sama sekali. Ada teks dalam Rationale C yang menyiratkan bahwa panitia bermaksud mewajibkan toko-toko itu untuk dilestarikan, tetapi standar itu sendiri tidak benar-benar membuat persyaratan itu, saat saya membacanya.Untuk diskusi lebih lanjut tentang apa yang standar diperlukan atau tidak, lihat Mengapa variabel lokal yang mudah menguap dioptimalkan secara berbeda dari argumen yang mudah menguap, dan mengapa pengoptimal menghasilkan loop tanpa operasi dari yang terakhir? , Apakah mengakses objek non-volatile yang dideklarasikan melalui referensi / penunjuk volatile memberikan aturan volatil atas akses tersebut? , dan bug GCC 71793 .
Untuk mengetahui lebih lanjut tentang apa yang dipikirkan komite
volatile
, cari C99 Rationale untuk kata "volatile". Makalah John Regehr " Volatiles are Miscompiled " menggambarkan secara rinci bagaimana ekspektasi programmervolatile
mungkin tidak dipenuhi oleh kompiler produksi. Rangkaian esai tim LLVM " Yang Harus Diketahui Setiap Pemrogram C Tentang Perilaku Tak Terdefinisi " tidak menyentuh secara khususvolatile
tetapi akan membantu Anda memahami bagaimana dan mengapa kompiler C modern bukan "perakit portabel".Untuk pertanyaan praktis tentang bagaimana mengimplementasikan suatu fungsi yang melakukan apa yang ingin Anda
volatileZeroMemory
lakukan: Terlepas dari apa yang diminta atau dimaksudkan oleh standar, akan lebih bijaksana untuk berasumsi bahwa Anda tidak dapat menggunakanvolatile
untuk ini. Ada adalah alternatif yang dapat diandalkan untuk bekerja, karena akan merusak terlalu banyak hal-hal lain jika tidak bekerja: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) {}
Namun, Anda harus benar-benar memastikan bahwa
memory_optimization_fence
itu tidak dalam keadaan apa pun. Itu harus dalam file sumbernya sendiri dan tidak boleh dikenai pengoptimalan waktu tautan.Ada opsi lain, mengandalkan ekstensi kompilator, yang mungkin dapat digunakan dalam beberapa keadaan dan dapat menghasilkan kode yang lebih ketat (salah satunya muncul di edisi sebelumnya dari jawaban ini), tetapi tidak ada yang universal.
(Saya sarankan memanggil fungsi tersebut
explicit_bzero
, karena ini tersedia dengan nama itu di lebih dari satu perpustakaan C. Setidaknya ada empat pesaing lain untuk nama tersebut, tetapi masing-masing telah diadopsi hanya oleh satu perpustakaan C.)Anda juga harus tahu bahwa, meskipun Anda bisa mendapatkan ini untuk bekerja, itu mungkin tidak cukup. Secara khusus, pertimbangkan
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); }
Dengan asumsi perangkat keras dengan instruksi akselerasi AES, jika
expand_key
danencrypt_with_ek
sebaris, kompilator mungkin dapat menyimpanek
seluruhnya dalam file register vektor - hingga panggilan keexplicit_bzero
, yang memaksanya untuk menyalin data sensitif ke tumpukan hanya untuk menghapusnya, dan, Lebih buruk lagi, tidak melakukan apa-apa tentang kunci yang masih ada di register vektor!sumber
volatile
sebagai [...] Oleh karena itu setiap ekspresi yang mengacu pada objek seperti itu harus dievaluasi secara ketat sesuai dengan aturan mesin abstrak, seperti yang dijelaskan dalam 5.1.2.3. Selanjutnya, pada setiap titik urutan nilai yang terakhir disimpan dalam objek harus sesuai dengan yang ditentukan oleh mesin abstrak , kecuali sebagaimana yang dimodifikasi oleh faktor-faktor yang tidak diketahui yang disebutkan sebelumnya. Apa yang dimaksud dengan akses ke objek yang memiliki tipe berkualifikasi volatile ditentukan oleh implementasi. ?volatile sig_atomic_t flag;
adalah objek yang mudah menguap .*(volatile char *)foo
hanyalah akses melalui nilai l yang memenuhi syarat volatile dan standarnya tidak mengharuskan untuk memiliki efek khusus.volatile
mungkin cukup untuk membuatnya menjadi implementasi yang "patuh", tetapi itu tidak berarti itu cukup untuk menjadi "baik" atau "berguna". Untuk banyak jenis pemrograman sistem, itu harus dianggap sangat kurang dalam hal itu.Untuk itulah fungsi standar
memset_s
itu.Mengenai apakah perilaku dengan volatile ini sesuai atau tidak, itu agak sulit untuk dikatakan, dan volatile telah lama dikatakan telah diganggu oleh bug.
Satu masalah adalah spesifikasi mengatakan bahwa "Akses ke objek volatil dievaluasi secara ketat sesuai dengan aturan mesin abstrak." Tapi itu hanya mengacu pada 'objek volatile', tidak mengakses objek non-volatile melalui pointer yang telah ditambahkan volatile. Jadi tampaknya jika kompiler dapat mengetahui bahwa Anda tidak benar-benar mengakses objek volatile, maka tidak perlu memperlakukan objek tersebut sebagai volatile.
sumber
memset_s
sebagai standar C11 adalah pernyataan yang berlebihan. Ini adalah bagian dari Annex K, yang opsional di C11 (dan karena itu juga opsional di C ++). Pada dasarnya semua pelaksana, termasuk Microsoft, yang idenya pertama kali (!), Telah menolak untuk mengambilnya; terakhir saya mendengar mereka berbicara tentang membatalkannya di C-next.size_t
diizinkan. Win64 ABI tidak sesuai dengan C90. Itu akan ... tidak baik , tetapi tidak buruk ... jika MSVC sebenarnya mengambil C99 hal-hal sepertiuintmax_t
dan%zu
pada waktu yang tepat, tetapi ternyata tidak .)Saya menawarkan versi ini sebagai C ++ portabel (meskipun semantiknya sedikit berbeda):
void volatileZeroMemory(volatile void* const ptr, unsigned long long size) { volatile unsigned char* bytePtr = new (ptr) volatile unsigned char[size]; while (size--) { *bytePtr++ = 0; } }
Sekarang Anda memiliki akses tulis ke objek volatile , tidak hanya akses ke objek non-volatile yang dibuat melalui tampilan objek volatile.
Perbedaan semantiknya adalah sekarang secara resmi mengakhiri masa pakai objek apa pun yang menempati wilayah memori, karena memori telah digunakan kembali. Jadi akses ke objek setelah memusatkan perhatian pada isinya sekarang jelas merupakan perilaku yang tidak terdefinisi (sebelumnya akan menjadi perilaku tidak terdefinisi dalam banyak kasus, tetapi beberapa pengecualian pasti ada).
Untuk menggunakan zeroing ini selama masa pakai objek, bukan di akhir, pemanggil harus menggunakan penempatan
new
untuk mengembalikan instance baru dari tipe asli.Kode dapat dibuat lebih pendek (meskipun kurang jelas) dengan menggunakan inisialisasi nilai:
void volatileZeroMemory(volatile void* const ptr, unsigned long long size) { new (ptr) volatile unsigned char[size] (); }
dan pada titik ini ini adalah satu-baris dan hampir tidak memerlukan fungsi pembantu sama sekali.
sumber
Seharusnya dimungkinkan untuk menulis versi portabel dari fungsi dengan menggunakan objek volatile di sisi kanan dan memaksa compiler untuk menyimpan penyimpanan ke array.
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]; }
The
zero
objek dinyatakanvolatile
yang menjamin compiler dapat membuat asumsi tentang nilai meskipun selalu mengevaluasi sebagai nol.Ekspresi tugas akhir membaca dari indeks volatile dalam array dan menyimpan nilai dalam objek volatile. Karena pembacaan ini tidak dapat dioptimalkan, ini memastikan bahwa kompilator harus menghasilkan penyimpanan yang ditentukan dalam loop.
sumber
*ptr
selama perulangan itu, atau sebenarnya apa pun ... hanya perulangan. wtf, begitulah otak saya.edx
: Saya mendapatkan ini:.L16: subq $1, %rax; movzbl -1(%rsp), %edx; jne .L16
volatile unsigned char const
byte pengisian sewenang-wenang ... itu bahkan tidak membacanya . Panggilan inline yang dihasilkanvolatileFill()
hanya[load RAX with sizeof] .L9: subq $1, %rax; jne .L9
. Mengapa pengoptimal (A) tidak membaca ulang byte isian dan (B) repot-repot mempertahankan loop di mana ia tidak melakukan apa-apa?