基于虚拟机源码分析move合约(一):本地变量

合约一:本地变量的分配

module test_05::test_move{
    public fun test_local_variable(){
        let i = 0;
    }
    
}

这是一个最简单的合约,其中只有一个方法test_local_variable,这个方法只会分配一个本地变量i,下面我们通过下面的命令执行反编译:

move disassemble --name test_move

我们通过反编译可以得到如下指令:

// Move bytecode v5
module f2.test_move {


public test_local_variable() {
L0:     i: u64
B0:
        0: LdU64(0)
        1: Pop
        2: Ret
}
}

1. LdU64(0)

这条指令的意思是加载一个u64类型的数据0到栈上,对应的interpreter.rs源代码如下:

Bytecode::LdU64(int_const) => {
      gas_meter.charge_simple_instr(S::LdU64)?;
      interpreter.operand_stack.push(Value::u64(*int_const))?;
}

这里的gas_meter是用来测算指令消耗的gas费用的,我们忽略。下面是将int_const包装成一个Value,然后压入栈上:

pub fn u64(x: u64) -> Self {
        Self(ValueImpl::U64(x))
}

enum ValueImpl {
    Invalid,

    U8(u8),
    U64(u64),
    U128(u128),
    Bool(bool),
    Address(AccountAddress),

    Container(Container),

    ContainerRef(ContainerRef),
    IndexedRef(IndexedRef),
}

我们可以看到Value内部使用了一个enum来表示不同的数据类型,默认是Invalid,基本类型有u8/u64/u128/bool/address,Container是用来包装vec或者strcut的,而ContainerRef和IndexRef都是用来包装引用的。

因此,当我们分配一个本地变量的时候,其实是生成一个Value,然后压入栈

2. Pop

Pop是第二个指令,代码如下:

Bytecode::Pop => {
      gas_meter.charge_simple_instr(S::Pop)?;
      interpreter.operand_stack.pop()?;
}

从代码来看,这个操作是从栈上弹出一个值,并且丢弃。这里值得注意的是弹出的值被直接丢弃了,因为我们的代码中并没有用到这个本地变量,因此直接丢弃是可以理解的,如果这个本地变量被操作了,会在Pop之前有其他指令。

3. Ret

Ret一般是一个函数的最后一条指令,来告诉VM这个函数已经结束了,代码如下:

Bytecode::Ret => {
     gas_meter.charge_simple_instr(S::Ret)?;
     return Ok(ExitCode::Return);
}

直接返回Return,结合我之前写的第零章,我们知道本次调用将会结束。

综合上面的分析,这个合约只是分配了一个本地变量,并没有做任何操作,因此从指令层面来说,仅仅是对数据进行入栈出栈操作,下面一个合约我们来看一下,如果使用了本地变量,指令又是如何变化的。

合约二:本地变量的使用

module test_05::test_move{
    public fun test_local_variable(){
        let i = 0;
        let j = i;
    }
    
}

反编译后的指令:

public test_local_variable() {
L0:     i: u64
L1:     j: u64
B0:
        0: LdU64(0)
        1: StLoc[0](i: u64)
        2: MoveLoc[0](i: u64)
        3: Pop
        4: Ret
}
}

我们可以看到,对本地变量i的操作,多出来了两条指令:

1. StLoc[0](i: u64)

直接看代码:

Bytecode::StLoc(idx) => {
    let value_to_store = interpreter.operand_stack.pop()?;
    gas_meter.charge_store_loc(&value_to_store)?;
    self.locals.store_loc(*idx as usize, value_to_store)?;
}

这里首先从栈上弹出一个值,这个值我们可以知道,就是i的值0,然后将这个值存入locals。通过上一章节,我们知道locals的作用类似于寄存器,这里的idx就是第几个寄存器,而store_loc就是像第idx个寄存器存入一个值,具体代码如下:

pub struct Locals(Rc<RefCell<Vec<ValueImpl>>>);

impl Locals {
    pub fn new(n: usize) -> Self {
        Self(Rc::new(RefCell::new(
            iter::repeat_with(|| ValueImpl::Invalid).take(n).collect(),
        )))
    }

    pub fn copy_loc(&self, idx: usize) -> PartialVMResult<Value> {
        let v = self.0.borrow();
        match v.get(idx) {
            Some(ValueImpl::Invalid) => Err(PartialVMError::new(
                StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
            )
            .with_message(format!("cannot copy invalid value at index {}", idx))),
            Some(v) => Ok(Value(v.copy_value()?)),
            None => Err(
                PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION).with_message(
                    format!("local index out of bounds: got {}, len: {}", idx, v.len()),
                ),
            ),
        }
    }

    fn swap_loc(&mut self, idx: usize, x: Value) -> PartialVMResult<Value> {
        let mut v = self.0.borrow_mut();
        match v.get_mut(idx) {
            Some(v) => {
                if let ValueImpl::Container(c) = v {
                    if c.rc_count() > 1 {
                        return Err(PartialVMError::new(
                            StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
                        )
                        .with_message("moving container with dangling references".to_string()));
                    }
                }
                Ok(Value(std::mem::replace(v, x.0)))
            }
            None => Err(
                PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION).with_message(
                    format!("local index out of bounds: got {}, len: {}", idx, v.len()),
                ),
            ),
        }
    }

    pub fn move_loc(&mut self, idx: usize) -> PartialVMResult<Value> {
        match self.swap_loc(idx, Value(ValueImpl::Invalid))? {
            Value(ValueImpl::Invalid) => Err(PartialVMError::new(
                StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
            )
            .with_message(format!("cannot move invalid value at index {}", idx))),
            v => Ok(v),
        }
    }

    pub fn store_loc(&mut self, idx: usize, x: Value) -> PartialVMResult<()> {
        self.swap_loc(idx, x)?;
        Ok(())
    }
}

我们可以看到locals本质是个Vec,Vec的每个格子可以想象成是个容器(下面我们统一称为寄存器),用来存放某种类型的Value,locals在初始化的时候会给每个寄存器初始化ValueImpl::Invalid值。

当我们调用store_loc的时候内部会调用swap_loc函数,这个函数先获取idx这个位置的数据,如果返回None,说明数组越界了,否则会返回一个值,这里由于寄存器之前没有使用过,因此返回的是ValueImpl::Invalid值,这里通过std::mem::replace将Invalid值替换成ValueImpl::U64的值,这个方法会把被替换掉的值,也就是ValueImpl::Invalid返回,不过我们这里用不到。

因此,StLoc[0](i: u64)这条指令干的事,就是将从栈中弹出的值(也就是本地变量i的值)写入寄存器0。

2. MoveLoc[0](i: u64)

直接看代码:

Bytecode::MoveLoc(idx) => {
        let local = self.locals.move_loc(*idx as usize)?;
        gas_meter.charge_move_loc(&local)?;

        interpreter.operand_stack.push(local)?;
}

从代码上看是对寄存器0调用了move_loc的操作,返回来一个值,然后将这个值压入栈中,下面再来看下move_loc:

pub fn move_loc(&mut self, idx: usize) -> PartialVMResult<Value> {
        match self.swap_loc(idx, Value(ValueImpl::Invalid))? {
            Value(ValueImpl::Invalid) => Err(PartialVMError::new(
                StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
            )
            .with_message(format!("cannot move invalid value at index {}", idx))),
            v => Ok(v),
        }
    }

这里直接调用swap_loc,将寄存器0的值替换成ValueImpl::Invalid,然后把之前的ValueImpl::U64返回回来,然后通过match进行匹配,进而返回。从代码我们可以看出,store_loc类似于update操作,而move_loc类似于remove操作。

因此,MoveLoc[0](i: u64)的作用就是从指定寄存器删除一个值,并把这个值压入栈上。

我们可以大致描述上面一连串指令的过程:

LdU64(0):   生成一个值为0的ValueImpl::U64的数据结构,压入栈上(分配本地变量i)

StLoc[0](i: u64):从栈上弹出一个值,存入寄存器0(准备使用本地变量i)

MoveLoc[0](i: u64):从寄存器0删除一个值,并且压入栈上(将本地变量i赋值给本地变量k)

Pop:从栈上弹出一个值(这里k没有具体操作,因此直接丢弃,如果有操作,会在Pop之前有其他指令)

Ret:程序结束,直接返回

综合上面的分析,对一个本地变量的使用,会先将这个变量从栈中转移到寄存器中,等真正使用的时候又从寄存器中转移到栈中,比如下面的例子:

module test_05::test_move{
    public fun test_local_variable(){
        let i = 0;
        let j = i;
        let k = j;
    }
    
}

实际执行的指令:

public test_local_variable() {
L0:     i: u64
L1:     j: u64
L2:     k: u64
B0:
        0: LdU64(0)
        1: StLoc[0](i: u64)
        2: MoveLoc[0](i: u64)
        3: StLoc[1](j: u64)
        4: MoveLoc[1](j: u64)
        5: Pop
        6: Ret
}
}

可以看到只是多了一组StLoc和MoveLoc,区别在于使用的寄存器位置不一样。

合约三:本地变量的多次使用

当某个本地变量被使用时,就会生成一组StLoc和MoveLoc,那么,当某个本地变量被多次使用是,会如何呢?

module test_05::test_move{
    public fun test_local_variable(){
        let i = 0;
        let j = i;
        let k = i;
    }
    
}

这个合约中本地变量i被多次使用,生成的指令如下:

public test_local_variable() {
L0:     i: u64
L1:     j: u64
L2:     k: u64
B0:
        0: LdU64(0)
        1: StLoc[0](i: u64)
        2: CopyLoc[0](i: u64)
        3: Pop
        4: MoveLoc[0](i: u64)
        5: Pop
        6: Ret
}
}

这里多了个新指令CopyLoc[0](i: u64):

Bytecode::CopyLoc(idx) => {
       // TODO(Gas): We should charge gas before copying the value.
       let local = self.locals.copy_loc(*idx as usize)?;
       gas_meter.charge_copy_loc(&local)?;
       interpreter.operand_stack.push(local)?;
}

首先执行copy_loc:

pub fn copy_loc(&self, idx: usize) -> PartialVMResult<Value> {
        let v = self.0.borrow();
        match v.get(idx) {
            Some(ValueImpl::Invalid) => Err(PartialVMError::new(
                StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
            )
            .with_message(format!("cannot copy invalid value at index {}", idx))),
            Some(v) => Ok(Value(v.copy_value()?)),
            None => Err(
                PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION).with_message(
                    format!("local index out of bounds: got {}, len: {}", idx, v.len()),
                ),
            ),
        }
    }

本质是从某个寄存器copy一个值,和move_loc不同的是,copy_loc不会删除原有的值。因此CopyLoc[0](i: u64)就是从寄存器copy一个值,然后压入栈中。

因此,当我们多次使用某个本地变量的时候,在最后一次使用之前都是copy值,最后一次使用则会remove掉。

我们可以大致描述上面一连串指令的过程:

LdU64(0):   生成一个值为0的ValueImpl::U64的数据结构,压入栈上(分配本地变量i)

StLoc[0](i: u64):从栈上弹出一个值,存入寄存器0(准备使用本地变量i)

CopyLoc[0](i: u64):从寄存器0中copy一个值压入栈上(将本地变量i赋值给本地变量j)

Pop:从栈上弹出一个值(因为这里变量j没有具体操作,因此可以看成直接丢弃,如果有具体操作,则在Pop之前会有其他指令)

MoveLoc[0](i: u64):从寄存器0删除一个值,并且压入栈上(将本地变量i赋值给本地变量k)

Pop:从栈上弹出一个值(这里一样,k没有具体操作,因此直接丢弃)

Ret:程序结束,直接返回

总结

1. 分配本地变量会使用LdXXX的操作,会将数据包装成Value值然后压入栈

2. 使用本地变量分为两步,准备使用和实际使用,准备使用会把数据从栈转移到寄存器,实际使用又会从寄存器取数据放入栈上

3. 当一个本地变量最后一次使用的时候,会从寄存器中删除,如果不是最后一次使用,则仅仅是从寄存器中复制


版权声明:本文为biakia0610原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。