1 điểm bởi GN⁺ 2024-09-30 | 1 bình luận | Chia sẻ qua WhatsApp
  • Trên các hệ thống glibc 32 bit, việc truy vấn thời gian hiện tại hoặc gọi stat() sau năm 2038 có thể thất bại, vì vậy Gentoo cần một lộ trình để chuyển sang time_t 64 bit một cách an toàn
  • time64 của glibc phải được dùng cùng Large File Support (LFS), và trong môi trường 32 bit tồn tại đồng thời ABI cũ, ABI LFS và ABI LFS+time64
  • Khi time_t xuất hiện trong API, cấu trúc hoặc tham số hàm, thay đổi độ rộng kiểu sẽ dẫn tới phá vỡ ABI, khiến việc trộn lẫn nhị phân time32 và time64 gây lỗi runtime và rủi ro bảo mật
  • Là một bản phân phối dựa trên mã nguồn, Gentoo có thể để lại hệ thống chuyển đổi dở dang do lỗi hoặc phụ thuộc vòng lặp trong quá trình rebuild @world
  • Sau đính chính ngày 2024-09-30, chỉ chuyển đổi libdir là không còn đủ nữa, và dấu hiệu nhận biết time64 bao gồm cả dynamic loader và nhiều toolchain ngôn ngữ trở thành ràng buộc cốt lõi

Vấn đề năm 2038 và phạm vi chuyển đổi time64

  • Các ứng dụng 32 bit dùng time_t 32 bit có thể nhận lỗi -1 thay vì thời gian hiện tại vào năm 2038, hoặc không thể thực hiện stat() trên tệp
  • Hướng chuyển đổi cơ bản là đổi time_t sang kiểu 64 bit
    • musl đã chuyển đổi
    • glibc hỗ trợ như một tùy chọn
    • một số bản phân phối như Debian đã chuyển đổi
  • Với các bản phân phối dựa trên mã nguồn như Gentoo, người dùng tự rebuild hệ thống nên cần giảm thời gian các gói ở trong những trạng thái ABI khác nhau
  • Rủi ro cốt lõi là việc thay đổi độ rộng của time_t sẽ phá vỡ ABI
    • Nếu time_t có trong API của thư viện, toàn bộ mã liên kết với thư viện đó phải dùng cùng độ rộng kiểu
    • Cách chỉ chuyển một phần sang time64 là không an toàn

LFS và ba ABI con trên 32 bit

  • Trên kiến trúc 32 bit, từ trước đã tồn tại vấn đề về độ rộng kiểu liên quan đến tệp
    • off_t được dùng cho offset tệp
    • ino_t được dùng cho số inode
    • Ban đầu chúng có độ rộng 32 bit nên gây vấn đề với tệp lớn hơn 2GiB hoặc số inode vượt quá phạm vi 32 bit
  • Để giải quyết, Large File Support (LFS) đã được đưa vào
    • Chuyển off_tino_t sang biến thể 64 bit
    • Trong glibc, đến nay đây vẫn là tùy chọn
    • Nhiều gói đã bật LFS từ upstream và xử lý phá vỡ ABI, nhưng vấn đề chưa được giải quyết hoàn toàn
  • Hỗ trợ time64 của glibc yêu cầu dùng LFS, nên đây là cấu trúc giải quyết đồng thời cả vấn đề kích thước tệp lẫn thời gian
  • Trên hệ thống 32 bit tồn tại ba ABI con
    • ABI cũ: kiểu 32 bit
    • LFS: off_t 64 bit, ino_t 64 bit, time_t 32 bit
    • time64: LFS + time_t 64 bit
  • Một bản build glibc duy nhất tương thích với cả ba biến thể, nhưng các thư viện dùng những kiểu này trong API thì không thể trộn lẫn ba biến thể với nhau

Cách thay đổi ABI thực sự gây hỏng

  • Khi time_t đổi từ 32 bit sang 64 bit, layout cấu trúc sẽ thay đổi
    • Trong cấu trúc ví dụ có int a, time_t b, int c theo thứ tự, offset của c sẽ khác nhau giữa time_t 32 bit và time_t 64 bit
    • Nếu trộn nhị phân time32 và time64, chương trình có thể đọc hoặc ghi sai field, thậm chí truy cập vượt phạm vi
  • Kích thước struct stat cũng thay đổi theo ABI
    • Mặc định glibc x86 32 bit: 88 byte
    • LFS: 96 byte
    • LFS + time64: 108 byte
  • Ngay cả khi không dùng cấu trúc, vấn đề vẫn xuất hiện ở tham số hàm
    • Trên x86, tham số hàm được truyền qua stack
    • Nếu một trong các tham số là time_t, vị trí trên stack của các tham số theo sau sẽ thay đổi
  • Trong thí nghiệm minh họa, chương trình time32 bị hỏng giá trị khi liên kết với thư viện được rebuild thành time64
    • Kết quả ban đầu là a = 1, giá trị thời gian bình thường, c = 3
    • Nếu chỉ rebuild thư viện với -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64, bc sẽ bị diễn giải sai
  • Hiện chưa có cơ chế bảo vệ thực tế nào để ngăn việc trộn ABI như vậy, nên có thể phát sinh lỗi runtime và vấn đề bảo mật

Vì sao việc chuyển đổi của Gentoo khó hơn

  • Với bản phân phối nhị phân, mọi gói được rebuild trước rồi người dùng nâng cấp theo bước tương đối nguyên tử
    • Có thể vẫn có vấn đề nếu dùng kho bên thứ ba hoặc chương trình build cục bộ, nhưng toàn bộ quá trình nhìn chung an toàn hơn
  • Gentoo phải thay đổi ABI ngay tại chỗ trong lúc rebuild @world
    • Giữa lúc hai gói được rebuild riêng rẽ, các ABI không tương thích có thể bị trộn lẫn
    • Nếu một số lần rebuild thất bại, hệ thống có thể bị kẹt ở trạng thái chuyển đổi dở dang
    • Do phụ thuộc vòng lặp, việc rebuild gói phụ thuộc có thể làm hỏng công cụ build, khiến các lần rebuild tiếp theo không thể thực hiện

Các biện pháp giảm thiểu đang được xem xét

  • Có ba hướng đang được thảo luận
    • Đổi platform tuple CHOST để phân biệt ABI mới với ABI 32 bit hiện tại
    • Đổi libdir của ABI mới để cài thư viện đã rebuild tách biệt với thư viện cũ
    • Đưa vào cơ chế phân biệt ABI ở cấp độ nhị phân để ngăn nhị phân thuộc các ABI con khác nhau liên kết với nhau
  • Ba cách này có thể được triển khai tương đối độc lập, nhưng một số có thể phụ thuộc lẫn nhau
  • Các chuỗi ví dụ trong bài viết có thể không phải chuỗi thực tế của giải pháp cuối cùng

Phân biệt ABI bằng CHOST

  • Platform tuple dùng để nhận diện nền tảng mà toolchain nhắm tới, và trong Gentoo còn được dùng để phân biệt ABI một cách duy nhất nhằm hỗ trợ multilib
  • Tuple gồm bốn phần: kiến trúc, vendor, hệ điều hành và libc
    • Ví dụ: i386-pc-linux-gnu
    • Ví dụ: i686-pc-linux-gnu
    • Ví dụ: i686-unknown-linux-gnu
  • Khi đưa vào ABI mới, từng có cách đổi trường vendor hoặc thêm ký hiệu ABI vào trường libc
    • Với ARM hardfloat ABI, trước đây từng dùng các dạng như armv7a-hardfloat-linux-gnueabiarmv7a-unknown-linux-gnueabihf
  • Với ABI time64 cũng có những lựa chọn tương tự
    • i686-gentoo_t64-linux-gnu
    • i686-pc-linux-gnut64
    • armv7a-gentoo_t64-linux-gnueabihf
    • armv7a-unknown-linux-gnueabihft64
  • Việc thay đổi tuple có vẻ sẽ không cần quá nhiều bản vá
    • GNU toolchain và GNU build system bỏ qua phần theo sau gnu trong trường libc
    • Clang sẽ cần bản vá để tự động chọn đúng ABI theo tuple

Đổi libdir và preserved-libs

  • libdir là tên mặc định của thư mục cài thư viện
    • Giá trị mặc định phổ biến là lib
    • Trên các kiến trúc hỗ trợ 64 bit, theo thông lệ thường dùng lib64
    • ABI x32 của x86 dùng libx32, ABI n32 của MIPS dùng lib32
  • Đối với ABI 32 bit time64, một phương án đang được xem xét là đổi libdir từ lib sang giá trị như libt64
  • Libdir riêng là cơ chế giúp giảm trộn ABI trong quá trình chuyển đổi
    • Giảm nguy cơ chương trình time64 vô tình liên kết với thư viện time32
    • Có thể giữ lại thư viện time32 nhờ tính năng preserved-libs của Portage
    • Có thể tùy chọn cung cấp profile multilib time32 + time64 để duy trì tương thích với các ứng dụng dựng sẵn time32 hiện có
  • Khi có preserved-libs, các chương trình cũ tiếp tục dùng thư viện time32 cho đến khi được rebuild, còn thư viện đã rebuild sang time64 sẽ được cài vào libdir mới
  • Việc đổi libdir cần các bản vá cho toolchain
    • glibc có thể được xử lý đặc biệt vì cùng một bộ thư viện có hiệu lực với nhiều ABI con
    • Có thể cần ld.so riêng để .interp của chương trình time64 trỏ tới ld.so dành cho time64
  • Để hỗ trợ multilib đúng nghĩa, vẫn cần platform tuple riêng cho ABI đó

Đánh dấu không tương thích ở cấp độ nhị phân

  • Khi trộn nhị phân thuộc các ABI khác nhau, thông thường linker hoặc dynamic loader phải chặn lại
    • Nếu liên kết chương trình 64 bit với thư viện 32 bit, linker sẽ từ chối với lỗi file in wrong format
    • Dynamic loader cũng sẽ từ chối với lỗi như wrong ELF class: ELFCLASS32
  • Việc phân biệt ABI hiện có sử dụng nhiều cơ chế khác nhau
    • ELFCLASS32ELFCLASS64
    • machine identifier như EM_386EM_X86_64
    • trường flags của ARM và MIPS
    • architecture-specific attribute section
  • time32 và time64 cũng cần cơ chế tương tự, nhưng không hề đơn giản
    • Có vẻ không có cơ chế chung có thể tái sử dụng
    • Cần một giải pháp phù hợp cho nhiều kiến trúc
    • Một ứng viên thực tế là thêm ELF note section mới và triển khai hỗ trợ trong toolchain
  • Cũng cần tính đến khả năng người dùng tắt cơ chế bảo vệ
    • Nếu phần mềm dựng sẵn không có mã nguồn không gọi các API dùng time_t, nó vẫn có thể tiếp tục hoạt động với thư viện hệ thống
    • Cách chặn tuyệt đối có thể còn tệ hơn chính vấn đề cần giải quyết
  • Nếu dùng libdir riêng, có thể tạo kiểm tra QA không gây lỗi nghiêm trọng tương đối đơn giản
    • Dùng .interp để phân biệt chương trình time64
    • Kiểm tra chương trình time32 không tải thư viện từ libt64
    • Kiểm tra chương trình time64 không tải trực tiếp thư viện từ lib

Giới hạn của các ứng dụng 32 bit dựng sẵn

  • Tách biệt với các gói được build từ mã nguồn, trên x86 và PowerPC có những ứng dụng chỉ được cung cấp dưới dạng nhị phân dựng sẵn cũ
    • Đặc biệt là phần mềm độc quyền và các game cũ
  • Chúng vừa gặp vấn đề tương thích với thư viện hệ thống, vừa chịu vấn đề năm 2038
  • Với vấn đề tương thích, cấu trúc multilib hiện tại phần nào đã có lời giải
    • Trên amd64, đã sẵn có cơ chế build nhiều phiên bản thư viện và layout multilib để hỗ trợ phần mềm 32 bit
    • Có thể mở rộng bằng cách phân biệt abi_x86_32abi_x86_t64
    • Có thể tạo profile x86 multilib mới hỗ trợ cả hai ABI
  • Việc chương trình 32 bit tự thân thất bại sau năm 2038 vẫn là bài toán khó hơn
    • Có thể điều khiển thời gian hệ thống bằng faketime
    • Hoặc chạy VM với thời gian được quay về quá khứ

Đính chính 2024-09-30: chỉ libdir là không đủ

  • Ý tưởng ban đầu quá lạc quan, và chỉ đổi libdir thì không đủ để tách biệt ổn định
  • Mọi libdir đều được liệt kê trong ld.so.conf, nên không thể dựa vào cách hardcode đường dẫn libdir trong ld.so
    • Ngay cả custom LLVM prefix cũng đã điều chỉnh đường dẫn, và trường hợp này cũng cần xử lý đặc biệt
  • Vì vậy, việc đổi libdir nhiều khả năng sẽ phải phụ thuộc vào phân biệt không tương thích ở cấp độ nhị phân
  • Có ba mục tiêu cơ bản cần đạt được
    • Dynamic loader phải phân biệt được nhị phân time32 và time64
    • Mọi nhị phân không có dấu hiệu time64 rõ ràng phải được coi là time32 để giữ tương thích ngược
    • Mọi nhị phân được build mới phải mang dấu hiệu time64 rõ ràng, bao gồm cả nhị phân được build trong môi trường không phải C như Rust
  • Đây là khối lượng công việc ở mức phải vá nhiều toolchain của nhiều ngôn ngữ
    • Gentoo khó có thể chỉ tự duy trì cục bộ mà cần sự hợp tác của nhiều bên
    • Các kiến trúc mục tiêu phần lớn bị xem là legacy hoặc không còn được hỗ trợ đầy đủ
  • Việc các toolchain khác có tạo ra đúng chương trình time64 hay không cũng là một vấn đề riêng
    • Nếu chúng không được điều chỉnh để tuân theo _TIME_BITS như chương trình C, chúng có thể hardcode một độ rộng time_t cụ thể và gây hỏng
  • Vì mọi nhị phân không có dấu hiệu time64 rõ ràng sẽ dùng thư viện time32, Gentoo sẽ không thể chạy các chương trình bên thứ ba nếu chúng không được vá để gắn dấu đúng
  • Cũng đang xem xét phương án đặt mục tiêu thấp hơn
    • Tiêm RPATH vào mọi chương trình time64 để ép trực tiếp libdir của time64
    • Cách này không chặn hoàn toàn dynamic loader dùng thư viện time32, nhưng có thể hỗ trợ quá trình chuyển đổi mà không gây vấn đề tương thích lớn
  • Ngược lại, cũng có phương án không đổi vĩnh viễn libdir time64 mà đổi tạm libdir time32
    • Tiêm RPATH vào các chương trình hiện có và đổi tên libdir
    • Cài thư viện time64 mới vào libdir hiện có
    • Các chương trình time64 mới sẽ không có RPATH ép dùng thư viện time32
    • Ưu điểm là giữ được tương thích với các bản phân phối khác đã chuyển đổi

Những việc còn lại

  • Nếu triển khai cả ba giải pháp, có thể cung cấp lộ trình chuyển đổi sạch hơn và tương đối an toàn hơn cho các hệ thống Gentoo 32 bit dùng glibc
  • Tuy nhiên, các giải pháp này chủ yếu áp dụng cho các gói được build từ mã nguồn
  • Các ứng dụng 32 bit dựng sẵn vẫn còn vấn đề năm 2038 ngay cả khi giữ được tương thích ABI
  • Toàn bộ thiết kế hiện vẫn là bản nháp và có thể tiếp tục thay đổi theo các thử nghiệm, thảo luận và việc gửi bản vá

1 bình luận

 
GN⁺ 2024-09-30
Các ý kiến trên Hacker News
  • Gentoo có vài lựa chọn không được đề cập trong bài, có lẽ bị bỏ qua vì khối lượng công việc lớn do thiết kế hệ thống của Gentoo

    1. Cho phép build nhắm tới một gói mà không cần cài đặt gói đó. Điểm cốt lõi là việc build và cài đặt gói trong Gentoo là một bước duy nhất, nên không thể build trước nhiều thành phần phụ thuộc lẫn nhau rồi cài đặt nguyên tử các kết quả. Trong các bản cập nhật có thay đổi ABI, hệ thống rất dễ bị hỏng một phần
    2. Mở rộng cơ chế quản lý phiên bản .so thông thường để phản ánh cả thay đổi ABI của các gói phụ thuộc. Thường thì thư viện chia sẻ có số phiên bản trong tên tệp và phiên bản nội bộ, như libfoo.so.1.0.0, và gói theo dõi việc phá vỡ ABI của chính nó. Để hỗ trợ time_t 64-bit, cần thêm một thành phần phiên bản do ABI phụ thuộc của từng .so kiểm soát. Kết quả tương tự “dùng libdir khác” trong bài, nhưng tuy có thể trở thành nền tảng tái sử dụng được cho các thay đổi ABI trong tương lai, nó có khả năng xâm lấn hơn nhiều
    • Để “build mà không cài đặt”, cập nhật theo giai đoạn một phần có vẻ phù hợp nhất
      Có thể lên lịch build nhiều gói mới và build chúng trong sandbox, rồi làm cho các lần biên dịch mới nhìn vào sandbox trước thông qua union và fallback về hệ thống. Khi tất cả đã build xong, chỉ cần đóng gói kết quả và chuyển từ sandbox sang hệ thống thật. Như vậy có thể biến toàn bộ cập nhật Gentoo thành dạng giao dịch, đem lại lợi ích lớn ở các mặt khác
    • Gentoo đã hỗ trợ mục 1 bằng cách chỉ định thư mục sẽ cài đặt image hoàn chỉnh cuối cùng. Chỉ cần đổi ROOT, vốn thường được đặt là /
      Có thể build lại toàn bộ @system@world, cài vào thư mục con đã chỉ định rồi đồng bộ một lần. Nếu có thể thì nên làm trong live session; về lý thuyết cũng có thể bind mount / vào một thư mục con của vị trí mới cài, rồi chroot vào đó và đồng bộ về / cấp trên thật
      https://devmanual.gentoo.org/ebuild-writing/variables/#root
    • Gentoo vốn đã giữ lại các thư viện cũ cho đến khi mọi phần phụ thuộc được cập nhật, vì vậy có thể giải quyết vấn đề này bằng cách mã hóa thay đổi ABI vào kiến trúc và SONAME, tức đúng nơi lẽ ra phải ghi nhận thay đổi ABI
  • Cách Mac OS X xử lý off_tino_t có thể là một gợi ý. Các lời gọi và cấu trúc cũ vẫn giữ nguyên hành vi, các lời gọi và kiểu mới có hậu tố 64 được thêm vào, và macro tiền xử lý có thể chọn đối tượng tham chiếu thực tế, dù hiếm khi phải dùng trực tiếp
    Thay vào đó, OS và SDK được quản lý phiên bản, và khi build có thể chỉ định phiên bản OS cũ nhất mà binary phải chạy được. Header dựa vào đó tự động chọn macro phù hợp, và các chú thích API mới/API bị loại bỏ cũng dùng cùng cơ chế để tạo weak link hoặc cảnh báo. Ban đầu việc này được xử lý bằng tiền xử lý, nhưng nay compiler hiểu tinh vi hơn cái Apple gọi là tính khả dụng của API, nên có vẻ các nền tảng khác cũng có thể làm theo cách tương tự

    • Điều này không giải quyết được vấn đề cốt lõi mà TFA mô tả. Các ứng dụng dùng “phiên bản OS mục tiêu khi biên dịch” khác nhau sẽ không còn link được với nhau
      Ngay cả khi đang chạy trên OS v.B, ứng dụng X được khai báo nhắm tới OS v.B có thể không link được với ứng dụng Y được khai báo nhắm tới OS v.A. Thực ra cách này khá gần với những gì hầu hết mọi nền tảng đã làm; nếu làm khác đi thì tương thích binary hiện có sẽ vỡ ngay lập tức
    • Các thư viện phụ không phải glibc sẽ không định nghĩa nhiều phiên bản hàm theo từng kích thước off_t, và cũng sẽ không có công tắc nào trong header để minh bạch chọn đúng tập hàm tùy theo kích thước kiểu mà chương trình client muốn
      Dù vậy bài viết vẫn nhấn mạnh rằng time_t là vấn đề lớn hơn off_t. Lý do hợp lý là time_t phổ biến hơn rất nhiều. off_t là một kiểu POSIX liên quan đến tương đối ít interface, trong khi time_t thuộc ISO C và được dùng khắp nơi. Hơn nữa, nhiều mã C giả định time_t là kiểu số nguyên có cùng độ rộng với int, còn giả định như vậy với off_t thì ít gặp hơn
    • Nghe như một giải pháp tao nhã, nhưng thực tế đọc lên giống một hack kinh khủng. Macro không có kiểu là cơn ác mộng mà tôi không bao giờ muốn đụng lại
    • Chỉ hoạt động khi có thể buộc toàn bộ nền tảng đi theo. Đây là giải pháp tốt, nhưng phải kiểm soát thư viện C. Gentoo không kiểm soát libc làm gì, và người dùng có thể dùng GNU libc, musl hoặc thứ khác
  • Debian cũng đã rất đau đớn. Có thể một số người đã kiệt sức, và nhiều người chỉ vào các bản phân phối dựa trên mã nguồn rồi nói “bên đó chắc dễ lắm”

    • Tôi muốn xem tài liệu chi tiết về việc chuyển đổi time64 của Debian đã đau đớn ra sao. Nhìn từ bên ngoài thì nó tương đối suôn sẻ và ít tranh cãi, chẳng hạn trông còn tốt hơn nhiều so với việc hợp nhất /usr
    • Tôi đã làm chuyển đổi m68k, powerpc, sh4 và cũng hỗ trợ một phần hppa; nhờ sự giúp đỡ của các developer Debian khác nên vẫn còn sống
    • Nếu “dễ” nghĩa là “chỉ cần bảo người dùng build lại tất cả trong một lần” thì có lẽ đúng
  • Mỗi khi thấy mọi người vật lộn với vấn đề như thế này, tôi lại cảm thấy thật may là thời kỳ port amd64 ban đầu của FreeBSD đã thúc đẩy việc này. Khi đó có thể quyết định các kiểu cơ bản của ABI, và đã chọn nhìn về tương lai thay vì quá khứ
    amd64 có một đặc điểm thú vị khiến việc này dễ hơn. Nhờ việc đối số hàm 32-bit được tự động cast thành 64-bit trong lúc gọi hàm, nên nếu truyền một số nguyên thời gian 32-bit vào hàm đang mong đợi time_t 64-bit thì trong giai đoạn đầu làm nền tảng, phần lớn vẫn cứ chạy. Vì vậy các phần hoàn thiện lặt vặt có thể để lại sau
    Khi đó cũng có các nền tảng 64-bit khác, nhưng chưa có time_t 64-bit, và FreeBSD/amd64 là nền tảng đầu tiên trong dòng đó vào khoảng 2003–2005. Theo tôi nhớ thì sparc64 cũng đã chuyển sang time_t 64-bit
    Vấn đề lớn nhất là khi ấy tzcode không an toàn với 64-bit. Thuật toán chuẩn hóa struct tm rơi vào điều kiện suy biến kiểu như cố lặp tính ngày/tháng/năm của time_t(2^62). Tôi nhớ là thay vì sửa lớn tzcode, họ xử lý cho thất bại với các mốc đại khái trước năm 1900 hoặc sau năm 10000. Giờ thì rất có thể upstream đã sửa từ lâu
    Trong vài năm, các đoạn mã bên thứ ba xử lý int/long/time_t trong cấu trúc dữ liệu trên file hoặc mạng một cách tùy tiện gây nhầm lẫn thời gian 32/64-bit, và phải xử lý kiểu đập chuột chũi, nhưng nhìn chung không phải vấn đề lớn. Việc dùng time_t 64-bit ngay từ ngày đầu đã giúp tránh phần lớn vấn đề, và làm từ đầu thì dễ. Linux đã bỏ lỡ một cơ hội lớn để làm điều tương tự khi bắt đầu port amd64/x86_64
    Nói thêm, khi đó ino_t 64-bit thì chưa hoàn tất được. Số inode 32-bit đã lộ ra ở quá nhiều nơi như cấu trúc on-disk của hệ thống tập tin, cấu trúc thư mục UFS, v.v. Vào thời điểm FreeBSD/amd64 còn là nền tảng hạng thấp, không có cách thực tế nào để xử lý ngay từ đầu mà không làm rung chuyển mạnh các kiến trúc tier-1 khác. Công việc đã được làm hai lần nhưng cuối cùng người khác mới hoàn tất, đồng thời cũng sửa các hằng số từng quá ngắn như độ dài đường dẫn mountpoint

    • Theo tôi hiểu thì mọi port Linux 64-bit đều đã dùng time_t, off_t, ino_t 64-bit ngay từ đầu. Vấn đề hiện nay là chuyển Linux 32-bit sang time_t 64-bit
    • FreeBSD cũng xử lý off_t táo bạo hơn, nên nó đã là 64-bit từ bản 2.0. Các phiên bản Linux 32-bit vẫn còn dấu vết của kích thước cũ
      Tôi hiểu rằng phần đối số hàm 32-bit được tự động cast thành 64-bit khi gọi chỉ hoạt động với đối số không dấu. Vì khi nạp vào %edi, phần trên của %rdi sẽ bị xóa. Đặc tả SysV ABI cho x86-64 không nói rằng mọi giá trị trong thanh ghi hoặc trên stack đều được mở rộng thành giá trị đủ 64-bit, và chú thích về boolean cũng nói kiểu như chỉ 1 byte thấp là có ý nghĩa, cho thấy đây là quy tắc chung
    • Nếu ý là khi amd64 xuất hiện, FreeBSD cũng port time_t của i386 sang 64-bit thì khá bất ngờ. Tôi tò mò liệu các kiến trúc 32-bit khác như Motorola 68000 hay sparc32 cũng đã chuyển sang time_t 64-bit chưa
  • Trên một hệ thống Unix 32-bit cũ cỡ lớn, để xử lý ngày trong tương lai, tôi từng đổi các hàm libc time_t 32-bit có dấu sang các hàm tương ứng dùng time_t 32-bit không dấu. Việc này giúp có thêm 68 năm sau 2038, và đến lúc đó thì chắc tôi đã không còn nữa
    Nhược điểm là không biểu diễn được các ngày trước Unix epoch năm 1970, nhưng vì là hệ thống quản lý lịch nên không thành vấn đề. Nếu ngày trong quá khứ quan trọng, cũng có thể dời epoch đi vài chục năm hoặc giảm độ phân giải thời gian từ 1 giây xuống 2 giây. Mỗi cách đều có vấn đề tinh vi riêng, nên tùy ca sử dụng

    • Nếu có thể đổi toàn hệ thống từ có dấu sang không dấu, tôi tự hỏi vì sao không đổi sang 64-bit
  • Trong trang man BSD gốc, phần “Bugs” của tunefs có câu đùa nổi tiếng “You can tune a file system, but you can't tune a fish.”, và theo “Expert C Programming”, trong mã nguồn trang man đó có chú thích bên cạnh câu đùa như sau
    “Nếu bỏ cái này ra, daemon UNIX sẽ đuổi theo bạn bốn bước chân từ giờ cho đến khi time_t wrap around.”
    Khi câu này được viết vào thập niên 70, chắc chắn năm 2038 là một tương lai xa không tưởng
    https://progforperf.github.io/Expert_C_Programming.pdf

  • Điều tôi cảm nhận rõ nhất là, dù tôn trọng nỗ lực đó, từ góc nhìn người dùng tôi muốn chuyển sang một bản phân phối không dựa trên mã nguồn như Debian để kết thúc vấn đề này

    • Khó khăn của các bản phân phối dựa trên mã nguồn dường như đến từ việc cố nâng cấp tại chỗ trong khi thực hiện thay đổi không tương thích ABI. Vì vậy việc chuyển sang một bản phân phối hoàn toàn khác có thể gây gián đoạn ở mức ít nhất tương đương, thậm chí tốn ít thời gian hơn, so với cài sạch Gentoo dùng ABI mới
    • Ngay cả với Gentoo cũng có cách xử lý dễ. Boot bằng USB hoặc thứ tương tự, chạy mkfs.ext4 hoặc hệ thống tập tin bạn dùng trên các phân vùng //usr, rồi mount, giải nén stage3, chroot vào và chạy emerge $all-my-packages-that-where-installed-before-mkfs
      Thay vì nâng cấp dần dần, bạn có thể cài một bản sao Gentoo mới
    • Cách phân biệt rằng chỉ cần chuyển sang bản phân phối không dựa trên mã nguồn thì tinh tế hơn một chút. Một bản phân phối dựa trên mã nguồn như NixOS không gặp cùng vấn đề. Điểm cốt lõi không nằm ở việc có build từ mã nguồn hay không, mà nằm ở cách Gentoo build và cài đặt gói
      Nếu có phần mềm mã nguồn đóng của bên thứ ba thì hệ thống nhị phân vẫn có thể gặp vấn đề. Ngay cả các gói first-party được cài riêng ở một bước độc lập cũng có thể phát sinh vấn đề
  • Tôi không phải chuyên gia C, nhưng tôi từng nghĩ các bí danh kiểu như off_t được đưa vào để sau này có thể thay đổi. Nhưng có vẻ nó không hoạt động rõ ràng như vậy, nên tôi tò mò không biết mình có hiểu sai không

    • Đây là khác biệt giữa tương thích mã nguồn và tương thích nhị phân. Nếu dùng typedef như off_t thì thường không cần viết lại mã, nhưng phải biên dịch lại mọi thứ dùng kiểu đó
    • Ở một mức nào đó thì có hoạt động, nhưng không phù hợp lắm với các bản phân phối dựa trên mã nguồn. Nếu có thể build lại @world một cách nguyên tử sau khi đổi định nghĩa off_t thì sẽ không có vấn đề, nhưng các bản phân phối dựa trên mã nguồn không rebuild @world nguyên tử mà build lại từng gói một
      Khi đó libc.so dùng off_t 64-bit trong khi gcc được build theo off_t 32-bit, có thể dẫn đến chuyện gcc bị treo. Các gói cần thiết để rebuild @world như bash, coreutils, make, binutils cũng có thể hỏng, và đến lúc đó thì bị kẹt. Vì vậy những nâng cấp kiểu này cần rất cẩn trọng
    • Đó chỉ là bước đầu của bài toán, thậm chí có thể chỉ là nửa bước. Như bài viết nói, ngay khoảnh khắc off_t nằm trong struct, được dùng trong lời gọi hàm, hoặc được tích hợp vào protocol, lớp trừu tượng biến mất và kích thước thực tế trở nên quan trọng
      Khi load thư viện hoặc giao tiếp qua protocol mà trộn mã cũ với mã mới, các offset sẽ lệch và bắt đầu xảy ra crash. Rốt cuộc, để chuyển đổi thì mọi người phải tách chương trình thành “legacy” và “đã port hoặc ít nhất đã được rà soát”, nên việc này rất đau đớn
    • Bí danh kiểu chỉ giúp mọi thứ dễ hơn ở cấp mã nguồn. Nó không phải là trừu tượng thật sự, đặc biệt là không phải trừu tượng hoàn toàn. Ví dụ, nếu đổi kiểu bên trong thành kiểu dấu phẩy động thì ngữ nghĩa thay đổi rất lớn và lộ hoàn toàn ra mã của người dùng
      Ngay cả khi đổi sang một kiểu lớn hơn nhưng có ngữ nghĩa tương tự, vẫn có thể bị vỡ. Ví dụ đơn giản là padding trong struct, và cũng có nhiều trường hợp đổi con trỏ sang số nguyên rồi đổi ngược lại, nên khi biểu diễn bên trong thay đổi thì kiểu gì cũng có thể hỏng. Việc đó có phải thực hành tốt hay không là chuyện khác, nhưng không phải hiếm. Điểm cốt lõi là tương thích ABI
    • Nó có hoạt động, nhưng vấn đề với thay đổi ABI là khi đổi thì phải đổi mọi nơi cùng lúc. Về cơ bản không có cơ chế nào ngăn liên kết một thư viện được build với off_t 32-bit với một thư viện được build với off_t 64-bit, và hành vi kết quả có thể cực kỳ khó đoán
  • Trong struct ví dụ, bài viết nói offset của c là 8 khi time_t 32-bit và là 12 khi kiểu 64-bit, nhưng thật ra tôi nghĩ không phải phải là 16 sao. Vì b phải được căn chỉnh 64-bit nên cần có padding giữa ab. Điều này thậm chí còn làm luận điểm tác giả muốn nêu mạnh hơn

    • Phần lớn ABI x86 không bắt buộc padding cho kiểu 64-bit, vì thời đó chưa có lệnh load 64-bit
  • Nhìn vào tất cả những điều này thì cách biểu diễn thời gian kỳ lạ của Windows, tức là đếm bằng số 64-bit theo đơn vị 100ns kể từ 00:00 GMT ngày 1 tháng 1 năm 1601 theo lịch Gregory, cũng có một ưu điểm nhỏ. Độ phân giải cũng rất tốt, và nó sẽ vẫn hoạt động cho đến khi cả thiên hà được chinh phục