1 คะแนน โดย GN⁺ 2024-09-30 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ระบบ glibc แบบ 32 บิตอาจล้มเหลวในการอ่านเวลาปัจจุบันหรือเรียก stat() หลังปี 2038 ได้ ดังนั้น Gentoo จึงต้องมีเส้นทางย้ายไปใช้ time_t แบบ 64 บิต อย่างปลอดภัย
  • time64 ของ glibc ต้องใช้ร่วมกับ Large File Support (LFS) และในสภาพแวดล้อม 32 บิตจะมีทั้ง ABI เดิม, LFS ABI และ LFS+time64 ABI อยู่พร้อมกัน
  • เมื่อ time_t อยู่ใน API, โครงสร้างข้อมูล หรืออาร์กิวเมนต์ของฟังก์ชัน การเปลี่ยนความกว้างของชนิดข้อมูลจะนำไปสู่ ABI แตกหัก ทำให้การปนกันของไบนารี time32 และ time64 เกิดการทำงานผิดพลาดขณะรันและมีความเสี่ยงด้านความปลอดภัย
  • Gentoo ซึ่งเป็นดิสโทรแบบซอร์สเบส อาจเหลือ ระบบที่ย้ายไม่สมบูรณ์ครึ่งหนึ่ง ได้จากความล้มเหลวระหว่างรีบิลด์ @world หรือจากวงจรการพึ่งพากัน
  • หลังการแก้ไขเมื่อ 2024-09-30 การเปลี่ยนเฉพาะ libdir ไม่เพียงพออีกต่อไป และ การทำเครื่องหมาย time64 ที่ครอบคลุมถึง dynamic loader และ toolchain หลายภาษากลายเป็นข้อจำกัดสำคัญ

ปัญหาปี 2038 และขอบเขตของการเปลี่ยนเป็น time64

  • แอปพลิเคชัน 32 บิตที่ใช้ time_t แบบ 32 บิตอาจได้รับข้อผิดพลาด -1 แทนเวลาปัจจุบันในปี 2038 หรือไม่สามารถทำ stat() กับไฟล์ได้
  • แนวทางพื้นฐานของการเปลี่ยนคือเปลี่ยน time_t ให้เป็น ชนิดข้อมูล 64 บิต
    • musl เปลี่ยนไปแล้ว
    • glibc รองรับสิ่งนี้แบบเป็นตัวเลือก
    • ดิสโทรบางตัวอย่าง Debian ได้เปลี่ยนไปแล้ว
  • ดิสโทรแบบ ซอร์สเบส อย่าง Gentoo ให้ผู้ใช้รีบิลด์ระบบเอง จึงต้องลดช่วงเวลาที่แพ็กเกจต่าง ๆ ค้างอยู่ในสถานะ ABI ที่ไม่ตรงกัน
  • ความเสี่ยงหลักคือการเปลี่ยนความกว้างของ time_t ทำให้ ABI พัง
    • ถ้า time_t อยู่ใน API ของไลบรารี โค้ดทั้งหมดที่ลิงก์กับไลบรารีนั้นต้องใช้ความกว้างชนิดข้อมูลเดียวกัน
    • การเปลี่ยนเพียงบางส่วนเป็น time64 ไม่ปลอดภัย

LFS และสาม ABI ย่อยของระบบ 32 บิต

  • สถาปัตยกรรม 32 บิตมีปัญหาเรื่องความกว้างของชนิดข้อมูลที่เกี่ยวกับไฟล์มานานแล้ว
    • off_t ใช้กับออฟเซ็ตของไฟล์
    • ino_t ใช้กับหมายเลข inode
    • เดิมมีความกว้าง 32 บิต จึงมีปัญหากับไฟล์ใหญ่กว่า 2GiB หรือหมายเลข inode ที่เกินช่วง 32 บิต
  • เพื่อแก้ปัญหานี้จึงมี Large File Support (LFS)
    • เปลี่ยน off_t และ ino_t เป็นแบบ 64 บิต
    • ใน glibc ปัจจุบันยังเป็นตัวเลือกอยู่
    • หลายแพ็กเกจเปิดใช้ LFS จาก upstream และจัดการ ABI break แล้ว แต่ปัญหายังไม่หมดไปทั้งหมด
  • การรองรับ time64 ของ glibc ต้องใช้ LFS จึงเป็นโครงสร้างที่แก้ปัญหาเรื่องขนาดไฟล์และเวลาไปพร้อมกัน
  • ในระบบ 32 บิตมีสาม ABI ย่อย
    • ABI เดิม: ชนิดข้อมูล 32 บิต
    • LFS: off_t 64 บิต, ino_t 64 บิต, time_t 32 บิต
    • time64: LFS + time_t 64 บิต
  • glibc build เดียวสามารถเข้ากันได้กับทั้งสามแบบ แต่ไลบรารีที่ใช้ชนิดข้อมูลเหล่านี้ใน API ไม่สามารถผสมทั้งสามแบบเข้าด้วยกันได้

วิธีที่ ABI เปลี่ยนแล้วพังจริงในทางปฏิบัติ

  • เมื่อ time_t เปลี่ยนจาก 32 บิตเป็น 64 บิต layout ของโครงสร้างข้อมูลจะเปลี่ยนตาม
    • ในตัวอย่างโครงสร้างที่มี int a, time_t b, int c ตามลำดับ ค่าออฟเซ็ตของ c จะแตกต่างกันระหว่าง time_t 32 บิตกับ 64 บิต
    • ถ้าปนไบนารี time32 กับ time64 จะเกิดการอ่านหรือเขียนฟิลด์ผิด และอาจเข้าถึงหน่วยความจำเกินขอบเขตได้
  • ขนาดของ struct stat ก็แตกต่างกันตาม ABI
    • ค่าเริ่มต้นของ glibc บน x86 แบบ 32 บิต: 88 ไบต์
    • LFS: 96 ไบต์
    • LFS + time64: 108 ไบต์
  • แม้ไม่ใช้โครงสร้างข้อมูลก็ยังมีปัญหาที่อาร์กิวเมนต์ของฟังก์ชัน
    • บน x86 อาร์กิวเมนต์ของฟังก์ชันจะถูกส่งผ่าน stack
    • ถ้าอาร์กิวเมนต์ตัวใดตัวหนึ่งเป็น time_t ตำแหน่งบน stack ของอาร์กิวเมนต์ถัดไปจะเปลี่ยนไป
  • ในการทดลองตัวอย่าง เมื่อโปรแกรม time32 ลิงก์กับไลบรารีที่รีบิลด์ใหม่เป็น time64 ค่าต่าง ๆ จะเสียหาย
    • เอาต์พุตเดิมคือ a = 1, ค่าเวลาปกติ, c = 3
    • ถ้ารีบิลด์เฉพาะไลบรารีด้วย -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64 จะตีความ b และ c ผิด
  • ปัจจุบันแทบไม่มีมาตรการป้องกันจริงเพื่อห้ามการปน ABI แบบนี้ จึงอาจเกิด การพังขณะรัน และปัญหาด้านความปลอดภัยได้

ทำไมการเปลี่ยนของ Gentoo จึงยากกว่า

  • ดิสโทรแบบไบนารีสามารถรีบิลด์ทุกแพ็กเกจก่อน แล้วให้ผู้ใช้อัปเกรดในขั้นตอนที่ค่อนข้างเป็นอะตอมมิก
    • ถ้ามีคลังแพ็กเกจภายนอกหรือโปรแกรมที่บิลด์เองในเครื่องก็อาจมีปัญหาได้ แต่กระบวนการโดยรวมยังปลอดภัยกว่าค่อนข้างมาก
  • Gentoo ต้องเปลี่ยน ABI ในระบบเดิมระหว่างการรีบิลด์ @world
    • ระหว่างที่สองแพ็กเกจถูกรีบิลด์แยกกัน อาจมี ABI ที่เข้ากันไม่ได้ปะปนอยู่
    • การรีบิลด์บางส่วนล้มเหลวอาจทำให้ระบบค้างอยู่ในสถานะ ย้ายไม่สมบูรณ์ครึ่งหนึ่ง
    • วงจรการพึ่งพากันอาจทำให้เมื่อรีบิลด์แพ็กเกจที่เป็น dependency แล้วเครื่องมือ build พัง จนรีบิลด์ต่อไม่ได้

แนวทางบรรเทาที่กำลังพิจารณา

  • มีสามแนวทางที่กำลังถูกพูดถึง
    • เปลี่ยน platform tuple หรือ CHOST เพื่อแยก ABI ใหม่ออกจาก ABI 32 บิตเดิม
    • เปลี่ยน libdir ของ ABI ใหม่ เพื่อให้ติดตั้งไลบรารีที่รีบิลด์แล้วแยกจากไลบรารีเดิม
    • เพิ่ม การแยก ABI ระดับไบนารี เพื่อไม่ให้ไบนารีจาก ABI ย่อยต่างกันลิงก์เข้าหากันได้
  • ทั้งสามวิธีสามารถทำได้ค่อนข้างอิสระต่อกัน แต่บางส่วนอาจต้องพึ่งพากัน
  • สตริงตัวอย่างในบทความอาจไม่ใช่สตริงจริงของวิธีแก้สุดท้าย

การแยก ABI ด้วย CHOST

  • platform tuple ใช้ระบุแพลตฟอร์มเป้าหมายของ toolchain และใน Gentoo ยังใช้แยก ABI ให้ไม่ซ้ำกันสำหรับการรองรับ multilib ด้วย
  • tuple ประกอบด้วย 4 ส่วน: สถาปัตยกรรม, vendor, ระบบปฏิบัติการ, libc
    • ตัวอย่าง: i386-pc-linux-gnu
    • ตัวอย่าง: i686-pc-linux-gnu
    • ตัวอย่าง: i686-unknown-linux-gnu
  • เมื่อต้องเพิ่ม ABI ใหม่ เคยมีการใช้วิธีเปลี่ยน vendor field หรือเติมข้อความบอก ABI ลงใน libc field
    • ใน ARM hardfloat ABI เคยใช้รูปแบบอย่าง armv7a-hardfloat-linux-gnueabi และ armv7a-unknown-linux-gnueabihf
  • สำหรับ time64 ABI ก็มีตัวเลือกคล้ายกัน
    • i686-gentoo_t64-linux-gnu
    • i686-pc-linux-gnut64
    • armv7a-gentoo_t64-linux-gnueabihf
    • armv7a-unknown-linux-gnueabihft64
  • การเปลี่ยน tuple ดูเหมือนจะไม่ต้องใช้แพตช์จำนวนมาก
    • GNU toolchain และ GNU build system จะมองข้ามข้อความหลัง gnu ใน libc field
    • Clang ต้องมีแพตช์เพื่อเลือก ABI ที่ถูกต้องโดยอัตโนมัติตาม tuple

การเปลี่ยน libdir และ preserved-libs

  • libdir คือชื่อไดเรกทอรีติดตั้งไลบรารีโดยปริยาย
    • ค่าปกติทั่วไปคือ lib
    • บนสถาปัตยกรรมที่รองรับ 64 บิต มักนิยมใช้ lib64
    • x32 ABI ของ x86 ใช้ libx32 และ n32 ABI ของ MIPS ใช้ lib32
  • มีการพิจารณาเปลี่ยน libdir ของ ABI 32 บิตแบบ time64 จาก lib เป็นค่าอย่าง libt64
  • การมี libdir แยกเป็นกลไกช่วยลดการปน ABI ระหว่างการเปลี่ยน
    • ลดความเสี่ยงที่ไฟล์ปฏิบัติการ time64 จะไปลิงก์กับไลบรารี time32 โดยไม่ตั้งใจ
    • สามารถใช้ฟีเจอร์ preserved-libs ของ Portage เพื่อเก็บไลบรารี time32 ไว้
    • อาจมีโปรไฟล์ multilib แบบ time32 + time64 เพื่อคงความเข้ากันได้กับแอปพลิเคชัน time32 แบบ prebuilt เดิม
  • เมื่อมี preserved-libs ไฟล์ปฏิบัติการเดิมจะยังใช้ไลบรารี time32 ต่อไปจนกว่าจะถูกรีบิลด์ ส่วนไลบรารีที่รีบิลด์เป็น time64 จะถูกติดตั้งใน libdir ใหม่
  • การเปลี่ยน libdir ต้องการแพตช์ใน toolchain
    • glibc อาจจัดการเป็นกรณีพิเศษได้ เพราะชุดไลบรารีเดียวกันใช้ได้กับหลาย ABI ย่อย
    • อาจต้องมี ld.so แยก เพื่อให้ .interp ของไฟล์ปฏิบัติการ time64 ชี้ไปยัง ld.so สำหรับ time64
  • หากต้องการรองรับ multilib อย่างสมบูรณ์ ก็ต้องมี platform tuple ที่เป็นเอกลักษณ์สำหรับ ABI นั้นด้วย

การระบุความไม่เข้ากันในระดับไบนารี

  • เมื่อไบนารีคนละ ABI ถูกปนกัน โดยทั่วไป linker หรือ dynamic loader ควรเป็นตัวห้าม
    • ถ้าลิงก์โปรแกรม 64 บิตกับไลบรารี 32 บิต linker จะปฏิเสธด้วย file in wrong format
    • dynamic loader ก็จะปฏิเสธด้วยข้อผิดพลาดอย่าง wrong ELF class: ELFCLASS32
  • การแยก ABI แบบเดิมใช้กลไกหลายอย่าง
    • ELFCLASS32 และ ELFCLASS64
    • machine identifier อย่าง EM_386 และ EM_X86_64
    • ฟิลด์ flags ของ ARM และ MIPS
    • attribute section เฉพาะสถาปัตยกรรม
  • time32 และ time64 ก็ต้องมีกลไกคล้ายกัน แต่ไม่ง่าย
    • ดูเหมือนยังไม่มีกลไกทั่วไปที่นำกลับมาใช้ได้ตรง ๆ
    • ต้องการวิธีแก้ที่ใช้ได้กับหลายสถาปัตยกรรม
    • ตัวเลือกที่ดูเป็นไปได้คือเพิ่ม ELF note section ใหม่และทำให้ toolchain รองรับ
  • ต้องคำนึงด้วยว่าผู้ใช้อาจปิดมาตรการป้องกันเหล่านี้
    • ซอฟต์แวร์ prebuilt ที่ไม่มีซอร์สแต่ไม่ได้เรียก API ที่ใช้ time_t อาจยังทำงานกับ system library ต่อไปได้
    • ถ้าบังคับห้ามทุกกรณีอาจเลวร้ายกว่าปัญหาที่พยายามแก้
  • หากใช้ libdir แยก ก็สามารถทำ การตรวจ QA แบบไม่ถึงขั้น fatal ได้ค่อนข้างง่าย
    • ใช้ .interp เพื่อแยกไฟล์ปฏิบัติการ time64
    • ตรวจว่าโปรแกรม time32 ไม่ได้โหลดไลบรารีจาก libt64
    • ตรวจว่าโปรแกรม time64 ไม่ได้โหลดไลบรารีตรงจาก lib

ข้อจำกัดของแอปพลิเคชัน 32 บิตแบบ prebuilt

  • นอกเหนือจากแพ็กเกจที่บิลด์จากซอร์ส ยังมีแอปพลิเคชันบน x86 และ PowerPC ที่มีให้เฉพาะไบนารี prebuilt เก่าเท่านั้น
    • โดยเฉพาะซอฟต์แวร์ proprietary และเกมเก่า
  • แอปเหล่านี้เจอทั้งปัญหาความเข้ากันได้กับ system library และ ปัญหาปี 2038 เองโดยตรง
  • สำหรับปัญหาความเข้ากันได้ โครงสร้าง multilib เดิมช่วยได้ในระดับหนึ่ง
    • บน amd64 มีระบบ layout แบบ multilib และกลไกบิลด์หลายเวอร์ชันของไลบรารีอยู่แล้วเพื่อรองรับซอฟต์แวร์ 32 บิต
    • สามารถขยายต่อโดยแยก abi_x86_32 กับ abi_x86_t64
    • สามารถสร้างโปรไฟล์ x86 แบบ multilib ใหม่ที่รองรับทั้งสอง ABI ได้
  • แต่ปัญหาที่โปรแกรม 32 บิตเองจะล้มเหลวหลังปี 2038 ยังแก้ยากกว่า
    • ใช้ faketime เพื่อควบคุมเวลาของระบบได้
    • หรือรัน VM ที่ย้อนเวลาไปอยู่ในอดีตก็ได้

แก้ไขเมื่อ 2024-09-30: แค่ libdir อย่างเดียวไม่พอ

  • แนวคิดเริ่มต้นมองโลกในแง่ดีเกินไป และการเปลี่ยนแค่ libdir อย่างเดียวทำให้แยกได้อย่างเสถียรได้ยาก
  • เนื่องจาก libdir ทั้งหมดถูกระบุไว้ใน ld.so.conf จึงไม่สามารถพึ่งวิธี hardcode path ของ libdir ไว้ใน ld.so ได้
    • ใน custom LLVM prefix ก็มีการปรับ path อยู่แล้ว และกรณีนี้ก็ต้องมีการจัดการพิเศษเช่นกัน
  • ด้วยเหตุนี้ การเปลี่ยน libdir จึงมีแนวโน้มต้องพึ่ง การแยกความไม่เข้ากันระดับไบนารี มากขึ้น
  • เป้าหมายพื้นฐานที่ต้องทำให้ได้มีสามข้อ
    • dynamic loader ต้องแยกไบนารี time32 กับ time64 ได้
    • ไบนารีทั้งหมดที่ไม่มีเครื่องหมาย time64 แบบชัดเจน ต้องถูกมองเป็น time32 เพื่อคง backward compatibility
    • ไบนารีใหม่ทั้งหมดที่ถูกบิลด์ต้องมีเครื่องหมาย time64 แบบชัดเจน รวมถึงไบนารีที่บิลด์จากสภาพแวดล้อมที่ไม่ใช่ C เช่น Rust
  • เป้าหมายนี้หมายถึงงานระดับที่ต้องแพตช์ toolchain หลายตัวจากหลายภาษา
    • Gentoo คงดูแลเป็นแพตช์เฉพาะภายในได้ยาก และต้องอาศัยความร่วมมือจากหลายฝ่าย
    • สถาปัตยกรรมเป้าหมายจำนวนมากมักถูกมองว่าเป็นระบบเก่าหรือไม่ได้รับการสนับสนุนมากพอแล้ว
  • อีกประเด็นคือ toolchain อื่นจะสร้างไฟล์ปฏิบัติการ time64 ที่ถูกต้องได้หรือไม่
    • ถ้าไม่ได้ถูกปรับให้ทำตาม _TIME_BITS แบบเดียวกับโปรแกรม C ก็อาจ hardcode ความกว้างของ time_t ไว้และพังได้
  • เนื่องจากไบนารีทั้งหมดที่ไม่มีเครื่องหมาย time64 ชัดเจนจะใช้ไลบรารี time32 ทำให้ Gentoo จะไม่สามารถรันไฟล์ปฏิบัติการของ third-party ที่ไม่ได้ถูกแพตช์ให้ใส่เครื่องหมายอย่างถูกต้องได้
  • ยังมีการพิจารณาทางเลือกที่ตั้งเป้าต่ำลง
    • ใส่ RPATH ให้กับไฟล์ปฏิบัติการ time64 ทุกตัวเพื่อบังคับให้ใช้ time64 libdir โดยตรง
    • วิธีนี้แม้ไม่ป้องกัน dynamic loader จากการใช้ไลบรารี time32 ได้อย่างสมบูรณ์ แต่ก็ช่วยให้เปลี่ยนผ่านได้โดยมีปัญหาความเข้ากันได้ไม่มาก
  • ในทางกลับกัน อาจไม่เปลี่ยน time64 libdir แบบถาวร แต่เปลี่ยน time32 libdir ชั่วคราวแทน
    • ใส่ RPATH ให้โปรแกรมเดิมและเปลี่ยนชื่อ libdir
    • ติดตั้งไลบรารี time64 ใหม่ไว้ใน libdir เดิม
    • โปรแกรม time64 ใหม่จะไม่มี RPATH ที่บังคับใช้ไลบรารี time32
    • ข้อดีคือยังคงความเข้ากันได้กับดิสโทรอื่นที่เปลี่ยนผ่านไปแล้ว

งานที่ยังเหลือ

  • หากทำทั้งสามแนวทางได้ครบ ก็จะให้เส้นทางเปลี่ยนผ่านที่สะอาดและค่อนข้างปลอดภัยกว่าสำหรับระบบ Gentoo 32 บิตที่ใช้ glibc
  • อย่างไรก็ตาม วิธีเหล่านี้ใช้ได้หลัก ๆ กับแพ็กเกจที่บิลด์จากซอร์ส
  • แอปพลิเคชัน 32 บิตแบบ prebuilt ยังมีปัญหาปี 2038 อยู่ แม้จะรักษาความเข้ากันได้ของ ABI เอาไว้ได้ก็ตาม
  • การออกแบบทั้งหมดนี้ยังเป็นเพียงฉบับร่าง และอาจเปลี่ยนแปลงต่อเนื่องตามการทดลอง การอภิปราย และการส่งแพตช์

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

 
GN⁺ 2024-09-30
ความคิดเห็นจาก Hacker News
  • ใน Gentoo มีทางเลือกอยู่บางอย่างที่บทความไม่ได้กล่าวถึง และดูเหมือนจะถูกละไว้เพราะภาระงานค่อนข้างมากตามการออกแบบระบบของ Gentoo

    1. ทำให้สามารถบิลด์โดยกำหนดเป้าหมายเป็นแพ็กเกจนั้นได้โดยไม่ต้องติดตั้งแพ็กเกจนั้นก่อน ประเด็นสำคัญคือใน Gentoo การบิลด์และการติดตั้งแพ็กเกจเป็นขั้นตอนเดียวกัน จึงไม่สามารถบิลด์รายการหลายอย่างที่พึ่งพากันให้เสร็จก่อน แล้วค่อย ติดตั้งผลลัพธ์แบบอะตอมิก ได้ ระหว่างการอัปเดตที่มีการเปลี่ยน ABI ระบบจึงมักเสียหายแบบบางส่วนได้ง่าย
    2. ขยายการจัดการเวอร์ชัน .so แบบทั่วไปให้สะท้อนถึงการเปลี่ยน ABI ของแพ็กเกจที่พึ่งพาด้วย โดยปกติไลบรารีแบบแชร์จะมีหมายเลขเวอร์ชันอยู่ในชื่อไฟล์และเวอร์ชันภายใน เช่น libfoo.so.1.0.0 และแพ็กเกจจะติดตามการ break ABI ของตัวเอง หากต้องการรองรับ time_t แบบ 64 บิต ก็ต้องเพิ่มองค์ประกอบเวอร์ชันที่ควบคุมโดย ABI ของ dependency ของแต่ละ .so ผลลัพธ์คล้ายกับ “การใช้ libdir อื่น” ในบทความ แต่แม้จะอาจเป็นรากฐานที่นำกลับมาใช้ได้กับการเปลี่ยน ABI ในอนาคต ก็มีแนวโน้มว่าจะกระทบระบบลึกกว่ามาก
    • หากต้องการ “บิลด์โดยไม่ติดตั้ง” ดูเหมือนว่า การอัปเดตแบบเป็นขั้นบางส่วน จะเหมาะที่สุด
      สามารถจัดคิวบิลด์แพ็กเกจใหม่หลายตัว แล้วบิลด์ใน sandbox จากนั้นให้การคอมไพล์ใหม่มองเห็น sandbox ก่อนผ่าน union และค่อย fallback ไปยังระบบ เมื่อบิลด์ทั้งหมดเสร็จ ก็แพ็กเกจผลลัพธ์แล้วย้ายจาก sandbox ไปยังระบบจริงได้ วิธีนี้ทำให้การอัปเดต Gentoo ทั้งหมดกลายเป็นเหมือนทรานแซกชันได้ ซึ่งให้ประโยชน์ใหญ่มากในด้านอื่นด้วย
    • Gentoo รองรับข้อ 1 อยู่แล้ว ด้วยวิธีกำหนดไดเรกทอรีที่จะติดตั้งอิมเมจที่สร้างเสร็จขั้นสุดท้ายลงไป เพียงเปลี่ยน ROOT ซึ่งปกติตั้งเป็น /
      สามารถบิลด์ @system และ @world ใหม่ทั้งหมด แล้วติดตั้งลงในไดเรกทอรีย่อยที่กำหนด จากนั้นซิงก์ทีเดียวได้ หากเป็นไปได้ควรทำใน live session และในทางทฤษฎีสามารถ bind mount / เข้าไปในไดเรกทอรีย่อยของตำแหน่งที่ติดตั้งใหม่ จากนั้น chroot เข้าไป แล้วซิงก์กลับไปยัง / ระดับบนจริงได้เช่นกัน
      https://devmanual.gentoo.org/ebuild-writing/variables/#root
    • Gentoo เก็บ ไลบรารีเดิม ไว้อยู่แล้วจนกว่า dependency ทั้งหมดจะได้รับการอัปเดต ดังนั้นหากเข้ารหัสการเปลี่ยน ABI ไว้ในสถาปัตยกรรมและ SONAME ซึ่งเป็นตำแหน่งที่ควรบันทึกการเปลี่ยน ABI เดิม ก็จะแก้ปัญหานี้ได้
  • วิธีที่ Mac OS X จัดการ off_t และ ino_t อาจเป็นแนวทางได้ การเรียกและ struct เดิมยังคงพฤติกรรมไว้ แล้วเพิ่มการเรียกและชนิดใหม่ที่มี suffix 64 และสามารถใช้มาโคร preprocessor เพื่อเลือกสิ่งที่อ้างถึงจริงได้ แม้แทบไม่ค่อยมีใครใช้โดยตรง
    แทนที่จะเป็นอย่างนั้น OS และ SDK ถูกจัดเวอร์ชัน และตอนบิลด์สามารถระบุเวอร์ชัน OS เก่าสุดที่ไบนารีต้องรันได้ เฮดเดอร์จะเลือกมาโครที่เหมาะสมให้อัตโนมัติตามข้อมูลนี้ และคำอธิบายประกอบ API ใหม่/เลิกใช้ API ก็ใช้กลไกเดียวกันเพื่อสร้าง weak link หรือ warning ตอนแรกจัดการด้วย preprocessor แต่ตอนนี้คอมไพเลอร์เข้าใจสิ่งที่ Apple เรียกว่า API availability ได้ละเอียดขึ้น จึงดูเหมือนว่าวิธีเดียวกันนี้เป็นไปได้บนแพลตฟอร์มอื่นด้วย

    • แก้ปัญหาหลักที่ TFA อธิบายไว้ไม่ได้ แอปพลิเคชันที่ใช้ “เวอร์ชัน OS เป้าหมายสำหรับคอมไพล์” ต่างกันจะไม่สามารถลิงก์กันได้อีกต่อไป
      แม้กำลังรันบน OS v.B อยู่ แอปพลิเคชัน X ที่ประกาศว่า target เป็น OS v.B ก็อาจลิงก์กับแอปพลิเคชัน Y ที่ประกาศว่า target เป็น OS v.A ไม่ได้ จริง ๆ แล้ววิธีนี้ใกล้เคียงกับสิ่งที่แทบทุกแพลตฟอร์มทำอยู่แล้ว และถ้าทำต่างออกไป ความเข้ากันได้กับไบนารีเดิมก็จะพังทันที
    • ไลบรารีเสริมที่ไม่ใช่ glibc คงไม่กำหนดฟังก์ชันหลายชุดแยกตามขนาดของ off_t และคงไม่มีสวิตช์ในเฮดเดอร์ที่เลือกชุดฟังก์ชันที่ถูกต้องให้แบบโปร่งใสตามขนาดชนิดที่โปรแกรมไคลเอนต์ต้องการ
      ถึงอย่างนั้น บทความก็ยังเน้นว่า time_t เป็นปัญหาใหญ่กว่า off_t เหตุผลที่น่าจะเป็นไปได้คือ เพราะ time_t แพร่หลายกว่ามาก off_t เป็นชนิด POSIX ที่เกี่ยวข้องกับอินเทอร์เฟซค่อนข้างน้อย ขณะที่ time_t อยู่ใน ISO C และถูกใช้ทั่วไป นอกจากนี้โค้ด C จำนวนมากยังสมมติว่า time_t เป็นชนิดจำนวนเต็มที่มีความกว้างเท่ากับ int แต่การสมมติแบบนั้นกับ off_t พบได้น้อยกว่า
    • ฟังดูเหมือนวิธีแก้ที่งดงาม แต่ในทางปฏิบัติอ่านแล้วเหมือนแฮ็กที่เลวร้าย มาโคร ที่ไม่มีชนิดเป็นฝันร้ายที่ไม่อยากยุ่งด้วยอีกแล้ว
    • ใช้ได้ก็ต่อเมื่อสามารถบังคับให้ทั้งแพลตฟอร์มทำตามได้ เป็นวิธีแก้ที่ดี แต่ต้องควบคุม ไลบรารี C ได้ Gentoo ควบคุมไม่ได้ว่า libc จะทำอะไร และผู้ใช้ก็อาจใช้ GNU libc, musl หรืออย่างอื่นก็ได้
  • Debian ก็เจ็บปวดมากเช่นกัน บางคนอาจถึงขั้นหมดไฟ และหลายคนชี้ไปที่ดิสโทรแบบ source-based แล้วพูดว่า “ฝั่งนั้นน่าจะง่ายมาก”

    • อยากเห็นข้อมูลละเอียด ๆ ว่า การเปลี่ยนผ่าน time64 ของ Debian เจ็บปวดอย่างไร จากภายนอกดูเหมือนค่อนข้างราบรื่นและไม่มีข้อถกเถียงมากนัก เช่น ดูดีกว่าการรวม /usr มาก
    • ได้ทำการเปลี่ยนผ่าน m68k, powerpc, sh4 และช่วย hppa บางส่วนด้วย โดยได้รับความช่วยเหลือจากนักพัฒนา Debian คนอื่น ๆ และยังรอดอยู่
    • ถ้า “ง่าย” หมายถึง “แค่บอกผู้ใช้ให้บิลด์ใหม่ทั้งหมดในคราวเดียวก็พอ” ก็น่าจะใช่
  • ทุกครั้งที่เห็นคนต้องลำบากกับปัญหาแบบนี้ ก็รู้สึกว่าโชคดีจริง ๆ ที่ตอนทำพอร์ต amd64 ของ FreeBSD ช่วงแรก ๆ เราผลักดันเรื่องนี้ไปได้ ตอนนั้นเราสามารถกำหนดชนิดพื้นฐานของ ABI ได้ และตัดสินใจมองไปข้างหน้าแทนที่จะยึดติดกับอดีต
    amd64 มีคุณลักษณะที่น่าสนใจซึ่งทำให้งานนี้ง่ายขึ้น คือระหว่างการเรียกฟังก์ชัน อาร์กิวเมนต์ฟังก์ชันแบบ 32 บิตจะถูก cast เป็น 64 บิตโดยอัตโนมัติ ดังนั้นแม้จะส่งจำนวนเต็มเวลาแบบ 32 บิตให้ฟังก์ชันที่คาดหวัง time_t แบบ 64 บิต ในช่วงงานเริ่มต้นของแพลตฟอร์มก็มักจะทำงานได้เลย ทำให้สามารถเลื่อนงานเก็บรายละเอียดเล็ก ๆ น้อย ๆ ไปทีหลังได้
    ตอนนั้นมีแพลตฟอร์ม 64 บิตอื่น ๆ อยู่แล้ว แต่ยังไม่มี time_t แบบ 64 บิต และ FreeBSD/amd64 น่าจะเป็นรายแรกในกลุ่มนั้นราวปี 2003~2005 เท่าที่จำได้ sparc64 ก็ย้ายไปใช้ time_t แบบ 64 บิตด้วย
    ปัญหาใหญ่ที่สุดคือในตอนนั้น tzcode ยังไม่ปลอดภัยกับ 64 บิต อัลกอริทึม normalize ของ struct tm จะตกไปอยู่ในกรณีเสื่อมถอยที่พยายามคำนวณวัน/เดือน/ปีของ time_t(2^62) ซ้ำ ๆ เท่าที่จำได้ แทนที่จะปรับ tzcode ครั้งใหญ่ เราจัดการให้ล้มเหลวสำหรับช่วงก่อนราวปี 1900 หรือหลังปี 10000 ตอนนี้มีความเป็นไปได้สูงว่า upstream แก้ไปนานแล้ว
    หลายปีต่อมา เราต้องไล่แก้ความสับสนเรื่องเวลา 32/64 บิตแบบตีตัวตุ่น ซึ่งเกิดจากโค้ด third-party ที่จัดการ int/long/time_t ในโครงสร้างข้อมูลของไฟล์หรือเครือข่ายแบบหลวม ๆ แต่โดยรวมแล้วไม่ใช่ปัญหาใหญ่ การใช้ time_t แบบ 64 บิตตั้งแต่วันแรกช่วยหลีกเลี่ยงปัญหาส่วนใหญ่ และถ้าทำตั้งแต่ต้นก็ง่าย Linux พลาดโอกาสครั้งใหญ่ที่จะทำแบบเดียวกันตอนเริ่มพอร์ต amd64/x86_64
    เพิ่มเติม ตอนนั้นเรายังทำ ino_t แบบ 64 บิตไม่สำเร็จ หมายเลข inode แบบ 32 บิตถูกเปิดเผยอยู่ในที่ต่าง ๆ มากมาย เช่น โครงสร้างบนดิสก์ของระบบไฟล์ โครงสร้างไดเรกทอรีของ UFS ฯลฯ ตอนที่ FreeBSD/amd64 ยังเป็นแพลตฟอร์มระดับรอง ไม่มีวิธีที่ทำได้จริงในการจัดการตั้งแต่ต้นโดยไม่กระทบสถาปัตยกรรม tier-1 อื่น ๆ อย่างหนัก งานนี้ทำกันสองครั้ง แต่สุดท้ายคนอื่นเป็นคนปิดงาน และแก้ค่าคงที่ที่สั้นเกินไปอย่างความยาวพาธของ mountpoint ไปพร้อมกันด้วย

    • เท่าที่เข้าใจ พอร์ต Linux 64 บิตทั้งหมดใช้ time_t, off_t, ino_t แบบ 64 บิตมาตั้งแต่ต้น ปัญหาตอนนี้คือการ ย้าย Linux 32 บิตไปเป็น time_t แบบ 64 บิต
    • FreeBSD จัดการ off_t อย่างกล้ากว่านั้นด้วย โดยทำให้เป็น 64 บิตตั้งแต่ 2.0 ใน Linux รุ่น 32 บิตยังมีร่องรอยของขนาดเก่าอยู่
      ส่วนที่ว่าอาร์กิวเมนต์ฟังก์ชันแบบ 32 บิตจะถูก cast เป็น 64 บิตโดยอัตโนมัติระหว่างการเรียกนั้น ผมเข้าใจว่าทำงานเฉพาะกับอาร์กิวเมนต์แบบไม่มีเครื่องหมาย เพราะเมื่อโหลดเข้า %edi ส่วนบนของ %rdi จะถูกล้าง สเปก SysV ABI สำหรับ x86-64 ไม่ได้บอกว่าค่าทั้งหมดในรีจิสเตอร์หรือสแต็กจะถูกขยายเป็นค่าเต็ม 64 บิต และหมายเหตุเกี่ยวกับบูลีนก็ระบุทำนองว่ามีความหมายแค่ 1 ไบต์ล่าง จึงชี้ว่าเรื่องนี้เป็นกฎทั่วไป
    • ถ้าหมายความว่าเมื่อ amd64 ปรากฏขึ้น FreeBSD ก็พอร์ต time_t ของ i386 ให้เป็น 64 บิตด้วย นั่นก็น่าประหลาดใจทีเดียว อยากรู้ว่าสถาปัตยกรรม 32 บิตอื่น ๆ อย่าง Motorola 68000 หรือ sparc32 ก็ย้ายไปใช้ time_t แบบ 64 บิตด้วยหรือไม่
  • เคยมีครั้งหนึ่งที่ต้องจัดการวันที่ในอนาคตบนระบบ Unix 32 บิตขนาดใหญ่รุ่นเก่า เลยเปลี่ยนฟังก์ชัน libc ที่ใช้ time_t แบบ 32 บิตมีเครื่องหมาย ให้เป็นฟังก์ชันรองรับ time_t แบบ 32 บิตไม่มีเครื่องหมาย วิธีนี้ทำให้ได้เวลาเพิ่มหลังปี 2038 อีก 68 ปี และถึงตอนนั้นผมคงไม่อยู่แล้ว
    ข้อเสียคือไม่สามารถแทนวันที่ก่อนปี 1970 ซึ่งเป็น Unix epoch ได้ แต่เพราะเป็นระบบจัดการตารางนัดหมายจึงไม่ใช่ปัญหา ถ้าวันที่ในอดีตสำคัญ ก็อาจเลื่อน epoch ไปหลายสิบปี หรือจะลดความละเอียดของเวลาจาก 1 วินาทีเป็น 2 วินาทีก็ได้ แต่ละวิธีก็มีปัญหาละเอียดอ่อนของตัวเอง ขึ้นอยู่กับ use case

    • ถ้าสามารถเปลี่ยนทั้งระบบจากมีเครื่องหมายเป็นไม่มีเครื่องหมายได้ แล้วทำไมไม่เปลี่ยนเป็น 64 บิต ไปเลยล่ะ น่าสงสัย
  • เดิมทีในหน้า man ของ BSD สำหรับ tunefs ส่วน “Bugs” มีมุกดังว่า “You can tune a file system, but you can't tune a fish.” และตามหนังสือ “Expert C Programming” ในซอร์สโค้ดของหน้า man นั้นมีคอมเมนต์ข้างมุกนี้ว่า
    “ถ้าลบสิ่งนี้ออก เดมอน UNIX จะตามคุณไปสี่ก้าวนับจากตอนนี้จนกว่า time_t จะ wrap around”
    ตอนประโยคนี้ถูกเขียนในยุค 70 ปี 2038 คงเป็นอนาคตที่ไกลเกินจินตนาการแน่ ๆ
    https://progforperf.github.io/Expert_C_Programming.pdf

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

    • ความยากของดิสโทรที่อิงซอร์สดูเหมือนจะมาจากการพยายามทำ การอัปเกรดในที่เดิม พร้อมกับเปลี่ยน ABI ที่เข้ากันไม่ได้ ดังนั้นการเปลี่ยนไปใช้ดิสโทรคนละตัวโดยสิ้นเชิง ก็อาจ disruptive ในระดับเดียวกับการติดตั้ง Gentoo ใหม่แบบสะอาดที่ใช้ ABI ใหม่ อย่างน้อยก็เท่ากัน แม้อาจใช้เวลาน้อยกว่า
    • ใน Gentoo เองก็มีวิธีจัดการให้ง่ายได้ บูตด้วย USB หรืออะไรทำนองนั้น แล้วรัน mkfs.ext4 หรือระบบไฟล์ที่ใช้อยู่บนพาร์ทิชัน / และ /usr จากนั้น mount แตก stage3 เข้า chroot แล้วรัน emerge $all-my-packages-that-where-installed-before-mkfs
      แทนที่จะอัปเกรดแบบค่อยเป็นค่อยไป ก็สามารถติดตั้ง สำเนา Gentoo ใหม่ ได้
    • การแบ่งว่าแค่เปลี่ยนไปใช้ดิสโทรที่ไม่อิงซอร์สก็พอนั้นละเอียดอ่อนกว่านั้น ดิสโทรที่อิงซอร์สอย่าง NixOS ไม่มีปัญหาเดียวกัน ประเด็นหลักไม่ได้อยู่ที่ว่าสร้างจากซอร์สหรือไม่ แต่อยู่ที่วิธีที่ Gentoo บิลด์และติดตั้งแพ็กเกจ
      ถ้ามีซอฟต์แวร์ third-party แบบ closed source ก็ยังเกิดปัญหาได้แม้ในระบบไบนารี แพ็กเกจ first-party ที่ติดตั้งแยกเป็นขั้นตอนอิสระก็อาจมีปัญหาได้เช่นกัน
  • ผมไม่ใช่ผู้เชี่ยวชาญ C แต่เคยคิดว่า alias ของประเภทอย่าง off_t ถูกนำมาใช้เพื่อให้ภายหลังสามารถเปลี่ยนได้ แต่ดูเหมือนมันไม่ได้ทำงานชัดเจนตามนั้น เลยสงสัยว่าผมเข้าใจผิดหรือเปล่า

    • นี่คือความต่างระหว่างความเข้ากันได้ของซอร์สกับความเข้ากันได้ของไบนารี การใช้ typedef อย่าง off_t โดยทั่วไปทำให้ไม่ต้องเขียนโค้ดใหม่ แต่ทุกอย่างที่ใช้ประเภทนั้นต้องคอมไพล์ใหม่ทั้งหมด
    • มันทำงานได้ในระดับหนึ่ง แต่ไม่ค่อยเข้ากับดิสโทรแบบอิงซอร์ส ถ้าหลังจากเปลี่ยนนิยามของ off_t แล้วสามารถบิลด์ @world ใหม่แบบ atomic ได้ก็คงไม่มีปัญหา แต่ดิสโทรแบบอิงซอร์สไม่ได้รีบิลด์ @world แบบ atomic แต่รีบิลด์แพ็กเกจทีละตัว
      แบบนั้นอาจเกิดปัญหาเช่น libc.so ใช้ off_t 64 บิต แต่ gcc ถูกบิลด์ตาม off_t 32 บิต ทำให้ gcc ค้างได้ แพ็กเกจที่จำเป็นต่อการรีบิลด์ @world อย่าง bash, coreutils, make, binutils ก็อาจพังได้ และถึงจุดนั้นก็ไปต่อไม่ได้ ดังนั้นการอัปเกรดแบบนี้ต้องระมัดระวัง
    • นั่นเป็นแค่ขั้นแรกของปริศนา หรืออาจจะแค่ครึ่งขั้นด้วยซ้ำ อย่างที่บทความบอก ทันทีที่ off_t เข้าไปอยู่ใน struct ถูกใช้ในการเรียกฟังก์ชัน หรือถูกรวมเข้าในโปรโตคอล abstraction ก็หายไป และขนาดจริงก็มีความสำคัญขึ้นมา
      ถ้าผสมโค้ดเก่ากับโค้ดใหม่ขณะโหลดไลบรารีหรือสื่อสารผ่านโปรโตคอล offset จะเริ่มคลาดเคลื่อนและเกิดการชนกัน สุดท้ายการเปลี่ยนผ่านทำให้ทุกคนต้องแยกโปรแกรมออกเป็น “legacy” กับ “พอร์ตแล้ว หรืออย่างน้อยตรวจสอบแล้ว” ซึ่งเจ็บปวดมาก
    • alias ของประเภทช่วยให้ง่ายขึ้นเฉพาะในระดับซอร์สโค้ดเท่านั้น จริง ๆ แล้วไม่ใช่ abstraction ที่แท้จริง โดยเฉพาะไม่ใช่ abstraction ที่สมบูรณ์ ตัวอย่างเช่น ถ้าเปลี่ยนประเภทภายในเป็นประเภท floating-point ความหมายจะเปลี่ยนไปมากและปรากฏต่อโค้ดของผู้ใช้ทั้งหมด
      ต่อให้เปลี่ยนเป็นประเภทที่ใหญ่ขึ้นและมีความหมายใกล้เคียงกันก็ยังพังได้ ตัวอย่างง่าย ๆ คือ padding ใน struct และยังมีกรณีใช้งานจำนวนมากที่แปลง pointer เป็น integer แล้วแปลงกลับ ดังนั้นถ้าการแทนค่าภายในเปลี่ยนก็หลีกเลี่ยงไม่ได้ที่จะพัง นั่นจะเป็นแนวปฏิบัติที่ดีหรือไม่เป็นอีกเรื่อง แต่ก็ไม่ใช่เรื่องที่พบได้ยาก ประเด็นหลักคือ ความเข้ากันได้ของ ABI
    • มันทำงานได้ แต่ปัญหาของการเปลี่ยน ABI คือเมื่อจะเปลี่ยน ต้องเปลี่ยน ทุกที่พร้อมกัน โดยพื้นฐานแล้วไม่มีสิ่งใดป้องกันไม่ให้ลิงก์ไลบรารีที่บิลด์ด้วย off_t 32 บิตเข้ากับไลบรารีที่บิลด์ด้วย off_t 64 บิต และพฤติกรรมที่ได้อาจคาดเดาไม่ได้อย่างมาก
  • ใน struct ตัวอย่างบอกว่าเมื่อ time_t เป็น 32 บิต offset ของ c คือ 8 และเมื่อเป็นประเภท 64 บิตคือ 12 แต่จริง ๆ แล้ว ไม่ควรเป็น 16 หรือ เพราะ b ต้องถูกจัดแนวแบบ 64 บิต ดังนั้นต้องมี padding ระหว่าง a กับ b ด้วย ซึ่งยิ่งทำให้ประเด็นที่ผู้เขียนต้องการสื่อหนักแน่นขึ้นด้วยซ้ำ

    • ABI ของ x86 ส่วนใหญ่ในตอนนั้นไม่มีการโหลด 64 บิต จึงไม่ได้บังคับ padding สำหรับประเภท 64 บิต
  • เมื่อดูทั้งหมดนี้ การแทนเวลาประหลาด ๆ ของ Windows คือการนับแบบ 64 บิตหน่วยละ 100ns ตั้งแต่วันที่ 1 มกราคม 1601 เวลา 00:00 GMT ตามปฏิทินเกรกอเรียน ก็มีข้อดีเล็ก ๆ อยู่เหมือนกัน ความละเอียดก็ดีเยี่ยม และมันน่าจะยังทำงานได้จนกว่าทั้งกาแล็กซีจะถูกพิชิต