Ảo hóa 6502 trên 6502 bằng 6o6
(oldvcr.blogspot.com)- 6o6 là một dự án chạy lại 6502 bằng phần mềm trên NMOS 6502 vốn có cơ chế bảo vệ yếu, bổ sung một lớp thực thi ảo có thể kiểm soát cho các hệ thống 8-bit cũ
- Bằng cách kiểm soát việc thực thi lệnh và truy cập bộ nhớ của mã guest ở giữa, dự án cung cấp các chức năng như ánh xạ lại địa chỉ, chặn đọc/ghi bất hợp pháp, và bẫy opcode jam
- Cốt lõi là cách mượn nguyên ALU của 6502 host: đưa các thanh ghi và cờ của guest lên host, chạy cùng lệnh đó, rồi lưu lại kết quả
- Việc kiểm chứng dùng 6502 functional test suite của Klaus Dormann và môi trường kiểm thử dựa trên lib6502; cấu hình được tối ưu hóa thực thi 1.602.516.769 lệnh, ít hơn cấu hình chưa tối ưu 36,5%
- The Incredible KIMplement 1.0 được công bố kèm theo cùng nhiều ví dụ cho thấy phạm vi ứng dụng của 6o6, từ mô phỏng KIM-1, ảo hóa lồng nhau, chuyển đổi tác vụ, đến hệ thống bộ nhớ ngoài dựa trên geoRAM
Những gì 6o6 và KIMplement công bố
- The Incredible KIMplement 1.0 mô phỏng máy tính bo mạch đơn MOS/Commodore KIM-1 6502, 1KB, 1MHz
- Chạy trên Commodore 64 không mở rộng
- Hỗ trợ TTY tích hợp của KIM, và cũng có thể truy cập qua cổng serial của máy tính thật
- Không gian địa chỉ được mở rộng lên 16K
- 6o6 là viết tắt của “6502-on-6502”, một CPU ảo NMOS 6502 hoàn chỉnh bằng phần mềm chạy trên CPU 6502
- Kiểm soát việc thực thi mã guest
- Bẫy các opcode không được tài liệu hóa và opcode jam
- Trừu tượng hóa mọi truy cập bộ nhớ
- Hỗ trợ ánh xạ lại địa chỉ, chặn đọc/ghi bất hợp pháp, và thực thi dựa trên bộ nhớ ảo
- Việc chạy
hello worldcủa guest trên Commodore 64 và Apple IIe hoạt động, và ảo hóa lồng nhau — chạy lại 6o6 bên trong 6o6 — cũng hoạt động- stage 1 chạy gần như tức thì
- stage 2 chậm hơn
- stage 3 rất chậm nhưng vẫn chạy
Vì sao 6502 cần ảo hóa
- Trên các máy tính cá nhân đời đầu, thông thường một chương trình kiểm soát toàn bộ máy; nếu chương trình chạy sai thì có thể giải quyết bằng cách khởi động lại
- Trong môi trường đa người dùng hoặc đa nhiệm, mã lỗi có thể làm hỏng không gian địa chỉ khác, thực thi lệnh nguy hiểm, hoặc độc chiếm tài nguyên
- NMOS 6502 là CPU đơn giản gồm chưa tới khoảng 4.000 transistor, nên khả năng bảo vệ có giới hạn
- Nhiều hệ thống NMOS 6502 trong lịch sử khó có thể tự do di chuyển zero page hoặc vị trí stack của bộ xử lý
- Không có khả năng ánh xạ lại địa chỉ mã tới vị trí tùy ý để chạy mà không cần fixup
- Không thể cấm một cách bao quát việc truy cập các vị trí bộ nhớ cụ thể
- Nếu opcode jam hoặc
KILkhông được tài liệu hóa được thực thi, bộ xử lý có thể dừng hoàn toàn
- Một số vấn đề có thể được giảm nhẹ bằng phần cứng
- Nếu tạo NMI định kỳ, có thể đặt cờ ngắt để từ bên ngoài dừng một tiến trình đang cố độc chiếm hệ thống
- Một số kernel đa nhiệm 6502 triển khai chuyển đổi tác vụ ưu tiên theo cách này
- In-circuit emulator như “Trap65” của Eastern House Software có thể thay opcode sai thành BRK có thể bẫy, nhưng đắt tiền và có giới hạn với các thao tác bus phức tạp
Cách 6o6 thực thi
- Kiểu thông dịch đơn giản cũng có thể thực dụng trên 6502
- 6502 có ít thanh ghi
- Có 56 lệnh và không quá nhiều chế độ định địa chỉ
- Việc theo dõi trạng thái bộ xử lý tương đối dễ
- Điểm “ảo hóa” của 6o6 nằm ở chỗ dùng ALU của 6502 host cho các phép toán bên trong guest
- Nạp accumulator và cờ của guest vào CPU host
- Chạy trên host cùng lệnh mà guest sẽ thực thi
- Lưu kết quả và cờ, rồi dọn trạng thái host
- Ưu điểm của cách này là không cần tự triển khai lại trực tiếp số học và xử lý cờ
- Decimal mode, tức số học BCD, cũng hoạt động tự nhiên
- Vì 6502 thật thực hiện phép tính, kết quả giống với 6502
- Khi đọc giá trị bộ nhớ hoặc truyền thanh ghi, cách tương tự cũng được dùng để xử lý cờ âm và cờ zero
- Việc triển khai dùng mã tự sửa đổi, nên cần lưu ý riêng nếu muốn đưa vào ROM
Cấu trúc VM, harness và kernel
- 6o6 VM đóng vai trò như một engine, không phải toàn bộ hệ thống
- Môi trường thực thi được chia thành ba phần
- VM: CPU ảo độc lập phần cứng chạy trên 6502 thật
- Harness: giao diện tới bộ nhớ guest và phần cứng được quản lý
- Kernel: vòng lặp điều khiển gọi VM và xử lý ngoại lệ cùng trạng thái guest
- Harness cung cấp giao diện nhị phân qua một jump table chuẩn hóa
- Triển khai load/store tại các địa chỉ cụ thể
- Xử lý instruction fetch
- Duy trì stack phần cứng và stack pointer
- VM không giả định kích thước page hay sự tồn tại của phân trang bộ nhớ
- Harness có thể triển khai từ chuyển đổi địa chỉ đơn giản dựa trên phép cộng/bit shift đến bộ nhớ ảo phân trang
- Harness có thể thực hiện paging in/out trong quá trình load/store mà không chuyển page fault ra ngoài
- Harness cũng có thể phát sinh ngoại lệ bảo vệ
- Kernel bắt đầu thực thi VM và diễn giải mã trạng thái mà VM trả về
- Xử lý ngoại lệ phát sinh từ harness hoặc bản thân 6o6
- Kiểm tra hoặc sửa các thanh ghi guest và PC
- Có thể xử lý một số service routine cụ thể theo cách native
- Giữa các lần gọi VM, CPU guest ở trạng thái “dừng”, nên có thể chụp trạng thái hoặc chuyển ngữ cảnh
- VM không trực tiếp phát sinh IRQ, NMI hay reset ảo
- Kernel quyết định thời điểm xảy ra các sự kiện này
- BRK được hỗ trợ, nhưng VM không thiết lập stack rồi nhảy tới PC mới; nó trả về ngoại lệ
Cách KIMplement sử dụng 6o6
- Harness của KIMplement ảo hóa stack 6502 chuẩn và thiết bị mở rộng KIM-4
$0000-$17ff: bộ nhớ đọc/ghi$1800-$1fff: ROM$2000-$3fff: RAM$4000-$fff7: vùng chưa ánh xạ, không thể ghi$1ff8-$1fffđược mirror sang$fff8-$ffffcho vector
- Commodore 64 host lưu 16K thấp tại
$4000-$7fff, phần còn lại được harness tổng hợp - Kernel của KIMplement xử lý mô phỏng RRIOT, hiển thị LED, dịch vụ TTY, chèn NMI cho stop và Single-Step Switch, cùng một số bẫy của ROM monitor KIM-1
- Sau khi VM chạy, kernel KIMplement kiểm tra PC của 6o6 để quyết định có chặn routine hiện tại hay không
- TTY và một số chức năng được triển khai theo cách này
Tối ưu hóa hiệu năng
- Các lời gọi giữa 6o6 và harness có thể là nút thắt lớn
- Ngay cả lệnh đơn giản cũng cần ít nhất một lần fetch
- Định địa chỉ gián tiếp có thể tạo ra nhiều truy cập bộ nhớ hơn
- Trong KIMplement 0.2, một phần memory load được inline hóa bằng macro tiền xử lý
- Nối trực tiếp vào VM routine load cho địa chỉ ảo tùy ý và routine load tối ưu cho zero page
- Tốc độ cải thiện đáng kể nhưng kích thước VM tăng
- Store ít xảy ra hơn và phức tạp hơn nên vẫn giữ dưới dạng lời gọi subroutine
- Ở vòng lặp mới nhất, sự kém hiệu quả trong cách macro inline truy cập program counter cũng được dọn dẹp, cải thiện thêm instruction fetch
- KIMplement 0.3 thêm một dạng instruction fusion nguyên thủy gọi là “extra helpings”
- Các lệnh không đụng tới bộ nhớ không cần quay lại kernel ngay lập tức
- Đối tượng gồm lệnh immediate, lệnh xoay quanh accumulator, hầu hết lệnh implied, và branch không rẽ nhánh
- Khi xảy ra load/store, thay đổi PC không tuần tự, hoặc ngoại lệ, VM dừng thử gộp lệnh
- Extra helpings không làm bản thân VM nhanh hơn
- Nếu giới hạn chức năng theo vị trí PC như KIMplement, nó thậm chí có thể chậm hơn một chút
- Thay vào đó, các phần khác của toàn hệ thống nhanh hơn vì kernel không bị chạy không cần thiết cho từng lệnh không tạo thay đổi cần quan sát
- Vì có thể gây cản trở cho ứng dụng cần kiểm soát PC chính xác, có tùy chọn tắt theo từng bước hoặc tắt hoàn toàn
Kiểm chứng và kết quả kiểm thử
- Việc kiểm chứng dùng functional test suite của Klaus Dormann
- Binary được cung cấp không giả định gì về phần cứng
- Kết thúc thành công được báo bằng một vòng lặp vô hạn tại vị trí cụ thể
- Kiểm thử được cấu hình để chạy trực tiếp trong shell bằng lib6502 CPU emulator của Ian Piumarta
- Ban đầu lib6502 thất bại vì edge case của decimal mode, và sau khi vá thì vượt qua
- Binary do Klaus cung cấp có đủ 64K nên không thể đặt cùng 6o6 trong không gian địa chỉ 6502 mặc định
- Thêm vào lib6502 bản vá hệ thống tối thiểu 32K bank-switched
- Dùng vùng
$7000-$efff, đặt 32K đầu và 32K sau của test binary ở các bank khác nhau
- Kiểm thử với ba cấu hình
- Không extra helpings, không inline fetch macro
- Có inline fetch macro, không extra helpings
- Có cả inline fetch macro và extra helpings
- Cả ba cấu hình đều vượt qua Klaus suite
- Kết quả số lệnh như sau
- lib6502 không có 6o6: 30.646.178 lệnh
- 6o6 không tối ưu: 2.188.322.914 lệnh
- Áp dụng inline fetch macro: 1.713.350.225 lệnh
- Áp dụng inline fetch macro và extra helpings: 1.602.516.769 lệnh
- Cấu hình 6o6 nhanh nhất thực thi ít hơn 36,5% số lệnh so với cấu hình ít được tối ưu nhất
- Cấu hình nhanh nhất thực thi trung bình 52,3 lệnh cho mỗi lệnh guest
- Con số này bao gồm cả harness, kernel và phần thực thi 6o6
- Vì cycle count của từng lệnh khác nhau, không nên diễn giải thành hệ số tốc độ
Các ví dụ đi kèm
- Ví dụ hello world chạy cùng một chương trình trước trên CPU native, rồi sau đó thông qua 6o6
- Trên Commodore 64, nó được ánh xạ tới routine xuất ký tự tại
$ffd2; trên Apple II là$fded - Khi kernel phát hiện PC trỏ tới routine xuất ký tự, nó lấy accumulator của guest, gọi routine ROM native, rồi lấy return address khỏi stack và quay lại vòng lặp
- Trên Commodore 64, nó được ánh xạ tới routine xuất ký tự tại
- Ví dụ inception dùng cùng harness và kernel để 6o6 chạy chính nó như payload
- Mỗi stage có zero page và stack riêng
- Vì 6o6 hiện dùng mã tự sửa đổi, mỗi stage cần một bản sao VM
- Ở stage 3, phần lớn bộ nhớ được dùng cho 3 bản sao VM; VM có inline fetch macro lớn hơn 10KB mỗi bản
- Trong thực thi lồng nhau, lời gọi
CHROUTđược chuyển từ stage 3 qua stage 2, stage 1 rồi cuối cùng tới routine native - Việc kết thúc payload dùng lệnh RTS như một “cú đá”
- Vì lúc bắt đầu không có return address trên stack, RTS gây stack underflow
- Khi harness báo điều này là ngoại lệ, kernel xử lý như kết thúc bình thường
- Ở các stage sâu, nó cũng được truyền lên kernel cấp trên theo cách tương tự
- Trên Apple II có thể chạy lại sau khi thực thi bằng
CALL 2051; trên Commodore 64 bằngRUN- Phiên bản Apple II dùng cả vùng DOS resident phía trên
$9000, nên khuyến nghị khởi động lại sau khi chạy
- Phiên bản Apple II dùng cả vùng DOS resident phía trên
Ví dụ chuyển đổi tác vụ
- Ví dụ tasks là một kernel task-switching nhỏ chuyển qua lại giữa hai tác vụ độc lập
- Mỗi tác vụ có zero page, stack, và vùng địa chỉ mã nhỏ của riêng mình, không biết về nhau cũng như sự tồn tại của VM
- Một tác vụ hiển thị bảng chữ cái, tác vụ kia hiển thị chữ số
- Chữ số được hiển thị bằng reverse video để dễ phân biệt trực quan
- Mỗi lần nhấn phím, tác vụ được chuyển đổi
- Hai tác vụ dùng cùng vị trí trong zero page để lưu trạng thái, nhưng vì zero page độc lập nên mỗi tác vụ tiếp tục chạy từ vị trí cuối của riêng mình
- Những gì cần cho chuyển ngữ cảnh là thông tin tác vụ hiện tại và vùng lưu trạng thái A, X, Y, P, S, PC của từng tác vụ
- Harness nhìn tác vụ “on CPU” để chọn địa chỉ vật lý cho zero page, stack và mã thực thi
- Khi chuyển đổi, kernel lưu/nạp trạng thái khác và đánh dấu tác vụ khác là “on processor”
Ví dụ bộ nhớ ngoài 64K dựa trên geoRAM
- Ví dụ vmgr dành riêng cho Commodore 64, cung cấp không gian địa chỉ 64K dưới dạng bộ nhớ ngoài không dùng RAM của chính hệ thống
- geoRAM là thiết bị RAM phân trang khác với REU chính thức của Commodore
- REU thiên về DMA, dùng MOS 8726 REC để thực hiện các thao tác đọc/ghi/trao đổi với main memory
- geoRAM ánh xạ bộ nhớ qua window page 256 byte trong dải I/O
$de00 - Thanh ghi điều khiển nằm tại
$dffe,$dfff - Các clone tương thích hiện đại cũng có dung lượng tới 4MB
- VICE hỗ trợ mô phỏng geoRAM
- Ví dụ dùng ROM của 6502 processor module từng được cung cấp cho RC2014 Z80 kit computer
- ROM chứa monitor và EhBASIC của Lee Davison
- ROM được dùng là pre-built ROM trên GitHub, dùng phiên bản 6551
- Harness bỏ qua thao tác ghi từ
$c100, vùng ROM của guest- Ghi dưới 16K dùng fast path
- Cao hơn mức đó thì điều chỉnh geoRAM bank bằng mask và shift
- Cache page geoRAM hiện tại để bỏ qua thao tác thiết lập khi truy cập cùng page
- Kernel và main program được gộp làm một trong ví dụ này
- Kiểm tra sự tồn tại và hoạt động của geoRAM
- Sao chép ảnh ROM vào geoRAM
- BRK đưa trở lại monitor
- Illegal instruction, user-defined instruction trap, v.v. được xử lý như BRK
- Chặn serial vector của RC2014 ROM để mô phỏng một terminal đơn giản
- Chuyển đổi giữa PETSCII và ký tự terminal
- Duy trì một con trỏ nhỏ
- Điều chỉnh thanh ghi và cờ của guest cho khớp kết quả
- Có thể reset hệ thống được mô phỏng bằng
CTRL-SHIFT-Commodorevà bộ nhớ vẫn được giữ lại
- Nếu không nhập trực tiếp kích thước bộ nhớ khi cold start EhBASIC, tổ hợp C64 và geoRAM mất khoảng 1 phút để tìm thấy 32768 byte free
- ROM được build với hard cap
$8000, nên trên thực tế bị giới hạn ở 32768 byte dù có nhiều hơn - Có thể đặt thứ khác tại
$8000-$c0ff - EhBASIC không nhận command hoặc keyword chữ thường, nên phải nhập toàn bộ bằng chữ hoa
- ROM được build với hard cap
- Nó cũng chạy trên Commodore 128DCR thật với cartridge geoRAM 512K
- Phép toán dấu phẩy động cũng hoạt động bình thường
- Bad instruction bị chặn ngay lập tức theo cách có kiểm soát
- Ngoại trừ window 256 byte, hệ thống trên màn hình không chạy trong không gian địa chỉ của chính 6502
- Ngay cả với geoRAM 512K cũng có thể đặt riêng 8 tác vụ hệ thống 6502 dung lượng 64K
Cải tiến và ứng dụng trong tương lai
- Có thể cải tiến để chạy 6o6 từ ROM, nhưng sẽ cần refactor và có thể chậm hơn, nên gần như chỉ là một tùy chọn
- Mô phỏng 65816 được xem là ngoài phạm vi, nhưng việc mô phỏng lệnh CMOS trên hệ thống NMOS có thể khả thi
- Vì dùng ALU, nếu NMOS 6502 mô phỏng CMOS 65C02 thì cờ vẫn được đặt theo kiểu NMOS
- Chiều ngược lại cũng tương tự
- Định địa chỉ hiện được viết theo kiểu CPU NMOS
- Cách dùng inline memory macro có cơ hội tối ưu hóa peephole
- Có thể đặt một pass “post-preprocessor” trước khi assembly thật
- Vì toolchain sẽ phức tạp hơn, cần xác nhận lợi ích chung
- Một ứng dụng rõ ràng của 6o6 là chạy mã đã tải xuống mà không làm hỏng tác vụ hiện tại
- Có ý tưởng dùng nó như một phần của Gopher client để chạy động nội dung đã tải xuống
- Nếu tự thiết kế một hệ thống 6502 mới, triển khai các chức năng cần thiết bằng phần cứng có thể nhanh hơn
- Với CPU NMOS có ít cơ chế bảo vệ hoặc khi muốn giảm tối thiểu silicon bổ sung, 6o6 trở thành một giải pháp thay thế linh hoạt và dễ thích nghi
Phát hành và giấy phép
- The Incredible KIMplement được cung cấp trên trang chủ và GitHub
- 6o6 được cung cấp trên GitHub, đồng thời bao gồm cả bốn ví dụ được đề cập trong bài
- Bản cập nhật KIMplement 1.0 chủ yếu tập trung vào việc dọn dẹp để phát hành công khai và sửa lỗi nhỏ
- KIMplement cũng bao gồm Tiny PILOT do Dave Hassler cung cấp
- Tiny PILOT dựa trên bản triển khai do Nicholas Vrtis viết trên tạp chí MICRO năm 1979, cộng thêm các bản vá của Bob Applegate và Dave Hassler
- Dave Hassler cũng port ELIZA từ bản triển khai Atari PILOT năm 1980 của Carol Shaw và Harry Stewart
- Cả KIMplement và 6o6 đều được phát hành theo Floodgap Free Software License
1 bình luận
Ý kiến trên Hacker News
Dù 6502 đơn giản và hạn chế, việc chứng kiến một kiến trúc gần 50 năm tuổi vẫn liên tục bị đẩy tới những giới hạn mới luôn rất thú vị
Một số SoC nhắm tới thị trường siêu rẻ, sản xuất hàng loạt vẫn còn có nhân 6502
Có vẻ không dễ để thắng được một nhân RISC-V giá 10 xu
Ban đầu tôi đã bật cười khi tưởng tượng một SoC đa nhân có 6502 nhân 6502, nhưng nếu làm bằng FPGA thì có vẻ sẽ là một dự án thú vị
Tôi đang cắm 65816, bản kế nhiệm 16-bit, vào Apple 2 dưới dạng thẻ mở rộng tốc độ biến thiên, nhưng phần lớn thời gian chạy ở chế độ 8-bit. Vì nó hoạt động tốt và phần lớn mã thư viện cũng là 8-bit
Ở tốc độ này, con chip rất nhanh. Đặc biệt nếu nghĩ đến mô hình đơn giản trong đó RAM và CPU được clock 1:1. Trong trường hợp của tôi, có thể chạy mã qua bus 1MHz, nên hầu hết các phép toán nhiều chu kỳ trở thành như một chu kỳ bus duy nhất cho mỗi lần fetch bộ nhớ
Hoặc trên thẻ có 1MB RAM, và RAM này chạy ở tốc độ CPU (0,15~16MHz). Như vậy đủ nhanh để chạy các chương trình lớn viết bằng ngôn ngữ bậc cao với tốc độ dùng được. Tất nhiên assembly thì nhanh đến mức vô lý
Đây là một môi trường khá thú vị để vọc vạch đủ thứ
Có thể chạy GEOS bên trong cửa sổ GEOS không?
Bài này đã nổi lên khá lâu mà không có bình luận, nên tôi nghĩ để sau sẽ đọc lướt, nhưng điểm cốt lõi nằm ở cách Commodore 64 mô phỏng một hệ thống hoàn toàn khác dựa trên 6502
“6o6”, tức “6502-on-6502”, là một CPU NMOS 6502 bằng phần mềm ảo hóa hoàn chỉnh chạy trên CPU 6502, kiểm soát hoàn toàn việc thực thi mã khách, bao gồm cả các opcode không được tài liệu hóa và bẫy jam opcode, đồng thời trừu tượng hóa toàn bộ truy cập bộ nhớ
Nhờ vậy có thể remap địa chỉ, chặn các lần đọc/ghi bất hợp pháp, thậm chí chạy bộ nhớ ảo hoàn chỉnh. Không chỉ vượt qua toàn bộ bộ kiểm thử chức năng, nó còn có thể ảo hóa chính bản thân đang ảo hóa chính nó; đây là một công trình ấn tượng không chỉ xét theo góc nhìn 6502 mà theo mọi góc nhìn
Tôi cũng nhớ đến video nói Zilog Z80 có chế độ bảo vệ: https://www.youtube.com/watch?v=DLSUAVPKeYk
Tôi nhớ lại thời tự học assembly 6502 lần đầu. Có một cuốn sách tên “The Visual Computer” và kèm theo một trình giả lập trên đĩa mềm; đó thực sự là trải nghiệm mở mang tầm mắt
Tôi đã tìm thấy PDF của sách [1], nhưng không biết phần mềm trên đĩa mềm còn ở đâu đó không
[1] https://files.commodore.software/reference-material/books/c6...
“Visual 6502” là tên của trình mô phỏng 6502 hiện đại ở mức cổng logic và transistor: http://visual6502.org/JSSim/index.html
https://archive.fo/2u3Y8