Tập 29: Binary đầu tiên

Mã nguồn là lời nói của lập trình viên trò chuyện với trình biên dịch. Tệp thực thi nhị phân gốc (binary) là khoảnh khắc hệ điều hành chính thức đồng ý tham gia vào cuộc trò chuyện ấy.

Khi lệnh duy build báo biên dịch thành công, bên trong thư mục đầu ra build xuất hiện một file mới không còn đuôi mở rộng .duy quen thuộc nữa. Nó không phải là một file văn bản tài liệu, một bản kế hoạch, hay mã nguồn C trung gian để con người mở ra đọc. Nó là một executable (tệp có khả năng thực thi). Hệ điều hành có thể trực tiếp load nó vào RAM, khởi tạo một tiến trình mới (process), đặt địa chỉ entry point vào thanh ghi lệnh của CPU và chờ đợi CPU thực thi các lệnh máy gốc của chương trình.

Sự chuyển đổi ấy làm cho dự án DUY thay đổi hoàn toàn tính chất. Trước khoảnh khắc ấy, DUY chỉ được xem như một thí nghiệm parser nhỏ gọn trong phòng lab. Sau khoảnh khắc ấy, dù compile time vẫn tạm thời dựa dẫm vào Node.js và Clang, sản phẩm đầu ra của nó đã là một tiến trình native độc lập (native process). Runtime thực thi không cần phải đọc bất kỳ dòng mã nguồn văn bản nào của DUY hay JavaScript để biết mình phải làm gì dưới CPU.

Chương trình hello in ra thành công con số 42, rồi hàm main trả về kết quả 0.

Hai con số ấy mang hai loại ý nghĩa hợp đồng hoàn toàn khác nhau. Con số 42 là hành vi logic mà người dùng nhìn thấy trên màn hình. Con số 0 là hợp đồng nhị phân với hệ điều hành xác nhận rằng tiến trình đã kết thúc thực thi thành công trọn vẹn. Lõi CLI của compiler bắt buộc phải chuyển tiếp trọn vẹn các luồng dữ liệu stdout, stderr và exit status của tiến trình con một cách chuẩn xác nhất. Nếu file nhị phân chạy sập giữa đường (crash) nhưng wrapper CLI vẫn báo kết quả 0, ca kiểm thử tự động sẽ báo xanh rực trong khi hệ thống tích hợp liên tục (CI) hiểu sai lệch hoàn toàn về tình trạng thực tế của sản phẩm.

Hệ thống tests vì thế không chỉ dừng lại ở việc kiểm tra xem tệp nhị phân có tồn tại trên đĩa hay không. Nó trực tiếp spawn tệp thực thi đó lên chạy, giám sát chặt chẽ exit status trả về và so khớp chính xác từng ký tự đầu ra stdout. Đây là một quy trình xác thực tuần tự nghiêm ngặt:

1. Biên dịch build thành công không lỗi.
2. File nhị phân gốc thực sự xuất hiện trên đĩa.
3. Spawn chạy file thực thi thành công.
4. Kết quả đầu ra stdout trùng khớp hoàn toàn.
5. Exit code trả về chính xác bằng 0.

Mỗi dòng kiểm tra trong quy trình trên sinh ra để loại bỏ một kiểu ảo tưởng tiến độ của lập trình viên. Trình biên dịch có thể báo biên dịch thành công nhưng thực tế đĩa cứng bị đầy không ghi được file. File có thể xuất hiện trên đĩa nhưng hệ điều hành từ chối chạy vì thiếu quyền thực thi. Tệp nhị phân có thể chạy được nhưng nhả ra kết quả in sai lệch. Kết quả in ra có thể đúng rồi tiến trình lập tức bị sập bất ngờ ở lệnh dọn dẹp cuối cùng. Một chữ “PASS” đáng tin cậy của hệ thống test bắt buộc phải phát biểu rõ ràng xem nó đã thực hiện quan sát và đo lường những dữ kiện vật lý gì.

Tôi vẫn nhớ cảm giác lần đầu tiên nhìn thấy file nhị phân hello xuất hiện trong Finder của macOS. Nó không mang lại cảm giác của một sản phẩm công nghệ hoàn chỉnh và bóng bẩy. Nó giống như việc bạn nghe thấy tiếng động cơ xe nổ giòn lần đầu tiên sau khi bạn dành cả tuần tự tay lắp ráp các chi tiết máy trong garage. Âm thanh nổ máy ấy chưa hề bảo đảm chiếc xe có thể đi được bao xa, hệ thống phanh có hoạt động an toàn hay động cơ có đang bị rò rỉ dầu ngầm hay không. Nhưng nó là bằng chứng đanh thép chứng minh hàng vạn chi tiết máy đã phối hợp với nhau chuẩn xác trong một khoảnh khắc vật lý thực tế.

Ngay tại khoảnh khắc ấy, sự sốt ruột của tôi lập tức đổi hình dạng. Trước khi có tệp nhị phân đầu tiên, tôi sốt ruột muốn có bất kỳ thứ gì chạy được ra kết quả. Sau khi file nhị phân chạy được, tôi lập tức nảy sinh khao khát muốn chương trình phải được sinh mã trực tiếp qua LLVM IR, muốn compiler phải tự viết bằng DUY, muốn bỏ hoàn toàn Node.js, muốn viết hệ điều hành và nhúng bare-metal. Lộ trình phát triển sản phẩm (roadmap) cực kỳ giỏi trong việc sinh sản thêm các mục tiêu mới ngay khi một mục tiêu cũ vừa được hoàn thành.

Kỷ luật kỹ thuật cần thiết lúc này là tuyệt đối không được phép biến tệp nhị phân đầu tiên chạy được thành một bằng chứng vạn năng cho mọi lời hứa của ngôn ngữ. Nó chưa hề chứng minh được an toàn bộ nhớ tĩnh. Chưa chứng minh được hiệu năng thực tế vượt trội so với C. Chưa chứng minh được tính di động đa nền tảng. Nó đơn thuần là bằng chứng thực tế cho một lát cắt tích hợp dọc (end-to-end slice): mã nguồn DUY viết đúng luật hoàn toàn có thể được compiler prototype biến đổi thành tệp nhị phân native chạy ổn định trên môi trường thử nghiệm hiện tại.

Nhưng hai chữ “đơn thuần” ấy hoàn toàn không hề nhỏ bé. Rất nhiều ý tưởng thiết kế ngôn ngữ lập trình vĩnh viễn không bao giờ đi xa hơn các trang tài liệu ngữ pháp trên giấy. Việc tạo ra được file binary bắt buộc frontend compiler, bộ biểu diễn trung gian, backend sinh mã và hệ thống linker của hệ điều hành phải gặp gỡ và đối thoại sòng phẳng với nhau. Nó tạo ra các artifact thực tế cho việc đo đạc hiệu năng, cho phòng thí nghiệm OS lab và cho quy trình tự dựng compiler về sau.

Từ thời điểm lịch sử ấy, dự án DUY thoát khỏi câu hỏi mơ hồ mang tính triết lý “Liệu chúng ta có thể làm được không?”. Nó bước vào vùng đất của các câu hỏi kỹ thuật cụ thể và sòng phẳng hơn nhiều: phần kiến trúc nào đang chạy đúng, phần việc nào đang phải nhờ cậy toolchain khác gánh hộ, và nếu muốn mở rộng phạm vi an toàn của ngôn ngữ thì cổng gác tiếp theo nào bắt buộc phải xây dựng trên compiler?

File binary đầu tiên đã chạy đúng. Niềm vui ấy kéo dài được một lúc. Rồi compiler nhìn vào biểu thức cộng đơn giản 40 + 2 trong file hello, sinh nguyên vẹn lệnh cộng vật lý xuống backend để CPU chạy ở runtime, và một ý nghĩ vô cùng tự nhiên xuất hiện trong đầu tôi: "Nếu kết quả của phép toán đã được biết chắc chắn ngay từ thời điểm biên dịch, tại sao compiler lại bắt CPU phải mất công tính toán lại nó lúc chương trình đang chạy?"


Bình luận (0)

Đang tải bình luận...