MỚI: Helius mua lại Light Protocol
cách xây dựng chương trình solana bằng hợp ngữ solana berkeley packet filter (sbpf)
Blog/Phát triển

Cách viết chương trình Solana bằng hợp ngữ SBPF

Đồng sáng lập kiêm Trưởng bộ phận Nội dung & Chiến lược tại BlueshiftLeonardo Donatacci trên XLeonardo Donatacci trên LinkedIn
Đọc trong 11 phút

Hệ sinh thái Solana hiện đang chứng kiến một cuộc đua song song nhằm tối ưu hóa chương trình đến mức thấp nhất. 

Ở cấp độ tổng quan, các thư viện như Pinocchio đang cách mạng hóa việc phát triển bằng Rust, giúp hiệu quả tính toán tăng lên nhiều bậc độ lớn. Trong khi đó, ở cấp độ thấp nhất, một nhóm nhà phát triển tận tâm, gắn kết bởi sự thiếu tin tưởng chung dành cho trình biên dịch, đang tiến thêm một bước. Thay vì viết chương trình Solana bằng các ngôn ngữ biên dịch như Rust hoặc C, họ tập trung tỉ mỉ tự viết mã byte để khai thác hiệu suất tối đa từ từng lệnh cuối cùng.

Những cải thiện ở cấp thấp này chỉ khả thi khi chúng ta trực tiếp ra lệnh cho VM bằng ngôn ngữ gốc của nó: hợp ngữ sBPF, biến thể riêng của Solana dựa trên extended Berkeley Packet Filter (eBPF), loại mã byte được sử dụng và thực thi trong mọi chương trình on-chain.

Viết hợp ngữ sBPF cho phép nhà phát triển truy cập trực tiếp vào giao diện cấp thấp nhất của Solana Virtual Machine. Mặc dù trình biên dịch Rust và LLVM cố gắng tối ưu hóa, nhưng do cú pháp ngôn ngữ không đủ chi tiết hoặc thiếu ngữ cảnh để đưa ra lựa chọn biên dịch tốt hơn, chúng thường tạo ra mã byte kém tối ưu hơn so với mã của một nhà phát triển lành nghề có toàn quyền kiểm soát ở cấp độ lệnh mà hợp ngữ mang lại.

Mức độ kiểm soát cao hơn này làm giảm tính thuận tiện, nhưng đổi lại giúp tiết kiệm đáng kể mức sử dụng đơn vị tính toán và kích thước tệp nhị phân (và do đó cả phí thuê). Những khoản tiết kiệm này đặc biệt quan trọng trong các hoạt động có mức cạnh tranh cao, nhiều tranh chấp và yêu cầu hiệu suất khắt khe.

Đồng thời, cũng có lý do để cho rằng không phải mọi chương trình đều nên được viết bằng hợp ngữ.

Dù mọi thứ đã được cải thiện đáng kể, trước đây công cụ hỗ trợ còn hạn chế. Quan trọng hơn, lợi ích về hiệu suất thường đi kèm với sự đánh đổi lớn: phải xác minh tính đúng đắn theo cách thủ công và chịu chi phí kiểm toán cao hơn. Nguyên nhân là thiếu công cụ tự động, còn cú pháp thì khó đọc, khó viết và khó hiểu hơn. 

Ngược lại, cũng có thể cho rằng các ngôn ngữ biên dịch là một hộp đen, che khuất những lựa chọn mà trình biên dịch đưa ra. Vì vậy, tính minh bạch và khả năng kiểm soát bổ sung của hợp ngữ thường hé lộ những điều khó nhận thấy khi làm việc với ngôn ngữ biên dịch. Trên thực tế, phần lớn các đột phá hiệu suất gần đây trong SDK Rust của chúng tôi đều được khám phá và định hướng từ việc nhận ra rằng chúng ta có thể tự viết mã byte hiệu quả hơn trình biên dịch.

Trong bài viết này, bạn sẽ tìm hiểu:

  • sBPF Assembly là gì và cách nó cung cấp quyền kiểm soát trực tiếp đối với máy ảo
  • Quá trình phát triển từ Berkeley Packet Filter đến eBPF và lý do Solana áp dụng công nghệ này
  • Kiến trúc máy ảo, tập lệnh và mô hình bộ nhớ của sBPF
  • Cách thiết lập môi trường phát triển và xây dựng chương trình sBPF
  • Cách lập trình hợp ngữ từng bước qua một ví dụ memo thực tế
  • Những vấn đề bảo mật thiết yếu khi viết mã cấp thấp

Hợp ngữ là gì?

Hợp ngữ là một biến thể mà con người có thể đọc được của mã máy: ngôn ngữ lập trình cấp thấp nhất, tương ứng trực tiếp với tập lệnh của CPU hoặc VM.

Thay vì biến và hàm, hợp ngữ thao tác trên thanh ghi (các vị trí lưu trữ tạm thời, tốc độ cao trong CPU), địa chỉ bộ nhớ (các vị trí vật lý trong RAM hoặc trên đĩa) và các phép toán cơ bản như nạp (đọc từ bộ nhớ), lưu (ghi vào bộ nhớ), số học và nhảy (luồng điều khiển).

Mỗi lệnh trong hợp ngữ ánh xạ một-một tới một lệnh tương đương trong mã máy. 

Phép ánh xạ một-một này cho phép lập trình viên kiểm soát chính xác những gì bộ xử lý thực thi, bao gồm thanh ghi nào chứa dữ liệu, cách truy cập bộ nhớ và trình tự thao tác chính xác. 

Không giống các ngôn ngữ cấp cao, nơi một hàm duy nhất có thể tạo ra hàng chục lệnh, hợp ngữ cung cấp tính minh bạch và quyền kiểm soát hoàn toàn đối với hành vi của máy mà không có các lớp trừu tượng khó quan sát.

Berkeley Packet Filter (BPF) và eBPF là gì?

Berkeley Packet Filter (BPF) ra đời năm 1992 dưới dạng một máy ảo giúp lọc hiệu quả các gói mạng trong nhân Unix. BPF nguyên bản sử dụng một tập lệnh đơn giản và kiến trúc dựa trên thanh ghi, cho phép mã chạy an toàn trong môi trường cô lập bên trong nhân.

Extended Berkeley Packet Filter (eBPF) đã hiện đại hóa khái niệm này, mở rộng từ một bộ lọc gói thành máy ảo đa dụng. eBPF giới thiệu kiến trúc 64 bit, nhiều thanh ghi hơn và tập lệnh phong phú hơn, cho phép các chương trình phức tạp chạy an toàn trong không gian nhân để phục vụ mạng, bảo mật và giám sát hệ thống.

Solana áp dụng eBPF vì công nghệ này cung cấp một môi trường thực thi an toàn, đã được kiểm chứng với khả năng cô lập tích hợp. Cơ chế cô lập ngăn chương trình truy cập tài nguyên hệ thống, làm sập node hoặc can thiệp vào các chương trình khác, trong khi quá trình thực thi tất định đảm bảo mọi validator đều tạo ra kết quả giống nhau. 

Ngoài ra, kiến trúc dựa trên thanh ghi và chuỗi công cụ hoàn thiện khiến eBPF trở thành lựa chọn lý tưởng để thực thi on-chain với hiệu suất cao, còn backend LLVM hiện có cho phép nhà phát triển biên dịch từ các ngôn ngữ cấp cao như Rust.

Kiến trúc máy ảo sBPF

Khi một chương trình Solana thực thi, môi trường thực thi sẽ nạp mã byte sBPF vào bộ nhớ, tiến hành xác minh tĩnh để đảm bảo an toàn (kiểm tra vòng lặp vô hạn, truy cập bộ nhớ không hợp lệ và việc sử dụng lệnh đúng cách), rồi thực thi mã trong máy ảo. 

VM cung cấp một môi trường thực thi 64 bit được kiểm soát, nơi các chương trình chạy hoàn toàn tách biệt với hệ thống máy chủ và các chương trình khác, đồng thời mọi hoạt động truy cập tài nguyên đều được môi trường thực thi làm trung gian.

Kiến trúc tập lệnh sBPF

sBPF hoạt động với mười một thanh ghi 64 bit (r0-r10), trong đó r10 đóng vai trò con trỏ khung chỉ đọc và r0 là thanh ghi trả về. 

Các lệnh tuân theo một định dạng nhất quán, với mã thao tác chỉ định thao tác (số học, logic, truy cập bộ nhớ, nhảy) và toán hạng cho biết thanh ghi nguồn/đích, độ lệch và/hoặc giá trị tức thời. 

Các nhóm lệnh chính gồm phép toán ALU (cộng, trừ, thao tác bit), thao tác bộ nhớ (nạp/lưu) và luồng điều khiển (nhảy có điều kiện/không điều kiện).

Mô hình bộ nhớ sBPF

Các chương trình sBPF hoạt động trong một bố cục bộ nhớ có cấu trúc: stack 4KB cho biến cục bộ và lời gọi hàm, heap cho phân bổ động, dữ liệu chương trình chỉ đọc chứa mã byte và hằng số, cùng các vùng dữ liệu account ánh xạ tới các account Solana mà chương trình có thể truy cập trong quá trình thực thi.

Mọi hoạt động truy cập bộ nhớ đều được kiểm tra giới hạn và chương trình không thể truy cập bộ nhớ bên ngoài các vùng được chỉ định.

Syscall Solana trong sBPF

Các chương trình sBPF không thể trực tiếp truy cập tài nguyên hệ thống hoặc thực hiện thao tác I/O. Thay vào đó, chúng yêu cầu dịch vụ thông qua syscall, tức các lệnh đặc biệt chuyển quyền điều khiển sang môi trường thực thi Solana. 

Trong hợp ngữ sBPF, syscall được gọi bằng lệnh call và một ký hiệu gọi được trình biên dịch sửa thành đích gọi tại thời điểm hợp dịch. Hiện tại, syscall được gọi thông qua các phép tái định vị động dựa trên văn bản; đây là một hệ thống bảng tra cứu chuỗi phức tạp ánh xạ các ký hiệu tới hàm băm Mumur3 32 bit khi biên dịch JIT. Tuy nhiên, đang có một đề xuất được thảo luận nhằm thay thế cơ chế này bằng syscall tĩnh, qua đó đơn giản hóa đáng kể quy ước gọi. Khi một syscall được gọi, các đối số được truyền qua thanh ghi 1 đến 5, trong đó thanh ghi 5 đôi khi đóng vai trò vùng tràn stack, còn giá trị trả về được ghi vào r0.

Các syscall phổ biến gồm thao tác bộ nhớ (sol_memcpy, sol_memcmp), hàm mật mã (băm, xác minh chữ ký), ghi log và lời gọi liên chương trình.

Hướng dẫn về hợp ngữ SBPF 

Thiết lập môi trường 

Theo cách truyền thống, việc viết hợp ngữ sBPF đòi hỏi toàn bộ chuỗi công cụ Solana: một quy trình cồng kềnh, phức tạp và phụ thuộc vào nền tảng.

Vì lý do này, Dean Little đã tạo ra sBPF SDK, cung cấp giải pháp toàn diện từ đầu đến cuối để khởi tạo, xây dựng, biên dịch, kiểm thử và triển khai chương trình sBPF.

Bạn có thể cài đặt SDK trên bất kỳ hệ điều hành nào bằng Cargo:

Mã
cargo install --git https://github.com/blueshift-gg/sbpf.git

Trước khi bắt đầu viết mã, bạn cũng nên cài đặt tiện ích mở rộng sBPF Assembly cho VS Code để tô sáng cú pháp, tự động hoàn thành và phát hiện lỗi.

Thiết lập dự án 

Tạo khung dự án mới bằng lệnh:

Mã
sbpf init <name_of_the_project>

Lệnh này tạo một dự án có các bài kiểm thử Rust bằng Mollusk.

Nếu muốn tạo khung dự án với các bài kiểm thử TypeScript, bạn có thể dùng lệnh sau để khởi tạo:

Mã
sbpf init <name_of_the_project> --ts-tests 

Ví dụ memo bằng hợp ngữ sBPF

Khó có thể hình dung một chương trình nào đơn giản hơn memo; chính vì vậy, đây là ví dụ nhập môn hoàn hảo cho hợp ngữ sBPF. 

Chương trình chỉ làm một việc: nhận bất kỳ dữ liệu lệnh nào được gửi tới và ghi dữ liệu đó vào blockchain. Không có account, không có logic phức tạp, chỉ có quyền kiểm soát thuần túy ở cấp độ lệnh đối với Solana Virtual Machine.

Hãy bắt đầu bằng cách xem xét toàn bộ chương trình:

Mã
.equ NUM_ACCOUNTS, 0x00
.equ DATA_LEN, 0x08
.equ DATA, 0x10
.globl entrypoint
entrypoint:
  ldxdw r0, [r1+NUM_ACCOUNTS]
  ldxdw r2, [r1+DATA_LEN]
  add64 r1, DATA
  call sol_log_
  exit

Định nghĩa các hằng số

Chương trình bắt đầu bằng cách định nghĩa ba hằng số ánh xạ cấu trúc của vùng đầu vào đã tuần tự hóa trong môi trường thực thi Solana:

Mã
.equ NUM_ACCOUNTS, 0x00 // Account count offset
.equ DATA_LEN, 0x08     // Data length offset  
.equ DATA, 0x10          // Data start offset

Các độ lệch này tương ứng với vị trí mà môi trường thực thi Solana đóng gói dữ liệu lệnh trong bộ nhớ.

Khi VM gọi chương trình, nó chuyển cho chúng ta một bộ đệm có cấu trúc trong thanh ghi r1 và các hằng số này cho phép chúng ta di chuyển trong cấu trúc đó. 

Các công cụ như sbpf.xyz có thể tự động tính toán những độ lệch này dựa trên bố cục dữ liệu account và dữ liệu lệnh của bạn.

Tạo điểm vào

Tiếp theo, chúng ta tạo điểm vào và logic xác thực cho chương trình.

Mã
.globl entrypoint
entrypoint:
  ldxdw r0, [r1+NUM_ACCOUNTS]

Chỉ thị điểm vào .globl yêu cầu trình liên kết đặt ký hiệu điểm vào ở chế độ hiển thị toàn cục. Môi trường thực thi Solana tìm ký hiệu này để biết vị trí bắt đầu thực thi chương trình. Một điều thú vị: dù chúng ta thường gọi nó là entrypoint, trên thực tế có thể đặt bất kỳ tên nào!

Lệnh đầu tiên sử dụng một kỹ thuật xác thực thông minh dành riêng cho hợp ngữ: vì r0 là thanh ghi trả về và mọi giá trị trả về khác 0 đều đóng vai trò mã lỗi, việc nạp trực tiếp số lượng account vào r0 buộc chương trình thoát với mã lỗi khác 0 nếu có nhiều hơn 0 account được truyền vào. 

Nhờ đó, chúng ta không cần bỏ qua thủ công bất kỳ account đầu vào nào để xác thực độ lệch của dữ liệu lệnh trong VM.

Gọi syscall sol_log

Cuối cùng, chúng ta có thể thực hiện syscall sol_log_:

Mã
ldxdw r2, [r1+DATA_LEN]   ; Load memo length
add64  r1, DATA           ; Point r1 to memo data
call sol_log_             ; Log the memo
exit                      ; Exit with r0 value

Syscall sol_log_ yêu cầu độ dài thông báo nằm trong r2 và con trỏ tới thông báo cần ghi log nằm trong r1. May mắn là trong vùng đầu vào đã tuần tự hóa, môi trường thực thi tự động thêm một bộ đếm độ dài 64 bit vào trước dữ liệu lệnh. Vì vậy, chúng ta có thể gọi syscall sol_log_ chỉ bằng cách:

  • Nạp giá trị tại độ lệch DATA_LEN vào r2
  • Cho r1 trỏ tới độ lệch tại vị trí bắt đầu dữ liệu lệnh

Khi hai thanh ghi đã trỏ tới đúng giá trị, chúng ta chỉ cần gọi syscall rồi thoát với bất kỳ giá trị nào nằm trong r0.

Xây dựng và triển khai chương trình

Bạn có thể xây dựng chương trình bằng trình hợp dịch tích hợp sẵn của SBPF, do Claire Fan viết; đây là một tệp nhị phân Rust 5mb thay thế chuỗi công cụ LLVM >2gb mà bạn phải dùng với các công cụ nền tảng Solana. 

Để chạy quy trình xây dựng, chỉ cần thực thi:

Mã
sbpf build

Sau khi xây dựng chương trình, bạn có thể triển khai bằng lệnh:

Mã
sbpf deploy

Tương tác với chương trình

Để kiểm thử chương trình bằng các bài kiểm thử đã được tạo trong khung dự án, bạn có thể chạy sbpf test. Hoặc nếu muốn, bạn có thể chạy toàn bộ quy trình bằng sbpf e2e để xây dựng, triển khai và kiểm thử chỉ với một lệnh.

Những vấn đề bảo mật trong hợp ngữ sBPF

Viết mã hợp ngữ đồng nghĩa với việc chịu toàn bộ trách nhiệm về bảo mật: không có trình biên dịch nào phát hiện lỗi thay bạn. Mỗi lệnh đều ảnh hưởng trực tiếp đến độ an toàn của chương trình, vì vậy các nguyên tắc bảo mật cốt lõi này là rất cần thiết.

Xác thực đầu vào

Các chương trình hợp ngữ phải xác thực thủ công mọi đầu vào. Luôn kiểm tra số lượng account, độ dài dữ liệu và kích thước bộ đệm trước khi sử dụng.

Ví dụ, trong chương trình memo của chúng ta, việc nạp số lượng account vào r0 đã tạo ra cơ chế xác thực tự động: nếu account bị truyền vào không đúng, chương trình sẽ thất bại. Khi xử lý dữ liệu, hãy kiểm tra độ dài theo phạm vi dự kiến trước khi truy cập bộ nhớ.

Kiểm tra giới hạn bộ nhớ

sBPF không cung cấp cơ chế kiểm tra giới hạn tự động. Trước khi truy cập mảng hoặc bộ đệm, hãy xác minh thủ công rằng các thao tác đọc/ghi vẫn nằm trong giới hạn được cấp phát. Một phép kiểm tra giới hạn đơn giản trước khi truy cập bộ nhớ có thể ngăn sự cố và hỏng dữ liệu:

Mã
jgt r2, MAX_LENGTH, error    # Check if length exceeds limit
ldxb r3, [r1+r2]            # Safe to load if check passes

Quản lý thanh ghi

Các thanh ghi lưu giữ trạng thái quan trọng của chương trình. Khi gọi hàm hoặc syscall, hãy bảo toàn những giá trị quan trọng bằng cách lưu chúng vào stack hoặc các thanh ghi khác. 

Con trỏ khung (r10) và các giá trị trả về trong r0 cần được chú ý đặc biệt: làm hỏng chúng có thể khiến chương trình bị sập hoặc tạo ra lỗ hổng bảo mật.

An toàn số học

Phát hiện tràn số thủ công là yếu tố then chốt đối với các phép toán số học. Trước khi cộng các giá trị lớn, hãy kiểm tra xem kết quả có thể vượt quá giới hạn 64 bit hay không. 

Các phép chia cần kiểm tra số 0 rõ ràng để tránh lỗi trong thời gian chạy.

Xác thực tham số syscall

Syscall yêu cầu các tham số hợp lệ và sẽ thất bại nếu đầu vào không hợp lệ. Trước khi gọi syscall, hãy đảm bảo các thanh ghi chứa đúng con trỏ, độ dài và giá trị. Tham số không hợp lệ không chỉ gây thất bại mà còn tiêu tốn đơn vị tính toán không cần thiết.

Kết luận

Hợp ngữ sBPF không dành cho tất cả mọi người, và đó chính là chủ đích. 

Hầu hết nhà phát triển nên tiếp tục dùng Rust và để trình biên dịch xử lý việc tối ưu hóa. Nhưng với những người tối ưu đến từng đơn vị tính toán cuối cùng hoặc xây dựng hạ tầng đòi hỏi hiệu suất cao, hợp ngữ mang lại điều mà không ngôn ngữ cấp cao nào có thể cung cấp: quyền kiểm soát hoàn toàn.

Chúng ta đã tìm hiểu những kiến thức cơ bản, từ cách sBPF phù hợp với kiến trúc Solana cho đến cách xây dựng chương trình memo đầu tiên. 

Ví dụ memo có vẻ đơn giản, nhưng nó minh họa các nguyên tắc cốt lõi mà bạn sẽ dùng trong những chương trình phức tạp hơn:

  • Thao tác trực tiếp trên thanh ghi
  • Quản lý bộ nhớ thủ công
  • Xử lý syscall một cách rõ ràng

Những lợi ích này đi kèm với cái giá phải trả: bạn đánh đổi các cơ chế bảo vệ để lấy tốc độ, đánh đổi lớp trừu tượng để lấy quyền kiểm soát. Bạn nên chọn hợp ngữ khi đã tối ưu mã Rust nhưng vẫn cần thêm hiệu suất, khi đang xây dựng hạ tầng mà từng micro giây đều quan trọng hoặc khi cần làm điều mà trình biên dịch không thể tối ưu tốt. 

Trong mọi trường hợp khác, hãy tránh tự làm khó mình và tiếp tục dùng Rust.

Các công cụ đang ngày càng tốt hơn, cộng đồng đang phát triển và lợi ích về hiệu suất đã tự chứng minh. Nhưng hãy nhớ: “quyền lực càng lớn thì trách nhiệm không làm hỏng mọi thứ càng cao”.

Nếu muốn đọc thêm nội dung về cách sử dụng hợp ngữ sBPF, hãy xem Khóa học nhập môn về hợp ngữ trên Blueshift và kiểm tra kỹ năng với một số thử thách có tại đó!

Đăng ký nhận tin từ Helius

Luôn cập nhật những thông tin mới nhất về phát triển Solana và nhận thông báo khi chúng tôi đăng bài