ทำไม printf (“% f”, 0); ให้พฤติกรรมที่ไม่ได้กำหนด?


87

คำสั่ง

printf("%f\n",0.0f);

พิมพ์ 0.

อย่างไรก็ตามคำสั่ง

printf("%f\n",0);

พิมพ์ค่าสุ่ม

ฉันรู้ว่าฉันกำลังแสดงพฤติกรรมบางอย่างที่ไม่ได้กำหนด แต่ฉันไม่สามารถหาสาเหตุได้โดยเฉพาะ

ค่าทศนิยมที่บิตทั้งหมดเป็น 0 ยังคงใช้ได้floatโดยมีค่าเป็น 0
floatและintมีขนาดเท่ากันบนเครื่องของฉัน (หากเกี่ยวข้องกัน)

เหตุใดการใช้ลิเทอรัลจำนวนเต็มแทนที่จะเป็นลิเทอรัลจุดลอยตัวจึงprintfทำให้เกิดพฤติกรรมนี้

ปล. พฤติกรรมเดียวกันนี้สามารถมองเห็นได้ถ้าฉันใช้

int i = 0;
printf("%f\n", i);

37
printfคาดหวังว่าจะได้รับdoubleและคุณกำลังให้ไฟล์int. floatและintอาจมีขนาดเท่ากันบนเครื่องของคุณ แต่0.0fจริงๆแล้วจะถูกแปลงเป็นdoubleเมื่อผลักเข้าไปในรายการอาร์กิวเมนต์ตัวแปร (และprintfคาดว่าจะเป็นเช่นนั้น) ในระยะสั้นคุณไม่ได้บรรลุจุดสิ้นสุดของการต่อรองโดยprintfพิจารณาจากตัวระบุที่คุณใช้และข้อโต้แย้งที่คุณระบุ
WhozCraig

22
ฟังก์ชัน Varargs จะไม่แปลงอาร์กิวเมนต์ของฟังก์ชันเป็นประเภทของพารามิเตอร์ที่เกี่ยวข้องโดยอัตโนมัติเนื่องจากไม่สามารถทำได้ ข้อมูลที่จำเป็นไม่มีให้สำหรับคอมไพลเลอร์ซึ่งแตกต่างจากฟังก์ชันที่ไม่ใช่ varargs กับต้นแบบ
EOF

3
โอ๊ะ ... "แปรปรวน" ฉันเพิ่งเรียนรู้คำใหม่ ...
Mike Robinson


3
สิ่งต่อไปที่การพยายามคือการผ่านการ(uint64_t)0แทน0และดูว่าคุณยังคงได้รับพฤติกรรมสุ่ม (สมมติdoubleและuint64_tมีขนาดเท่ากันและการจัดตำแหน่ง) โอกาสที่เอาต์พุตจะยังคงเป็นแบบสุ่มในบางแพลตฟอร์ม (เช่น x86_64) เนื่องจากมีการส่งผ่านประเภทต่างๆในรีจิสเตอร์ที่แตกต่างกัน
Ian Abbott

คำตอบ:


122

รูปแบบต้องมีข้อโต้แย้งของพิมพ์"%f" คุณกำลังให้มันข้อโต้แย้งของพิมพ์double intนั่นเป็นสาเหตุที่พฤติกรรมไม่ได้กำหนด

มาตรฐานไม่ได้รับประกันว่าทุกบิตเป็นศูนย์เป็นตัวแทนที่ถูกต้อง0.0(แม้ว่ามันมักจะเป็น) หรือของใด ๆ ที่doubleคุ้มค่าหรือว่าintและdoubleมีขนาดเท่ากัน (จำมันdoubleไม่ได้float) หรือถึงแม้ว่าพวกเขาจะเหมือนกัน ขนาดที่ส่งผ่านเป็นอาร์กิวเมนต์ไปยังฟังก์ชันตัวแปรในลักษณะเดียวกัน

อาจเกิดขึ้นกับ "งาน" ในระบบของคุณ นั่นเป็นอาการที่แย่ที่สุดที่เป็นไปได้ของพฤติกรรมที่ไม่ได้กำหนดเพราะทำให้ยากที่จะวินิจฉัยข้อผิดพลาด

N1570 7.21.6.1 วรรค 9:

... หากอาร์กิวเมนต์ใด ๆ ไม่ใช่ประเภทที่ถูกต้องสำหรับข้อกำหนดการแปลงที่เกี่ยวข้องพฤติกรรมนั้นจะไม่ถูกกำหนด

อาร์กิวเมนต์ประเภทfloatได้รับการเลื่อนระดับdoubleซึ่งเป็นเหตุผลที่ใช้printf("%f\n",0.0f)งานได้ อาร์กิวเมนต์ชนิดจำนวนเต็มแคบกว่าintจะเลื่อนตำแหน่งให้เป็นหรือint unsigned intกฎระเบียบเหล่านี้โปรโมชั่น (ระบุโดย N1570 6.5.2.2 วรรค 6) printf("%f\n", 0)ไม่ได้ช่วยในกรณีของ

โปรดทราบว่าหากคุณส่งค่าคงที่0ไปยังฟังก์ชันที่ไม่ใช่ตัวแปรที่คาดว่าจะมีdoubleอาร์กิวเมนต์พฤติกรรมนั้นจะถูกกำหนดไว้อย่างดีโดยสมมติว่าสามารถมองเห็นต้นแบบของฟังก์ชันได้ ตัวอย่างเช่นsqrt(0)(หลัง#include <math.h>) แปลงอาร์กิวเมนต์โดยปริยาย0จากintเป็นdouble- เนื่องจากคอมไพเลอร์สามารถเห็นได้จากการประกาศsqrtว่าคาดว่าจะมีdoubleอาร์กิวเมนต์ printfมันไม่มีข้อมูลดังกล่าวเพื่อ ฟังก์ชัน Variadic printfเป็นแบบพิเศษและต้องการความระมัดระวังในการเขียนเรียกใช้


13
ประเด็นหลักที่ยอดเยี่ยมสองสามข้อที่นี่ ประการแรกว่ามันdoubleไม่เป็นfloatเช่นนั้นสมมติฐานความกว้างของ OP อาจไม่ (อาจไม่) ประการที่สองสมมติฐานที่ว่าจำนวนเต็มศูนย์และศูนย์ทศนิยมมีรูปแบบบิตเดียวกันก็ไม่ถือเช่นกัน Good work
Lightness Races in Orbit

2
@LucasTrzesniewski: โอเค แต่ฉันไม่เห็นว่าคำตอบของฉันทำให้เกิดคำถามอย่างไร ฉันระบุว่าfloatได้รับการเลื่อนตำแหน่งdoubleโดยไม่ได้อธิบายว่าทำไม แต่นั่นไม่ใช่ประเด็นหลัก
Keith Thompson

2
@ robertbristow-johnson: คอมไพเลอร์ไม่จำเป็นต้องมีตะขอพิเศษสำหรับprintfแม้ว่า gcc จะมีบางส่วนเพื่อให้สามารถวินิจฉัยข้อผิดพลาดได้ ( หากสตริงรูปแบบเป็นตัวอักษร) คอมไพเลอร์สามารถดูประกาศของprintfจาก<stdio.h>ที่บอกว่าพารามิเตอร์แรกเป็นและส่วนที่เหลือจะมีการแสดงconst char* , ...ไม่%fเป็นdouble(และfloatจะเลื่อนตำแหน่งให้เป็นdouble) และเป็น%lf long doubleมาตรฐาน C ไม่ได้บอกอะไรเกี่ยวกับสแต็ก ระบุลักษณะการทำงานprintfเฉพาะเมื่อเรียกถูกต้องเท่านั้น
Keith Thompson

2
@ robertbristow-johnson: ในความงุนงงในสมัยก่อน "ผ้าสำลี" มักจะทำการตรวจสอบเพิ่มเติมที่ gcc ดำเนินการอยู่ในขณะนี้ floatผ่านไปprintfจะเลื่อนตำแหน่งให้เป็นdouble; ไม่มีอะไรวิเศษเกี่ยวกับเรื่องนี้มันเป็นเพียงกฎภาษาสำหรับการเรียกฟังก์ชันตัวแปร printfตัวเองรู้ผ่านสตริงรูปแบบสิ่งที่ผู้โทรอ้างว่าส่งผ่านไป หากการอ้างสิทธิ์นั้นไม่ถูกต้องจะไม่มีการกำหนดพฤติกรรม
Keith Thompson

2
การแก้ไขขนาดเล็กที่: lความยาวปรับปรุง "ไม่มีผลต่อการต่อไปa, A, e, E, f, F, gหรือGการแปลงระบุ" ปรับความยาวสำหรับการแปลงเป็นlong double L(@ robertbristow-johnson ก็อาจสนใจ)
Daniel Fischer

58

ก่อนอื่นตามที่สัมผัสในคำตอบอื่น ๆ หลายคำ แต่ไม่ใช่ในใจของฉันสะกดชัดเจนเพียงพอ: ใช้งานได้เพื่อระบุจำนวนเต็มในบริบทส่วนใหญ่ที่ฟังก์ชันไลบรารีรับdoubleหรือfloatอาร์กิวเมนต์ คอมไพเลอร์จะแทรกการแปลงโดยอัตโนมัติ ตัวอย่างเช่นsqrt(0)มีการกำหนดไว้อย่างชัดเจนและจะทำงานเหมือนกันsqrt((double)0)และเช่นเดียวกับนิพจน์ประเภทจำนวนเต็มอื่น ๆ ที่ใช้ที่นั่น

printfแตกต่างกัน แตกต่างกันเพราะต้องใช้อาร์กิวเมนต์เป็นจำนวนตัวแปร ต้นแบบฟังก์ชันคือ

extern int printf(const char *fmt, ...);

ดังนั้นเมื่อคุณเขียน

printf(message, 0);

คอมไพเลอร์ไม่มีข้อมูลเกี่ยวกับประเภทที่printf คาดว่าอาร์กิวเมนต์ที่สองจะเป็น มันมีเฉพาะประเภทของนิพจน์อาร์กิวเมนต์ซึ่งเป็นintไปตามนั้น ดังนั้นสิ่งที่แตกต่างจากฟังก์ชันไลบรารีส่วนใหญ่จะอยู่ที่คุณโปรแกรมเมอร์เพื่อให้แน่ใจว่ารายการอาร์กิวเมนต์ตรงกับความคาดหวังของสตริงรูปแบบ

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

ตอนนี้อีกครึ่งหนึ่งของคำถามคือ: ระบุว่า (int) 0 และ (float) 0.0 อยู่ในระบบที่ทันสมัยที่สุดทั้งคู่แสดงเป็น 32 บิตซึ่งทั้งหมดเป็นศูนย์ทำไมมันถึงไม่ทำงานโดยบังเอิญ? มาตรฐาน C บอกเพียงว่า "สิ่งนี้ไม่จำเป็นในการทำงานคุณอยู่คนเดียว" แต่ขอให้ฉันระบุสาเหตุที่พบบ่อยที่สุดสองประการที่ทำให้ไม่ได้ผล ซึ่งอาจช่วยให้คุณเข้าใจว่าเหตุใดจึงไม่จำเป็นต้องใช้

ครั้งแรกสำหรับเหตุผลทางประวัติศาสตร์เมื่อคุณผ่านการfloatผ่านรายการอาร์กิวเมนต์ตัวแปรที่จะได้รับการเลื่อนตำแหน่งไปdoubleซึ่งในระบบที่ทันสมัยที่สุดเป็น64บิตกว้าง ดังนั้นจึงprintf("%f", 0)ส่งผ่านเพียง 32 ศูนย์บิตไปยัง callee โดยคาดหวัง 64 ของพวกเขา

เหตุผลประการที่สองที่สำคัญไม่แพ้กันคืออาร์กิวเมนต์ของฟังก์ชันทศนิยมอาจถูกส่งไปในที่อื่นที่ไม่ใช่อาร์กิวเมนต์จำนวนเต็ม ตัวอย่างเช่นซีพียูส่วนใหญ่มีไฟล์รีจิสเตอร์แยกกันสำหรับจำนวนเต็มและค่าทศนิยมดังนั้นจึงอาจเป็นกฎที่อาร์กิวเมนต์ 0 ถึง 4 จะเข้าสู่การลงทะเบียน r0 ถึง r4 หากเป็นจำนวนเต็ม แต่ f0 ถึง f4 หากเป็นทศนิยม ดังนั้นprintf("%f", 0)ดูใน register f1 สำหรับศูนย์นั้น แต่มันไม่มีเลย


1
มีสถาปัตยกรรมใดบ้างที่ใช้รีจิสเตอร์สำหรับฟังก์ชันแบบแปรผันแม้กระทั่งในรูปแบบที่ใช้สำหรับฟังก์ชันปกติ ฉันคิดว่านั่นเป็นเหตุผลที่ต้องมีการประกาศฟังก์ชันตัวแปรอย่างถูกต้องแม้ว่าจะสามารถประกาศฟังก์ชันอื่น ๆ [ยกเว้นฟังก์ชันที่มีอาร์กิวเมนต์ float / short / char] ด้วย()ก็ตาม
Random832

3
@ Random832 ปัจจุบันความแตกต่างเพียงอย่างเดียวระหว่างรูปแบบการเรียกของตัวแปรและฟังก์ชันปกติคืออาจมีข้อมูลเพิ่มเติมบางอย่างที่ให้มากับตัวแปรเช่นจำนวนอาร์กิวเมนต์ที่แท้จริงที่ให้มา มิฉะนั้นทุกอย่างจะเข้าที่เดิมทุกประการสำหรับฟังก์ชันปกติ ดูตัวอย่างส่วน 3.2 ของx86-64.org/documentation/abi.pdfซึ่งการปฏิบัติพิเศษเฉพาะสำหรับตัวแปรคือคำใบ้ที่ส่งเข้าALมา (ใช่หมายความว่าการนำไปใช้va_argนั้นซับซ้อนกว่าที่เคยเป็นมามาก)
zwol

@ Random832: ฉันคิดเสมอว่าเหตุผลก็คือในบางฟังก์ชันสถาปัตยกรรมที่มีจำนวนและประเภทของอาร์กิวเมนต์ที่ทราบสามารถนำไปใช้งานได้อย่างมีประสิทธิภาพมากขึ้นโดยใช้คำสั่งพิเศษ
celtschk

@celtschk คุณอาจนึกถึง "หน้าต่างลงทะเบียน" บน SPARC และ IA64 ซึ่งควรจะเร่งกรณีการเรียกใช้ฟังก์ชันทั่วไปด้วยอาร์กิวเมนต์จำนวนน้อย (อนิจจาในทางปฏิบัติพวกเขาทำตรงกันข้าม) พวกเขาไม่ต้องการให้คอมไพเลอร์จัดการกับการเรียกฟังก์ชันตัวแปรโดยเฉพาะเนื่องจากจำนวนอาร์กิวเมนต์ในไซต์การเรียกใดไซต์หนึ่งจะเป็นค่าคงที่เวลาคอมไพล์เสมอไม่ว่าคาลลีจะเป็นตัวแปรหรือไม่ก็ตาม
zwol

@zwol: ไม่ฉันกำลังคิดถึงret nคำสั่งของ 8086 ซึ่งnเป็นจำนวนเต็มแบบฮาร์ดโค้ดซึ่งไม่สามารถใช้กับฟังก์ชันตัวแปรได้ อย่างไรก็ตามฉันไม่รู้ว่าคอมไพเลอร์ C ตัวใดใช้ประโยชน์จากมันจริง ๆ (คอมไพเลอร์ที่ไม่ใช่ C ทำ)
celtschk

13

โดยปกติเมื่อคุณเรียกใช้ฟังก์ชันที่ต้องการ a doubleแต่คุณระบุintคอมไพลเลอร์จะแปลงเป็น a doubleให้คุณโดยอัตโนมัติ สิ่งนี้ไม่ได้เกิดขึ้นprintfเนื่องจากไม่ได้ระบุประเภทของอาร์กิวเมนต์ไว้ในฟังก์ชันต้นแบบ - คอมไพเลอร์ไม่ทราบว่าควรใช้การแปลง


4
นอกจากนี้printf() โดยเฉพาะได้รับการออกแบบมาเพื่อให้อาร์กิวเมนต์เป็นประเภทใดก็ได้ คุณต้องทราบว่าแต่ละองค์ประกอบในรูปแบบสตริงคาดหวังประเภทใดและคุณต้องระบุอย่างถูกต้อง
Mike Robinson

@ MikeRobinson: อืมประเภท C ดั้งเดิมใด ๆ ซึ่งเป็นส่วนย่อยที่เล็กมากของประเภทที่เป็นไปได้ทั้งหมด
MSalters

13

เหตุใดการใช้ลิเทอรัลจำนวนเต็มแทนที่จะเป็นลิเทอรัลแบบลอยจึงทำให้เกิดพฤติกรรมนี้

เนื่องจากprintf()ไม่ได้พิมพ์พารามิเตอร์นอกเหนือจากพารามิเตอร์const char* formatstringที่ 1 ใช้จุดไข่ปลาสไตล์ c ( ...) สำหรับส่วนที่เหลือทั้งหมด

เป็นเพียงการตัดสินใจว่าจะตีความค่าที่ส่งผ่านไปอย่างไรตามประเภทการจัดรูปแบบที่กำหนดในสตริงรูปแบบ

คุณจะมีพฤติกรรมที่ไม่ได้กำหนดแบบเดียวกับเมื่อพยายาม

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB

3
การนำไปใช้งานบางอย่างprintfอาจทำงานในลักษณะนั้น (ยกเว้นว่ารายการที่ส่งผ่านเป็นค่าไม่ใช่ที่อยู่) มาตรฐาน C ไม่ได้ระบุว่า printfฟังก์ชันตัวแปรอื่น ๆ ทำงานอย่างไร แต่จะระบุพฤติกรรมของมัน โดยเฉพาะอย่างยิ่งไม่มีการพูดถึงสแต็กเฟรม
Keith Thompson

เล่นลิ้นเล็ก ๆ : printfจะมีหนึ่งconst char*พารามิเตอร์ที่พิมพ์สตริงรูปแบบซึ่งเป็นประเภท BTW คำถามถูกแท็กทั้ง C และ C ++ และ C มีความเกี่ยวข้องมากกว่า ฉันคงไม่ได้ใช้reinterpret_castเป็นตัวอย่าง
Keith Thompson

ข้อสังเกตที่น่าสนใจ: พฤติกรรมที่ไม่ได้กำหนดเหมือนกันและน่าจะเกิดจากกลไกที่เหมือนกัน แต่มีความแตกต่างเล็กน้อยในรายละเอียด: การส่ง int ตามคำถาม UB จะเกิดขึ้นภายใน printf เมื่อพยายามตีความ int เป็น double - ในตัวอย่างของคุณ มันเกิดขึ้นแล้วภายนอกเมื่อ dereferencing pf ...
Aconcagua

@Aconcagua เพิ่มคำชี้แจง
πάνταῥεῖ

ตัวอย่างโค้ดนี้เป็น UB สำหรับการละเมิดนามแฝงที่เข้มงวดซึ่งเป็นปัญหาที่แตกต่างอย่างสิ้นเชิงกับคำถามที่ถามเกี่ยวกับ ตัวอย่างเช่นคุณเพิกเฉยต่อความเป็นไปได้ที่การลอยตัวจะถูกส่งผ่านในการลงทะเบียนที่แตกต่างกันไปเป็นจำนวนเต็ม
MM

12

การใช้ตัวprintf()ระบุ"%f"และประเภทที่จับคู่(int) 0ไม่ถูกต้องนำไปสู่พฤติกรรมที่ไม่ได้กำหนด

หากข้อกำหนดการแปลงไม่ถูกต้องจะไม่มีการกำหนดลักษณะการทำงาน C11dr §7.21.6.1 9

สาเหตุผู้สมัครของ UB

  1. มันเป็น UB ต่อข้อมูลจำเพาะและการคอมไพล์นั้นหรูหรา - 'nuf กล่าว

  2. doubleและintมีขนาดแตกต่างกัน

  3. doubleและintอาจส่งผ่านค่าโดยใช้สแต็กที่แตกต่างกัน (ทั่วไปเทียบกับสแต็กFPU )

  4. double 0.0 อาจไม่ได้รับการกำหนดโดยทุกรูปแบบศูนย์บิต (หายาก)


10

นี่เป็นหนึ่งในโอกาสที่ดีในการเรียนรู้จากคำเตือนของคอมไพเลอร์ของคุณ

$ gcc -Wall -Wextra -pedantic fnord.c 
fnord.c: In function ‘main’:
fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
  printf("%f\n",0);
  ^

หรือ

$ clang -Weverything -pedantic fnord.c 
fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
        printf("%f\n",0);
                ~~    ^
                %d
1 warning generated.

ดังนั้นprintfกำลังสร้างพฤติกรรมที่ไม่ได้กำหนดเพราะคุณส่งผ่านประเภทของอาร์กิวเมนต์ที่เข้ากันไม่ได้


9

ฉันไม่แน่ใจว่ามีอะไรสับสน

สตริงรูปแบบของคุณคาดว่าจะมีdouble; คุณระบุไฟล์intไฟล์.

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


3
@Voo: น่าเสียดายที่ตัวปรับแต่งสตริงรูปแบบนั้นถูกตั้งชื่อ แต่ฉันยังไม่เห็นว่าทำไมคุณถึงคิดว่าintมันจะเป็นที่ยอมรับที่นี่
Lightness Races ใน Orbit

1
@Voo: "(ซึ่งถือว่าเป็นรูปแบบโฟลตที่ถูกต้องด้วย)"ทำไมจึงมีintคุณสมบัติเป็นรูปแบบโฟลตที่ถูกต้อง ส่วนประกอบของ Two และการเข้ารหัสทศนิยมต่างๆแทบไม่มีอะไรเหมือนกัน
Lightness Races ใน Orbit

2
มันสับสนเพราะสำหรับฟังก์ชันไลบรารีส่วนใหญ่การระบุลิเทอรัลจำนวนเต็ม0ให้กับอาร์กิวเมนต์ที่พิมพ์doubleจะทำสิ่งที่ถูกต้อง มันไม่ได้เป็นที่ชัดเจนที่จะเริ่มต้นที่คอมไพเลอร์ไม่ได้ทำแปลงเดียวกันสำหรับช่องโต้แย้งการแก้ไขโดยprintf %[efg]
zwol

1
@Voo: หากคุณสนใจว่าความผิดพลาดที่น่ากลัวนี้สามารถเกิดขึ้นได้อย่างไรให้พิจารณาว่าบน x86-64 SysV ABI อาร์กิวเมนต์ทศนิยมจะถูกส่งผ่านในชุดการลงทะเบียนที่แตกต่างจากอาร์กิวเมนต์จำนวนเต็ม
EOF

1
@LightnessRacesinOrbit ฉันคิดว่ามันเหมาะสมเสมอที่จะพูดถึงว่าทำไมบางอย่างถึงเป็น UB ซึ่งมักจะเกี่ยวข้องกับการพูดถึงสิ่งที่อนุญาตให้ใช้ละติจูดได้และสิ่งที่เกิดขึ้นจริงในกรณีทั่วไป
zwol

4

"%f\n"รับประกันผลลัพธ์ที่คาดเดาได้ก็ต่อเมื่อprintf()พารามิเตอร์ที่สองมีประเภทของdouble. ถัดไปอาร์กิวเมนต์พิเศษของฟังก์ชันตัวแปรจะอยู่ภายใต้การส่งเสริมอาร์กิวเมนต์เริ่มต้น อาร์กิวเมนต์จำนวนเต็มอยู่ภายใต้การส่งเสริมจำนวนเต็มซึ่งไม่ส่งผลให้เกิดค่าที่พิมพ์ด้วยทศนิยม และพารามิเตอร์การเลื่อนตำแหน่งให้floatdouble

ปิดท้าย: มาตรฐานอนุญาตให้อาร์กิวเมนต์ที่สองเป็นหรือfloatหรือdoubleไม่มีอะไรอื่น


4

เหตุใดจึงมีการกล่าวถึง UB อย่างเป็นทางการในหลายคำตอบ

สาเหตุที่คุณได้รับโดยเฉพาะพฤติกรรมนี้ขึ้นอยู่กับแพลตฟอร์ม แต่อาจเป็นดังต่อไปนี้:

  • printfคาดว่าอาร์กิวเมนต์ตามการขยายพันธุ์ vararg มาตรฐาน นั่นหมายความว่าfloatพินัยกรรมเป็นdoubleและอะไรที่เล็กกว่าintจะเป็นintจะเป็น
  • คุณกำลังส่งผ่านโดยintที่ฟังก์ชันต้องการ a double. ของคุณintน่าจะเป็น 32 บิตdouble64 บิตของคุณ นั่นหมายความว่าสี่ไบต์สแต็กที่เริ่มต้นจากตำแหน่งที่อาร์กิวเมนต์ควรจะนั่งอยู่0แต่สี่ไบต์ต่อไปนี้มีเนื้อหาที่กำหนดเอง นั่นคือสิ่งที่ใช้ในการสร้างมูลค่าที่แสดง

0

สาเหตุหลักของปัญหา "ค่าที่ไม่ได้กำหนด" นี้อยู่ในตัวชี้ที่intค่าที่ส่งผ่านไปยังprintfส่วนพารามิเตอร์ตัวแปรไปยังตัวชี้ที่doubleประเภทที่va_argมาโครดำเนินการ

สิ่งนี้ทำให้เกิดการอ้างอิงไปยังพื้นที่หน่วยความจำที่ไม่ได้เตรียมใช้งานอย่างสมบูรณ์พร้อมกับค่าที่ส่งผ่านเป็นพารามิเตอร์ไปยัง printf เนื่องจากdoubleขนาดพื้นที่บัฟเฟอร์หน่วยความจำมากกว่าintขนาด

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


สามารถพิจารณาส่วนที่เฉพาะเจาะจงเหล่านี้ของการติดตั้งโค้ดที่มีความหมายของ "printf" และ "va_arg" ...

printf

va_list arg;
....
case('%f')
      va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf..
.... 


การใช้งานจริงใน vprintf (พิจารณาจาก gnu im.) ของการจัดการกรณีรหัสพารามิเตอร์ค่าคู่คือ:

if (__ldbl_is_dbl)
{
   args_value[cnt].pa_double = va_arg (ap_save, double);
   ...
}



va_arg

char *p = (double *) &arg + sizeof arg;  //printf parameters area pointer

double i2 = *((double *)p); //casting to double because va_arg(arg, double)
   p += sizeof (double);



การอ้างอิง

  1. gnu โครงการ glibc การดำเนินการ "printf" (vprintf))
  2. ตัวอย่างรหัสความหมายของ printf
  3. ตัวอย่างรหัส semplification ของ va_arg
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.