Apa yang saya tanyakan adalah trik terkenal "anggota terakhir dari struct memiliki panjang variabel". Ini berjalan seperti ini:
struct T {
int len;
char s[1];
};
struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");
Karena cara struct diletakkan dalam memori, kami dapat melapisi struct di atas blok yang lebih besar dari yang diperlukan dan memperlakukan anggota terakhir seolah-olah lebih besar dari yang 1 char
ditentukan.
Jadi pertanyaannya adalah: Apakah teknik ini secara teknis merupakan perilaku yang tidak terdefinisi? . Saya berharap begitu, tetapi penasaran apa yang dikatakan standar tentang ini.
PS: Saya mengetahui pendekatan C99 untuk ini, saya ingin jawaban menempel secara khusus pada versi trik seperti yang tercantum di atas.
c
undefined-behavior
c89
Evan Teran
sumber
sumber
Jawaban:
Seperti yang dikatakan C FAQ :
dan:
Alasan di balik bit 'yang benar-benar sesuai' ada dalam spesifikasi, bagian J.2 Perilaku tidak terdefinisi, yang mencakup dalam daftar perilaku yang tidak ditentukan:
Paragraf 8 dari Bagian 6.5.6 Operator aditif memiliki penyebutan lain bahwa akses di luar batas array yang ditentukan tidak ditentukan:
sumber
p->s
tidak pernah digunakan sebagai array. Ini diteruskan kestrcpy
, dalam hal ini meluruh ke dataranchar *
, yang kebetulan mengarah ke objek yang secara hukum dapat ditafsirkan sebagaichar [100];
di dalam objek yang dialokasikan.malloc
, ketika Anda hanya mengonversi yang dikembalikanvoid *
ke pointer ke [sebuah struct berisi] sebuah array. Masih valid untuk mengakses bagian mana pun dari objek yang dialokasikan menggunakan penunjuk kechar
(atau lebih disukaiunsigned char
).malloc
. Cari "objek" dalam standar sebelum Anda mengeluarkan bs.Saya percaya bahwa secara teknis ini adalah perilaku yang tidak terdefinisi. Standar (bisa dibilang) tidak membahasnya secara langsung, jadi itu termasuk di bawah "atau dengan menghilangkan definisi eksplisit apa pun dari perilaku." klausa (§4 / 2 dari C99, §3.16 / 2 dari C89) yang mengatakan itu perilaku yang tidak ditentukan.
"Bisa dibilang" di atas bergantung pada definisi operator langganan array. Secara khusus, ia mengatakan: "Ekspresi postfix diikuti dengan ekspresi dalam tanda kurung siku [] adalah penunjukan objek array yang disubscripsikan." (C89, §6.3.2.1 / 2).
Anda dapat berargumen bahwa "objek array" sedang dilanggar di sini (karena Anda berlangganan di luar rentang objek array yang ditentukan), dalam hal ini perilakunya (sedikit lebih) secara eksplisit tidak ditentukan, bukan hanya tidak terdefinisi milik tidak ada yang cukup mendefinisikannya.
Secara teori, saya dapat membayangkan kompiler yang melakukan pemeriksaan batas-batas array dan (misalnya) akan membatalkan program ketika / jika Anda mencoba menggunakan subskrip di luar jangkauan. Faktanya, saya tidak tahu tentang hal seperti itu, dan mengingat popularitas gaya kode ini, bahkan jika kompiler mencoba menerapkan langganan dalam beberapa keadaan, sulit untuk membayangkan bahwa ada orang yang tahan melakukannya di situasi ini.
sumber
arr[x] = y;
mungkin akan ditulis ulang sebagaiarr[0] = y;
; untuk larik berukuran 2,arr[i] = 4;
mungkin akan ditulis ulang sebagaii ? arr[1] = 4 : arr[0] = 4;
Meskipun saya belum pernah melihat kompilator melakukan pengoptimalan seperti itu, pada beberapa sistem tertanam mereka bisa sangat produktif. Pada PIC18x, menggunakan tipe data 8-bit, kode untuk pernyataan pertama adalah enam belas byte, yang kedua, dua atau empat, dan yang ketiga, delapan atau dua belas. Bukan pengoptimalan yang buruk jika legal.a[2] == a + 2
), itu tidak. Jika saya benar, semua standar C mendefinisikan akses array sebagai penunjuk aritmatik.Ya, ini adalah perilaku yang tidak ditentukan.
Laporan Cacat Bahasa C # 051 memberikan jawaban pasti untuk pertanyaan ini:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html
Dalam dokumen C99 Rationale, C Committee menambahkan:
sumber
malloc
) valid sebagai tambahan, jadi bagaimana penunjuk identik, diperoleh melalui rute lain, apakah tidak valid dalam penambahan? Bahkan jika mereka ingin mengklaimnya sebagai UB, itu tidak berarti apa-apa, karena secara komputasi tidak ada cara implementasi untuk membedakan antara penggunaan yang didefinisikan dengan baik dan penggunaan yang seharusnya tidak ditentukan.*foo
berisi array elemen tunggalboz
, ekspresifoo->boz[biz()*391]=9;
dapat disederhanakan sebagaibiz(),foo->boz[0]=9;
). Sayangnya, array nol elemen penolakan compiler berarti banyak kode yang menggunakan array elemen tunggal, dan akan rusak oleh pengoptimalan itu.Cara tertentu untuk melakukannya tidak secara eksplisit didefinisikan dalam standar C mana pun, tetapi C99 menyertakan "struct hack" sebagai bagian dari bahasa. Di C99, anggota terakhir dari sebuah struct dapat berupa "anggota array fleksibel", dideklarasikan sebagai
char foo[]
(dengan tipe apa pun yang Anda inginkan sebagai gantinyachar
).sumber
Ini bukan perilaku yang tidak terdefinisi , terlepas dari apa yang dikatakan seseorang, resmi atau tidak , karena itu ditentukan oleh standar.
p->s
, kecuali jika digunakan sebagai nilai l, mengevaluasi ke penunjuk yang identik dengan(char *)p + offsetof(struct T, s)
. Secara khusus, ini adalahchar
penunjuk yang valid di dalam objek malloc'd, dan ada 100 (atau lebih, tergantung pada pertimbangan penyelarasan) alamat berturut-turut segera mengikutinya yang juga valid sebagaichar
objek di dalam objek yang dialokasikan. Fakta bahwa pointer diturunkan dengan menggunakan->
bukannya secara eksplisit menambahkan offset ke pointer yang dikembalikan olehmalloc
, cast tochar *
, tidak relevan.Secara teknis,
p->s[0]
adalah elemen tunggal darichar
array di dalam struct, beberapa elemen berikutnya (misalnyap->s[1]
melaluip->s[3]
) kemungkinan besar padding byte di dalam struct, yang dapat rusak jika Anda melakukan penugasan ke struct secara keseluruhan tetapi tidak jika Anda hanya mengakses individu anggota, dan elemen lainnya adalah ruang tambahan dalam objek yang dialokasikan yang dapat Anda gunakan secara bebas sesuka Anda, selama Anda mematuhi persyaratan penyelarasan (danchar
tidak memiliki persyaratan penyelarasan).Jika Anda khawatir bahwa kemungkinan tumpang tindih dengan byte padding di struct mungkin entah bagaimana memanggil setan hidung, Anda dapat menghindari ini dengan mengganti
1
in[1]
dengan nilai yang memastikan bahwa tidak ada padding di akhir struct. Cara sederhana namun boros untuk melakukan ini adalah dengan membuat struct dengan anggota yang identik kecuali tidak ada larik di akhir, dan digunakans[sizeof struct that_other_struct];
untuk larik. Kemudian,p->s[i]
secara jelas didefinisikan sebagai elemen dari array di struct untuki<sizeof struct that_other_struct
dan sebagai objek char di alamat setelah akhir dari struct untuki>=sizeof struct that_other_struct
.Sunting: Sebenarnya, dalam trik di atas untuk mendapatkan ukuran yang tepat, Anda mungkin juga perlu meletakkan gabungan yang berisi setiap tipe sederhana sebelum larik, untuk memastikan bahwa larik itu sendiri dimulai dengan perataan maksimal daripada di tengah padding elemen lain . Sekali lagi, saya tidak percaya semua ini perlu, tapi saya menawarkannya untuk pengacara bahasa paling paranoid di luar sana.
Sunting 2: Tumpang tindih dengan padding byte jelas bukan masalah, karena bagian lain dari standar. C mensyaratkan bahwa jika dua struct setuju dalam urutan awal elemen mereka, elemen awal yang sama dapat diakses melalui penunjuk ke salah satu tipe. Akibatnya, jika sebuah struct identik dengan
struct T
tetapi dengan larik akhir yang lebih besar dideklarasikan, elemens[0]
tersebut harus bertepatan dengan elemens[0]
distruct T
, dan keberadaan elemen tambahan ini tidak dapat mempengaruhi atau dipengaruhi dengan mengakses elemen umum dari struct yang lebih besar. menggunakan penunjuk kestruct T
.sumber
malloc
yang diakses sebagai array atau jika itu adalah struct yang lebih besar yang diakses melalui pointer ke struct yang lebih kecil yang elemennya merupakan subset awal dari elemen dari struct yang lebih besar, antara lain kasus.malloc
tidak mengalokasikan kisaran memori yang dapat diakses dengan aritmatika pointer, apa gunanya? Dan jikap->s[1]
ini didefinisikan oleh standar sebagai sintaksis gula untuk pointer aritmetika, maka jawaban ini hanya menegaskan kembali bahwamalloc
berguna. Apa yang tersisa untuk didiskusikan? :)1
. Sesederhana itu.int m[1]; int n[1]; if(m+1 == n) m[1] = 0;
dengan asumsiif
cabang sudah dimasukkan. Ini adalah UB (dan tidak dijamin akan dimulain
) sesuai 6.5.6 p8 (kalimat terakhir), saat saya membacanya. Terkait: 6.5.9 hal6 dengan catatan kaki 109. (Referensi untuk C11 n1570.) [...]Ya, ini adalah perilaku yang secara teknis tidak ditentukan.
Perhatikan, setidaknya ada tiga cara untuk menerapkan "struct hack":
(1) Mendeklarasikan trailing array dengan ukuran 0 (cara paling "populer" di kode lama). Ini jelas UB, karena deklarasi larik ukuran nol selalu ilegal di C. Bahkan jika dikompilasi, bahasa tersebut tidak menjamin tentang perilaku kode yang melanggar batasan.
(2) Menyatakan array dengan ukuran legal minimal - 1 (kasus Anda). Dalam hal ini setiap upaya untuk mengambil pointer ke
p->s[0]
dan menggunakannya untuk aritmatika pointer yang melampauip->s[1]
perilaku tidak terdefinisi. Misalnya, implementasi debugging diizinkan untuk menghasilkan pointer khusus dengan informasi rentang yang disematkan, yang akan menjebak setiap kali Anda mencoba membuat pointer di luarp->s[1]
.(3) Mendeklarasikan array dengan ukuran "sangat besar" seperti 10000, misalnya. Idenya adalah bahwa ukuran yang dinyatakan seharusnya lebih besar dari apa pun yang mungkin Anda butuhkan dalam praktik aktual. Metode ini gratis di UB terkait dengan jangkauan akses array. Namun, dalam praktiknya, tentunya kita akan selalu mengalokasikan jumlah memori yang lebih kecil (hanya sebanyak yang benar-benar dibutuhkan). Saya tidak yakin tentang legalitas ini, yaitu saya bertanya-tanya seberapa legal mengalokasikan lebih sedikit memori untuk objek daripada ukuran objek yang dinyatakan (dengan asumsi kita tidak pernah mengakses anggota "non-dialokasikan").
sumber
s[1]
bukan perilaku tidak terdefinisi. Ini sama dengan*(s+1)
, yang sama dengan*((char *)p + offsetof(struct T, s) + 1)
, yang merupakan penunjuk yang valid kechar
dalam objek yang dialokasikan.foo[]
gula sintaksis untuk*foo
), maka akses apa pun di luar yang lebih kecil dari ukuran yang dinyatakan dan ukuran yang dialokasikan adalah UB, terlepas dari bagaimana aritmatika pointer dilakukan.foo[]
di struct bukanlah gula sintaksis untuk*foo
; ini adalah anggota array fleksibel C99. Selebihnya, simak jawaban dan komentar saya di jawaban lainnya.unsigned char [sizeof object]
larik hamparan imajiner . Saya mendukung klaim saya bahwa anggota array fleksibel "hack" untuk pra-C99 memiliki perilaku yang terdefinisi dengan baik.Standar cukup jelas bahwa Anda tidak dapat mengakses hal-hal di samping akhir larik. (dan melalui pointer tidak membantu, karena Anda tidak diizinkan untuk menambah bahkan pointer melewati satu setelah akhir array).
Dan untuk "bekerja dalam praktek". Saya telah melihat pengoptimal gcc / g ++ menggunakan bagian standar ini sehingga menghasilkan kode yang salah saat memenuhi C yang tidak valid ini.
sumber
Jika kompiler menerima sesuatu seperti
Saya pikir cukup jelas bahwa itu harus siap menerima subskrip pada 'dat' di luar panjangnya. Di sisi lain, jika seseorang membuat kode seperti:
dan kemudian mengakses somestruct-> dat [x]; Saya tidak akan berpikir compiler berkewajiban untuk menggunakan kode alamat-komputasi yang akan bekerja dengan nilai x yang besar. Saya pikir jika seseorang ingin benar-benar aman, paradigma yang tepat akan lebih seperti:
dan kemudian lakukan malloc sebesar (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + dikehendaki_array_length) byte (ingat bahwa jika diinginkan_array_length lebih besar dari LARGEST_DAT_SIZE, hasilnya mungkin tidak ditentukan).
Kebetulan, saya pikir keputusan untuk melarang array dengan panjang nol adalah keputusan yang tidak menguntungkan (beberapa dialek lama seperti Turbo C mendukungnya) karena array dengan panjang nol dapat dianggap sebagai tanda bahwa kompiler harus menghasilkan kode yang akan bekerja dengan indeks yang lebih besar .
sumber