1.概述本章概述了TVM的主要功能和设计原则,后续章节将详细讨论每个主题。1.0比特串的符号表示在本文中,我们将使用以下符号表示比特串(bitstrings),即由二进制数字(比特)0和1组成的有限字符串。1.0.1十六进制表示法:当比特串的长度是4的倍数时,我们将其划分
本章概述了 TVM 的主要功能和设计原则,后续章节将详细讨论每个主题。
在本文中,我们将使用以下符号表示比特串(bitstrings),即由二进制数字(比特)0 和 1 组成的有限字符串。
1.0.1 十六进制表示法:当比特串的长度是 4 的倍数时,我们将其划分为四比特一组,并以 0-9 和 A-F 的十六进制数字表示每组,如:
1.0.2 长度非四的倍数的比特串:如果比特串的长度不是 4 的倍数,我们在其末尾添加一个 1 和若干(可能是 0 个) 0,使其长度变为 4 的倍数,然后按上述方式转换为十六进制字符串。为了表明此类转换,我们在十六进制字符串末尾加上特殊符号“”作为补全标记。
例如,8A 对应的二进制串是 10001010,而 8A 和 8A0 均对应二进制串 100010。空比特串可以表示为‘’、‘8’、‘0’、‘’或‘00_’。
1.0.3 强调字符串是比特串的十六进制表示:有时我们需要强调某个十六进制字符串是比特串的表示。此时,我们可以在字符串前加上 x(如 x8A)或使用 x{和}将其括起来(如 x{2D9_} 表示 00101101100)。
1.0.4 比特串的序列化为八位字节序列:当需要将比特串表示为由 8 位字节(取值为 0 到 255 的整数)组成的序列时,可以类似处理:将比特串划分为 8 位一组,并将每组作为无符号整数解释。如果比特串的长度不是 8 的倍数,则在末尾添加一个二进制 1 和最多 7 个二进制 0。处理后的字节序列即为比特串的等效表示。
首先,TVM 是一种栈机。这意味着与将值存储在“变量”或“通用寄存器”中不同,TVM 将值保存在一个栈中(后进先出,LIFO),至少从“低级别”(TVM)的角度看是这样。
大多数操作和用户定义的函数从栈顶获取参数,并将结果替换这些参数。例如,整数加法原语(内置操作)ADD 不需要任何参数来描述哪些寄存器或立即数应相加,或者结果应存储在哪里。相反,它从栈顶取出两个值,将它们相加,并将它们的和放入栈顶。
高层的智能合约编程语言可能会为编程方便而创建变量的“可见性”;然而,这些操作在编译为 TVM 机器代码时,所有变量的值仍会保存在 TVM 的栈中。
1.1.1 TVM 值
可以存储在 TVM 栈中的实体称为 TVM 值,或者为简便起见称为“值”。它们属于预定义值类型中的一种,每个值都属于某个具体的值类型。值在栈中始终与唯一确定其类型的标签一起保存,所有内置的 TVM 操作(即原语)只接受特定类型的值。
例如,整数加法原语 ADD 只接受两个整数值,并返回一个整数结果。不能用两个字符串代替两个整数来传递给 ADD,期望它们会被拼接,或被隐式转换为它们的十进制整数值。任何这样的操作都将导致运行时的类型检查异常。
1.1.2 静态类型、动态类型和运行时类型检查
从某些方面来看,TVM 执行了一种动态类型检查,即运行时的类型检查。然而,这并不意味着 TVM 代码是像 PHP 或 JavaScript 那样的“动态类型”语言,因为所有的原语只接受并返回预定义类型的值,每个值严格属于一种类型,且值从不被隐式转换为另一种类型。
与传统的微处理器机器代码相比,TVM 的值标记机制防止了使用字符串的地址作为数字,或错误地使用数字作为字符串的地址,避免了由无效内存访问引发的内存损坏或段错误。这一特性对于执行智能合约的虚拟机来说非常重要。因此,TVM 对所有值类型进行标签检查,而不是根据操作的需要重新解释寄存器中的位序列,提供了一种额外的运行时类型安全机制。
一种替代方式是,在允许智能合约代码在虚拟机中执行之前,甚至在将其上传到区块链中作为智能合约的代码之前,分析其类型正确性和类型安全性。然而,对于图灵完备的机器来说,对代码进行静态类型检查是一个耗时且复杂的问题,可能与图灵机的停机问题等价,因此我们希望在区块链智能合约的背景下避免这种复杂性。
需要注意的是,用户始终可以将静态类型的高级智能合约语言编译为 TVM 代码(我们也确实预期大部分 TON 智能合约会以这样的语言编写),就像可以将静态类型语言编译为常规机器代码(例如 x86 架构)一样。如果编译器工作正常,生成的机器代码将永远不会引发运行时类型检查异常。所有在 TVM 中处理的值的类型标签都将是预期的,除了 TVM 的运行时生成和验证这些类型标签会略微降低代码执行速度外,这些标签在分析 TVM 代码时可以被安全地忽略。
1.1.3 值类型的初步列表
TVM 支持的值类型的初步列表如下:
此列表并不完整,未来可能会在不破坏旧 TVM 代码的前提下进行扩展。进一步的值类型扩展和向后兼容性机制将在后续章节中讨论。
TVM 指令,也称为原语,有时也被称为(内置)操作,它们是 TVM 中可以存在的、最小的原子操作。这些指令根据它们处理的值类型(参见 1.1.3)分为几类。最重要的类别包括:
栈(操作)原语 — 用于在 TVM 栈中重新排列数据,使得其他原语和用户定义的函数可以以正确的参数顺序被调用。与大多数其他原语不同,栈原语是多态的,也就是说它们可以操作任意类型的值。
元组(操作)原语 — 用于构建、修改和解构元组。与栈原语类似,它们也是多态的。
常量或字面量原语 — 将“常量”或“字面量”值推入栈中,这些值嵌入在 TVM 代码中,为其他原语提供参数。它们与栈原语类似,但它们的通用性较低,因为它们仅适用于特定类型的值。
算术原语 — 执行整数类型的值的常规整数算术运算。
单元(操作)原语 — 用于创建新单元并向其存储数据(单元创建原语),或从先前创建的单元读取数据(单元解析原语)。由于 TVM 的所有内存和持久存储都由单元组成,这些单元操作原语实际上相当于其他架构中的“内存访问指令”。单元创建原语通常与生成器类型的值一起使用,而单元解析原语则与切片类型的值一起使用。
延续与控制流原语 — 用于创建和修改延续,以及以不同方式执行现有的延续,包括条件和重复执行。
自定义或应用特定原语 — 用于高效地执行应用程序(在我们的案例中是 TON 区块链)所需的特定高级操作,如计算哈希函数、执行椭圆曲线加密、发送新的区块链消息、创建新的智能合约等。这些原语类似于标准库函数,而不是微处理器指令。
尽管 TVM 是一种栈机,有些不经常更改、但几乎所有函数都需要的值,最好存储在某些特殊寄存器中,而不是栈顶。否则,管理这些值将需要过多的栈重排操作。
为此,TVM 模型除了栈外,还包括最多 16 个特殊控制寄存器,标记为 c0 到 c15,或 c(0) 到 c(15)。原始版本的 TVM 仅使用其中的一部分寄存器,其余寄存器可能会在将来支持。
1.3.1 存储在控制寄存器中的值
存储在控制寄存器中的值与存储在栈中的值类型相同。然而,某些控制寄存器只接受特定类型的值,尝试加载其他类型的值将会导致异常。
1.3.2 控制寄存器列表
TVM 的原始版本定义并使用以下控制寄存器:
更多的控制寄存器可能会在未来根据特定的 TON 区块链或高级编程语言的需求定义。
TVM 的整体状态由以下几个部分组成:
注意,这里没有“返回栈”来保存所有先前调用但未完成的函数的返回地址。取而代之的是使用控制寄存器 c0,原因将在 4.1.9 中解释。
此外,TVM 没有通用寄存器,因为 TVM 是一个栈机(参见 1.1)。因此,上述列表可以概括为“栈、控制、延续、代码页和 gas”(SCCCG),类似于经典的 SECD 机器状态(“栈、环境、控制、转储”),这确实构成了 TVM 的整体状态。
TVM 的所有算术原语都对栈顶的整数值进行操作,并将结果(同为整数类型)放回栈中。回顾一下,整数代表的是 -2<sup>256</sup> ≤ x < 2<sup>256</sup> 范围内的整数值,此外还包含一个特殊的 NaN(非数字)值。
如果结果超出了支持的整数范围,或者参数之一是 NaN,那么结果将被替换为 NaN,且默认情况下会生成整数溢出异常。然而,某些特殊的“安静”版本的算术操作只会产生 NaN 并继续执行。如果这些 NaN 被用于非“安静”的算术操作或非算术操作,将触发整数溢出异常。
1.5.1 无自动整数转换
注意,TVM 中的整数是“数学”意义上的整数,而不是像其他机器代码设计中那样,使用不同的原语时将 257 位的字符串重新解释。例如,TVM 只有一个乘法原语 MUL,而不像流行的 x86 架构那样有两个乘法原语(MUL 用于无符号乘法,IMUL 用于有符号乘法)。
1.5.2 自动溢出检查
TVM 的所有算术原语都会执行结果的溢出检查。如果结果不适合整数类型,它将被替换为 NaN,通常还会引发异常。特别是,结果不会像大多数硬件机器代码架构那样自动进行模 2<sup>256</sup> 或模 2<sup>257</sup> 的运算。
1.5.3 自定义溢出检查
除了自动溢出检查,TVM 还提供了自定义溢出检查原语 FITS n 和 UFITS n(其中 1 ≤ n ≤ 256)。这些原语用于检查栈顶的值是否在 -2<sup>n-1</sup> ≤ x < 2<sup>n-1</sup> 或 0 ≤ x < 2<sup>n</sup> 范围内,如果不在此范围内,则将值替换为 NaN,并(可选)生成整数溢出异常。这极大简化了任意 n 位整数类型的实现,无论是有符号还是无符号类型:程序员或编译器必须在每次算术操作之后插入适当的 FITS 或 UFITS 原语(这更合理,但需要更多检查),或者在存储计算值并返回函数之前插入这些原语。这对于智能合约尤其重要,因为意外的整数溢出往往是错误的最常见原因之一。
1.5.4 模 2<sup>n</sup> 还原
TVM 还提供了一个原语 MODPOW2 n,它将栈顶的整数按 2<sup>n</sup> 取模,结果在 0 到 2<sup>n</sup>-1 之间。
1.5.5 整数是 257 位而非 256 位
现在可以理解为什么 TVM 中的整数是带符号的 257 位而不是 256 位。原因在于它是包含 256 位有符号整数和 256 位无符号整数的最小整数类型,不需要根据所使用的操作自动重新解释相同的 256 位字符串。
1.5.6 除法与取整
TVM 最重要的除法原语是 DIV、MOD 和 DIVMOD。它们从栈中取出两个整数参数 x 和 y(y 在栈顶,x 在 y 之下),计算商 q 和余数 r(即两个整数,使得 x = yq + r 且 |r| < |y|),然后返回 q、r 或两者。如果 y 为 0,所有期望的结果将被替换为 NaN,且通常会触发整数溢出异常。
TVM 的除法实现与大多数其他实现不同,尤其是在取整方面。默认情况下,这些原语向负无穷大取整,即 q = ⌊x/y⌋,且 r 与 y 同号(大多数常规实现使用“向零取整”,即 r 与 x 同号)。除了这种“向下取整”,还提供了其他两种取整模式,分别是“向上取整”(q = ⌈x/y⌉,r 和 y 异号)和“最近取整”(q = ⌊x/y + 1/2⌋ 且 |r| ≤ |y|/2)。这些取整模式由带有 C 和 R 后缀的其他除法原语选择。例如,DIVMODR 使用最近取整模式来计算商和余数。
1.5.7 组合乘除、乘移和移除操作
为了简化定点算术的实现,TVM 支持具有双长度(即 514 位)中间乘积的组合乘除、乘移和移除操作。例如,MULDIVMODR 从栈中取出三个整数参数 a、b 和 c,首先使用 514 位的中间结果计算 ab,然后将 ab 除以 c,结果四舍五入为最接近的整数。如果 c 为 0 或商不适合整数类型,则返回两个 NaN,或生成整数溢出异常,具体取决于使用的是安静版还是非安静版的操作。否则,商和余数都将被推入栈中。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!