运行机制
反汇编样例分析
pragma solidity ^0.4.23;
contract example {
uint public a;
uint public b;
function hello1(){
a = 1;
}
function hello2(uint var1){
b = var1;
}
}
contract Contract {
function main() {
memory[0x40:0x60] = 0x80;
if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }
var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;
if (var0 == 0x0dbe671f) {
// Dispatch table entry for a()
var var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x007c;
var var2 = a();
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = var2;
var temp1 = memory[0x40:0x60];
return memory[temp1:temp1 + (temp0 + 0x20) - temp1];
} else if (var0 == 0x297c930b) {
// Dispatch table entry for hello2(uint256)
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x00bd;
var2 = msg.data[0x04:0x24];
hello2(var2);
stop();
} else if (var0 == 0x4df7e3d0) {
// Dispatch table entry for b()
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x00d4;
var2 = b();
var temp2 = memory[0x40:0x60];
memory[temp2:temp2 + 0x20] = var2;
var temp3 = memory[0x40:0x60];
return memory[temp3:temp3 + (temp2 + 0x20) - temp3];
} else if (var0 == 0xdf022cbc) {
// Dispatch table entry for hello1()
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x00ff;
hello1();
stop();
} else { revert(memory[0x00:0x00]); }
}
function a() returns (var r0) { return storage[0x00]; }
function hello2(var arg0) {
storage[0x01] = arg0;
}
function b() returns (var r0) { return storage[0x01]; }
function hello1() {
storage[0x00] = 0x01;
}
}
memory[0x40:0x60] = 0x80;
if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }
var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;
if (var0 == 0x0dbe671f) {
......
} else if (var0 == 0x297c930b) {
......
} else if (var0 == 0x4df7e3d0) {
......
} else if (var0 == 0xdf022cbc) {
......
} else { revert(memory[0x00:0x00]); }
}
peth > sha3 a()
0dbe671f81a573cff601b9c227db0ed2e5339d3e0a07edc45c42f315a9cb8f0f
if (var0 == 0x0dbe671f) {
// Dispatch table entry for a()
var var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x007c;
var var2 = a();
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = var2;
var temp1 = memory[0x40:0x60];
return memory[temp1:temp1 + (temp0 + 0x20) - temp1];
memory[0x80:0x80 + 0x20] = a();
memory[temp1:temp1 + (temp0 + 0x20) - temp1];
即返回 memory[0x80:0x80 + 0x20];
也就是运行函数 a() 返回的结果。 function a() returns (var r0) { return storage[0x00]; }
contract example {
uint public a;
uint public b;
peth > sha3 b()
4df7e3d0fdffd35719c59893b4839a04b686be9ac7bec9cdd04a272e9ad7c628
peth > sha3 hello1()
df022cbc1f505806982da95791292644c22f8a2b2c487b63844bdf6ca13714ec
} else if (var0 == 0xdf022cbc) {
// Dispatch table entry for hello1()
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x00ff;
hello1();
stop();
function hello1() {
storage[0x00] = 0x01;
}
peth > sha3 hello2(uint256)
297c930ba4983097298563c2a37f1f470dc5e81bf4c68fdf08e199c2eb95dbe6
} else if (var0 == 0x297c930b) {
// Dispatch table entry for hello2(uint256)
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x00bd;
var2 = msg.data[0x04:0x24];
hello2(var2);
stop();
function hello2(var arg0) {
storage[0x01] = arg0;
}
状态变量在储存中的布局
-
存储插槽中的第一项会以低位对齐的方式储存。 -
值类型仅使用存储他们所需的字节。 -
如果一个被使用的存储插槽中剩余空间不足以存储一个值类型,那么它会被存入下一个存储插槽。 -
结构体和数组数据总是会开启一个新插槽(但是结构体或数组中的各元素还是按规则紧急打包)。 -
结构体和数组之后的数据也会开启一个新插槽。
————————————————————————————————
| a (32byte) | <- slot 0
————————————————————————————————
| b (32byte) | <- slot 1
————————————————————————————————
uint120 public a;
uint120 public b;
————————————————————————————————
| 0 0 | b (15byte) | a (15byte) | <- slot 0
————————————————————————————————
映射和动态数组
uint public a;
address public owner;
uint[] public users;
mapping(address => uint) balance;
————————————————————————————————
| a (32byte) | <- slot 0
————————————————————————————————
| 0 | owner (20byte) | <- slot 1
————————————————————————————————
| length of users[] | <- slot 2
————————————————————————————————
| [balance] | <- slot 3
————————————————————————————————
| | <- slot 4
————————————————————————————————
| ... | <- ......
————————————————————————————————
| users[0] | <- slot keccak(2)
————————————————————————————————
| users[1] | <- slot keccak(2)+1
————————————————————————————————
| ... | <- ......
keecak256(keecak256(p))
开始。x[i][j]
的位置,x
是 uint256[][]
类型,占用了第 p
个插槽,那么计算公式如下 keccak256( keccak256(p) + i) + j
如果数据类型不是 uint256,那么最后的偏移要稍微计算一下。k
所对应的值 v
所在的插槽则位于 keccak256(h(k).p)
处,其中 h()
是一个填充函数,规则根据键 k
的类型如下所示,.
是连接符,p
是声明时所占用的槽位。-
如果 k
是数值类型,h()
则将该值填充为 32 字节 -
如果 k
是字符串和字节数组,h()
则不做填充。
地址计算样例
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract C {
struct S { uint16 a; uint16 b; uint256 c; }
uint x;
mapping(uint => mapping(uint => S)) data;
}
data[4][9].c
的位置,首先映射占用 slot 1
(前面结构体的声明并不占用插槽),因此 data[4]
是从 keccak256(uint256(4).uint256(1))
开始存储,但是 data[4]
又是一个映射,所以它的占用的插槽 p
则为前面我们计算的值,那么 data[4][9]
则是从 keccak256(uint256(9).keccak256(uint256(4).uint256(1)))
开始储存,而 c
在结构体 S
中的偏移是 1
,因为 a,b
合占一个 slot
,所以最后 data[4][9].c
的位置为 keccak256(uint256(9).keccak256(uint256(4).uint256(1)))+1
,且 c
是一个 uint256
类型,所以它会占用一整个存储插槽。// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract C {
struct S { uint16 a; uint16 b; uint256 c; }
uint x;
mapping(uint => mapping(uint => S)) data;
constructor() public {
data[4][9].c = 0xdeadbeef;
}
}
function address_calc() public returns (bytes32){
return bytes32(uint256(keccak256(abi.encode(uint256(9),uint256(keccak256(abi.encode(uint256(4),uint256(1)))))))+1);
}
比特串和字符串
-
byte
bytes1,bytes2,...,bytes32
32 种,是值类型。可以进行如下几种运算-
比较运算符: <=
,<
,==
,!=
,>=
,>
(返回布尔型) -
位运算符: &
,|
,^
(按位异或),~
(按位取反) -
移位运算符: <<
(左移位),>>
(右移位) -
索引访问:如果 x
是bytesI
类型,那么x[k]
(其中0 <= k < I
)返回第k
个字节(只读)。
-
bytes
bytes
和 string
类型的变量是特殊的数组。 bytes
类似于 bytes1[]
,但是它在 calldata 和 memory 中是连续地存在一起的,不会按每 32 字节一单元的方式来存放。不过我们更多时候应该使用 bytes
而不是 bytes1[]
,因为 Gas 费用更低, 在 memory 中使用 bytes1[]
时,会在元素之间添加31个填充字节。 而在 storage 中,由于紧密包装,这没有填充字节。-
string
"foo"
或者 'bar'
)。 它们也可以分为多个连续的部分( "foo" "bar"
等效于 "foobar"
),这在处理长字符串时很有用。 不像在 C 语言中那样带有结束符; "foo"
相当于 3 个字节而不是 4 个。 和整数字面常量一样,字符串字面常量的类型也可以发生改变,bytes1
,……, bytes32
,如果合适的话,还可以转换成 bytes
以及 string
。bytes32 samevar = "stringliteral"
字符串字面常量在赋值给 bytes32
时被解释为原始的字节形式。-
<newline>
(转义实际换行) -
\
(反斜杠) -
'
(单引号) -
"
(双引号) -
b
(退格) -
f
(换页) -
n
(换行符) -
r
(回车) -
t
(标签 tab) -
v
(垂直标签) -
xNN
(十六进制转义,见下文) -
uNNNN
(unicode 转义,见下文)
xNN
表示一个 16 进制值,最终转换成合适的字节,而 uNNNN
表示 Unicode 编码值,最终会转换为 UTF-8 的序列。存储
bytes
和 string
编码是一样的。bytes1[]
类似,即有一个槽用于存放数组本身同时还有一个数据区,数据区位置使用槽的 keccak256
hash计算。 然而,对于短字节数组(短于32字节),数组元素与长度一起存储在同一个槽中。31
字节,则元素存储在高位字节(左对齐),最低位字节存储值 length * 2
。 如果数据长度大于等于 32 字节,则在占位插槽 p
存储 length * 2 + 1
,数据照常存储在 keccak256(p)
中。 因此,可以通过检查是否设置了最低位(因为 length * 2 + 1
的最低位肯定为 1
)来区分短数组和长数组。calldata布局
-
函数选择器 Function Selector
-
uint<M>
:M
位的无符号整数,0 < M <= 256
、M % 8 == 0
。例如:uint32
,uint8
,uint256
。 -
int<M>
:以 2 的补码作为符号的M
位整数,0 < M <= 256
、M % 8 == 0
。 -
address
:除了字面上的意思和语言类型的区别以外,等价于uint160
。在计算和 函数选择器Function Selector 中,通常使用address
。 -
uint
、int
:uint256
、int256
各自的同义词。在计算和 函数选择器Function Selector 中,通常使用uint256
和int256
。 -
bool
:等价于uint8
,取值限定为 0 或 1 。在计算和 函数选择器Function Selector 中,通常使用bool
。 -
bytes<M>
:M
字节的二进制类型,0 < M <= 32
。 -
function
:一个地址(20 字节)之后紧跟一个 函数选择器Function Selector (4 字节)。编码之后等价于bytes24
。
-
参数编码
-
calldata样例
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.4.16;
contract Foo {
function bar(bytes3[2]) public pure {}
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
function sam(bytes, bool, uint[]) public pure {}
}
Foo
,如果我们想用 69
和 true
做参数调用 baz
,我们总共需要传送 68 字节,可以分解为:-
0xcdcd77c0
:函数选择器。即baz(uint32,bool)
的 Keccak 哈希的前 4 字节。 -
0x0000000000000000000000000000000000000000000000000000000000000045
:第一个参数,一个被用 0 值字节补充到 32 字节的 uint32 值69
。 -
0x0000000000000000000000000000000000000000000000000000000000000001
:第二个参数,一个被用 0 值字节补充到 32 字节的 boolean 值true
。
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
bool
。比如它返回 false
,那么它的输出将是一个字节数组 0x0000000000000000000000000000000000000000000000000000000000000000
,一个bool值。["abc", "def"]
做参数调用 bar
,我们总共需要传送68字节,可以分解为:-
0xfce353f6
:函数选择器。即bar(bytes3[2])
的签名。 -
0x6162630000000000000000000000000000000000000000000000000000000000
:第一个参数的第一部分,一个bytes3
值"abc"
(左对齐)。 -
0x6465660000000000000000000000000000000000000000000000000000000000
:第一个参数的第二部分,一个bytes3
值"def"
(左对齐)。
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
"dave"
、 true
和 [1,2,3]
作为参数调用 sam
,我们总共需要传送 292 字节,可以分解为:-
0xa5643bf2
:函数选择器,即sam(bytes,bool,uint256[])
的签名。注意,uint
被替换为了它的权威代表uint256
。 -
0x0000000000000000000000000000000000000000000000000000000000000060
:第一个参数(动态类型)的数据部分的位置,即从参数编码块开始位置算起的字节数。在这里,是0x60
。因为0x00-0x20
存了这个值,0x20-0x40
存了bool
的值,0x40-0x60
存了uint256[]
数据部分的位置 -
0x0000000000000000000000000000000000000000000000000000000000000001
:第二个参数:boolean 的 true。 -
0x00000000000000000000000000000000000000000000000000000000000000a0
:第三个参数(动态类型)的数据部分的位置,由字节数计量。在这里,是0xa0
。 # 0x40 -
0x0000000000000000000000000000000000000000000000000000000000000004
:第一个参数的数据部分,以字节数组的元素个数作为开始,在这里,是 4。# 0x60 -
0x6461766500000000000000000000000000000000000000000000000000000000
:第一个参数的内容:"dave"
的 UTF-8 编码(在这里等同于 ASCII 编码),并在右侧(低位)用 0 值字节补充到 32 字节。 (区别 bytes[] 在 calldata 、memory、storage中布局的不同点)# 0x80 -
0x0000000000000000000000000000000000000000000000000000000000000003
:第三个参数的数据部分,以数组的元素个数作为开始,在这里,是 3。# 0xa0 -
0x0000000000000000000000000000000000000000000000000000000000000001
:第三个参数的第一个数组元素。 -
0x0000000000000000000000000000000000000000000000000000000000000002
:第三个参数的第二个数组元素。 -
0x0000000000000000000000000000000000000000000000000000000000000003
:第三个参数的第三个数组元素。
0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
小结
原文始发于微信公众号(山石网科安全技术研究院):区块链学习笔记之再探以太坊