QEMU.js: hiện đang nghiêm túc và có WASM

Ngày xửa ngày xưa tôi quyết định cho vui chứng minh tính thuận nghịch của quá trình và tìm hiểu cách tạo JavaScript (chính xác hơn là Asm.js) từ mã máy. QEMU đã được chọn cho cuộc thử nghiệm và một thời gian sau, một bài báo đã được viết trên Habr. Trong phần nhận xét, tôi được khuyên nên làm lại dự án trong WebAssugging và thậm chí tự bỏ cuộc gần như đã hoàn thành Bằng cách nào đó tôi đã không muốn dự án... Công việc đang diễn ra, nhưng rất chậm, và bây giờ, gần đây trong bài báo đó đã xuất hiện chú thích về chủ đề “Vậy mọi chuyện đã kết thúc như thế nào?” Đáp lại câu trả lời chi tiết của tôi, tôi nghe thấy “Cái này nghe giống như một bài báo vậy”. Vâng, nếu bạn có thể, sẽ có một bài viết. Có lẽ ai đó sẽ thấy nó hữu ích. Từ đó, người đọc sẽ tìm hiểu một số thông tin thực tế về thiết kế của chương trình phụ trợ tạo mã QEMU, cũng như cách viết trình biên dịch Đúng lúc cho ứng dụng web.

nhiệm vụ

Vì tôi đã học cách “bằng cách nào đó” chuyển QEMU sang JavaScript nên lần này tôi quyết định thực hiện một cách khôn ngoan và không lặp lại những sai lầm cũ.

Lỗi số một: nhánh từ điểm phát hành

Sai lầm đầu tiên của tôi là phân nhánh phiên bản của mình từ phiên bản ngược dòng 2.4.1. Sau đó, đối với tôi, đó có vẻ là một ý tưởng hay: nếu việc phát hành điểm tồn tại, thì nó có thể ổn định hơn phiên bản 2.4 đơn giản và thậm chí còn ổn định hơn cả nhánh master. Và vì tôi dự định bổ sung khá nhiều lỗi của riêng mình nên tôi không cần lỗi của ai cả. Có lẽ đó là cách nó diễn ra. Nhưng vấn đề là thế này: QEMU không đứng yên, thậm chí có lúc họ còn tuyên bố tối ưu hóa mã được tạo thêm 10%. “Ừ, bây giờ tôi sắp đóng băng,” tôi nghĩ và suy sụp. Ở đây chúng ta cần thực hiện một phép hồi quy: do tính chất đơn luồng của QEMU.js và thực tế là QEMU ban đầu không ngụ ý sự vắng mặt của đa luồng (nghĩa là khả năng vận hành đồng thời một số đường dẫn mã không liên quan và không chỉ “sử dụng tất cả các hạt nhân”) là rất quan trọng đối với nó, các chức năng chính của các luồng tôi đã phải “tắt nó ra” để có thể gọi từ bên ngoài. Điều này tạo ra một số vấn đề tự nhiên trong quá trình sáp nhập. Tuy nhiên, thực tế là một số thay đổi từ chi nhánh master, mà tôi đã cố gắng hợp nhất mã của mình, cũng đã được chọn trong bản phát hành điểm (và do đó trong chi nhánh của tôi) cũng có thể sẽ không mang lại sự tiện lợi hơn.

Nói chung, tôi quyết định rằng vẫn hợp lý nếu loại bỏ nguyên mẫu, tháo rời nó thành các bộ phận và xây dựng một phiên bản mới từ đầu dựa trên thứ gì đó mới hơn và bây giờ là từ master.

Sai lầm thứ hai: Phương pháp TLP

Về bản chất, đây không phải là một sai lầm, nói chung, nó chỉ là một đặc điểm của việc tạo một dự án trong điều kiện hoàn toàn hiểu lầm về cả “di chuyển ở đâu và như thế nào?” và nói chung là “liệu ​​chúng ta có đến được đó không?” Trong những điều kiện này lập trình vụng về là một lựa chọn hợp lý, nhưng tất nhiên, tôi không muốn lặp lại nó một cách không cần thiết. Lần này tôi muốn thực hiện điều đó một cách khôn ngoan: các cam kết nguyên tử, thay đổi mã có ý thức (chứ không phải “xâu chuỗi các ký tự ngẫu nhiên lại với nhau cho đến khi nó biên dịch (có cảnh báo)”, như Linus Torvalds đã từng nói về ai đó, theo Wikiquote), v.v.

Sai lầm thứ ba: xuống nước mà không biết chỗ cạn

Tôi vẫn chưa hoàn toàn thoát khỏi điều này, nhưng bây giờ tôi đã quyết định không đi theo con đường ít phản kháng nhất và thực hiện điều đó “với tư cách là một người trưởng thành”, cụ thể là viết phần phụ trợ TCG của tôi từ đầu, để không để sau này phải nói, “Đúng, điều này tất nhiên là chậm rãi, nhưng tôi không thể kiểm soát mọi thứ - đó là cách viết TCI…” Hơn nữa, điều này ban đầu có vẻ như là một giải pháp hiển nhiên, vì Tôi tạo mã nhị phân. Như người ta nói, “Ghent tập hợpу, nhưng không phải cái đó”: mã tất nhiên là nhị phân, nhưng quyền kiểm soát không thể được chuyển sang nó một cách đơn giản - nó phải được đẩy rõ ràng vào trình duyệt để biên dịch, dẫn đến một đối tượng nhất định từ thế giới JS, vẫn cần phải được cứu ở đâu đó. Tuy nhiên, trên các kiến ​​​​trúc RISC thông thường, theo như tôi hiểu, một tình huống điển hình là cần phải đặt lại rõ ràng bộ đệm lệnh cho mã được tạo lại - nếu đây không phải là thứ chúng ta cần thì trong mọi trường hợp, nó sẽ đóng lại. Ngoài ra, từ lần thử cuối cùng của tôi, tôi đã học được rằng điều khiển dường như không được chuyển đến giữa khối dịch, vì vậy chúng tôi không thực sự cần mã byte được giải thích từ bất kỳ phần bù nào và chúng tôi có thể chỉ cần tạo nó từ hàm trên TB .

Họ đến và đá

Mặc dù tôi đã bắt đầu viết lại mã từ tháng XNUMX, nhưng một cú hích kỳ diệu đã xuất hiện mà không được chú ý: thông thường các lá thư từ GitHub đến dưới dạng thông báo về phản hồi cho các Vấn đề và Yêu cầu kéo, nhưng ở đây, đột ngột đề cập đến trong chủ đề Binaryen như một chương trình phụ trợ qemu trong ngữ cảnh, "Anh ấy đã làm điều gì đó như thế, có thể anh ấy sẽ nói điều gì đó." Chúng tôi đang nói về việc sử dụng thư viện liên quan của Emscripten Nhị phân để tạo WASM JIT. Chà, tôi đã nói rằng bạn có giấy phép Apache 2.0 ở đó và toàn bộ QEMU được phân phối theo GPLv2 và chúng không tương thích lắm. Đột nhiên hóa ra giấy phép có thể sửa nó bằng cách nào đó (Tôi không biết: có thể thay đổi nó, có thể cấp phép kép, có thể cái gì khác...). Tất nhiên điều này làm tôi vui vì lúc đó tôi đã xem xét kỹ càng rồi. định dạng nhị phân WebAssembly, tôi cảm thấy buồn và khó hiểu. Ngoài ra còn có một thư viện có thể sử dụng các khối cơ bản với biểu đồ chuyển tiếp, tạo mã byte và thậm chí chạy nó trong chính trình thông dịch, nếu cần.

Sau đó còn nhiều hơn nữa thư trong danh sách gửi thư của QEMU, nhưng phần này thiên về câu hỏi, "Ai cần nó?" Và nó là đột ngột, hóa ra nó là cần thiết. Ở mức tối thiểu, bạn có thể kết hợp các khả năng sử dụng sau, nếu nó hoạt động nhanh hơn hoặc ít hơn:

  • khởi chạy một cái gì đó mang tính giáo dục mà không cần cài đặt gì cả
  • ảo hóa trên iOS, theo tin đồn, ứng dụng duy nhất có quyền tạo mã nhanh chóng là công cụ JS (điều này có đúng không?)
  • trình diễn hệ điều hành mini - đĩa mềm đơn, đĩa mềm tích hợp, tất cả các loại phần mềm cơ sở, v.v...

Tính năng thời gian chạy của trình duyệt

Như tôi đã nói, QEMU gắn liền với đa luồng, nhưng trình duyệt không có tính năng này. Chà, nghĩa là không... Lúc đầu nó hoàn toàn không tồn tại, sau đó WebWorkers xuất hiện - theo như tôi hiểu thì đây là đa luồng dựa trên việc truyền tin nhắn không có biến chung. Đương nhiên, điều này tạo ra các vấn đề nghiêm trọng khi chuyển mã hiện có dựa trên mô hình bộ nhớ dùng chung. Sau đó, dưới áp lực của dư luận, nó cũng được thực hiện dưới cái tên SharedArrayBuffers. Nó dần dần được giới thiệu, họ kỷ niệm sự ra mắt của nó trên các trình duyệt khác nhau, sau đó họ ăn mừng Năm mới, và sau đó là Meltdown... Sau đó, họ đi đến kết luận rằng phép đo thời gian thô hoặc thô, nhưng với sự trợ giúp của bộ nhớ dùng chung và một thread tăng bộ đếm, tất cả đều giống nhau nó sẽ hoạt động khá chính xác. Vì vậy, chúng tôi đã tắt tính năng đa luồng bằng bộ nhớ dùng chung. Có vẻ như sau đó họ đã bật lại nó, nhưng, như đã thấy rõ từ thử nghiệm đầu tiên, vẫn có cuộc sống không có nó và nếu vậy, chúng tôi sẽ cố gắng làm điều đó mà không cần dựa vào đa luồng.

Tính năng thứ hai là không thể thực hiện các thao tác cấp thấp với ngăn xếp: bạn không thể chỉ cần lấy, lưu bối cảnh hiện tại và chuyển sang bối cảnh mới bằng ngăn xếp mới. Ngăn xếp cuộc gọi được quản lý bởi máy ảo JS. Có vẻ như vấn đề là gì vì chúng tôi vẫn quyết định quản lý các luồng trước đó hoàn toàn theo cách thủ công? Thực tế là khối I/O trong QEMU được triển khai thông qua coroutine và đây là lúc các thao tác ngăn xếp cấp thấp sẽ có ích. May mắn thay, Emscipten đã có sẵn một cơ chế cho các hoạt động không đồng bộ, thậm chí là hai cơ chế: Không đồng bộ hóa и Phiên dịch viên. Cái đầu tiên hoạt động thông qua sự phình to đáng kể trong mã JavaScript được tạo và không còn được hỗ trợ. Thứ hai là "cách chính xác" hiện tại và hoạt động thông qua việc tạo mã byte cho trình thông dịch gốc. Tất nhiên, nó hoạt động chậm nhưng không làm nặng thêm mã. Đúng, việc hỗ trợ các coroutine cho cơ chế này phải được đóng góp một cách độc lập (đã có các coroutine được viết cho Asyncify và đã triển khai gần như cùng một API cho Emterpreter, bạn chỉ cần kết nối chúng).

Hiện tại, tôi vẫn chưa quản lý được việc chia mã thành một mã được biên dịch trong WASM và diễn giải bằng Emterpreter, vì vậy các thiết bị chặn vẫn chưa hoạt động (xem trong loạt bài tiếp theo, như họ nói...). Nghĩa là, cuối cùng bạn sẽ nhận được thứ gì đó giống như thứ xếp lớp vui nhộn này:

  • khối I/O được thông dịch. Chà, bạn có thực sự mong đợi NVMe mô phỏng với hiệu suất gốc không? 🙂
  • Mã QEMU chính được biên dịch tĩnh (trình dịch, các thiết bị mô phỏng khác, v.v.)
  • mã khách được biên dịch động vào WASM

Đặc điểm của nguồn QEMU

Như bạn có thể đã đoán, mã mô phỏng kiến ​​trúc khách và mã tạo hướng dẫn máy chủ được phân tách trong QEMU. Trên thực tế, nó thậm chí còn phức tạp hơn một chút:

  • có kiến ​​trúc của khách
  • máy gia tốc, cụ thể là KVM để ảo hóa phần cứng trên Linux (dành cho hệ thống khách và máy chủ tương thích với nhau), TCG để tạo mã JIT ở mọi nơi. Bắt đầu với QEMU 2.9, hỗ trợ cho tiêu chuẩn ảo hóa phần cứng HAXM trên Windows đã xuất hiện (các chi tiết)
  • nếu TCG được sử dụng thay vì ảo hóa phần cứng thì nó có hỗ trợ tạo mã riêng cho từng kiến ​​trúc máy chủ cũng như cho trình thông dịch phổ quát
  • ... và xung quanh tất cả những điều này - thiết bị ngoại vi mô phỏng, giao diện người dùng, di chuyển, phát lại bản ghi, v.v.

Nhân tiện, bạn có biết: QEMU có thể mô phỏng không chỉ toàn bộ máy tính mà còn cả bộ xử lý cho một tiến trình người dùng riêng biệt trong nhân máy chủ, chẳng hạn như được sử dụng bởi bộ làm mờ AFL cho thiết bị đo nhị phân. Có lẽ ai đó muốn chuyển chế độ hoạt động này của QEMU sang JS? 😉

Giống như hầu hết các phần mềm miễn phí lâu đời, QEMU được xây dựng thông qua cuộc gọi configure и make. Giả sử bạn quyết định thêm một thứ gì đó: chương trình phụ trợ TCG, triển khai luồng, thứ gì đó khác. Đừng vội vui mừng/kinh hoàng (gạch chân nếu thích hợp) trước viễn cảnh giao tiếp với Autoconf - trên thực tế, configure QEMU rõ ràng là tự viết và không được tạo ra từ bất cứ thứ gì.

lắp ráp web

Vậy thứ được gọi là WebAssembly (hay còn gọi là WASM) này là gì? Đây là sự thay thế cho Asm.js, không còn giả vờ là mã JavaScript hợp lệ nữa. Ngược lại, nó hoàn toàn là nhị phân và được tối ưu hóa, và thậm chí chỉ cần viết một số nguyên vào nó cũng không đơn giản lắm: để gọn, nó được lưu trữ ở định dạng LEB128.

Bạn có thể đã nghe nói về thuật toán lặp lại cho Asm.js - đây là sự khôi phục các hướng dẫn điều khiển luồng “cấp cao” (nghĩa là if-then-else, vòng lặp, v.v.), mà các công cụ JS được thiết kế, từ đó LLVM IR cấp thấp, gần với mã máy được bộ xử lý thực thi hơn. Đương nhiên, cách biểu diễn trung gian của QEMU gần với cách biểu diễn thứ hai hơn. Có vẻ như đây rồi, mã byte, sự kết thúc của sự dày vò... Và sau đó là các khối, if-then-else và các vòng lặp!..

Và đây là một lý do khác tại sao Binaryen lại hữu ích: nó có thể chấp nhận các khối cấp cao một cách tự nhiên gần với những gì sẽ được lưu trữ trong WASM. Nhưng nó cũng có thể tạo mã từ biểu đồ gồm các khối cơ bản và chuyển tiếp giữa chúng. Chà, tôi đã nói rằng nó ẩn định dạng lưu trữ WebAssugging đằng sau API C/C++ tiện lợi.

TCG (Trình tạo mã nhỏ)

TCG ban đầu phụ trợ cho trình biên dịch C. Sau đó, rõ ràng là nó không thể chịu được sự cạnh tranh với GCC, nhưng cuối cùng nó đã tìm được vị trí của mình trong QEMU như một cơ chế tạo mã cho nền tảng máy chủ. Ngoài ra còn có một phần phụ trợ TCG tạo ra một số mã byte trừu tượng, được trình thông dịch thực thi ngay lập tức, nhưng tôi quyết định tránh sử dụng nó vào lúc này. Tuy nhiên, thực tế là trong QEMU đã có thể kích hoạt quá trình chuyển đổi sang TB được tạo thông qua chức năng tcg_qemu_tb_exec, hóa ra nó rất hữu ích cho tôi.

Để thêm phần phụ trợ TCG mới vào QEMU, bạn cần tạo thư mục con tcg/<имя архитектуры> (trong trường hợp này, tcg/binaryen) và nó chứa hai tệp: tcg-target.h и tcg-target.inc.c и kê đơn tất cả là về configure. Bạn có thể đặt các tệp khác vào đó, nhưng, như bạn có thể đoán từ tên của hai tệp này, cả hai tệp này sẽ được đưa vào đâu đó: một tệp là tệp tiêu đề thông thường (nó được bao gồm trong tcg/tcg.h, và cái đó đã có trong các tệp khác trong thư mục tcg, accel và không chỉ), cái còn lại - chỉ dưới dạng đoạn mã trong tcg/tcg.c, nhưng nó có quyền truy cập vào các hàm tĩnh của nó.

Quyết định rằng tôi sẽ dành quá nhiều thời gian cho việc điều tra chi tiết về cách thức hoạt động của nó, tôi chỉ cần sao chép “bộ khung” của hai tệp này từ một triển khai phụ trợ khác, chỉ ra điều này một cách trung thực trong tiêu đề giấy phép.

hồ sơ tcg-target.h chứa chủ yếu các cài đặt ở dạng #define-S:

  • có bao nhiêu thanh ghi và chiều rộng bao nhiêu trên kiến ​​trúc đích (chúng tôi có bao nhiêu tùy thích, bao nhiêu tùy thích - câu hỏi đặt ra là những gì sẽ được trình duyệt tạo thành mã hiệu quả hơn trên kiến ​​trúc “hoàn toàn nhắm mục tiêu” ...)
  • căn chỉnh các hướng dẫn máy chủ: trên x86 và thậm chí trong TCI, các hướng dẫn hoàn toàn không được căn chỉnh, nhưng tôi sẽ đặt vào bộ đệm mã không phải hướng dẫn nào cả mà là con trỏ tới cấu trúc thư viện Binaryen, vì vậy tôi sẽ nói: 4 byte
  • những hướng dẫn tùy chọn mà phần phụ trợ có thể tạo ra - chúng tôi bao gồm mọi thứ chúng tôi tìm thấy trong Binaryen, để trình tăng tốc chia phần còn lại thành những phần đơn giản hơn
  • Kích thước gần đúng của bộ đệm TLB mà chương trình phụ trợ yêu cầu là bao nhiêu. Thực tế là trong QEMU mọi thứ đều nghiêm túc: mặc dù có các hàm trợ giúp thực hiện tải/lưu trữ có tính đến MMU khách (chúng ta sẽ ở đâu nếu không có nó bây giờ?), chúng lưu bộ nhớ đệm dịch của chúng dưới dạng một cấu trúc, việc xử lý thuận tiện để nhúng trực tiếp vào các khối phát sóng. Câu hỏi đặt ra là phần bù nào trong cấu trúc này được xử lý hiệu quả nhất bằng một chuỗi lệnh nhỏ và nhanh?
  • ở đây bạn có thể điều chỉnh mục đích của một hoặc hai thanh ghi dành riêng, cho phép gọi TB thông qua một chức năng và tùy ý mô tả một vài thao tác nhỏ inline-chức năng như flush_icache_range (nhưng đây không phải là trường hợp của chúng tôi)

hồ sơ tcg-target.inc.cTất nhiên, thường có kích thước lớn hơn nhiều và chứa một số chức năng bắt buộc:

  • khởi tạo, bao gồm các hạn chế về việc lệnh nào có thể hoạt động trên toán hạng nào. Bị tôi sao chép một cách trắng trợn từ một chương trình phụ trợ khác
  • hàm nhận một lệnh mã byte nội bộ
  • Bạn cũng có thể đặt các hàm phụ trợ ở đây và cũng có thể sử dụng các hàm tĩnh từ tcg/tcg.c

Đối với bản thân tôi, tôi đã chọn chiến lược sau: trong những từ đầu tiên của khối dịch tiếp theo, tôi viết ra bốn con trỏ: một dấu bắt đầu (một giá trị nhất định ở vùng lân cận). 0xFFFFFFFF, xác định trạng thái hiện tại của TB), bối cảnh, mô-đun được tạo và số ma thuật để gỡ lỗi. Lúc đầu dấu hiệu được đặt ở 0xFFFFFFFF - nĐâu n - một số dương nhỏ và mỗi lần nó được thực thi thông qua trình thông dịch, nó sẽ tăng thêm 1. Khi nó đạt đến 0xFFFFFFFE, quá trình biên dịch diễn ra, mô-đun được lưu trong bảng chức năng, được nhập vào một "trình khởi chạy" nhỏ, nơi quá trình thực thi diễn ra từ đó tcg_qemu_tb_execvà mô-đun đã bị xóa khỏi bộ nhớ QEMU.

Để diễn giải những câu nói kinh điển, “Crutch, âm thanh này quyện vào nhau biết bao đối với trái tim của người chuyên nghiệp…”. Tuy nhiên, bộ nhớ đã bị rò rỉ ở đâu đó. Hơn nữa, đó là bộ nhớ do QEMU quản lý! Tôi có một đoạn mã mà khi viết lệnh tiếp theo (tức là một con trỏ), đã xóa lệnh có liên kết ở vị trí này trước đó, nhưng điều này không giúp ích được gì. Trên thực tế, trong trường hợp đơn giản nhất, QEMU phân bổ bộ nhớ khi khởi động và ghi mã được tạo vào đó. Khi hết bộ đệm, mã sẽ bị loại bỏ và mã tiếp theo sẽ bắt đầu được ghi vào vị trí của nó.

Sau khi nghiên cứu mã, tôi nhận ra rằng thủ thuật với con số ma thuật cho phép tôi không thất bại trong việc phá hủy đống bằng cách giải phóng một cái gì đó sai trên bộ đệm chưa được khởi tạo trong lần vượt qua đầu tiên. Nhưng ai sẽ viết lại bộ đệm để bỏ qua chức năng của tôi sau này? Như các nhà phát triển Emscripten khuyên, khi gặp sự cố, tôi đã chuyển mã kết quả trở lại ứng dụng gốc, đặt Mozilla Record-Replay trên đó... Nói chung, cuối cùng tôi nhận ra một điều đơn giản: đối với mỗi khối, Một struct TranslationBlock với mô tả của nó. Đoán xem... Đúng rồi, ngay trước khối ngay trong bộ đệm. Nhận ra điều này, tôi quyết định ngừng sử dụng nạng (ít nhất là một số), và chỉ đơn giản là ném con số ma thuật ra ngoài, và chuyển những từ còn lại sang struct TranslationBlock, tạo một danh sách liên kết đơn có thể được duyệt nhanh chóng khi bộ đệm dịch được đặt lại và giải phóng bộ nhớ.

Một số nạng vẫn còn: ví dụ: các con trỏ được đánh dấu trong bộ đệm mã - một số trong số đó chỉ đơn giản là BinaryenExpressionRef, tức là họ xét các biểu thức cần đưa tuyến tính vào khối cơ bản được sinh ra, một phần là điều kiện để chuyển tiếp giữa các BB, một phần là nơi cần đi. Chà, đã có sẵn các khối dành cho Relooper cần được kết nối theo các điều kiện. Để phân biệt chúng, giả định được sử dụng là tất cả chúng đều được căn chỉnh ít nhất bốn byte, vì vậy bạn có thể sử dụng hai bit có ý nghĩa nhỏ nhất cho nhãn một cách an toàn, bạn chỉ cần nhớ xóa nó nếu cần. Nhân tiện, các nhãn như vậy đã được sử dụng trong QEMU để cho biết lý do thoát khỏi vòng lặp TCG.

Sử dụng nhị phân

Các mô-đun trong WebAssembly chứa các hàm, mỗi hàm chứa một phần nội dung, là một biểu thức. Biểu thức là các phép toán đơn nhất và nhị phân, các khối bao gồm danh sách các biểu thức khác, luồng điều khiển, v.v. Như tôi đã nói, luồng điều khiển ở đây được tổ chức chính xác dưới dạng các nhánh, vòng lặp, lệnh gọi hàm cấp cao, v.v. Các đối số của hàm không được truyền vào ngăn xếp mà được truyền một cách rõ ràng, giống như trong JS. Ngoài ra còn có các biến toàn cục, nhưng tôi chưa sử dụng chúng nên tôi sẽ không nói cho bạn biết về chúng.

Hàm cũng có các biến cục bộ, được đánh số từ 32, kiểu: int64/intXNUMX/float/double. Trong trường hợp này, n biến cục bộ đầu tiên là các đối số được truyền cho hàm. Xin lưu ý rằng mặc dù mọi thứ ở đây không hoàn toàn ở cấp độ thấp về mặt luồng điều khiển, các số nguyên vẫn không mang thuộc tính “đã ký/không dấu”: cách hoạt động của số phụ thuộc vào mã hoạt động.

Nói chung, Binaryen cung cấp API C đơn giản: bạn tạo một mô-đun, trong anh ấy tạo biểu thức - đơn nguyên, nhị phân, khối từ các biểu thức khác, luồng điều khiển, v.v. Sau đó, bạn tạo một hàm có biểu thức làm nội dung. Nếu bạn, giống như tôi, có biểu đồ chuyển tiếp cấp thấp, thành phần relooper sẽ giúp bạn. Theo tôi hiểu, có thể sử dụng điều khiển cấp cao đối với luồng thực thi trong một khối, miễn là nó không vượt ra ngoài ranh giới của khối - nghĩa là có thể tạo đường dẫn nhanh/chậm bên trong phân nhánh đường dẫn bên trong mã xử lý bộ đệm TLB tích hợp, nhưng không can thiệp vào luồng điều khiển “bên ngoài”. Khi bạn giải phóng một relooper, các khối của nó sẽ được giải phóng, khi bạn giải phóng một mô-đun, các biểu thức, hàm, v.v. được phân bổ cho nó sẽ biến mất đấu trường.

Tuy nhiên, nếu bạn muốn diễn giải mã một cách nhanh chóng mà không cần tạo và xóa một phiên bản trình thông dịch không cần thiết, thì có thể hợp lý khi đặt logic này vào một tệp C++ và từ đó quản lý trực tiếp toàn bộ API C++ của thư viện, bỏ qua tính năng sẵn sàng- đã làm giấy gói.

Vì vậy, để tạo mã bạn cần

// настроить глобальные параметры (можно поменять потом)
BinaryenSetAPITracing(0);

BinaryenSetOptimizeLevel(3);
BinaryenSetShrinkLevel(2);

// создать модуль
BinaryenModuleRef MODULE = BinaryenModuleCreate();

// описать типы функций (как создаваемых, так и вызываемых)
helper_type  BinaryenAddFunctionType(MODULE, "helper-func", BinaryenTypeInt32(), int32_helper_args, ARRAY_SIZE(int32_helper_args));
// (int23_helper_args приоб^Wсоздаются отдельно)

// сконструировать супер-мега выражение
// ... ну тут уж вы как-нибудь сами :)

// потом создать функцию
BinaryenAddFunction(MODULE, "tb_fun", tb_func_type, func_locals, FUNC_LOCALS_COUNT, expr);
BinaryenAddFunctionExport(MODULE, "tb_fun", "tb_fun");
...
BinaryenSetMemory(MODULE, (1 << 15) - 1, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
BinaryenAddMemoryImport(MODULE, NULL, "env", "memory", 0);
BinaryenAddTableImport(MODULE, NULL, "env", "tb_funcs");

// запросить валидацию и оптимизацию при желании
assert (BinaryenModuleValidate(MODULE));
BinaryenModuleOptimize(MODULE);

... nếu tôi quên bất cứ điều gì, xin lỗi, đây chỉ là để thể hiện tỷ lệ và chi tiết có trong tài liệu.

Và bây giờ quá trình crack-fex-pex bắt đầu, đại loại như thế này:

static char buf[1 << 20];
BinaryenModuleOptimize(MODULE);
BinaryenSetMemory(MODULE, 0, -1, NULL, NULL, NULL, NULL, NULL, 0, 0);
int sz = BinaryenModuleWrite(MODULE, buf, sizeof(buf));
BinaryenModuleDispose(MODULE);
EM_ASM({
  var module = new WebAssembly.Module(new Uint8Array(wasmMemory.buffer, $0, $1));
  var fptr = $2;
  var instance = new WebAssembly.Instance(module, {
      'env': {
          'memory': wasmMemory,
          // ...
      }
  );
  // и вот уже у вас есть instance!
}, buf, sz);

Để bằng cách nào đó kết nối thế giới của QEMU và JS, đồng thời truy cập nhanh chóng các hàm đã biên dịch, một mảng đã được tạo (một bảng các hàm để nhập vào trình khởi chạy) và các hàm được tạo được đặt ở đó. Để tính toán nhanh chỉ mục, chỉ mục của khối dịch từ XNUMX ban đầu được sử dụng như nó, nhưng sau đó chỉ mục được tính bằng công thức này bắt đầu khớp đơn giản với trường trong struct TranslationBlock.

Ngẫu nhiên, bản demo (hiện có giấy phép âm u) chỉ hoạt động tốt trong Firefox. Các nhà phát triển Chrome đã bằng cách nào đó chưa sẵn sàng đến thực tế là ai đó muốn tạo hơn một nghìn phiên bản mô-đun WebAssembly, vì vậy họ chỉ cần phân bổ một gigabyte không gian địa chỉ ảo cho mỗi...

Đó là tất cả cho bây giờ. Có lẽ sẽ có bài viết khác nếu có ai quan tâm. Cụ thể là vẫn còn ít nhất chỉ làm cho các thiết bị khối hoạt động. Cũng có thể hợp lý nếu làm cho việc biên dịch các mô-đun WebAssembly không đồng bộ, như thông lệ trong thế giới JS, vì vẫn có một trình thông dịch có thể thực hiện tất cả những điều này cho đến khi mô-đun gốc sẵn sàng.

Cuối cùng là một câu đố: bạn đã biên dịch nhị phân trên kiến ​​trúc 32 bit, nhưng mã, thông qua các hoạt động bộ nhớ, leo lên từ Binaryen, ở đâu đó trên ngăn xếp hoặc ở nơi nào khác trong 2 GB phía trên của không gian địa chỉ 32 bit. Vấn đề là theo quan điểm của Binaryen, điều này đang truy cập vào một địa chỉ tổng hợp quá lớn. Làm thế nào để giải quyết vấn đề này?

Theo cách của quản trị viên

Tôi đã không thử nghiệm điều này nhưng suy nghĩ đầu tiên của tôi là "Điều gì sẽ xảy ra nếu tôi cài đặt Linux 32-bit?" Khi đó phần trên của không gian địa chỉ sẽ bị kernel chiếm giữ. Câu hỏi duy nhất là sẽ chiếm bao nhiêu: 1 hoặc 2 Gb.

Theo cách của một lập trình viên (tùy chọn cho người thực hành)

Hãy thổi bong bóng ở đầu không gian địa chỉ. Bản thân tôi cũng không hiểu tại sao nó lại hoạt động - đó đã phải có một ngăn xếp. Nhưng “chúng tôi là những người thực hành: mọi thứ đều có lợi cho chúng tôi, nhưng không ai biết tại sao…”

// 2gbubble.c
// Usage: LD_PRELOAD=2gbubble.so <program>

#include <sys/mman.h>
#include <assert.h>

void __attribute__((constructor)) constr(void)
{
  assert(MAP_FAILED != mmap(1u >> 31, (1u >> 31) - (1u >> 20), PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
}

... đúng là nó không tương thích với Valgrind, nhưng may mắn thay, chính Valgrind đã đẩy mọi người ra khỏi đó một cách rất hiệu quả :)

Có lẽ ai đó sẽ đưa ra lời giải thích rõ hơn về cách thức hoạt động của mã này của tôi...

Nguồn: www.habr.com

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