นิยามใหม่ของ NULL


118

ฉันกำลังเขียนรหัส C สำหรับระบบที่ที่อยู่ 0x0000 ถูกต้องและมีพอร์ต I / O ดังนั้นจุดบกพร่องที่เป็นไปได้ที่เข้าถึงตัวชี้ NULL จะยังคงตรวจไม่พบและในขณะเดียวกันก็ทำให้เกิดพฤติกรรมที่เป็นอันตราย

ด้วยเหตุนี้ฉันจึงต้องการกำหนด NULL ใหม่ให้เป็นที่อยู่อื่นตัวอย่างเช่นที่อยู่ที่ไม่ถูกต้อง หากฉันเข้าถึงที่อยู่ดังกล่าวโดยบังเอิญฉันจะได้รับฮาร์ดแวร์ขัดจังหวะซึ่งฉันสามารถจัดการกับข้อผิดพลาดได้ ฉันมีสิทธิ์เข้าถึง stddef.h สำหรับคอมไพเลอร์นี้ดังนั้นฉันจึงสามารถเปลี่ยนส่วนหัวมาตรฐานและกำหนดค่า NULL ใหม่ได้

คำถามของฉันคือสิ่งนี้จะขัดแย้งกับมาตรฐาน C หรือไม่? เท่าที่ฉันสามารถบอกได้จาก 7.17 ในมาตรฐานมาโครถูกกำหนดให้ใช้งานได้ มีอะไรในมาตรฐานที่ระบุว่า NULL ต้องเป็น 0 หรือไม่?

ปัญหาอีกประการหนึ่งคือคอมไพเลอร์จำนวนมากดำเนินการเริ่มต้นแบบคงที่โดยตั้งค่าทุกอย่างเป็นศูนย์ไม่ว่าจะเป็นประเภทข้อมูลก็ตาม แม้ว่ามาตรฐานจะบอกว่าคอมไพเลอร์ควรกำหนดจำนวนเต็มเป็นศูนย์และพอยน์เตอร์เป็น NULL ถ้าฉันจะกำหนด NULL ใหม่สำหรับคอมไพเลอร์ของฉันฉันก็รู้ว่าการเริ่มต้นแบบคงที่จะล้มเหลว ฉันสามารถพิจารณาว่าเป็นพฤติกรรมของคอมไพเลอร์ที่ไม่ถูกต้องแม้ว่าฉันจะแก้ไขส่วนหัวของคอมไพเลอร์ด้วยตนเองอย่างกล้าหาญ? เพราะฉันรู้แน่นอนว่าคอมไพเลอร์เฉพาะนี้ไม่เข้าถึงมาโคร NULL เมื่อทำการเริ่มต้นแบบคงที่


3
นี่เป็นคำถามที่ดีจริงๆ ฉันไม่มีคำตอบให้คุณ แต่ต้องถามคุณแน่ใจหรือว่าไม่สามารถย้ายข้อมูลที่ถูกต้องของคุณที่ 0x00 ออกไปและปล่อยให้ NULL เป็นที่อยู่ที่ไม่ถูกต้องเหมือนในระบบ "ปกติ" หากคุณทำไม่ได้ที่อยู่ที่ไม่ถูกต้องอย่างปลอดภัยเพียงแห่งเดียวที่จะใช้คือที่อยู่ที่คุณมั่นใจได้ว่าคุณสามารถจัดสรรและmprotectรักษาความปลอดภัยได้ หรือถ้าแพลตฟอร์มไม่มี ASLR หรือสิ่งที่คล้ายกันจะอยู่นอกเหนือหน่วยความจำกายภาพของแพลตฟอร์ม โชคดี.
Borealid

8
วิธีการมันจะทำงานหากรหัสของคุณคือการใช้if(ptr) { /* do something on ptr*/ }? จะใช้งานได้หรือไม่ถ้า NULL ถูกกำหนดให้แตกต่างจาก 0x0
Xavier T.

3
ตัวชี้ C ไม่มีความสัมพันธ์บังคับกับที่อยู่หน่วยความจำ ตราบเท่าที่ปฏิบัติตามกฎของการคำนวณทางคณิตศาสตร์ของตัวชี้ค่าพอยน์เตอร์อาจเป็นอะไรก็ได้ การใช้งานส่วนใหญ่เลือกใช้ที่อยู่หน่วยความจำเป็นค่าตัวชี้ แต่สามารถใช้อะไรก็ได้ตราบเท่าที่เป็นไอโซมอร์ฟิซึม
datenwolf

2
@bdonlan นั่นจะละเมิดกฎ (ที่ปรึกษา) ใน MISRA-C เช่นกัน
Lundin

2
@Andreas ใช่นั่นคือความคิดของฉันด้วย คนฮาร์ดแวร์ไม่ควรได้รับอนุญาตให้ออกแบบฮาร์ดแวร์ที่ซอฟต์แวร์ควรใช้! :)
Lundin

คำตอบ:


84

มาตรฐาน C ไม่ต้องการให้พอยน์เตอร์ว่างอยู่ที่ศูนย์แอดเดรสของเครื่อง อย่างไรก็ตามการส่ง0ค่าคงที่เป็นค่าตัวชี้จะต้องส่งผลให้NULLตัวชี้ (§6.3.2.3 / 3) และการประเมินค่าตัวชี้ว่างเป็นบูลีนจะต้องเป็นเท็จ นี้สามารถเป็นบิตที่น่าอึดอัดใจจริงๆถ้าคุณไม่ต้องการที่อยู่ที่ศูนย์และNULLไม่ได้เป็นที่อยู่ศูนย์

อย่างไรก็ตามด้วยการปรับเปลี่ยนคอมไพลเลอร์และไลบรารีมาตรฐาน (หนัก) จึงเป็นไปไม่ได้ที่NULLจะแสดงด้วยรูปแบบบิตทางเลือกในขณะที่ยังคงเป็นไปตามไลบรารีมาตรฐานอย่างเคร่งครัด มันเป็นไม่เพียงพอที่จะเพียงแค่เปลี่ยนคำจำกัดความของNULLตัวเอง แต่เป็นแล้วNULLจะประเมินให้เป็นจริง

โดยเฉพาะคุณจะต้อง:

  • จัดให้มีศูนย์ที่แท้จริงในการมอบหมายงานที่จะชี้ (หรือปลดเปลื้องเพื่อชี้) ที่จะแปลงเป็นค่าความมหัศจรรย์บางอย่างอื่น ๆ -1เช่น
  • จัดให้มีการทดสอบความเท่าเทียมกันระหว่างพอยน์เตอร์และจำนวนเต็มคงที่0เพื่อตรวจสอบค่าเวทมนตร์แทน (§6.5.9 / 6)
  • จัดเรียงสำหรับบริบททั้งหมดที่ชนิดตัวชี้ถูกประเมินเป็นบูลีนเพื่อตรวจสอบความเท่าเทียมกับค่าวิเศษแทนที่จะตรวจสอบเป็นศูนย์ สิ่งนี้ตามมาจากความหมายของการทดสอบความเท่าเทียมกัน แต่คอมไพเลอร์อาจนำไปใช้ภายในที่แตกต่างกัน ดู§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 ชี้ให้อัปเดตความหมายสำหรับการเริ่มต้นของวัตถุคงที่ (§6.7.8 / 10) และตัวเริ่มต้นผสมบางส่วน (§6.7.8 / 21) เพื่อสะท้อนการแสดงตัวชี้ค่าว่างใหม่
  • สร้างวิธีอื่นในการเข้าถึงศูนย์ที่อยู่จริง

มีบางสิ่งที่คุณไม่ต้องจัดการ ตัวอย่างเช่น:

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

หลังจากนี้pไม่รับประกันว่าจะเป็นตัวชี้โมฆะ ต้องจัดการเฉพาะการกำหนดค่าคงที่เท่านั้น (นี่เป็นแนวทางที่ดีสำหรับการเข้าถึงที่อยู่จริงเป็นศูนย์) ในทำนองเดียวกัน:

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

นอกจากนี้:

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

x ไม่รับประกันว่าจะเป็น 0ไม่รับประกันว่าจะเป็น

ในระยะสั้นเห็นได้ชัดว่าเงื่อนไขนี้ได้รับการพิจารณาโดยคณะกรรมการภาษาซีและมีการพิจารณาสำหรับผู้ที่จะเลือกตัวแทนอื่นสำหรับ NULL สิ่งที่คุณต้องทำตอนนี้คือทำการเปลี่ยนแปลงที่สำคัญกับคอมไพเลอร์ของคุณและเดี๋ยวก่อนคุณทำเสร็จแล้ว :)

ตามหมายเหตุด้านข้างอาจเป็นไปได้ที่จะใช้การเปลี่ยนแปลงเหล่านี้ด้วยขั้นตอนการแปลงซอร์สโค้ดก่อนที่คอมไพเลอร์จะเหมาะสม นั่นคือแทนที่จะเป็นโฟลว์ปกติของตัวประมวลผลล่วงหน้า -> คอมไพเลอร์ -> แอสเซมเบลอร์ -> ตัวเชื่อมโยงคุณจะต้องเพิ่มตัวประมวลผลล่วงหน้า -> การแปลงค่า NULL -> คอมไพเลอร์ -> แอสเซมเบลอร์ -> ตัวเชื่อม จากนั้นคุณสามารถทำการเปลี่ยนแปลงเช่น:

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

สิ่งนี้จะต้องใช้ตัวแยกวิเคราะห์ C แบบเต็มรวมถึงตัวแยกวิเคราะห์ประเภทและการวิเคราะห์ typedefs และการประกาศตัวแปรเพื่อพิจารณาว่าตัวระบุใดที่สอดคล้องกับพอยน์เตอร์ อย่างไรก็ตามการทำเช่นนี้ทำให้คุณไม่ต้องทำการเปลี่ยนแปลงส่วนการสร้างโค้ดของคอมไพเลอร์ให้เหมาะสม เสียงดังกราวอาจมีประโยชน์สำหรับการนำสิ่งนี้ไปใช้ - ฉันเข้าใจว่ามันถูกออกแบบมาโดยคำนึงถึงการเปลี่ยนแปลงเช่นนี้ คุณยังคงต้องทำการเปลี่ยนแปลงในไลบรารีมาตรฐานด้วยเช่นกัน


2
ตกลงฉันไม่พบข้อความใน§6.3.2.3 แต่ฉันสงสัยว่าจะมีข้อความดังกล่าวอยู่ที่ไหนสักแห่ง :) ฉันเดาว่านี่ตอบคำถามของฉันโดยมาตรฐานฉันไม่ได้รับอนุญาตให้กำหนด NULL ใหม่เว้นแต่ฉันจะเขียนคอมไพเลอร์ C ใหม่เพื่อสำรองข้อมูล :)
Lundin

2
เคล็ดลับที่ดีคือการแฮ็กคอมไพเลอร์เพื่อให้ตัวชี้ <-> จำนวนเต็มแปลง XOR เป็นค่าเฉพาะที่เป็นตัวชี้ที่ไม่ถูกต้องและยังไม่สำคัญพอที่สถาปัตยกรรมเป้าหมายจะทำได้ในราคาถูก (โดยปกตินั่นจะเป็นค่าที่มีการกำหนดบิตเดียว เช่น 0x20000000)
Simon Richter

2
สิ่งที่คุณจะต้องมีการเปลี่ยนแปลงในคอมไพเลอร์ก็คือ initialisation ของวัตถุที่มีประเภทสารประกอบ - ถ้าวัตถุ initialised บางส่วนแล้วชี้ใด ๆ ซึ่ง initaliser NULLอย่างชัดเจนไม่อยู่จะต้องมีการเริ่มต้นใช้งานไป
caf

20

มาตรฐานระบุว่านิพจน์คงที่จำนวนเต็มที่มีค่า 0 หรือนิพจน์ดังกล่าวถูกแปลงเป็นvoid *ชนิดเป็นค่าคงที่ของตัวชี้ค่าว่าง ซึ่งหมายความว่า(void *)0เสมอตัวชี้โมฆะ แต่ให้int i = 0;, (void *)iไม่จำเป็นต้องเป็น

การใช้งาน C ประกอบด้วยคอมไพเลอร์พร้อมกับส่วนหัว หากคุณแก้ไขส่วนหัวเพื่อกำหนดนิยามใหม่NULLแต่อย่าแก้ไขคอมไพเลอร์เพื่อแก้ไขการเริ่มต้นแบบคงที่แสดงว่าคุณได้สร้างการใช้งานที่ไม่เป็นไปตามข้อกำหนด เป็นการใช้งานทั้งหมดร่วมกันซึ่งมีพฤติกรรมที่ไม่ถูกต้องและหากคุณทำพังคุณจะไม่มีใครตำหนิจริงๆ;)

คุณต้องแก้ไขปัญหามากกว่า initialisations เพียงคงที่ของหลักสูตร - กำหนดตัวชี้p, if (p)เทียบเท่ากับif (p != NULL)เนื่องจากกฎดังกล่าวข้างต้น


8

หากคุณใช้ไลบรารี C std คุณจะพบปัญหากับฟังก์ชันที่สามารถคืนค่า NULL ตัวอย่างเช่นรัฐเอกสาร malloc :

หากฟังก์ชันไม่สามารถจัดสรรบล็อกหน่วยความจำที่ร้องขอได้ตัวชี้ค่า null จะถูกส่งกลับ

เนื่องจาก malloc และฟังก์ชันที่เกี่ยวข้องถูกรวบรวมเป็นไบนารีที่มีค่า NULL เฉพาะอยู่แล้วหากคุณกำหนดค่า NULL ใหม่คุณจะไม่สามารถใช้ไลบรารี C std ได้โดยตรงเว้นแต่คุณจะสามารถสร้างห่วงโซ่เครื่องมือทั้งหมดของคุณใหม่รวมถึง C std libs

นอกจากนี้เนื่องจากการใช้ NULL ของไลบรารี std หากคุณกำหนด NULL ใหม่ก่อนที่จะรวมส่วนหัว std คุณอาจเขียนทับการกำหนด NULL ที่แสดงรายการในส่วนหัว สิ่งที่อินไลน์จะไม่สอดคล้องกับวัตถุที่คอมไพล์

ฉันจะกำหนด NULL ของคุณเอง "MYPRODUCT_NULL" สำหรับการใช้งานของคุณเองและหลีกเลี่ยงหรือแปลจาก / ไปยังไลบรารี C std


6

ปล่อยให้ NULL อยู่คนเดียวและถือว่า IO เป็นพอร์ต 0x0000 เป็นกรณีพิเศษอาจใช้รูทีนที่เขียนในแอสเซมเบลอร์จึงไม่อยู่ภายใต้ความหมาย C มาตรฐาน IOW อย่ากำหนดค่า NULL ใหม่กำหนดพอร์ต 0x00000 ใหม่

โปรดทราบว่าหากคุณกำลังเขียนหรือแก้ไขคอมไพเลอร์ C งานที่จำเป็นในการหลีกเลี่ยงการยกเลิกการอ้างอิง NULL (สมมติว่าในกรณีของคุณ CPU ไม่ช่วย) จะเหมือนกันไม่ว่าจะกำหนดค่า NULL อย่างไรดังนั้นจึงง่ายกว่าที่จะปล่อยให้ NULL กำหนดไว้ เป็นศูนย์และตรวจสอบให้แน่ใจว่าไม่มีการอ้างอิงศูนย์จาก C


ปัญหาจะเกิดขึ้นเมื่อมีการเข้าถึง NULL โดยไม่ตั้งใจเท่านั้นไม่ใช่เมื่อมีการเข้าถึงพอร์ตโดยเจตนา เหตุใดฉันจึงกำหนดพอร์ต I / O ใหม่ในตอนนั้น มันทำงานตามที่ควรแล้ว
Lundin

2
@Lundin บังเอิญหรือไม่โมฆะสามารถเพียง แต่จะ dereferenced ในโปรแกรม C ใช้*p, p[]หรือp()เพื่อให้คอมไพเลอร์เพียงต้องการที่จะดูแลเกี่ยวกับผู้ที่จะปกป้อง 0x0000 พอร์ต IO
Apalala

@Lundin ส่วนที่สองของคำถามของคุณ: เมื่อคุณ จำกัด การเข้าถึงที่อยู่เป็นศูนย์จากภายใน C คุณต้องมีวิธีอื่นในการเข้าถึงพอร์ต 0x0000 ฟังก์ชันที่เขียนในแอสเซมเบลอร์สามารถทำได้ จากภายใน C พอร์ตสามารถแมปกับ 0xFFFF หรืออะไรก็ได้ แต่ควรใช้ฟังก์ชันและลืมหมายเลขพอร์ตไป
Apalala

3

เมื่อพิจารณาถึงความยากลำบากอย่างยิ่งยวดในการกำหนดค่า NULL ใหม่ตามที่ผู้อื่นกล่าวไว้อาจจะง่ายกว่าในการกำหนดนิยามใหม่สำหรับการอ้างอิงที่อยู่ฮาร์ดแวร์ที่รู้จักกันดี เมื่อสร้างที่อยู่ให้เพิ่ม 1 ในทุกที่อยู่ที่รู้จักเพื่อให้พอร์ต IO ที่รู้จักกันดีของคุณเป็น:

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

หากที่อยู่ที่คุณเกี่ยวข้องถูกจัดกลุ่มเข้าด้วยกันและคุณสามารถรู้สึกปลอดภัยที่การเพิ่ม 1 ในที่อยู่จะไม่ขัดแย้งกับสิ่งใด ๆ (ซึ่งส่วนใหญ่ไม่ควร) คุณอาจดำเนินการได้อย่างปลอดภัย จากนั้นคุณไม่ต้องกังวลเกี่ยวกับการสร้างห่วงโซ่เครื่องมือ / std lib และนิพจน์ในรูปแบบใหม่:

  if (pointer)
  {
     ...
  }

ยังใช้งานได้

บ้าฉันรู้ แต่แค่คิดว่าฉันจะโยนความคิดออกไป :)


ปัญหาจะเกิดขึ้นเมื่อมีการเข้าถึง NULL โดยไม่ตั้งใจเท่านั้นไม่ใช่เมื่อมีการเข้าถึงพอร์ตโดยเจตนา เหตุใดฉันจึงกำหนดพอร์ต I / O ใหม่ในตอนนั้น มันทำงานตามที่ควรแล้ว
Lundin

@Lund ในฉันเดาว่าคุณต้องเลือกว่าอันไหนเจ็บปวดกว่าปรับแต่ง toolchain ใหม่ทั้งหมดหรือเปลี่ยนส่วนนี้ของโค้ดของคุณ
ดั๊กต.

2

รูปแบบบิตสำหรับตัวชี้ค่าว่างอาจไม่เหมือนกับรูปแบบบิตสำหรับจำนวนเต็ม 0 แต่การขยายของมาโคร NULL ต้องเป็นค่าคงที่ของตัวชี้ที่เป็นโมฆะนั่นคือจำนวนเต็มคงที่ของค่า 0 ซึ่งอาจถูกเหวี่ยงเป็น (โมฆะ *)

เพื่อให้ได้ผลลัพธ์ที่คุณต้องการในขณะที่ยังคงความสอดคล้องคุณจะต้องปรับเปลี่ยน (หรืออาจกำหนดค่า) ห่วงโซ่เครื่องมือของคุณ แต่ก็ทำได้


1

คุณกำลังถามปัญหา RedefiningNULLเป็นค่าที่ไม่ใช่ค่าว่างจะทำให้โค้ดนี้แตก:

   ถ้า (myPointer)
   {
      // myPointer ไม่เป็นโมฆะ
      ...
   }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.