Một vài năm trước Fabrice Bellard
- mã nguồn mở
- khả năng làm việc mà không cần trình điều khiển kernel
- khả năng làm việc ở chế độ phiên dịch
- hỗ trợ một số lượng lớn kiến trúc máy chủ và khách
Về điểm thứ ba, bây giờ tôi có thể giải thích rằng trên thực tế, trong chế độ TCI, không phải bản thân các lệnh của máy khách được diễn giải mà là mã byte thu được từ chúng, nhưng điều này không thay đổi bản chất - để xây dựng và chạy Qemu trên kiến trúc mới, nếu bạn may mắn, trình biên dịch AC là đủ - việc viết trình tạo mã có thể bị hoãn lại.
Và bây giờ, sau hai năm nhàn nhã mày mò mã nguồn Qemu trong thời gian rảnh rỗi, một nguyên mẫu hoạt động đã xuất hiện, trong đó bạn có thể chạy, chẳng hạn như Kolibri OS.
Emscripten là gì
Ngày nay có rất nhiều trình biên dịch đã xuất hiện và kết quả cuối cùng của nó là JavaScript. Một số, như Type Script, ban đầu được dự định là cách tốt nhất để viết cho web. Đồng thời, Emscripten là một cách lấy mã C hoặc C++ hiện có và biên dịch nó thành dạng có thể đọc được trên trình duyệt. TRÊN
Lần thử đầu tiên
Nói chung, tôi không phải là người đầu tiên nảy ra ý tưởng chuyển Qemu sang JavaScript. Đã có một câu hỏi được đặt ra trên diễn đàn ReactOS liệu điều này có thể thực hiện được bằng Emscripten hay không. Thậm chí trước đó, còn có tin đồn rằng Fabrice Bellard đã đích thân thực hiện việc này, nhưng chúng ta đang nói về jslinux, theo như tôi biết, chỉ là một nỗ lực nhằm đạt được hiệu suất đủ trong JS theo cách thủ công và được viết từ đầu. Sau đó, Virtual x86 đã được viết - các nguồn không bị xáo trộn đã được đăng cho nó và, như đã nêu, “tính chân thực” lớn hơn của mô phỏng đã giúp bạn có thể sử dụng SeaBIOS làm chương trình cơ sở. Ngoài ra, đã có ít nhất một lần thử chuyển Qemu bằng Emscripten - Tôi đã thử thực hiện việc này
Vì vậy, có vẻ như đây là nguồn, đây là Emscripten - hãy lấy nó và biên dịch. Nhưng cũng có những thư viện mà Qemu phụ thuộc và các thư viện mà các thư viện đó phụ thuộc, v.v., và một trong số đó là
Lúc đầu, có một ý tưởng đơn giản là viết một bản thay thế cho libffi bằng JS và chạy các bài kiểm tra tiêu chuẩn, nhưng cuối cùng tôi lại bối rối về cách tạo các tệp tiêu đề của mình để chúng hoạt động với mã hiện có - tôi có thể làm gì, như họ nói, “Có phải nhiệm vụ quá phức tạp không? "Chúng ta có ngu ngốc không?" Có thể nói, tôi đã phải chuyển libffi sang một kiến trúc khác - may mắn thay, Emscripten có cả macro để lắp ráp nội tuyến (trong Javascript, vâng - à, bất kể kiến trúc nào, vì vậy trình biên dịch mã) và khả năng chạy mã được tạo nhanh chóng. Nói chung, sau một thời gian mày mò các đoạn libffi phụ thuộc vào nền tảng, tôi đã nhận được một số mã có thể biên dịch được và chạy nó trong lần thử nghiệm đầu tiên mà tôi gặp. Thật ngạc nhiên, cuộc thử nghiệm đã thành công. Choáng váng trước thiên tài của tôi - không đùa đâu, nó đã hoạt động ngay từ lần ra mắt đầu tiên - Tôi, vẫn không tin vào mắt mình, đi xem lại mã kết quả, để đánh giá xem sẽ đào tiếp ở đâu. Ở đây tôi đã phát điên lần thứ hai - điều duy nhất chức năng của tôi làm là ffi_call
- nó báo cuộc gọi thành công. Không có cuộc gọi nào cả. Vì vậy, tôi đã gửi yêu cầu kéo đầu tiên của mình để sửa một lỗi trong bài kiểm tra mà bất kỳ học sinh Olympic nào cũng thấy rõ - không nên so sánh số thực như a == b
và thậm chí làm thế nào a - b < EPS
- bạn cũng cần nhớ mô-đun, nếu không 0 sẽ rất bằng 1/3... Nói chung, tôi đã nghĩ ra một cổng libffi nhất định, vượt qua các bài kiểm tra đơn giản nhất và với glib đó đã biên soạn - Tôi quyết định nó sẽ cần thiết, tôi sẽ bổ sung nó sau. Nhìn về phía trước, tôi sẽ nói rằng, hóa ra, trình biên dịch thậm chí không bao gồm hàm libffi trong mã cuối cùng.
Tuy nhiên, như tôi đã nói, có một số hạn chế và trong số việc sử dụng miễn phí các hành vi không xác định khác nhau, một tính năng khó chịu hơn đã bị ẩn - JavaScript theo thiết kế không hỗ trợ đa luồng với bộ nhớ dùng chung. Về nguyên tắc, điều này thường có thể được coi là một ý tưởng hay, nhưng không phải để chuyển mã có kiến trúc gắn liền với các luồng C. Nói chung, Firefox đang thử nghiệm hỗ trợ các nhân viên dùng chung và Emscripten có triển khai pthread cho họ, nhưng tôi không muốn phụ thuộc vào nó. Tôi đã phải từ từ loại bỏ đa luồng từ mã Qemu - nghĩa là tìm ra nơi các luồng đang chạy, di chuyển phần thân của vòng lặp đang chạy trong luồng này sang một hàm riêng biệt và gọi từng hàm đó từ vòng lặp chính.
Lần thử thứ hai
Tại một thời điểm nào đó, rõ ràng là vấn đề vẫn còn đó và việc dùng nạng đẩy mã một cách bừa bãi sẽ không mang lại điều gì tốt đẹp. Kết luận: chúng ta cần bằng cách nào đó hệ thống hóa quá trình thêm nạng. Vì vậy, phiên bản 2.4.1 mới ra mắt vào thời điểm đó đã được sử dụng (không phải 2.5.0, bởi vì, ai biết được, sẽ có những lỗi trong phiên bản mới chưa được phát hiện và tôi đã có đủ lỗi của riêng mình. ), và điều đầu tiên là viết lại nó một cách an toàn thread-posix.c
. Chà, nghĩa là an toàn: nếu ai đó cố gắng thực hiện một thao tác dẫn đến chặn, chức năng này sẽ được gọi ngay lập tức abort()
- tất nhiên, điều này không giải quyết được tất cả các vấn đề cùng một lúc, nhưng ít nhất nó cũng dễ chịu hơn là lặng lẽ nhận được dữ liệu không nhất quán.
Nói chung, các tùy chọn Emscripten rất hữu ích trong việc chuyển mã sang JS -s ASSERTIONS=1 -s SAFE_HEAP=1
- họ nắm bắt được một số loại hành vi không xác định, chẳng hạn như các cuộc gọi đến một địa chỉ không được căn chỉnh (hoàn toàn không nhất quán với mã cho các mảng được gõ như HEAP32[addr >> 2] = 1
) hoặc gọi một hàm với số lượng đối số không đúng.
Nhân tiện, lỗi căn chỉnh là một vấn đề riêng biệt. Như tôi đã nói, Qemu có một phần phụ trợ diễn giải “thoái hóa” để tạo mã TCI (trình thông dịch mã nhỏ), đồng thời để xây dựng và chạy Qemu trên một kiến trúc mới, nếu bạn may mắn, trình biên dịch C là đủ. "nếu bạn may mắn". Tôi đã không may mắn và hóa ra TCI sử dụng quyền truy cập không được phân bổ khi phân tích mã byte của nó. Nghĩa là, trên tất cả các loại kiến trúc ARM và các kiến trúc khác có quyền truy cập nhất thiết phải được cấp độ, Qemu sẽ biên dịch vì chúng có phần phụ trợ TCG bình thường tạo ra mã gốc, nhưng liệu TCI có hoạt động trên chúng hay không lại là một câu hỏi khác. Tuy nhiên, hóa ra, tài liệu TCI rõ ràng chỉ ra điều gì đó tương tự. Kết quả là, các lệnh gọi hàm để đọc không được căn chỉnh đã được thêm vào mã, được phát hiện trong một phần khác của Qemu.
Phá hủy đống
Kết quả là quyền truy cập không được phân bổ vào TCI đã được sửa, một vòng lặp chính được tạo ra lần lượt được gọi là bộ xử lý, RCU và một số thứ nhỏ khác. Và vì vậy tôi khởi chạy Qemu với tùy chọn -d exec,in_asm,out_asm
, có nghĩa là bạn cần phải cho biết khối mã nào đang được thực thi và tại thời điểm phát sóng để viết mã khách là gì, mã máy chủ đã trở thành gì (trong trường hợp này là mã byte). Nó khởi động, thực thi một số khối dịch, viết thông báo gỡ lỗi mà tôi đã để lại rằng RCU sẽ khởi động và... gặp sự cố abort()
bên trong một hàm free()
. Bằng cách mày mò với chức năng free()
Chúng tôi đã phát hiện ra rằng trong tiêu đề của khối heap, nằm ở tám byte trước bộ nhớ được phân bổ, thay vì kích thước khối hoặc thứ gì đó tương tự, lại có rác.
Phá hủy đống - thật dễ thương... Trong trường hợp như vậy, có một biện pháp khắc phục hữu ích - từ (nếu có thể) cùng một nguồn, tập hợp một tệp nhị phân gốc và chạy nó trong Valgrind. Sau một thời gian, hệ nhị phân đã sẵn sàng. Tôi khởi chạy nó với các tùy chọn tương tự - nó gặp sự cố ngay cả trong quá trình khởi tạo, trước khi thực sự đạt đến mức thực thi. Tất nhiên, điều đó thật khó chịu - rõ ràng là các nguồn không hoàn toàn giống nhau, điều này không có gì đáng ngạc nhiên, bởi vì cấu hình đã tìm ra các tùy chọn hơi khác nhau, nhưng tôi có Valgrind - trước tiên tôi sẽ sửa lỗi này, và sau đó, nếu tôi may mắn , bản gốc sẽ xuất hiện. Tôi đang chạy điều tương tự trong Valgrind... Y-y-y, y-y-y, uh-uh, nó đã bắt đầu, trải qua quá trình khởi tạo bình thường và vượt qua lỗi ban đầu mà không có một cảnh báo nào về việc truy cập bộ nhớ không chính xác, chưa kể đến việc bị ngã. Như người ta nói, cuộc sống đã không chuẩn bị cho tôi điều này - một chương trình gặp sự cố sẽ ngừng hoạt động khi khởi chạy dưới Walgrind. Những gì nó là một bí ẩn. Giả thuyết của tôi là khi ở gần hướng dẫn hiện tại sau sự cố trong quá trình khởi tạo, gdb đã hoạt động tốt memset
-a với một con trỏ hợp lệ bằng cách sử dụng một trong hai mmx
, hoặc xmm
registers, thì có lẽ đó là một loại lỗi căn chỉnh nào đó, mặc dù điều đó vẫn khó tin.
Được rồi, Valgrind dường như không giúp được gì ở đây. Và ở đây điều kinh tởm nhất đã bắt đầu - mọi thứ dường như thậm chí còn bắt đầu, nhưng lại gặp sự cố vì những lý do hoàn toàn không xác định do một sự kiện có thể đã xảy ra hàng triệu hướng dẫn trước đó. Trong một thời gian dài, thậm chí không rõ cách tiếp cận. Cuối cùng tôi vẫn phải ngồi xuống và gỡ lỗi. Việc in ra những gì tiêu đề được viết lại cho thấy nó không giống một con số mà giống như một loại dữ liệu nhị phân nào đó. Và, lạ thay, chuỗi nhị phân này đã được tìm thấy trong tệp BIOS - nghĩa là, bây giờ có thể nói một cách chắc chắn rằng đó là lỗi tràn bộ đệm và thậm chí còn rõ ràng rằng nó đã được ghi vào bộ đệm này. Chà, đại loại như thế này - trong Emscripten, may mắn thay, không có sự ngẫu nhiên của không gian địa chỉ, cũng không có lỗ hổng trong đó, vì vậy bạn có thể viết ở đâu đó ở giữa mã để xuất dữ liệu bằng con trỏ từ lần khởi chạy trước, nhìn vào dữ liệu, nhìn vào con trỏ và nếu nó không thay đổi, hãy lấy thức ăn để suy nghĩ. Đúng, phải mất vài phút để liên kết sau bất kỳ thay đổi nào, nhưng bạn có thể làm gì? Kết quả là, một dòng cụ thể đã được tìm thấy đã sao chép BIOS từ bộ đệm tạm thời sang bộ nhớ khách - và thực sự là không có đủ dung lượng trong bộ đệm. Tìm nguồn của địa chỉ bộ đệm lạ đó dẫn đến một hàm qemu_anon_ram_alloc
trong tập tin oslib-posix.c
- logic là thế này: đôi khi việc căn chỉnh địa chỉ thành một trang lớn có kích thước 2 MB có thể hữu ích, vì điều này chúng tôi sẽ yêu cầu mmap
đầu tiên là thêm một chút, sau đó chúng tôi sẽ trả lại phần thừa với sự trợ giúp munmap
. Và nếu không cần căn chỉnh như vậy thì chúng tôi sẽ chỉ ra kết quả thay vì 2 MB getpagesize()
- mmap
nó vẫn sẽ đưa ra một địa chỉ phù hợp... Vì vậy, trong Emscripten mmap
chỉ cần gọi malloc
, nhưng tất nhiên là nó không căn chỉnh trên trang. Nói chung, một lỗi khiến tôi thất vọng trong vài tháng đã được sửa chữa bằng một sự thay đổi trong hai dòng.
Đặc điểm của chức năng gọi
Và bây giờ bộ xử lý đang đếm thứ gì đó, Qemu không gặp sự cố, nhưng màn hình không bật và bộ xử lý nhanh chóng đi vào các vòng lặp, đánh giá bằng đầu ra -d exec,in_asm,out_asm
. Một giả thuyết đã xuất hiện: các ngắt hẹn giờ (hoặc nói chung là tất cả các ngắt) không đến. Và thực sự, nếu bạn tháo các ngắt khỏi hội đồng gốc, vì lý do nào đó đã hoạt động, bạn sẽ có được một bức tranh tương tự. Nhưng đây hoàn toàn không phải là câu trả lời: so sánh các dấu vết được đưa ra với tùy chọn trên cho thấy quỹ đạo thực hiện đã chuyển hướng từ rất sớm. Ở đây phải nói rằng so sánh những gì được ghi lại bằng launcher emrun
đầu ra gỡ lỗi với đầu ra của hợp ngữ gốc không phải là một quá trình hoàn toàn cơ học. Tôi không biết chính xác cách một chương trình chạy trên trình duyệt kết nối với emrun
, nhưng một số dòng ở đầu ra hóa ra đã được sắp xếp lại, do đó, sự khác biệt về độ khác biệt vẫn chưa phải là lý do để cho rằng các quỹ đạo đã phân kỳ. Nói chung, rõ ràng là theo hướng dẫn ljmpl
có sự chuyển đổi sang các địa chỉ khác nhau và mã byte được tạo về cơ bản là khác nhau: một địa chỉ chứa lệnh gọi hàm trợ giúp, địa chỉ kia thì không. Sau khi tìm kiếm các hướng dẫn trên Google và nghiên cứu mã dịch các hướng dẫn này, trước tiên, rõ ràng là ngay trước nó trong sổ đăng ký cr0
một bản ghi đã được thực hiện - cũng sử dụng một trình trợ giúp - đã chuyển bộ xử lý sang chế độ được bảo vệ và thứ hai, phiên bản js không bao giờ chuyển sang chế độ được bảo vệ. Nhưng thực tế là một tính năng khác của Emscripten là nó miễn cưỡng chấp nhận mã chẳng hạn như việc thực hiện các hướng dẫn call
trong TCI, bất kỳ con trỏ hàm nào cũng dẫn đến kiểu long long f(int arg0, .. int arg9)
- hàm phải được gọi với số lượng đối số chính xác. Nếu quy tắc này bị vi phạm, tùy thuộc vào cài đặt gỡ lỗi, chương trình sẽ gặp sự cố (điều này tốt) hoặc gọi sai chức năng (điều này sẽ rất buồn khi gỡ lỗi). Ngoài ra còn có một tùy chọn thứ ba - cho phép tạo các trình bao bọc để thêm / xóa đối số, nhưng nhìn chung các trình bao bọc này chiếm rất nhiều dung lượng, mặc dù thực tế là trên thực tế tôi chỉ cần hơn một trăm trình bao bọc một chút. Riêng điều này đã rất đáng buồn, nhưng hóa ra lại có một vấn đề nghiêm trọng hơn: trong mã được tạo của các hàm bao bọc, các đối số đã được chuyển đổi và chuyển đổi, nhưng đôi khi hàm với các đối số được tạo ra không được gọi - à, giống như trong việc triển khai libffi của tôi. Đó là, một số người trợ giúp đơn giản là không bị xử tử.
May mắn thay, Qemu có danh sách người trợ giúp có thể đọc được bằng máy dưới dạng tệp tiêu đề như
DEF_HELPER_0(lock, void)
DEF_HELPER_0(unlock, void)
DEF_HELPER_3(write_eflags, void, env, tl, i32)
Chúng được sử dụng khá buồn cười: đầu tiên, macro được định nghĩa lại theo cách kỳ quái nhất DEF_HELPER_n
, rồi bật helper.h
. Trong phạm vi macro được mở rộng thành bộ khởi tạo cấu trúc và dấu phẩy, sau đó một mảng được xác định và thay vì các phần tử - #include <helper.h>
Kết quả là cuối cùng tôi đã có cơ hội dùng thử thư viện tại nơi làm việc
Và như vậy, sau đó bộ xử lý dường như đã hoạt động. Có vẻ như là do màn hình chưa bao giờ được khởi tạo, mặc dù memtest86+ có thể chạy trong phiên bản gốc. Ở đây cần phải làm rõ rằng mã I/O khối Qemu được viết bằng coroutine. Emscripten có cách triển khai rất phức tạp, nhưng nó vẫn cần được hỗ trợ trong mã Qemu và bạn có thể gỡ lỗi bộ xử lý ngay bây giờ: Qemu hỗ trợ các tùy chọn -kernel
, -initrd
, -append
, nhờ đó bạn có thể khởi động Linux hoặc, ví dụ: memtest86+ mà không cần sử dụng các thiết bị chặn. Nhưng đây là vấn đề: trong hợp ngữ gốc, người ta có thể thấy nhân Linux xuất ra bảng điều khiển với tùy chọn -nographic
và không có đầu ra từ trình duyệt đến thiết bị đầu cuối nơi nó được khởi chạy emrun
, đã không đến. Tức là không rõ ràng: bộ xử lý không hoạt động hoặc đầu ra đồ họa không hoạt động. Và rồi tôi chợt nghĩ phải đợi một chút. Hóa ra là “bộ xử lý không ngủ mà chỉ nhấp nháy chậm” và sau khoảng năm phút, kernel ném một loạt thông báo lên bảng điều khiển và tiếp tục treo. Rõ ràng là bộ xử lý nói chung hoạt động và chúng ta cần tìm hiểu mã để làm việc với SDL2. Thật không may, tôi không biết cách sử dụng thư viện này nên ở một số chỗ tôi phải hành động tùy tiện. Tại một thời điểm nào đó, dòng song song0 nhấp nháy trên màn hình trên nền xanh lam, điều này gợi lên một số suy nghĩ. Cuối cùng, vấn đề là Qemu mở một số cửa sổ ảo trong một cửa sổ vật lý, giữa các cửa sổ đó bạn có thể chuyển đổi bằng Ctrl-Alt-n: nó hoạt động trong bản dựng gốc, nhưng không hoạt động trong Emscripten. Sau khi loại bỏ các cửa sổ không cần thiết bằng cách sử dụng tùy chọn -monitor none -parallel none -serial none
và hướng dẫn vẽ lại toàn bộ màn hình trên từng khung hình một cách bắt buộc, mọi thứ đột nhiên hoạt động.
quân đoàn
Vì vậy, mô phỏng trong trình duyệt hoạt động, nhưng bạn không thể chạy bất kỳ đĩa mềm thú vị nào trong đó vì không có khối I/O - bạn cần triển khai hỗ trợ cho coroutine. Qemu đã có một số chương trình phụ trợ coroutine, nhưng do tính chất của JavaScript và trình tạo mã Emscripten, bạn không thể bắt đầu sắp xếp các ngăn xếp. Có vẻ như “mọi thứ đã biến mất, lớp thạch cao đang được gỡ bỏ,” nhưng các nhà phát triển Emscripten đã lo liệu mọi việc. Điều này được thực hiện khá buồn cười: hãy gọi một lệnh gọi hàm như thế này đáng ngờ emscripten_sleep
và một số cơ chế khác sử dụng cơ chế Asyncify, cũng như các lệnh gọi con trỏ và lệnh gọi tới bất kỳ hàm nào trong đó một trong hai trường hợp trước có thể xảy ra sâu hơn trong ngăn xếp. Và bây giờ, trước mỗi cuộc gọi đáng ngờ, chúng tôi sẽ chọn bối cảnh không đồng bộ và ngay sau cuộc gọi, chúng tôi sẽ kiểm tra xem cuộc gọi không đồng bộ có xảy ra hay không và nếu có, chúng tôi sẽ lưu tất cả các biến cục bộ trong bối cảnh không đồng bộ này, cho biết chức năng nào để chuyển quyền điều khiển sang thời điểm chúng ta cần tiếp tục thực thi và thoát khỏi hàm hiện tại. Đây là nơi có phạm vi để nghiên cứu tác động -O3
. Tôi chạy mã được tạo và Chrome ngốn bộ nhớ và gặp sự cố. Sau đó, tôi vô tình nhìn vào những gì anh ấy đang cố tải xuống... Chà, tôi có thể nói gì đây, tôi cũng sẽ chết lặng nếu được yêu cầu nghiên cứu kỹ lưỡng và tối ưu hóa Javascript hơn 500 MB.
Thật không may, việc kiểm tra mã thư viện hỗ trợ Asyncify không hoàn toàn thân thiện với longjmp
-s được sử dụng trong mã bộ xử lý ảo, nhưng sau một bản vá nhỏ vô hiệu hóa các kiểm tra này và khôi phục mạnh mẽ các bối cảnh như thể mọi thứ đều ổn, mã đã hoạt động. Và sau đó, một điều kỳ lạ bắt đầu: đôi khi việc kiểm tra mã đồng bộ hóa được kích hoạt - những kiểm tra tương tự làm hỏng mã nếu theo logic thực thi, nó phải bị chặn - ai đó đã cố lấy một mutex đã bị bắt. May mắn thay, điều này hóa ra không phải là một vấn đề logic trong mã được xê-ri hóa - tôi chỉ đơn giản sử dụng chức năng vòng lặp chính tiêu chuẩn do Emscripten cung cấp, nhưng đôi khi lệnh gọi không đồng bộ sẽ mở hoàn toàn ngăn xếp và tại thời điểm đó nó sẽ thất bại setTimeout
từ vòng lặp chính - do đó, mã đã đi vào vòng lặp chính mà không rời khỏi lần lặp trước đó. Viết lại trên một vòng lặp vô hạn và emscripten_sleep
và các vấn đề với mutexes đã dừng lại. Mã thậm chí còn trở nên hợp lý hơn - trên thực tế, tôi không có mã nào chuẩn bị cho khung hình động tiếp theo - bộ xử lý chỉ tính toán một cái gì đó và màn hình được cập nhật định kỳ. Tuy nhiên, vấn đề không dừng lại ở đó: đôi khi việc thực thi Qemu sẽ kết thúc một cách âm thầm mà không có bất kỳ ngoại lệ hoặc lỗi nào. Vào thời điểm đó, tôi đã từ bỏ nó, nhưng nhìn về phía trước, tôi sẽ nói rằng vấn đề là ở chỗ: trên thực tế, mã coroutine không sử dụng setTimeout
(hoặc ít nhất là không thường xuyên như bạn nghĩ): chức năng emscripten_yield
chỉ cần đặt cờ cuộc gọi không đồng bộ. Toàn bộ vấn đề là ở chỗ đó emscripten_coroutine_next
không phải là một hàm không đồng bộ: bên trong nó kiểm tra cờ, đặt lại cờ và chuyển quyền điều khiển đến nơi cần thiết. Tức là việc thăng hạng ngăn xếp kết thúc ở đó. Vấn đề là do use-after-free, xuất hiện khi nhóm coroutine bị vô hiệu hóa do tôi không sao chép một dòng mã quan trọng từ chương trình phụ trợ coroutine hiện có, hàm qemu_in_coroutine
trả về true trong khi thực tế nó phải trả về false. Điều này dẫn đến một cuộc gọi emscripten_yield
, ở trên đó không có ai trên ngăn xếp emscripten_coroutine_next
, ngăn xếp được mở lên trên cùng, nhưng không setTimeout
, như tôi đã nói, không được trưng bày.
Tạo mã JavaScript
Và trên thực tế, đây là lời hứa “trả lại thịt băm”. Không thực sự. Tất nhiên, nếu chúng ta chạy Qemu trong trình duyệt và Node.js trong đó, thì đương nhiên, sau khi tạo mã trong Qemu, chúng ta sẽ nhận được JavaScript hoàn toàn sai. Tuy nhiên, vẫn có một số loại chuyển đổi ngược lại.
Đầu tiên, một chút về cách hoạt động của Qemu. Hãy tha thứ cho tôi ngay: Tôi không phải là nhà phát triển Qemu chuyên nghiệp và kết luận của tôi có thể sai ở một số chỗ. Như người ta nói, “ý kiến của học sinh không nhất thiết phải trùng khớp với ý kiến của giáo viên, các tiên đề và lẽ thường của Peano”. Qemu có một số kiến trúc khách được hỗ trợ nhất định và mỗi kiến trúc có một thư mục như target-i386
. Khi xây dựng, bạn có thể chỉ định hỗ trợ cho một số kiến trúc khách, nhưng kết quả sẽ chỉ là một số tệp nhị phân. Ngược lại, mã để hỗ trợ kiến trúc khách sẽ tạo ra một số hoạt động Qemu nội bộ mà TCG (Trình tạo mã nhỏ) đã biến thành mã máy cho kiến trúc máy chủ. Như đã nêu trong tệp readme nằm trong thư mục tcg, đây ban đầu là một phần của trình biên dịch C thông thường, sau này được điều chỉnh cho phù hợp với JIT. Do đó, ví dụ, kiến trúc đích theo tài liệu này không còn là kiến trúc khách nữa mà là kiến trúc máy chủ. Tại một thời điểm nào đó, một thành phần khác xuất hiện - Trình thông dịch mã nhỏ (TCI), sẽ thực thi mã (gần như các hoạt động nội bộ giống nhau) trong trường hợp không có trình tạo mã cho một kiến trúc máy chủ cụ thể. Trên thực tế, như tài liệu của nó nêu rõ, trình thông dịch này không phải lúc nào cũng hoạt động tốt như trình tạo mã JIT, không chỉ về mặt định lượng về tốc độ mà còn về mặt chất lượng. Mặc dù tôi không chắc mô tả của anh ấy có hoàn toàn phù hợp hay không.
Lúc đầu, tôi đã cố gắng tạo một chương trình phụ trợ TCG đầy đủ, nhưng nhanh chóng bị nhầm lẫn về mã nguồn và mô tả không hoàn toàn rõ ràng về hướng dẫn mã byte, vì vậy tôi quyết định bọc trình thông dịch TCI. Điều này đã mang lại một số lợi thế:
- khi triển khai trình tạo mã, bạn không thể xem mô tả hướng dẫn mà xem mã trình thông dịch
- bạn có thể tạo các hàm không phải cho mọi khối dịch gặp phải, nhưng, chẳng hạn, chỉ sau lần thực thi thứ một trăm
- nếu mã được tạo thay đổi (và điều này dường như có thể xảy ra, dựa trên các hàm có tên chứa bản vá từ), tôi sẽ cần vô hiệu hóa mã JS được tạo, nhưng ít nhất tôi sẽ có thứ gì đó để tạo lại nó từ đó.
Về điểm thứ ba, tôi không chắc có thể vá lỗi sau khi mã được thực thi lần đầu tiên, nhưng hai điểm đầu tiên là đủ.
Ban đầu, mã được tạo dưới dạng một công tắc lớn tại địa chỉ của lệnh bytecode ban đầu, nhưng sau đó, nhớ đến bài viết về Emscripten, tối ưu hóa JS được tạo và lặp lại, tôi quyết định tạo thêm mã con người, đặc biệt là vì theo kinh nghiệm nó hóa ra điểm vào duy nhất của khối dịch là Bắt đầu. Nói sớm hơn làm, sau một thời gian, chúng tôi đã có một trình tạo mã tạo mã bằng if (mặc dù không có vòng lặp). Nhưng thật không may, nó bị hỏng, đưa ra một thông báo rằng các hướng dẫn có độ dài không chính xác. Hơn nữa, lệnh cuối cùng ở mức đệ quy này là brcond
. Được rồi, tôi sẽ thêm một kiểm tra giống hệt vào việc tạo lệnh này trước và sau lệnh gọi đệ quy và... không một lệnh gọi nào trong số chúng được thực thi, nhưng sau khi chuyển đổi xác nhận, chúng vẫn không thành công. Cuối cùng, sau khi nghiên cứu mã được tạo, tôi nhận ra rằng sau khi chuyển đổi, con trỏ tới lệnh hiện tại sẽ được tải lại từ ngăn xếp và có thể bị ghi đè bởi mã JavaScript được tạo. Và hóa ra là vậy. Việc tăng bộ đệm từ một megabyte lên mười megabyte không dẫn đến kết quả gì và rõ ràng là trình tạo mã đang chạy theo vòng tròn. Chúng tôi phải kiểm tra xem chúng tôi có vượt quá ranh giới của TB hiện tại không, nếu có thì cấp địa chỉ của TB tiếp theo bằng dấu trừ để chúng tôi có thể tiếp tục thực hiện. Ngoài ra, điều này còn giải quyết được vấn đề “hàm nào được tạo sẽ bị vô hiệu nếu đoạn mã byte này thay đổi?” — chỉ có chức năng tương ứng với khối dịch này mới cần bị vô hiệu hóa. Nhân tiện, mặc dù tôi đã sửa lỗi mọi thứ trong Chrome (vì tôi sử dụng Firefox và tôi sử dụng một trình duyệt riêng để thử nghiệm dễ dàng hơn), Firefox đã giúp tôi khắc phục những điểm không tương thích với tiêu chuẩn asm.js, sau đó mã bắt đầu hoạt động nhanh hơn trong Crôm.
Ví dụ về mã được tạo
Compiling 0x15b46d0:
CompiledTB[0x015b46d0] = function(stdlib, ffi, heap) {
"use asm";
var HEAP8 = new stdlib.Int8Array(heap);
var HEAP16 = new stdlib.Int16Array(heap);
var HEAP32 = new stdlib.Int32Array(heap);
var HEAPU8 = new stdlib.Uint8Array(heap);
var HEAPU16 = new stdlib.Uint16Array(heap);
var HEAPU32 = new stdlib.Uint32Array(heap);
var dynCall_iiiiiiiiiii = ffi.dynCall_iiiiiiiiiii;
var getTempRet0 = ffi.getTempRet0;
var badAlignment = ffi.badAlignment;
var _i64Add = ffi._i64Add;
var _i64Subtract = ffi._i64Subtract;
var Math_imul = ffi.Math_imul;
var _mul_unsigned_long_long = ffi._mul_unsigned_long_long;
var execute_if_compiled = ffi.execute_if_compiled;
var getThrew = ffi.getThrew;
var abort = ffi.abort;
var qemu_ld_ub = ffi.qemu_ld_ub;
var qemu_ld_leuw = ffi.qemu_ld_leuw;
var qemu_ld_leul = ffi.qemu_ld_leul;
var qemu_ld_beuw = ffi.qemu_ld_beuw;
var qemu_ld_beul = ffi.qemu_ld_beul;
var qemu_ld_beq = ffi.qemu_ld_beq;
var qemu_ld_leq = ffi.qemu_ld_leq;
var qemu_st_b = ffi.qemu_st_b;
var qemu_st_lew = ffi.qemu_st_lew;
var qemu_st_lel = ffi.qemu_st_lel;
var qemu_st_bew = ffi.qemu_st_bew;
var qemu_st_bel = ffi.qemu_st_bel;
var qemu_st_leq = ffi.qemu_st_leq;
var qemu_st_beq = ffi.qemu_st_beq;
function tb_fun(tb_ptr, env, sp_value, depth) {
tb_ptr = tb_ptr|0;
env = env|0;
sp_value = sp_value|0;
depth = depth|0;
var u0 = 0, u1 = 0, u2 = 0, u3 = 0, result = 0;
var r0 = 0, r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0, r7 = 0, r8 = 0, r9 = 0;
var r10 = 0, r11 = 0, r12 = 0, r13 = 0, r14 = 0, r15 = 0, r16 = 0, r17 = 0, r18 = 0, r19 = 0;
var r20 = 0, r21 = 0, r22 = 0, r23 = 0, r24 = 0, r25 = 0, r26 = 0, r27 = 0, r28 = 0, r29 = 0;
var r30 = 0, r31 = 0, r41 = 0, r42 = 0, r43 = 0, r44 = 0;
r14 = env|0;
r15 = sp_value|0;
START: do {
r0 = HEAPU32[((r14 + (-4))|0) >> 2] | 0;
r42 = 0;
result = ((r0|0) != (r42|0))|0;
HEAPU32[1445307] = r0;
HEAPU32[1445321] = r14;
if(result|0) {
HEAPU32[1445322] = r15;
return 0x0345bf93|0;
}
r0 = HEAPU32[((r14 + (16))|0) >> 2] | 0;
r42 = 8;
r0 = ((r0|0) - (r42|0))|0;
HEAPU32[(r14 + (16)) >> 2] = r0;
r1 = 8;
HEAPU32[(r14 + (44)) >> 2] = r1;
r1 = r0|0;
HEAPU32[(r14 + (40)) >> 2] = r1;
r42 = 4;
r0 = ((r0|0) + (r42|0))|0;
r2 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
HEAPU32[1445307] = r0;
HEAPU32[1445308] = r1;
HEAPU32[1445309] = r2;
HEAPU32[1445321] = r14;
HEAPU32[1445322] = r15;
qemu_st_lel(env|0, r0|0, r2|0, 34, 22759218);
if(getThrew() | 0) abort();
r0 = 3241038392;
HEAPU32[1445307] = r0;
r0 = qemu_ld_leul(env|0, r0|0, 34, 22759233)|0;
if(getThrew() | 0) abort();
HEAPU32[(r14 + (24)) >> 2] = r0;
r1 = HEAPU32[((r14 + (12))|0) >> 2] | 0;
r2 = HEAPU32[((r14 + (40))|0) >> 2] | 0;
HEAPU32[1445307] = r0;
HEAPU32[1445308] = r1;
HEAPU32[1445309] = r2;
qemu_st_lel(env|0, r2|0, r1|0, 34, 22759265);
if(getThrew() | 0) abort();
r0 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
HEAPU32[(r14 + (40)) >> 2] = r0;
r1 = 24;
HEAPU32[(r14 + (52)) >> 2] = r1;
r42 = 0;
result = ((r0|0) == (r42|0))|0;
if(result|0) {
HEAPU32[1445307] = r0;
HEAPU32[1445308] = r1;
}
HEAPU32[1445307] = r0;
HEAPU32[1445308] = r1;
return execute_if_compiled(22759392|0, env|0, sp_value|0, depth|0) | 0;
return execute_if_compiled(23164080|0, env|0, sp_value|0, depth|0) | 0;
break;
} while(1); abort(); return 0|0;
}
return {tb_fun: tb_fun};
}(window, CompilerFFI, Module.buffer)["tb_fun"]
Kết luận
Vì vậy, công việc vẫn chưa hoàn thành nhưng tôi cũng mệt mỏi khi bí mật đưa công trình lâu đời này đến mức hoàn hảo. Vì vậy, tôi quyết định xuất bản những gì tôi có bây giờ. Mã này đôi khi hơi đáng sợ vì đây là một thử nghiệm và không rõ trước những gì cần phải làm. Có lẽ, đáng để ban hành các cam kết nguyên tử thông thường trên một số phiên bản Qemu hiện đại hơn. Trong khi đó, có một chủ đề trong Gita ở dạng blog: đối với mỗi “cấp độ” ít nhất đã được vượt qua bằng cách nào đó, một bình luận chi tiết bằng tiếng Nga đã được thêm vào. Trên thực tế, bài viết này phần lớn là kể lại kết luận git log
.
Bạn có thể thử tất cả
Những gì đã hoạt động:
- bộ xử lý ảo x86 đang chạy
- Có một nguyên mẫu hoạt động của trình tạo mã JIT từ mã máy sang JavaScript
- Có một mẫu để tập hợp các kiến trúc khách 32-bit khác: ngay bây giờ bạn có thể chiêm ngưỡng Linux về kiến trúc MIPS bị đóng băng trong trình duyệt ở giai đoạn tải
Bạn còn có thể làm gì khác nữa không
- Tăng tốc độ mô phỏng. Ngay cả trong chế độ JIT, nó dường như chạy chậm hơn Virtual x86 (nhưng có khả năng có cả một Qemu với nhiều phần cứng và kiến trúc được mô phỏng)
- Để tạo một giao diện bình thường - thành thật mà nói, tôi không phải là một nhà phát triển web giỏi, vì vậy hiện tại tôi đã làm lại trình bao Emscripten tiêu chuẩn một cách tốt nhất có thể
- Hãy thử khởi chạy các chức năng Qemu phức tạp hơn - kết nối mạng, di chuyển VM, v.v.
- UPD: bạn sẽ cần gửi một số phát triển và báo cáo lỗi của mình cho Emscripten ngược dòng, giống như những người porter trước đây của Qemu và các dự án khác đã làm. Cảm ơn họ vì đã có thể ngầm sử dụng sự đóng góp của họ cho Emscripten như một phần nhiệm vụ của tôi.
Nguồn: www.habr.com