Neden bu hak talebinde bulunulmayan tür cezalandırılan işaretçi uyarı derleyicisine özel?


38

Okuduğum çeşitli mesajları üzerinde yığın taşması RE: derefercing tip punned işaretçi hatası. Anladığım kadarıyla, hata aslında bir nesneye farklı tipte bir işaretçi (bir istisna yapılmış gibi gözükse de char*) üzerinden erişilme tehlikesinin anlaşılabilir ve makul bir uyarı olduğu uyarısıdır.

Benim sorum aşağıdaki koda özgüdür: neden bir işaretçi adresi void**bu uyarı için bir kalifiye (hata yoluyla terfi) için döküm -Werror?

Dahası, bu kod, yalnızca biri uyarı / hata oluşturan birden fazla hedef mimarisi için derlenmiştir - bu, yasal olarak derleyici sürümüne özgü bir eksiklik anlamına gelebilir mi?

// main.c
#include <stdlib.h>

typedef struct Foo
{
  int i;
} Foo;

void freeFunc( void** obj )
{
  if ( obj && * obj )
  {
    free( *obj );
    *obj = NULL;
  }
}

int main( int argc, char* argv[] )
{
  Foo* f = calloc( 1, sizeof( Foo ) );
  freeFunc( (void**)(&f) );

  return 0;
}

Yukarıda belirtilen anlayışım doğruysa, a void**, sadece bir işaretçi olarak, bu güvenli bir döküm olmalıdır.

Bu derleyiciye özgü uyarıyı / hatayı pasifize edecek lvalues ​​kullanmayan bir geçici çözüm var mı ? Yani anlıyorum ve neden bu sorunu çözecektir, ancak freeFunc() NULL amaçlanan bir arg-arg yararlanmak istiyorum çünkü bu yaklaşımdan kaçınmak istiyorum :

void* tmp = f;
freeFunc( &tmp );
f = NULL;

Problem derleyici (bir tanesinden):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc --version && /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-fc3-linux-gnu-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

./main.c: In function `main':
./main.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules

user@8d63f499ed92:/build$

Şikayet etmeyen derleyici (çok sayıda):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc --version && /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-rh73-linux-gnu-gcc (GCC) 3.2.3
Copyright (C) 2002 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

user@8d63f499ed92:/build$

Güncelleme: Uyarının özellikle derlendiğinde oluşturulduğunu fark ettim -O2(yalnızca belirtilen "sorun derleyicisiyle")



1
"a void**, hala sadece bir işaretçi olarak, bu güvenli bir döküm olmalıdır." Vay be atla! Bazı temel varsayımlarınız var gibi görünüyor. Baytlar ve kaldıraçlar açısından daha az ve soyutlamalar açısından daha fazla düşünmeye çalışın, çünkü aslında programladığınız şey budur
Orbit'teki Lightness Races

7
Teğet olarak, kullandığınız derleyiciler 15 ve 17 yaşında! İkisine de güvenmem.
Tavian Barnes

4
@TavianBarnes Ayrıca, herhangi bir nedenle GCC 3'e güvenmeniz gerekiyorsa, 3.4.6 olan lif sonu sonu sürümünü kullanmak en iyisidir. Dinlenmek için serilmeden önce bu serinin tüm mevcut düzeltmelerinden neden yararlanmıyorsunuz?
Kaz

Ne tür C ++ kodlama standardı tüm bu alanları reçete eder?
Peter Mortensen

Yanıtlar:


33

Tür değeri, türün void**bir nesnesinin göstergesidir void*. Tür nesnesi tür nesnesi Foo*değildir void*.

Ve türü değerleri arasında örtük bir dönüşüm var . Bu dönüşüm, değerin temsilini değiştirebilir. Benzer şekilde, yazabilirsiniz ve bu, değerin ayarlanması için iyi tanımlanmış bir davranışa sahiptir , ancak tanımlanmamış bir davranışa sahiptir (ve pratikte herhangi bir ortak mimari için bir "işaretçi " olarak ayarlanmaz ).Foo*void*int n = 3; double x = n;x3.0double *p = (double*)&n;p3.0

Günümüzde, nesnelerin farklı göstergelerinin farklı temsili olduğu mimariler nadirdir, ancak C standardı tarafından izin verilir. Hafızadaki bir sözcüğün adresleri olan kelime işaretçileri olan ve bu sözcükteki bir bayt uzaklığı ile birlikte bir sözcüğün adresleri olan bayt işaretçileri olan (nadir) eski makineler vardır ; Foo*bir kelime işaretçisi ve void*bu mimarilerde bir bayt işaretçisi olurdu. Sadece nesnenin adresi hakkında değil, aynı zamanda türü, boyutu ve erişim kontrol listeleri hakkında bilgi içeren (nadir) yağ işaretçileri olan makineler vardır ; belirli bir türün işaretçisi void*, çalışma zamanında ek tür bilgisi gerektiren bir göstergeden farklı bir temsile sahip olabilir .

Bu tür makineler nadirdir, ancak C standardı tarafından izin verilir. Ve bazı C derleyicileri, kodu en iyi duruma getirmek için türden işaretlenmiş işaretçileri farklı muamele etme izninden yararlanır. İşaretçilerin örtüşmesi riski, bir derleyicinin kodu optimize etme yeteneği için büyük bir sınırlamadır, bu nedenle derleyiciler bu izinlerden yararlanma eğilimindedir.

Bir derleyici, yanlış bir şey yaptığınızı söylemek veya istemediğiniz şeyi sessizce yapmak veya istediğiniz şeyi sessizce yapmak için ücretsizdir. Tanımsız davranış bunlardan herhangi birine izin verir.

freefuncBir makro yapabilirsiniz :

#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)

Bu, makroların olağan sınırlamaları ile birlikte gelir: tip güvenliği eksikliği, piki kez değerlendirilir. Bunun yalnızca p, serbest bırakılmış nesnenin tek işaretçisi olsaydı , sarkan işaretçileri bırakmamanın güvenliğini sağladığını unutmayın .


1
Ve mimariniz üzerinde aynı temsile sahip olsa da Foo*ve bunları void*temsil ediyor olsanız bile , onları cezalandırmak için hala tanımsız olduğunu bilmek güzel .
Tavian Barnes

12

A void *, kısmen C standardına göre muamele görür, çünkü eksik tipe işaret eder. Bu tedavi yok değil uzanır void **o kadar yapar özel olarak ise, komple türüne noktası void *.

Katı takma kuralları, bir türdeki bir işaretçiyi başka bir işaretçiye dönüştüremeyeceğinizi ve daha sonra bu işaretçiyi serbest bırakacağınızı, çünkü bunu yapmak bir türün baytlarını başka bir şekilde yeniden yorumlamak anlamına gelir. Tek istisna, bir nesnenin temsilini okumanıza izin veren bir karakter türüne dönüştürmektir.

Bir işlev yerine işlev benzeri bir makro kullanarak bu sınırlamayı aşabilirsiniz:

#define freeFunc(obj) (free(obj), (obj) = NULL)

Buna şöyle diyebilirsiniz:

freeFunc(f);

Bununla birlikte, bunun bir sınırlaması vardır, çünkü yukarıdaki makro objiki kez değerlendirilecektir . GCC kullanıyorsanız, bazı typeofanahtar kelimelerle , özellikle anahtar kelime ve ifade ifadeleriyle bu önlenebilir :

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })

3
+1, amaçlanan davranışın daha iyi uygulanmasını sağlar. Benim gördüğüm tek sorun #define, bunun objiki kez değerlendirilmesi . Yine de ikinci değerlendirmeden kaçınmanın iyi bir yolunu bilmiyorum. Bir ifade ifadesi (GNU uzantısı) bile obj, değerini kullandıktan sonra atamanız gereken hile yapmaz .
cmaster

2
@cmaster: Böyle deyimi ifadeler olarak GNU uzantıları kullanmak için istekli iseniz, o zaman kullanabilirsiniz typeofdeğerlendirerek önlemek için objiki kez: #define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; }).
ruakh

@ruakh Çok güzel :-) Eğer dbush bu cevabı düzenleyecek olsaydı harika olurdu, bu yüzden yorumlarla toplu silinmez.
cmaster

9

Bir tür ceza işaretçisi silme işlemi UB'dir ve ne olacağına güvenemezsiniz.

Farklı derleyiciler farklı uyarılar üretir ve bu amaçla aynı derleyicinin farklı sürümleri farklı derleyiciler olarak kabul edilebilir. Bu, gördüğünüz varyans için mimariye bağımlı olmaktan daha iyi bir açıklama gibi görünüyor.

Bu durumda punning tipinin neden kötü olabileceğini anlamanıza yardımcı olabilecek bir durum, işlevinizin hangi mimaride çalışmayacağıdır sizeof(Foo*) != sizeof(void*). Bunun geçerli olduğu herhangi bir geçerli bilmiyorum, ancak standart tarafından yetkilendirildi.

Çözüm, işlev yerine makro kullanmak olacaktır.

freeBoş göstergeleri kabul ettiğine dikkat edin .


2
Bunun mümkün olması büyüleyici sizeof Foo* != sizeof void*. "Vahşi doğada" işaretçi boyutlarının türe bağlı olduğunu hiç görmedim, bu yüzden yıllar boyunca, işaretçi boyutlarının belirli bir mimaride aynı olduğunu aksiyomatik olarak düşünmeye başladım.
StoneThrow

1
@Standart örnek, kelime adreslenebilir mimari içindeki baytları adreslemek için kullanılan yağ işaretçileridir. Ama mevcut kelime adreslenebilir makineleri alternatif sizeof char == sizeof word kullanın düşünüyorum .
AProgrammer

2
Tür boyutu için parantez içinde olması gerektiğini unutmayın ...
Antti Haapala

@StoneThrow: İşaretçi boyutlarından bağımsız olarak, türe dayalı diğer ad analizi onu güvensiz yapar; Bu derleyiciler bir kullanarak bir mağaza varsayarak optimize yardımcı olan float*bir değiştirmez int32_tböylece örneğin, nesne int32_t*gerekmez int32_t *restrict ptraynı belleğe işaret etmiyor varsayıyoruz derleyici için. void**Bir Foo*nesneyi değiştirmediği varsayılan bir varlık aracılığıyla depolar için de aynı şey geçerlidir .
Peter Cordes

4

Bu kod C Standardı uyarınca geçersizdir, bu nedenle bazı durumlarda çalışabilir, ancak taşınabilir olması gerekmez.

Farklı bir işaretçi türüne atanmış bir işaretçi aracılığıyla bir değere erişmek için "katı diğer adlandırma kuralı" 6.5 paragraf 7'de bulunur:

Bir nesnenin depolanmış değerine yalnızca aşağıdaki türlerden birine sahip bir değer değeri ifadesiyle erişilir:

  • nesnenin etkili türüyle uyumlu bir tür,

  • nesnenin etkili türüyle uyumlu bir türün nitelikli bir sürümü,

  • nesnenin etkili türüne karşılık gelen imzalı veya imzasız tür olan bir tür,

  • etkin nesne türünün nitelikli bir sürümüne karşılık gelen imzalı veya imzasız tür olan bir tür,

  • üyeleri arasında yukarıda belirtilen türlerden birini içeren bir toplu veya sendika türü (özyineli olarak, bir alt kümenin veya içerdiği birliğin bir üyesi dahil) veya

  • karakter türü.

Senin içinde *obj = NULL;açıklamaya nesne etkili türü vardır Foo*ama lvalue ifade ile erişilir *objtipiyle void*.

6.7.5.1 paragraf 2'de,

İki işaretçi türünün uyumlu olması için her ikisi de aynı nitelikte olmalı ve her ikisi de uyumlu türlere işaret etmelidir.

Bu nedenle void*ve Foo*uyumlu türler veya niteleyiciler eklenmiş uyumlu türler değildir ve kesinlikle katı takma kuralının diğer seçeneklerine uymaz.

Kodun geçersiz olmasının teknik nedeni olmasa da, bölüm 6.2.5 paragraf 26'ya da dikkat edilmelidir:

Bir işaretçi için voidbir karakter tipi için bir işaretçi olarak, aynı gösterim ve hizalama gereksinimleri sahip olacaktır. Benzer şekilde, uyumlu tiplerin nitelikli veya niteliksiz versiyonlarına işaretçiler aynı temsil ve hizalama gerekliliklerine sahip olacaktır. Yapı türlerine yönelik tüm işaretçiler, birbirleriyle aynı temsil ve hizalama gereksinimlerine sahip olmalıdır. Birlik tiplerine yönelik tüm işaretçiler, birbirleriyle aynı temsil ve hizalama gerekliliklerine sahip olmalıdır. Diğer türlere işaretçiler aynı gösterim veya hizalama gereksinimlerine sahip olmak zorunda değildir.

Uyarılardaki farklılıklara gelince, bu Standart'ın bir teşhis mesajı gerektirdiği bir durum değildir, bu yüzden derleyicinin veya versiyonunun potansiyel sorunları fark etmek ve yararlı bir şekilde belirtmek konusunda ne kadar iyi olduğu meselesidir. Optimizasyon ayarlarının bir fark yaratabileceğini fark ettiniz. Bunun nedeni genellikle, programın çeşitli parçalarının pratikte gerçekte nasıl bir araya getirildiği hakkında daha fazla bilgi üretilmesidir ve bu nedenle uyarı kontrolleri için ek bilgilerin de mevcut olmasıdır.


2

Diğer cevapların söylediklerinin üstüne, bu C'de klasik bir anti-desen ve ateşle yakılması gereken bir model . Şurada görünür:

  1. Uyarıyı bulduğunuz gibi serbest ve boş işlevler.
  2. Bir hata bayrağı döndürmek ve bir işaretçi-işaretçi aracılığıyla sonucu saklamak yerine , standart C geri dönme deyimini kapatan void *(bu, punning türü yerine bir değer dönüşümü içerdiği için bu sorundan muzdarip değildir) ayırma işlevleri .

(1) 'in başka bir örneği için, ffmpeg / libavcodec'in av_freefonksiyonunda uzun süredir devam eden rezil bir vaka vardı . Sonunda bir makro veya başka bir numara ile düzeltildiğine inanıyorum, ama emin değilim.

(2) için her ikisi de cudaMallocve posix_memalignörneklerdir.

Her iki durumda da arayüz doğal olarak geçersiz kullanım gerektirmez , ancak onu kesinlikle teşvik eder ve doğru kullanımı yalnızca void *serbest ve boş işlevin amacını yenen ve ayırmayı garip hale getiren ekstra geçici bir nesne ile kabul eder .


(1) 'in neden bir anti-desen olduğu hakkında daha fazla bilgi veren bir bağlantınız var mı? Bu duruma / tartışmaya aşina olduğumu sanmıyorum ve daha fazla bilgi edinmek istiyorum.
StoneThrow

1
@StoneThrow: Bu gerçekten basit - amaç, işaretçiyi serbest bırakılan belleğe depolayan nesneyi geçersiz kılarak kötüye kullanımın önlenmesidir, ancak aslında bunu yapabilmenin tek yolu, arayanın işaretçiyi aslında bir nesnede saklamasıdır. yazın void *ve ona bunu KQUEUE istediği her defasında dönüştürme / döküm. Bu pek olası değil. Arayan başka bir işaretçi türü saklıyorsa, işlevi UB'yi çağırmadan çağırmanın tek yolu, işaretçiyi türdeki geçici bir nesneye kopyalamak void *ve adresini serbest bırakma işlevine aktarmaktır.
R .. GitHub BUZA YARDIMCI DURMAK

1
... arayanın işaretçiye sahip olduğu gerçek depolama yerine geçici bir nesneyi geçersiz kılar. Tabii ki gerçekte olan şey, fonksiyonun kullanıcılarının sonuç olarak (void **)tanımlanmamış davranışlar üretmeleri.
R .. GitHub BUZA YARDIMCI DURDUR

2

C, tüm işaretçiler için aynı temsili kullanan makineler için tasarlanmış olsa da, Standardın yazarları, dili farklı nesne türlerine işaretçiler için farklı gösterimler kullanan makinelerde kullanılabilir hale getirmek istediler. Bu nedenle, farklı işaretçiler için farklı işaretçi gösterimleri kullanan makinelerin, birçok makine bunu sıfır maliyetle yapabilmesine rağmen, "herhangi bir işaretçi işaretçisi" türünü desteklemesini gerektirmediler.

Standart yazılmadan önce, tüm işaretçi türleri için aynı temsili kullanan platformlar için uygulamalar, oybirliğiyle void**, en azından uygun dökümde, "herhangi bir işaretçiye işaretçi" olarak kullanılmasına izin verecektir . Standardın yazarları, bunun onu destekleyen platformlarda yararlı olacağını neredeyse kesin olarak kabul ettiler, ancak evrensel olarak desteklenemediğinden, onu zorunlu kılmayı reddetti. Bunun yerine, kalite uygulamasının, mantığın mantıklı olacağı durumlarda Gerekçenin "popüler bir uzantı" olarak tanımlayacağı yapıları işlemesini beklediler.

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.