以太坊虚拟机按位运算指令
EVM定义了8条按位运算指令,见下表:
按位运算指令 | 操作码 | 说明 |
---|---|---|
AND | 0x16 | 按位与 |
OR | 0x17 | 按位或 |
XOR | 0x18 | 按位异或 |
NOT | 0x19 | 按位取反 |
BYTE | 0x1A | 取第n个字节 |
SHL | 0x1B | 左移 |
SHR | 0x1C | 逻辑右移 |
SAR | 0x1D | 算术右移 |
下面是按位运算指令的操作码分布图:
AND、OR、XOR、NOT
AND、OR、XOR指令从栈顶弹出两个元素,进行按位运算,然后把结果推入栈顶。以AND指令为例,下面是它的操作示意图:
NOT指令将栈元素按位取反,下面是它的操作示意图:
这四条指令分别与Solidity语言里的&
、|
、^
和~
运算符直接对应,下面的Solidity代码演示了这四条指令的具体应用(读者可以运行solc --asm --opcodes bitwise_demo1.sol
命令观察编译器生成的字节码):
// bitwise_demo1.sol
pragma solidity ^0.4.24;
contract C {
function test() public pure {
int s1; int s2; int s3;
uint u1; uint u2; uint u3;
s3 = s1 & s2; // ADD
s3 = s1 | s2; // OR
s3 = s1 ^ s2; // XOR
s3 = ~ s1; // NOT
u3 = u1 & u2; // ADD
u3 = u1 | u2; // OR
u3 = u1 ^ u2; // XOR
u3 = ~ u1; // NOT
}
}
BYTE
BYTE指令先后从栈顶弹出n和x,取x的第n个字节并推入栈顶。由于EVM的字长是32个字节,所以n在[0, 31]区间内才有意义,否则BYTE的运算结果就是0。另外,字节是从左到右数的,因此第0个字节占据字的最高位8个比特。以n=1为例,下面是BYTE指令操作示意图:
读者可以通过下面的Solidity代码观察BYTE指令的用法:
// byte_demo2.sol
pragma solidity ^0.4.24;
contract C {
function test() public pure {
bytes32 a;
bytes1 b = a[31]; // ... BYTE ...
}
}
SHL、SHR、SAR
这三条位移指令是由EIP-145引入的,从Constantinople虚拟机开始支持。这三条指令都是先后从栈顶弹出两个数n和x,其中x是要进行位移操作顶数,n是位移比特数,然后把结果推入栈顶。以左移指令SHL为例,下面是它的操作示意图:
SHR和SAR的区别在于,前者执行逻辑右移(空缺补0),后者执行算术右移(空缺补符号位)。下表总结了这三条位移指令对于操作数的解释,以及计算结果(这里^
表示指数运算):
指令 | x | n | 结果 |
---|---|---|---|
SHL | unsigned | unsigned | (arg2 * 2^arg1) mod 2^256 |
SHR | unsigned | unsigned | floor(arg2 / 2^arg1) |
SAR | signed | unsigned | floor(arg2 / 2^arg1) |
Solidity语言提供了<<
和>>
运算符,下表总结了这两个运算符的含义(这里**
表示指数运算):
运算符 | 解释 |
---|---|
x >> n | x * 2**y |
x << n | x / 2**y |
在Constantinople之前,位移运算符使用EXP、MUL、DIV、SDIV等指令实现;从Constantinople开始,位移运算符可以使用位移指令实现。不过请读者注意,<<
运算符可以直接编译成SHL指令,但是由于取整方式不同,所以>>
运算符并不能直接编译成SAR指令,详见EIP-145和Solidity文档。读者可以通过下面的Solidity代码观察位移指令的用法(可以通过--evm-version
选项告诉Solidity编译器目标EVM版本,例如solc --asm --opcodes --evm-version constantinople bitwise_demo2.sol
,如不指定,默认是byzantium):
// bitwise_demo2.sol
pragma solidity ^0.4.24;
contract C {
function test() public pure {
int s1; int s2;
uint u1; uint u2;
uint n;
u2 = u1 << n; // SHL
s2 = s1 << n; // SHL
u2 = u1 >> n; // SHR
s2 = s1 >> n; // EXP、SDIV
//s2 = s1 >>> n; // SHR?
}
}
总结
本文介绍了EVM按位运算指令,下一篇文章将介绍EVM比较操作指令。如果大家对编程语言虚拟机有更多的兴趣,请关注我写的《自己动手写Java虚拟机》,以及马上将要出版的《自己动手实现Lua:虚拟机、编译器、标准库》。