1 คะแนน โดย GN⁺ 3 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ใน 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(): ใช้กับปลายทางที่ปิดท้ายด้วย NUL
    • strscpy_pad(): ใช้เมื่อปลายทางปิดท้ายด้วย NUL และต้องมีการ pad ด้วย 0
    • strtomem_pad(): ใช้กับฟิลด์ความกว้างคงที่ที่ไม่ปิดท้ายด้วย NUL
    • memcpy_and_pad(): ใช้กับการคัดลอกแบบจำกัดขนาดที่มีการ pad อย่างชัดเจน
    • memcpy(): ใช้กับการคัดลอกหน่วยความจำที่รู้ความยาวอยู่แล้ว

1 ความคิดเห็น

 
GN⁺ 3 시간 전
ความเห็นจาก 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 มาแทน เหมือนจะบอกว่าเอ้า มาฉลองกันเถอะ

    • บทความปี 2007 ของ Walter Bright อยู่ที่นี่: https://digitalmars.com/articles/C-biggest-mistake.html
      ตอนค้นเพิ่มยังเจอโพสต์ Reddit เรื่องเดียวกันด้วย และดราม่าแบบ bikeshedding ก็ตลกดี: https://www.reddit.com/r/C_Programming/comments/90uq7c/cs_bi...
      สงสัยว่าทำไมพฤติกรรมที่ array ของ C สลายเป็น pointer ถึงถูกออกแบบมาแบบนั้น มีคำอธิบายว่าทำเพื่อให้คอมไพล์โค้ด B เป็น C ได้โดยแก้น้อยที่สุด โดยใน B การประกาศ array จะนิยามทั้ง pointer และ array จริง ๆ และตั้งค่าเริ่มต้นให้ pointer นั้นชี้ไปยังสมาชิกตัวแรกของ array
    • VLA ถูกลดชั้นเป็นฟีเจอร์ทางเลือกใน C11 ซึ่งผมคิดว่าเป็นเรื่องดี
      ตอนนี้ปัญหาใหญ่กว่าคือไลบรารีมาตรฐานของ C ยังติดอยู่กับยุค K&R และแม้แต่ความสามารถของภาษาอย่างการส่งหรือคืนค่า struct ที่เพิ่มเข้ามาใน C99 ก็ยังไม่ถูกนำไปใช้ใน API ของไลบรารีมาตรฐาน ถ้าในไลบรารีมาตรฐานมี range struct แบบคู่ pointer/size พร้อมฟังก์ชัน string แบบใหม่หรือฟังก์ชัน string ที่ปรับปรุงแล้วซึ่งใช้มัน ก็น่าจะดีขึ้นมาก
    • ลิงก์ข้อเสนอของ Ritchie: https://web.archive.org/web/20150611114358/https://www.bell-...
    • นี่เป็นรูปแบบที่น่าหงุดหงิดที่สุดในการทำงานเป็นทีม มีทางแก้ A, B, C แต่ละแบบก็มีข้อดีข้อเสีย เลยถกกันอยู่ 2 สัปดาห์ แล้วสุดท้ายก็ ไม่เลือกอะไรเลย
    • มันก็แค่สะท้อนว่า WG14 ให้ความสำคัญกับอะไร
  • ว่ากันว่า 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 ก็จะยังคงเป็นโค้ดที่เกี่ยวกับการแสดงผลตามความหมายตรงตัวของมัน

    • เคยทำโปรเจ็กต์แปลงข้อมูลที่ใช้ตัวคั่น field/record ของ ASCII ในการจัดกรอบข้อมูล และจัดการได้ง่ายมากจริง ๆ
      ความปวดหัวเรื่อง การ escape แบบสกปรก ๆ ที่เจอในข้อมูลคั่นด้วย comma หายไปเลย ทำให้ทุกอย่างง่ายขึ้นมาก
    • ตอนนี้ Unicode ก็มีตัวเลือกเพิ่มขึ้นแล้ว มี NL Next line ที่เหมือนหลุดมาจาก EBCDIC, LS Line separator และ PS Paragraph separator ที่ Unicode กำหนดขึ้นมา
      มาตรฐาน Unicode บอกว่าควรปฏิบัติต่อ CR, LF, CRLF และตัวอักษรเหล่านี้ รวมถึง vertical tab และ form feed ว่าเป็นตัวแบ่งบรรทัดด้วย
    • UTF-8 ใช้งานได้ดีมากกับ standard input/output แน่นอน ถ้าไม่ได้พูดถึง Windows ที่ยังติดอยู่ในยุคต้นทศวรรษ 90 เรื่องการเข้ารหัสข้อความสากล
      ตัวจบบรรทัดอย่าง LF, CR, CRLF ก็เป็นธรรมเนียมของระบบปฏิบัติการด้วย และจะดีกว่าถ้าภาษาโปรแกรมไม่พยายาม “เดา” ตัวจบบรรทัดที่ถูกต้อง เพราะมันสร้างปัญหามากกว่าที่จะแก้ และอย่างที่บอก โดยมากก็เป็นปัญหาเฉพาะของ Windows ซึ่ง Microsoft ควรพา Windows เข้าสู่ศตวรรษปัจจุบันได้แล้ว
    • LF ดูสมเหตุสมผลที่สุด แต่ถ้าเป็นไฟล์ข้อความจะใช้อะไรก็ได้ ปัญหาคือ CSV ไม่ใช่ข้อความ
      ครั้งล่าสุดที่ต้องจัดการไฟล์ CSV ใน bash ผมแปลงมันเป็น RS และ FS ภายในก่อนค่อยทำงาน
    • ผมว่าก็ใช้ UTF-8 ไปทุกที่ให้หมดเลย
  • แทนที่จะใช้ strncpy ในโค้ดเคอร์เนล Linux เขาแนะนำให้ใช้ strscpy() สำหรับปลายทางที่ต้องลงท้ายด้วย NUL, strscpy_pad() สำหรับปลายทางที่ต้องลงท้ายด้วย NUL และต้องเติม 0, strtomem_pad() สำหรับฟิลด์ความกว้างคงที่ที่ไม่ได้ลงท้ายด้วย NUL, memcpy_and_pad() สำหรับการคัดลอกตามขอบเขตที่มี padding ชัดเจน และ memcpy() สำหรับการคัดลอกหน่วยความจำที่รู้ความยาว
    ฟังดูเหมือนฝันร้าย และไม่แน่ใจว่าจำเป็นต้อง ซับซ้อนขนาดนี้ไหม

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

    • เข้าใจว่าทำไมมันถึงกินเวลาระดับหลายสิบปี เพราะหางยาวของผู้ใช้และ dependency นั้นยาวมากจริง ๆ
      แต่ก็ไม่แน่ใจว่าในความเร็วระดับนี้จะสร้างความก้าวหน้าระยะยาวที่มีความหมายได้หรือเปล่า ไม่ถึงกับเป็นการบ่น เท่าจะเป็น ความย้อนแย้งของโครงสร้างพื้นฐานหลัก มากกว่า
  • เป็นงานที่น่าทึ่งและทำให้ถ่อมตัวลงได้มาก น่าประหลาดใจที่มีคนมีส่วนร่วมมากขนาดนี้
    “ฟีเจอร์ใหม่สุดเจ๋ง” มักได้รับการยกย่องได้ง่ายกว่า แต่กับสิ่งพื้นฐานอย่างเคอร์เนล การเอาฟีเจอร์แย่ ๆ ออกอาจสำคัญยิ่งกว่าด้วยซ้ำ
    อีก 50 ปีข้างหน้า หากผู้คนลืมวิธีอ่านซอร์สโค้ดไปแล้ว และเศษซากจาก Claude/Codex ค่อย ๆ กองพอกพูนอย่างเงียบ ๆ พร้อมเผาผลาญพลังงานส่วนใหญ่ของโลก เรื่องแบบนี้ก็คงจะหลงเหลือเป็นตำนานจาก “ยุคก่อตั้ง”

    • ทำให้นึกถึง Deepness in the Sky ของ Vernor Vinge ในนั้นมีคนคนหนึ่งคอยบำรุงรักษายานอวกาศด้วยโบราณคดีซอฟต์แวร์
      และยังเป็นคนเดียวที่รู้ว่า Unix epoch คืออะไร
    • คิดว่าอีก 50 ปีข้างหน้า คนคงไม่ลืมวิธีเข้าใจซอร์สโค้ดกันหมดหรอก ความต้องการของมนุษย์ที่อยากรู้ว่าสิ่งต่าง ๆ ทำงานอย่างไรคงยังอยู่
    • คิดว่า โค้ดจับฉ่าย ที่ AI สร้างขึ้นจะกลายเป็นสิ่งที่รับมือไม่ไหวก่อนหน้านั้นมาก
  • คิดว่า สตริงแบบลงท้ายด้วย 0 เป็นความผิดพลาดครั้งใหญ่ที่สุดในประวัติศาสตร์คอมพิวติ้ง สตริงแบบ Pascal ปลอดภัยกว่ามาก

    • ก็มีทางสายกลางอย่าง BSTR ที่ Visual Basic และต่อมา COM เลือกใช้
      มันยังคงเป็นพอยน์เตอร์ที่ชี้ไปยังอาร์เรย์อักขระที่ลงท้ายด้วย 0 แต่มีฟิลด์ความยาวอยู่ก่อนหน้าไบต์แรกที่พอยน์เตอร์ชี้ทันที ภายใต้สมมติฐานว่าไม่มีอักขระ NUL ฝังอยู่ภายใน มันก็ยังเข้ากันได้กับสตริงแบบ C และฟังก์ชันที่ใช้ชนิด BSTR ก็สามารถใช้ค่าความยาวได้
    • เห็นด้วยระดับหนึ่ง แต่ก็คงมีการถกเถียงกันเรื่องชนิดข้อมูลของฟิลด์ขนาดแน่ ๆ ถ้าไม่ใช่ความยาวแบบแปรผันก็คงยิ่งเถียงกันมาก และถ้าเป็นแบบแปรผันก็จะมีปัญหาอีกแบบ
      ช่วงหนึ่งแม้แต่ 16 บิตก็อาจถูกมองว่าเกินจำเป็น แต่ตอนนี้ 32 บิตอาจดูเล็กเกินไป ภาษาอย่าง C ที่ถูกเรียกว่าเป็นภาษา “strongly typed” กลับค่อนข้างหลวมในจุดที่สำคัญจริง ๆ
    • สตริงแบบลงท้ายด้วย 0 เป็นรากฐานของซอฟต์แวร์ที่มีประโยชน์มหาศาล จะเรียกว่ามันเป็นความผิดพลาดครั้งใหญ่ที่สุดของวงการคอมพิวติ้งก็ดูจะพูดเกินไปหน่อย
      ไม่ได้เขียนโค้ด Pascal มาเกิน 30 ปีแล้ว แต่ก็ยังจำความรู้สึกเลือน ๆ ได้ว่า แม้ในตอนนั้นก็คิดว่าระบบสตริงของมันใช้งานยากเกินไป
    • 255 อักขระก็น่าจะพอสำหรับทุกคนไม่ใช่หรือ?
    • แย่พอ ๆ กับบรรทัดที่ลงท้ายด้วยอักขระขึ้นบรรทัดใหม่
  • ความเจ็บปวดและการเสียแรงเปล่าที่เกิดจากการไม่มี ชนิดข้อมูลสตริง เพียงตัวเดียวนั้นมีมากเกินไป

    • จะให้แม่นกว่านั้น ไม่ใช่ว่าไม่มีชนิดข้อมูลสตริง แต่เป็นความเจ็บปวดและการเสียแรงที่เกิดจากการต้องหาทางอ้อมข้อเท็จจริงที่ว่า C ไม่มีชนิดข้อมูลสตริง
    • ถ้าจะนำ strong typing เข้ามาในที่นี้ จะทำได้ด้วยวิธีไหน? ดูเหมือนว่าคงต้องรีแฟกเตอร์ครั้งใหญ่ให้โค้ดรอบ ๆ strncpy หันมาใช้ชนิดและฟังก์ชันเหล่านั้นด้วยหรือเปล่า
  • สงสัยว่าการเขียนการใช้งาน strncpy ใหม่มันยากอะไรนักหนาถึงใช้เวลา 6 ปี
    อยากรู้ว่าเพราะมันถูกใช้อย่างแพร่หลายมากขนาดนั้น หรือเป็นงานระยะยาวที่ค่อยเปลี่ยนเฉพาะตอนมีเหตุให้แตะไฟล์เดียวกันอยู่แล้ว หรือมีความยากอย่างอื่นอีก

  • เคยต้องจัดการโค้ดในแอป Win32 ที่ใช้ สตริงเติมช่องว่าง สตริงปลายทางจะถูกเติมด้วยช่องว่าง แต่ไบต์สุดท้ายก็ยังเป็น null terminator อยู่
    สำหรับงานอย่างการหาความยาวหรือการคัดลอก ต้องใช้ฟังก์ชันสตริงเวอร์ชันเฉพาะ ไม่รู้ว่าทำไปทำไม แต่โค้ดเบสเก่ามากจนเป็นไปได้ว่าอาจสืบทอดมาจากพฤติกรรมของโครงสร้าง Pascal

    • อาจเป็นเพราะสตริงนั้นมาจาก ฟิลด์ char ของฐานข้อมูล SQL ก็ได้ ไม่ใช่ varchar เพราะฟิลด์ char จะถูกเติมด้วยช่องว่าง
    • รากของพฤติกรรมนี้น่าจะมาจาก COBOL มากกว่า Pascal
    • อาจทำไปเพื่อหลีกเลี่ยงการจัดสรรหน่วยความจำใหม่เมื่อขนาดสตริงเปลี่ยน หรืออาจเกี่ยวกับการจัดแนวตาม cache line ของ CPU