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