代码分析
在CPU内部中,执行一条指令,一般经历这5个阶段:取指 >> 译码 >> 执行 >> 访存 >> 回写
对于BTC的虚拟机来说,其逻辑大致粗略可分为取指及执行两部分
取指
在script.h中,存放着类CScript的成员函数,其中最关键的是函数GetOp
bool GetOp(const_iterator& pc, opcodetype& opcodeRet, vector<unsigned char>& vchRet) const
GetOp的参数包括程序计数器PC、操作码OPCode及立即数返回值。该函数的作用是从脚本PC处读出操作码,调整PC指向下一个操作码;若当前读取的操作码为立即数操作码,则也一并读出立即数。
执行
在script.cpp中,存放着与脚本密切相关的函数,其中最基础、最关键的是函数EvalScript
bool EvalScript(const CScript& script, const CTransaction& txTo, unsigned int nIn, int nHashType,
vector<vector<unsigned char> >* pvStackRet)
EvalScript的参数包括待执行的程序(待执行的脚本),运行的环境(待填充的交易,交易输入的索引等)以及运行的结果
EvalScript主要局部变量及解析如下:
CScript::const_iterator pc = script.begin(); //程序计数器,用于指向下一个执行的指令
CScript::const_iterator pend = script.end(); //用于判断程序结束
CScript::const_iterator pbegincodehash = script.begin(); //与操作码OP_CODESEPARATOR一起用于辅助定位待签名脚本
vector<bool> vfExec; //条件判断结果
vector<valtype> stack; //主运行栈,用于运行时存放中间数值
vector<valtype> altstack; //次栈,未找到具体用法
EvalScript主要逻辑是从待执行脚本中取出操作码并执行,直至取完、执行过程中遇到OP_RETURN、执行过程中VERIFY类验证失败、执行过程中遇到错误(例如操作数不足等)才会结束执行。
各类操作码大致行为如下表:
类型 | 操作 |
---|---|
立即数 | 往主栈压入立即数 |
流程控制 | 对操作数进行条件判断;根据条件判断结果控制指令解析执行;对条件判断结果进行操作 |
栈操作 | 对栈顶某些元素进行复制、反转、交换等操作 |
切片运算 | 对栈顶某些元素进行拼接、位移等操作 |
位运算 | 对栈顶某些元素进行位运算 |
数值运算 | 对栈顶某些元素进行数值运算 |
加密运算 | 对栈顶某些元素进行计算哈希值、验签等密码学运算 |
边界符 | 无 |
模板 | 无 |
非法字节码 | 无 |
其中,边界符、模板、非法字节码三类操作码并无实际的执行代码(即这三类操作码在输入到脚本执行前已被预处理)。
所有指令中,较难理解的是OP_CHECKSIGVERIFY(OP_CHECKMULTISIGVERIFY可看作升级版OP_CHECKSIGVERIFY)
case OP_CHECKSIGVERIFY:
{
// (sig pubkey -- bool)
// OP_CHECKSIGVERIFY输入参数为sig及pubkey,输出结果bool
if (stack.size() < 2)
return false;
valtype& vchSig = stacktop(-2);
valtype& vchPubKey = stacktop(-1);
// debug print
//PrintHex(vchSig.begin(), vchSig.end(), "sig: %s\n");
//PrintHex(vchPubKey.begin(), vchPubKey.end(), "pubkey: %s\n");
// Subset of script starting at the most recent codeseparator
// 从最近的分隔符OP_CODESEPARATOR一直到结束的脚本需要进行验签
CScript scriptCode(pbegincodehash, pend);
// Drop the signature, since there's no way for a signature to sign itself
// 去掉签名
scriptCode.FindAndDelete(CScript(vchSig));
// 跟进 CheckSig -> SignatureHash,最终在SignatureHash中,将scriptCode填充到txTo的vin[nIn]中的scriptSig,
// 序列化后在进行计算哈希值,并对该哈希值进行验签
bool fSuccess = CheckSig(vchSig, vchPubKey, scriptCode, txTo, nIn, nHashType);
stack.pop_back();
stack.pop_back();
stack.push_back(fSuccess ? vchTrue : vchFalse);
if (opcode == OP_CHECKSIGVERIFY)
{
if (fSuccess)
stack.pop_back();
else
// 验签失败直接返回
pc = pend;
}
}
break;