NEO从源码分析看NEOVM

0x00 前言

这篇文章是为下一篇《NEO从源码分析看UTXO转帐交易》打前站,为交易的构造及执行的一些技术基础作个探索。因为这个东西实在有点干,干到简直咽不下,因此我来个自顶向下,从合约代码开始慢慢深刻。此外,文中不免有些不详尽或者疏漏偏颇的地方,还望大佬们不吝指教。html

0x01 锁仓合约(Lock)

在官方提供的三个合约示例中,这个锁仓合约是惟一一个不须要Storage的,目前我是感受可能简单些。若是这把坑了本身,我无怨无悔,毕竟别的合约早晚也要分析,/(ㄒoㄒ)/~~。 锁仓合约的代码和解释均可以在官方文档中找到,中文版地址在这里,github地址在这里git

public class Lock : SmartContract
{
    public static bool Main(byte[] signature)
    {
        Header header = Blockchain.GetHeader(Blockchain.GetHeight());
        if (header.Timestamp < 1520554200) // 2018-3-9 8:10:00
            return false;
        return true;
   }
}
复制代码

我这里把原来的时间戳改了,还把签名验证删了。建立新合约项目的步骤我就再也不多说,官网上都有。 这个合约只有最新的区块时间戳大于我既定的时间才能够转帐,不然转帐失败。理论是这样的,官网解释也基本就这么言简意赅。我接下来要作的,就是最苦逼的——追踪这个合约脚本的生成和执行过程。下面涉及的代码主要是三个项目:github

0x02 编译

不得不说NEO开发团队这块作的仍是蛮好的,虽然这个编译的过程灰常复杂,可是操做起来确实很简单,直接右键项目选择生成就能够了:数组

从这里能够看到不少消息,每一步执行了什么,生成了什么,结果是什么。最最重要的是,这里有关键字啊,以前社区有人问我怎么看源码的,就这么看的,可怜兮兮的找蛛丝马迹,一个关键字一个关键字去查引用。 从这个日志里能够看出,编译的时候是先生成dll动态连接库,这固然是.net的工做了。而后调用的是Neo.Compiler.MSIL这个东东。我就先找这个东西。缓存

0x03 解析

根据上小结的关键字,我定位到neo-compiler项目的Program.cs文件,这个文件里有编译器的入口函数Main。不要问我怎么调用的,不care,就这么傲娇(实在是没找到)。Main方法会接收一个参数,就是dll文件的路径:bash

源码位置:neo/Compiler/Program.cs/Main(string[] args)app

log.Log("Neo.Compiler.MSIL console app v" + Assembly.GetEntryAssembly().GetName().Version);
if (args.Length == 0)
{
      log.Log("need one param for DLL filename.");
      return;
}
string filename = args[0];
string onlyname = System.IO.Path.GetFileNameWithoutExtension(filename);
string filepdb = onlyname + ".pdb";
复制代码

说实话我对C#的了解并无深刻到字节码的水平,使用经验也就止于鹅厂实习作游戏的那几个月,这从DLL转AVM我只能尽全力而为。 转换的主要函数是ModuleConverter的Convert,这个方法接收一个ILModule类型的对象做为参数,而这个ILModule对象就是负责解析dll文件获取IL指令的。因为我没找到办法动态分析这个compiler,因此我直接将Lock.dll文件进行了逆向,直接对照IL指令静态分析compiler。逆向工具我用的是ILSPY,github有售。如下是逆向IL代码:ide

.class public auto ansi beforefieldinit Lock
	extends [Neo.SmartContract.Framework]Neo.SmartContract.Framework.SmartContract
{
	// 方法
	.method public hidebysig static 
		bool Main (
			uint8[] signature
		) cil managed 
	{
		// 方法起始 RVA 地址 0x2050
		// 方法起始地址(相对于文件绝对值:0x0250)
		// 代码长度 62 (0x3e)
		.maxstack 4
		.locals init (
			[0] class [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Header,
			[1] bool,
			[2] bool
		)

		// 0x025C: 00
		IL_0000: nop
		// 0x025D: 28 10 00 00 0A
		IL_0001: call uint32 [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Blockchain::GetHeight()
		// 0x0262: 28 11 00 00 0A
		IL_0006: call class [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Header [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Blockchain::GetHeader(uint32)
		// 0x0267: 0A
		IL_000b: stloc.0
		// 0x0268: 06
		IL_000c: ldloc.0
		// 0x0269: 6F 12 00 00 0A
		IL_000d: callvirt instance uint32 [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Header::get_Timestamp()
		// 0x026E: 20 20 2F A1 5A
		IL_0012: ldc.i4 1520512800
		// 0x0273: FE 05
		IL_0017: clt.un
		// 0x0275: 0B
		IL_0019: stloc.1
		// 0x0276: 07
		IL_001a: ldloc.1
		// 0x0277: 2C 04
		IL_001b: brfalse.s IL_0021

		// 0x0279: 16
		IL_001d: ldc.i4.0
		// 0x027A: 0C
		IL_001e: stloc.2
		// 0x027B: 2B 1B
		IL_001f: br.s IL_003c

		// 0x027D: 02
		IL_0021: ldarg.0
		// 0x027E: 1F 21
		IL_0022: ldc.i4.s 33
		// 0x0280: 8D 16 00 00 01
		IL_0024: newarr [mscorlib]System.Byte
		// 0x0285: 25
		IL_0029: dup
		// 0x0286: D0 01 00 00 04
		IL_002a: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=33' '<PrivateImplementationDetails>'::'09B200FB2B3E1BDC14112F99F08AA4576CF64321'
		// 0x028B: 28 13 00 00 0A
		IL_002f: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
		// 0x0290: 28 14 00 00 0A
		IL_0034: call bool [Neo.SmartContract.Framework]Neo.SmartContract.Framework.SmartContract::VerifySignature(uint8[], uint8[])
		// 0x0295: 0C
		IL_0039: stloc.2
		// 0x0296: 2B 00
		IL_003a: br.s IL_003c

		// 0x0298: 08
		IL_003c: ldloc.2
		// 0x0299: 2A
		IL_003d: ret
	} // 方法 Lock::Main 结束

	.method public hidebysig specialname rtspecialname 
		instance void .ctor () cil managed 
	{
		// 方法起始 RVA 地址 0x209a
		// 方法起始地址(相对于文件绝对值:0x029a)
		// 代码长度 8 (0x8)
		.maxstack 8

		// 0x029B: 02
		IL_0000: ldarg.0
		// 0x029C: 28 15 00 00 0A
		IL_0001: call instance void [Neo.SmartContract.Framework]Neo.SmartContract.Framework.SmartContract::.ctor()
		// 0x02A1: 00
		IL_0006: nop
		// 0x02A2: 2A
		IL_0007: ret
	} // 方法 Lock::.ctor 结束

} // 类 Lock 结束
复制代码

从上面ILSpy逆向出的IL代码就能够很清晰的看出来函数名、参数、类型、系统调用等等关键信息,neo-vm对C#字节码的解析就是根据这些东西。Compiler从dll获取IL指令使用的是mono.cecil,这个工具的代码github也有售。基本上NEO-VM定义了本身的一套完整指令集,能够逐条来作翻译,把IL指令翻译成avm指令,这个翻译的结果就是avm脚本了。翻译的过程首先是把IL指令中的方法提取出来,提取的部分有些对自动生成代码及系统调用的判断,比较繁琐,并且对于咱们理解这个转换过程帮助也不大,我就不讲了。对于每一个方法的核心处理代码以下:函数

源码位置:neon/MSIL/Converter.cs/Convert(ILModule _in)工具

//方法参数获取
foreach (var src in m.Value.paramtypes)
{
         nm.paramtypes.Add(new NeoParam(src.name, src.type));
}
//是否为neo系统调用
byte[] outcall; string name;
if (IsAppCall(m.Value.method, out outcall))
        continue;
if (IsNonCall(m.Value.method))
          continue;
if (IsOpCall(m.Value.method, out name))
          continue;
if (IsSysCall(m.Value.method, out name))
          continue;
//方法代码转换为opcode
this.ConvertMethod(m.Value, nm);
复制代码

在每一个方法解析完以后会调用ConvertMethod方法来把方法内部的IL指令转换为对应的avm指令,指令转换的方法是ConvertCode,这个方法里定义有完整的IL到avm的映射关系,这里就不一一分析了。 这里我就先伪装这个转换过程已经讲完了,细节部分可能之后的博客中还会涉猎到,都之后再说。 前面分析完了,到建立合约的时候我就凉了,这竟然涉及到应用合约和鉴权合约(下下篇博客专题介绍),这个东西我简直一直以来都云里雾里,如今竟然直接迎头撞上了,苦也。这里不明白的能够静待我接下来专门介绍合约的博客,我就先直接往下走了。锁仓合约自己是不须要部署在区块链上的,它跟帐户合约同样都是鉴权合约。我在上一篇文章《从源码分析看nep2和nep6》中详细分析过,NEO的帐户自己其实就是一个合约,一个不须要部署在区块链上,在每次交易的时候执行的鉴权合约。Lock合约如是。

0x04 转帐

由于这个锁仓合约是个鉴权合约,不须要部署到区块链上,因此我咱们只须要在本地进行部署就能够了,这个过程用neo-GUI就能够很方便的完成。为了测试的直观,我在本地只保留了一个有3.8gas的账户: AV5XmH49Gzz8puT5iMdv5ycmhqWGH5VNq7,下文中咱们把这个帐户叫徐峥。 新建的合约地址是 Aaigh8uGWwsmPTWKkxfXx8ZRJNYk6RvnBQ,这个帐户叫王宝强。除此以外,我还另外有一个帐户ASCjW4xpfr8kyVHY1J2PgvcgFbPYa1qX7F,咱们叫黄渤,用于向徐峥帐户转帐,以确认徐峥帐户收款功能正常。 故事背景以下,王宝强向徐峥借了3.8个GAS当回家路费,约定 3/9/2018 8:10:00 这个时间以后还。 故事发展:

  • 第一幕:王宝强回家过年没路费,向徐峥借3.8个GAS。因而徐峥借给王宝强3.8GAS,而且约定归还时间为8:10以后。没办法,只有回家了以后才有钱还。交易1 id为: 0x7f5be9b212c81958428a416f5afad3ca26d3d032e85330b6837f9fea559e1785
  • 第二幕:徐峥路上和王宝强闹翻,徐峥强行向王宝强索要3.8GAS。可怜的宝强迫不得已了么?徐峥索取3.8GAS是交易2 id为: 0xfa17a8d74a8ebf75f839286de21e011209177930551f7b52a09161250a39df66
  • 第三幕:可怜的宝宝是执着的,是个人就是个人,不是个人我也不要,说好了8:10之后还就8:10之后还。兔子急了还咬人呢,宝宝坚定不退让,孩子是个人,GAS也是个人。徐峥百般索要无果,交易2宣告失败。
  • 第四幕:最终在8:10以后的8:23,徐峥才成功拿走了借给宝宝的3.8GAS。取回GAS交易3 id: 0xfa17a8d74a8ebf75f839286de21e011209177930551f7b52a09161250a39df66
  • 终幕:在囧途历经坎坷共同患难以后,两人化干戈为玉帛感情更深一步今后再也不争吵,今后幸福美满的生活在了一块儿。

在以上小故事中,因为锁仓合约约定取款时间为8:10以后,在这个时间以前进行资产转出都会失败。在小故事中的全部交易都是真实的,能够在测试网上查到交易信息。接下来咱们分析一下这个交易2是如何执行失败的。

0x05 合约执行

当咱们从锁仓合约中转出资产的交易广播出去后,在新一轮共识中会被共识节点进行验证(共识部分请移步个人博客《NEO从源码分析看共识协议》),若是验证成功,则会放在缓存中等待写入新的区块中,若是验证失败,这个交易就会被丢弃:

源码位置:neo/Core/Helper/VerifyScripts(this IVerifiable verifiable)

using (StateReader service = new StateReader())
{
        ApplicationEngine engine = new ApplicationEngine(TriggerType.Verification, verifiable, Blockchain.Default, service, Fixed8.Zero);
        engine.LoadScript(verification, false);
        engine.LoadScript(verifiable.Scripts[i].InvocationScript, true);
        if (!engine.Execute()) return false;
        if (engine.EvaluationStack.Count != 1 || !engine.EvaluationStack.Pop().GetBoolean()) return false;
}
复制代码

ApplicationEngine是neo-vm中用来执行脚本的类。能够看到这里设置了脚本执行引擎的triggertype为验证,而且传入了交易的脚本进去。这里咱们跟进Execute方法。

源码位置:neo/SmartContract/ApplicationEngine/Execute()

while (!State.HasFlag(VMState.HALT) && !State.HasFlag(VMState.FAULT)) {
    if (CurrentContext.InstructionPointer < CurrentContext.Script.Length) {
        //读取下一条指令
        OpCode nextOpcode = CurrentContext.NextInstruction;
        //按指令收费
        gas_consumed = checked(gas_consumed + GetPrice(nextOpcode) * ratio);
        if (!testMode && gas_consumed > gas_amount) {
            State |= VMState.FAULT;
            return false;
        }

        if (!CheckItemSize(nextOpcode) ||
            !CheckStackSize(nextOpcode) ||
            !CheckArraySize(nextOpcode) ||
            !CheckInvocationStack(nextOpcode) ||
            !CheckBigIntegers(nextOpcode) ||
            !CheckDynamicInvoke(nextOpcode)) {
            State |= VMState.FAULT;
            return false;
        }
    }
    //执行
    StepInto();
}    
复制代码

不难看出这个engine执行avm脚本的方式和cpu差很少,都是每次取一条指令执行。因为跟着StepInto一条一条执行还不如直接看AVM指令代码,因此这里咱们就跳出源码,来分析AVM。个人合约脚本是:

54c56b6c766b00527ac4616168184e656f2e426c6f636b636861696e2e4765744865696768746168184e656f2e426c6f636b636861696e2e4765744865616465726c766b51527ac46c766b51c36168174e656f2e4865616465722e47657454696d657374616d7004d8d0a15a9f6c766b52527ac46c766b52c3640e00006c766b53527ac4620e00516c766b53527ac46203006c766b53c3616c7566

通过NEL轻钱包工具转ASM代码以下:

0:PUSH4
1:NEWARRAY
2:TOALTSTACK
3:FROMALTSTACK
4:DUP
5:TOALTSTACK
6:PUSH0(false)
7:PUSH2
8:ROLL
9:SETITEM
a:NOP
b:NOP
c:SYSCALL[781011114666108111991079910497105110467110111672101105103104116]
26:NOP
27:SYSCALL[78101111466610811199107991049710511046711011167210197100101114]
41:FROMALTSTACK
42:DUP
43:TOALTSTACK
44:PUSH1(true)
45:PUSH2
46:ROLL
47:SETITEM
48:FROMALTSTACK
49:DUP
4a:TOALTSTACK
4b:PUSH1(true)
4c:PICKITEM
4d:NOP
4e:SYSCALL[7810111146721019710010111446711011168410510910111511697109112]
67:PUSHBYTES4[0xd8d0a15a]
6c:LT
6d:FROMALTSTACK
6e:DUP
6f:TOALTSTACK
70:PUSH2
71:PUSH2
72:ROLL
73:SETITEM
74:FROMALTSTACK
75:DUP
76:TOALTSTACK
77:PUSH2
78:PICKITEM
79:JMPIFNOT[14]
7c:PUSH0(false)
7d:FROMALTSTACK
7e:DUP
7f:TOALTSTACK
80:PUSH3
81:PUSH2
82:ROLL
83:SETITEM
84:JMP[14]
87:PUSH1(true)
88:FROMALTSTACK
89:DUP
8a:TOALTSTACK
8b:PUSH3
8c:PUSH2
8d:ROLL
8e:SETITEM
8f:JMP[3]
92:FROMALTSTACK
93:DUP
94:TOALTSTACK
95:PUSH3
96:PICKITEM
97:NOP
98:FROMALTSTACK
99:DROP
9a:RET
复制代码

这个avm2asm工具的地址是 sdk.nel.group ,源码github开放。这个逆向出的asm代码是否是很像咱们的汇编代码呢,除了这个指令不是像汇编那样是三元的。这点在官方的文档也有介绍,说是由于这个虚拟机上操做数是单独维护在一个操做数栈上的,对于数据的操做只有简单的push和pop,因此不必指定地址。我说我能一条条对照avm指令把整个合约执行流程走一遍你确定不信,我也不信,若是有人愿意帮我翻译一遍的话能够从neo-vm/OpCode.cs这个文件中找到每条指令对应的定义。我我的的话是感受既然不想手撸avm脚本,那么知道这个东西是这么个过程就差很少了。

0x06 系统调用

在上一节贴出来的avm代码中有三个syscall指令,分别带着一个字节数组,其实经过IL代码也能看出来这三个字节数组中存放的确定就是系统调用的路径了。可这个东西是如何来的呢?

  • 第一个syscall的地址是:781011114666108111991079910497105110467110111672101105103104116,对应16进制的:4e656f2e426c6f636b636861696e2e476574486569676874,这个转换为字符串就是Neo.Blockchain.GetHeight。
  • 第二个syscall的地址是:78101111466610811199107991049710511046711011167210197100101114,对应16进制的:4e656f2e426c6f636b636861696e2e476574486561646572,翻译出来就是Neo.Blockchain.GetHeader。
  • 第三个syscall地址是:7810111146721019710010111446711011168410510910111511697109112,对应的16进制是:4e656f2e4865616465722e47657454696d657374616d70,翻译出来是Neo.Header.GetTimestamp。

能够看出,系统调用的地址其实就是咱们C#中调用的方法的路径。这块的构造代码以下:

源码位置:neo/Compiler/MSIL/ModuleConverter/_ConverterCall(OpCode src,NeoMethod to)

var bytes = Encoding.UTF8.GetBytes(callname);
if (bytes.Length > 252) throw new Exception("string is to long");
byte[] outbytes = new byte[bytes.Length + 1];
outbytes[0] = (byte)bytes.Length;
Array.Copy(bytes, 0, outbytes, 1, bytes.Length);
//bytes.Prepend 函数在 dotnet framework 4.6 编译不过
_Convert1by1(VM.OpCode.SYSCALL, null, to, outbytes);
复制代码

从代码中能够看出来,这个syscall指令的地址长度最大只能有252字节。 调用这个syscall指令的代码在nep-vm 的ExecuteEngine类里:

源码位置:neo/vm/ExecuteEngine/ExecuteOp

case OpCode.SYSCALL:
      if (!service.Invoke(Encoding.ASCII.GetString(context.OpReader.ReadVarBytes(252)), this))
           State |= VMState.FAULT;
       break;
复制代码

这里是调用了Invoke方法,并将系统调用的路径传过去,咱们跟进去这个Invoke方法:

源码位置:neo/vm/InteropService

internal bool Invoke(string method, ExecutionEngine engine)
{
      if (!dictionary.ContainsKey(method)) return false;
            return dictionary[method](engine);
}
复制代码

能够看到这里是将地址做为key来从map中取对应的方法来执行。这个map里的内容定义在智能合约的StateReader类中,这个类继承了InteropService,而且在构造方法中向dictionary中添加了元素:

源码位置:neo/SmartContract/StateReader

public StateReader()
{
    Register("Neo.Runtime.GetTrigger", Runtime_GetTrigger);
    Register("Neo.Runtime.CheckWitness", Runtime_CheckWitness);
    //省略N多Register
    Register("Neo.Iterator.Next", Iterator_Next);
    Register("Neo.Iterator.Key", Iterator_Key);
    Register("Neo.Iterator.Value", Iterator_Value);
}
复制代码

至于这些系统调用方法的返回值,则由各个系统调用接收的ExecutionEngine对象获取。

好啦,以上就是NEO VM的大概流程和原理,因为这个项目涉及的东西实在普遍,文章不能详尽之处万望见谅。

做者:暖冰

转载自:my.oschina.net/u/2276921/b…

相关文章
相关标签/搜索