(ATAU quibble mengetik, perilaku dan penyelarasan yang samar, oh my God!)Teman-teman, sangat sedikit waktu yang tersisa sebelum peluncuran utas baru di kursus
"Pengembang C ++" . Sudah waktunya untuk menerbitkan terjemahan dari bagian kedua dari materi, yang menceritakan tentang apa yang diketik pun.
Apa itu pelafalan kata benda?Kami telah mencapai titik di mana kami mungkin bertanya-tanya mengapa kami mungkin perlu nama samaran sama sekali? Biasanya untuk implementasi puns typing, tk. metode yang sering digunakan melanggar aturan aliasing yang ketat.

Terkadang kita ingin menyiasati sistem tipe dan menafsirkan objek sebagai tipe lain. Menafsirkan kembali segmen memori sebagai tipe lain disebut tipe
punning pun . Permainan mengetik berguna untuk tugas-tugas yang memerlukan akses ke representasi dasar dari suatu objek untuk melihat, memindahkan, atau memanipulasi data yang disediakan. Area umum tempat kita dapat menemukan penggunaan permainan ketikan mengetik: kompiler, serialisasi, kode jaringan, dll.
Secara tradisional, ini dicapai dengan mengambil alamat objek, menuangnya ke pointer ke jenis yang ingin kita tafsirkan, dan kemudian mengakses nilai, atau dengan kata lain, menggunakan alias. Sebagai contoh:
int x = 1 ; // C float *fp = (float*)&x ; // // C++ float *fp = reinterpret_cast<float*>(&x) ; // printf( β%f\nβ, *fp ) ;
Seperti yang kita lihat sebelumnya, ini alias tidak dapat diterima, ini akan menyebabkan perilaku yang tidak terdefinisi. Namun secara tradisional, kompiler tidak menggunakan aturan aliasing yang ketat, dan kode jenis ini biasanya hanya berfungsi, dan pengembang, sayangnya, terbiasa membiarkan hal-hal seperti itu. Metode umum mengetik-mengetik adalah melalui gabungan, yang valid dalam C, tetapi akan menyebabkan perilaku tidak terdefinisi dalam C ++ (
lihat contoh ):
union u1 { int n; float f; } ; union u1 u; uf = 1.0f; printf( "%d\nβ, un ); // UB(undefined behaviour) C++ βn is not the active memberβ
Ini tidak dapat diterima di C ++, dan beberapa percaya bahwa serikat pekerja dimaksudkan hanya untuk mengimplementasikan tipe varian, dan menganggap bahwa menggunakan serikat pekerja untuk mengetik permainan kata merupakan penyalahgunaan.
Bagaimana cara menerapkan permainan kata?Metode standar yang diberkati untuk mengetik permainan kata dalam C dan C ++ adalah memcpy. Ini mungkin tampak agak rumit, tetapi pengoptimal perlu mengenali penggunaan memcpy untuk permainan kata, mengoptimalkannya dan membuat register untuk mendaftarkan gerakan. Misalnya, jika kita tahu bahwa int64_t berukuran sama dengan ganda:
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17
Kita bisa menggunakan
memcpy
:
void func1( double d ) { std::int64_t n; std::memcpy(&n, &d, sizeof d); //β¦
Dengan tingkat optimisasi yang memadai, setiap kompiler modern yang layak menghasilkan kode yang identik dengan metode reinterpret_cast yang disebutkan sebelumnya atau metode bergabung untuk mendapatkan permainan kata-kata. Mempelajari kode yang dihasilkan, kita melihat bahwa itu hanya menggunakan register mov (
contoh ).
Jenis dan Array PunTetapi bagaimana jika kita ingin mengimplementasikan permainan dari array char yang tidak ditandatangani ke dalam serangkaian unsigned int dan kemudian melakukan operasi pada masing-masing nilai int unsigned? Kita dapat menggunakan memcpy untuk mengubah array char yang tidak ditandatangani menjadi tipe int yang tidak ditandai untuk sementara. Pengoptimal masih akan dapat melihat semuanya melalui memcpy dan mengoptimalkan objek sementara dan salinan, dan bekerja secara langsung dengan data yang mendasarinya (
contoh ):
// , int foo( unsigned int x ) { return x ; } // , len sizeof(unsigned int) int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { unsigned int ui = 0; std::memcpy( &ui, &p[index], sizeof(unsigned int) ); result += foo( ui ) ; } return result; }
Dalam contoh ini, kita ambil
char*p
, asumsikan itu menunjuk ke beberapa fragmen data
sizeof(unsigned int)
, menginterpretasikan setiap fragmen data sebagai
unsigned int
, menghitung
foo()
untuk setiap fragmen permainan kata, menjumlahkan hasilnya dan mengembalikan nilai akhir .
Perakitan untuk loop body menunjukkan bahwa pengoptimal mengubah tubuh menjadi akses langsung ke array basis
unsigned char
sebagai
unsigned int
, menambahkannya langsung ke
eax
:
add eax, dword ptr [rdi + rcx]
Kode yang sama, tetapi menggunakan
reinterpret_cast
untuk menerapkan permainan kata (melanggar aliasing yang ketat):
// , len sizeof(unsigned int) int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { unsigned int ui = *reinterpret_cast<unsigned int*>(&p[index]); result += foo( ui ); } return result; }
C ++ 20 dan bit_castDi C ++ 20, kami memiliki
bit_cast
, yang menyediakan cara yang mudah dan aman untuk menafsirkan, dan juga dapat digunakan dalam konteks
constexpr
.
Berikut ini adalah contoh cara menggunakan
bit_cast
untuk menafsirkan integer yang tidak ditandai dalam
float
(
contoh ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //, sizeof(float) == sizeof(unsigned int)
Dalam kasus ketika tipe Ke dan Dari tidak memiliki ukuran yang sama, ini mengharuskan kami untuk menggunakan struktur perantara. Kami akan menggunakan struktur yang berisi berbagai karakter array
sizeof(unsigned int)
(diasumsikan int unsigned 4-byte) sebagai tipe Dari, dan
unsigned int
sebagai Kepada.
struct uint_chars { unsigned char arr[sizeof( unsigned int )] = {} ; // sizeof( unsigned int ) == 4 }; // len 4 int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { uint_chars f; std::memcpy( f.arr, &p[index], sizeof(unsigned int)); unsigned int result = bit_cast<unsigned int>(f); result += foo( result ); } return result ; }
Sayangnya, kami membutuhkan jenis perantara ini - ini adalah batasan
bit_cast
saat ini.
PerataanDalam contoh sebelumnya, kami melihat bahwa pelanggaran aturan aliasing yang ketat dapat menyebabkan pengecualian penyimpanan selama optimisasi. Pelanggaran aliasing yang ketat juga dapat menyebabkan pelanggaran persyaratan penyelarasan. Baik standar C dan C ++ menyatakan bahwa objek tunduk pada persyaratan pelurusan yang membatasi tempat di mana objek dapat ditempatkan (dalam memori) dan karenanya dapat diakses.
C11 bagian 6.2.8 Penyelarasan objek menyatakan :
Jenis objek yang lengkap memiliki persyaratan pelurusan yang memberlakukan batasan pada alamat tempat objek jenis ini dapat ditempatkan. Alignment adalah nilai integer yang ditentukan implementasi yang mewakili jumlah byte antara alamat berurutan di mana objek ini dapat ditempatkan. Jenis objek memaksakan persyaratan perataan pada setiap objek jenis ini: perataan yang lebih ketat dapat diminta menggunakan
_Alignas
.
Standar proyek C ++ 17 di bagian 1 [basic.align] :
Jenis objek memiliki persyaratan pelurusan (6.7.1, 6.7.2) yang memberikan batasan pada alamat tempat objek jenis ini dapat ditempatkan. Alignment adalah nilai integer yang ditentukan implementasi yang mewakili jumlah byte antara alamat berurutan di mana objek yang diberikan dapat ditempatkan. Jenis objek memaksakan persyaratan perataan pada setiap objek jenis ini; Penjajaran yang lebih ketat dapat diminta menggunakan penjajaran penjajaran (10.6.2).
Baik C99 dan C11 secara eksplisit menunjukkan bahwa konversi yang menghasilkan pointer yang tidak selaras adalah perilaku yang tidak ditentukan, bagian 6.3.2.3.
Pointers mengatakan:
Pointer ke objek atau tipe parsial dapat dikonversi ke pointer ke objek atau tipe parsial lain. Jika pointer yang dihasilkan tidak selaras dengan benar untuk tipe pointer, perilaku tidak terdefinisi. ...
Meskipun C ++ tidak begitu jelas, saya percaya bahwa kalimat ini dari ayat 1
[basic.align]
cukup:
... Jenis objek memaksakan persyaratan perataan pada setiap objek jenis ini; ...
ContohJadi mari kita asumsikan:
- alignof (char) dan alignof (int) masing-masing adalah 1 dan 4
- sizeof (int) adalah 4
Dengan demikian, menafsirkan array ar dengan ukuran 4 sebagai
int
melanggar aliasing yang ketat, dan juga dapat melanggar persyaratan penyelarasan jika array memiliki keselarasan 1 atau 2 byte.
char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; // 1 2 int x = *reinterpret_cast<int*>(arr); // Undefined behavior
Yang dapat mengakibatkan penurunan kinerja atau kesalahan bus dalam beberapa situasi. Sedangkan menggunakan alignas untuk memaksa alignment yang sama untuk array di int akan mencegah persyaratan alignment dari melanggar:
alignas(alignof(int)) char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; int x = *reinterpret_cast<int*>(arr);
AtomicityHukuman lain yang tidak terduga untuk akses yang tidak seimbang adalah bahwa itu melanggar atomicity beberapa arsitektur. Penyimpanan atom mungkin tidak muncul atom untuk utas lain di x86 jika tidak sejajar.
Menangkap pelanggaran alias ketatKami tidak memiliki banyak alat bagus untuk melacak alias ketat di C ++. Alat yang kami miliki akan menangkap beberapa kasus pelanggaran dan beberapa kasus pemuatan dan penyimpanan yang tidak tepat.
gcc menggunakan
-fstrict-aliasing
dan
-Wstrict-aliasing
dapat menangkap beberapa case, walaupun bukan tanpa false positive / masalah. Misalnya, kasus-kasus berikut akan menghasilkan peringatan dalam gcc (
contoh ):
int a = 1; short j; float f = 1.f; // , TIS , printf("%i\n", j = *(reinterpret_cast<short*>(&a))); printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
meskipun dia tidak akan menangkap kasus tambahan ini (
contoh ):
int *p; p=&a; printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Meskipun
clang
menyelesaikan bendera ini, tampaknya tidak benar-benar menerapkan peringatan tersebut.
Alat lain yang kami miliki adalah ASan, yang dapat menangkap perekaman dan penyimpanan yang tidak selaras. Meskipun mereka bukan pelanggaran langsung alias ketat, ini adalah hasil yang cukup umum. Sebagai contoh, kasus-kasus berikut akan menghasilkan kesalahan runtime selama perakitan menggunakan dentang menggunakan
-fsanitize=address
int *x = new int[2]; // 8 : [0,7]. int *u = (int*)((char*)x + 6); // x *u = 1; // [6-9] printf( "%d\n", *u ); // [6-9]
Alat terakhir yang saya rekomendasikan adalah khusus untuk C ++ dan, pada kenyataannya, tidak hanya alat, tetapi juga praktik pengkodean yang tidak memungkinkan casting gaya C. Baik
gcc
dan
clang
akan melakukan diagnostik untuk
-Wold-style-cast
C menggunakan
-Wold-style-cast
. Ini akan memaksa setiap permainan ketikan yang tidak ditentukan untuk menggunakan reinterpret_cast. Secara umum,
reinterpret_cast
harus menjadi suar untuk analisis kode yang lebih menyeluruh.
Juga lebih mudah untuk mencari basis kode untuk
reinterpret_cast
untuk melakukan audit.
Untuk C, kami memiliki semua alat yang sudah dijelaskan, dan kami juga memiliki
tis-interpreter
, penganalisa statis yang secara mendalam menganalisis program untuk subset besar C. Diberikan versi C dari contoh sebelumnya, di mana menggunakan -fstrict-aliasing melewatkan satu kasus (
contoh )
int a = 1; short j; float f = 1.0 ; printf("%i\n", j = *((short*)&a)); printf("%i\n", j = *((int*)&f)); int *p; p=&a; printf("%i\n", j = *((short*)p));
TIS interpreter dapat mencegat ketiganya, contoh berikut memanggil kernel TIS sebagai juru TIS (output diedit untuk singkatnya):
./bin/tis-kernel -sa example1.c ... example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing rules by accessing a cell with effective type int. ... example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by accessing a cell with effective type float. Callstack: main ... example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by accessing a cell with effective type int.
Dan akhirnya,
TySan , yang sedang dalam pengembangan. Pembersih ini menambahkan tipe memeriksa informasi ke segmen memori bayangan dan memeriksa akses untuk menentukan apakah mereka melanggar aturan alias. Alat ini berpotensi dapat melacak semua pelanggaran alias, tetapi mungkin memiliki overhead yang besar saat runtime.
KesimpulanKami belajar tentang aturan aliasing dalam C dan C ++, yang berarti bahwa kompiler mengharapkan kita untuk secara ketat mengikuti aturan ini dan menerima konsekuensi dari tidak memenuhinya. Kami telah mempelajari tentang beberapa alat yang dapat membantu kami mengidentifikasi beberapa penyalahgunaan nama samaran. Kita telah melihat bahwa penggunaan aliasing yang biasa adalah permainan kata-kata. Kami juga belajar cara mengimplementasikannya dengan benar.
Pengoptimal secara bertahap meningkatkan analisis alias berbasis tipe dan telah melanggar beberapa kode yang didasarkan pada pelanggaran alias ketat. Kita dapat mengharapkan optimasi untuk menjadi lebih baik dan memecahkan lebih banyak kode yang baru saja bekerja sebelumnya.
Kami memiliki metode standar yang siap pakai untuk menafsirkan tipe. Kadang-kadang untuk debug membangun metode ini harus abstraksi gratis. Kami memiliki beberapa alat untuk mendeteksi pelanggaran aliasing yang parah, tetapi untuk C ++ mereka hanya akan menangkap sebagian kecil dari kasus, dan untuk C yang menggunakan tis-interpreter kami dapat melacak sebagian besar pelanggaran.
Terima kasih kepada mereka yang mengomentari artikel ini: JF Bastien, Christopher Di Bella, Pascal Quoc, Matt P. Dziubinski, Patrice Roy dan Olafur Vaage
Tentu saja, pada akhirnya, semua kesalahan adalah milik penulis.
Jadi terjemahan dari bahan yang agak besar telah berakhir, bagian pertama dapat dibaca di
sini . Dan kami secara tradisional mengundang Anda ke
hari buka pintu , yang akan diadakan pada 14 Maret oleh kepala departemen pengembangan teknologi di Rambler & Co -
Dmitry Shebordaev.