本文目标

​ 本文的主要目的 :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/solidity

2、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进行链上测试,合约部署和调用过程不在赘述。

THE END!

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐