

Bài viết này là phần thứ hai của một loạt các bài báo.
Nếu bạn chưa đọc lời nói đầu của bài viết này, vui lòng làm như vậy trước,Phần 1 Giới thiệu
Chúng tôi đang giải cấu trúc mã byte EVM của một hợp đồng thông minh vững chắc đơn giản.
Hôm nay, hãy bắt đầu sử dụng chiến lược "chia để trị" để tháo rời mã phức tạp của hợp đồng thông minh. Như tôi đã nói trong lời nói đầu giới thiệu, mã được phân tách này thực sự ở mức rất thấp, nhưng sẽ tương đối dễ đọc so với mã byte gốc.
Vui lòng đảm bảo rằng bạn đã làm theo thao tác mà tôi đã giới thiệu trong lời nói đầu và triển khai mã BasicToken trong trình biên dịch phối lại.
Tuyên bố miễn trừ trách nhiệm: Tất cả các hướng dẫn được cung cấp trong bài viết này tùy thuộc vào cách giải thích của riêng tôi về cách thức hoạt động của giao dịch và không đại diện cho ý kiến chính thức của Ethereum.
Hiện nay,cho phépChúng tôi tập trung vào các hoạt động JUMP, JUMPI, JUMPDES, RETURN và STOP và bỏ qua tất cả các hoạt động khác. Bất cứ khi nào chúng tôi tìm thấy một opcode không phải là một trong số này, chúng tôi chỉ cần bỏ qua nó và chuyển sang hướng dẫn tiếp theo mà không can thiệp vào chúng.
Khi EVM thực thi mã, nó sẽ theo thứ tự từ trên xuống, không có điểm nhập nào khác trong mã và quá trình thực thi luôn bắt đầu từ trên cùng. JUMP và JUMPI có thể làm cho mã nhảy. JUMP lấy giá trị cao nhất trên ngăn xếp và sẽ thực hiện lệnh di chuyển đến vị trí đó. Tuy nhiên, vị trí đích phải chứa opcode JUMPDEST, nếu không thì quá trình thực thi sẽ thất bại. Mục đích duy nhất của việc này là: JUMPDEST đánh dấu vị trí là mục tiêu nhảy hợp lệ. JUMPI cũng giống hệt như vậy, nhưng không được có "0" ở vị trí thứ hai của ngăn xếp, nếu không sẽ không có bước nhảy.Vì vậy, đây là bước nhảy có điều kiện, STOP là lệnh dừng hoàn toàn hợp đồng thông minh, RETURN là tạm dừng thực thi hợp đồng thông minh, nhưng trả lại một phần dữ liệu trong bộ nhớ EVM, rất tiện lợi。
Vì vậy, hãy bắt đầu giải thích mã với tất cả những điều này trong tâm trí. Trong trình gỡ lỗi của Remix, trượt thanh trượt cho "giao dịch" sang bên trái. Bạn có thể sử dụng nút Bước vào (trông giống như một mũi tên nhỏ hướng xuống) và làm theo hướng dẫn.
Bạn có thể bỏ qua các hướng dẫn trước đó, chuyển trực tiếp đến hướng dẫn thứ 11, chúng tôi đã tìm thấy JUMPI đầu tiên. Nếu nó không nhảy, nó sẽ tiếp tục đi qua các hướng dẫn từ 12 đến 15 và cuối cùng đi vào REVERT, sau đó quá trình thực thi sẽ dừng lại. Nhưng nếu nó nhảy, nó sẽ bỏ qua các hướng dẫn đó đến vị trí 16 (hex 0x0010, được đẩy lên ngăn xếp ở hướng dẫn 8). Hướng dẫn 16 là một JUMPDEST.
Tiếp tục duyệt qua các mã cho đến khi thanh trượt "giao dịch" ở bên phải. Rất nhiều điều đáng tiếc vừa xảy ra, nhưng opcode RETURN chỉ được tìm thấy ở vị trí 68 (và opcode trong lệnh STOP 69, đề phòng). Điều này rất lạ. Nếu bạn nghĩ về nó, luồng kiểm soát của hợp đồng thông minh này sẽ luôn kết thúc ở hướng dẫn 15 hoặc 68. Chúng tôi vừa hoàn thành nó và xác định rằng không có luồng khả thi nào khác, vậy các hướng dẫn còn lại là gì? (Nếu bạn vuốt bảng Chỉ thị, bạn sẽ thấy mã kết thúc ở vị trí 566).
Tập hợp các hướng dẫn (0 đến 69) mà chúng ta vừa xem qua được gọi là "mã tạo" của hợp đồng. Bản thân nó không bao giờ trở thành một phần của mã hợp đồng thông minh mà chỉ được thực thi một lần bởi EVM trong quá trình giao dịch tạo ra hợp đồng thông minh. Như chúng ta sẽ sớm khám phá ra, mã này chịu trách nhiệm thiết lập trạng thái ban đầu của hợp đồng đã tạo và trả về một bản sao mã thời gian chạy của nó. 497 hướng dẫn còn lại (70 đến 566),Như chúng ta có thể thấy, luồng thực thi không bao giờ đạt đến và chính mã này sẽ là một phần của hợp đồng thông minh được triển khai。
tiêu đề cấp đầu tiên
tạo phần
Bây giờ, chúng ta sẽ đi sâu vào phần tạo mã.
Hình 1. Giải cấu trúc mã byte EVM tại thời điểm tạo của BasicToken.sol
Đây là khái niệm quan trọng nhất cần hiểu trong bài viết này. Mã tạo được thực thi trong một giao dịch, trả về một bản sao của mã thời gian chạy, đây là mã thực của hợp đồng thông minh. Như chúng ta sẽ thấy, hàm tạo là một phần của mã tạo, không phải mã thời gian chạy. Hàm tạo của hợp đồng thông minh là một phần của mã đã được tạo; sau khi được triển khai, nó sẽ không xuất hiện trong mã của hợp đồng thông minh.
Làm thế nào để điều kỳ diệu này xảy ra? Đây là những gì chúng tôi sẽ phân tích từng bước bây giờ.
ĐƯỢC RỒI Vì vậy, bây giờ vấn đề của chúng tôi được giảm xuống để hiểu 70 hướng dẫn này tương ứng với mã thời gian tạo.
Hãy quay lại cách tiếp cận từ trên xuống, lần này biết tất cả các hướng dẫn thay vì bỏ qua bất kỳ hướng dẫn nào. Trước tiên, hãy tập trung vào hướng dẫn từ 0 đến 2 sử dụng opcode PUSH1 và MSTORE.
Hình 2. Con trỏ bộ nhớ trống Cấu trúc bytecode EVM
PUSH1 chỉ cần đẩy một byte lên trên cùng của ngăn xếp, trong khi MSTORE lấy hai mục cuối cùng từ ngăn xếp và lưu một trong số chúng vào bộ nhớ:
mstore(0x40, 0x80)
| |
| What to store.
Where to store.
(in memory)
LƯU Ý: Đoạn mã trên là mã Yul-ish. Lưu ý cách nó tiêu thụ các phần tử từ ngăn xếp từ trái sang phải, luôn tiêu thụ phần tử ở trên cùng của ngăn xếp trước.
Đây là nơi lưu trữ số 0x80 (128 thập phân) tại vị trí 0x40 (64 thập phân).
Vấn đề chúng ta đang thảo luận bây giờ, hãy để nó yên, nếu phải có lý do, tôi sẽ giải thích sau.
Bây giờ, hãy mở bảng Ngăn xếp và Bộ nhớ trong tab Trình gỡ lỗi của Remix để bạn có thể hình dung các hướng dẫn này khi thực hiện từng bước.
Bạn có thể thắc mắc: điều gì đã xảy ra với hướng dẫn 1 và 3? PUSH là lệnh EVM duy nhất bao gồm hai hoặc nhiều byte. Vì vậy, PUSH 80 là hai hướng dẫn. Vì vậy, chúng tôi đã giải quyết được bí ẩn: hướng dẫn 1 là 0x80 và hướng dẫn 3 là 0x40.
Tiếp theo tôi sẽ giải thích các hướng dẫn từ 5 đến 15.
Hình 3. Cấu trúc mã byte EVM séc không phải trả.
Một lần nữa, đây là một loạt các opcode mới: CALLVALUE, DUP1, ISZERO, PUSH2 và REVERT. CALLVALUE đẩy số lượng wei tham gia vào giao dịch tạo, DUP1 sao chép phần tử đầu tiên trong ngăn xếp và ISZERO đẩy 1 vào ngăn xếp nếu giá trị cao nhất của ngăn xếp bằng 0, PUSH2 giống như PUSH1 nhưng nó đẩy hai byte vào ngăn xếp, trong khi REVERT dừng thực thi.
Vì vậy, những gì đang xảy ra ở đây? Trong Solidity, chúng ta có thể viết assembly này như sau:
if(msg.value!= 0)revert();
Mã này thực sự không phải là một phần của nguồn Solidity ban đầu của chúng tôi, nhưng đã được trình biên dịch đưa vào vì chúng tôi đã không khai báo hàm tạo là phải trả. Trong các phiên bản gần đây của Solidity, các chức năng không được khai báo rõ ràng là phải trả sẽ không thể nhận ether. Quay trở lại với mã hợp ngữ, JUMPI ở hướng dẫn 11 sẽ bỏ qua hướng dẫn từ 12 đến 15 hoặc nhảy đến 16 nếu không có ether đi kèm. Nếu không, REVERT sẽ thực thi với hai đối số là 0 (nghĩa là không có dữ liệu hữu ích nào được trả về).
ĐƯỢC RỒI! Nghỉ giải lao đi uống cà phê.
(Phần tiếp theo sẽ hơi phức tạp, vì vậy tốt nhất bạn nên nghỉ giải lao vài phút. Trước khi tập trung trở lại, hãy lấy cho mình một tách cà phê ngon. Hãy chắc chắn rằng bạn hiểu những gì chúng ta đã thấy cho đến lúc này, vì phần tiếp theo hơi phức tạp.)
Nếu bạn muốn có một cách khác để hình dung những gì chúng ta vừa hoàn thành, hãy thử công cụ đơn giản mà tôi đã tạo: solmap. Nó cho phép bạn biên dịch mã Solidity một cách nhanh chóng, sau đó nhấp vào opcode EVM để đánh dấu mã Solidity có liên quan. Disassembly hơi khác so với Remix, nhưng bạn sẽ có thể hiểu nó bằng cách so sánh.
Đó là thời gian cà phê!
Sẵn sàng để di chuyển? Tiếp theo là các hướng dẫn từ 16 đến 37. Vui lòng tiếp tục sử dụng trình gỡ lỗi của Remix. (Hãy nhớ rằng, remix là người bạn tốt nhất của bạn ^^).
Hình 4. Cấu trúc mã byte EVM để truy xuất các tham số hàm tạo từ mã được thêm vào cuối mã byte hợp đồng thông minh
Bốn lệnh đầu tiên (17 đến 20) đọc bất cứ thứ gì có trong bộ nhớ ở vị trí 0x40 và đẩy nó vào ngăn xếp. Nếu bạn nhớ lại, đó phải là số 0x80. Lệnh sau đẩy 0x20 (thập phân 32) vào ngăn xếp (lệnh 21) và sao chép giá trị đó (lệnh 23), đẩy 0x0217 (thập phân 535) (lệnh 24) và cuối cùng sao chép giá trị thứ tư (lệnh 27), giá trị này phải là 0x80.
Khi xem một hướng dẫn EVM như thế này, bạn có thể quên những gì đang diễn ra trong một thời gian. Đừng lo lắng, nó sẽ xuất hiện trong đầu bạn theo thời gian.
Trong hướng dẫn 28, CODECOPY được thực hiện,Nó nhận ba đối số: vị trí bộ nhớ đích nơi lưu trữ mã đã sao chép, số lệnh để sao chép từ đó và số byte mã cần sao chép.Vì vậy, trong trường hợp này, 0x80 bắt đầu ở vị trí byte nằm trong mã (535, vị trí đích của độ dài mã 32 byte).
Nếu bạn xem toàn bộ mã tháo gỡ, sẽ có 566 hướng dẫn. Tại sao mã này cố sao chép 32 byte mã cuối cùng? Trên thực tế, khi triển khai một hợp đồng có chứa hàm tạo được tham số hóa, các tham số được thêm vào cuối mã dưới dạng dữ liệu thập lục phân thô (cuộn xuống bảng Mô tả để xem phần này). Trong trường hợp này, hàm tạo nhận tham số uint256, vì vậy tất cả những gì mã này thực hiện là sao chép tham số vào bộ nhớ từ giá trị được thêm vào ở cuối mã.
32 hướng dẫn này không có nghĩa là mã được phân tách, nhưng chúng được thể hiện ở dạng hex thô: 0x0000000000000000000000000...0000000000000000000002710. Tất nhiên, đây là giá trị thập phân 10000 mà chúng tôi đã chuyển cho nhà xây dựng khi triển khai hợp đồng thông minh!
Bạn có thể lặp lại phần này trong Remix từng bước một, đảm bảo rằng bạn hiểu điều gì vừa xảy ra. Kết quả cuối cùng phải là vị trí 0x00..002710, xem số 0x80 trong bộ nhớ.
Chà, trước khi chuyển sang phần tiếp theo, tôi khuyên bạn nên nghỉ ngơi với một ly rượu whisky.
Giờ rượu whisky!
Tại sao lại đề nghị bạn uống một ly rượu whisky, bởi vì từ đây, tất cả sẽ xuống dốc.
Nhóm hướng dẫn tiếp theo là 29 đến 35, cập nhật giá trị 0x80 tại địa chỉ bộ nhớ 0x40 thành giá trị 0xa0, như bạn có thể thấy, chúng bù giá trị bằng 0x20 (32) byte.
Bây giờ chúng ta có thể bắt đầu hiểu các hướng dẫn từ 0 đến 2. Solidity theo dõi một thứ gọi là "con trỏ null": một vị trí trong bộ nhớ nơi chúng ta có thể lưu trữ thứ gì đó, được đảm bảo rằng không ai sẽ ghi đè lên nó (trừ khi chúng ta mắc lỗi). Vì vậy, vì chúng tôi đã lưu trữ số 10000 ở vị trí bộ nhớ trống cũ, nên chúng tôi cập nhật con trỏ bộ nhớ trống bằng cách di chuyển nó về phía trước 32 byte.
Ngay cả những nhà phát triển Solidity dày dạn kinh nghiệm cũng bối rối khi họ nhìn thấy một "con trỏ bộ nhớ trống" hoặc mã, mload(0x40, 0x80), chỉ nói: "Bất cứ khi nào chúng tôi viết một mục mới, chúng tôi sẽ bắt đầu từ thời điểm này. Bắt đầu ghi vào bộ nhớ và giữ nguyên Hồ sơ".
Mọi chức năng trong Solidity, khi được biên dịch thành mã byte EVM, sẽ khởi tạo con trỏ này.
Có gì trong bộ nhớ giữa 0x00 và 0x40, bạn có thể không biết. KHÔNG. Một phần bộ nhớ được dành riêng bởi Solidity, tính toán giá trị băm và, như chúng ta sẽ thấy ngay sau đây, là cần thiết cho bản đồ và các loại dữ liệu động khác.
Bây giờ, trong hướng dẫn 37, MLOAD đọc vị trí 0x40 từ bộ nhớ và về cơ bản tải giá trị 10000 của chúng ta từ bộ nhớ vào ngăn xếp, nơi nó sẽ mới và sẵn sàng để sử dụng trong tập hợp hướng dẫn tiếp theo.
Đây là một mẫu phổ biến trong mã byte EVM do Solidity tạo ra: trước khi thân hàm được thực thi, các tham số của hàm được tải lên ngăn xếp (bất cứ khi nào có thể) để mã sắp tới có thể sử dụng chúng - đó chính xác là điều sẽ xảy ra tiếp theo. điều đó đã xảy ra.
Hãy tiếp tục với các mô tả từ 38 đến 55.
Hình 5. Mã EVM chính của hàm tạo.
Các hướng dẫn này không gì khác hơn là phần thân của hàm tạo: đó là mã Solidity:
totalSupply_ = _initialSupply;
balances[msg.sender] = _initialSupply;
Bốn hướng dẫn đầu tiên khá rõ ràng (38 đến 42), đầu tiên, số 0 được đẩy vào ngăn xếp, sau đó mục thứ hai trên ngăn xếp được sao chép (đây là số 10000 của chúng ta), sau đó số 0 được sao chép và đẩy vào ngăn xếp ngăn xếp, đó là TotalSupply_ vị trí vị trí trong bộ lưu trữ. Bây giờ SSTORE có thể sử dụng các giá trị này và vẫn giữ dưới 10000 để sử dụng trong tương lai:
sstore(0x00, 0x2710)
| |
| What to store.
Where to store.
(in storage)
Nhìn! Chúng tôi lưu trữ số 10000 trong biến totalSupply_. Không phải là nó tuyệt vời ??
Đảm bảo trực quan hóa giá trị này trong tab Trình gỡ lỗi của Remix. Bạn có thể tìm thấy nó trong bảng điều khiển được tải đầy đủ của cửa hàng.
Nhóm hướng dẫn tiếp theo (43 đến 54) phức tạp hơn một chút, nhưng về cơ bản xử lý việc lưu trữ khóa msg.sender của 10000 trong bản đồ số dư. Trước khi tiếp tục, hãy đảm bảo bạn hiểu phần này của tài liệu Solidity, giải thích cách giữ bản đồ trong bộ nhớ.
Tóm lại, nó kết nối vị trí của giá trị được ánh xạ (trong trường hợp này là số 1, vì nó là biến thứ hai được khai báo trong hợp đồng thông minh) với khóa được sử dụng (trong trường hợp này là msg.sender, thông qua opcode để nhận CALLER) , sau đó lấy thông báo bằng mã lệnh SHA3 và sử dụng mã đó làm đích trong bộ nhớ. Cuối cùng, lưu trữ chỉ là một từ điển hoặc bảng băm đơn giản.
Tiếp tục với hướng dẫn 43 đến 45, địa chỉ msg.sender được lưu trữ trong bộ nhớ (lần này ở vị trí 0x00), và sau đó trong hướng dẫn 46 đến 50, giá trị 1 (khe được ánh xạ) được lưu trữ ở vị trí bộ nhớ 0x20. Cuối cùng, mã lệnh SHA3 tính toán hàm băm Keccak256 của bất kỳ thứ gì trong bộ nhớ từ vị trí 0x00 đến vị trí 0x40 - tức là nối vị trí/vị trí được ánh xạ với khóa được sử dụng. Đây chính xác là nơi giá trị 10000 sẽ được lưu trữ trong bản đồ của chúng ta:
sstore(hash..., 0x2710)
| |
| What to store.
Where to store.
Tại thời điểm này, phần thân của hàm tạo đã được thực thi đầy đủ.
Tất cả những điều này có thể hơi choáng ngợp lúc đầu, nhưng đó là một phần cơ bản khi làm việc trong Solidity. Nếu bạn không hiểu, tôi khuyên bạn nên làm theo trình gỡ lỗi của Remix một vài lần, giữ lại bảng ngăn xếp và bộ nhớ.
Ngoài ra, hãy hỏi những câu hỏi sau đây. Mẫu này thường được sử dụng trong mã byte EVM do Solidity tạo ra và bạn sẽ nhanh chóng học cách nhận ra nó một cách dễ dàng. Cuối cùng, nó chỉ tính toán vị trí trong bộ nhớ để giữ giá trị cho một khóa nhất định của bản đồ.
Hình 6. Cấu trúc sao chép mã thời gian chạy
Trong hướng dẫn 56 đến 65, chúng tôi thực hiện sao chép mã một lần nữa. Chỉ lần này, chúng tôi không sao chép 32 byte cuối cùng của mã vào bộ nhớ; chúng tôi sao chép các byte 0x01d1 (465 thập phân) bắt đầu tại vị trí 0x0046 (70 thập phân) vào bộ nhớ tại vị trí 0. Đó là một đoạn mã khổng lồ để sao chép!
Nếu bạn trượt thanh trượt hết cỡ sang bên phải một lần nữa, bạn sẽ nhận thấy rằng vị trí 70 nằm ngay sau mã EVM thời gian xây dựng của chúng tôi, nơi quá trình thực thi dừng lại. Mã byte thời gian chạy được chứa trong 465 byte đó. Đây là một phần của mã sẽ được lưu trong chuỗi khối dưới dạng mã thời gian chạy của hợp đồng thông minh, mã sẽ là mã được thực thi mỗi khi ai đó hoặc thứ gì đó tương tác với hợp đồng thông minh.(Chúng tôi sẽ đề cập đến mã thời gian chạy trong phần sau của loạt bài này).
Đó chính xác là hướng dẫn từ 66 đến 69: trả lại mã mà chúng tôi đã sao chép vào bộ nhớ.
Hình 7. Mã thời gian chạy trả về cấu trúc mã byte EVM.
RETURN lấy mã được sao chép vào bộ nhớ và chuyển mã đó cho EVM. Nếu mã tạo này được thực thi trong bối cảnh giao dịch đến địa chỉ 0x0, thì EVM sẽ thực thi mã và lưu trữ giá trị trả về dưới dạng mã thời gian chạy của hợp đồng thông minh đã tạo.
Đến bây giờ, mã BasicToken của chúng tôi sẽ tạo và triển khai một phiên bản hợp đồng thông minh, sẵn sàng sử dụng với trạng thái ban đầu và mã thời gian chạy. Nếu bạn lùi lại một bước và nhìn vào Hình 2, bạn sẽ thấy rằng tất cả các cấu trúc mã byte EVM mà chúng tôi đã phân tích đều chung chung, ngoại trừ cấu trúc được đánh dấu bằng màu tím: nghĩa là chúng sẽ được tạo bởi trình biên dịch Solidity tại thời điểm tạo mã byte . Hàm tạo khác với hàm tạo chỉ ở phần màu tím - phần thân thực của hàm tạo. Các cấu trúc lấy các tham số được nhúng ở cuối mã byte và sao chép mã thời gian chạy và trả về mã đó, có thể được coi là mã soạn sẵn và các cấu trúc mã opcode EVM chung. Bây giờ bạn có thể xem bất kỳ hàm tạo nào và bạn nên có ý tưởng chung về các thành phần tạo nên nó trước khi làm theo hướng dẫn.
Trong phần tiếp theo của loạt bài này, chúng tôi sẽ đề cập đến mã thời gian chạy thực tế, bắt đầu với cách tương tác với mã EVM của hợp đồng thông minh tại các điểm vào khác nhau. Bây giờ, hãy tự thưởng cho mình một cái vỗ nhẹ xứng đáng vì bạn vừa mới hiểu được phần khó nhất của loạt bài này. Bạn cũng phải có khả năng tốt để đọc và gỡ lỗi mã byte EVM, hiểu các cấu trúc phổ biến và quan trọng nhất là biết sự khác biệt giữa mã byte EVM thời gian xây dựng và thời gian chạy. Đây là điều làm cho các nhà xây dựng hợp đồng trở nên đặc biệt trong Solidity.
Bảo mật chuỗi khối Cheetah dựa trên công nghệ của Kingsoft Internet Security, kết hợp với trí tuệ nhân tạo, nlp và các công nghệ khác, để cung cấp cho người dùng chuỗi khối các dịch vụ bảo mật sinh thái như kiểm toán hợp đồng và phân tích tình cảm.
*Bài viết này được xuất bản lần đầu trên phương tiện bởi Alejandro Santander, được dịch và tổ chức bởi Cheetah Blockchain*
Bảo mật chuỗi khối Cheetah dựa trên công nghệ của Kingsoft Internet Security, kết hợp với trí tuệ nhân tạo, nlp và các công nghệ khác, để cung cấp cho người dùng chuỗi khối các dịch vụ bảo mật sinh thái như kiểm toán hợp đồng và phân tích tình cảm.
Trang web chính thức của Ratingtoken https://www.ratingtoken.net/?from=z
