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/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!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章