- 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
Vec>>); -
- 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
{ - 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
{ - 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
{ - 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
{ - 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
{ - 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. 当一个本地变量最后一次使用的时候,会从寄存器中删除,如果不是最后一次使用,则仅仅是从寄存器中复制