Tại Sao Tôi Ghét Trình Biên Dịch: Những Thách Thức Trong Việc Xây Dựng Có Thể Tái Tạo
Bài viết này khám phá những khó khăn bất ngờ khi cố gắng tạo ra các bản dựng phần mềm có thể tái tạo hoàn toàn, nơi cùng một mã nguồn đầu vào có thể tạo ra các tệp thực thi khác nhau. Tác giả chia sẻ kinh nghiệm giải quyết vấn đề này trong dự án Anubis khi làm việc với WebAssembly và công cụ chuyển đổi mã, làm nổi bật sự phức tạp ẩn giấu bên trong các trình biên dịch hiện đại.

Tại Sao Tôi Ghét Trình Biên Dịch: Những Thách Thức Trong Việc Xây Dựng Có Thể Tái Tạo
Bạn có thể nghĩ rằng với cùng một dữ liệu đầu vào, bạn sẽ luôn nhận được kết quả đầu ra giống hệt nhau. Thực tế trong thế giới lập trình hiện đại lại phức tạp hơn nhiều.
Dự án Anubis của tôi đang chuẩn bị tích hợp cơ chế kiểm tra Proof of Work (bằng chứng công việc) dựa trên WebAssembly. Điều này cho phép các quản trị viên sử dụng phương pháp bảo vệ khác ngoài SHA256 cho trang web của mình. Mục tiêu cốt lõi của việc triển khai này là đảm bảo logic kiểm tra được định nghĩa tại một nơi duy nhất và hoạt động đồng bộ trên cả máy khách (client) và máy chủ (server).
Vấn đề về sự tương thích
Một vấn đề nhỏ phát sinh: Điều gì sẽ xảy ra khi người dùng vô hiệu hóa WebAssembly trên trình duyệt? Tôi thực sự không muốn khóa người dùng ra khỏi các trang web một cách mặc định. Anubis tồn tại trong sự cân bằng khó khăn giữa trải nghiệm người dùng, trải nghiệm quản trị viên và trải nghiệm nhà phát triển.
Để giải quyết vấn đề này mà vẫn giữ mục tiêu định nghĩa logic duy nhất, tôi quyết định lấy cảm hứng từ bài nói chuyện nổi tiếng "The Birth and Death of JavaScript" và biên dịch lại WebAssembly sang JavaScript. Mặc dù JavaScript kết quả sẽ chậm hơn WebAssembly tương đương, nhưng nó sẽ hoàn thành được nhiệm vụ.
May mắn thay, công cụ tôi cần là wasm2js từ dự án binaryen đã có sẵn trong các bản phân phối Linux. Tin xấu là các bản phân phối này cung cấp các phiên bản quá cũ, không tạo ra cùng một đầu ra với phiên bản trên máy phát triển của tôi (từ Homebrew).
Bản dựng có thể tái tạo (Reproducible Builds) khó hơn bạn tưởng
Để đảm bảo đầu ra là xác định (deterministic) — yếu tố thiết yếu cho các bản dựng có thể tái tạo — tôi cần đóng gói một bản sao của wasm2js. Tôi đã thực hiện điều này bằng cách xây dựng một phiên bản wasm2js được biên dịch sang WebAssembly bằng wasi-sdk.
Về lý thuyết, nếu bạn có cùng một byte đầu vào cho trình biên dịch, bạn sẽ nhận được cùng một byte đầu ra, giả sử rằng các cờ trình biên dịch, mục tiêu và chi tiết nền tảng khác được kiểm soát. Một trình biên dịch chỉ là một hàm xác định lấy mã nguồn đầu vào và tạo ra bytecode đầu ra, đúng không?
Sai rồi. Trong thực tế, các trình biên dịch là những "quái vật" lạ lùng và phức tạp mà không một người phàm nào có thể hiểu thấu đáo hoàn toàn.
Có một số lượng đáng kinh ngạc các cách để vô tình tạo ra đầu ra không xác định khi phát triển C/C++. Một trong những cách dễ nhất là sử dụng các macro tích hợp sẵn __DATE__ và __TIME__ để đóng dấu thời gian biên dịch vào bản dựng.
Một vấn đề khác phức tạp hơn liên quan đến cách các bảng băm (hash maps) hoạt động trong C++. Trong C++, thứ tự lặp qua các phần tử của một bảng băm không được đảm bảo là nhất quán. Điều này có nghĩa là nếu trình biên dịch sử dụng bảng băm để lưu trữ các biểu tượng hoặc dữ liệu nội bộ, thứ tự xử lý chúng có thể thay đổi giữa các lần chạy, dẫn đến các tệp nhị phân khác nhau dù mã nguồn giống hệt nhau.
Giải pháp và Kiểm tra
Để khắc phục điều này, tôi đã thiết lập quy trình kiểm tra nghiêm ngặt. Sau khi biên dịch công cụ wasm2js, tôi tính toán mã băm SHA256 của tệp thực thi kết quả và so sánh nó với một giá trị đã biết trước. Nếu mã băm không khớp, quy trình xây dựng sẽ thất bại.
"Để đảm bảo thêm, chúng tôi cho công việc này chạy trên cả máy chủ x86_64 và arm64. Tôi thực sự muốn nó có thể tái tạo trên các kiến trúc máy chủ khác nhau, nhưng đó là một lỗi ở phía LLVM mà tôi chưa đủ sức để giải quyết."
Hiện tại, tính nhất quán ít nhất được đảm bảo trong cùng một kiến trúc phần cứng. Đối với một dự án mã nguồn mở, việc duy trì tính toàn vẹn của bản dựng là một cuộc chiến không ngừng, nhưng cần thiết để đảm bảo tin cậy cho người dùng cuối.



