Apakah __attribute __ ((dikemas)) / #pragma paket gcc tidak aman?

164

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 intobjek untuk memiliki 4-byte alignment, __attribute__((packed))dapat menyebabkan intanggota 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?

Keith Thompson
sumber
1
Laporan bug gcc sekarang ditandai sebagai TETAP dengan penambahan peringatan pada penunjuk pointer (dan opsi untuk menonaktifkan peringatan). Detail dalam jawaban saya .
Keith Thompson

Jawaban:

148

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-referensi int*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 intobjek 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:

#include <stdio.h>
#include <stddef.h>
int main(void)
{
    struct foo {
        char c;
        int x;
    } __attribute__((packed));
    struct foo arr[2] = { { 'a', 10 }, {'b', 20 } };
    int *p0 = &arr[0].x;
    int *p1 = &arr[1].x;
    printf("sizeof(struct foo)      = %d\n", (int)sizeof(struct foo));
    printf("offsetof(struct foo, c) = %d\n", (int)offsetof(struct foo, c));
    printf("offsetof(struct foo, x) = %d\n", (int)offsetof(struct foo, x));
    printf("arr[0].x = %d\n", arr[0].x);
    printf("arr[1].x = %d\n", arr[1].x);
    printf("p0 = %p\n", (void*)p0);
    printf("p1 = %p\n", (void*)p1);
    printf("*p0 = %d\n", *p0);
    printf("*p1 = %d\n", *p1);
    return 0;
}

Pada x86 Ubuntu dengan gcc 4.5.2, ia menghasilkan output berikut:

sizeof(struct foo)      = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = 0xbffc104f
p1 = 0xbffc1054
*p0 = 10
*p1 = 20

Pada SPARC Solaris 9 dengan gcc 4.5.1, ini menghasilkan yang berikut:

sizeof(struct foo)      = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = ffbff317
p1 = ffbff31c
Bus error

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 xanggota disejajarkan dengan benar. Dengan array dua struct fooobjek, setidaknya satu atau yang lain akan memiliki anggota yang tidak selaras x.)

(Dalam hal ini, p0menunjuk ke alamat yang tidak selaras, karena menunjuk ke anggota yang dikemas intmengikuti charanggota. p1Kebetulan disejajarkan dengan benar, karena menunjuk ke anggota yang sama dalam elemen kedua array, sehingga ada dua charobjek sebelumnya - dan pada SPARC Solaris, array arrtampaknya dialokasikan pada alamat yang genap, tetapi bukan kelipatan 4.)

Ketika merujuk ke xanggota struct foodengan nama, kompiler tahu bahwa xberpotensi berpotensi tidak selaras, dan akan menghasilkan kode tambahan untuk mengaksesnya dengan benar.

Setelah alamat arr[0].xatau arr[1].xtelah disimpan dalam objek pointer, baik kompiler maupun program yang berjalan tidak tahu bahwa itu menunjuk ke intobjek 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-memberopsi baru , diaktifkan secara default.

Ketika alamat anggota struct atau gabungan yang diambil diambil, itu dapat menghasilkan nilai penunjuk yang tidak selaras. Patch ini menambahkan -Waddress-of-packed-member untuk memeriksa perataan pada penugasan pointer dan memperingatkan alamat yang tidak selaras serta pointer yang tidak selaras

Saya baru saja membangun versi gcc dari sumber. Untuk program di atas, ini menghasilkan diagnostik ini:

c.c: In function main’:
c.c:10:15: warning: taking address of packed member of struct foo may result in an unaligned pointer value [-Waddress-of-packed-member]
   10 |     int *p0 = &arr[0].x;
      |               ^~~~~~~~~
c.c:11:15: warning: taking address of packed member of struct foo may result in an unaligned pointer value [-Waddress-of-packed-member]
   11 |     int *p1 = &arr[1].x;
      |               ^~~~~~~~~
Keith Thompson
sumber
1
berpotensi tidak selaras, dan akan menghasilkan ... apa?
Almo
5
elemen struct yang tidak selaras pada ARM melakukan hal-hal aneh: Beberapa mengakses menyebabkan kesalahan, yang lain menyebabkan data yang diambil disusun ulang secara intuitif atau menggabungkan data tak terduga yang berdekatan.
wallyk
8
Tampaknya pengepakan itu sendiri aman, tetapi bagaimana anggota yang dikemas digunakan dapat tidak aman. CPU berbasis ARM yang lebih lama juga tidak mendukung akses memori yang tidak selaras, versi yang lebih baru melakukannya tetapi saya tahu Symbian OS masih melarang akses yang tidak selaras ketika berjalan pada versi yang lebih baru ini (dukungannya dimatikan).
James
14
Cara lain untuk memperbaikinya dalam gcc adalah dengan menggunakan sistem tipe: mengharuskan pointer ke anggota struct dikemas hanya dapat ditugaskan ke pointer yang ditandai sebagai dikemas (mis. Berpotensi tidak selaras). Tapi sungguh: struct dikemas, katakan saja tidak.
caf
9
@ Flavius: Tujuan utama saya adalah untuk mendapatkan informasi di luar sana. Lihat juga meta.stackexchange.com/questions/17463/…
Keith Thompson
62

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-alignopsi, gcc tidak diaktifkan secara default atau dengan -Wallatau -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:

struct  __attribute__((__packed__)) my_struct {
    char c;
    int i;
};

struct my_struct a = {'a', 123};
struct my_struct *b = &a;
int c = a.i;
int d = b->i;
int *e __attribute__((aligned(1))) = &a.i;
int *f = &a.i;

Di sini, jenisnya aadalah struct yang dikemas (seperti yang didefinisikan di atas). Demikian pula, badalah pointer ke struct yang dikemas. Jenis dari ekspresi a.iadalah (pada dasarnya) int l-nilai dengan 1 byte keselarasan. cdan dkeduanya normal int. Saat membaca a.i, kompiler menghasilkan kode untuk akses yang tidak selaras. Ketika Anda membaca b->i, btipe masih tahu itu dikemas, jadi tidak masalah mereka juga. eadalah pointer ke int one-byte-sejajar, sehingga kompiler tahu bagaimana melakukan dereferensi dengan benar. Tetapi ketika Anda membuat tugas f = &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 -Wallatau -Wextra).

Daniel Santos
sumber
6
+1 untuk menjelaskan cara menggunakan pointer dengan struct yang tidak selaras!
Soumya
@ Soumum Terima kasih atas poinnya! :) Perlu diingat bahwa itu __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.
Daniel Santos
4
@ Soumum Sedihnya, __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.
Daniel Santos
@DanielSantos: Kompiler berkualitas yang saya gunakan (Keil) mengenali kualifikasi "penuh" untuk pointer; jika suatu struktur dinyatakan "penuh", mengambil alamat seorang uint32_tanggota akan menghasilkan a uint32_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].
supercat
49

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).

am
sumber
Hmm, saya bertanya-tanya apa yang terjadi jika Anda meletakkan satu struct dikemas di dalam struct dikemas lain di mana penyelarasan akan berbeda? Pertanyaan yang menarik, tetapi seharusnya tidak mengubah jawabannya.
pagi
GCC juga tidak akan selalu menyelaraskan struktur itu sendiri. Sebagai contoh: struct foo {int x; char c; } __attribusi __ ((dikemas)); struct bar {char c; struct foo f; }; Saya menemukan bahwa bilah :: f :: x tidak perlu disejajarkan, setidaknya pada citarasa MIPS tertentu.
Anton
3
@ Cantonm: Ya, sebuah struct dalam sebuah struct yang dikemas mungkin tidak selaras, tetapi, sekali lagi, kompiler tahu apa penyelarasan setiap bidang, dan itu sangat aman selama Anda tidak mencoba menggunakan pointer ke dalam struct. Anda harus membayangkan sebuah struct dalam sebuah struct sebagai satu rangkaian bidang yang datar, dengan nama tambahan hanya untuk keterbacaan.
pagi
6

Menggunakan atribut ini jelas tidak aman.

Satu hal tertentu yang rusak adalah kemampuan unionyang 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:

6 Satu jaminan khusus dibuat untuk menyederhanakan penggunaan serikat: jika serikat pekerja mengandung beberapa struktur yang memiliki urutan awal yang sama (lihat di bawah), dan jika objek serikat pekerja saat ini berisi salah satu dari struktur ini, maka diizinkan untuk memeriksa bagian awal yang umum dari salah satu dari mereka di mana saja bahwa deklarasi jenis serikat yang lengkap terlihat. Dua struktur berbagi urutan awal yang sama jika anggota yang sesuai memiliki tipe yang kompatibel (dan, untuk bidang bit, lebar yang sama) untuk urutan satu atau lebih anggota awal.

...

9 CONTOH 3 Berikut ini adalah fragmen yang valid:

union {
    struct {
        int    alltypes;
    }n;
    struct {
        int    type;
        int    intnode;
    } ni;
    struct {
        int    type;
        double doublenode;
    } nf;
}u;
u.nf.type = 1;
u.nf.doublenode = 3.14;
/*
...
*/
if (u.n.alltypes == 1)
if (sin(u.nf.doublenode) == 0.0)
/*
...
*/

Ketika __attribute__((packed))diperkenalkan itu istirahat ini. Contoh berikut dijalankan di Ubuntu 16.04 x64 menggunakan gcc 5.4.0 dengan optimasi dinonaktifkan:

#include <stdio.h>
#include <stdlib.h>

struct s1
{
    short a;
    int b;
} __attribute__((packed));

struct s2
{
    short a;
    int b;
};

union su {
    struct s1 x;
    struct s2 y;
};

int main()
{
    union su s;
    s.x.a = 0x1234;
    s.x.b = 0x56789abc;

    printf("sizeof s1 = %zu, sizeof s2 = %zu\n", sizeof(struct s1), sizeof(struct s2));
    printf("s.y.a=%hx, s.y.b=%x\n", s.y.a, s.y.b);
    return 0;
}

Keluaran:

sizeof s1 = 6, sizeof s2 = 8
s.y.a=1234, s.y.b=5678

Meskipun struct s1dan struct s2memiliki "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 anggota x.btidak sama dengan nilai yang dibacakan dari anggota y.b, meskipun standar mengatakan mereka harus sama.

dbush
sumber
Satu mungkin berpendapat bahwa jika Anda mengemas salah satu struct dan bukan yang lain, maka Anda tidak akan mengharapkan mereka memiliki tata letak yang konsisten. Tapi ya, ini adalah persyaratan standar lain yang bisa dilanggar.
Keith Thompson
1

(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:

0: message type (1 byte)
1: target address, MSB
2: target address, LSB
3: data (chars)
...
F: checksum (1 byte)

Maka saya bisa mendeklarasikan sesuatu seperti

typedef struct {
  uint8_t msgType;
  uint16_t targetAddr; // may have to bswap
  uint8_t data[12];
  uint8_t checksum;
} __attribute__((packed)) myStruct;

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,

John Allsup
sumber
Kecuali saya melewatkan sesuatu, ini tidak menjawab pertanyaan. Anda berpendapat bahwa pengemasan struktur nyaman (yang mana), tetapi Anda tidak menjawab pertanyaan apakah itu aman. Juga, Anda menyatakan bahwa hukuman kinerja untuk bacaan yang tidak selaras; itu berlaku untuk x86, tetapi tidak untuk semua sistem, seperti yang saya tunjukkan dalam jawaban saya.
Keith Thompson