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


    合约一:本地变量的分配

    1. module test_05::test_move{
    2. public fun test_local_variable(){
    3. let i = 0;
    4. }
    5. }

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

    move disassemble --name test_move

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

    1. // Move bytecode v5
    2. module f2.test_move {
    3. public test_local_variable() {
    4. L0: i: u64
    5. B0:
    6. 0: LdU64(0)
    7. 1: Pop
    8. 2: Ret
    9. }
    10. }

    1. LdU64(0)

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

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

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

    1. pub fn u64(x: u64) -> Self {
    2. Self(ValueImpl::U64(x))
    3. }
    4. enum ValueImpl {
    5. Invalid,
    6. U8(u8),
    7. U64(u64),
    8. U128(u128),
    9. Bool(bool),
    10. Address(AccountAddress),
    11. Container(Container),
    12. ContainerRef(ContainerRef),
    13. IndexedRef(IndexedRef),
    14. }

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

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

    2. Pop

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

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

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

    3. Ret

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

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

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

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

    合约二:本地变量的使用

    1. module test_05::test_move{
    2. public fun test_local_variable(){
    3. let i = 0;
    4. let j = i;
    5. }
    6. }

    反编译后的指令:

    1. public test_local_variable() {
    2. L0: i: u64
    3. L1: j: u64
    4. B0:
    5. 0: LdU64(0)
    6. 1: StLoc[0](i: u64)
    7. 2: MoveLoc[0](i: u64)
    8. 3: Pop
    9. 4: Ret
    10. }
    11. }

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

    1. StLoc[0](i: u64)

    直接看代码:

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

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

    1. pub struct Locals(RcVec>>);
    2. impl Locals {
    3. pub fn new(n: usize) -> Self {
    4. Self(Rc::new(RefCell::new(
    5. iter::repeat_with(|| ValueImpl::Invalid).take(n).collect(),
    6. )))
    7. }
    8. pub fn copy_loc(&self, idx: usize) -> PartialVMResult {
    9. let v = self.0.borrow();
    10. match v.get(idx) {
    11. Some(ValueImpl::Invalid) => Err(PartialVMError::new(
    12. StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
    13. )
    14. .with_message(format!("cannot copy invalid value at index {}", idx))),
    15. Some(v) => Ok(Value(v.copy_value()?)),
    16. None => Err(
    17. PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION).with_message(
    18. format!("local index out of bounds: got {}, len: {}", idx, v.len()),
    19. ),
    20. ),
    21. }
    22. }
    23. fn swap_loc(&mut self, idx: usize, x: Value) -> PartialVMResult {
    24. let mut v = self.0.borrow_mut();
    25. match v.get_mut(idx) {
    26. Some(v) => {
    27. if let ValueImpl::Container(c) = v {
    28. if c.rc_count() > 1 {
    29. return Err(PartialVMError::new(
    30. StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
    31. )
    32. .with_message("moving container with dangling references".to_string()));
    33. }
    34. }
    35. Ok(Value(std::mem::replace(v, x.0)))
    36. }
    37. None => Err(
    38. PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION).with_message(
    39. format!("local index out of bounds: got {}, len: {}", idx, v.len()),
    40. ),
    41. ),
    42. }
    43. }
    44. pub fn move_loc(&mut self, idx: usize) -> PartialVMResult {
    45. match self.swap_loc(idx, Value(ValueImpl::Invalid))? {
    46. Value(ValueImpl::Invalid) => Err(PartialVMError::new(
    47. StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
    48. )
    49. .with_message(format!("cannot move invalid value at index {}", idx))),
    50. v => Ok(v),
    51. }
    52. }
    53. pub fn store_loc(&mut self, idx: usize, x: Value) -> PartialVMResult<()> {
    54. self.swap_loc(idx, x)?;
    55. Ok(())
    56. }
    57. }

    我们可以看到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)

    直接看代码:

    1. Bytecode::MoveLoc(idx) => {
    2. let local = self.locals.move_loc(*idx as usize)?;
    3. gas_meter.charge_move_loc(&local)?;
    4. interpreter.operand_stack.push(local)?;
    5. }

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

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

    这里直接调用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:程序结束,直接返回

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

    1. module test_05::test_move{
    2. public fun test_local_variable(){
    3. let i = 0;
    4. let j = i;
    5. let k = j;
    6. }
    7. }

    实际执行的指令:

    1. public test_local_variable() {
    2. L0: i: u64
    3. L1: j: u64
    4. L2: k: u64
    5. B0:
    6. 0: LdU64(0)
    7. 1: StLoc[0](i: u64)
    8. 2: MoveLoc[0](i: u64)
    9. 3: StLoc[1](j: u64)
    10. 4: MoveLoc[1](j: u64)
    11. 5: Pop
    12. 6: Ret
    13. }
    14. }

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

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

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

    1. module test_05::test_move{
    2. public fun test_local_variable(){
    3. let i = 0;
    4. let j = i;
    5. let k = i;
    6. }
    7. }

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

    1. public test_local_variable() {
    2. L0: i: u64
    3. L1: j: u64
    4. L2: k: u64
    5. B0:
    6. 0: LdU64(0)
    7. 1: StLoc[0](i: u64)
    8. 2: CopyLoc[0](i: u64)
    9. 3: Pop
    10. 4: MoveLoc[0](i: u64)
    11. 5: Pop
    12. 6: Ret
    13. }
    14. }

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

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

    首先执行copy_loc:

    1. pub fn copy_loc(&self, idx: usize) -> PartialVMResult {
    2. let v = self.0.borrow();
    3. match v.get(idx) {
    4. Some(ValueImpl::Invalid) => Err(PartialVMError::new(
    5. StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
    6. )
    7. .with_message(format!("cannot copy invalid value at index {}", idx))),
    8. Some(v) => Ok(Value(v.copy_value()?)),
    9. None => Err(
    10. PartialVMError::new(StatusCode::VERIFIER_INVARIANT_VIOLATION).with_message(
    11. format!("local index out of bounds: got {}, len: {}", idx, v.len()),
    12. ),
    13. ),
    14. }
    15. }

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

  • 相关阅读:
    明天报名!!济宁教师招聘报名照片及常见问题
    LeetCode-764. 最大加号标志【动态规划,二维数组】
    【面试题】JSON.stringify 和fast-json-stringify有什么区别
    第三章:运算符
    jira自定义字段userpicker类型不能用
    Vue项目实战——实现一个任务清单(学以致用,两小时带你巩固和强化Vue知识点)
    kubernetes集群搭建
    【教学类-34-10】20240313 春天拼图(Midjounery生成线描图,4*4格拼图块)(AI对话大师)
    果园自主跟随碎枝机器人
    Clickhouse—聚合函数组合
  • 原文地址:https://blog.csdn.net/biakia0610/article/details/127105468