WebAssembly101 - Chạy đoạn mã WASM đầu tiên

Tôi có viết một bài giới thiệu về WebAssembly tại đây, nội dung cũng khá dài, ở đây tôi chỉ note lại một số phần về kỹ thuật cho bạn đọc.

Một vài đặc điểm của WASM  bạn đọc cần chú ý, theo quan điểm của tác giả:
  • WASM sinh ra để giải quyết bài toán hiệu năng của Javascript trước tiên, tận dụng sức mạnh tính toán ở phía thiết bị của người dùng, đặc biệt ở các tác vụ xử lý truyền thông đa phương tiện (video, audio, image), game, mô phỏng, thuật toán,...
  • WASM là hợp ngữ (Assembly Language) được thiết kế riêng cho các Javascript Engine, đây không hẳn là một ngôn ngữ lập trình độc lập, chỉ có các Javascript Engine mới có thể hiểu và thực thi các Instruction của WASM.
  • WASM có thể làm được mọi việc Javascript có thể làm, nhưng không phải được thiết kế để thay thế một ngôn ngữ linh động như Javascript, ít nhất ở thời điểm hiện tại vẫn cần Javascript để chạy được WASM.
  • WASM được nạp vào trình duyệt dưới định dạng nhị phân (chuỗi bytecode), vì vậy mã nguồn thường sẽ được viết bằng cách ngôn ngữ cấp cao như C/C++, Rust, … sau đó sẽ được biên dịch về định dạng nhị phân của WASM.
Để hiểu thêm về công nghệ WASM, ta sẽ bắt đầu bằng một ví dụ đơn giản. Giả sử trong lập trình Javascript ta cài đặt một hàm tên add2num cho phép cộng 2 số nguyên, nhìn vào bảng bên dưới ta sẽ có được so sánh về cách biểu diễn mã nguồn
Javascript
WASM Text Format
WASM Bytecode
function add2num(p0,p1)
{
 return p0 + p1;
}
(func $add2num
     (param $p0 i32)
     (param $p1 i32)
     (result i32)
      (i32.add
          (get_local $p0)
          (get_local $p1)
      )
)
<add2num>:
20 00
20 01
6a
0b
WASM Bytecode chính là đoạn mã cuối cùng mà trình duyệt cần để thực thi. Tiếp theo đây ta sẽ cần làm một số thao tác để hàm add2num viết bằng WASM chạy được trên trình duyệt Web. Đầu tiên, tạo tập tin module.wat có nội dung như sau:
(module
(func $add2num (param $p0 i32) (param $p1 i32) (result i32)
(i32.add
(get_local $p0)
(get_local $p1)
)
)
(export "wasm_add" (func $add2num))
)

Đoạn mã này là WASM ở định dạng Text (WebAssembly Text Format, gọi tắt là WAT), sử dụng cú pháp S-Expression tương tự ngôn ngữ lập trình LISP. Các ngôn ngữ Assembly đều được thiết kế các định dạng Text đi kèm, giúp cho việc đọc và sử dụng ngôn ngữ đó được dễ dàng hơn thay vì sử dụng các Bytecode Instruction khô khan và khó nhớ.

Với đoạn mã trên, ta đã định nghĩa một hàm cộng 2 số nguyên 32-bit:

  • Các đoạn mã WASM được đóng gói trong một đối tượng gọi là module, trình duyệt sẽ dựa vào các module này để thực thi WASM.
  • Tên hàm là add2num, gồm 2 tham số $p0$p1 kiểu số nguyên 32-bit. Hàm sẽ trả về một giá trị số nguyên 32-bit thông qua lệnh chỉ thị result. Trong WASM, đặt tên biến hoặc hàm sẽ dùng dấu dollar “$” ở phía trước.
  • Khi gọi hàm sẽ có một vùng nhớ gọi là locals chứa các giá trị khởi tạo, cụ thể ở đây là 2 tham số truyền vào hàm là $p0$p1, lệnh get_local sẽ đọc các giá trị trong vùng locals này và đưa vào Stack.
  • Lệnh i32.add sẽ lấy 2 giá trị nằm trên Stack, ở đây là $p0$p1, cộng lại với nhau, kết quả trả về đưa lại vào Stack. Hàm sẽ trả về giá trị nằm trên cùng của Stack, chính là tổng của $p0$p1.
  • Lệnh export sẽ định nghĩa hàm add2num sẽ được gọi bên ngoài module thông qua nhãn wasm_add

Như đã đề cập, WASM nạp vào trình duyệt dưới dạng nhị phân, nên ta cần biên dịch nội dung của module.wat thành dạng nhị phân. Rất may, WebAssembly Binary Toolkit (gọi tắt là WABT) cung cấp rất nhiều công cụ để làm việc với WASM
(https://github.com/WebAssembly/wabt), trong đó có công cụ wat2wasm cho phép biên dịch các đoạn mã WAT thành bytecode.

Thực hiện lệnh sau để biên dịch module.wat

$ wat2wasm module.wat -o module.wasm


Đoạn byte-code như trên khá khó đọc, ta có thể sử dụng công cụ wasm-objdump giúp việc hiển thị byte-code bên trong tập tin WASM nhị phân module.wasm dễ đọc hiểu hơn.

$ ./wabt/bin/wasm-objdump -d module.wasm


Bạn đọc có thể tham khảo thêm ý nghĩa và cách sử dụng của các WASM Instruction tại https://github.com/sunfishcode/wasm-reference-manual/blob/master/WebAssembly.md

Ở thời điểm hiện tại, bytecode của WASM sẽ được gọi từ Javascript. Tiếp tục tạo một tập tin index.html với nội dung như sau

<!DOCTYPE html>
<html>
<script type="text/javascript">
var wasm_instance = null;
fetch('module.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes)
).then(obj => {
wasm_instance = obj.instance.exports;
});
</script>
<body>
Hello, WASM!
</body>
</html>



Đoạn Javascript trên sẽ nạp mã WASM từ tập tin module.wasm vào đối tượng wasm_instance, sau khi quá trình này diễn ra thành công, ta có thể sử dụng hàm add2num. Để kiểm tra kết quả, có thể mở một Web Server đơn giản tại thư mục đang làm việc, nhanh nhất là dùng php-cli

$ php -S 127.0.01:8888


Truy cập http://127.0.0.1:8888/index.html để xem kết quả, trong bài viết này tác giả sử dụng trình duyệt Google Chrome


Mở Chrome DevTools, kiểm tra ở Network-Tab ta sẽ thấy khi truy cập index.html, đoạn Javascript sẽ tải về tập tin module.wasm, nội dung ở định dạng nhị phân. Tiếp tục chuyển sang Console-Tab để xem mã WASM tải về có được khởi tạo thành công không.


Như ta thấy, đối tượng wasm_instance được khởi tạo thành công, đang chứa hàm wasm_add được viết bằng WASM (chính là hàm add2num được export). Có thể thử gọi hàm wasm_add ngay tại Console để kiểm tra


Ở ví dụ trên, Javascript nạp WASM bytecode từ một tập tin riêng, bên cạnh đó ta cũng có thể truyền một chuỗi bytecode trực tiếp vào Javascript thông qua một Byte Array.

Thực hiện chuyển nội dung tập tin module.wasm sang dạng Hex Array, đơn giản nhất là sử dụng Python

$ python -c "print [hex(ord(c)) for c in open('module.wasm').read()]" | tr -d "'"


Ta thực hiện nạp bytecode trên trực tiếp vào mã Javascript như sau
const wasm_bytecodes = new Uint8Array(
[
0x00, 0x61, 0x73, 0x6d, // header
0x01, 0x00, 0x00, 0x00, // header
0x01, 0x07, 0x01, 0x60, // header
0x02, 0x7f, 0x7f, 0x01, // header
0x7f, 0x03, 0x02, 0x01, // header
0x00, 0x07, 0x0c, 0x01, // header
0x08, 0x77, 0x61, 0x73, // header
0x6d, 0x5f, 0x61, 0x64, // header
0x64, 0x00, 0x00, 0x0a, // header
0x09, 0x01, 0x07, 0x00, // header
0x20, 0x00, // get_local $0
0x20, 0x01, // get_local $1
0x6a, // i32.add
0x0b // end
]
);

var new_instance = new WebAssembly.Instance(new WebAssembly.Module(wasm_bytecodes));

Có thể kiểm tra nhanh đoạn mã này sử dụng DevTools Console


Ta thấy mã WASM đã chạy thành công!

Ở bài viết tiếp theo, tác giả sẽ giải thích thêm về cơ chế stack-based trong WASM, đồng thời tìm hiểu cách Debug mã WASM trên các trình duyệt, áp dụng để giải quyết Challenge-5 tại sự kiện Flare On 2018 của hãng bảo mật FireEye.


Nhận xét

Bài đăng phổ biến từ blog này

[Steganography] Kỹ thuật che dấu thông tin - Phần 2

[Steganography] Kỹ thuật che dấu thông tin - Phần 1

Forcing CRC-32 Attack