Qemu.js có hỗ trợ JIT: bạn vẫn có thể lật ngược thịt băm

Một vài năm trước Fabrice Bellard được viết bởi jslinux là trình giả lập PC được viết bằng JavaScript. Sau đó ít nhất có thêm ảo x86. Nhưng theo như tôi biết, tất cả họ đều là thông dịch viên, trong khi Qemu, được viết trước đó nhiều bởi cùng một Fabrice Bellard, và có lẽ, bất kỳ trình giả lập hiện đại nào có uy tín, đều sử dụng trình biên dịch JIT của mã khách thành mã hệ thống máy chủ. Đối với tôi, có vẻ như đã đến lúc thực hiện nhiệm vụ ngược lại liên quan đến nhiệm vụ mà trình duyệt giải quyết: biên dịch JIT mã máy thành JavaScript, điều mà có vẻ hợp lý nhất là chuyển Qemu. Có vẻ như, tại sao lại là Qemu, có những trình giả lập đơn giản và thân thiện với người dùng hơn - chẳng hạn như VirtualBox tương tự - đã được cài đặt và hoạt động. Nhưng Qemu có một số tính năng thú vị

  • 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 trang này Chúng tôi đã thu thập được nhiều cổng của các chương trình nổi tiếng: đâyVí dụ: bạn có thể xem PyPy - nhân tiện, họ tuyên bố đã có JIT. Trên thực tế, không phải mọi chương trình đều có thể được biên dịch và chạy đơn giản trên trình duyệt - có một số Tính năng, đặc điểm, tuy nhiên, điều mà bạn phải chấp nhận, vì dòng chữ trên cùng một trang cho biết “Emscripten có thể được sử dụng để biên dịch hầu hết mọi thứ di động Mã C/C++ thành JavaScript". Nghĩa là, có một số thao tác có hành vi không xác định theo tiêu chuẩn, nhưng thường hoạt động trên x86 - ví dụ: quyền truy cập không được phân bổ vào các biến, thường bị cấm trên một số kiến ​​trúc. Nói chung , Qemu là một chương trình đa nền tảng và tôi muốn tin rằng nó chưa chứa nhiều hành vi không xác định - hãy lấy nó và biên dịch, sau đó mày mò một chút với JIT - và bạn đã hoàn thành! Nhưng đó không phải là trường hợp...

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 ổ cắm, nhưng sự phát triển, theo như tôi hiểu, đã bị đóng băng.

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à libffi, độ trượt phụ thuộc vào điều gì. Có tin đồn trên Internet rằng có một cái trong bộ sưu tập lớn các cổng thư viện dành cho Emscripten, nhưng thật khó tin: thứ nhất, nó không nhằm mục đích trở thành một trình biên dịch mới, thứ hai, nó quá thấp. thư viện để chọn và biên dịch sang JS. Và đó không chỉ là vấn đề về các phần chèn lắp ráp - có thể, nếu bạn vặn nó, đối với một số quy ước gọi, bạn có thể tạo các đối số cần thiết trên ngăn xếp và gọi hàm mà không cần chúng. Nhưng Emscripten là một điều phức tạp: để làm cho mã được tạo trông quen thuộc với trình tối ưu hóa công cụ JS của trình duyệt, một số thủ thuật sẽ được sử dụng. Cụ thể, cái gọi là vòng lặp lại - một trình tạo mã sử dụng LLVM IR nhận được với một số hướng dẫn chuyển tiếp trừu tượng sẽ cố gắng tạo lại các if, vòng lặp hợp lý, v.v. Vâng, các đối số được truyền vào hàm như thế nào? Đương nhiên, với tư cách là đối số của các hàm JS, nghĩa là, nếu có thể, không phải thông qua ngăn xếp.

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 phân tích cú phápvà một tập lệnh đã được viết để tạo ra chính xác các trình bao bọc đó cho chính xác các chức năng mà chúng cần.

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 -nographicvà 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 phung phí — đối với nhu cầu tiếp tục thực thi mã sau khi quay về từ một lệnh gọi không đồng bộ, trình biên dịch sẽ tạo ra các “sơ khai” của hàm bắt đầu sau một lệnh gọi đáng ngờ - như thế này: nếu có n lệnh gọi đáng ngờ thì hàm sẽ được mở rộng ở đâu đó n/2 lần — điều này vẫn xảy ra, nếu không. Hãy nhớ rằng sau mỗi lệnh gọi có khả năng không đồng bộ, bạn cần thêm việc lưu một số biến cục bộ vào hàm ban đầu. Sau đó, tôi thậm chí còn phải viết một tập lệnh đơn giản bằng Python, dựa trên một tập hợp các hàm đặc biệt bị lạm dụng nhất định mà được cho là “không cho phép sự không đồng bộ tự đi qua” (nghĩa là, khuyến mãi ngăn xếp và mọi thứ mà tôi vừa mô tả không hoạt động trong chúng), biểu thị các cuộc gọi thông qua các con trỏ trong đó các hàm nên được trình biên dịch bỏ qua để các hàm này không bị coi là không đồng bộ. Và khi đó, các tệp JS dưới 60 MB rõ ràng là quá nhiều - giả sử ít nhất là 30. Mặc dù, có lần tôi đang thiết lập một tập lệnh hợp ngữ và vô tình loại bỏ các tùy chọn trình liên kết, trong số đó có -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_sleepvà 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ả đây (hãy cẩn thận với giao thông).

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

Thêm một lời nhận xét