Qtum量子链 x86虚拟机 编码格式

Qtum量子链 x86虚拟机 编码格式

http://earlz.net/view/2018/10/22/2252/exposed-formats-in-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 为每个系统调用都封装了易用的函数,大多数人永远不用对系统调用执行任何操作。