การพิมพ์พอยน์เตอร์ว่างด้วย% p เป็นพฤติกรรมที่ไม่ได้กำหนด?


93

เป็นพฤติกรรมที่ไม่ได้กำหนดไว้ในการพิมพ์พอยน์เตอร์ว่างด้วยตัว%pระบุการแปลงหรือไม่

#include <stdio.h>

int main(void) {
    void *p = NULL;

    printf("%p", p);

    return 0;
}

คำถามนี้ใช้กับมาตรฐาน C ไม่ใช่กับการใช้งาน C


อย่าคิดว่าใคร (รวมถึงคณะกรรมการ C) จะใส่ใจกับเรื่องนี้มากเกินไป เป็นปัญหาที่ค่อนข้างประดิษฐ์โดยไม่มีความสำคัญในทางปฏิบัติ (หรือแทบไม่มีเลย)
P__J สนับสนุนผู้หญิงในโปแลนด์

เป็นดังที่ printf แสดงค่าเท่านั้นและไม่สัมผัส (ในความหมายของการอ่านหรือเขียนวัตถุปลายแหลม) - ไม่สามารถเป็นตัวชี้ UB i มีค่าประเภทที่ถูกต้อง (ค่า NULL เป็นค่าที่ถูกต้อง )
P__J รองรับผู้หญิงใน โปแลนด์

3
@PeterJ สมมติว่าสิ่งที่คุณพูดเป็นความจริง (แม้ว่าจะเห็นได้ชัดว่าเป็นรัฐมาตรฐานเป็นอย่างอื่น) ความจริงเพียงอย่างเดียวที่เรากำลังถกเถียงกันอยู่นี้ทำให้คำถามเป็นคำถามที่ถูกต้องและถูกต้องเนื่องจากดูเหมือนว่าส่วนที่ยกมาด้านล่างของมาตรฐานทำให้ มันยากมากที่จะเข้าใจสำหรับนักพัฒนาทั่วไปว่าเกิดอะไรขึ้น .. ความหมาย: คำถามไม่สมควรได้รับการโหวตลงเพราะปัญหานี้ต้องการคำชี้แจง!
Peter Varo

1
ที่เกี่ยวข้อง: stackoverflow.com/q/10461360/694576
alk

2
@PeterJ นั่นเป็นเรื่องที่แตกต่างไปแล้วขอบคุณสำหรับการชี้แจง :)
Peter Varo

คำตอบ:


93

นี่เป็นหนึ่งในกรณีมุมแปลก ๆ ที่เราอยู่ภายใต้ข้อ จำกัด ของภาษาอังกฤษและโครงสร้างที่ไม่สอดคล้องกันในมาตรฐาน อย่างดีที่สุดฉันสามารถโต้แย้งโต้แย้งที่น่าสนใจได้เนื่องจากเป็นไปไม่ได้ที่จะพิสูจน์ :) 1


รหัสในคำถามแสดงพฤติกรรมที่กำหนดไว้อย่างชัดเจน

เนื่องจาก[7.1.4]เป็นพื้นฐานของคำถามมาเริ่มกันเลย:

แต่ละคำสั่งต่อไปนี้ใช้เว้นแต่จะระบุไว้อย่างชัดเจนเป็นอย่างอื่นในคำอธิบายโดยละเอียดที่ตามมา: หากอาร์กิวเมนต์ของฟังก์ชันมีค่าที่ไม่ถูกต้อง ( เช่นค่าภายนอกโดเมนของฟังก์ชันหรือตัวชี้ที่อยู่นอกพื้นที่ที่อยู่ของโปรแกรมหรือตัวชี้โมฆะ , [ ... ตัวอย่างอื่น ๆ ... ] ) [ ... ]ลักษณะการทำงานจะไม่ได้กำหนด [... ข้อความอื่น ๆ ... ]

นี่เป็นภาษาที่เงอะงะ การตีความอย่างหนึ่งคือรายการในรายการเป็น UB สำหรับฟังก์ชันไลบรารีทั้งหมดเว้นแต่จะถูกแทนที่โดยคำอธิบายแต่ละรายการ แต่รายการจะขึ้นต้นด้วย "เช่น" ซึ่งบ่งบอกว่าเป็นเพียงตัวอย่างเท่านั้น ตัวอย่างเช่นไม่ได้กล่าวถึงการยุติสตริงที่เป็นโมฆะที่ถูกต้อง (วิกฤตสำหรับพฤติกรรมของเช่นstrcpy)

ดังนั้นจึงชัดเจนว่าเจตนา / ขอบเขตของ 7.1.4 คือ "ค่าที่ไม่ถูกต้อง" นำไปสู่ ​​UB ( เว้นแต่จะระบุไว้เป็นอย่างอื่น ) เราต้องดูคำอธิบายของแต่ละฟังก์ชันเพื่อพิจารณาว่าค่าใดเป็น "ค่าที่ไม่ถูกต้อง"

ตัวอย่างที่ 1 - strcpy

[7.21.2.3]พูดเพียงสิ่งนี้:

strcpyสำเนาฟังก์ชั่นสตริงชี้ไปตามs2(รวมถึงตัวละครยุติโมฆะ) s1ลงในอาร์เรย์ที่ชี้ไปตาม หากการคัดลอกเกิดขึ้นระหว่างออบเจ็กต์ที่ทับซ้อนกันพฤติกรรมนั้นจะไม่ได้กำหนดไว้

ไม่มีการกล่าวถึงตัวชี้โมฆะอย่างชัดเจน แต่ก็ไม่มีการกล่าวถึงตัวบอกเลิกว่างด้วยเช่นกัน แทนที่จะอนุมานจาก "สตริงที่ชี้โดยs2" ว่าค่าที่ถูกต้องเท่านั้นคือสตริง (เช่นพอยน์เตอร์ไปยังอาร์เรย์อักขระที่สิ้นสุดด้วยค่า null)

อันที่จริงรูปแบบนี้สามารถเห็นได้ตลอดทั้งคำอธิบายแต่ละรายการ ตัวอย่างอื่น ๆ :

  • [7.6.4.1 (fenv)]เก็บสภาพแวดล้อมจุดลอยตัวปัจจุบันในวัตถุชี้ไปโดยenvp

  • [7.12.6.4 (frexp)]เก็บจำนวนเต็มใน int ที่วัตถุชี้ไปโดยexp

  • [7.19.5.1 (fclose)] กระแสชี้ไปโดยstream

ตัวอย่างที่ 2 - printf

[7.19.6.1]กล่าวเกี่ยวกับ%p:

p- voidข้อโต้แย้งที่จะเป็นตัวชี้ไปยัง ค่าของตัวชี้จะถูกแปลงเป็นลำดับของอักขระการพิมพ์ในลักษณะที่กำหนดไว้สำหรับการนำไปใช้งาน

Null เป็นค่าตัวชี้ที่ถูกต้องและส่วนนี้จะไม่มีการกล่าวถึงอย่างชัดเจนว่า null เป็นกรณีพิเศษและตัวชี้จะต้องชี้ไปที่วัตถุ ดังนั้นจึงเป็นพฤติกรรมที่กำหนด


1. เว้นแต่ผู้เขียนมาตรฐานจะมาข้างหน้าหรือเว้นแต่เราจะพบสิ่งที่คล้ายกับเอกสารเหตุผลที่ชี้แจงสิ่งต่างๆ


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Bhargav Rao

1
"แต่ไม่มีการกล่าวถึงตัวยุติที่เป็นโมฆะ" ในตัวอย่างที่ 1 มีความอ่อนแอ - strcpy เนื่องจากข้อมูลจำเพาะระบุว่า "คัดลอกสตริง " สตริงถูกกำหนดไว้อย่างชัดเจนว่ามีอักขระ null
chux - คืนสถานะ Monica

1
@chux - นั่นเป็นประเด็นของฉัน - เราต้องอนุมานว่าอะไรถูกต้อง / ไม่ถูกต้องจากบริบทแทนที่จะคิดว่ารายการใน 7.1.4 นั้นครบถ้วนสมบูรณ์ (อย่างไรก็ตามการมีอยู่ของส่วนนี้ของคำตอบของฉันค่อนข้างสมเหตุสมผลกว่าในบริบทของความคิดเห็นที่ถูกลบไปโดยอ้างว่า strcpy เป็นตัวอย่างที่ตอบโต้)
Oliver Charlesworth

1
ปมของปัญหาคือวิธีการที่ผู้อ่านจะตีความเช่น หมายความว่าตัวอย่างของค่าที่ไม่ถูกต้องที่เป็นไปได้คืออะไร? หมายความว่าตัวอย่างบางส่วนที่มีค่าที่ไม่ถูกต้องเสมอไปหรือไม่? สำหรับบันทึกฉันไปกับการตีความครั้งแรก
ninjalj

1
@ninjalj - ใช่ตกลง นั่นคือสิ่งที่ฉันพยายามจะสื่อในคำตอบของฉันที่นี่กล่าวคือ "นี่คือตัวอย่างของประเภทของสิ่งที่อาจเป็นค่าที่ไม่ถูกต้อง" :)
Oliver Charlesworth

20

คำตอบสั้น ๆ

ครับ . การพิมพ์ตัวชี้ค่าว่างด้วยตัว%pระบุการแปลงมีพฤติกรรมที่ไม่ได้กำหนดไว้ ต้องบอกว่าฉันไม่ทราบถึงการนำไปใช้งานที่เป็นไปตามข้อกำหนดที่มีอยู่ซึ่งจะเป็นการทำงานผิด

คำตอบใช้กับมาตรฐาน C (C89 / C99 / C11)


คำตอบยาว

ตัว%pระบุการแปลงคาดว่าอาร์กิวเมนต์ของตัวชี้ชนิดจะเป็นโมฆะการแปลงตัวชี้เป็นอักขระที่พิมพ์ได้จะถูกกำหนดให้ใช้งานได้ ไม่ระบุว่าคาดว่าจะมีตัวชี้ค่าว่าง

การแนะนำฟังก์ชันไลบรารีมาตรฐานระบุว่าพอยน์เตอร์ว่างเป็นอาร์กิวเมนต์ของฟังก์ชัน (ไลบรารีมาตรฐาน) ถือว่าเป็นค่าที่ไม่ถูกต้องเว้นแต่จะระบุไว้เป็นอย่างอื่นอย่างชัดเจน

C99 / C11 §7.1.4 p1

[... ] ถ้าอาร์กิวเมนต์ของฟังก์ชันมีค่าที่ไม่ถูกต้อง (เช่น [... ] ตัวชี้ค่าว่าง [... ] พฤติกรรมจะไม่ได้กำหนด

ตัวอย่างสำหรับฟังก์ชัน (ไลบรารีมาตรฐาน) ที่คาดว่าพอยน์เตอร์ว่างเป็นอาร์กิวเมนต์ที่ถูกต้อง:

  • fflush() ใช้ตัวชี้โมฆะเพื่อล้าง "สตรีมทั้งหมด" (ที่เกี่ยวข้อง)
  • freopen() ใช้ตัวชี้ค่าว่างเพื่อระบุไฟล์ "เชื่อมโยงในปัจจุบัน" กับสตรีม
  • snprintf() อนุญาตให้ส่งตัวชี้ค่าว่างเมื่อ 'n' เป็นศูนย์
  • realloc() ใช้ตัวชี้ค่าว่างสำหรับการจัดสรรวัตถุใหม่
  • free() อนุญาตให้ส่งตัวชี้โมฆะ
  • strtok() ใช้ตัวชี้ค่าว่างสำหรับการโทรในภายหลัง

หากเราใช้กรณีนี้snprintf()มันสมเหตุสมผลแล้วที่จะอนุญาตให้ส่งตัวชี้ค่าว่างเมื่อ 'n' เป็นศูนย์ แต่นี่ไม่ใช่กรณีสำหรับฟังก์ชัน (ไลบรารีมาตรฐาน) อื่น ๆ ที่อนุญาตให้มีค่าเป็นศูนย์ 'n' ที่คล้ายกัน ตัวอย่างเช่น: memcpy(), memmove(), strncpy(), ,memset()memcmp()

ไม่เพียง แต่ระบุไว้ในบทนำสู่ไลบรารีมาตรฐาน แต่ยังรวมถึงการแนะนำฟังก์ชันเหล่านี้อีกครั้ง:

C99 §7.21.1 p2 / C11 §7.24.1 p2

โดยที่อาร์กิวเมนต์ที่ประกาศเป็นsize_tn ระบุความยาวของอาร์เรย์สำหรับฟังก์ชัน n สามารถมีค่าเป็นศูนย์ในการเรียกใช้ฟังก์ชันนั้น เว้นแต่จะระบุไว้อย่างชัดเจนเป็นอย่างอื่นในคำอธิบายของฟังก์ชันเฉพาะในข้อย่อยนี้อาร์กิวเมนต์ตัวชี้ในการเรียกดังกล่าวจะยังคงมีค่าที่ถูกต้องตามที่อธิบายไว้ใน 7.1.4


เป็นเจตนาหรือไม่?

ฉันไม่รู้ว่า UB ของ%pตัวชี้ null นั้นเป็นความตั้งใจจริงหรือไม่ แต่เนื่องจากมาตรฐานระบุอย่างชัดเจนว่าตัวชี้ค่าว่างถือเป็นค่าที่ไม่ถูกต้องเป็นอาร์กิวเมนต์ของฟังก์ชันไลบรารีมาตรฐานจากนั้นจะไปและระบุกรณีที่เป็นโมฆะ ชี้เป็นอาร์กิวเมนต์ที่ถูกต้อง (snprintf, ฟรี, ฯลฯ ) และจากนั้นมันจะไปอีกครั้งและซ้ำความต้องการสำหรับข้อโต้แย้งที่จะสามารถใช้งานได้แม้ในศูนย์กรณี 'n' ( memcpy, memmove, memset) แล้วฉันคิดว่ามันเป็นเหตุผลที่จะสมมติว่า คณะกรรมการมาตรฐาน C ไม่ได้กังวลกับการมีสิ่งเหล่านี้ที่ไม่ได้กำหนดไว้มากเกินไป


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Bhargav Rao

1
@JeroenMostert: อะไรคือเจตนาของข้อโต้แย้งนี้? คำพูดที่ระบุของ 7.1.4 ค่อนข้างชัดเจนใช่หรือไม่ มีอะไรจะโต้แย้งเกี่ยวกับ"เว้นแต่จะระบุไว้อย่างชัดเจนเป็นอย่างอื่น"เมื่อไม่มีการระบุไว้เป็นอย่างอื่น มีอะไรจะโต้แย้งเกี่ยวกับข้อเท็จจริงที่ว่าไลบรารีฟังก์ชันสตริง (ไม่เกี่ยวข้อง) มีถ้อยคำที่คล้ายกันดังนั้นถ้อยคำจึงดูเหมือนจะไม่บังเอิญ ฉันคิดว่าคำตอบนี้ (แม้ว่าจะไม่มีประโยชน์ในทางปฏิบัติจริงๆ) ถูกต้องเท่าที่จะทำได้
Damon

3
@Damon: ฮาร์ดแวร์ในตำนานของคุณไม่ได้เป็นตำนานมีสถาปัตยกรรมมากมายที่ค่าที่ไม่ได้แสดงถึงที่อยู่ที่ถูกต้องอาจไม่ถูกโหลดในการลงทะเบียนที่อยู่ อย่างไรก็ตามการส่งพอยน์เตอร์ว่างเป็นอาร์กิวเมนต์ของฟังก์ชันยังคงต้องทำงานบนแพลตฟอร์มเหล่านั้นเหมือนกลไกทั่วไป เพียงแค่วางไว้บนสแต็กจะไม่ระเบิดสิ่งต่างๆ
Jeroen Mostert

1
@anatolyg: บนโปรเซสเซอร์ x86 แอดเดรสมีสองส่วนคือเซ็กเมนต์และออฟเซ็ต ใน 8086 การโหลดการลงทะเบียนเซ็กเมนต์ก็เหมือนกับการโหลดเครื่องอื่น ๆ แต่ในเครื่องที่ใหม่กว่าทั้งหมดจะดึงตัวบอกเซ็กเมนต์ การโหลดตัวอธิบายที่ไม่ถูกต้องทำให้เกิดกับดัก จำนวนมากสำหรับ 80386 และการประมวลผลต่อมา แต่ใช้เพียงส่วนหนึ่งจึงไม่เคยลงทะเบียนส่วนโหลดที่ทั้งหมด
supercat

1
ฉันคิดว่าทุกคนคงเห็นด้วยว่าการพิมพ์ตัวชี้ค่าว่างด้วย%pไม่ควรเป็นพฤติกรรมที่ไม่ได้กำหนด
MM

-1

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

คำถามที่ว่าสิ่งที่เรียกร้อง UB นั้นแทบจะไม่มีหรือไม่มีประโยชน์ในตัวมันเอง คำถามสำคัญที่แท้จริงคือ:

  1. คนที่พยายามเขียนคอมไพเลอร์ที่มีคุณภาพควรทำให้มันมีพฤติกรรมที่คาดเดาได้หรือไม่? สำหรับสถานการณ์ที่อธิบายคำตอบคือใช่

  2. โปรแกรมเมอร์ควรมีสิทธิ์คาดหวังว่าคอมไพเลอร์ที่มีคุณภาพสำหรับสิ่งที่คล้ายกับแพลตฟอร์มปกติจะทำงานในลักษณะที่คาดเดาได้หรือไม่? ในสถานการณ์ที่อธิบายฉันจะบอกว่าคำตอบคือใช่

  3. ผู้เขียนคอมไพเลอร์ป้านบางคนอาจยืดการตีความมาตรฐานเพื่อพิสูจน์ว่าทำอะไรแปลก ๆ หรือไม่? ฉันจะไม่หวัง แต่จะไม่ออกกฎ

  4. ควรทำความสะอาดคอมไพเลอร์เกี่ยวกับพฤติกรรมหรือไม่? ขึ้นอยู่กับระดับความหวาดระแวงของผู้ใช้ คอมไพเลอร์ที่ทำความสะอาดอาจไม่ควรตั้งค่าเริ่มต้นในการพูดคุยเกี่ยวกับพฤติกรรมดังกล่าว แต่อาจมีตัวเลือกการกำหนดค่าให้ทำในกรณีที่โปรแกรมอาจถูกพอร์ตไปยังคอมไพเลอร์ที่ "ฉลาด" / เป็นใบ้ที่มีพฤติกรรมแปลก ๆ

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


1. ไม่ใช่เรื่องแปลกที่โปรแกรมเมอร์จะพบว่าสมมติฐานของนักเพิ่มประสิทธิภาพสมัยใหม่ / เชิงรุกขัดแย้งกับสิ่งที่พวกเขาคิดว่า "สมเหตุสมผล" หรือ "คุณภาพ" 2. เมื่อพูดถึงความคลุมเครือในข้อกำหนดไม่ใช่เรื่องแปลกที่ผู้ปฏิบัติงานจะขัดแย้งกับสิทธิเสรีภาพที่พวกเขาอาจคิดได้ 3. เมื่อพูดถึงสมาชิกของคณะกรรมการมาตรฐาน C แม้ว่าพวกเขาจะไม่เห็นด้วยเสมอว่าการตีความที่ 'ถูกต้อง' คืออะไรนับประสาอะไรกับสิ่งที่ควรจะเป็น จากที่กล่าวมาเราควรปฏิบัติตามการตีความที่สมเหตุสมผลของใคร?
Dror K.

6
การตอบคำถาม "โค้ดชิ้นนี้เรียกใช้ UB หรือไม่" พร้อมวิทยานิพนธ์เกี่ยวกับสิ่งที่คุณคิดเกี่ยวกับประโยชน์ของ UB หรือวิธีการที่คอมไพเลอร์ควรปฏิบัติเป็นความพยายามที่ไม่ดีในการตอบโดยเฉพาะอย่างยิ่งเมื่อคุณสามารถคัดลอกวางสิ่งนี้เป็น คำตอบเกือบทุกคำถามเกี่ยวกับ UB โดยเฉพาะ เพื่อเป็นการชื่นชมยินดีในวาทศิลป์ของคุณ: ใช่มันมีความสำคัญอย่างยิ่งกับสิ่งที่ Standard กล่าวไม่ว่านักเขียนคอมไพเลอร์บางคนจะทำอะไรหรือคิดอย่างไรกับพวกเขาในการทำเช่นนั้นเพราะ Standard เป็นสิ่งที่ทั้งโปรแกรมเมอร์และนักเขียนคอมไพเลอร์เริ่มต้นจาก
Jeroen Mostert

1
@JeroenMostert: คำตอบของ "Does X invoke Undefined behavior" มักจะขึ้นอยู่กับความหมายของคำถาม หากโปรแกรมได้รับการพิจารณาว่ามีพฤติกรรมที่ไม่ได้กำหนดหากมาตรฐานไม่ได้กำหนดข้อกำหนดเกี่ยวกับพฤติกรรมของการนำไปใช้งานที่สอดคล้องกันโปรแกรมเกือบทั้งหมดจะเรียกใช้ UB ผู้เขียนมาตรฐานอนุญาตให้นำไปใช้งานได้ตามอำเภอใจอย่างชัดเจนหากโปรแกรมทำรังเรียกฟังก์ชันลึกเกินไปตราบใดที่การนำไปใช้งานสามารถประมวลผลข้อความต้นฉบับอย่างน้อยหนึ่งข้อความ (อาจมีการจัดทำขึ้นอย่างถูกต้อง) ที่ใช้ขีด จำกัด การแปลใน Stadard
supercat

@supercat: น่าสนใจมาก แต่printf("%p", (void*) 0)พฤติกรรมที่ไม่ได้กำหนดหรือไม่ตามมาตรฐาน? การเรียกฟังก์ชันที่ซ้อนกันอย่างลึกซึ้งมีความเกี่ยวข้องกับราคาชาในประเทศจีน และใช่ UB เป็นเรื่องธรรมดามากในโปรแกรมจริง - มันคืออะไร?
Jeroen Mostert

1
@JeroenMostert: เนื่องจาก Standard อนุญาตให้มีการใช้งานแบบป้านโดยถือว่าเกือบทุกโปรแกรมมี UB สิ่งที่สำคัญคือพฤติกรรมของการใช้งานที่ไม่ป้าน ในกรณีที่คุณไม่สังเกตเห็นฉันไม่ได้เขียนแค่สำเนา / วางเกี่ยวกับ UB แต่ตอบคำถามเกี่ยวกับ%pความหมายที่เป็นไปได้ของคำถาม
supercat
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.