เหตุใด printf ที่มีอาร์กิวเมนต์เดียว (ไม่มีตัวระบุการแปลง) จึงเลิกใช้งาน


102

ในหนังสือที่ฉันกำลังอ่านมีเขียนว่าprintfด้วยอาร์กิวเมนต์เดียว (โดยไม่มีตัวระบุการแปลง) จะเลิกใช้งานแล้ว ขอแนะนำให้เปลี่ยน

printf("Hello World!");

ด้วย

puts("Hello World!");

หรือ

printf("%s", "Hello World!");

ใครช่วยบอกทีว่าทำไมถึงprintf("Hello World!");ผิด? มีเขียนไว้ในหนังสือว่ามีช่องโหว่ ช่องโหว่เหล่านี้คืออะไร?


34
หมายเหตุ: printf("Hello World!")เป็นไม่ได้puts("Hello World!")เช่นเดียวกับ puts()ผนวกก'\n'. แทนที่จะเปรียบเทียบprintf("abc")กับfputs("abc", stdout)
chux - Reinstate Monica

5
หนังสือเล่มนั้นคืออะไร? ฉันไม่คิดว่าprintfจะเลิกใช้งานในลักษณะเดียวกับที่เช่นgetsเลิกใช้งานใน C99 ดังนั้นคุณอาจพิจารณาแก้ไขคำถามของคุณให้แม่นยำยิ่งขึ้น
el.pescado

14
ดูเหมือนว่าหนังสือที่คุณกำลังอ่านนั้นไม่ค่อยดีนักหนังสือที่ดีไม่ควรพูดทำนองนี้ว่า "เลิกใช้แล้ว" (ซึ่งเป็นเท็จจริง ๆ เว้นแต่ผู้เขียนจะใช้คำนั้นเพื่ออธิบายความคิดเห็นของตนเอง) และควรอธิบายว่าการใช้ เป็นสิ่งที่ไม่ถูกต้องและเป็นอันตรายแทนที่จะแสดงรหัสปลอดภัย / ถูกต้องเป็นตัวอย่างของสิ่งที่คุณ "ไม่ควรทำ"
R .. GitHub STOP HELPING ICE

8
คุณสามารถระบุหนังสือได้หรือไม่?
Keith Thompson

7
โปรดระบุชื่อหนังสือผู้แต่งและการอ้างอิงหน้า ขอบคุณ.
Greenonline

คำตอบ:


122

printf("Hello World!"); IMHO ไม่เสี่ยง แต่ให้พิจารณาสิ่งนี้:

const char *str;
...
printf(str);

หากstrเกิดขึ้นเพื่อชี้ไปที่สตริงที่มีตัว%sระบุรูปแบบโปรแกรมของคุณจะแสดงพฤติกรรมที่ไม่ได้กำหนดไว้ (ส่วนใหญ่เป็นข้อขัดข้อง) ในขณะที่puts(str)จะแสดงสตริงตามที่เป็นอยู่

ตัวอย่าง:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s\n"

21
นอกจากจะทำให้โปรแกรมขัดข้องแล้วยังมีช่องโหว่อื่น ๆ อีกมากมายที่เป็นไปได้ด้วยสตริงรูปแบบ ดูข้อมูลเพิ่มเติมได้ที่นี่: en.wikipedia.org/wiki/Uncontrolled_format_string
e.dan

9
อีกเหตุผลหนึ่งคือputsน่าจะเร็วกว่า
edmz

38
@black: putsคือ "สันนิษฐาน" ได้เร็วขึ้นและนี่อาจจะเป็นเหตุผลที่คนแนะนำให้มันอีก แต่มันไม่ได้เป็นจริงได้เร็วขึ้น ฉันพิมพ์แค่"Hello, world!"1,000,000 ครั้งทั้งสองวิธี โดยprintfใช้เวลา 0.92 วินาที โดยputsใช้เวลา 0.93 วินาที มีหลายสิ่งที่ต้องกังวลเกี่ยวกับประสิทธิภาพ แต่printfกับputsไม่ใช่หนึ่งในนั้น
Steve Summit

10
@KonstantinWeitz: แต่ (ก) ฉันไม่ได้ใช้ gcc และ (b) ไม่สำคัญว่าทำไมการอ้างสิทธิ์ " putsเร็วกว่า" จึงเป็นเท็จ แต่ก็ยังเป็นเท็จ
Steve Summit

6
@KonstantinWeitz: การอ้างสิทธิ์ที่ฉันให้หลักฐานคือ (ตรงกันข้ามกับ) ผู้ใช้อ้างว่าเป็นสีดำ ฉันแค่พยายามชี้แจงว่าโปรแกรมเมอร์ไม่ควรกังวลเกี่ยวกับการโทรputsด้วยเหตุผลนี้ (แต่ถ้าคุณต้องการโต้แย้งเกี่ยวกับเรื่องนี้ฉันจะแปลกใจถ้าคุณสามารถหาคอมไพเลอร์ที่ทันสมัยสำหรับเครื่องจักรสมัยใหม่ใด ๆ ที่putsเร็วกว่าในprintfทุกสถานการณ์)
Steve Summit

75

printf("Hello world");

ใช้ได้ดีและไม่มีช่องโหว่ด้านความปลอดภัย

ปัญหาอยู่ที่:

printf(p);

ที่pเป็นตัวชี้การป้อนข้อมูลที่ถูกควบคุมโดยผู้ใช้ มีแนวโน้มที่จะจัด รูปแบบการโจมตีสตริง : ผู้ใช้สามารถแทรกข้อกำหนดการแปลงเพื่อควบคุมโปรแกรมเช่น%xการถ่ายโอนข้อมูลหน่วยความจำหรือ%nเพื่อเขียนทับหน่วยความจำ

โปรดทราบว่าputs("Hello world")จะไม่เทียบเท่าในพฤติกรรมการแต่printf("Hello world") คอมไพเลอร์มักจะฉลาดพอที่จะเพิ่มประสิทธิภาพการโทรหลังที่จะแทนที่ด้วยprintf("Hello world\n")puts


10
แน่นอน จะเป็นเช่นเดียวกับปัญหาหากผู้ใช้มีการควบคุมprintf(p,x) pดังนั้นปัญหาไม่ใช่การใช้printfกับอาร์กิวเมนต์เดียว แต่ใช้กับสตริงรูปแบบที่ผู้ใช้ควบคุม
Hagen von Eitzen

2
@HagenvonEitzen นั่นเป็นความจริงในทางเทคนิค แต่มีเพียงไม่กี่คนที่จงใจใช้สตริงรูปแบบที่ผู้ใช้ระบุ เวลาคนเขียนprintf(p)นั่นเป็นเพราะพวกเขาไม่รู้ว่ามันเป็นสตริงรูปแบบพวกเขาแค่คิดว่ากำลังพิมพ์ตัวอักษร
Barmar

33

นอกจากคำตอบอื่น ๆprintf("Hello world! I am 50% happy today")ยังเป็นข้อผิดพลาดที่ทำได้ง่ายซึ่งอาจทำให้เกิดปัญหาหน่วยความจำที่น่ารังเกียจได้ทุกรูปแบบ (มันคือ UB!)

มันเป็นเพียงง่ายขึ้นง่ายขึ้นและมีประสิทธิภาพมากขึ้นเพื่อ "ต้องการ" การเขียนโปรแกรมเพื่อเป็นอย่างชัดเจนเมื่อพวกเขาต้องการสตริงคำต่อคำและไม่มีอะไรอื่น

และนั่นคือสิ่งที่printf("%s", "Hello world! I am 50% happy today")ทำให้คุณได้รับ มันเข้าใจผิดทั้งหมด

(แน่นอนว่าสตีฟprintf("He has %d cherries\n", ncherries)ไม่ใช่สิ่งเดียวกันอย่างแน่นอนในกรณีนี้โปรแกรมเมอร์ไม่ได้อยู่ในกรอบความคิด "สตริงคำต่อคำ" เธออยู่ในกรอบความคิด "สตริงรูปแบบ")


2
สิ่งนี้ไม่คุ้มค่าที่จะโต้แย้งและฉันเข้าใจว่าคุณพูดอะไรเกี่ยวกับความคิดแบบคำต่อคำกับรูปแบบสตริง แต่ก็ไม่ใช่ทุกคนที่คิดแบบนั้นซึ่งเป็นเหตุผลหนึ่งที่กฎขนาดเดียวที่เหมาะกับทุกคนสามารถจัดอันดับได้ การพูดว่า "ไม่เคยพิมพ์สตริงคงที่ด้วยprintf" ก็เหมือนกับการพูดว่า "เขียนเสมอif(NULL == p)กฎเหล่านี้อาจมีประโยชน์สำหรับโปรแกรมเมอร์บางคน แต่ไม่ใช่ทั้งหมดและในทั้งสองกรณี ( printfรูปแบบที่ไม่ตรงกันและเงื่อนไขของ Yoda) คอมไพเลอร์สมัยใหม่จะเตือนเกี่ยวกับความผิดพลาดอยู่ดี ดังนั้นกฎเทียมจึงมีความสำคัญน้อยลง
Steve Summit

1
@Steve หากมีการใช้บางอย่างเป็นศูนย์ แต่มีข้อเสียอยู่เล็กน้อยใช่แล้วไม่มีเหตุผลที่จะใช้มัน เงื่อนไข Yoda บนมืออื่น ๆที่ต้องทำมีข้อเสียที่พวกเขาทำรหัสที่ยากต่อการอ่าน (ที่คุณต้องการอย่างสังหรณ์ใจพูดว่า "ถ้า P เป็นศูนย์" ไม่ได้ "ถ้าเป็นศูนย์ p")
Voo

2
@ วูprintf("%s", "hello")จะช้ากว่าprintf("hello")จึงมีข้อเสีย มีขนาดเล็กเนื่องจาก IO มักจะช้ากว่าการจัดรูปแบบธรรมดาเช่นนี้ แต่มีข้อเสีย
Yakk - Adam Nevraumont

1
@Yakk ฉันสงสัยว่าจะช้าลง
MM

gcc -Wall -W -Werrorจะป้องกันผลเสียจากความผิดพลาดดังกล่าว
chqrlie

17

ฉันจะเพิ่มข้อมูลเล็กน้อยเกี่ยวกับส่วนช่องโหว่ที่นี่

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

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

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

ตัวอย่าง (C):

int main(int argc, char *argv[])
{
    printf(argv[argc - 1]); // takes the first argument if it exists
}

ถ้าฉันใส่เป็นอินพุตของโปรแกรมนี้ "%08x %08x %08x %08x %08x\n"

printf ("%08x %08x %08x %08x %08x\n"); 

คำสั่งนี้สั่งให้ฟังก์ชัน printf ดึงพารามิเตอร์ห้าตัวจากสแต็กและแสดงเป็นตัวเลขฐานสิบหกที่มีเบาะ 8 หลัก ดังนั้นผลลัพธ์ที่เป็นไปได้อาจมีลักษณะดังนี้:

40012980 080628c4 bffff7a4 00000005 08059c04

ดูนี้สำหรับคำอธิบายที่สมบูรณ์มากขึ้นและตัวอย่างอื่น ๆ


13

การโทรprintfด้วยสตริงรูปแบบตามตัวอักษรนั้นปลอดภัยและมีประสิทธิภาพและมีเครื่องมือที่จะเตือนคุณโดยอัตโนมัติหากการเรียกprintfใช้สตริงรูปแบบที่ผู้ใช้ระบุนั้นไม่ปลอดภัย

การโจมตีที่รุนแรงที่สุดในการprintfใช้ประโยชน์จากตัว%nระบุรูปแบบ ในทางตรงกันข้ามกับทุก specifiers รูปแบบอื่น ๆ เช่น%d, %nจริงเขียนมูลค่าให้กับหน่วยความจำที่อยู่ที่ให้ไว้ในหนึ่งของการขัดแย้งรูปแบบ ซึ่งหมายความว่าผู้โจมตีสามารถเขียนทับหน่วยความจำและอาจเข้าควบคุมโปรแกรมของคุณได้ Wikipedia ให้รายละเอียดเพิ่มเติม

หากคุณโทรprintfด้วยสตริงรูปแบบตามตัวอักษรผู้โจมตีจะไม่สามารถแอบ%nเข้าไปในสตริงรูปแบบของคุณได้และคุณจะปลอดภัย ในความเป็นจริง gcc จะเปลี่ยนการโทรของคุณprintfเป็นการโทรไปputsดังนั้นจึงไม่มีความแตกต่างใด ๆ (ทดสอบโดยการเรียกใช้gcc -O3 -S)

หากคุณโทรprintfด้วยสตริงรูปแบบที่ผู้ใช้ระบุไว้ผู้โจมตีอาจแอบ%nเข้าไปในสตริงรูปแบบของคุณและเข้าควบคุมโปรแกรมของคุณ -Wformat-securityคอมไพเลอร์ของคุณมักจะเตือนคุณว่าเขาจะไม่ปลอดภัยให้ดู นอกจากนี้ยังมีเครื่องมือที่ทันสมัยมากขึ้นว่าให้มั่นใจว่าการภาวนาของความปลอดภัยแม้จะมีผู้ใช้บริการที่มีให้สตริงรูปแบบและพวกเขาก็อาจจะตรวจสอบว่าคุณผ่านจำนวนที่เหมาะสมและประเภทของข้อโต้แย้งprintf printfตัวอย่างเช่นสำหรับ Java มีข้อผิดพลาดของ Google คว่ำ และตรวจสอบกรอบ


12

นี่เป็นคำแนะนำที่เข้าใจผิด ใช่ถ้าคุณมีสตริงรันไทม์ที่จะพิมพ์

printf(str);

ค่อนข้างอันตรายและคุณควรใช้เสมอ

printf("%s", str);

แทนเพราะโดยทั่วไปคุณไม่มีทางรู้ได้ว่าstrอาจมี%เครื่องหมายหรือไม่ อย่างไรก็ตามหากคุณมีสตริงค่าคงที่ของเวลาคอมไพล์ก็ไม่มีอะไรผิดปกติ

printf("Hello, world!\n");

(เหนือสิ่งอื่นใดนั่นคือโปรแกรม C ที่คลาสสิกที่สุดเท่าที่เคยมีมาจากหนังสือการเขียนโปรแกรม C ของ Genesis ดังนั้นใครก็ตามที่ปฏิเสธการใช้งานนั้นจะค่อนข้างนอกคอกและฉันสำหรับคนหนึ่งจะค่อนข้างขุ่นเคือง!)


because printf's first argument is always a constant stringฉันไม่แน่ใจว่าคุณหมายถึงอะไร
Sebastian Mach

อย่างที่บอกว่า"He has %d cherries\n"เป็นสตริงคงที่หมายความว่ามันเป็นค่าคงที่เวลาคอมไพล์ แต่เพื่อความเป็นธรรมคำแนะนำของผู้เขียนไม่ใช่ "อย่าส่งผ่านสตริงคงที่เป็นprintfอาร์กิวเมนต์แรก" แต่ก็คือ "อย่าส่งผ่านสตริงโดยไม่%เป็นprintfอาร์กิวเมนต์แรกของ"
Steve Summit

literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical- คุณไม่ได้อ่าน K&R ในช่วงไม่กี่ปีที่ผ่านมา มีคำแนะนำและรูปแบบการเขียนโค้ดมากมายในที่นั่นไม่เพียง แต่เลิกใช้แล้ว แต่เป็นเพียงการปฏิบัติที่ไม่ดีเท่านั้นในทุกวันนี้
Voo

@Voo: ดีขอเพียงแค่บอกว่าทุกอย่างไม่ว่าถือว่าปฏิบัติไม่ดีเป็นจริงการปฏิบัติที่ไม่ดี (คำแนะนำในการ "ไม่ใช้ธรรมดาint" ในใจ)
Steve Summit

1
@ สตีฟฉันไม่รู้ว่าคุณเคยได้ยินเรื่องนั้นมาจากไหน แต่นั่นไม่ใช่วิธีปฏิบัติที่ไม่ดี (ไม่ดี) ที่เรากำลังพูดถึงที่นั่น อย่าเข้าใจผิดฉันในขณะที่รหัสนั้นดีอย่างสมบูรณ์แบบ แต่คุณไม่ต้องการดู k & r มากนัก แต่เป็นบันทึกประวัติศาสตร์ในทุกวันนี้ "อยู่ใน k & r" ไม่ได้เป็นตัวบ่งชี้คุณภาพที่ดีในทุกวันนี้นั่นคือทั้งหมด
Voo

9

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


8

เนื่องจากไม่มีใครพูดถึงฉันจึงเพิ่มหมายเหตุเกี่ยวกับการแสดงของพวกเขา

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

เพื่อยืนยันสิ่งนี้ฉันได้ทำการทดสอบบางอย่าง การทดสอบดำเนินการบน Ubuntu 14.04 พร้อม gcc 4.8.4 เครื่องของฉันใช้ซีพียู Intel i5 โปรแกรมที่กำลังทดสอบมีดังนี้:

#include <stdio.h>
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM");
        // or
        fputs("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM", stdout);
    }
    fflush(stdout);
    return 0;
}

ทั้งสองรวบรวมด้วยgcc -Wall -O0. เวลาวัดโดยใช้time ./a.out > /dev/null. ต่อไปนี้เป็นผลลัพธ์ของการวิ่งโดยทั่วไป (ฉันเรียกใช้ห้าครั้งผลลัพธ์ทั้งหมดอยู่ภายใน 0.002 วินาที)

สำหรับprintf()ตัวแปร:

real    0m0.416s
user    0m0.384s
sys     0m0.033s

สำหรับfputs()ตัวแปร:

real    0m0.297s
user    0m0.265s
sys     0m0.032s

เอฟเฟกต์นี้จะถูกขยายหากคุณมีสตริงที่ยาวมาก

#include <stdio.h>
#define STR "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
#define STR2 STR STR
#define STR4 STR2 STR2
#define STR8 STR4 STR4
#define STR16 STR8 STR8
#define STR32 STR16 STR16
#define STR64 STR32 STR32
#define STR128 STR64 STR64
#define STR256 STR128 STR128
#define STR512 STR256 STR256
#define STR1024 STR512 STR512
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf(STR1024);
        // or
        fputs(STR1024, stdout);
    }
    fflush(stdout);
    return 0;
}

สำหรับprintf()ตัวแปร (วิ่งสามครั้งบวกจริง / ลบ 1.5 วินาที):

real    0m39.259s
user    0m34.445s
sys     0m4.839s

สำหรับfputs()ตัวแปร (วิ่งสามครั้งบวกจริง / ลบ 0.2 วินาที):

real    0m12.726s
user    0m8.152s
sys     0m4.581s

หมายเหตุ:หลังจากการตรวจสอบที่เกิดการชุมนุมโดย GCC ฉันตระหนัก GCC ที่เพิ่มประสิทธิภาพfputs()การโทรไปยังโทรแม้จะมีfwrite() -O0(การprintf()เรียกยังคงไม่เปลี่ยนแปลง) ฉันไม่แน่ใจว่าสิ่งนี้จะทำให้การทดสอบของฉันเป็นโมฆะหรือไม่เนื่องจากคอมไพลเลอร์คำนวณความยาวสตริงสำหรับfwrite()เวลาคอมไพล์


2
มันจะไม่ทำให้การทดสอบของคุณเป็นโมฆะตามที่fputs()มักจะใช้กับค่าคงที่ของสตริงและโอกาสในการเพิ่มประสิทธิภาพนั้นเป็นส่วนหนึ่งของจุดที่คุณต้องการทำสิ่งนี้กล่าวว่าการเพิ่มการทดสอบด้วยสตริงที่สร้างขึ้นแบบไดนามิกfputs()และfprintf()จะเป็นจุดข้อมูลเสริมที่ดี .
Patrick Schlüter

@ PatrickSchlüterการทดสอบด้วยสตริงที่สร้างขึ้นแบบไดนามิกดูเหมือนจะเอาชนะจุดประสงค์ของคำถามนี้แม้ว่า ... OP ดูเหมือนจะสนใจเฉพาะตัวอักษรสตริงเท่านั้นที่จะพิมพ์
user12205

1
เขาไม่ได้ระบุอย่างชัดเจนแม้ว่าตัวอย่างของเขาจะใช้ตัวอักษรสตริงก็ตาม อันที่จริงฉันคิดว่าความสับสนของเขาเกี่ยวกับคำแนะนำของหนังสือเล่มนี้เป็นผลมาจากการใช้ตัวอักษรสตริงในตัวอย่าง ด้วยตัวอักษรสตริงคำแนะนำหนังสือจึงน่าสงสัย แต่ด้วยสตริงแบบไดนามิกจึงเป็นคำแนะนำที่ดี
Patrick Schlüter

1
/dev/nullการจัดเรียงทำให้เป็นของเล่นโดยปกติแล้วเมื่อสร้างเอาต์พุตที่จัดรูปแบบเป้าหมายของคุณคือเพื่อให้ผลลัพธ์ไปที่ใดที่หนึ่งโดยไม่ถูกทิ้ง เมื่อคุณเพิ่มเวลา "ไม่ทิ้งข้อมูลจริง" แล้วจะเปรียบเทียบกันอย่างไร
Yakk - Adam Nevraumont

7
printf("Hello World\n")

คอมไพล์โดยอัตโนมัติให้เทียบเท่า

puts("Hello World")

คุณสามารถตรวจสอบได้ด้วยการแยกไฟล์ปฏิบัติการของคุณ:

push rbp
mov rbp,rsp
mov edi,str.Helloworld!
call dword imp.puts
mov eax,0x0
pop rbp
ret

โดยใช้

char *variable;
... 
printf(variable)

จะนำไปสู่ปัญหาด้านความปลอดภัยอย่าใช้ printf แบบนั้นเป็นอันขาด!

ดังนั้นหนังสือของคุณจึงถูกต้องจริงการใช้ printf กับตัวแปรเดียวจึงเลิกใช้งาน แต่คุณยังสามารถใช้ printf ("my string \ n") ได้เนื่องจากหนังสือจะกลายเป็น


12
พฤติกรรมนี้ขึ้นอยู่กับคอมไพเลอร์ทั้งหมด
Jabberwocky

6
นี่เป็นความเข้าใจผิด คุณรัฐแต่ในความเป็นจริงคุณหมายถึงA compiles to B A and B compile to C
Sebastian Mach

6

สำหรับ gcc คุณสามารถเปิดใช้คำเตือนเฉพาะสำหรับการตรวจสอบprintf()และscanf()และ

เอกสาร gcc ระบุ:

-Wformatรวมอยู่ใน-Wall. สำหรับการควบคุมที่มากกว่าบางแง่มุมของรูปแบบการตรวจสอบตัวเลือก-Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-securityและ-Wformat=2มี -Wallแต่จะไม่รวมอยู่ใน

-Wformatซึ่งถูกเปิดใช้งานใน-Wallตัวเลือกที่ไม่เปิดใช้งานคำเตือนพิเศษหลายอย่างที่ช่วยในการค้นหากรณีเหล่านี้:

  • -Wformat-nonliteral จะเตือนหากคุณไม่ส่งสตริง litteral เป็นตัวระบุรูปแบบ
  • -Wformat-securityจะเตือนหากคุณส่งสตริงที่อาจมีโครงสร้างที่เป็นอันตราย -Wformat-nonliteralมันเป็นส่วนหนึ่งของ

ฉันต้องยอมรับว่าการเปิดใช้งานได้-Wformat-securityเปิดเผยจุดบกพร่องหลายอย่างที่เรามีใน codebase ของเรา (โมดูลการบันทึกโมดูลการจัดการข้อผิดพลาดโมดูลเอาต์พุต xml ทั้งหมดมีฟังก์ชันบางอย่างที่สามารถทำสิ่งที่ไม่ได้กำหนดได้หากมีการเรียกด้วยอักขระ% ในพารามิเตอร์สำหรับข้อมูล ตอนนี้ codebase ของเรามีอายุประมาณ 20 ปีและแม้ว่าเราจะทราบถึงปัญหาเหล่านี้ แต่เราก็รู้สึกประหลาดใจอย่างมากเมื่อเราเปิดใช้งานคำเตือนว่ามีบั๊กเหล่านี้กี่ข้อที่ยังอยู่ใน codebase)


1

นอกเหนือจากคำตอบที่อธิบายอย่างดีพร้อมข้อกังวลด้านใด ๆ แล้วฉันต้องการให้คำตอบที่ชัดเจนและกระชับสำหรับคำถามที่ให้มา


เหตุใดprintfด้วยอาร์กิวเมนต์เดียว (ไม่มีตัวระบุการแปลง) จึงเลิกใช้งาน

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

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

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

แน่นอนว่ามันไม่ถูกต้องและเหมาะสมที่สตริงซึ่งจัดให้เป็นอาร์กิวเมนต์เดียวเท่านั้นจะมีรูปแบบหรือตัวระบุการแปลงใด ๆ เนื่องจากจะไม่มีการแปลงเกิดขึ้น

ที่กล่าวว่าให้สตริงที่เรียบง่ายเหมือน"Hello World!"อาร์กิวเมนต์เดียวโดยไม่มีตัวระบุรูปแบบใด ๆ ภายในสตริงนั้นเหมือนที่คุณระบุไว้ในคำถาม:

printf("Hello World!");

จะไม่เลิกหรือ " การปฏิบัติที่ไม่ดี " ที่ทุกคนมิได้มีช่องโหว่ใด ๆ

ในความเป็นจริงโปรแกรมเมอร์ C หลายคนเริ่มต้นและเริ่มเรียนรู้และใช้ภาษา C หรือแม้แต่ภาษาโปรแกรมโดยทั่วไปด้วยโปรแกรม HelloWorld และprintfคำสั่งนี้เป็นคำพูดแรกของมัน

พวกเขาจะไม่เป็นเช่นนั้นหากเลิกใช้งาน

ในหนังสือที่ฉันกำลังอ่านมีเขียนว่าprintfด้วยอาร์กิวเมนต์เดียว (โดยไม่มีตัวระบุการแปลง) จะเลิกใช้งานแล้ว

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

ตามที่ผมกล่าวไว้ข้างต้นโดยใช้ printfมีเพียงหนึ่งอาร์กิวเมนต์ (ตัวอักษรสตริง) และโดยไม่ต้อง specifiers รูปแบบใด ๆ ไม่ว่าในกรณีใดเลิกใช้หรือถือว่าเป็น"การปฏิบัติที่ไม่ดี"

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


คุณอาจเพิ่มสิ่งprintf("Hello World!");นั้นไม่เทียบเท่ากับputs("Hello World!");อย่างไรก็ตามซึ่งจะบอกบางสิ่งเกี่ยวกับผู้เขียนคำแนะนำ
chqrlie
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.