技术入门 | Solidity编程语言 : 基本编译原理介绍与添加新指令
本文目标
本文的主要目的 :1、了解solidity的基本编译原理 2、通过示例的方式了解如何添加新的指令,不会涉及到solidity语言的语法讲解。solidity简介
solidity是智能合约的开发语言,是一种语法类似于javascript的高级语言。合约源码经过编译生成虚拟机代码运行在虚拟机中。开发文档: https://solidity.readthedocs.io/en/latest/introduction-to-smart-contracts.html
常用IDE: http://remix.ethereum.org/ #包含了开发环境,编译器,调试器
solidity源码: https://github.com/ethereum/solidity
solidity合约实例
合约代码
下面的solidity例程是存储并获取块号的智能合约。通过发送交易调用set接口设置块号到storedData中,然后通过静态调用get接口获取存储的storedData。pragma solidity >=0.5.0;contract storenumber{
uint storedData=0;
function set() public {
storedData = block.number;
}
function get() public view returns (uint) {
return storedData;
}
}
abi,data,opcodes
以上代码在remix: http://remix.ethereum.org/ 中使用0.5.1 commit版本编译生成abi=[{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]
data="0x60806040526000805534801561001457600080fd5b5060c2806100236000396000f3fe6080604052600436106043576000357c0100000000000000000000000000000000000000000000000000000000900480636d4ce63c146048578063b8e010de146070575b600080fd5b348015605357600080fd5b50605a6084565b6040518082815260200191505060405180910390f35b348015607b57600080fd5b506082608d565b005b60008054905090565b4360008190555056fea165627a7a72305820825c534e94b487410e10fa0ba5da11584c0b0ad2bd9e56397a3dfa89e504ee1f0029"
opcodes="
固定指令:PUSH1 0x80 PUSH1 0x40 MSTORE
变量:PUSH1 0x0 DUP1 SSTORE //对应的storedData=0
内联函数:CALLVALUE DUP1 ISZERO PUSH2 0x14 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP //用于出错回滚
部署代码指令:PUSH1 0xC2 DUP1 PUSH2 0x23 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID //部署合约的核心指令
固定指令:PUSH1 0x80 PUSH1 0x40 MSTORE
固定指令:PUSH1 0x4 CALLDATASIZE LT //用于校验input大小。
加载合约代码:PUSH1 0x43 JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV DUP1 PUSH4 0x6D4CE63C EQ PUSH1 0x48 JUMPI DUP1 PUSH4 0xB8E010DE EQ PUSH1 0x70 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST
内联函数:CALLVALUE DUP1 ISZERO PUSH1 0x53 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP
get函数:PUSH1 0x5A PUSH1 0x84 JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST
内联函数:CALLVALUE DUP1 ISZERO PUSH1 0x7B JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP
set函数:PUSH1 0x82 PUSH1 0x8D JUMP JUMPDEST STOP JUMPDEST PUSH1 0x0 DUP1 SLOAD SWAP1 POP SWAP1 JUMP JUMPDEST NUMBER PUSH1 0x0 DUP2 SWAP1 SSTORE POP JUMP INVALID
其他指令:LOG1 PUSH6 0x627A7A723058 KECCAK256 DUP3 0x5c MSTORE8 0x4e SWAP5 0xb4 DUP8 COINBASE 0xe LT STATICCALL SIGNEXTEND 0xa5 0xda GT PC 0x4c SIGNEXTEND EXP 0xd2 0xbd SWAP15 JUMP CODECOPY PUSH27 0x3DFA89E504EE1F0029000000000000000000000000000000000000 " //(具体作用还不了解)
上述abi,data是在部署合约和执行合约需要的数据。其中abi包含了合约中用到的函数名,函数的输入输出,与函数的属性。opcodes是虚拟机要执行的具体代码指令,data是opcodes的16进制,二者之间可以互相转化。下面介绍下如何生成abi与opcodes。
solidity编译原理简述
这里以上述合约代码为例,简单介绍下解析流程1、 以字符串的形式读入完整合约代码,转第2步;
2、 去除字符串前的空格,然后遍历字符串,并以 空格,‘{’,'}', ';' ,'(',')'等为分隔符将字符串进行分割,然后与TOKEN_LIST中定义的TOKEN进行对比,并替换为应的TOKEN,转第3步。
3、 第一个TOKEN是pragma,然后以pragma为开始,直到 ';' 结束,确定语言为solidity,版本号大于等于0.5.0,并比较当前编译器版本是否匹配,转第4步。
4、 继续遍历,TOKEN为 contract ,(这里contract,interface,library的处理是一样的),然后从contract开始,确定下一个字符串storenumber为contractname,继续遍历,从 ‘{’ 开始,(中间处理过程转第5步),到配对的 ‘}’ 结束,此时确定了合约名为storenumber的合约内容,转第9步。
5、 继续遍历,TOKEN为 uint ,判断为数据类型,以 ‘ ;’ 为结尾,确定数据类型为uint,类型名 为 storedData,转第6步
6、 继续遍历,TOKEN为function,后续字符串set为函数名,以‘(’,开始,以 ‘)’为终确定input为空,继续遍历TOKEN为public,确定函数属性,继续遍历TOKEN为‘{’,以配对的‘}’为结束,确定函数体,转第7步。
7、 继续遍历,TOKEN为function,处理逻辑与第6步相同,但是增加了view 属性与returns,returns的解析结果对应了abi中的outputs,转第8步。
8、 继续遍历遇到与合约初始‘{’ 配对的‘}’,转第4步继续处理。
9、 遍历结束,进行合法性检查(语法检查,命名规则检查,指令检查等),转第10步。
10、 开始编译合约,即opcodes的生成过程。编译过程可分成三个过程,转第11步。
11、 编译初始化。初始化指令是固定的:PUSH1 0x80 PUSH1 0x40 MSTORE。然后取出所有的状态变量,这里的状态变量会被编译为: PUSH1 0x0 DUP1 SSTORE,转到第12步。
备注: 1、这里的指令并不是一开始就是这样,而是后期经过翻译过的,比如PUSH1 0x80在这里的正确表示方式是AssembllyItem(type:pushdata,data:0x80),之后经过token,instruction的对应转化为指令 2、状态变量指令PUSH1 0x0 DUP1 SSTORE 表示 初始化变量为值为零,变量位置偏移为0。如果代码中初始化为1,这里的指令会编译成PUSH1 0x1 PUSH1 0x0 SSTORE。如果增加一个变量初始化为3,则会被编译为PUSH1 0x1 PUSH1 0x0 SSTORE PUSH1 0x3 PUSH1 0x1 SSTORE
12、 继续编译,主要是完成对函数的编译,添加一个用于检查并回滚的内联函数。对应的指令:CALLVALUE DUP1 ISZERO PUSH2 0x14 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP,转13步;
13、 添加合约初始化:PUSH1 0xC2 DUP1 PUSH2 0x23 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN 。至此,部署合约的主要opcodes 生成完毕。下面开始编译函数,转14步;
14、 先根据所有的函数名生成对应的函数地址,如例子中的0x6D4CE63C,0xB8E010DE,实际调用函数的时候在查看交易的input中,就有这个值,转15步;
15 、编译函数,生成各个函数的指令,可参照前文示例。转16步;
16 、最后编译missingFunctions(存疑)。转17步;
17 、打印结果,编译结束。
上述解析的流程只是介绍了基本的思路,实际的处理过程要复杂的多,因为合约中可以有类,继承,多态,接口,库等形式的存在,需要进行一些额外的处理。
生成abi:
abi的内容是合约内函数的相关信息,包括函数的constant,name,inputs,outputs,payable,stateMutability,type,从上述第2至8步的解析即可获取到上述信息,然后封装成json返回给前端即可。生成opcodes:
上述第10到16步内流程即是生成cpcodes的过程,在实际使用中,用的opcodes的16进制。
添加新指令
影响范围
根据上述编译流程的解析,要添加新的指令,需要考虑以下4点1、token的定义:语法定义 ,比如 token{Add,+},将+与Add对应起来,解析的时候将代码中的+替换为Add
2、instruction的定义:提供给虚拟机执行的指令,需要在编译器和虚拟机中添加相同的定义
3、case token 的处理:将token与instruction对应起来,编译的过程中将token::Add替换为instruction::ADD指令,供虚拟机识别。
4、新指令对编译的影响:比如对函数的影响(是否影响函数的pure,view,payable属性),对存储的影响等,这个修改可以参考其他的同类型指令,比如添加的是运算符就参考加减乘除指令,添加的是块属性就参考已有的number,gaslimit指令。
5、虚拟机中对新加指令的定义与处理
示例:以添加RANDOM指令(获取块中的随机数属性,可参考number属性,合约中以block.number,block.random的方式进行使用)为例,说明在代码中添加的位置。
修改编译器代码
1、查看token定义,代码位置:liblangutil/Token.h。在TOKEN_LIST已定义了2中类型的token,一种是关键字token,一种是非关键字token,如括号,运算符,数据类型。要添加的random不是以上类型,不需要进行token定义。#token定义示例,格式为M(name,string,precedence),M可以是T或者K,T表示非关键字token,K表示关键字token。name表示token名称,string为token的原生字符串,precedence表示优先级。
#define TOKEN_LIST(T, K) \
......
T(LParen, "(", 0) \
T(RParen, ")", 0) \
T(LBrack, "[", 0) \
T(RBrack, "]", 0) \
T(AssignShr, ">>>=", 2) \
T(AssignAdd, "+=", 2) \
T(AssignSub, "-=", 2)
......
K(Continue, "continue", 0) \
K(Contract, "contract", 0) \
K(Do, "do", 0) \
K(Else, "else", 0)
......
2、指令定义,代码位置:libevmasm/Instruction.h。在enum calss Instruction中找到block的相关属性,并在其后追加RANDOM指令。如下所示,RANDOM=0x46 。注意添加的指令号不能与其他的冲突,比如不能再添加一个0x40的指令,会与现有的BLOCKHASH指令冲突。
enum class Instruction: uint8_t { ...... BLOCKHASH = 0x40, ///< get hash of most recent complete block COINBASE, ///< get the block's coinbase address TIMESTAMP, ///< get the block's timestamp NUMBER, ///< get the block's number DIFFICULTY, ///< get the block's difficulty GASLIMIT, ///< get the block's gas limit RANDOM, ......
}
上述定义为16进制,需要有一个字符串的"RANDOM"与指令对应,代码位置libevmasm/Instruction.cpp中。
std::map<std::string, Instruction> const dev::solidity::c_instructions = { ...... { "NUMBER", Instruction::NUMBER }, { "DIFFICULTY", Instruction::DIFFICULTY }, { "GASLIMIT", Instruction::GASLIMIT }, { "RANDOM", Instruction::RANDOM }, ...... } static std::map<Instruction, InstructionInfo> const c_instructionInfo = { ...... { Instruction::ADD, { "ADD", 0, 2, 1, false, Tier::VeryLow } }, { Instruction::NUMBER, { "NUMBER", 0, 0, 1, false, Tier::Base } }, { Instruction::DIFFICULTY, { "DIFFICULTY", 0, 0, 1, false, Tier::Base } }, { Instruction::GASLIMIT, { "GASLIMIT", 0, 0, 1, false, Tier::Base } }, { Instruction::RANDOM, { "RANDOM", 0, 0, 1, false, Tier::Base } }, ...... }//后面的0,0,1,false,Tier::Base 是可变的,根据指令的需要。第一个默认为0即可,第二个0表示参数个数,1表示需要1个返回值。false可理解为只在虚拟机内部使用,如果涉及到数据库的读写,这里要填成true。最后的Tier::Base是gasprice的级别,根据需要填写即可。
3、指令的处理:代码位置 libsolidity/codegen/ExpressionCompiler.cpp
bool ExpressionCompiler::visit(MemberAccess const& _memberAccess) { ...... case Type::Category::Magic: if (member == "coinbase") m_context << Instruction::COINBASE; else if (member == "timestamp") m_context << Instruction::TIMESTAMP; else if (member == "difficulty") m_context << Instruction::DIFFICULTY; else if (member == "number") m_context << Instruction::NUMBER; else if (member == "gaslimit") m_context << Instruction::GASLIMIT; else if (member == "random") m_context << Instruction::RANDOM; ...... } //不同的指令有不同的case进行处理,比如token:Add的处理如下: void ExpressionCompiler::appendArithmeticOperatorCode(Token _operator, Type const& _type) { ...... switch (_operator) { case Token::Add: m_context << Instruction::ADD; break; case Token::Sub: m_context << Instruction::SUB; break; case Token::Mul: m_context << Instruction::MUL; break; ...... } //如果添加的是其他类型的指令,就找到对应的case添加即可。
4、对函数,存储的影响:
确定数据类型,代码位置libsolidity/ast/Types.cpp
MemberList::MemberMap MagicType::nativeMembers(ContractDefinition const*) const { //指定存储的数据类型 ...... case Kind::Block: return MemberList::MemberMap({ {"coinbase", make_shared<AddressType>(StateMutability::Payable)}, {"timestamp", make_shared<IntegerType>(256)}, {"blockhash", make_shared<FunctionType>(strings{"uint"}, strings{"bytes32"}, FunctionType::Kind::BlockHash, false, StateMutability::View)}, {"difficulty", make_shared<IntegerType>(256)}, {"number", make_shared<IntegerType>(256)}, {"gaslimit", make_shared<IntegerType>(256)}, {"random", make_shared<IntegerType>(256)} //注意这里,设置数据类型为uint256,如果需要其他数据类型,参考libsolidity/ast/Types.h中的类型定义 }); ......
对函数的影响:代码位置 libevmasm/Semanticlnformation.cpp
bool SemanticInformation::invalidInPureFunctions(Instruction _instruction)
{
switch (_instruction)
{
......
case Instruction::TIMESTAMP:
case Instruction::NUMBER:
case Instruction::DIFFICULTY:
case Instruction::GASLIMIT:
case Instruction::RANDOM: //增加的random指令影响函数的Pure属性。return true表示该函数不能使用pure关键字。
case Instruction::STATICCALL:
case Instruction::SLOAD:
return true;
default:
break;
}
return invalidInViewFunctions(_instruction);
}
修改虚拟机代码
random指令的定义,代码位置:hvm/evm/opcodes.go
const (
// 0x40 range - block operations
BLOCKHASH OpCode = 0x40 + iota
COINBASE
TIMESTAMP
NUMBER
DIFFICULTY
GASLIMIT
RANDOM //新增
)
var opCodeToString = map[OpCode]string{
......
NUMBER: "NUMBER",
DIFFICULTY: "DIFFICULTY",
GASLIMIT: "GASLIMIT",
RANDOM: "RANDOM", //新增
......
}
var stringToOp = map[string]OpCode{
......
"NUMBER": NUMBER,
"DIFFICULTY": DIFFICULTY,
"GASLIMIT": GASLIMIT,
"RANDOM": RANDOM, //新增
......
}指令操作的定义:代码位置:hvm/evm/jump_table.go ,添加指令的操作属性
instructionSet[RANDOM] = operation{
execute: opRandom,
gasCost: constGasFunc(GasQuickStep),
validateStack: makeStackFunc(0, 1),
valid: true,
}
上述操作码对应函数opRandom的定义:代码位置hvm/evm/instrucitons.go,可参考number函数的定义
func opNumber(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
stack.push(math.U256(new(big.Int).Set(evm.BlockNumber)))
return nil, nil
}
func opRandom(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
stack.push(math.U256(new(big.Int).Set(evm.Random)))
return nil, nil
}
上述opRandom中使用了evm.Random,因此需要在evm结构体增加Random的属性。代码位置hvm/evm/evm.go
type Context struct {
......
Coinbase common.Address // Provides information for COINBASE
GasLimit *big.Int // Provides information for GASLIMIT
BlockNumber *big.Int // Provides information for NUMBER
Time *big.Int // Provides information for TIME
Difficulty *big.Int // Provides information for DIFFICULTY
Random *big.Int //新增
}上述增加了Random属性,需要对其进行初始化,代码位置为:hvm/hvm.go
func NewEVMContext(msg Message, header *types.Header, chain ChainContext, author *common.Address) evm.Context {
......
return evm.Context{
CanTransfer: CanTransfer,
Transfer: Transfer,
GetHash: GetHashFn(header, chain),
Origin: msg.From(),
Coinbase: beneficiary,
BlockNumber: new(big.Int).Set(header.Number),
Time: new(big.Int).Set(header.Time),
Difficulty: new(big.Int).Set(header.Difficulty),
GasLimit: new(big.Int).Set(header.GasLimit),
Random: new(big.Int).Set(header.Random),//新增
GasPrice: new(big.Int).Set(msg.GasPrice()),
}
}
上述获取的header为当前校验的块的header。header.Random的增加与生成此处不介绍了。
至此,编译源码与虚拟机源码添加Random指令修改完成。
生成编译器
1、下载源码:git clone https://github.com/ethereum/solidity2、cd solidity && git checkout v0.5.7 #本文例子以v0.5.7版本为基础版本进行修改
3、按照前文介绍修改相关代码
4、编译源码生成编译器
二进制编译器:mkdir build && cd build && cmake .. && make #执行完成后生成二进制文件:solc
js编译器:执行 ./scripts/build_emscripten.sh #执行完成后生成js文件:soljson.js
5、使用编译器编译合约代码
使用二进制编译器:solc --abi test.sol #生成abi
solc --bin test.sol #生成data
solc --opcodes test.sol #查看opcodes
使用js编译器:可以将soljson.js替换到remix中进行测试。需要搭建remix环境并修改soljson.js的加载路径 或者 自行编写js脚本进行测试。
6、按照前文介绍修改虚拟机代码并部署到测试链,使用上述生成的abi,data进行链上测试,合约部署和调用过程不在赘述。
注: 如有问题请在下方留言联系我们技术社群。
汪晓明博客: http://wangxiaoming.com/
汪晓明: HPB芯链创始人,巴比特专栏作家。十余年金融大数据、区块链技术开发经验,曾参与创建银联大数据。主创区块链教学视频节目《明说》30多期,编写了《以太坊官网文档中文版》,并作为主要作者编写了《区块链开发指南》,在中国区块链社区以ID“蓝莲花”知名。
Bitcoin Price Consolidates Below Resistance, Are Dips Still Supported?
Bitcoin Price Consolidates Below Resistance, Are Dips Still Supported?
XRP, Solana, Cardano, Shiba Inu Making Up for Lost Time as Big Whale Transaction Spikes Pop Up
XRP, Solana, Cardano, Shiba Inu Making Up for Lost Time as Big Whale Transaction Spikes Pop Up
Justin Sun suspected to have purchased $160m in Ethereum
Justin Sun suspected to have purchased $160m in Ethereum