Di C, kompiler akan mengeluarkan anggota dari sebuah struct dengan urutan di mana mereka dideklarasikan, dengan kemungkinan byte padding dimasukkan di antara anggota, atau setelah anggota terakhir, untuk memastikan bahwa setiap anggota disejajarkan dengan benar.
gcc menyediakan ekstensi bahasa __attribute__((packed))
,, yang memberi tahu kompiler untuk tidak memasukkan padding, yang memungkinkan anggota struct tidak selaras. Sebagai contoh, jika sistem biasanya membutuhkan semua int
objek untuk memiliki 4-byte alignment, __attribute__((packed))
dapat menyebabkan int
anggota struct dialokasikan pada offset ganjil.
Mengutip dokumentasi gcc:
Atribut `packed 'menentukan bahwa bidang variabel atau struktur harus memiliki penyelarasan sekecil mungkin - satu byte untuk variabel, dan satu bit untuk bidang, kecuali jika Anda menetapkan nilai yang lebih besar dengan atribut` sejajar'.
Jelas penggunaan ekstensi ini dapat menghasilkan persyaratan data yang lebih kecil tetapi kode lebih lambat, karena kompiler harus (pada beberapa platform) menghasilkan kode untuk mengakses anggota yang tidak selaras satu byte pada suatu waktu.
Tetapi apakah ada kasus di mana ini tidak aman? Apakah kompiler selalu menghasilkan kode yang benar (walaupun lebih lambat) untuk mengakses anggota yang tidak selaras dengan struct yang dikemas? Apakah mungkin untuk melakukannya dalam semua kasus?
sumber
Jawaban:
Ya,
__attribute__((packed))
berpotensi tidak aman di beberapa sistem. Gejala ini mungkin tidak akan muncul di x86, yang hanya membuat masalah lebih berbahaya; pengujian pada sistem x86 tidak akan mengungkapkan masalah. (Pada x86, akses yang tidak selaras ditangani dalam perangkat keras; jika Anda men-referensiint*
pointer yang menunjuk ke alamat ganjil, itu akan sedikit lebih lambat daripada jika itu benar selaras, tetapi Anda akan mendapatkan hasil yang benar.)Pada beberapa sistem lain, seperti SPARC, mencoba mengakses
int
objek yang tidak selaras menyebabkan kesalahan bus, menabrak program.Ada juga sistem di mana akses yang tidak selaras diam-diam mengabaikan bit urutan rendah dari alamat, menyebabkannya mengakses potongan memori yang salah.
Pertimbangkan program berikut:
Pada x86 Ubuntu dengan gcc 4.5.2, ia menghasilkan output berikut:
Pada SPARC Solaris 9 dengan gcc 4.5.1, ini menghasilkan yang berikut:
Dalam kedua kasus, program dikompilasi tanpa opsi tambahan, adil
gcc packed.c -o packed
.(Program yang menggunakan struct tunggal daripada array tidak menunjukkan masalah, karena kompiler dapat mengalokasikan struct pada alamat ganjil sehingga
x
anggota disejajarkan dengan benar. Dengan array duastruct foo
objek, setidaknya satu atau yang lain akan memiliki anggota yang tidak selarasx
.)(Dalam hal ini,
p0
menunjuk ke alamat yang tidak selaras, karena menunjuk ke anggota yang dikemasint
mengikutichar
anggota.p1
Kebetulan disejajarkan dengan benar, karena menunjuk ke anggota yang sama dalam elemen kedua array, sehingga ada duachar
objek sebelumnya - dan pada SPARC Solaris, arrayarr
tampaknya dialokasikan pada alamat yang genap, tetapi bukan kelipatan 4.)Ketika merujuk ke
x
anggotastruct foo
dengan nama, kompiler tahu bahwax
berpotensi berpotensi tidak selaras, dan akan menghasilkan kode tambahan untuk mengaksesnya dengan benar.Setelah alamat
arr[0].x
atauarr[1].x
telah disimpan dalam objek pointer, baik kompiler maupun program yang berjalan tidak tahu bahwa itu menunjuk keint
objek yang tidak selaras . Itu hanya mengasumsikan bahwa itu benar selaras, menghasilkan (pada beberapa sistem) kesalahan bus atau kegagalan lainnya yang serupa.Memperbaiki ini dalam gcc, saya percaya, tidak praktis. Sebuah solusi umum akan membutuhkan, untuk setiap upaya untuk melakukan dereferensi pointer ke jenis apa pun dengan persyaratan penyelarasan non-trivial baik (a) membuktikan pada waktu kompilasi bahwa pointer tidak menunjuk ke anggota yang tidak selaras dari struct yang dikemas, atau (b) menghasilkan kode bulkier dan lebih lambat yang dapat menangani objek yang disejajarkan atau tidak selaras.
Saya telah mengirimkan laporan bug gcc . Seperti yang saya katakan, saya tidak percaya itu praktis untuk memperbaikinya, tetapi dokumentasi harus menyebutkannya (saat ini tidak).
UPDATE : Pada 2018-12-20, bug ini ditandai sebagai TETAP. Tambalan akan muncul di gcc 9 dengan penambahan
-Waddress-of-packed-member
opsi baru , diaktifkan secara default.Saya baru saja membangun versi gcc dari sumber. Untuk program di atas, ini menghasilkan diagnostik ini:
sumber
Seperti yang saya katakan di atas, jangan bawa pointer ke anggota struct yang dipaket. Ini hanya bermain dengan api. Ketika Anda mengatakan
__attribute__((__packed__))
atau#pragma pack(1)
, apa yang sebenarnya Anda katakan adalah "Hei, ya, saya benar-benar tahu apa yang saya lakukan." Ketika ternyata Anda tidak melakukannya, Anda tidak dapat menyalahkan kompiler dengan benar.Mungkin kita bisa menyalahkan kompiler untuk kepuasan itu. Meskipun gcc memiliki
-Wcast-align
opsi, gcc tidak diaktifkan secara default atau dengan-Wall
atau-Wextra
. Ini tampaknya disebabkan oleh pengembang gcc yang menganggap jenis kode ini sebagai " kekejian " yang mematikan otak yang tidak layak ditangani - penghinaan yang dapat dimengerti, tetapi itu tidak membantu ketika seorang programmer yang tidak berpengalaman bertumbangan dengannya.Pertimbangkan yang berikut ini:
Di sini, jenisnya
a
adalah struct yang dikemas (seperti yang didefinisikan di atas). Demikian pula,b
adalah pointer ke struct yang dikemas. Jenis dari ekspresia.i
adalah (pada dasarnya) int l-nilai dengan 1 byte keselarasan.c
dand
keduanya normalint
. Saat membacaa.i
, kompiler menghasilkan kode untuk akses yang tidak selaras. Ketika Anda membacab->i
,b
tipe masih tahu itu dikemas, jadi tidak masalah mereka juga.e
adalah pointer ke int one-byte-sejajar, sehingga kompiler tahu bagaimana melakukan dereferensi dengan benar. Tetapi ketika Anda membuat tugasf = &a.i
, Anda menyimpan nilai dari pointer int yang tidak selaras dalam variabel pointer yang selaras - di situlah Anda salah. Dan saya setuju, gcc harus mengaktifkan peringatan inidefault (bahkan tidak dalam-Wall
atau-Wextra
).sumber
__attribute__((aligned(1)))
adalah ekstensi gcc dan tidak portabel. Sepengetahuan saya, satu-satunya cara yang sangat portabel untuk melakukan akses tidak selaras di C (dengan kombinasi kompiler / perangkat keras) adalah dengan salinan memori byte-bijaksana (memcpy atau serupa). Beberapa perangkat keras bahkan tidak memiliki instruksi untuk akses yang tidak selaras. Keahlian saya adalah dengan arm dan x86 yang dapat melakukan keduanya, meskipun akses yang tidak selaras lebih lambat. Jadi jika Anda perlu melakukan ini dengan kinerja tinggi, Anda harus mengendus perangkat keras dan menggunakan trik khusus lengkung.__attribute__((aligned(x)))
sekarang tampaknya diabaikan ketika digunakan untuk pointer. :( Saya belum memiliki detail lengkap tentang ini, tetapi menggunakan__builtin_assume_aligned(ptr, align)
tampaknya mendapatkan gcc untuk menghasilkan kode yang benar. Ketika saya jawaban yang lebih ringkas (dan semoga laporan bug) saya akan memperbarui jawaban saya.uint32_t
anggota akan menghasilkan auint32_t packed*
; mencoba membaca dari pointer seperti pada misalnya Cortex-M0 akan memanggil IIRC subrutin yang akan memakan waktu ~ 7x selama pembacaan normal jika pointer tidak selaras atau ~ 3x selama itu disejajarkan, tetapi akan berperilaku dapat diprediksi dalam kedua kasus [kode in-line akan memakan waktu 5x lebih lama baik selaras atau tidak selaras].Ini sangat aman selama Anda selalu mengakses nilai melalui struct melalui
.
(titik) atau->
notasi.Apa yang tidak aman adalah mengambil pointer dari data yang tidak selaras dan kemudian mengaksesnya tanpa memperhitungkannya.
Juga, meskipun setiap item dalam struct diketahui tidak selaras, itu diketahui tidak selaras dengan cara tertentu , sehingga struct secara keseluruhan harus disejajarkan seperti yang diharapkan oleh kompiler atau akan ada masalah (pada beberapa platform, atau di masa depan jika cara baru ditemukan untuk mengoptimalkan akses yang tidak selaras).
sumber
Menggunakan atribut ini jelas tidak aman.
Satu hal tertentu yang rusak adalah kemampuan
union
yang berisi dua atau lebih struct untuk menulis satu anggota dan membaca yang lain jika struct memiliki urutan awal umum anggota. Bagian 6.5.2.3 dari standar C11 menyatakan:Ketika
__attribute__((packed))
diperkenalkan itu istirahat ini. Contoh berikut dijalankan di Ubuntu 16.04 x64 menggunakan gcc 5.4.0 dengan optimasi dinonaktifkan:Keluaran:
Meskipun
struct s1
danstruct s2
memiliki "urutan awal umum", kemasan yang diterapkan pada yang pertama berarti bahwa anggota yang sesuai tidak hidup pada byte byte yang sama. Hasilnya adalah nilai yang dituliskan kepada anggotax.b
tidak sama dengan nilai yang dibacakan dari anggotay.b
, meskipun standar mengatakan mereka harus sama.sumber
(Berikut ini adalah contoh yang sangat buatan untuk menggambarkan.) Salah satu penggunaan utama struct dikemas adalah di mana Anda memiliki aliran data (katakanlah 256 byte) yang ingin Anda berikan makna. Jika saya mengambil contoh yang lebih kecil, misalkan saya memiliki program yang berjalan di Arduino saya yang mengirimkan melalui serial paket 16 byte yang memiliki arti sebagai berikut:
Maka saya bisa mendeklarasikan sesuatu seperti
dan kemudian saya bisa merujuk ke byte targetAddr melalui aStruct.targetAddr daripada mengutak-atik aritmatika pointer.
Sekarang dengan hal-hal penyelarasan terjadi, mengambil void * pointer di memori ke data yang diterima dan melemparkannya ke myStruct * tidak akan berfungsi kecuali jika kompiler memperlakukan struct sebagai paket (yaitu, ia menyimpan data dalam urutan yang ditentukan dan menggunakan tepat 16 byte untuk contoh ini). Ada penalti kinerja untuk pembacaan yang tidak selaras, jadi menggunakan struct yang dikemas untuk data yang secara aktif bekerja dengan program Anda tidak selalu merupakan ide yang bagus. Tetapi ketika program Anda dilengkapi dengan daftar byte, struct dikemas membuatnya lebih mudah untuk menulis program yang mengakses konten.
Kalau tidak, Anda akhirnya menggunakan C ++ dan menulis kelas dengan metode accessor dan hal-hal yang mengarahkan aritmatika di belakang layar. Singkatnya, struct yang dikemas untuk menangani secara efisien dengan data yang dikemas, dan data yang dikemas mungkin sesuai dengan program Anda. Untuk sebagian besar, Anda kode harus membaca nilai dari struktur, bekerja dengan mereka, dan menuliskannya kembali setelah selesai. Semua yang lain harus dilakukan di luar struktur yang dikemas. Bagian dari masalah adalah hal-hal tingkat rendah yang C coba sembunyikan dari programmer, dan lompatan melingkar yang diperlukan jika hal-hal seperti itu benar-benar penting bagi programmer. (Anda hampir membutuhkan konstruk 'tata letak data' yang berbeda dalam bahasa sehingga Anda dapat mengatakan 'panjang benda ini 48 byte, foo merujuk pada data 13 byte, dan harus ditafsirkan demikian'; dan konstruk data terstruktur terpisah,
sumber