NULL yeniden tanımlanıyor


118

0x0000 adresinin geçerli olduğu ve bağlantı noktası G / Ç içeren bir sistem için C kodu yazıyorum. Bu nedenle, bir NULL işaretçisine erişen olası hatalar tespit edilemeyecek ve aynı zamanda tehlikeli davranışlara neden olacaktır.

Bu nedenle, NULL'u başka bir adres, örneğin geçerli olmayan bir adres olacak şekilde yeniden tanımlamak istiyorum. Yanlışlıkla böyle bir adrese erişirsem, hatayı halledebileceğim bir donanım kesintisi alacağım. Bu derleyici için stddef.h'ye erişimim var, bu yüzden aslında standart başlığı değiştirebilir ve NULL'u yeniden tanımlayabilirim.

Sorum şu: bu C standardıyla çelişecek mi? Standartta 7.17'den anlayabildiğim kadarıyla makro uygulama tanımlı. Standartta NULL'un 0 olması gerektiğini belirten herhangi bir şey var mı ?

Diğer bir sorun, birçok derleyicinin, veri türü ne olursa olsun her şeyi sıfıra ayarlayarak statik başlatma gerçekleştirmesidir. Standart derleyicinin tamsayıları sıfıra ve işaretçileri NULL olarak ayarlaması gerektiğini söylese de. Derleyicim için NULL'u yeniden tanımlayacaksam, bu tür statik başlatmanın başarısız olacağını biliyorum. Derleyici başlıklarını elle cesurca değiştirmeme rağmen, bunu yanlış derleyici davranışı olarak görebilir miyim? Çünkü bu derleyicinin statik başlatma yaparken NULL makroya erişmediğini kesin olarak biliyorum.


3
Bu gerçekten güzel bir soru. Size bir cevabım yok, ama sormak zorundayım: Geçerli öğelerinizi 0x00'den uzağa taşımanın ve "normal" sistemlerdeki gibi NULL'un geçersiz bir adres olmasına izin vermenin mümkün olmadığından emin misiniz? Yapamazsanız, güvenli bir şekilde geçersiz olan tek adres , tahsis edebileceğinizden emin olabileceğiniz ve daha sonra mprotectgüvenliğini sağlayabileceğiniz adresler olacaktır . Veya platformda ASLR veya benzeri yoksa, platformun fiziksel belleğinin ötesinde adresler. İyi şanslar.
Borealid

8
Kodunuz kullanılıyorsa nasıl çalışacak if(ptr) { /* do something on ptr*/ }? NULL, 0x0'dan farklı olarak tanımlanırsa çalışır mı?
Xavier T.

3
C işaretçisinin bellek adresleriyle zorunlu ilişkisi yoktur. İşaretçi aritmetiğinin kurallarına uyulduğu sürece, bir işaretçi değeri herhangi bir şey olabilir. Çoğu uygulama bellek adreslerini işaretçi değerleri olarak kullanmayı seçer, ancak bir izomorfizm olduğu sürece her şeyi kullanabilirler.
datenwolf

2
@bdonlan Bu, MISRA-C'de de (danışma) kuralları ihlal eder.
Lundin

2
@Andreas Evet bu benim de düşüncelerim. Donanım çalışanlarının, yazılımın çalışması gereken donanımı tasarlamasına izin verilmemelidir! :)
Lundin

Yanıtlar:


84

C standardı, boş işaretçilerin makinenin sıfır adresinde olmasını gerektirmez. ANCAK, 0bir işaretçi değerine bir sabit değer NULLatamak bir işaretçi (§6.3.2.3 / 3) ile sonuçlanmalı ve boş göstericiyi bir boolean olarak değerlendirmek yanlış olmalıdır. Bu gerçekten eğer biraz garip olabilir do sıfır adresi istiyor ve NULLsıfır adresi değil.

Bununla birlikte, derleyicide ve standart kitaplıkta (ağır) değişiklikler yapıldığında NULL, standart kitaplığa tam anlamıyla uyumlu kalırken alternatif bir bit örüntüsü ile temsil edilmek imkansız değildir . Öyle değil sadece tanımını değiştirmek için yeterli NULLsonra da, ancak kendisi NULLgerçek olarak değerlendirilir.

Özellikle şunları yapmanız gerekir:

  • Atamalarda işaretçiler (veya işaretçiler için atmalar) gibi başka bir sihirli değere dönüştürülecek sıfırları düzenleyin -1.
  • 0Bunun yerine sihirli değeri kontrol etmek için işaretçiler ve sabit bir tam sayı arasında eşitlik testleri düzenleyin (§6.5.9 / 6)
  • Sıfır için kontrol etmek yerine sihirli değere eşitliği kontrol etmek için bir işaretçi türünün boole olarak değerlendirildiği tüm bağlamları düzenleyin. Bu eşitlik testi anlambiliminden kaynaklanır, ancak derleyici bunu dahili olarak farklı şekilde uygulayabilir. Bkz. §6.5.13 / 3, §6.5.14 / 3, §6.5.15 / 4, §6.5.3.3 / 5, §6.8.4.1 / 2, §6.8.5 / 4
  • Caf'in belirttiği gibi, yeni boş gösterici temsilini yansıtmak için statik nesnelerin (§6.7.8 / 10) ve kısmi bileşik başlatıcıların (§6.7.8 / 21) başlatılması için anlambilimini güncelleyin.
  • Gerçek sıfır adresine erişmek için alternatif bir yol oluşturun.

Halletmeniz gerekmeyen bazı şeyler var. Örneğin:

int x = 0;
void *p = (void*)x;

Bundan sonra pboş gösterici olacağı garanti EDİLMEZ. Yalnızca sabit atamaların ele alınması gerekir (bu, gerçek sıfır adresine erişmek için iyi bir yaklaşımdır). Aynı şekilde:

int x = 0;
assert(x == (void*)0); // CAN BE FALSE

Ayrıca:

void *p = NULL;
int x = (int)p;

xolması garanti edilmez 0.

Kısacası, bu koşul görünüşe göre C dili komitesi tarafından değerlendirildi ve NULL için alternatif bir temsil seçecek olanlar için değerlendirmeler yapıldı. Şimdi yapmanız gereken tek şey, derleyicinizde büyük değişiklikler yapmak ve hey, işiniz bitti :)

Bir yan not olarak, bu değişiklikleri derleyici uygun olmadan önce bir kaynak kodu dönüştürme aşaması ile uygulamak mümkün olabilir. Yani, önişlemci -> derleyici -> assembler -> bağlayıcının normal akışı yerine, bir önişlemci -> NULL dönüşüm -> derleyici -> assembler -> bağlayıcı eklersiniz. O zaman aşağıdaki gibi dönüşümler yapabilirsiniz:

p = 0;
if (p) { ... }
/* becomes */
p = (void*)-1;
if ((void*)(p) != (void*)(-1)) { ... }

Bu, tam bir C ayrıştırıcısının yanı sıra, bir tür ayrıştırıcısı ve hangi tanımlayıcıların işaretleyicilere karşılık geldiğini belirlemek için tiplerin ve değişken bildirimlerinin analizini gerektirir. Ancak, bunu yaparak, derleyicinin kod oluşturma bölümlerinde uygun şekilde değişiklik yapmak zorunda kalmazsınız. clang bunu uygulamak için yararlı olabilir - bunun gibi dönüşümler düşünülerek tasarlandığını anlıyorum. Elbette standart kitaplıkta da değişiklik yapmanız gerekebilir.


2
Tamam, §6.3.2.3'teki metni bulamadım, ancak bir yerde böyle bir ifade olacağından şüphelendim :). Sanırım bu benim sorumu yanıtlıyor, standart olarak beni desteklemek için yeni bir C derleyicisi yazmak istemiyorsam NULL'u yeniden tanımlamama izin verilmiyor :)
Lundin

2
İyi bir hile, derleyiciyi hacklemektir, böylece işaretçi <-> tamsayı dönüşümleri XOR, geçersiz bir işaretçi olan belirli bir değeri ve hedef mimarinin bunu ucuza yapabilmesi için yeterince önemsizdir (genellikle, bu tek bit setli bir değerdir) , 0x20000000 gibi).
Simon Richter

2
Derleyicide değiştirmeniz gereken başka bir şey, bileşik tipli nesnelerin ilklendirilmesidir - eğer bir nesne kısmen başlatılmışsa, o zaman açık bir başlatıcının bulunmadığı herhangi bir işaretleyicinin başlatılması gerekir NULL.
caf

20

Standart, 0 değerine sahip bir tamsayı sabiti ifadesinin veya void *türe dönüştürülmüş böyle bir ifadenin bir boş gösterici sabiti olduğunu belirtir . Bu araçlar (void *)0her zaman bir boş gösterici olmakla birlikte, verilen int i = 0;, (void *)igerek olmayabilir.

C uygulaması, başlıkları ile birlikte derleyiciden oluşur. Başlıkları yeniden tanımlayacak NULLşekilde değiştirir, ancak derleyiciyi statik başlatmaları düzeltecek şekilde değiştirmezseniz, uygun olmayan bir uygulama oluşturmuş olursunuz. Yanlış davranışa sahip olan tüm uygulama birlikte ele alınır ve eğer onu bozduysanız, gerçekten suçlayacak kimseniz yoktur;)

Bir işaretçi verilen - Elbette, sadece statik alıştırmalarını daha düzeltmek gerekir p, if (p)eşdeğerdir if (p != NULL)nedeniyle yukarıdaki kurala.


8

C std kitaplığını kullanırsanız, NULL döndürebilen işlevlerle ilgili sorunlarla karşılaşırsınız. Örneğin malloc dokümantasyonu şunları belirtir:

İşlev, istenen bellek bloğunu ayıramazsa, bir boş gösterici döndürülür.

Malloc ve ilgili işlevler zaten belirli bir NULL değeriyle ikili dosyalar halinde derlendiğinden, NULL'u yeniden tanımlarsanız, C std kitaplıkları da dahil olmak üzere tüm araç zincirinizi yeniden oluşturamazsanız, C std kitaplığını doğrudan kullanamazsınız.

Ayrıca std kitaplığının NULL kullanması nedeniyle, std başlıklarını dahil etmeden önce NULL'u yeniden tanımlarsanız, başlıklarda listelenen bir NULL tanımının üzerine yazabilirsiniz. Satır içi herhangi bir şey, derlenen nesnelerden tutarsız olacaktır.

Bunun yerine, kendi kullanımlarınız için kendi NULL'unuzu, "MYPRODUCT_NULL" tanımlar ve C std kitaplığından / kitaplığından kaçınır veya buna çeviririm.


6

NULL'u yalnız bırakın ve IO'yu 0x0000 bağlantı noktasına özel bir durum olarak ele alın, belki assembler'da yazılmış bir yordam kullanın ve bu nedenle standart C semantiğine tabi değildir. IOW, NULL'u yeniden tanımlamayın, 0x00000 bağlantı noktasını yeniden tanımlayın.

Bir C derleyicisini yazıyor veya değiştiriyorsanız, NULL referansını geri almaktan kaçınmak için gereken işin (sizin durumunuzda CPU'nun yardımcı olmadığını varsayarak) NULL nasıl tanımlanırsa tanımlansın aynıdır, bu nedenle NULL tanımlı bırakmak daha kolaydır. sıfır olarak ayarlayın ve sıfırın C'den hiçbir zaman ayrılamayacağından emin olun.


Sorun, bağlantı noktasına kasıtlı olarak erişildiğinde değil, yalnızca NULL'a yanlışlıkla erişildiğinde ortaya çıkacaktır. O zaman neden G / Ç bağlantı noktasını yeniden tanımlayayım? Zaten olması gerektiği gibi çalışıyor.
Lundin

2
Yanlışlıkla ya da değil @Lundin, NULL olabilir ancak kullanarak bir C programında indirgenmedikleri edilmesi *p, p[]veya p()derleyici yalnızca IO portu 0x0000 korumak için bu önem gerekiyor bu yüzden.
Apalala

@Lundin Sorunuzun ikinci kısmı: C içinden sıfır adresine erişimi kısıtladığınızda, 0x0000 bağlantı noktasına ulaşmak için başka bir yola ihtiyacınız var. Assembler'da yazılmış bir fonksiyon bunu yapabilir. C içinden, bağlantı noktası 0xFFFF veya herhangi bir şekilde eşlenebilir, ancak en iyisi bir işlev kullanmak ve bağlantı noktası numarasını unutmaktır.
Apalala

3

Başkalarının da bahsettiği gibi NULL'u yeniden tanımlamanın aşırı zorluğunu göz önünde bulundurursak, iyi bilinen donanım adresleri için başvuruyu yeniden tanımlamak belki daha kolaydır . Bir adres oluştururken, iyi bilinen her adrese 1 ekleyin, böylece iyi bilinen IO bağlantı noktanız şöyle olur:

  #define CREATE_HW_ADDR(x)(x+1)
  #define DEREFERENCE_HW_ADDR(x)(*(x-1))

  int* wellKnownIoPort = CREATE_HW_ADDR(0x00000000);

  printf("IoPortIs" DEREFERENCE_HW_ADDR(wellKnownIoPort));

İlgilendiğiniz adresler bir arada gruplandıysa ve adrese 1 eklemenin hiçbir şeyle çakışmayacağını hissedebiliyorsanız (çoğu durumda olmaması gerekir), bunu güvenle yapabilirsiniz. Ve sonra araç zincirinizi / std kitaplığınızı ve ifadelerinizi şu biçimde yeniden oluşturma konusunda endişelenmenize gerek kalmaz:

  if (pointer)
  {
     ...
  }

Hala çalışmak

Çılgınca biliyorum, ama fikri oraya fırlatacağımı düşündüm :).


Sorun, bağlantı noktasına kasıtlı olarak erişildiğinde değil, yalnızca NULL'a yanlışlıkla erişildiğinde ortaya çıkacaktır. O zaman neden G / Ç bağlantı noktasını yeniden tanımlayayım? Zaten olması gerektiği gibi çalışıyor.
Lundin

@LundIn Sanırım hangisinin daha acı verici olduğunu seçmeniz gerekiyor, tüm araç zincirini yeniden yapılandırmak veya kodunuzun bu bir bölümünü değiştirmek.
Doug T.

2

Boş göstericinin bit örüntüsü, tamsayı 0 için bit örüntüsü ile aynı olmayabilir. Ancak NULL makrosunun genişletilmesi, dönüştürülebilecek 0 değerinin sabit bir tamsayısı olan bir boş işaretçi sabiti olmalıdır (void *).

Uyumlu kalarak istediğiniz sonucu elde etmek için, takım zincirinizi değiştirmeniz (veya belki de yapılandırmanız) gerekir, ancak bu elde edilebilir.


1

Bela istiyorsun. NULLBoş olmayan bir değere yeniden tanımlama bu kodu bozar:

   eğer (myPointer)
   {
      // myPointer boş değil
      ...
   }
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.