Qtum量子鏈 x86虛擬機器 編碼格式
暴露給介面和智慧合約程式碼的編碼格式如下
執行時長度編碼格式
(Runtime Length Encoding Format)
在區塊鏈上,x86 所有的合約資料都使用執行時長度編碼(Runtime Length Encoding,RLE)來表示 0 位元組。而非零資料不被編碼。當在位元組流中遇到 0 時,它被編碼成一個長度。該長度表示跟隨它的零位元組數。RLE 的長度為 0 是明確禁止的,其結果是生成無法包含在區塊或記憶體池裡的無效交易。
RLE 有效載荷的字首是一個 32 bit 整數。其中 24 bit 是解碼大小欄位。如果解碼大小與解碼的有效載荷不完全匹配,則交易無效。其餘 8 bit 保留為版本號,以便實現其他壓縮格式。
注意,有效負載的大小可以在不分配額外記憶體的情況下進行驗證。
相關程式碼註釋:
//格式:解碼的有效載荷長度(uint32_t)| 有效載荷 //壓縮只壓縮 0 位元組資料 //當在位元組流中遇到 0x00 時,它用執行時長度編碼 //編碼示例: // 0x00 00 00 00 - > 0x00 04 // 0x00 - > 0x00 01 // 0x00 00 - > 0x00 02 // 0x00(重複500次) - > 0x00 0xFF 0x00 0xF5 //作為編碼過程的一部分,有效載荷的前 32 bit 是字首
RLE 編碼將使用在其他任何格式之上。
編碼和解碼的程式碼已包含在 x86Lib 中,不過在其他程式語言中實現都應該是很簡單的。
由於目前長度欄位沒有提供任何好處,以後可能會移除掉。我們的初衷是使用記憶體分配配置檔案而不是要多次手動來分配記憶體。但不管怎樣,現在解碼後的資料儲存在 Qtum 的備份資料庫中,可以實現一次性解碼。
智慧合約位元組碼格式
(Smart contract bytecode format)
在編碼和儲存合約的方式上,x86 VM 和 EVM 不一樣。EVM 中是直接給出了操作碼陣列,然後執行這些操作碼。它們決定了這些操作碼和資料的哪一部分可以作為智慧合約位元組碼實際儲存在永久儲存器中。
但在Qtum 的 x86 VM 中更為簡單,也更容易理解。這是一種自定義二進位制格式,分為選項(options,即智慧合約配置資訊等),初始化資料(initialized data)和程式碼(code)三個部分。整個二進位制檔案直接儲存在永久儲存器中。這可以避免在 EVM 合約裡寫合約保護程式的情況。儘管這在 EVM 中不太複雜,但是在 x86 中開發還這樣寫,就有點奇怪了。因為你可能要寫兩個完全獨立的程式來防止地址記憶體錯誤等等。
x86Lib 測試平臺提供了將 ELF 程式轉換為這種自定義二進位制格式的工具。由於 ELF 包含大量潛在的複雜性,我們不打算直接使用 ELF。不過大多數 ELF 程式解析起來也是相當簡單的。所以我們沒有實現類似“你不能在ELF中使用這些功能”來處理特殊情況,而是使用更簡單的自定義格式,並提供轉換工具。基於此,我們還可以提供對扁平二進位制格式,PE和其他二進位制格式的支援。
自定義格式是一個扁平位元組陣列 + 一個字首,字首 map 儲存了每個部分的長度:
//可用的資料欄位只是一個扁平的資料欄位,因此我們需要自定義格式來儲存 //程式碼,資料和選項 //因此,它把 4 個 uint32 整數作為字首。 //第一個是選項的大小,第二個是程式碼的大小,第三個是資料的大小 //第四個未使用(暫時),但保留用於填充和對齊 struct ContractMapInfo { //這個結構是達成共識的關鍵 CONSENSUS-CRITICAL //不要新增或刪除欄位,也不要重新排序! uint32_t optionsSize; uint32_t codeSize; uint32_t dataSize; uint32_t reserved; } __attribute__((__packed__));
在這個字首 map 後,就只剩下一個扁平的位元組陣列了。
具體過程:
-
LOCATION = 0,這是 ContractMapInfo 之後的第一個位元組
-
將選項資料複製到從 LOCATION 到 LOCATION + optionsSize 的緩衝區中。通過 optionsSize 移動 LOCATION 指標
-
將程式碼資料複製到從 LOCATION 到 LOCATION + codeSize 的緩衝區中。 通過 codeSize 移動 LOCATION 指標
-
以此類推......
對每部分的說明:
-
選項。由於當前未使用選項,optionsSize 必須為 0.之後它可能會包含依賴關係圖,可信任的合約選項以及其他專用配置資料。除非選項明確指明,否則資料不會直接暴露給合約程式碼
-
程式碼是隻讀可執行程式碼。它目前最大為 1Mb,將被載入到合約的記憶體空間 0x1000 處。合約修改這部分記憶體的內容是不允許的。執行這部分以外的程式碼可能要額外的 gas,因為這可能使 JIT 和其他優化更加困難。現在這部分固定大小為1Mb,codeSize 之外的任何資料都設定為0。
-
資料分初始化資料和讀寫資料。它也有1Mb的限制,將被載入到合約的記憶體空間 0x100000 處。在一些文件中,這部分儲存也稱為“臨時儲存器”。現在這部分空間固定大小為1Mb,dataSize 以外的任何資料都設定為0. 同時, ELF 檔案的“.BSS”部分(未初始化的記憶體)放在記憶體區域的結尾部分,但由於沒有與之關聯的資料,所以沒有必要向 VM 提供有關 .BSS 的任何資訊。ELF檔案轉換器會檢查資料大小+ bss 大小是否不超過1Mb。
智慧合約交易呼叫格式
(Smart contract transaction-call format)
合約有一個呼叫堆疊,可以用來發送引數呼叫合約以及呼叫合約返回資料。但是,就驗證目的而言,一個交易中有大量資料實體是非常重要的。因此,智慧合約交易呼叫格式中實際上只有1個數據欄位(與目前 EVM 相同)。不過,提供ABI是為了簡化智慧合約的解析任務。當然也可以忽略這個ABI,只使用一個要傳遞的位元組陣列來代替。
具體過程:
-
LOCATION = 0,資料的第一個位元組
-
從 LOCATION 讀取 32 位整數,記作 SIZE
-
LOCATION 增加 2(整數)
-
分配大小為 SIZE 的緩衝區,然後將記憶體從 LOCATION 複製到 LOCATION + SIZE
-
LOCATION 按 SIZE 遞增
-
重複,直到沒有資料
系統呼叫介面
Qtum 的系統呼叫方式和 Linux 非常相似。它根本不使用堆疊,只使用暫存器。如果需要傳比暫存器更多的引數,那麼一個暫存器需要在某種結構中指向這些存有引數的記憶體區域。目前中斷碼 0x40 用於 Qtum 所有的系統呼叫。
呼叫暫存器:
EAX - 系統呼叫碼:
-
EBX, ECX, EDX, ESI, EDI, EBP - 引數 1-6
-
ESP, EFLAGS - 未使用
系統呼叫返回暫存器:
-
EAX - 返回值(0表示成功,不包括返回長度的操作)
-
EBX,ECX,EDX,ESI,EBP,ESP,EFLAGS - 未修改
libqtum 封裝了一個函式簡化 C ABI 介面:
.global __qtum_syscall // long syscall(long number, long p1, long p2, long p3, long p4, long p5, long p6) __qtum_syscall: push %ebp mov %esp,%ebp push %edi push %esi push %ebx mov 8+0*4(%ebp),%eax mov 8+1*4(%ebp),%ebx mov 8+2*4(%ebp),%ecx mov 8+3*4(%ebp),%edx mov 8+4*4(%ebp),%esi mov 8+5*4(%ebp),%edi mov 8+6*4(%ebp),%ebp int $0x40 pop %ebx pop %esi pop %edi pop %ebp ret
不過,因為 libqtum 為每個系統呼叫都封裝了易用的函式,大多數人永遠不用對系統呼叫執行任何操作。