"Struct hack" teknik olarak tanımlanmamış bir davranış mı?


111

Sorduğum şey, iyi bilinen "bir yapının son üyesinin değişken uzunluklu" numarasıdır. Şöyle bir şey oluyor:

struct T {
    int len;
    char s[1];
};

struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");

Yapının belleğe yerleştirilme şekli nedeniyle, yapıyı gerekenden daha büyük bir bloğun üzerine yerleştirebilir ve son üyeye 1 charbelirtilenden daha büyükmiş gibi davranabiliriz.

Yani soru şu: Bu teknik teknik olarak tanımlanmamış bir davranış mı? . Öyle olmasını beklerdim, ancak standardın bunun hakkında ne söylediğini merak ediyordum.

Not: Bu konudaki C99 yaklaşımının farkındayım, cevapların özellikle yukarıda listelenen numaraya bağlı kalmasını istiyorum.


33
Bu oldukça açık makul ve her şeyden gibi görünüyor sorumlu soruya. Yakın oylamanın sebebini göremiyorum.
cHao

2
Struct hack'ini desteklemeyen bir "ansi c" derleyicisi tanıttıysanız, tanıdığım çoğu c programcısı derleyicinizin "doğru çalıştığını" kabul etmeyecektir. Standardın katı bir şekilde okunmasını kabul etmelerine rağmen. Komite bu konuda bir tanesini kaçırdı.
dmckee --- eski moderatör yavru kedi

4
@james Hack, minimal bir dizi bildirmiş olmasına rağmen kastettiğiniz dizi için yeterince büyük bir nesneyi mallocing yaparak çalışır. Dolayısıyla , yapının katı tanımının dışında ayrılmış belleğe erişiyorsunuz . Tahsisatınızın ötesine yazmak tartışılmaz bir hatadır, ancak bu, tahsisatınıza yazmaktan farklıdır, ancak "yapı" nın dışındadır.
dmckee --- eski moderatör yavru kedi

2
@James: Büyük boy malloc burada kritik önem taşıyor. Yapının nominal ucunu geçen hafızanın --- yasal adresli ve yapının "sahip olduğu" (yani başka herhangi bir kuruluşun kullanması yasa dışıdır) olduğunu garanti eder. Bunun, struct hack'i otomatik değişkenlerde kullanamayacağınız anlamına geldiğini unutmayın: dinamik olarak tahsis edilmeleri gerekir.
dmckee --- eski moderatör yavru kedi

5
@detly: Bir şeyi ayırmak / ayırmak, iki şeyi ayırmaktan / ayırmaktan daha kolaydır, özellikle ikincisinin uğraşmanız gereken iki başarısızlık yolu olduğu için. Bu benim için marjinal maliyet / hız tasarruflarından daha önemli.
jamesdlin

Yanıtlar:


52

As C SSS diyor:

Yasal mı yoksa taşınabilir mi olduğu net değil, ancak oldukça popüler.

ve:

... resmi bir yorum, bilinen tüm uygulamalar altında işe yarıyor gibi görünse de, C Standardına tam olarak uymadığını kabul etmiştir. (Dizi sınırlarını dikkatlice kontrol eden derleyiciler uyarılar verebilir.)

'Kesinlikle uyumlu' bitinin ardındaki mantık , tanımsız davranışlar listesine dahil olan J.2 Tanımlanmamış davranış bölümündeki spesifikasyondadır :

  • Bir nesne belirli bir alt simge ile görünürde erişilebilir olsa bile ( a[1][7]bildirime verilen lvalue ifadesinde olduğu gibi int a[4][5]) (6.5.6) bir dizi alt simgesi aralık dışındadır.

Bölüm 6.5.6 Katkı operatörlerinin 8. paragrafında , tanımlanmış dizi sınırlarının ötesinde erişimin tanımsız olduğuna dair başka bir ifade daha vardır:

Hem işaretçi işleneni hem de sonuç aynı dizi nesnesinin öğelerini işaret ederse veya dizi nesnesinin son öğesini geçerse, değerlendirme bir taşma üretmemelidir; aksi takdirde davranış tanımsızdır.


1
OP'nin kodunda, p->sasla bir dizi olarak kullanılmaz. Bu geçirilen strcpy, bu durumda bir ova dönüşür char *yasal olarak yorumlanabilir bir nesneye noktasına olur, char [100];tahsis nesnenin içine.
R .. GitHub DUR YARDIMCI ICE

3
Belki de buna bakmanın başka bir yolu, dilin J.2'de açıklandığı gibi gerçek dizi değişkenlerine erişme şeklinizi makul bir şekilde kısıtlayabileceğidir , ancak mallocyalnızca döndürülenleri dönüştürdüğünüzde, tarafından tahsis edilen bir nesne için bu tür kısıtlamalar yapmanın bir yolu yoktur. void *bir dizi [yapı içeren] bir göstericiye. Ayrılan nesnenin herhangi bir kısmına bir işaretçi char(veya tercihen unsigned char) kullanarak erişmek hala geçerlidir .
R .. GitHub BUZA YARDIM ETMEYİ DURDUR

@R. - J2'nin bunu nasıl kapsamadığını görebiliyorum, ancak 6.5.6'da da yer almıyor mu?
detly

1
Elbette olabilir! Tip ve boyut bilgisi her işaretçiye gömülebilir ve daha sonra herhangi bir hatalı işaretçi aritmetiği tuzaklanmak için yapılabilir - örneğin CCured'e bakın . Daha felsefi bir düzeyde, olası hiçbir uygulamanın sizi yakalayıp yakalayamayacağı önemli değil, yine de tanımlanmamış bir davranıştır (Duraklama Probleminin çivilenmesi için bir kehanet gerektiren tanımlanmamış davranış vakaları vardır iirc - tam da bu yüzden tanımsızdırlar).
zwol

4
Nesne bir dizi nesnesi değildir, bu nedenle 6.5.6 önemsizdir. Nesne, tarafından ayrılan bellek bloğudur malloc. Bs püskürtmeden önce standartta "nesne" yi arayın.
R .. GitHub BUZA YARDIM ETMEYİ DURDUR

34

Teknik olarak bunun tanımlanmamış bir davranış olduğuna inanıyorum. Standart (muhtemelen) onu doğrudan ele almıyor, bu nedenle "herhangi bir açık davranış tanımının ihmal edilmesiyle" kapsamına giriyor. tanımsız bir davranış olduğunu söyleyen madde (C99 §4 / 2, C89 §3.16 / 2).

Yukarıdaki "tartışmalı olarak", dizi abone oluşturma operatörünün tanımına bağlıdır. Özellikle şöyle der: "Köşeli parantez [] içindeki bir ifadenin ardından gelen bir sonek ifadesi, bir dizi nesnesinin alt simgeli bir atamasıdır." (C89, §6.3.2.1 / 2).

Burada "bir dizi nesnesinin" ihlal edildiğini iddia edebilirsiniz (dizi nesnesinin tanımlanmış aralığının dışında abone olduğunuz için), bu durumda davranış (biraz daha fazla), tanımsız olmak yerine açıkça tanımsızdır. onu tam olarak tanımlayan hiçbir şeyin nezaketi.

Teoride, dizi sınırlarını kontrol eden ve (örneğin) aralık dışı bir alt simge kullanmaya çalıştığınızda / çalıştığınızda programı iptal eden bir derleyici hayal edebiliyorum. Aslında, böyle bir şeyin var olduğunu bilmiyorum ve bu kod tarzının popülaritesi göz önüne alındığında, bir derleyici bazı durumlarda abonelikleri zorlamaya çalışsa bile, herhangi birinin bunu yapmaya katlanacağını hayal etmek zor. bu durum.


2
Ayrıca bir dizinin 1 boyutunda olması durumunda arr[x] = y;şu şekilde yeniden yazılabileceğine karar verebilecek bir derleyici hayal edebiliyorum arr[0] = y;; boyut 2 dizisi için, arr[i] = 4;şu şekilde yeniden yazılabilir: i ? arr[1] = 4 : arr[0] = 4; Bir derleyicinin bu tür optimizasyonları gerçekleştirdiğini hiç görmedim, bazı gömülü sistemlerde çok üretken olabilirler. 8 bitlik veri türleri kullanan bir PIC18x'te, ilk ifadenin kodu on altı bayt, ikinci, iki veya dört ve üçüncü, sekiz veya on iki olacaktır. Yasal ise kötü bir optimizasyon değil.
supercat

Standart, dizi sınırları dışındaki dizi erişimini tanımlanmamış davranış olarak tanımlıyorsa, yapı saldırısı da öyle. Ancak standart, dizi erişimini işaretçi aritmetiği ( a[2] == a + 2) için sözdizimsel şeker olarak tanımlıyorsa, bu tanımlamaz . Eğer haklıysam, tüm C standartları dizi erişimini işaretçi aritmatik olarak tanımlar.
yyny

13

Evet, tanımlanmamış bir davranış.

C Dil Kusur Raporu # 051, bu soruya kesin bir cevap verir:

Deyim, yaygın olmakla birlikte, kesinlikle uymuyor

http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html

C99 Gerekçe belgesinde C Komitesi şunları ekler:

Bu yapının geçerliliği her zaman şüpheli olmuştur. Bir Kusur Raporuna yanıt olarak, Komite bunun tanımlanmamış bir davranış olduğuna karar verdi, çünkü p-> öğeleri dizisi, alanın var olup olmadığına bakılmaksızın yalnızca bir öğe içeriyordu.


2
Bunu bulmak için +1, ancak yine de çelişkili olduğunu iddia ediyorum. Aynı nesneye iki işaretçi (bu durumda, verilen bayt) eşittir ve ona bir işaretçi (ile elde edilen tüm nesnenin temsil dizisine işaretçi malloc) toplamada geçerlidir, öyleyse özdeş işaretçi nasıl olabilir, başka bir yolla elde edildiyse, ilavede geçersiz mi? UB olduğunu iddia etmek isteseler bile, bu oldukça anlamsız, çünkü bir uygulamanın iyi tanımlanmış kullanım ile sözde tanımlanmamış kullanım arasında ayrım yapmasının sayısal olarak bir yolu yoktur.
R .. GitHub BUZA YARDIM ETMEYİ DUR

C derleyicilerinin sıfır uzunluklu dizilerin bildirimini yasaklamaya başlaması çok kötü; eğer o yasaklanması yönünde birçok derleyiciler onlara "gereken" ile çalışması için herhangi özel işlem yapmak zorunda olmazdı değildi, ama yine de tek eleman diziler için özel durum kodu (örneğin mümkün olurdu *foobir içeriyor tek elemanlı dizi boz, ifade foo->boz[biz()*391]=9;şu şekilde basitleştirilebilir biz(),foo->boz[0]=9;). Ne yazık ki, derleyicilerin reddi sıfır elemanlı diziler, çok sayıda kodun bunun yerine tek elemanlı dizileri kullandığı ve bu optimizasyonla bozulacağı anlamına gelir.
supercat

11

Bunu yapmanın bu özel yolu, herhangi bir C standardında açıkça tanımlanmamıştır, ancak C99, dilin bir parçası olarak "struct hack" i içerir. C99'da, bir yapının son üyesi, char foo[](yerine istediğiniz türden) olarak bildirilen bir "esnek dizi üyesi" olabilir char.


Bilgiçlikçi olmak için, bu yapı hilesi değil. Yapı hacklemesi, esnek bir dizi üyesi değil, sabit boyutlu bir dizi kullanır. Yapı hacklemesi sorulan şeydir ve UB'dir. Esnek dizi üyeleri, bu konudaki bu gerçeği şikayet eden halkı yatıştırma girişimi gibi görünüyor.
underscore_d

7

Resmi ya da başka birinin ne söylediğine bakılmaksızın tanımsız bir davranış değildir , çünkü standart tarafından tanımlanmıştır. p->s, bir değer olarak kullanılması dışında, özdeş bir gösterici olarak değerlendirilir (char *)p + offsetof(struct T, s). Özellikle, bu, charmalloc'lu nesnenin içinde geçerli bir göstericidir ve hemen ardından char, tahsis edilen nesnenin içindeki nesneler olarak da geçerli olan 100 (veya daha fazla, hizalama hususlarına bağlı olarak) ardışık adres vardır . İmlecin, ->döndürme tarafından döndürülen işaretçiye açıkça eklemek yerine kullanılarak türetilmiş olması malloc, çevirme ile char *, alakasızdır.

Teknik olarak, yapı içindeki dizinin p->s[0]tek öğesidir char, sonraki birkaç öğe (örneğin p->s[1]aracılığıyla p->s[3]) muhtemelen yapı içindeki doldurma baytlarıdır; bu, yapıya bir bütün olarak atama gerçekleştirirseniz bozulabilir, ancak yalnızca bireye erişirseniz değil üyeler ve diğer öğeler, tahsis edilen nesnede, hizalama gereksinimlerine uyduğunuz sürece (ve charhizalama gereksinimleri olmadığı sürece) istediğiniz gibi kullanmakta serbest olduğunuz ek alandır .

Eğer dolgu ile örtüşen olasılığı her nasılsa burun iblisler çağırmak, sen değiştirerek bu önlemek olabilir belki yapı içinde bayt endişe varsa 1içinde [1]yapı sonunda hiçbir doldurma halde kalmasını sağlayan bir değere sahip. Bunu yapmanın basit ama savurgan bir yolu, sonunda hiçbir dizi dışında aynı üyelere sahip bir yapı s[sizeof struct that_other_struct];oluşturmak ve dizi için kullanmak olacaktır. Daha sonra, p->s[i]yapıdaki dizinin bir öğesi ve için yapının i<sizeof struct that_other_structsonunu izleyen bir adreste bir char nesnesi olarak açıkça tanımlanır i>=sizeof struct that_other_struct.

Düzenleme: Aslında, doğru boyutu elde etmek için yukarıdaki numarada, dizinin kendisinin başka bir öğenin dolgusunun ortasından ziyade maksimum hizalamayla başlamasını sağlamak için diziden önce her basit türü içeren bir birleşim koymanız gerekebilir. . Yine, bunların hiçbirinin gerekli olduğuna inanmıyorum, ama bunu en paranoyak dil avukatları için öneriyorum.

Düzenleme 2: Standardın başka bir parçası nedeniyle dolgu baytlarıyla örtüşme kesinlikle sorun teşkil etmez. C, eğer iki yapı, elemanlarının bir ilk alt dizisinde uyuşursa, ortak ilk elemanlara herhangi bir tip için bir gösterici aracılığıyla erişilebilmesini gerektirir. Özdeş bir yapı, eğer bir sonucu olarak, struct Tancak daha büyük bir nihai dizi ile beyan edilmiştir, eleman s[0]elemanına denk gerekir s[0]olarak struct Tve bu ilave elemanların varlığı etkilemez olabilir ya da daha büyük bir yapı ortak elemanları erişmek etkilenebilir bir işaretçi kullanarak struct T.


4
İşaretçi aritmetiğinin doğasının alakasız olduğu konusunda haklısınız, ancak dizinin belirtilen boyutunun ötesinde erişim konusunda yanılıyorsunuz . Bkz N1494 (son kamu C1x taslak) bölümünde 6.5.6 paragraf 8 - hatta yapmaya izin yok eklenmesini birden fazla eleman dizinin ilan boyutuna geçmiş bir işaretçi alır ve dahi bunun KQUEUE olamaz sadece bir unsur geçmiş.
zwol

1
@Zack: Nesne bir dizi ise bu doğrudur. Nesne, bir mallocdizi olarak erişilen bir nesne tahsis edilmişse veya öğeleri daha büyük yapının öğelerinin ilk alt kümesi olan daha küçük bir yapıya bir işaretçi aracılığıyla erişilen daha büyük bir yapı ise doğru değildir. vakalar.
R .. GitHub

6
+1 mallocİşaretçi aritmetiği ile erişilebilen bir bellek aralığı ayırmazsa, bunun ne faydası olur? Ve eğer p->s[1]bir tanımlanmış sözdizimsel sonra işaretçi aritmetik için şeker, bu cevap sadece yeniden ortaya koyar olarak standardında mallocyararlıdır. Tartışılacak ne kaldı? :)
Daniel Earwicker

3
İstediğiniz kadar iyi tanımlandığını iddia edebilirsiniz, ancak bu olmadığı gerçeğini değiştirmez. Standart, bir dizinin sınırlarının ötesine erişim konusunda çok nettir ve bu dizinin sınırı öyledir 1. Kesinlikle bu kadar basit.
Orbit'te Hafiflik Yarışları

3
@R .., bence, eşitliği karşılaştıran iki göstericinin aynı şekilde davranması gerektiği varsayımınız yanlış. Düşünün int m[1]; int n[1]; if(m+1 == n) m[1] = 0;varsayarak ifşube girilir. Bu, nokuduğum 6.5.6 p8'e (son cümle) göre UB'dir (ve başlatılması garanti edilmez ). İlgili: 6.5.9 p6, dipnot 109. (Referanslar C11 n1570'e aittir.) [...]
mafso

7

Evet, teknik olarak tanımlanmamış bir davranıştır.

"Struct hack" uygulamasını uygulamanın en az üç yolu olduğunu unutmayın:

(1) Sondaki diziyi 0 boyutuyla bildirmek (eski koddaki en "popüler" yol). Bu açık bir şekilde UB'dir, çünkü sıfır boyutlu dizi bildirimleri C'de her zaman geçersizdir. Derleme yapsa bile, dil kısıtlamayı ihlal eden herhangi bir kodun davranışı hakkında hiçbir garanti vermez.

(2) Diziyi minimum yasal boyutta beyan etme - 1 (sizin durumunuz). Bu durumda, işaretçi alma p->s[0]ve onu ötesine geçen işaretçi aritmetiği için kullanma girişimleri p->s[1]tanımsız davranıştır. Örneğin, bir hata ayıklama uygulamasının katıştırılmış aralık bilgisine sahip özel bir işaretçi üretmesine izin verilir; bu, ötesinde bir işaretçi oluşturmaya çalıştığınız her seferinde yakalanacaktır p->s[1].

(3) Diziyi "çok büyük" boyutlu olarak bildirmek Örneğin, 10000 gibi . Buradaki fikir, beyan edilen boyutun gerçek uygulamada ihtiyaç duyabileceğiniz her şeyden daha büyük olması gerektiğidir. Bu yöntem, dizi erişim aralığı açısından UB içermez. Bununla birlikte, pratikte, elbette, her zaman daha küçük miktarda bellek ayıracağız (sadece gerçekten gerektiği kadar). Bunun yasallığından emin değilim, yani nesneye nesnenin beyan edilen boyutundan daha az bellek ayırmanın ne kadar yasal olduğunu merak ediyorum ("tahsis edilmemiş" üyelere asla erişemeyeceğimizi varsayarak).


1
(2) s[1]'de tanımsız davranış değildir. Bu aynı şey *(s+1)aynı olan, *((char *)p + offsetof(struct T, s) + 1)bir için geçerli bir işaretçi olan chartahsis nesnede.
R .. GitHub BUZA YARDIM ETMEYİ DUR

Öte yandan, (3) 'ün tanımsız bir davranış olduğundan neredeyse eminim. Bu adreste bulunan böyle bir yapıya bağlı olan herhangi bir işlemi gerçekleştirdiğinizde, derleyici yapının herhangi bir bölümünden okuyan makine kodu üretmekte özgürdür. Yararsız olabilir veya katı tahsis kontrolü için bir güvenlik özelliği olabilir, ancak bir uygulamanın bunu yapmaması için hiçbir neden yoktur.
R .. GitHub BUZA YARDIM ETMEYİ DUR

R: Bir dizinin bir boyuta sahip olduğu bildirildiyse (yalnızca foo[]sözdizimsel şekeri değil *foo), o zaman işaretçi aritmetiğinin nasıl yapıldığına bakılmaksızın, bildirilen boyutundan ve tahsis edilen boyutundan daha küçük olanın ötesinde herhangi bir erişim UB'dir.
zwol

1
@Zack, birkaç konuda yanılıyorsun. foo[]bir yapıda sözdizimsel şeker değildir *foo; bir C99 esnek dizi üyesidir. Geri kalanı için cevabımı ve diğer cevaplarla ilgili yorumlarımı görün.
R .. GitHub BUZA YARDIM ETMEYİ DURDUR

6
Sorun komitesinin bazı üyeleri umutsuzca olmasıdır istiyorum bir C uygulaması işaretçi sınırlarını zorlamak olabilecek bazı masallar ülkesi öngörülüyor, çünkü bu "hack" UB olmak. Daha iyisi veya daha kötüsü için, bunu yapmak standardın diğer bölümleriyle çelişecektir - eşitlik için işaretçileri karşılaştırma yeteneği (sınırlar işaretçinin kendisinde kodlanmışsa) veya herhangi bir nesneye hayali bir örtüşme unsigned char [sizeof object]dizisi aracılığıyla erişilebilir olma gerekliliği gibi şeyler. . Ön-C99 için esnek dizi üyesinin "hacklenmesi" nin iyi tanımlanmış davranışa sahip olduğu iddiamın arkasında duruyorum.
R .. GitHub BUZA YARDIM ETMEYİ DUR

3

Standart, bir dizinin sonundaki şeylere erişemeyeceğiniz konusunda oldukça açıktır. (ve dizi bittikten sonra işaretçileri birden fazla artırmanıza izin verilmediğinden işaretçiler üzerinden gitmek yardımcı olmaz).

Ve "pratikte çalışmak" için. Standardın bu bölümünü kullanan gcc / g ++ optimizer'ı gördüm, bu nedenle bu geçersiz C'yi karşılarken yanlış kod üretiyordum.


bir örnek verebilir misin?
Tal

1

Bir derleyici aşağıdaki gibi bir şeyi kabul ederse

typedef struct {
  int len;
  char dat [];
};

Sanırım, uzunluğunun ötesinde 'dat' üzerindeki bir alt simgeyi kabul etmeye hazır olması gerektiği oldukça açık. Öte yandan, biri aşağıdaki gibi bir şeyi kodlarsa:

typedef struct {
  int her neyse;
  char dat [1];
} MY_STRUCT;

ve daha sonra somestruct-> dat [x]; Derleyicinin büyük x değerleriyle çalışacak adres hesaplama kodunu kullanma yükümlülüğü altında olduğunu düşünmüyorum. Bence biri gerçekten güvende olmak isterse, uygun paradigma daha çok şöyle olurdu:

#define LARGEST_DAT_SIZE 0xF000
typedef struct {
  int her neyse;
  char dat [LARGEST_DAT_SIZE];
} MY_STRUCT;

ve sonra (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + istenilen_array_length) baytlık bir malloc yapın (eğer istenen_array_length LARGEST_DAT_SIZE'den daha büyükse, sonuçların tanımlanmamış olabileceğini aklınızda bulundurun).

Bu arada, sıfır uzunluklu dizileri yasaklama kararının talihsiz bir karar olduğunu düşünüyorum (Turbo C gibi bazı eski lehçeler bunu destekliyor) çünkü sıfır uzunluklu bir dizi, derleyicinin daha büyük indekslerle çalışacak kod üretmesi gerektiğinin bir işareti olarak kabul edilebilir. .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.