ความเสี่ยงของการเปลี่ยนไปใช้ `time_t` แบบ 64 บิต
(blogs.gentoo.org)- ระบบ 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_t64 บิต,ino_t64 บิต,time_t32 บิต - time64: LFS +
time_t64 บิต
- glibc build เดียวสามารถเข้ากันได้กับทั้งสามแบบ แต่ไลบรารีที่ใช้ชนิดข้อมูลเหล่านี้ใน API ไม่สามารถผสมทั้งสามแบบเข้าด้วยกันได้
วิธีที่ ABI เปลี่ยนแล้วพังจริงในทางปฏิบัติ
- เมื่อ
time_tเปลี่ยนจาก 32 บิตเป็น 64 บิต layout ของโครงสร้างข้อมูลจะเปลี่ยนตาม- ในตัวอย่างโครงสร้างที่มี
int a,time_t b,int cตามลำดับ ค่าออฟเซ็ตของcจะแตกต่างกันระหว่างtime_t32 บิตกับ 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 ย่อยต่างกันลิงก์เข้าหากันได้
- เปลี่ยน platform tuple หรือ
- ทั้งสามวิธีสามารถทำได้ค่อนข้างอิสระต่อกัน แต่บางส่วนอาจต้องพึ่งพากัน
- สตริงตัวอย่างในบทความอาจไม่ใช่สตริงจริงของวิธีแก้สุดท้าย
การแยก 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
- ใน ARM hardfloat ABI เคยใช้รูปแบบอย่าง
- สำหรับ time64 ABI ก็มีตัวเลือกคล้ายกัน
i686-gentoo_t64-linux-gnui686-pc-linux-gnut64armv7a-gentoo_t64-linux-gnueabihfarmv7a-unknown-linux-gnueabihft64
- การเปลี่ยน tuple ดูเหมือนจะไม่ต้องใช้แพตช์จำนวนมาก
- GNU toolchain และ GNU build system จะมองข้ามข้อความหลัง
gnuใน libc field - Clang ต้องมีแพตช์เพื่อเลือก ABI ที่ถูกต้องโดยอัตโนมัติตาม tuple
- GNU toolchain และ GNU build system จะมองข้ามข้อความหลัง
การเปลี่ยน 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
- ถ้าลิงก์โปรแกรม 64 บิตกับไลบรารี 32 บิต linker จะปฏิเสธด้วย
- การแยก 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 ต่อไปได้ - ถ้าบังคับห้ามทุกกรณีอาจเลวร้ายกว่าปัญหาที่พยายามแก้
- ซอฟต์แวร์ prebuilt ที่ไม่มีซอร์สแต่ไม่ได้เรียก API ที่ใช้
- หากใช้ 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 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ใน Gentoo มีทางเลือกอยู่บางอย่างที่บทความไม่ได้กล่าวถึง และดูเหมือนจะถูกละไว้เพราะภาระงานค่อนข้างมากตามการออกแบบระบบของ Gentoo
.soแบบทั่วไปให้สะท้อนถึงการเปลี่ยน ABI ของแพ็กเกจที่พึ่งพาด้วย โดยปกติไลบรารีแบบแชร์จะมีหมายเลขเวอร์ชันอยู่ในชื่อไฟล์และเวอร์ชันภายใน เช่นlibfoo.so.1.0.0และแพ็กเกจจะติดตามการ break ABI ของตัวเอง หากต้องการรองรับtime_tแบบ 64 บิต ก็ต้องเพิ่มองค์ประกอบเวอร์ชันที่ควบคุมโดย ABI ของ dependency ของแต่ละ.soผลลัพธ์คล้ายกับ “การใช้ libdir อื่น” ในบทความ แต่แม้จะอาจเป็นรากฐานที่นำกลับมาใช้ได้กับการเปลี่ยน ABI ในอนาคต ก็มีแนวโน้มว่าจะกระทบระบบลึกกว่ามากสามารถจัดคิวบิลด์แพ็กเกจใหม่หลายตัว แล้วบิลด์ใน sandbox จากนั้นให้การคอมไพล์ใหม่มองเห็น sandbox ก่อนผ่าน union และค่อย fallback ไปยังระบบ เมื่อบิลด์ทั้งหมดเสร็จ ก็แพ็กเกจผลลัพธ์แล้วย้ายจาก sandbox ไปยังระบบจริงได้ วิธีนี้ทำให้การอัปเดต Gentoo ทั้งหมดกลายเป็นเหมือนทรานแซกชันได้ ซึ่งให้ประโยชน์ใหญ่มากในด้านอื่นด้วย
ROOTซึ่งปกติตั้งเป็น/สามารถบิลด์
@systemและ@worldใหม่ทั้งหมด แล้วติดตั้งลงในไดเรกทอรีย่อยที่กำหนด จากนั้นซิงก์ทีเดียวได้ หากเป็นไปได้ควรทำใน live session และในทางทฤษฎีสามารถ bind mount/เข้าไปในไดเรกทอรีย่อยของตำแหน่งที่ติดตั้งใหม่ จากนั้น chroot เข้าไป แล้วซิงก์กลับไปยัง/ระดับบนจริงได้เช่นกันhttps://devmanual.gentoo.org/ebuild-writing/variables/#root
วิธีที่ Mac OS X จัดการ
off_tและino_tอาจเป็นแนวทางได้ การเรียกและ struct เดิมยังคงพฤติกรรมไว้ แล้วเพิ่มการเรียกและชนิดใหม่ที่มี suffix64และสามารถใช้มาโคร preprocessor เพื่อเลือกสิ่งที่อ้างถึงจริงได้ แม้แทบไม่ค่อยมีใครใช้โดยตรงแทนที่จะเป็นอย่างนั้น OS และ SDK ถูกจัดเวอร์ชัน และตอนบิลด์สามารถระบุเวอร์ชัน OS เก่าสุดที่ไบนารีต้องรันได้ เฮดเดอร์จะเลือกมาโครที่เหมาะสมให้อัตโนมัติตามข้อมูลนี้ และคำอธิบายประกอบ API ใหม่/เลิกใช้ API ก็ใช้กลไกเดียวกันเพื่อสร้าง weak link หรือ warning ตอนแรกจัดการด้วย preprocessor แต่ตอนนี้คอมไพเลอร์เข้าใจสิ่งที่ Apple เรียกว่า API availability ได้ละเอียดขึ้น จึงดูเหมือนว่าวิธีเดียวกันนี้เป็นไปได้บนแพลตฟอร์มอื่นด้วย
แม้กำลังรันบน OS v.B อยู่ แอปพลิเคชัน X ที่ประกาศว่า target เป็น OS v.B ก็อาจลิงก์กับแอปพลิเคชัน Y ที่ประกาศว่า target เป็น OS v.A ไม่ได้ จริง ๆ แล้ววิธีนี้ใกล้เคียงกับสิ่งที่แทบทุกแพลตฟอร์มทำอยู่แล้ว และถ้าทำต่างออกไป ความเข้ากันได้กับไบนารีเดิมก็จะพังทันที
off_tและคงไม่มีสวิตช์ในเฮดเดอร์ที่เลือกชุดฟังก์ชันที่ถูกต้องให้แบบโปร่งใสตามขนาดชนิดที่โปรแกรมไคลเอนต์ต้องการถึงอย่างนั้น บทความก็ยังเน้นว่า
time_tเป็นปัญหาใหญ่กว่าoff_tเหตุผลที่น่าจะเป็นไปได้คือ เพราะtime_tแพร่หลายกว่ามากoff_tเป็นชนิด POSIX ที่เกี่ยวข้องกับอินเทอร์เฟซค่อนข้างน้อย ขณะที่time_tอยู่ใน ISO C และถูกใช้ทั่วไป นอกจากนี้โค้ด C จำนวนมากยังสมมติว่าtime_tเป็นชนิดจำนวนเต็มที่มีความกว้างเท่ากับintแต่การสมมติแบบนั้นกับoff_tพบได้น้อยกว่าDebian ก็เจ็บปวดมากเช่นกัน บางคนอาจถึงขั้นหมดไฟ และหลายคนชี้ไปที่ดิสโทรแบบ source-based แล้วพูดว่า “ฝั่งนั้นน่าจะง่ายมาก”
/usrมากทุกครั้งที่เห็นคนต้องลำบากกับปัญหาแบบนี้ ก็รู้สึกว่าโชคดีจริง ๆ ที่ตอนทำพอร์ต 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 ไปพร้อมกันด้วยtime_t,off_t,ino_tแบบ 64 บิตมาตั้งแต่ต้น ปัญหาตอนนี้คือการ ย้าย Linux 32 บิตไปเป็นtime_tแบบ 64 บิตoff_tอย่างกล้ากว่านั้นด้วย โดยทำให้เป็น 64 บิตตั้งแต่ 2.0 ใน Linux รุ่น 32 บิตยังมีร่องรอยของขนาดเก่าอยู่ส่วนที่ว่าอาร์กิวเมนต์ฟังก์ชันแบบ 32 บิตจะถูก cast เป็น 64 บิตโดยอัตโนมัติระหว่างการเรียกนั้น ผมเข้าใจว่าทำงานเฉพาะกับอาร์กิวเมนต์แบบไม่มีเครื่องหมาย เพราะเมื่อโหลดเข้า
%ediส่วนบนของ%rdiจะถูกล้าง สเปก SysV ABI สำหรับ x86-64 ไม่ได้บอกว่าค่าทั้งหมดในรีจิสเตอร์หรือสแต็กจะถูกขยายเป็นค่าเต็ม 64 บิต และหมายเหตุเกี่ยวกับบูลีนก็ระบุทำนองว่ามีความหมายแค่ 1 ไบต์ล่าง จึงชี้ว่าเรื่องนี้เป็นกฎทั่วไป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
เดิมทีในหน้า 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 แล้วจบปัญหานี้ไปเลย
mkfs.ext4หรือระบบไฟล์ที่ใช้อยู่บนพาร์ทิชัน/และ/usrจากนั้น mount แตก stage3 เข้า chroot แล้วรันemerge $all-my-packages-that-where-installed-before-mkfsแทนที่จะอัปเกรดแบบค่อยเป็นค่อยไป ก็สามารถติดตั้ง สำเนา Gentoo ใหม่ ได้
ถ้ามีซอฟต์แวร์ third-party แบบ closed source ก็ยังเกิดปัญหาได้แม้ในระบบไบนารี แพ็กเกจ first-party ที่ติดตั้งแยกเป็นขั้นตอนอิสระก็อาจมีปัญหาได้เช่นกัน
ผมไม่ใช่ผู้เชี่ยวชาญ C แต่เคยคิดว่า alias ของประเภทอย่าง
off_tถูกนำมาใช้เพื่อให้ภายหลังสามารถเปลี่ยนได้ แต่ดูเหมือนมันไม่ได้ทำงานชัดเจนตามนั้น เลยสงสัยว่าผมเข้าใจผิดหรือเปล่าoff_tโดยทั่วไปทำให้ไม่ต้องเขียนโค้ดใหม่ แต่ทุกอย่างที่ใช้ประเภทนั้นต้องคอมไพล์ใหม่ทั้งหมดoff_tแล้วสามารถบิลด์@worldใหม่แบบ atomic ได้ก็คงไม่มีปัญหา แต่ดิสโทรแบบอิงซอร์สไม่ได้รีบิลด์@worldแบบ atomic แต่รีบิลด์แพ็กเกจทีละตัวแบบนั้นอาจเกิดปัญหาเช่น
libc.soใช้off_t64 บิต แต่gccถูกบิลด์ตามoff_t32 บิต ทำให้gccค้างได้ แพ็กเกจที่จำเป็นต่อการรีบิลด์@worldอย่างbash,coreutils,make,binutilsก็อาจพังได้ และถึงจุดนั้นก็ไปต่อไม่ได้ ดังนั้นการอัปเกรดแบบนี้ต้องระมัดระวังoff_tเข้าไปอยู่ใน struct ถูกใช้ในการเรียกฟังก์ชัน หรือถูกรวมเข้าในโปรโตคอล abstraction ก็หายไป และขนาดจริงก็มีความสำคัญขึ้นมาถ้าผสมโค้ดเก่ากับโค้ดใหม่ขณะโหลดไลบรารีหรือสื่อสารผ่านโปรโตคอล offset จะเริ่มคลาดเคลื่อนและเกิดการชนกัน สุดท้ายการเปลี่ยนผ่านทำให้ทุกคนต้องแยกโปรแกรมออกเป็น “legacy” กับ “พอร์ตแล้ว หรืออย่างน้อยตรวจสอบแล้ว” ซึ่งเจ็บปวดมาก
ต่อให้เปลี่ยนเป็นประเภทที่ใหญ่ขึ้นและมีความหมายใกล้เคียงกันก็ยังพังได้ ตัวอย่างง่าย ๆ คือ padding ใน struct และยังมีกรณีใช้งานจำนวนมากที่แปลง pointer เป็น integer แล้วแปลงกลับ ดังนั้นถ้าการแทนค่าภายในเปลี่ยนก็หลีกเลี่ยงไม่ได้ที่จะพัง นั่นจะเป็นแนวปฏิบัติที่ดีหรือไม่เป็นอีกเรื่อง แต่ก็ไม่ใช่เรื่องที่พบได้ยาก ประเด็นหลักคือ ความเข้ากันได้ของ ABI
off_t32 บิตเข้ากับไลบรารีที่บิลด์ด้วยoff_t64 บิต และพฤติกรรมที่ได้อาจคาดเดาไม่ได้อย่างมากใน struct ตัวอย่างบอกว่าเมื่อ
time_tเป็น 32 บิต offset ของcคือ 8 และเมื่อเป็นประเภท 64 บิตคือ 12 แต่จริง ๆ แล้ว ไม่ควรเป็น 16 หรือ เพราะbต้องถูกจัดแนวแบบ 64 บิต ดังนั้นต้องมี padding ระหว่างaกับbด้วย ซึ่งยิ่งทำให้ประเด็นที่ผู้เขียนต้องการสื่อหนักแน่นขึ้นด้วยซ้ำเมื่อดูทั้งหมดนี้ การแทนเวลาประหลาด ๆ ของ Windows คือการนับแบบ 64 บิตหน่วยละ 100ns ตั้งแต่วันที่ 1 มกราคม 1601 เวลา 00:00 GMT ตามปฏิทินเกรกอเรียน ก็มีข้อดีเล็ก ๆ อยู่เหมือนกัน ความละเอียดก็ดีเยี่ยม และมันน่าจะยังทำงานได้จนกว่าทั้งกาแล็กซีจะถูกพิชิต