ฟังก์ชันของไลบรารี C ควรคาดหวังความยาวของสตริงหรือไม่?


15

ขณะนี้ฉันกำลังทำงานกับไลบรารีที่เขียนเป็น C ฟังก์ชันจำนวนมากของไลบรารีนี้คาดว่าจะมีสตริงเป็นchar*หรือconst char*ในอาร์กิวเมนต์ ฉันเริ่มต้นด้วยฟังก์ชั่นเหล่านั้นมักจะคาดหวังความยาวของสตริงsize_tเพื่อไม่ให้มีการยกเลิกค่า null อย่างไรก็ตามเมื่อเขียนการทดสอบสิ่งนี้ส่งผลให้มีการใช้งานบ่อยstrlen()เช่น:

const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));

การวางใจให้ผู้ใช้ส่งผ่านสายอักขระที่ถูกยกเลิกอย่างถูกต้องจะนำไปสู่ความปลอดภัยที่น้อยลง แต่มีความรัดกุมมากกว่าและ (ในความคิดของฉัน) รหัสที่อ่านได้:

libFunction("I hope there's a null-terminator there!");

ดังนั้นการปฏิบัติที่เหมาะสมที่นี่คืออะไร? ทำให้ API มีความซับซ้อนมากขึ้นในการใช้งาน แต่บังคับให้ผู้ใช้นึกถึงการป้อนข้อมูลหรือบันทึกข้อกำหนดสำหรับสตริงที่สิ้นสุดด้วยค่า null และเชื่อใจผู้โทร

คำตอบ:


4

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

มันแย่มากที่ถ้าคุณเจอคำถามนี้ในการสัมภาษณ์ - และผู้สัมภาษณ์ทางเทคนิคของคุณดูเหมือนว่าเขามีประสบการณ์ไม่กี่ปี - ความกระตือรือร้นอย่างแท้จริงอาจมาจากงาน - คุณสามารถก้าวไปข้างหน้าได้จริงถ้าคุณสามารถอ้างอิง ก่อนหน้านี้ในการถ่ายภาพบุคคลที่ใช้งาน API ที่กำลังมองหา C string terminator

ปล่อยให้อารมณ์ของมันหมดไปมีหลายสิ่งที่ผิดพลาดกับ NULL ที่ปลายสายของคุณทั้งในการอ่านและการจัดการ - และมันเป็นการละเมิดแนวคิดการออกแบบสมัยใหม่โดยตรงเช่นการป้องกันเชิงลึก (ไม่จำเป็นต้องใช้กับการรักษาความปลอดภัย แต่เป็นการออกแบบ API) ตัวอย่างของ C APIs ที่มีความยาวมากมาย Windows API

ในความเป็นจริงปัญหานี้ถูกตัดสินในช่วงยุค 90 เห็นพ้องในวันนี้คือการที่คุณไม่ควรได้สัมผัสสายของคุณ

ต่อมาแก้ไข : นี้ค่อนข้างอภิปรายสดดังนั้นฉันจะเพิ่มที่ไว้วางใจทุกคนด้านล่างและด้านบนคุณสามารถที่จะมีความสุขและใช้ STR ห้องสมุด * ฟังก์ชั่นเป็น OK จนกว่าคุณจะเห็นสิ่งที่คลาสสิกเหมือนหรือoutput = malloc(strlen(input)); strcpy(output, input); while(*src) { *dest=transform(*src); dest++; src++; }ฉันเกือบจะได้ยิน Lacrimosa ของ Mozart อยู่เบื้องหลัง


1
ฉันไม่เข้าใจตัวอย่างของคุณเกี่ยวกับ Windows API ที่กำหนดให้ผู้โทรต้องระบุความยาวของสตริง ตัวอย่างเช่นฟังก์ชัน Win32 API ทั่วไปเช่นCreateFileรับLPTCSTR lpFileNameพารามิเตอร์เป็นอินพุต ไม่คาดหวังความยาวของสตริงจากผู้เรียก ในความเป็นจริงการใช้สตริงที่ถูกยกเลิก NUL นั้นฝังอยู่ในเอกสารไม่ได้กล่าวถึงว่าชื่อไฟล์จะต้องถูกยกเลิกด้วย NUL (แต่แน่นอนว่ามันต้องเป็น)
Greg Hewgill

1
จริง ๆ แล้วใน Win32 LPSTRชนิดที่ระบุว่าสายอักขระอาจถูกยกเลิกด้วย NUL และถ้าไม่เช่นนั้นจะถูกระบุในข้อกำหนดที่เกี่ยวข้อง ดังนั้นเว้นแต่จะระบุไว้เป็นอย่างอื่นสตริงดังกล่าวใน Win32 คาดว่าจะถูกยกเลิกด้วย NUL
Greg Hewgill

จุดที่ดีฉันไม่แน่ใจ พิจารณาว่า CreateFile และเครือข่ายของมันมีอยู่ตั้งแต่ Windows NT 3.1 (ต้นยุค 90) API ปัจจุบัน (เช่นนับตั้งแต่มีการแนะนำ Strsafe.h ใน XP SP2 - ด้วยคำขอโทษสาธารณะของ Microsoft) คัดค้าน NULL ทั้งหมดที่ถูกยกเลิกสิ่งที่ทำได้ ครั้งแรกที่ Microsoft รู้สึกเสียใจอย่างยิ่งต่อการใช้สตริงที่ถูกยกเลิกค่า NULL ก่อนหน้านี้เมื่อพวกเขาต้องแนะนำ BSTR ในข้อกำหนด OLE 2.0 เพื่อนำ VB, COM และ WINAPI เก่ามาใช้ในเรือลำเดียวกัน
vski

1
แม้ในStringCbCatตัวอย่างเพียงปลายทางเท่านั้นที่มีบัฟเฟอร์สูงสุดซึ่งสมเหตุสมผล แหล่งที่ยังคงเป็น NUL สิ้นสุดสตริง C สามัญ บางทีคุณอาจปรับปรุงคำตอบของคุณโดยการอธิบายความแตกต่างระหว่างพารามิเตอร์อินพุตและพารามิเตอร์เอาต์พุต พารามิเตอร์เอาต์พุตควรมีความยาวบัฟเฟอร์สูงสุดเสมอ พารามิเตอร์อินพุตมักจะสิ้นสุดด้วย NUL (มีข้อยกเว้น แต่ไม่ค่อยพบในประสบการณ์ของฉัน)
เกร็กฮิวกิลล์

1
ใช่. สตริงไม่สามารถเปลี่ยนแปลงได้ทั้ง JVM / Dalvik และ. NET CLR ที่ระดับแพลตฟอร์มรวมถึงในภาษาอื่น ๆ อีกมากมาย ฉันจะไปไกลและคาดเดาว่าโลกพื้นเมืองยังไม่สามารถทำสิ่งนี้ได้ (มาตรฐาน C ++ 11) เพราะ) มรดก (คุณไม่ได้รับมากจริง ๆ โดยมีเพียงส่วนหนึ่งของสายอักขระของคุณไม่เปลี่ยนรูป) และ b ) คุณจำเป็นต้องมี GC และตารางสตริงเพื่อทำให้งานนี้ตัวจัดสรรที่กำหนดขอบเขตใน C ++ 11 ไม่สามารถตัดได้
vski

16

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

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


-1, ฉันขอโทษนี่เป็นคำแนะนำที่ไม่ดี
vski

ในสมัยก่อนสิ่งนี้ไม่เป็นความจริงเสมอไป ฉันทำงานเป็นจำนวนมากด้วยโปรโตคอลเลขฐานสองที่ใส่ข้อมูลสตริงในฟิลด์ความยาวคงที่ซึ่งไม่ได้เป็นค่า NULL ในกรณีเช่นนี้มันเป็นเรื่องที่ถนัดมากในการทำงานกับฟังก์ชั่นที่ใช้เวลานาน แม้ว่าฉันจะไม่ได้ทำ C ในรอบทศวรรษ แต่
Gort the Robot

4
@ vski บังคับให้ผู้ใช้เรียก 'strlen' ก่อนเรียกฟังก์ชันเป้าหมายทำอะไรเพื่อหลีกเลี่ยงปัญหาบัฟเฟอร์ล้น? อย่างน้อยถ้าคุณตรวจสอบความยาวของตัวคุณเองในฟังก์ชั่นเป้าหมายคุณสามารถมั่นใจได้ว่าความรู้สึกของความยาวใดที่ถูกใช้ (รวมถึงเทอร์มินัลว่างหรือไม่)
Charles E. Grant

@Charles E. Grant: ดูความคิดเห็นด้านบนเกี่ยวกับ StringCbCat และ StringCbCatN ใน Strsafe.h หากคุณมี char * และไม่มีความยาวแน่นอนว่าคุณไม่มีทางเลือกจริง แต่ใช้ฟังก์ชั่น str * แต่ประเด็นคือการพกพาความยาวไปรอบ ๆ ดังนั้นมันจึงกลายเป็นตัวเลือกระหว่าง str * และ strn * ฟังก์ชั่นซึ่งเป็นที่ต้องการหลัง
vski

2
@vski ไม่จำเป็นต้องผ่านความยาวของสตริง มีเป็นความจำเป็นที่จะผ่านรอบบัฟเฟอร์ 's ยาว ไม่ใช่บัฟเฟอร์ทั้งหมดที่เป็นสตริงและไม่ใช่สตริงทั้งหมดที่เป็นบัฟเฟอร์
jamesdlin

10

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

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

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

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


ไม่เห็นด้วยอย่างยิ่งกับวิธีการนี้ อย่าไว้วางใจผู้โทรของคุณโดยเฉพาะอย่างยิ่งหลัง API ห้องสมุดให้ใช้ความพยายามอย่างเต็มที่เพื่อตั้งคำถามกับสิ่งที่พวกเขาให้และไม่ได้อย่างสง่างาม ดำเนินการความยาวสาปการทำงานกับสาย NULL- สิ้นสุดไม่ใช่สิ่งที่ "จะหลวมกับผู้โทรของคุณและเข้มงวดกับ callees ของคุณ" หมายถึง
vski

2
ฉันเห็นด้วยกับตำแหน่งของคุณเป็นส่วนใหญ่แต่คุณดูเหมือนจะไว้ใจมากในการโต้แย้งความยาวนั้น - ไม่มีเหตุผลว่าทำไมมันควรจะเชื่อถือได้กว่า null terminator ตำแหน่งของฉันคือขึ้นอยู่กับว่าห้องสมุดทำอะไร
Mat

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

2

ไม่สตริงจะสิ้นสุดด้วย null เสมอโดยนิยามความยาวสตริงจะซ้ำซ้อน

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

ไว้วางใจโทรของฟังก์ชัน API ที่ไม่ปลอดภัย ; พฤติกรรมที่ไม่ได้กำหนดนั้นใช้ได้อย่างสมบูรณ์แบบหากไม่มีเงื่อนไขตามเอกสาร

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


ไม่เพียง แต่ตกลงอย่างสมบูรณ์แบบ แต่หลีกเลี่ยงไม่ได้จริง ๆ ยกเว้นว่ามีใครย้ายไปยังหน่วยความจำที่ปลอดภัยภาษาแบบเธรดเดียว อาจจะมีการปรับตัวลดลงข้อ จำกัด มากขึ้นจำเป็นบางอย่าง ...
Deduplicator

1

คุณควรรักษาความยาวของคุณ สำหรับหนึ่งผู้ใช้ของคุณอาจต้องการมี NULL ในพวกเขา และประการที่สองอย่าลืมว่าstrlenเป็น O (N) และต้องสัมผัสแคชสตริงทั้งหมด และประการที่สามมันง่ายกว่าที่จะส่งผ่านชุดย่อย - ตัวอย่างเช่นพวกเขาอาจให้ความยาวน้อยกว่าความยาวจริง


4
ฟังก์ชันไลบรารีเกี่ยวข้องกับ NULL แบบฝังในสตริงหรือไม่จำเป็นต้องมีการบันทึกไว้เป็นอย่างดี ฟังก์ชันไลบรารี C ส่วนใหญ่หยุดที่ NULL หรือความยาวแล้วแต่ว่าอันใดจะถึงก่อน (และถ้าเขียนอย่างมีประสิทธิภาพผู้ที่ไม่ใช้เวลานานจะไม่ใช้strlenในการทดสอบลูป)
Gort the Robot

1

คุณควรจะแยกแยะความแตกต่างระหว่างการส่งผ่านรอบสตริงและผ่านรอบบัฟเฟอร์

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

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

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

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


นี่คือสิ่งที่คำถามและคำตอบอื่น ๆ ที่ไม่ได้รับ
Blrfl

0

หากฟังก์ชั่นส่วนใหญ่ใช้กับตัวอักษรสตริงความเจ็บปวดในการจัดการกับความยาวที่ชัดเจนอาจถูกย่อให้เล็กสุดโดยกำหนดมาโครบางตัว ตัวอย่างเช่นกำหนดฟังก์ชั่น API:

void use_string(char *string, int length);

หนึ่งสามารถกำหนดแมโคร:

#define use_strlit(x) use_string(x, sizeof ("" x "")-1)

จากนั้นเรียกใช้ตามที่แสดงใน:

void test(void)
{
  use_strlit("Hello");
}

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

แนวทางอื่นใน C99 คือการกำหนดประเภทโครงสร้าง "ตัวชี้และความยาว" และกำหนดแมโครที่แปลงสตริงตัวอักษรให้เป็นตัวอักษรผสมของประเภทโครงสร้างนั้น ตัวอย่างเช่น:

struct lstring { char const *ptr; int length; };
#define as_lstring(x) \
  (( struct lstring const) {x, sizeof("" x "")-1})

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

struct lstring *p;
if (foo)
{
  p = &as_lstring("Hello");
}
else
{
  p = &as_lstring("Goodbye!");
}
use_lstring(p);

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

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