Mendefinisikan ulang NULL


118

Saya menulis kode C untuk sistem di mana alamat 0x0000 valid dan berisi port I / O. Oleh karena itu, setiap kemungkinan bug yang mengakses pointer NULL akan tetap tidak terdeteksi dan pada saat yang sama menyebabkan perilaku berbahaya.

Untuk alasan ini saya ingin mendefinisikan kembali NULL menjadi alamat lain, misalnya alamat yang tidak valid. Jika saya tidak sengaja mengakses alamat seperti itu, saya akan mendapatkan interupsi perangkat keras di mana saya dapat menangani kesalahan tersebut. Saya kebetulan memiliki akses ke stddef.h untuk kompiler ini, jadi saya benar-benar dapat mengubah header standar dan mendefinisikan ulang NULL.

Pertanyaan saya adalah: apakah ini akan bertentangan dengan standar C? Sejauh yang saya tahu dari 7.17 dalam standar, makro adalah implementasi-didefinisikan. Apakah ada hal lain dalam standar yang menyatakan bahwa NULL harus 0?

Masalah lainnya adalah banyak kompiler melakukan inisialisasi statis dengan mengatur semuanya ke nol, apa pun tipe datanya. Meskipun standar mengatakan bahwa kompilator harus mengatur bilangan bulat ke nol dan penunjuk ke NULL. Jika saya akan mendefinisikan ulang NULL untuk kompiler saya, maka saya tahu bahwa inisialisasi statis seperti itu akan gagal. Dapatkah saya menganggapnya sebagai perilaku kompilator yang salah meskipun saya dengan berani mengubah header kompiler secara manual? Karena saya tahu pasti bahwa kompilator khusus ini tidak mengakses makro NULL ketika melakukan inisialisasi statis.


3
Ini pertanyaan yang sangat bagus. Saya tidak punya jawaban untuk Anda, tetapi saya harus bertanya: apakah Anda yakin tidak mungkin untuk memindahkan barang-barang Anda yang valid pada jarak 0x00 dan membiarkan NULL menjadi alamat yang tidak valid seperti dalam sistem "normal"? Jika Anda tidak bisa, maka satu-satunya alamat aman tidak valid untuk digunakan akan yang Anda dapat yakin Anda dapat mengalokasikan dan kemudian mprotectuntuk mengamankan. Atau, jika platform tidak memiliki ASLR atau sejenisnya, alamat di luar memori fisik platform. Semoga berhasil.
Borealid

8
Bagaimana cara kerjanya jika kode Anda menggunakan if(ptr) { /* do something on ptr*/ }? Apakah akan berhasil jika NULL didefinisikan berbeda dari 0x0?
Xavier T.

3
Pointer C tidak memiliki hubungan paksa dengan alamat memori. Selama aturan aritmatika pointer dipatuhi, nilai pointer bisa berupa apa saja. Sebagian besar implementasi memilih untuk menggunakan alamat memori sebagai nilai penunjuk, tetapi mereka dapat menggunakan apa saja selama itu isomorfisme.
datenwolf

2
@bdonlan Itu akan melanggar aturan (penasehat) di MISRA-C juga.
Lundin

2
@Andreas Yap itu pikiranku juga. Orang perangkat keras seharusnya tidak diizinkan untuk merancang perangkat keras yang harus dijalankan oleh perangkat lunak! :)
Lundin

Jawaban:


84

Standar C tidak memerlukan penunjuk nol untuk berada di alamat mesin nol. NAMUN, mentransmisikan 0konstanta ke nilai pointer harus menghasilkan NULLpointer (§6.3.2.3 / 3), dan mengevaluasi pointer nol sebagai boolean harus salah. Ini bisa sedikit canggung jika Anda benar - benar menginginkan alamat nol, dan NULLbukan alamat nol.

Namun demikian, dengan modifikasi (berat) pada kompilator dan pustaka standar, bukan tidak mungkin untuk NULLdirepresentasikan dengan pola bit alternatif sambil tetap benar-benar sesuai dengan pustaka standar. Namun, tidak cukup hanya mengubah definisi NULLitu sendiri, karena kemudian NULLmengevaluasi menjadi benar.

Secara khusus, Anda perlu:

  • Atur nol literal dalam tugas ke pointer (atau cast ke pointer) untuk diubah menjadi beberapa nilai ajaib lainnya seperti -1.
  • Atur uji kesetaraan antara pointer dan bilangan bulat konstan 0untuk memeriksa nilai ajaib sebagai gantinya (§6.5.9 / 6)
  • Atur semua konteks di mana tipe penunjuk dievaluasi sebagai boolean untuk memeriksa kesetaraan dengan nilai ajaib alih-alih memeriksa nol. Ini mengikuti semantik pengujian kesetaraan, tetapi kompilator dapat mengimplementasikannya secara berbeda secara internal. Lihat §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
  • Seperti yang ditunjukkan caf, perbarui semantik untuk inisialisasi objek statis (§6.7.8 / 10) dan inisialisasi gabungan parsial (§6.7.8 / 21) untuk mencerminkan representasi penunjuk null baru.
  • Buat cara alternatif untuk mengakses alamat benar nol.

Ada beberapa hal yang tidak harus Anda tangani. Sebagai contoh:

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

Setelah ini, pTIDAK dijamin akan menjadi pointer nol. Hanya tugas konstan yang perlu ditangani (ini adalah pendekatan yang baik untuk mengakses alamat benar nol). Juga:

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

Juga:

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

xtidak dijamin 0.

Singkatnya, kondisi ini rupanya dipertimbangkan oleh komite bahasa C, dan pertimbangan dibuat bagi mereka yang akan memilih representasi alternatif untuk NULL. Yang harus Anda lakukan sekarang adalah membuat perubahan besar pada kompiler Anda, dan hai, presto Anda sudah selesai :)

Sebagai catatan tambahan, dimungkinkan untuk mengimplementasikan perubahan ini dengan tahap transformasi kode sumber sebelum kompiler tepat. Artinya, alih-alih aliran normal preprocessor -> compiler -> assembler -> linker, Anda akan menambahkan preprocessor -> transformasi NULL -> compiler -> assembler -> linker. Kemudian Anda bisa melakukan transformasi seperti:

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

Ini akan membutuhkan parser C lengkap, serta parser tipe dan analisis typedefs dan deklarasi variabel untuk menentukan pengidentifikasi mana yang sesuai dengan pointer. Namun, dengan melakukan ini, Anda dapat menghindari keharusan untuk membuat perubahan pada bagian pembuatan kode dari kompilator yang sesuai. dentang mungkin berguna untuk mengimplementasikan ini - saya mengerti itu dirancang dengan transformasi seperti ini dalam pikiran. Anda mungkin masih perlu melakukan perubahan pada pustaka standar juga tentunya.


2
Oke, saya belum menemukan teks di §6.3.2.3, tapi saya curiga akan ada pernyataan seperti itu di suatu tempat :). Saya kira ini menjawab pertanyaan saya, dengan standar saya tidak diizinkan untuk mendefinisikan ulang NULL kecuali saya suka menulis kompiler C baru untuk mendukung saya :)
Lundin

2
Trik yang baik adalah meretas kompiler sehingga pointer <-> konversi integer XOR nilai tertentu yang merupakan pointer tidak valid dan masih cukup sepele sehingga arsitektur target dapat melakukannya dengan murah (biasanya, itu akan menjadi nilai dengan set bit tunggal , seperti 0x20000000).
Simon Richter

2
Hal lain yang perlu Anda ubah dalam kompilator adalah inisialisasi objek dengan tipe gabungan - jika sebuah objek diinisialisasi sebagian, maka setiap pointer yang tidak ada initaliser eksplisit harus diinisialisasi NULL.
kafe

20

Standar menyatakan bahwa ekspresi konstanta integer dengan nilai 0, atau ekspresi semacam itu dikonversi ke void *tipe, adalah konstanta penunjuk nol. Ini berarti bahwa (void *)0pointer selalu null, tetapi diberikan int i = 0;, (void *)itidak perlu.

Implementasi C terdiri dari kompilator bersama dengan headernya. Jika Anda mengubah tajuk untuk didefinisikan ulang NULL, tetapi tidak mengubah kompiler untuk memperbaiki inisialisasi statis, Anda telah membuat implementasi yang tidak sesuai. Ini adalah keseluruhan implementasi yang disatukan yang memiliki perilaku yang salah, dan jika Anda melanggarnya, Anda benar-benar tidak punya orang lain untuk disalahkan;)

Anda harus memperbaiki lebih dari sekedar inisialisasi statis, tentu saja - diberi pointer p, if (p)sama dengan if (p != NULL), karena aturan di atas.


8

Jika Anda menggunakan pustaka C std, Anda akan mengalami masalah dengan fungsi yang dapat mengembalikan NULL. Misalnya dokumentasi malloc menyatakan:

Jika fungsi gagal mengalokasikan blok memori yang diminta, pointer nol dikembalikan.

Karena malloc dan fungsi terkait telah dikompilasi ke dalam biner dengan nilai NULL tertentu, jika Anda mendefinisikan ulang NULL, Anda tidak akan dapat langsung menggunakan pustaka C std kecuali Anda dapat membangun kembali seluruh rantai alat Anda, termasuk C std libs.

Juga karena penggunaan NULL di perpustakaan std, jika Anda mendefinisikan ulang NULL sebelum menyertakan header std, Anda mungkin menimpa definisi NULL yang terdaftar di header. Apa pun yang sebaris akan menjadi tidak konsisten dari objek yang dikompilasi.

Saya malah akan mendefinisikan NULL Anda sendiri, "MYPRODUCT_NULL", untuk penggunaan Anda sendiri dan baik menghindari atau menerjemahkan dari / ke perpustakaan C std.


6

Biarkan NULL sendiri dan perlakukan IO ke port 0x0000 sebagai kasus khusus, mungkin menggunakan rutin yang ditulis dalam assembler, dan dengan demikian tidak tunduk pada semantik C standar. IOW, jangan tentukan ulang NULL, tentukan ulang port 0x00000.

Perhatikan bahwa jika Anda menulis atau memodifikasi kompiler C, pekerjaan yang diperlukan untuk menghindari dereferensi NULL (dengan asumsi bahwa dalam kasus Anda CPU tidak membantu) adalah sama tidak peduli bagaimana NULL didefinisikan, jadi lebih mudah untuk membiarkan NULL didefinisikan sebagai nol, dan pastikan bahwa nol tidak pernah dapat dideferensiasi dari C.


Masalahnya hanya akan muncul saat NULL diakses secara tidak sengaja, bukan saat port diakses secara sengaja. Mengapa saya harus mendefinisikan ulang port I / O untuk saat itu? Ini sudah berfungsi sebagaimana mestinya.
Lundin

2
@Lundin Secara tidak sengaja atau tidak, NULL hanya dapat didereferensi dalam program C menggunakan *p, p[]atau p(), jadi kompilator hanya perlu memperhatikan yang melindungi IO port 0x0000.
Apalala

@Lundin Bagian kedua dari pertanyaan Anda: Setelah Anda membatasi akses ke alamat nol dari dalam C, Anda memerlukan cara lain untuk mencapai port 0x0000. Fungsi yang ditulis dalam assembler dapat melakukan itu. Dari dalam C, port dapat dipetakan ke 0xFFFF atau apa pun, tetapi yang terbaik adalah menggunakan fungsi dan melupakan nomor portnya.
Apalala

3

Mempertimbangkan kesulitan ekstrim dalam mendefinisikan ulang NULL seperti yang disebutkan oleh orang lain, mungkin lebih mudah untuk mendefinisikan ulang dereferensi untuk alamat perangkat keras terkenal. Saat membuat alamat, tambahkan 1 ke setiap alamat terkenal, sehingga porta IO Anda yang terkenal adalah:

  #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));

Jika alamat yang Anda tuju dikelompokkan bersama dan Anda dapat merasa aman bahwa menambahkan 1 ke alamat tidak akan menimbulkan konflik dengan apa pun (yang seharusnya tidak dalam banyak kasus), Anda mungkin dapat melakukannya dengan aman. Dan kemudian Anda tidak perlu khawatir tentang membangun kembali rantai alat / std lib dan ekspresi Anda dalam bentuk:

  if (pointer)
  {
     ...
  }

masih bekerja

Gila saya tahu, tapi saya hanya berpikir saya akan membuang ide itu :).


Masalahnya hanya akan muncul saat NULL diakses secara tidak sengaja, bukan saat port diakses secara sengaja. Mengapa saya harus mendefinisikan ulang port I / O untuk saat itu? Ini sudah berfungsi sebagaimana mestinya.
Lundin

@LundIn Saya rasa Anda harus memilih mana yang lebih menyakitkan, mengutak-atik membangun kembali seluruh rantai alat atau mengubah satu bagian dari kode Anda ini.
Doug T.

2

Pola bit untuk penunjuk nol mungkin tidak sama dengan pola bit untuk bilangan bulat 0. Tetapi perluasan makro NULL harus berupa konstanta penunjuk nol, yaitu bilangan bulat konstan dengan nilai 0 yang dapat dicor ke (void *).

Untuk mencapai hasil yang Anda inginkan sambil tetap menyesuaikan diri, Anda harus memodifikasi (atau mungkin mengonfigurasi) rantai perkakas Anda, tetapi hal itu dapat dicapai.


1

Anda sedang mencari masalah. Mendefinisikan ulang NULLke nilai bukan nol akan merusak kode ini:

   jika (myPointer)
   {
      // myPointer bukan null
      ...
   }
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.