Toán học là khó khăn: Câu chuyện về OpenBSD và những cạm bẫy của kiến trúc VAX

23 tháng 4, 2026·5 phút đọc

Bài viết chia sẻ một câu chuyện thú vị trong quá trình phát triển nhân OpenBSD, nơi sự thay đổi tinh vi trong thiết kế phần cứng của kiến trúc VAX về xử lý ngoại lệ số học đã tạo ra một thách thức kỹ thuật phức tạp. Chúng ta sẽ cùng tìm hiểu sự khác biệt giữa "trap" và "fault", và cách các nhà phát triển phải giải mã mã máy ngay trong nhân hệ điều hành để giải quyết vấn đề hiếm gặp này.

Toán học là khó khăn: Câu chuyện về OpenBSD và những cạm bẫy của kiến trúc VAX

Khi bạn bước sang "bên kia hàng rào" và làm việc trực tiếp trên nhân (kernel) hệ điều hành, những chi tiết kỹ thuật phức tạp và khác biệt giữa các nền tảng phần cứng không thể nào bỏ qua được. Đôi khi, những thiếu sót trong thiết kế của một kiến trúc bộ vi xử lý cụ thể có thể trở thành cơn đau đầu thực sự cho các lập trình viên.

Cạm bẫy trong thiết kế phần cứngCạm bẫy trong thiết kế phần cứng

Hãy cùng xem xét một lựa chọn thiết kế bộ vi xử lý đã gây ra những hậu quả đáng kể trong nhân OpenBSD, mặc dù tình huống này xảy ra khá hiếm.

Mô hình ngoại lệ của VAX: Trap hay Fault?

Trong tài liệu tham khảo kiến trúc VAX, chương về "Ngoại lệ và Ngắt" không quá dày, nhưng mô hình xử lý của nó rất quan trọng. Về cơ bản, có hai khái niệm chính:

  • Trap (Bẫy): Khi bộ vi xử lý gặp tình huống không thể phục hồi (ví dụ: chia cho 0), đó là một trap. Quy trình sẽ bị chấm dứt và gửi tín hiệu SIGFPE (Ngoại lệ dấu chấm động) để kết thúc chương trình. Không có cách nào sửa chữa lỗi tính toán này.
  • Fault (Lỗi): Nếu có khả năng khắc phục sự cố và cho lệnh gây lỗi chạy lại, đó là fault. Ví dụ điển hình là việc truy cập vào một trang bộ nhớ chưa được ánh xạ (page fault). Hệ điều hành có thể tải trang nhớ từ ổ đĩa và cho phép lệnh chạy lại thành công.

Ban đầu, mọi thứ hoạt động tốt. Trình xử lý ngoại lệ của VAX sẽ cho phép hệ thống quản lý bộ nhớ khôi phục các page fault, và gửi tín hiệu SIGFPE cho các lỗi tính toán. Mã này đã gần như không thay đổi kể từ thời 3BSD vào năm 1980.

Tài liệu kiến trúc VAXTài liệu kiến trúc VAX

Vấn đề phát sinh

Vấn đề nảy sinh khi một nhà phát triển phát hiện ra rằng trên nền tảng i386, nếu một chương trình bắt tín hiệu SIGFPE và cố gắng tiếp tục thực thi (thay vì chết đi), nó sẽ hoạt động bình thường. Tuy nhiên, trên VAX, chương trình lại bị kẹt trong một vòng lặp vô tận.

Nguyên nhân nằm ở sự thay đổi trong kiến trúc phần cứng của VAX theo thời gian. Trên các mẫu máy VAX đời đầu (như 11/780), tất cả các ngoại lệ số học đều là trap. Khi một trap xảy ra, phần cứng đã tự động bỏ qua lệnh gây lỗi, nên nhân hệ điều hành không cần làm gì thêm.

Tuy nhiên, các mẫu máy VAX sau đó đã thay đổi thiết kế để biến các ngoại lệ số học này thành fault. Khi một fault xảy ra, bộ vi xử lý dừng lại và mong đợi hệ điều hành khắc phục sự cố để chạy lại lệnh đó. Nếu chương trình bắt tín hiệu SIGFPE và cố gắng chạy tiếp mà không sửa đổi dữ liệu, bộ vi xử lý sẽ chỉ chạy lại chính lệnh chia cho 0 đó mãi mãi.

Để giải quyết vấn đề này, nhân hệ điều hành cần phải tự động bỏ qua (skip) lệnh gây lỗi khi cần thiết. Nhưng đây không phải là nhiệm vụ dễ dàng.

Giải mã mã máy trong nhân

Lệnh của VAX có độ dài thay đổi, phụ thuộc vào các toán hạng và chế độ địa chỉ thực tế. Một lệnh VAX có thể dài hơn 16 byte! Để bỏ qua lệnh đúng cách, nhân phải biết chính xác lệnh đó dài bao nhiêu byte.

Điều này có nghĩa là nhân phải thực hiện việc disassemble (giải mã/ngược dịch) lệnh gây lỗi để tính toán độ dài của nó. Đây là một nhiệm vụ phức tạp vì phải xử lý nhiều chế độ địa chỉ "điên rồ" mà VAX hỗ trợ.

Logic ở mức độ cao thì đơn giản: nếu lệnh gây lỗi là một ngoại lệ số học và chương trình muốn bỏ qua nó, hãy tính độ dài lệnh và tăng con trỏ lệnh (PC) lên tương ứng.

Sau khoảng 6 giờ làm việc, một giải pháp tạm thời đã được tạo ra bằng cách tái sử dụng một phần mã của trình gỡ lỗi (debugger) trong nhân để phân tích lệnh. Tuy nhiên, để tránh phụ thuộc vào mã debugger, mã này sau đó đã được viết lại độc lập, dù phải sao chép một vài dòng mã.

Michael Hitch sau đó đã phát hiện và sửa một lỗi trong thay đổi này chỉ hai ngày sau đó.

Tại sao chờ đến năm 2002?

Một câu hỏi thú vị là: Tại sao vấn đề này không được sửa cho đến năm 2002, trong khi BSD đã chạy trên phần cứng VAX từ năm 1979?

Có thể rất ít chương trình bỏ qua hoặc cố gắng xử lý tín hiệu SIGFPE, nên vòng lặp vô tận hiếm khi xảy ra. Nhưng lý do thực sự có thể nằm ở lịch sử phần cứng.

Trong phiên bản đầu của tài liệu kiến trúc VAX, có ghi chú rằng các ngoại lệ số học là trap. Điều này có nghĩa là trên các hệ thống VAX 11/780 đầu tiên, phần cứng đã tự động xử lý việc bỏ qua lệnh. Không cần nhân hệ điều hành can thiệp.

Khi kiến trúc được thay đổi để biến chúng thành fault (có thể là từ mẫu VAX-11/750 năm 1980), có vẻ như không ai chú ý đủ đến hậu quả đối với hệ điều hành Unix (BSD). Digital (nhà sản xuất VAX) có thể chỉ xem xét vấn đề từ góc độ hệ điều hành VMS của họ.

Kết quả là phần cứng không cung cấp cơ chế dễ dàng để lấy địa chỉ lệnh tiếp theo trong trường hợp fault cần được xử lý như trap. Chi phí để nhân tự thực hiện công việc giải mã lệnh là chấp nhận được vì tình huống này rất hiếm gặp.

Câu chuyện này là một minh chứng tuyệt vời cho thấy sự phức tạp trong việc phát triển hệ điều hành, nơi những quyết định thiết kế phần cứng từ hàng thập kỷ trước vẫn có thể ảnh hưởng đến mã nguồn nguồn mở ngày nay.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗