Giải mã kỹ thuật giả lập bộ đồng xử lý 8087 trên hệ thống 8086
Bài viết đi sâu vào cơ chế giả lập phần mềm 8087 FPU trên các máy tính 8086/8088, phân tích cách Intel và Microsoft sử dụng các kỹ thuật linker và vector ngắt thông minh để đảm bảo tính tương thích phần mềm trong kỷ nguyên máy tính cá nhân đầu tiên.
Gần đây, tôi có cơ hội tìm hiểu lại cơ chế giả lập phần mềm cho bộ đồng xử lý 8087 (FPU) trên các máy tính sử dụng CPU 8086/8088. Đây là một câu chuyện thú vị về kỹ thuật máy tính thời kỳ đầu, nơi các giải pháp phần mềm đầy sáng tạo được tạo ra để vượt qua các hạn chế về phần cứng.
Bối cảnh lịch sử: Sự ra đời của 8087
Vào năm 1978, Intel ra mắt CPU 8086 với một giao diện bộ đồng xử lý (co-processor interface) chung. Giao diện này ban đầu được sử dụng bởi bộ xử lý nhập xuất Intel 8089 (1979) và sau đó là bộ đồng xử lý dấu phẩy động Intel 8087 (1980), ban đầu được gọi là Numeric Processor Extension (NPX).
Tuy nhiên, 8087 là một linh kiện phụ trợ khá đắt đỏ. Hơn nữa, không phải mọi hệ thống 8086/8088 đều có sẵn socket để cắm 8087 (máy IBM PC thì có, nhưng nhiều hệ thống khác thì không). Trong khi đó, có một lượng lớn phần mềm, đặc biệt là các bảng tính (spreadsheets), có thể hưởng lợi rất lớn từ khả năng tính toán dấu phẩy động của 8087.
Thách thức đặt ra cho các nhà phát triển là làm thế nào để vận chuyển phần mềm có thể sử dụng 8087 khi có mặt, nhưng vẫn chạy được trên máy tính chỉ có CPU 8086/8088 trần mà không có FPU. Đồng thời, Intel cũng muốn các lập trình viên có thể phát triển và kiểm thử phần mềm mà không cần phải lắp đặt chip 8087 vật lý trên mọi hệ thống.
Giải pháp E8087 của Intel
Intel đã phát hành gói giả lập phần mềm E8087 cùng với chip 8087. Cơ chế này được mô tả trong tài liệu "Numerics Supplement to The 8086 Family User’s Manual" từ tháng 7 năm 1980.
Vì 8086 không có cơ chế tích hợp sẵn để giả lập FPU (khác với các thế hệ sau như 80286), cơ chế giả lập khá phức tạp và đòi hỏi sự phối hợp chặt chẽ giữa trình hợp ngữ (assembler), trình biên dịch (compiler), trình liên kết (linker) và thư viện thời gian chạy.
Vai trò của Assembler và Compiler
Trình hợp ngữ hoặc trình biên dịch sẽ tạo ra mã "có thể giả lập được" (emulatable). Trình dịch thực sự tạo ra mã 8086/8087 bình thường, nhưng các mô-đun đối tượng (object modules) sẽ bao gồm các "fix-up" đặc biệt cho mọi lệnh ESC của 8087 và cho lệnh (F)WAIT.
Điểm mấu chốt là trình dịch ngôn ngữ không trực tiếp tạo ra mã giả lập 8087. Nó chỉ chuẩn bị các mô-đun đối tượng cho việc giả lập bằng cách phát ra các lệnh 8087 thông thường. Quyết định có giả lập hay không sẽ được đưa ra ở thời gian liên kết (link time).
Vai trò của Linker
Trong quá trình liên kết, quyết định giả lập hay không sẽ được thực hiện. Người dùng có thể liên kết với thư viện không giả lập (8087.LIB), trong trường hợp đó, trình liên kết sẽ giữ nguyên mã đối tượng.
Tuy nhiên, khi liên kết với các thư viện giả lập (E8087.LIB hoặc PE8087.LIB), các fix-up đặc biệt sẽ khiến trình liên kết thay thế các chuỗi lệnh NOP/ESC hoặc WAIT/ESC bằng các lệnh ngắt phần mềm (INT).
Trong triển khai gốc của Intel, các opcode ESC từ D8h đến DFh được thay thế bằng các ngắt INT 18h đến INT 1Fh. Tám vector ngắt riêng biệt được yêu cầu để thay thế cho tám opcode ESC có thể có, giúp bảo toàn thông tin opcode của FPU.
Triển khai của Microsoft trên DOS
Microsoft đã áp dụng cơ chế giả lập 8087 của Intel với một số thay đổi trong các công cụ phát triển DOS của mình (và cũng được các nhà cung cấp khác như Borland, Watcom sử dụng).
Vì các lý do hiển nhiên, Microsoft cần thay đổi phạm vi ngắt phần mềm được sử dụng. Thay vì các ngắt 18h-1Fh, trình giả lập của Microsoft sử dụng các vector 34h-3Dh. Điều thú vị là họ dùng 10 vector thay vì 8. Trong khi Intel thay thế lệnh WAIT bằng NOP khi giả lập, Microsoft cũng giả lập cả lệnh WAIT và có quy định để giả lập các lệnh FPU với sự ghi đè đoạn ES (ES segment override).
Cải tiến vượt bậc: Phát hiện phần cứng
Microsoft đã thêm một cải tiến đáng kể so với trình giả lập gốc của Intel. Nếu chương trình có tích hợp trình giả lập được chạy trên một hệ thống có sẵn chip 8087, trình giả lập sẽ phát hiện ra điều này trong quá trình khởi động.
Bất cứ khi nào một lệnh được giả lập được thực thi (thông qua INT 34h-3Dh), trình giả lập sẽ thay thế lệnh INT phần mềm bằng lệnh NOP hoặc WAIT gốc cùng với opcode ESC tương ứng, sau đó quay lại thực thi lệnh dấu phẩy động thực tế.
Cơ chế này có tác động tối thiểu đến hiệu suất (các lệnh giả lập được thay thế bằng lệnh 8087 thực ngay lần chạy đầu tiên) và đảm bảo rằng các chương trình có trình giả lập chạy ở hiệu suất 100% trên hệ thống có 8087, trong khi cùng một tệp nhị phân đó vẫn có thể chạy trên hệ thống không có FPU.
Chi tiết kỹ thuật về Fix-ups trong MASM
Cơ chế giả lập dựa trên các fix-up mang tính toán học rất thú vị. Hãy xem xét cách Microsoft xử lý việc này thông qua MASM (Macro Assembler).
Các fix-up có các tên kỳ lạ như FIDRQQ (cho lệnh FP thông thường), FIWRQQ (cho FWAIT), FIARQQ (cho lệnh FP với ghi đè đoạn DS), v.v. Các tên này là các ký hiệu tuyệt đối với các giá trị được chọn kỹ lưỡng.
Ví dụ, fix-up FIWRQQ (cho FWAIT) có giá trị 0A23Dh. Trình hợp ngữ phát ra FWAIT dưới dạng chuỗi opcode NOP/WAIT là 90 9B. Khi được hiểu là giá trị 16-bit little-endian, nó là 9B90h.
Phép tính của trình liên kết như sau:
09B90h (NOP/WAIT)
+ 0A23Dh (Giá trị FIWRQQ)
= 13DCDh
Bit cao bị loại bỏ và chuỗi byte 90 9B trong tệp đối tượng được thay thế bằng CD 3D trong tệp thực thi cuối cùng. Và CD 3D chính là opcode của INT 3Dh.
Cách tiếp cận tương tự được sử dụng cho các lệnh dấu phẩy động thông thường. Ví dụ, lệnh FINIT được hợp ngữ thành 9B DB E3 với fix-up FIDRQQ. Giá trị của FIDRQQ là 05C32h, dẫn đến kết quả là opcode INT 37h.
Đối với các lệnh có ghi đè đoạn (segment override) như CS, DS, SS, ES, cơ chế phức tạp hơn một chút với việc sử dụng hai fix-up để mã hóa thông tin về thanh ghi đoạn đang được sử dụng, cho phép trình giả lập giải mã chính xác lệnh gốc.
Kết luận
Cơ chế giả lập 8087 thực sự là một tác phẩm nghệ thuật kỹ thuật. Các trình dịch ngôn ngữ chỉ cần phát ra một vài fix-up đặc biệt khi tạo mã dấu phẩy động. Các trình liên kết đơn giản không cần hỗ trợ đặc biệt nào và chỉ áp dụng các fix-up do thư viện cung cấp. Quyết định xem các lệnh 8087 có nên được giả lập hay không được hoãn lại đến thời gian liên kết.
Hầu hết sự "ma thuật" nằm trong thư viện giả lập, cung cấp các fix-up đặc biệt và quan trọng nhất là cung cấp mã để giả lập các lệnh dấu phẩy động của 8087 – một nhiệm vụ không hề đơn giản.
Intel là người đầu tiên triển khai giả lập 8087 vào năm 1980. Microsoft đã triển khai biến thể của riêng mình vào khoảng năm 1982-1983. Triển khai của Microsoft rõ ràng dựa trên bản gốc của Intel, mặc dù không hoàn toàn giống nhau và có một bộ thiếu sót và ưu điểm khác nhau. Sự thông minh trong thiết kế này đã cho phép phần mềm thời kỳ đó phát triển mạnh mẽ bất kể sự biến đổi của cấu hình phần cứng người dùng.


