Linux ถอด API `strncpy` ออก หลังใช้เวลา 6 ปีและแพตช์กว่า 360 รายการ
(phoronix.com/news)- ใน Linux 7.2 การใช้งาน API
strncpyภายในเคอร์เนลหมดไป ทำให้อินเทอร์เฟซคัดลอกสตริงที่ถูกวางแผนยกเลิกมานานถูกถอดออกอย่างสมบูรณ์ strncpy()คัดลอกตามจำนวนไบต์ที่กำหนด แต่พฤติกรรมการ ปิดท้ายด้วย NUL ไม่ชัดเจน ทำให้ตลอดหลายปีที่ผ่านมาเป็นต้นตอของบั๊กในเคอร์เนล- คุณสมบัติที่เติม 0 ลงในบัฟเฟอร์ปลายทางโดยไม่จำเป็นยังสร้าง ปัญหาด้านประสิทธิภาพ และการกำจัดสิ่งนี้ต้องใช้เวลาราว 6 ปีและ 362 คอมมิต
- ใน การ merge เมื่อวันศุกร์ นอกจากตัว API หลักแล้ว ยังลบ implementation แบบ per-CPU ตามสถาปัตยกรรม ตัวสุดท้ายออกไปด้วย
- ตอนนี้โค้ดเคอร์เนลต้องเลือกใช้ฟังก์ชันทดแทนตามลักษณะงาน เช่น
strscpy(),strscpy_pad(),strtomem_pad(),memcpy_and_pad(),memcpy()
strncpy ที่หายไปจาก Linux 7.2
- Linux 7.2 ถอด API
strncpyที่อยู่ในสถานะ เตรียมเลิกใช้ มานานออกจากเคอร์เนลอย่างสมบูรณ์ - หลังงานปรับโครงสร้างที่กินเวลาถึง 6 ปี ในที่สุดก็ไม่มีโค้ดภายในเคอร์เนลที่ยังใช้อินเทอร์เฟซ
strncpyเหลืออยู่ - การเปลี่ยนแปลงครั้งนี้ไม่ใช่แค่การแทนที่ฟังก์ชัน แต่ใกล้เคียงกับการกวาดล้างแนวปฏิบัติการคัดลอกสตริงแบบเก่าออกจากเคอร์เนลทั้งระบบ
ขนาดของงานที่ต้องทำกว่าจะถอดออกได้
- การถอด
strncpyต้องใช้ประมาณ 362 คอมมิต - งานนี้ดำเนินไปในรูปแบบการกำจัดโค้ดที่ใช้
strncpyภายในเคอร์เนลทีละขั้น - ใน Linux 7.2 งานเก็บกวาดนี้ก็มาถึงจุดเสร็จสมบูรณ์
เหตุผลที่ strncpy เป็นปัญหาในเคอร์เนล
strncpyถูกมองว่าเป็น สาเหตุของบั๊กอย่างต่อเนื่อง ภายใน Linux kernel มาหลายปี- ปัญหาหลักมีอยู่สองพฤติกรรม
- ความหมายและการทำงานของ การปิดท้ายด้วย NUL ไม่ชัดเจน ทำให้ผู้ใช้พลาดได้ง่าย
- การเติม 0 ซ้ำในบัฟเฟอร์ปลายทางทำให้เกิดต้นทุนด้านประสิทธิภาพโดยไม่จำเป็น
การ merge ที่ลบออกจริง
- การ merge เมื่อวันศุกร์ได้ลบ API
strncpyออก - ใน merge เดียวกันนั้น implementation ของ
strncpyแบบ per-CPU ตามสถาปัตยกรรม ตัวสุดท้ายก็หายไปด้วย
API ทางเลือกที่ใช้ในโค้ดเคอร์เนล
- แทนที่จะใช้
strncpyตอนนี้ต้องเลือกฟังก์ชันให้เหมาะกับปลายทางการคัดลอกและเงื่อนไขการจบสตริงstrscpy(): ใช้กับปลายทางที่ปิดท้ายด้วย NULstrscpy_pad(): ใช้เมื่อปลายทางปิดท้ายด้วย NUL และต้องมีการ pad ด้วย 0strtomem_pad(): ใช้กับฟิลด์ความกว้างคงที่ที่ไม่ปิดท้ายด้วย NULmemcpy_and_pad(): ใช้กับการคัดลอกแบบจำกัดขนาดที่มีการ pad อย่างชัดเจนmemcpy(): ใช้กับการคัดลอกหน่วยความจำที่รู้ความยาวอยู่แล้ว
1 ความคิดเห็น
ความเห็นจาก Hacker News
เมื่อก่อนคนชอบล้อว่า นักพัฒนาเคอร์เนล Linux ซึ่งเป็นนักพัฒนา C ระดับแนวหน้าของโลกกลับไม่รู้จักสร้าง type อย่าง stringbuffer หรือ stringview แต่ก็พอเข้าใจได้ เพราะในยุคนั้นยังไม่มีฉันทามติเรื่องนี้ชัดเจนแบบทุกวันนี้
คนที่มองเห็นทิศทางที่ถูกต้องตั้งแต่แรกคือ Dennis Ritchie ซึ่งเคยเสนอ fat pointer type สำหรับ C ตั้งแต่ปี 1990 ถ้ามันได้เข้าไปอยู่ใน C99 ก็คงเป็นส่วนเสริมที่ยอดเยี่ยมมาก และถ้าคณะกรรมการใส่มันเข้าไป โลกอาจเปลี่ยนไปพอสมควรก็ได้
ต่อมาในปี 2007 ก็มีโอกาสครั้งที่สองจากบทความ “C's greatest mistake” ของ Walter Bright ซึ่งอธิบายแนวคิด slice/stringview ที่โดยแก่นแล้วเหมือนกับของ Ritchie แต่ชัดเจนขึ้น ทว่าก็ยังไม่ถูกใส่ใน C11 แม้ตอนนี้จะมาถึง C23 แล้วก็ยังไม่มีอยู่ดี กลายเป็นว่าได้ _Generic กับ VLA มาแทน เหมือนจะบอกว่าเอ้า มาฉลองกันเถอะ
ตอนค้นเพิ่มยังเจอโพสต์ Reddit เรื่องเดียวกันด้วย และดราม่าแบบ bikeshedding ก็ตลกดี: https://www.reddit.com/r/C_Programming/comments/90uq7c/cs_bi...
สงสัยว่าทำไมพฤติกรรมที่ array ของ C สลายเป็น pointer ถึงถูกออกแบบมาแบบนั้น มีคำอธิบายว่าทำเพื่อให้คอมไพล์โค้ด B เป็น C ได้โดยแก้น้อยที่สุด โดยใน B การประกาศ array จะนิยามทั้ง pointer และ array จริง ๆ และตั้งค่าเริ่มต้นให้ pointer นั้นชี้ไปยังสมาชิกตัวแรกของ array
ตอนนี้ปัญหาใหญ่กว่าคือไลบรารีมาตรฐานของ C ยังติดอยู่กับยุค K&R และแม้แต่ความสามารถของภาษาอย่างการส่งหรือคืนค่า struct ที่เพิ่มเข้ามาใน C99 ก็ยังไม่ถูกนำไปใช้ใน API ของไลบรารีมาตรฐาน ถ้าในไลบรารีมาตรฐานมี range struct แบบคู่ pointer/size พร้อมฟังก์ชัน string แบบใหม่หรือฟังก์ชัน string ที่ปรับปรุงแล้วซึ่งใช้มัน ก็น่าจะดีขึ้นมาก
ว่ากันว่า strncpy ในเคอร์เนล Linux เป็น “แหล่งกำเนิดบั๊กเรื้อรัง” มาหลายปี เพราะมี semantics ที่ขัดกับสัญชาตญาณ การจัดการ NUL terminator และปัญหาด้านประสิทธิภาพจากการเติม 0 ลงในปลายทางโดยไม่จำเป็น
ทุกครั้งที่ถูกขอให้รีวิวโค้ด C ผมจะไปหา strncpy ก่อน และก็เจอบั๊กตรงนั้นทุกที
มีหลายอย่างที่ทำให้รำคาญมา 40 ปีแล้ว ทั้ง สตริงที่ลงท้ายด้วย NUL และตอนนี้ก็รวมถึงสตริงที่ไม่ใช่ UTF-8 ในงาน I/O ด้วย
ธรรมเนียมการใช้ LF, CR, CRLF เป็นตัวจบบรรทัดก็เช่นกัน รวมถึงการใช้ pipe หรือ comma เป็นตัวคั่นฟิลด์ ถ้าใช้ตัวอักษร ASCII ที่ไม่กำกวมอย่าง GS, FS, RS การเข้ารหัส/ถอดรหัสตัวจบบรรทัดก็ควรเป็นปัญหาระดับ I/O ไป และ HT/VT/CR/LF/FF ก็จะยังคงเป็นโค้ดที่เกี่ยวกับการแสดงผลตามความหมายตรงตัวของมัน
ความปวดหัวเรื่อง การ escape แบบสกปรก ๆ ที่เจอในข้อมูลคั่นด้วย comma หายไปเลย ทำให้ทุกอย่างง่ายขึ้นมาก
มาตรฐาน Unicode บอกว่าควรปฏิบัติต่อ CR, LF, CRLF และตัวอักษรเหล่านี้ รวมถึง vertical tab และ form feed ว่าเป็นตัวแบ่งบรรทัดด้วย
ตัวจบบรรทัดอย่าง LF, CR, CRLF ก็เป็นธรรมเนียมของระบบปฏิบัติการด้วย และจะดีกว่าถ้าภาษาโปรแกรมไม่พยายาม “เดา” ตัวจบบรรทัดที่ถูกต้อง เพราะมันสร้างปัญหามากกว่าที่จะแก้ และอย่างที่บอก โดยมากก็เป็นปัญหาเฉพาะของ Windows ซึ่ง Microsoft ควรพา Windows เข้าสู่ศตวรรษปัจจุบันได้แล้ว
ครั้งล่าสุดที่ต้องจัดการไฟล์ CSV ใน bash ผมแปลงมันเป็น RS และ FS ภายในก่อนค่อยทำงาน
แทนที่จะใช้ strncpy ในโค้ดเคอร์เนล Linux เขาแนะนำให้ใช้ strscpy() สำหรับปลายทางที่ต้องลงท้ายด้วย NUL, strscpy_pad() สำหรับปลายทางที่ต้องลงท้ายด้วย NUL และต้องเติม 0, strtomem_pad() สำหรับฟิลด์ความกว้างคงที่ที่ไม่ได้ลงท้ายด้วย NUL, memcpy_and_pad() สำหรับการคัดลอกตามขอบเขตที่มี padding ชัดเจน และ memcpy() สำหรับการคัดลอกหน่วยความจำที่รู้ความยาว
ฟังดูเหมือนฝันร้าย และไม่แน่ใจว่าจำเป็นต้อง ซับซ้อนขนาดนี้ไหม
เวลาอ่านโค้ด แค่ดูว่าฟังก์ชันไหนถูกเลือกใช้ก็เข้าใจเจตนาได้ชัดเจน แบบนั้นน่าจะดีกว่า
งานน่าเบื่อซ้ำ ๆ แบบนี้แหละคือพื้นที่ที่ งานระบบวิศวกรรมตัวจริง เกิดขึ้น
โปรเจ็กต์โครงสร้างพื้นฐานขนาดใหญ่แบบนี้ที่ทำให้เคอร์เนล Linux เชื่อถือได้มากขึ้น โดยยังคงใช้งานได้จริงตลอดกระบวนการทั้งหมด ไม่ได้เดินกันเป็นหลักเดือนแต่เป็นหลักหลายสิบปี
แต่ก็ไม่แน่ใจว่าในความเร็วระดับนี้จะสร้างความก้าวหน้าระยะยาวที่มีความหมายได้หรือเปล่า ไม่ถึงกับเป็นการบ่น เท่าจะเป็น ความย้อนแย้งของโครงสร้างพื้นฐานหลัก มากกว่า
เป็นงานที่น่าทึ่งและทำให้ถ่อมตัวลงได้มาก น่าประหลาดใจที่มีคนมีส่วนร่วมมากขนาดนี้
“ฟีเจอร์ใหม่สุดเจ๋ง” มักได้รับการยกย่องได้ง่ายกว่า แต่กับสิ่งพื้นฐานอย่างเคอร์เนล การเอาฟีเจอร์แย่ ๆ ออกอาจสำคัญยิ่งกว่าด้วยซ้ำ
อีก 50 ปีข้างหน้า หากผู้คนลืมวิธีอ่านซอร์สโค้ดไปแล้ว และเศษซากจาก Claude/Codex ค่อย ๆ กองพอกพูนอย่างเงียบ ๆ พร้อมเผาผลาญพลังงานส่วนใหญ่ของโลก เรื่องแบบนี้ก็คงจะหลงเหลือเป็นตำนานจาก “ยุคก่อตั้ง”
และยังเป็นคนเดียวที่รู้ว่า Unix epoch คืออะไร
คิดว่า สตริงแบบลงท้ายด้วย 0 เป็นความผิดพลาดครั้งใหญ่ที่สุดในประวัติศาสตร์คอมพิวติ้ง สตริงแบบ Pascal ปลอดภัยกว่ามาก
มันยังคงเป็นพอยน์เตอร์ที่ชี้ไปยังอาร์เรย์อักขระที่ลงท้ายด้วย 0 แต่มีฟิลด์ความยาวอยู่ก่อนหน้าไบต์แรกที่พอยน์เตอร์ชี้ทันที ภายใต้สมมติฐานว่าไม่มีอักขระ NUL ฝังอยู่ภายใน มันก็ยังเข้ากันได้กับสตริงแบบ C และฟังก์ชันที่ใช้ชนิด BSTR ก็สามารถใช้ค่าความยาวได้
ช่วงหนึ่งแม้แต่ 16 บิตก็อาจถูกมองว่าเกินจำเป็น แต่ตอนนี้ 32 บิตอาจดูเล็กเกินไป ภาษาอย่าง C ที่ถูกเรียกว่าเป็นภาษา “strongly typed” กลับค่อนข้างหลวมในจุดที่สำคัญจริง ๆ
ไม่ได้เขียนโค้ด Pascal มาเกิน 30 ปีแล้ว แต่ก็ยังจำความรู้สึกเลือน ๆ ได้ว่า แม้ในตอนนั้นก็คิดว่าระบบสตริงของมันใช้งานยากเกินไป
ความเจ็บปวดและการเสียแรงเปล่าที่เกิดจากการไม่มี ชนิดข้อมูลสตริง เพียงตัวเดียวนั้นมีมากเกินไป
สงสัยว่าการเขียนการใช้งาน strncpy ใหม่มันยากอะไรนักหนาถึงใช้เวลา 6 ปี
อยากรู้ว่าเพราะมันถูกใช้อย่างแพร่หลายมากขนาดนั้น หรือเป็นงานระยะยาวที่ค่อยเปลี่ยนเฉพาะตอนมีเหตุให้แตะไฟล์เดียวกันอยู่แล้ว หรือมีความยากอย่างอื่นอีก
เคยต้องจัดการโค้ดในแอป Win32 ที่ใช้ สตริงเติมช่องว่าง สตริงปลายทางจะถูกเติมด้วยช่องว่าง แต่ไบต์สุดท้ายก็ยังเป็น null terminator อยู่
สำหรับงานอย่างการหาความยาวหรือการคัดลอก ต้องใช้ฟังก์ชันสตริงเวอร์ชันเฉพาะ ไม่รู้ว่าทำไปทำไม แต่โค้ดเบสเก่ามากจนเป็นไปได้ว่าอาจสืบทอดมาจากพฤติกรรมของโครงสร้าง Pascal