Tin tức chung·Hacker News·0 lượt xem

Tôi đã chuyển Mac OS X sang Nintendo Wii

I Ported Mac OS X to the Nintendo Wii

AI Summary

SKIP

Mac OS X 10.0 (Cheetah) chạy tự nhiên trên Nintendo Wii Kể từ khi ra mắt vào năm 2007, Wii đã thấy một số hệ điều hành được chuyển sang nó: Linux, NetBSD và gần đây nhất là Windows NT. Hôm nay, Mac OS X...

Mac OS X Cheetah running on the Nintendo Wii Mac OS X 10.0 (Cheetah) chạy native trên Nintendo Wii

Kể từ khi ra mắt vào năm 2007, Wii đã chứng kiến nhiều hệ điều hành được port sang: Linux, NetBSD và gần đây nhất là Windows NT. Hôm nay, Mac OS X đã góp mặt trong danh sách đó.

Trong bài viết này, tôi sẽ chia sẻ cách mình đã port phiên bản đầu tiên của Mac OS X, 10.0 Cheetah, lên Nintendo Wii. Nếu bạn không phải là chuyên gia về hệ điều hành hay kỹ sư phần cứng cấp thấp, đừng lo, bạn không hề đơn độc; dự án này hoàn toàn là về việc học hỏi và điều hướng vô số những “ẩn số chưa biết”. Hãy cùng tôi khám phá phần cứng của Wii, phát triển bootloader, vá kernel và viết driver - để mang lại sức sống mới cho các phiên bản PowerPC của Mac OS X trên Nintendo Wii.

Hãy truy cập kho lưu trữ bootloader wiiMac để xem hướng dẫn cách tự mình thực hiện dự án này.

Khảo sát tính khả thi

Trước khi tìm cách bắt tay vào dự án này, tôi cần biết liệu nó có khả thi hay không. Theo một bình luận trên Reddit năm 2021:

Không có cơ hội nào cho việc này xảy ra cả.

Cảm thấy được khích lệ, tôi bắt đầu với những điều cơ bản: phần cứng nào có trong Wii và nó so sánh như thế nào với phần cứng được sử dụng trong các máy Mac thực thụ thời bấy giờ.

Khả năng tương thích phần cứng

Wii sử dụng bộ vi xử lý PowerPC 750CL - một sự tiến hóa của PowerPC 750CXe từng được sử dụng trong các dòng G3 iBooks và một số máy G3 iMac. Với dòng dõi gần gũi này, tôi cảm thấy tự tin rằng CPU sẽ không phải là rào cản.

Về RAM, Wii có cấu hình độc đáo: tổng cộng 88 MB, chia thành 24 MB 1T-SRAM (MEM1) và 64 MB GDDR3 SDRAM (MEM2) chậm hơn; cấu hình này không phổ biến nhưng về mặt kỹ thuật là đủ cho Mac OS X Cheetah, vốn yêu cầu chính thức 128 MB RAM nhưng vẫn có thể boot không chính thức với dung lượng thấp hơn. Để đảm bảo an toàn, tôi đã sử dụng QEMU để boot Cheetah với 64 MB RAM và xác nhận rằng không có vấn đề gì xảy ra.

Các phần cứng khác mà tôi cần hỗ trợ bao gồm:

  • Xuất log debug qua serial bằng USB Gecko
  • Thẻ SD để boot phần còn lại của hệ thống khi kernel đã chạy
  • Bộ điều khiển ngắt (Interrupt controllers)
  • Xuất video qua framebuffer nằm trong RAM
  • Các cổng USB của Wii để sử dụng chuột và bàn phím

Sau khi tin rằng phần cứng của Wii không hoàn toàn không tương thích với Mac OS X, tôi chuyển sự chú ý sang việc nghiên cứu các thành phần phần mềm mà tôi sẽ port.

Khả năng tương thích phần mềm

Mac OS X có một lõi mã nguồn mở (Darwin, với XNU làm kernel và IOKit làm mô hình driver), với các thành phần mã nguồn đóng được đặt lên trên (Quartz, Dock, Finder, các ứng dụng và framework hệ thống). Về lý thuyết, nếu tôi có thể sửa đổi đủ các phần mã nguồn mở để chạy được Darwin, thì các phần mã nguồn đóng sẽ chạy được mà không cần thêm bản vá nào.

Kiến trúc Mac OS X Nguồn: Wikipedia: Kiến trúc macOS

Việc port Mac OS X cũng đòi hỏi phải hiểu cách một chiếc Mac thực thụ khởi động. Các máy Mac PowerPC từ đầu những năm 2000 sử dụng Open Firmware làm môi trường phần mềm cấp thấp nhất; đơn giản mà nói, đó là đoạn mã đầu tiên chạy khi Mac được bật nguồn. Open Firmware có một số trách nhiệm, bao gồm:

  • Phát hiện và cấu hình phần cứng
  • Xây dựng cây thiết bị (device tree) dựa trên phần cứng được phát hiện
  • Cung cấp các hàm hữu ích cho I/O, đồ họa và giao tiếp phần cứng
  • Tải và thực thi bootloader của hệ điều hành từ hệ thống tệp

Open Firmware cuối cùng sẽ bàn giao quyền điều khiển cho BootX, bootloader của Mac OS X. BootX chuẩn bị hệ thống để có thể chuyển quyền điều khiển cho kernel. Các trách nhiệm của BootX bao gồm:

  • Đọc cây thiết bị từ Open Firmware
  • Tải và giải mã kernel XNU, một tệp thực thi Mach-O, từ hệ thống tệp gốc
  • Chuyển quyền điều khiển cho kernel

Khi XNU đã chạy, không còn phụ thuộc vào BootX hay Open Firmware nữa. XNU tiếp tục khởi tạo các bộ vi xử lý, bộ nhớ ảo, IOKit, BSD và cuối cùng tiếp tục quá trình boot bằng cách tải và chạy các tệp thực thi khác từ hệ thống tệp gốc.

Mảnh ghép cuối cùng của câu đố là làm thế nào để chạy mã tùy chỉnh của riêng tôi trên Wii - một nhiệm vụ dễ dàng nhờ việc Wii đã được “jailbreak”, cho phép bất kỳ ai cũng có thể chạy homebrew với toàn quyền truy cập vào phần cứng thông qua Homebrew ChannelBootMii.

Cách tiếp cận port

Được trang bị kiến thức về cách hoạt động của quy trình boot trên Mac thực thụ, cùng với cách chạy mã cấp thấp trên Wii, tôi cần chọn một phương pháp để boot Mac OS X trên Wii. Tôi đã đánh giá ba lựa chọn:

  1. Port Open Firmware, sử dụng nó để chạy BootX chưa chỉnh sửa nhằm boot Mac OS X
  2. Port BootX và sửa đổi nó để không dựa vào Open Firmware, sử dụng nó để boot Mac OS X
  3. Viết một bootloader tùy chỉnh thực hiện các thiết lập tối thiểu để boot Mac OS X

Vì Mac OS X không phụ thuộc vào Open Firmware hay BootX khi đã chạy, nên việc dành thời gian port một trong hai thứ đó có vẻ là một sự xao lãng không cần thiết. Ngoài ra, cả Open Firmware và BootX đều chứa đựng sự phức tạp bổ sung để hỗ trợ nhiều cấu hình phần cứng khác nhau - sự phức tạp mà tôi không cần tới vì dự án này chỉ cần chạy trên Wii. Theo bước chân của dự án Wii Linux, tôi quyết định viết bootloader của riêng mình từ đầu. Bootloader sẽ cần, tối thiểu là:

  • Khởi tạo phần cứng của Wii
  • Tải kernel từ thẻ SD
  • Xây dựng cây thiết bị và đối số boot
  • Chuyển quyền điều khiển cho kernel

Khi kernel đã chạy, mã bootloader không còn quan trọng nữa. Tại thời điểm đó, trọng tâm của tôi sẽ chuyển sang việc vá kernel và viết driver.

Viết một Bootloader

Tôi quyết định xây dựng bootloader của mình dựa trên một số đoạn mã ví dụ cấp thấp cho Wii có tên là ppcskel. ppcskel đưa hệ thống vào một trạng thái ban đầu ổn định và cung cấp các hàm hữu ích cho những công việc phổ biến như đọc tệp từ thẻ SD, vẽ văn bản lên framebuffer và ghi log debug vào USB Gecko.

Tải Kernel

Tiếp theo, tôi phải tìm cách tải kernel XNU vào bộ nhớ để có thể chuyển quyền điều khiển cho nó. Kernel được lưu trữ ở một định dạng nhị phân đặc biệt gọi là Mach-O và cần được giải mã đúng cách trước khi sử dụng.

Định dạng tệp thực thi Mach-O được ghi chép rất kỹ, và có thể được coi là một danh sách các lệnh tải (load commands) chỉ cho loader biết nơi đặt các phần khác nhau của tệp nhị phân vào bộ nhớ. Ví dụ, một lệnh tải có thể hướng dẫn loader đọc dữ liệu từ file offset 0x2cf000 và lưu trữ nó tại địa chỉ bộ nhớ 0x2e0000. Sau khi xử lý tất cả các lệnh tải của kernel, chúng ta sẽ có bố cục bộ nhớ như sau:

0x00000000: Exception vectors
0x00011000: LC_SEGMENT __TEXT
0x002e0000: LC_SEGMENT __DATA
0x00367000: LC_SEGMENT __KLD
0x00395000: LC_SEGMENT __LINKEDIT
0x00434000: LC_SEGMENT __SYMTAB
0x004d3000: LC_SEGMENT __HEADER

Tệp kernel cũng chỉ định địa chỉ bộ nhớ nơi quá trình thực thi sẽ bắt đầu. Khi bootloader nhảy tới địa chỉ này, kernel sẽ nắm toàn quyền kiểm soát và bootloader sẽ không còn chạy nữa.

Gọi Kernel

Để nhảy tới địa chỉ bộ nhớ của kernel-entry-point, tôi cần cast địa chỉ đó thành một hàm và gọi nó:

(*(void (*)())kernel_entry_point)(boot_args_address, MAC_OS_X_SIGNATURE);

Sau khi đoạn mã này chạy, màn hình chuyển sang màu đen và các bản ghi debug của tôi không còn được gửi qua kết nối debug serial nữa - mặc dù không mấy kịch tính, nhưng đây là dấu hiệu cho thấy kernel đang chạy.

LED nhấp nháy sau khi gọi kernel

Câu hỏi đặt ra là: tôi đã thực hiện được bao xa trong quá trình boot? Để trả lời, tôi phải bắt đầu xem xét mã nguồn XNU. Đoạn mã đầu tiên chạy là một routine assembly _start của PowerPC. Đoạn mã này cấu hình lại phần cứng, ghi đè lên tất cả các thiết lập đặc thù của Wii mà bootloader đã thực hiện và trong quá trình đó, vô hiệu hóa chức năng của bootloader đối với việc debug qua serial và xuất video. Nếu không có các phương tiện xuất thông tin debug thông thường, tôi cần theo dõi tiến trình theo một cách khác.

Cách tiếp cận mà tôi nghĩ ra hơi giống một kiểu hack: patch nhị phân (binary-patch) kernel, thay thế các lệnh bằng những lệnh làm sáng một trong các đèn LED ở mặt trước của Wii. Nếu đèn LED sáng lên sau khi nhảy tới kernel, tôi sẽ biết rằng kernel đã chạy được ít nhất đến đoạn đó. Việc bật một trong những đèn LED này đơn giản là ghi một giá trị vào một địa chỉ bộ nhớ cụ thể. Trong assembly PowerPC, các lệnh đó là:

lis    r5, 0xd80        ; tải nửa trên của 0x0D8000C0 vào r5
ori    r5, r5, 0xc0     ; tải nửa dưới của 0x0D8000C0 vào r5
lwz    r4, (r5)         ; đọc giá trị 32-bit từ 0x0D8000C0
sync                    ; rào cản bộ nhớ
xori   r4, r4, 0x20     ; bật/tắt bit 5
stw    r4, (r5)         ; ghi giá trị ngược lại vào 0x0D8000C0

Để biết những phần nào của kernel cần patch, tôi đã đối chiếu tên các hàm trong mã nguồn XNU với các offset hàm trong tệp nhị phân kernel đã biên dịch, sử dụng Hopper Disassembler để giúp quá trình này dễ dàng hơn. Sau khi xác định được offset chính xác trong tệp nhị phân tương ứng với đoạn mã tôi muốn patch, tôi chỉ cần thay thế các lệnh hiện có tại offset đó bằng các lệnh để nhấp nháy đèn LED.

Ảnh chụp màn hình Xcode Ảnh chụp màn hình Hopper

Để làm cho quá trình patch này dễ dàng hơn, tôi đã thêm một số mã vào bootloader để patch tệp nhị phân kernel ngay khi đang chạy, cho phép tôi thử các offset khác nhau mà không cần phải sửa đổi thủ công tệp kernel trên đĩa.

Sau khi theo dõi qua nhiều routine khởi động kernel, cuối cùng tôi đã lập ra được lộ trình thực thi này:

1. start.s: start
2. start.s: allStart
3. start.s: nextPVR
4. start.s: donePVR
5. start.s: doOurInit
6. start.s: noFloat
7. start.s: noVector
8. start.s: noSMP
9. start.s: noThermometer
10. ppc_init.c: ppcInit
11. pe_init.c: PE_INIT_PLATFORM
12. device_tree.c: find_entry (lỗi với exception 300)

Đây là một cột mốc thú vị - kernel chắc chắn đã chạy và tôi thậm chí đã đi được vào một số mã C cấp cao hơn. Để vượt qua lỗi exception 300, bootloader sẽ cần chuyển một con trỏ tới một device tree hợp lệ.

Tạo và chuyển Device Tree

Device tree là một cấu trúc dữ liệu đại diện cho tất cả phần cứng trong hệ thống cần được hiển thị với hệ điều hành. Đúng như tên gọi, nó là một cái cây gồm các nút, mỗi nút có khả năng chứa các thuộc tính và tham chiếu đến các nút con.

Trên các máy tính Mac thực sự, bootloader quét phần cứng và xây dựng một device tree dựa trên những gì nó tìm thấy. Vì phần cứng của Wii luôn giống nhau, bước quét này có thể được bỏ qua. Tôi đã kết thúc bằng việc hard-code device tree trong bootloader, lấy cảm hứng từ device tree mà dự án Wii Linux sử dụng.

Vì không chắc mình cần hỗ trợ bao nhiêu phần cứng của Wii để đưa quá trình boot đi xa hơn, tôi bắt đầu với một device tree tối thiểu: một nút gốc với các nút con cho CPU và bộ nhớ:

/
└── cpus
    └── PowerPC,750
└── memory

Kế hoạch của tôi là mở rộng device tree với nhiều phần cứng hơn khi tôi tiến xa hơn trong quá trình boot - cuối cùng là xây dựng một mô hình hoàn chỉnh về tất cả phần cứng của Wii mà tôi dự định hỗ trợ trong Mac OS X.

Sau khi đã tạo xong device tree và lưu trữ trong bộ nhớ, tôi cần chuyển nó cho kernel như một phần của boot_args:

typedef struct boot_args {
    u16	Revision;	                /* Phiên bản của cấu trúc boot_args */
    u16	Version;	                /* Phiên bản của cấu trúc boot_args */
    char CommandLine[256];	        /* Truyền vào dòng lệnh */
    DRAMBank PhysicalDRAM[26];	    /* Các cặp base và range cho 26 bank DRAM */
    Boot_Video Video;		        /* Thông tin Video */
    u32	machineType;	            /* Loại máy (gestalt) */
    void *deviceTreeP;	            /* Cơ sở của device tree phẳng */
    u32	deviceTreeLength;           /* Độ dài của cây phẳng */
    u32	topOfKernelData;            /* Địa chỉ cao nhất được sử dụng trong vùng dữ liệu kernel */
} boot_args_t;

Với device tree trong bộ nhớ, tôi đã vượt qua được lỗi device_tree.c. Bootloader đang thực hiện tốt các tác vụ cơ bản: tải kernel, tạo các đối số boot và device tree, và cuối cùng là gọi kernel. Để đạt được tiến bộ hơn nữa, tôi cần chuyển sự chú ý sang việc patch mã nguồn kernel để sửa các vấn đề tương thích còn lại.

Patch Kernel

Tại thời điểm này, kernel bị kẹt khi chạy một số đoạn mã để thiết lập bộ nhớ video và I/O. XNU từ thời kỳ này đưa ra các giả định về vị trí của bộ nhớ video và I/O, sau đó cấu hình lại Block Address Translations (BATs) theo cách không tương thích với bố cục bộ nhớ của Wii (MEM1 bắt đầu tại 0x00000000, MEM2 bắt đầu tại 0x10000000). Để giải quyết những hạn chế này, đã đến lúc phải sửa đổi mã nguồn kernel và khởi động bằng một kernel binary đã được sửa đổi.

Việc tìm ra một môi trường phát triển phù hợp để xây dựng một OS kernel từ 25 năm trước thực sự tốn không ít công sức. Đây là những gì tôi đã chọn:

  • Guest Mac OS X Cheetah (chạy qua QEMU), không giao diện (headless), trên host macOS hiện đại
  • Mã nguồn XNU nằm trên hệ thống tệp của host và được chia sẻ qua máy chủ NFS
  • Guest truy cập mã nguồn XNU thông qua mount NFS
  • Host sử dụng SSH để điều khiển guest
  • Chỉnh sửa mã nguồn XNU trên host, bắt đầu build qua SSH trên guest, các tệp tin kết quả được lưu trên hệ thống tệp mà cả host và guest đều có thể truy cập

Để thiết lập các phụ thuộc cần thiết cho việc build kernel Mac OS X Cheetah trên guest Mac OS X Cheetah, tôi đã làm theo hướng dẫn tại đây. Chúng phần lớn khớp với những gì tôi cần làm. Các nguồn liên quan có sẵn từ Apple tại đây.

Sau khi sửa phần thiết lập BAT và thêm một số bản vá nhỏ để chuyển hướng đầu ra console sang USB Gecko, tôi đã có đầu ra video và nhật ký gỡ lỗi serial hoạt động - giúp việc phát triển và gỡ lỗi trong tương lai trở nên dễ dàng hơn đáng kể. Nhờ khả năng hiển thị mới này vào những gì đang diễn ra, tôi có thể thấy rằng bộ nhớ ảo, IOKit và các hệ thống con BSD đều đã được khởi tạo và chạy - mà không bị treo. Đây là một cột mốc quan trọng, giúp tôi tự tin rằng mình đang đi đúng hướng để hoàn thiện một hệ thống đầy đủ.

Verbose boot logs, ending with Still waiting for root device

Những độc giả từng cố gắng chạy Mac OS X trên PC thông qua phương pháp “hackintoshing” có thể nhận ra dòng cuối cùng trong nhật ký khởi động: thông báo đáng sợ “Still waiting for root device”. Lỗi này xảy ra khi hệ thống không thể tìm thấy hệ thống tệp gốc để tiếp tục khởi động. Trong trường hợp của tôi, điều này đã được dự đoán trước: kernel đã làm tất cả những gì có thể và sẵn sàng tải phần còn lại của hệ thống Mac OS X từ hệ thống tệp, nhưng nó không biết vị trí của hệ thống tệp này ở đâu. Để tiến xa hơn, tôi cần chỉ cho kernel cách đọc từ thẻ SD của Wii. Để làm được điều đó, tôi cần giải quyết giai đoạn tiếp theo của dự án này: viết trình điều khiển (drivers).

Viết trình điều khiển (Drivers)

Tìm hiểu mô hình trình điều khiển IOKit

Các trình điều khiển trên Mac OS X được xây dựng bằng IOKit - một tập hợp các thành phần phần mềm nhằm giúp việc mở rộng kernel để hỗ trợ các thiết bị phần cứng khác nhau trở nên dễ dàng hơn. Trình điều khiển được viết bằng một tập con của C++ và sử dụng rộng rãi các khái niệm lập trình hướng đối tượng như kế thừa và kết hợp (composition). Nhiều chức năng hữu ích được cung cấp, bao gồm:

  • Các lớp cơ sở và “họ” (families) triển khai các hành vi chung cho các loại phần cứng khác nhau
  • Kiến trúc runtime phân lớp thể hiện mối quan hệ giữa nhà cung cấp và máy khách
  • Dò tìm và kết nối trình điều khiển với phần cứng hiện diện trong cây thiết bị (device tree)
  • Các lớp trừu tượng để truy cập bộ nhớ thiết bị

Trong IOKit, có hai loại trình điều khiển: trình điều khiển thiết bị cụ thể và nub. Trình điều khiển thiết bị cụ thể là một đối tượng quản lý một phần cứng nhất định. Nub là một đối tượng đóng vai trò là điểm gắn kết cho một trình điều khiển thiết bị cụ thể, đồng thời cung cấp khả năng cho trình điều khiển được gắn kết đó giao tiếp với trình điều khiển đã tạo ra nub. Chính chuỗi trình điều khiển-đến-nub-đến-trình điều khiển này tạo ra các mối quan hệ nhà cung cấp-máy khách đã đề cập ở trên. Tôi đã mất một thời gian để hiểu khái niệm này và thấy rằng một ví dụ cụ thể sẽ hữu ích hơn.

Các máy Mac thực thụ có thể có bus PCI với nhiều cổng PCI. Trong ví dụ này, hãy xem xét một thẻ ethernet được cắm vào một trong các cổng PCI. Một trình điều khiển có tên IOPCIBridge sẽ xử lý giao tiếp với phần cứng bus PCI trên bo mạch chủ. Trình điều khiển này quét bus, tạo ra các nub IOPCIDevice (điểm gắn kết) cho mỗi thiết bị được tìm thấy. Một trình điều khiển giả định cho thẻ ethernet được cắm vào (hãy gọi là SomeEthernetCard) có thể gắn vào nub này, sử dụng nó làm proxy để gọi các chức năng PCI được cung cấp bởi trình điều khiển IOPCIBridge ở phía bên kia. Trình điều khiển SomeEthernetCard cũng có thể tạo ra các nub IOEthernetInterface riêng để các phần ở cấp độ cao hơn của ngăn xếp mạng IOKit có thể gắn vào nó.

IOKit Provider / Client Relationship

Ai đó phát triển trình điều khiển thẻ ethernet PCI sẽ chỉ cần viết SomeEthernetCard; việc giao tiếp bus PCI ở cấp độ thấp hơn và mã ngăn xếp mạng ở cấp độ cao hơn đều đã được cung cấp bởi các họ trình điều khiển IOKit hiện có. Miễn là SomeEthernetCard có thể gắn vào một nub IOPCIDevice và xuất bản các nub IOEthernetInterface của riêng nó, nó có thể tự chèn mình vào giữa hai họ hiện có trong ngăn xếp trình điều khiển, hưởng lợi từ tất cả các chức năng do IOPCIFamily cung cấp, đồng thời đáp ứng các nhu cầu của IONetworkingFamily.

Thể hiện phần cứng của Wii

Không giống như các máy Mac cùng thời, Wii không sử dụng PCI để kết nối các phần cứng khác nhau vào bo mạch chủ. Thay vào đó, nó sử dụng một system-on-a-chip (SoC) tùy chỉnh có tên là Hollywood. Thông qua Hollywood, nhiều phần cứng có thể được truy cập: GPU, thẻ SD, WiFi, Bluetooth, bộ điều khiển ngắt, cổng USB và nhiều thứ khác. Hollywood cũng chứa một bộ đồng xử lý ARM, có biệt danh là Starlet, giúp cung cấp chức năng phần cứng cho bộ xử lý PowerPC chính thông qua inter-processor-communication (IPC).

Nintendo Wii Hardware Diagram Nguồn: WiiBrew: Hardware

Bố cục phần cứng và giao thức truyền thông độc đáo này đồng nghĩa với việc tôi không thể tận dụng một driver family IOKit hiện có như IOPCIFamily. Thay vào đó, tôi cần phải triển khai một driver tương đương cho Hollywood SoC, tạo ra các "nub" đại diện cho các điểm kết nối (attach-point) cho tất cả phần cứng mà nó chứa. Tôi đã chọn bố cục driver và nub này (lưu ý rằng đây chỉ là một tập con của các driver cần được viết):

Wii IOKit Driver Layout

Giờ đây, khi đã có ý tưởng rõ ràng hơn về cách thể hiện phần cứng của Wii trong IOKit, tôi bắt đầu công việc với driver Hollywood của mình.

Viết Driver Hollywood

Tôi bắt đầu bằng việc tạo một file header và file thực thi C++ mới cho driver NintendoWiiHollywood. "Cá tính" (personality) của driver này cho phép nó được khớp với một node trong device tree có tên là “hollywood”. Khi driver đã được khớp và đang chạy, đã đến lúc xuất bản các nub cho tất cả các thiết bị con của nó.

Một lần nữa, dựa vào device tree làm nguồn dữ liệu chính xác cho biết những phần cứng nào nằm dưới Hollywood, tôi đã lặp qua tất cả các phần tử con của node Hollywood, tạo và xuất bản các nub NintendoWiiHollywoodDevice cho mỗi phần tử:

bool NintendoWiiHollywood::publishBelow(OSIterator *iter)
{
  IORegistryEntry *next;
  IOService *nub;
  if (!iter)
  {
    return false;
  }
  // loop through all children of /hollywood
  while ((next = (IORegistryEntry *)iter->getNextObject()))
  {
    // create a nub
    nub = createNub(next);
    if (!nub)
    {
      continue;
    }
    // publish nubs so that drivers can attach to them
    if (nub->attach(this))
    {
      nub->registerService();
    }
    nub->release();
  }
  iter->release();
  return true;
}
IOService *NintendoWiiHollywood::createNub(IORegistryEntry *from)
{
  NintendoWiiHollywoodDevice *nub = new NintendoWiiHollywoodDevice;
  if (nub && nub->init(from, gIODTPlane))
  {
    // give the nub a reference back to its hollywood "provider"
    nub->hollywood = this;
    return nub;
  }
  if (nub)
  {
    nub->release();
  }
  return 0;
}

Sau khi các nub NintendoWiiHollywoodDevice được tạo và xuất bản, hệ thống sẽ có thể cho phép các trình điều khiển thiết bị khác, như driver thẻ SD, kết nối với chúng.

Viết Driver thẻ SD

Tiếp theo, tôi chuyển sang viết một driver để cho phép hệ thống đọc và ghi từ thẻ SD của Wii. Driver này chính là thứ sẽ giúp hệ thống tiếp tục quá trình khởi động, vì nó đang bị mắc kẹt khi tìm kiếm filesystem gốc để tải các file khởi động bổ sung.

Tôi bắt đầu bằng việc tạo subclass từ IOBlockStorageDevice, lớp này có nhiều phương thức trừu tượng dự định được triển khai bởi các subclass:

virtual IOReturn doAsyncReadWrite(IOMemoryDescriptor *buffer, UInt32 block, UInt32 nblks, IOStorageCompletion completion) = 0;
virtual IOReturn doSyncReadWrite(IOMemoryDescriptor *buffer, UInt32 block, UInt32 nblks) = 0;
virtual IOReturn doEjectMedia(void) = 0;
virtual IOReturn doFormatMedia(UInt64 byteCapacity) = 0;
virtual UInt32 doGetFormatCapacities(UInt64 *capacities, UInt32 capacitiesMaxCount) const = 0;
virtual IOReturn doLockUnlockMedia(bool doLock) = 0;
virtual IOReturn doSynchronizeCache(void) = 0;
virtual char *getVendorString(void) = 0;
virtual char *getProductString(void) = 0;
virtual char *getRevisionString(void) = 0;
virtual char *getAdditionalDeviceInfoString(void) = 0;
virtual IOReturn reportBlockSize(UInt64 *blockSize) = 0;
virtual IOReturn reportEjectability(bool *isEjectable) = 0;
virtual IOReturn reportLockability(bool *isLockable) = 0;
virtual IOReturn reportMaxReadTransfer(UInt64 blockSize, UInt64 *max) = 0;
virtual IOReturn reportMaxWriteTransfer(UInt64 blockSize, UInt64 *max) = 0;
virtual IOReturn reportMaxValidBlock(UInt64 *maxBlock) = 0;
virtual IOReturn reportMediaState(bool *mediaPresent, bool *changedState) = 0;
virtual IOReturn reportPollRequirements(bool *pollRequired, bool *pollIsExpensive) = 0;
virtual IOReturn reportRemovability(bool *isRemovable) = 0;
virtual IOReturn reportWriteProtection(bool *isWriteProtected) = 0;

Với hầu hết các phương thức này, tôi có thể triển khai chúng bằng các giá trị được gán cứng (hard-coded) khớp với phần cứng thẻ SD của Wii; chuỗi nhà cung cấp, kích thước khối, kích thước truyền đọc và ghi tối đa, khả năng đẩy thẻ, và nhiều thông số khác đều trả về các giá trị hằng số, nên việc triển khai khá đơn giản.

Các phương thức thú vị hơn cần triển khai là những phương thức thực sự cần giao tiếp với thẻ SD đang được cắm vào: lấy dung lượng của thẻ SD, đọc từ thẻ SD và ghi vào thẻ SD:

virtual IOReturn doAsyncReadWrite(IOMemoryDescriptor *buffer, UInt32 block, UInt32 nblks, IOStorageCompletion completion) = 0;
virtual IOReturn doSyncReadWrite(IOMemoryDescriptor *buffer, UInt32 block, UInt32 nblks) = 0;
virtual IOReturn reportMaxValidBlock(UInt64 *maxBlock) = 0;

Để giao tiếp với thẻ SD, tôi đã sử dụng chức năng IPC được cung cấp bởi MINI đang chạy trên bộ đồng xử lý Starlet. Bằng cách ghi dữ liệu vào các địa chỉ bộ nhớ dành riêng nhất định, trình điều khiển (driver) thẻ SD có thể gửi lệnh đến MINI. Sau đó, MINI sẽ thực thi các lệnh đó và truyền kết quả ngược lại bằng cách ghi vào một địa chỉ bộ nhớ dành riêng khác mà trình điều khiển có thể theo dõi.

MINI hỗ trợ nhiều loại lệnh hữu ích. Các lệnh được trình điều khiển thẻ SD sử dụng là:

  • IPC_SDMMC_SIZE: Trả về số lượng sector trên thẻ SD đang được cắm vào
  • IPC_SDMMC_READ: Đọc dữ liệu từ một sector vào bộ đệm bộ nhớ
  • IPC_SDMMC_WRITE: Ghi dữ liệu từ bộ đệm bộ nhớ vào một sector

Với ba loại lệnh này, các thao tác đọc, ghi và kiểm tra dung lượng đều có thể được triển khai, cho phép tôi đáp ứng các yêu cầu cốt lõi của lớp con thiết bị lưu trữ khối (block storage device).

Giống như hầu hết các nỗ lực lập trình khác, mọi thứ hiếm khi hoạt động ngay trong lần thử đầu tiên. Để điều tra các vấn đề, công cụ gỡ lỗi chính của tôi là gửi các thông báo nhật ký (log) đến trình gỡ lỗi nối tiếp thông qua các lệnh gọi IOLog. Với kỹ thuật này, tôi có thể thấy những phương thức nào đang được gọi trên trình điều khiển của mình, giá trị nào được truyền vào và giá trị nào mà quá trình triển khai IPC của tôi đang gửi đếnnhận từ MINI - nhưng tôi không có khả năng đặt điểm dừng (breakpoint) hoặc phân tích quá trình thực thi một cách linh hoạt trong khi kernel đang chạy.

Một trong những lỗi khó nhằn nhất mà tôi gặp phải liên quan đến bộ nhớ đệm (cached memory). Khi trình điều khiển thẻ SD muốn đọc từ thẻ SD, lệnh mà nó gửi tới MINI (đang chạy trên CPU ARM) bao gồm địa chỉ bộ nhớ để lưu trữ bất kỳ dữ liệu nào được tải. Sau khi MINI ghi xong vào bộ nhớ, trình điều khiển thẻ SD (đang chạy trên CPU PowerPC) có thể không nhìn thấy nội dung đã cập nhật nếu vùng nhớ đó được đánh dấu là có thể đệm (cacheable). Trong trường hợp đó, PowerPC sẽ đọc từ các dòng cache của nó thay vì RAM, dẫn đến việc trả về dữ liệu cũ thay vì nội dung mới được tải. Để khắc phục điều này, trình điều khiển thẻ SD phải sử dụng bộ nhớ không đệm (uncached memory) cho các bộ đệm của nó.

Sau vài ngày sửa lỗi, tôi đã đạt được một cột mốc mới: IOBlockStorageDriver, vốn được gắn vào trình điều khiển thẻ SD của tôi, đã bắt đầu xuất bản các nub IOMedia đại diện cho các phân vùng logic có trên thẻ SD. Thông qua các nub này, các thành phần cấp cao hơn của hệ thống đã có thể gắn kết và bắt đầu sử dụng thẻ SD. Quan trọng hơn, hệ thống giờ đây đã có thể tìm thấy một root filesystem để tiếp tục quá trình khởi động, và tôi không còn bị kẹt ở thông báo “Still waiting for root device” nữa:

Nhật ký khởi động của tôi giờ trông như thế này:

Waiting on <dict ID="0"><key>IOProviderClass</key><string ID="1">IOService</string><key>BSD Name</key><string ID="2">disk0s4</string></dict>
NintendoWiiSDCard: started
Got boot device = IOService:/NintendoWiiPE/hollywood/NintendoWiiHollywood/sdhc@D070000/NintendoWiiSDCard/IOBlockStorageDriver/Nintendo Nintendo Wii SD Media/IOApplePartitionScheme/Untitled 4@4
BSD root: disk0s4, major 14, minor 3
devfs on /dev

Vượt qua lỗi "Still waiting for root device” khi đang đi tàu

Sau vài vòng sửa lỗi nữa (trong khi đang di chuyển), tôi đã có thể khởi động vượt qua chế độ single-user:

Khởi động đến "Tuning system" khi đang đi máy bay

Và cuối cùng, hoàn thành toàn bộ chuỗi khởi động ở chế độ verbose, kết thúc bằng thông báo: “Startup complete”:

Khởi động đến “Startup complete”, sau đó bị treo

Tại thời điểm này, hệ thống đang cố gắng tìm một trình điều khiển framebuffer để giao diện người dùng GUI của Mac OS X có thể hiển thị. Như đã chỉ ra trong nhật ký, WindowServer đã không hoạt động - để khắc phục điều này, tôi cần viết trình điều khiển framebuffer của riêng mình.

Viết Trình điều khiển Framebuffer

Framebuffer là một vùng RAM lưu trữ dữ liệu điểm ảnh (pixel) được dùng để tạo hình ảnh trên màn hình. Dữ liệu này thường bao gồm các giá trị thành phần màu cho mỗi pixel. Để thay đổi nội dung hiển thị, dữ liệu điểm ảnh mới được ghi vào framebuffer, sau đó sẽ được hiển thị vào lần làm mới màn hình tiếp theo. Đối với Wii, framebuffer thường nằm ở đâu đó trong MEM1 do nó nhanh hơn MEM2 một chút. Tôi đã chọn đặt framebuffer của mình vào megabyte cuối cùng của MEM1 tại địa chỉ 0x01700000. Với độ phân giải 640x480 và 16 bit trên mỗi pixel, dữ liệu điểm ảnh cho framebuffer hoàn toàn nằm gọn trong chưa đầy một megabyte bộ nhớ.

Vào giai đoạn đầu của quá trình khởi động, Mac OS X sử dụng địa chỉ framebuffer do bộ nạp khởi động (bootloader) cung cấp để hiển thị các đồ họa khởi động đơn giản thông qua video_console.c. Trong trường hợp khởi động ở chế độ verbose, các bitmap ký tự phông chữ được ghi vào framebuffer để tạo ra một nhật ký trực quan về những gì đang xảy ra trong quá trình khởi động. Khi hệ thống đã khởi động đủ xa, nó không thể sử dụng đoạn mã framebuffer ban đầu này nữa; desktop, window server, dock và tất cả các quy trình liên quan đến GUI khác tạo nên giao diện người dùng Aqua của Mac OS X đều yêu cầu một trình điều khiển framebuffer thực sự, hỗ trợ IOKit.

Để giải quyết trình điều khiển tiếp theo này, tôi đã kế thừa lớp IOFramebuffer. Tương tự như việc kế thừa IOBlockStorageDevice cho trình điều khiển thẻ SD, IOFramebuffer cũng có một vài phương thức trừu tượng mà lớp con framebuffer của tôi cần triển khai:

virtual class IODeviceMemory* getApertureRange(IOPixelAperture aperture);
virtual const char* getPixelFormats();
virtual IOItemCount getDisplayModeCount();
virtual IOReturn getDisplayModes(IODisplayModeID *);
virtual IOReturn getInformationForDisplayMode(long int, IODisplayModeInformation *);
virtual UInt64 getPixelFormatsForDisplayMode(long int, long int);
virtual IOReturn getPixelInformation(long int, long int, long int, IOPixelInformation *);
virtual IOReturn getCurrentDisplayMode(IODisplayModeID *, IOIndex *);
virtual IOReturn setGammaTable(UInt32, UInt32, UInt32, void *);
virtual IOReturn setDisplayMode(IODisplayModeID, IOIndex);
virtual IOReturn setApertureEnable(IOPixelAperture, IOOptionBits);
virtual IOReturn newUserClient(task_t, void *, UInt32, IOUserClient **);
virtual bool isConsoleDevice(void);

Một lần nữa, hầu hết các phương thức này rất dễ triển khai, chỉ đơn giản là trả về các giá trị tương thích với Wii đã được định nghĩa cứng để mô tả chính xác phần cứng. Một trong những phương thức quan trọng nhất cần triển khai là getApertureRange, phương thức này trả về một instance IODeviceMemory với địa chỉ cơ sở và kích thước mô tả vị trí của framebuffer trong bộ nhớ:

IODeviceMemory* NintendoWiiFramebuffer::getApertureRange(IOPixelAperture aperature)
{
  // 0x01700000, 640x480 resoluton, 2 bytes (16 bits) per pixel
  return IODeviceMemory::withRange(0x01700000, 640 * 480 * 2);
}

Sau khi trả về instance bộ nhớ thiết bị chính xác từ phương thức này, hệ thống đã có thể chuyển đổi từ framebuffer xuất văn bản trong quá trình khởi động sớm, sang một framebuffer có khả năng hiển thị toàn bộ giao diện GUI của Mac OS X. Tôi thậm chí đã có thể khởi động trình cài đặt Mac OS X:

Những độc giả tinh mắt có thể nhận thấy một vài vấn đề:

  • Framebuffer văn bản ở chế độ verbose vẫn đang hoạt động, khiến văn bản được hiển thị và framebuffer bị cuộn
  • Mọi thứ đều có màu đỏ tươi (magenta)

Cách sửa lỗi cho bảng điều khiển video khởi động sớm vẫn đang ghi đầu ra văn bản vào framebuffer rất đơn giản: thông báo cho hệ thống rằng framebuffer IOKit mới của chúng tôi giống với cái đã được sử dụng trước đó bằng cách trả về true từ isConsoleDevice:

bool NintendoWiiFramebuffer::isConsoleDevice(void)
{
  return true;
}

Trình cài đặt Mac OS X với màu sắc không chính xác

Việc sửa lỗi màu sắc không chính xác phức tạp hơn nhiều, vì nó liên quan đến sự không tương thích cơ bản giữa phần cứng video của Wii và mã đồ họa mà Mac OS X sử dụng.

Phần cứng bộ mã hóa video của Nintendo Wii được tối ưu hóa cho đầu ra tín hiệu TV analog, và do đó, mong đợi dữ liệu pixel YUV 16-bit trong framebuffer của nó. Đây là một vấn đề vì Mac OS X mong đợi framebuffer chứa dữ liệu pixel RGB. Nếu framebuffer mà Wii hiển thị chứa dữ liệu pixel không phải YUV, thì màu sắc sẽ hoàn toàn bị sai.

Để giải quyết sự không tương thích này, tôi đã lấy cảm hứng từ dự án Wii Linux, dự án đã giải quyết vấn đề này từ nhiều năm trước. Chiến lược là sử dụng hai framebuffer: một framebuffer RGB mà Mac OS X tương tác, và một framebuffer YUV mà phần cứng video của Wii xuất ra màn hình hiển thị. 60 lần mỗi giây, trình điều khiển framebuffer sẽ chuyển đổi dữ liệu pixel trong framebuffer RGB sang dữ liệu pixel YUV, đặt dữ liệu đã chuyển đổi vào framebuffer mà phần cứng video của Wii hiển thị:

Hệ thống framebuffer kép

Sau khi triển khai chiến lược framebuffer kép, tôi đã có thể khởi động vào một hệ thống Mac OS X có màu sắc chính xác - lần đầu tiên, Mac OS X đã chạy được trên một chiếc Nintendo Wii:

Trình cài đặt Mac OS X với màu sắc chính xác Đã khởi động vào màn hình desktop Mac OS X (Vâng, tôi đã mang chiếc Wii đi trong chuyến du lịch đến Hawaii - thật khó để tạm dừng một dự án khi bạn đang đứng trước ngưỡng cửa đạt được một cột mốc quan trọng!)

Hệ thống hiện đã khởi động đến tận màn hình desktop - nhưng có một vấn đề - tôi không có cách nào để tương tác với bất cứ thứ gì. Để đưa dự án này từ một bản demo kỹ thuật thành một hệ thống có thể sử dụng được, tôi cần thêm hỗ trợ cho bàn phím và chuột USB.

Thêm hỗ trợ USB

Để kích hoạt đầu vào bàn phím và chuột USB, tôi cần làm cho các cổng USB phía sau của Wii hoạt động trên Mac OS X - cụ thể là, tôi cần đưa bộ điều khiển máy chủ (host controller) USB 1.1 OHCI tốc độ thấp vào hoạt động. Hy vọng của tôi là sử dụng lại mã từ IOUSBFamily - một tập hợp các trình điều khiển USB giúp trừu tượng hóa phần lớn sự phức tạp khi giao tiếp với phần cứng USB. Trình điều khiển cụ thể mà tôi cần chạy là AppleUSBOHCI - một trình điều khiển xử lý việc giao tiếp với đúng loại bộ điều khiển máy chủ USB được sử dụng bởi Wii.

Hy vọng của tôi nhanh chóng biến thành sự thất vọng khi tôi gặp phải nhiều trở ngại.

Trở ngại 1:

Mã nguồn IOUSBFamily cho Mac OS X Cheetah và Puma, vì lý do nào đó, không nằm trong bộ sưu tập các bản phát hành mã nguồn mở toàn diện do Apple cung cấp. Điều này có nghĩa là khả năng gỡ lỗi các vấn đề hoặc sự không tương thích phần cứng của tôi sẽ bị hạn chế nghiêm trọng. Về cơ bản, nếu ngăn xếp USB không tự động hoạt động mà không cần điều chỉnh hay sửa đổi gì (bật mí: tất nhiên là nó không hoạt động), việc chẩn đoán vấn đề sẽ cực kỳ khó khăn nếu không có quyền truy cập vào mã nguồn.

Trở ngại 2:

AppleUSBOHCI không khớp với bất kỳ phần cứng nào trong cây thiết bị (device tree), và do đó không bắt đầu chạy, vì thuộc tính trình điều khiển (driver personality) của nó yêu cầu lớp nhà cung cấp (nub mà nó gắn vào) phải là IOPCIDevice. Như tôi đã tìm ra, Wii chắc chắn không sử dụng IOPCIFamily, nghĩa là các nub IOPCIDevice sẽ không bao giờ được tạo và AppleUSBOHCI sẽ không có gì để gắn vào.

Giải pháp của tôi để giải quyết vấn đề này là tạo một nub NintendoWiiHollywoodDevice mới, gọi là NintendoWiiHollywoodPCIDevice, kế thừa từ IOPCIDevice. Bằng cách để NintendoWiiHollywood xuất bản một nub kế thừa từ IOPCIDevice, và điều chỉnh thuộc tính trình điều khiển của AppleUSBOHCI trong tệp Info.plist của nó để sử dụng NintendoWiiHollywoodPCIDevice làm lớp nhà cung cấp, tôi có thể khiến nó khớp và bắt đầu chạy.

Để tìm hiểu cách AppleUSBOHCI sử dụng nub thiết bị PCI của nó, tôi đã sử dụng kết hợp ghi nhật ký thời gian chạy, phân tích dịch ngược và phân tích mã nguồn của IOUSBFamily trên Mac OS X 10.2 Jaguar (đây là những nguồn đầu tiên có sẵn từ Apple). Thật nhẹ nhõm, việc giao tiếp với phần cứng PCI thông qua nub thiết bị PCI bị hạn chế - điều chính mà AppleUSBOHCI cần từ phần cứng PCI là địa chỉ cơ sở của bộ điều khiển máy chủ USB - thứ mà nó truy xuất bằng cách sử dụng các lệnh PCI. Tôi đã có thể chặn các lệnh này trong nub PCI giả của mình và trả về địa chỉ cơ sở của phần cứng OHCI trên Wii.

Với những giải pháp thay thế này, AppleUSBOHCI hiện đã chạy - tuy nhiên, các cổng USB của tôi vẫn không phản hồi.

Trở ngại 3:

Khám phá tiếp theo của tôi là AppleUSBOHCI giả định thứ tự byte little-endian cho các thao tác đọc và ghi thanh ghi. Sau khi tìm hiểu, tôi biết được rằng đây thực tế là hành vi khá tiêu chuẩn cho phần cứng OHCI, ngay cả khi phần cứng máy chủ là hệ thống big-endian (như trường hợp của các hệ thống PowerPC như Wii và PowerPC Macs). Vậy tại sao điều này lại không hoạt động trên Wii?

Sự không tương thích nằm ở sự khác biệt trong cách IOUSBFamily và Wii xử lý các khác biệt về endianness giữa phần cứng USB và bộ xử lý chủ - trong trường hợp của IOUSBFamily, dữ liệu được hoán đổi byte bằng phần mềm khi làm việc với các thanh ghi OHCI, trong khi với Wii, dữ liệu được hoán đổi bằng phần cứng thông qua các đường truyền byte đã hoán đổi, khiến các thanh ghi OHCI tự động xuất hiện dưới dạng big-endian khi được đọc hoặc ghi. Hệ thống này trên Wii được gọi là reversed-little-endian.

Để giải quyết vấn đề này, tôi cần ngăn chặn "cú hoán đổi kép" đang xảy ra bằng cách loại bỏ việc hoán đổi byte bằng phần mềm mà IOUSBFamily đang thực hiện - nhưng nếu không có quyền truy cập vào mã nguồn, điều này sẽ không dễ dàng. Một lần nữa đối mặt với một vấn đề khó khăn khi đang đi du lịch, tôi đã dành vài giờ trên chuyến bay để sử dụng Ghidra nhằm tìm và vá các lệnh hoán đổi byte. Việc này nhanh chóng trở nên rắc rối, vì có nhiều nơi trong ngăn xếp USB có các trường hợp sử dụng hoán đổi byte hợp lệ và không nên bị gỡ bỏ.

Cuối cùng, tệp nhị phân AppleUSBOHCI mà tôi vá thủ công đã trở nên mong manh, gần như không thể chỉnh sửa và gần như chắc chắn là không chính xác. Không có gì ngạc nhiên khi nó không hoạt động và các cổng USB của tôi vẫn không phản hồi.

Đã giải tỏa tắc nghẽn:

Điều tôi thực sự cần là quyền truy cập vào mã nguồn IOUSBFamily cho Mac OS X Cheetah. Nếu có được điều đó, tôi có thể loại bỏ sự phụ thuộc vào IOPCIFamily, xóa tất cả các phần hoán đổi byte bằng phần mềm không cần thiết và hy vọng xây dựng được một bản fork hoạt động tốt với phần cứng của Wii.

Sau nhiều ngày tìm kiếm trên các diễn đàn cũ, duyệt các trang web trên Wayback Machine và cố gắng truy cập vào các máy chủ FTP cổ xưa, tôi quyết định quay lại một trong những nơi đầu tiên mà tôi từng sử dụng để yêu cầu trợ giúp trên internet: IRC.

Cuộc trò chuyện IRC để có quyền truy cập vào IOUSBFamily

Đúng như dự đoán, kho lưu trữ CVS mà @bbraun (của synack.net) cung cấp có mọi tệp cần thiết để xây dựng IOUSBFamily cho Mac OS X Cheetah. Nếu bạn đang đọc những dòng này - cảm ơn bạn đã giúp đỡ một người lạ trên internet :)

Với IOUSBFamily đã được vá và xây dựng từ mã nguồn, bàn phím và chuột USB của tôi đã có thể điều khiển hệ thống, biến Wii thành một máy tính Mac OS X có thể sử dụng được.

Làm cho mọi thứ tốt đẹp hơn ™

Cải thiện Bootloader

Để hỗ trợ trường hợp sử dụng quy trình cài đặt Mac OS X đầy đủ, tôi cần thêm hỗ trợ khởi động từ các phân vùng khác nhau trên cùng một thẻ SD (một cho trình cài đặt, một cho hệ thống đã cài đặt). Cách tiếp cận mà tôi thực hiện là cải tiến menu khởi động để liệt kê tất cả các phân vùng có thể khởi động, cho phép người dùng chọn phân vùng họ muốn khởi động bằng cách luân chuyển qua các tùy chọn có sẵn.

Để liệt kê các phân vùng có sẵn, tôi cần phân tích Apple Partition Map (APM) tại sector 1 của thẻ SD. Sau khi phân tích, tôi có thể lấy các offset, loại hệ thống tệp và tên của từng phân vùng trên đĩa:

Luân chuyển qua các phân vùng trong bootloader

Tiếp theo, tôi muốn thêm khả năng khởi động từ một phân vùng hệ thống và trình cài đặt chưa sửa đổi - loại bỏ nhu cầu thay thế trình điều khiển (driver) hoặc nhân (kernel) sau khi cài đặt hoặc cập nhật Mac OS X. Để thực hiện điều này, tôi cần làm cho bootloader, thay vì kernel, chịu trách nhiệm tải tất cả các trình điều khiển dành riêng cho Wii. Rất may, Mac OS X có hỗ trợ đưa các trình điều khiển được tải bởi bootloader vào thông qua node /chosen/memory-map của cây thiết bị (device tree). Node này chứa các mục cho từng trình điều khiển được tải bởi bootloader:

/
└── chosen
	└── memory-map
    	├── Driver-4d6000
      	├── Driver-4d7000
        ├── Driver-4d8000
        ├── Driver-4d9000
        ├── Driver-4da000
            etc.

Mỗi mục chứa một địa chỉ trỏ đến cấu trúc mục nhập trình điều khiển trong bộ nhớ:

typedef struct driver_info  {
    char *info_plist_start;
    u32 info_plist_size;
    void *bin_start;
    u32 bin_size;
} driver_info_t;

Bản thân nó chứa các con trỏ đến các tệp nhị phân trình điều khiển và các tệp Info.plist đã được tải vào bộ nhớ.

Để tải các trình điều khiển và xây dựng node memory-map của cây thiết bị, bootloader đệ quy tìm kiếm phân vùng hỗ trợ FAT32 cho bất kỳ gói mở rộng kernel (kext) nào, tải các cặp tệp nhị phân và Info.plist cho mỗi gói mà nó tìm thấy. Đây là một ví dụ về cấu trúc gói kext:

SomeDriver.kext
	└── Contents
    	├── Info.plist
        └── MacOS
        	└── SomeDriver
        └── PlugIns
        	└── SomeOtherDriver.kext

Sau khi triển khai việc tải trình điều khiển trong bootloader, giờ đây tôi có thể khởi động từ các phân vùng hệ thống và trình cài đặt Mac OS X chưa sửa đổi, đơn giản hóa quy trình cài đặt và làm cho Wii hoạt động giống như một chiếc Mac thực thụ hơn.

Đơn giản hóa Kernel

Bằng cách di chuyển các trình điều khiển ra khỏi kernel, số lượng các sửa đổi kernel cần thiết để hệ thống chạy trên Wii đã giảm xuống chỉ còn những mục sau:

  • Thiết lập BAT được vá cho địa chỉ I/O và bộ nhớ khung hình (framebuffer) của Wii
  • Hỗ trợ lấy địa chỉ cơ sở I/O từ một node cây thiết bị có tên là "hollywood"
  • Các bản sửa lỗi về tính nhất quán của bộ nhớ đệm framebuffer

Việc tách các trình điều khiển khỏi kernel giúp dễ dàng suy luận về kernel hơn, giảm thời gian xây dựng khi phát triển trình điều khiển và mở đường cho việc hỗ trợ các hệ thống như Mac OS X 10.1 Puma, vốn đã di chuyển một số dòng trình điều khiển ra khỏi kernel và đưa vào hệ thống tệp gốc.

Suy nghĩ cuối cùng

Có một cảm giác vô cùng thỏa mãn khi đạt được điều gì đó mà lúc bắt đầu, bạn thậm chí không chắc là mình có thể làm được hay không.

Tôi lần đầu có ý tưởng cho dự án này vào năm 2013 - khi tôi còn là sinh viên năm hai đại học. Trong hơn một thập kỷ, nó đã bị gác lại; rất dễ để trì hoãn một dự án như thế này, đặc biệt là khi công việc hàng ngày của bạn đã liên quan đến việc giải quyết các vấn đề kỹ thuật.

Năm ngoái, khi thấy Windows NT được port sang Wii, tôi cảm thấy động lực của mình như được khơi dậy. Ngay cả khi việc thiếu kinh nghiệm về low-level dẫn đến thất bại, thì việc thử sức với dự án này vẫn là một cơ hội để học hỏi những điều mới mẻ.

Cuối cùng, tôi đã học được (và đạt được) nhiều hơn những gì tôi từng mong đợi - và quan trọng hơn, tôi được nhắc nhở rằng những dự án tưởng chừng như nằm ngoài tầm với lại chính là những dự án đáng để theo đuổi.

Tháng 4 năm 2026

Tác giả: blkhp19

#discussion