中文翻译文档:http://www.tryblockchain.org/Solidity-%E8%AF%AD%E8%A8%80%E4%BB%8B%E7%BB%8D.html
英文官方文档:http://solidity.readthedocs.io/en/develop/
Solidity是一种智能合约高级语言,运行在Ethereum虚拟机(EVM)之上。
它的语法接近于Javascript,是一种面向对象的语言。但作为一种真正意义上运行在网络上的去中心合约,它又有很多的不同,下面列举一些:
Address
的类型。用于定位用户,定位合约,定位合约的代码(合约本身也是一个帐户)。payable
,可以在语言层面直接支持支付,而且超级简单。听起来高大上,其实入手玩起来也可以很简单:
pragma solidity ^0.4.0;
contract HelloWorld{
uint balance;
function update(uint amount) returns (address, uint){
balance += amount;
return (msg.sender, balance);
}
}
通过读取参数输入的新值,并将之累加至合约的变量中,返回发送人的地址,和最终的累计值。
使用无需安装的浏览器编译器Remix可以立即看到效果。打开后,如下图所示:
输入上述代码,点击Create按钮,就能在浏览器中创建能调用函数的按钮。在update
按钮旁输入入参,点击按钮,就能执行函数调用并打印出函数返回的结果了。
备注
如果出现错误,可以等待浏览器资源加载完成,或强制刷新后再试。
new
)作为AST输出的一部分,编译器会提供AST某个节点以应的源代码的范围。这可以被用来做基于AST的静态代码错误分析,可以高亮本地变量,和他们对应使用的调试工具。
此外,编译器也可以生成字节码到生成指令源代码的范围映射。这对静态分析工具来说非常重要,它们在字节码级别分析,可以来在调试工具内显示对应代码位置,或支持断点操作。
上述的源代码映射都使用整数来引用源代码。
在Solidity的类型系统里面有一些类型有一些在其它语言中没有的语法。其中之一就是函数类型。但依然,使用var
时,可以把函数类型作为本地变量。
contract FunctionSelector {
function select(bool useB, uint x) returns (uint z) {
var f = a;
if (useB) f = b;
return f(x);
}
function a(uint x) returns (uint z) {
return x * x;
}
function b(uint x) returns (uint z) {
return 2 * x;
}
}
可以对var赋值为不同的函数。
当一个值占用的位数小于32字节时,那些没有用到的位必须被清除掉。Solidity编译器设计实现为,在任何可能受到潜在的残存数据带来的副作用之前,清理掉这些脏数据。比如,在向内存写入一个值前,不需要的字节位需要被清除掉,因为没有用到的内存位可能被用来计算哈希,或作为消息调用的发送的数据存储。同样的,在向storage
中存储时,未用到的字节位需要被清理掉,否则这些脏数据会带来意想不到的事情。
另一方面,如果接下来的后述操作不会产生副作用,我们不会主动清理这些字节位。比如,由于任何非0的值被JUMP
指令认为是true
。在它作用JUMPI
指令的条件前,我们在不会清理这个布尔值。
在上述设计准则之外,Solidity编译器会在输入数据加载到栈上后清理掉它们。
不同的类型,有不同的无效值的清理规则。
类型 | 有效值 | 无效值意味着 |
---|---|---|
有n的成员的枚举类型 | 0到(n - 1) | 异常(exception) |
布尔 | 0或1 | 1 |
有符号整数 | sign-extended word | 当前静默的包装了结果,以后会以异常的形式抛出来 |
无符号整数 | 高位节是0 | 当前静默的包装了结果,以后会以异常的形式抛出来 |
Solidity是基于汇编优化的,所以它可以,同时也被其它编程语言所使用(译者注:其它语言编译为汇编)。编译器会在JUMP
和JUMPDEST
处拆分基本的指令序列为一个个的基本块。在这些代码块内,所有的指令都被分析。所有的对栈,内存或存储的操作被记录成由指令及其参数组成的一个个表达式,这些表达式又会指向另一个表达式。核心目的是找到一些表达式在任何输入的情况下都恒等,然后将它们组合成一个表达式类。优化器首先尝试在一系列已知的表达式中,找出来一些全新的表达式。如果找不到,表达式通过一些简单的原则进行简化,比如 constant + constant = sum_of_constants
或 X * 1 = X
。由于这一切是递归进行的,我们可以在第二项是一个更复杂的表达时,应用上述后续规则。对内存或存储的修改,存储的位置经常会被擦除,由此我们并不知道存的数据有什么不同:如果我们首先写入一个值x,再写入另一个值y,这两个都是输入变量,第二个写入时会覆盖第一个,所以我们实际在写入第二个值时,不知道第一个值是什么了。所以,如果一个简单的表达式x-y
指向一个非0的常量,这样我们就能在操作y时知道x内存储的值。
在流程最后,我们知道哪一个表达式会在栈顶,并且有一系列的对内存或存储的修改。这些信息与基本的块存在一起以方便的用来连接他们。此外,关于栈,存储和内存配置的信息会传递到下一个块。如果我们知道所有JUMP和JUMPI指令的目标,我们可以构建程序的完整的控制流程图。如果有任何一个我们不知道目标的跳转(因为目标是通过输入参数进行计算的,所以原则上可能发生),我们必须擦除块知识的输入,因为他有可能是某个跳转的目的地(译者注:因为可能某个跳转在运行时会指向他,修改他的状态,所以他的推算状态是错误的)。如果某个JUMPI
被发现他的条件是常量,它会被转化为一个无状态的跳转。
在最后一步,每个块中的代码都将重新生成。在某个块结束时,将生成栈上表达式的依赖树,不在这个树上的操作就被丢弃了。在我们原始代码中想要应用的对内存、存储想要的修改顺序的代码就生成出来了(被丢弃的修改被认为是完全不需要的),最终,生成了所有的需要在栈上存在的值。
这些步骤应用于每个基本的块,如果新生成的代码更小,将会替换现有的代码。如果一个块在分析期间在JUMPI
处分裂,条件被证实为一个常量,JUMPI
将可以基于常量值被替换掉,比如下述代码:
var x = 7;
data[7] = 9;
if (data[x] != x + 2)
return 2;
else
return 1;
简化的代码可以被编译为:
data[7] = 9;
return 1;
尽管上述代码在一开始有一个跳转指令。
当Solidity合约被部署后,从某个帐户调用这个合约,需要输入的数据是需要符合the ABI specification, 中文翻译见这里: http://me.tryblockchain.org/Solidity-abi-abstraction.html。ABI规范需要参数被填充为多个32字节。内部的函数调用,则使用了不同的约定。
Solidity预留了3个32字节大小的槽位:
暂存空间(scratch space)
空闲内存指针(free memory pointer)
)暂存空间可在语句之间使用(如在内联编译时使用)
Solidity总是在空闲内存指针
所在位置创建一个新对象,且对应的内存永远不会被释放(也许未来会改变这种做法)。
有一些在Solidity中的操作需要超过64字节的临时空间,这样就会超过预留的暂存空间
。他们就将会分配到空闲内存指针
所在的地方,但由于他们自身的特点,生命周期相对较短,且指针本身不能更新,内存也许会,也许不会被清零(zerod out)
。因此,大家不应该认为空闲的内存一定已经是清零(zeroed out)
的。
大小固定的变量(除了映射
,变长数组
以外的所有类型)在存储(storage)中是依次连续从位置0
开始排列的。如果多个变量占用的大小少于32字节,会尽可能的打包到单个storage
槽位里,具体规则如下:
结构体
和数组
总是使用一个全新的槽位,并占用整个槽(但在结构体内或数组内的每个项仍遵从上述规则)当使用的元素占用少于32字节,你的合约的gas使用也许更高。这是因为EVM每次操作32字节。因此,如果元素比这小,EVM需要更多操作来从32字节减少到需要的大小。
因为编译器会将多个元素打包到一个storage
槽位,这样就可以将多次读或写组合进一次操作中,只有在这时,通过缩减变量大小来优化存储结构才有意义。当操作函数参数和memory
的变量时,因为编译器不会这样优化,所以没有上述的意义。
最后,为了方便EVM进行优化,尝试有意识排序storage
的变量和结构体的成员,从而让他们能打包得更紧密。比如,按这样的顺序定义,uint128, uint128, uint256
,而不是uint128, uint256, uint128
。因为后一种会占用三个槽位。
结构体和数组里的元素按它们给定的顺序存储。
由于它们不可预知的大小。映射
和变长数组
类型,使用Keccak-256
哈希运算来找真正数据存储的起始位置。这些起始位置往往是完整的堆栈槽。
映射
和动态数组
根据上述规则在位置p
占用一个未满的槽位(对映射
里嵌套映射
,数组中嵌套数组的情况则递归应用上述规则)。对一个动态数组,位置p
这个槽位存储数组的元素个数(字节数组和字符串例外,见下文)。而对于映射
,这个槽位没有填充任何数据(但这是必要的,因为两个挨着的映射
将会得到不同的哈希值)。数组的原始数据位置是keccak256(p)
;而映射
类型的某个键k
,它的数据存储位置则是keccak256(k . p)
,其中的.
表示连接符号。如果定位到的值以是一个非基本类型,则继续运用上述规则,是基于keccak256(k . p)
的新的偏移offset
。
bytes
和string
占用的字节大小如果足够小,会把其自身长度和原始数据存在当前的槽位。具体来说,如果数据最多31位长,高位存数据(左对齐),低位存储长度lenght * 2
。如果再长一点,主槽位就只存lenght * 2 + 1
。原始数据按普通规则存储在keccak256(slot)
所以对于接下来的代码片段:
pragma solidity ^0.4.4;
contract C {
struct s { uint a; uint b; }
uint x;
mapping(uint => mapping(uint => s)) data;
}
按上面的代码来看,结构体从位置0开始,这里定义了一个结构体,但并没有对应的结构体变量,故不占用空间。uint x
实际是uint256
,单个占32字节,占用槽位0,所以映射data
将从槽位1开始。
data[4][9].b
的位置在keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1
有人在这里尝试直接读取区块链的存储值,https://github.com/ethereum/solidity/issues/1550
上面介绍的在Solidity中嵌入的内联汇编语言也可以单独使用。实际上,它是被计划用来作为编译器的一种中间语言。在这个目的下,它尝试达到下述的目标:
为了达到第一条和最后一条的目标,Solidity汇编语言提供了高层级的组件比如,for
循环,switch
语句和函数调用。这样的话,可以不直接使用SWAP
,DUP
,JUMP
,JUMPI
语句,因为前两个有混淆的数据流,后两个有混淆的控制流。此外,函数形式的语句如mul(add(x, y), 7)
比纯的指令码的形式7 y x add num
更加可读。
第二个目标是通过引入一个绝对阶段来实现,该阶段只能以非常规则的方式去除较高级别的构造,并且仍允许检查生成的低级汇编代码。Solidity汇编语言提供的非原生的操作是用户定义的标识符的命名查找(函数名,变量名等),这些都遵循简单和常规的作用域规则,会清理栈上的局部变量。
作用域:一个标识符(标签,变量,函数,汇编)在定义的地方,均只有块级作用域(作用域会延伸到,所在块所嵌套的块)。跨函数边界访问局部变量是不合法的,即使可能在作用域内(译者注:这里可能说的是,函数内定义多个函数的情况,JavaScript有这种语法)。不允许shadowing
。局部变量不能在定义前被访问,但标签,函数和汇编可以。汇编是非常特殊的块结构可以用来,如,返回运行时的代码,或创建合约。外部定义的汇编变量在子汇编内不可见。
如果控制流来到了块的结束,局部变量数匹配的pop
指令会插入到栈底(译者注:移除局部变量,因为局部变量失效了)。无论何时引用局部变量,代码生成器需要知道其当前在堆栈中的相对位置,因此需要跟踪当前所谓的堆栈高度。由于所有的局部变量在块结束时会被移除,因此在进入块之前和之后的栈高应该是不变的,如果不是这样的,将会抛出一个警告。
我们为什么要使用高层级的构造器,比如switch
,for
和函数。
使用switch
,for
和函数,可以在不用jump
和jumpi
的情况下写出来复杂的代码。这会让分析控制流更加容易,也可以进行更多的形式验证及优化。
此外,如果手动使用jumps
,计算栈高是非常复杂的。栈内所有的局部变量的位置必须是已知的,否则指向本地变量的引用,或者在块结束时自动删除局部变量都不会正常工作。脱机处理机制正确的在块内不可达的地方插入合适的操作以修正栈高来避免出现jump
时非连续的控制流带来的栈高计算不准确的问题。
示例:
我们从一个例子来看一下Solidity到这种中间的脱机汇编结果。我们可以一起来考虑下下述Soldity程序的字节码:
contract C {
function f(uint x) returns (uint y) {
y = 1;
for (uint i = 0; i < x; i++)
y = 2 * y;
}
}
它将生成下述的汇编内容:
{
mstore(0x40, 0x60) // store the "free memory pointer"
// function dispatcher
switch div(calldataload(0), exp(2, 226))
case 0xb3de648b {
let (r) = f(calldataload(4))
let ret := $allocate(0x20)
mstore(ret, r)
return(ret, 0x20)
}
default { revert(0, 0) }
// memory allocator
function $allocate(size) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, size))
}
// the contract function
function f(x) -> y {
y := 1
for { let i := 0 } lt(i, x) { i := add(i, 1) } {
y := mul(2, y)
}
}
}
在经过脱机汇编阶段,它会编译成下述的内容:
{
mstore(0x40, 0x60)
{
let $0 := div(calldataload(0), exp(2, 226))
jumpi($case1, eq($0, 0xb3de648b))
jump($caseDefault)
$case1:
{
// the function call - we put return label and arguments on the stack
$ret1 calldataload(4) jump(f)
// This is unreachable code. Opcodes are added that mirror the
// effect of the function on the stack height: Arguments are
// removed and return values are introduced.
pop pop
let r := 0
$ret1: // the actual return point
$ret2 0x20 jump($allocate)
pop pop let ret := 0
$ret2:
mstore(ret, r)
return(ret, 0x20)
// although it is useless, the jump is automatically inserted,
// since the desugaring process is a purely syntactic operation that
// does not analyze control-flow
jump($endswitch)
}
$caseDefault:
{
revert(0, 0)
jump($endswitch)
}
$endswitch:
}
jump($afterFunction)
allocate:
{
// we jump over the unreachable code that introduces the function arguments
jump($start)
let $retpos := 0 let size := 0
$start:
// output variables live in the same scope as the arguments and is
// actually allocated.
let pos := 0
{
pos := mload(0x40)
mstore(0x40, add(pos, size))
}
// This code replaces the arguments by the return values and jumps back.
swap1 pop swap1 jump
// Again unreachable code that corrects stack height.
0 0
}
f:
{
jump($start)
let $retpos := 0 let x := 0
$start:
let y := 0
{
let i := 0
$for_begin:
jumpi($for_end, iszero(lt(i, x)))
{
y := mul(2, y)
}
$for_continue:
{ i := add(i, 1) }
jump($for_begin)
$for_end:
} // Here, a pop instruction will be inserted for i
swap1 pop swap1 jump
0 0
}
$afterFunction:
stop
}
汇编有下面四个阶段:
我们将简单的以步骤1到3指定步骤。更加详细的步骤将在后面说明。
解析的任务如下:
组合词典遵循由Solidity本身定义的词组。
空格用于分隔标记,它由空格,制表符和换行符组成。 注释是常规的JavaScript / C ++注释,并以与Whitespace相同的方式进行解释。
语法:
AssemblyBlock = '{' AssemblyItem* '}'
AssemblyItem =
Identifier |
AssemblyBlock |
FunctionalAssemblyExpression |
AssemblyLocalDefinition |
FunctionalAssemblyAssignment |
AssemblyAssignment |
LabelDefinition |
AssemblySwitch |
AssemblyFunctionDefinition |
AssemblyFor |
'break' | 'continue' |
SubAssembly | 'dataSize' '(' Identifier ')' |
LinkerSymbol |
'errorLabel' | 'bytecodeSize' |
NumberLiteral | StringLiteral | HexLiteral
Identifier = [a-zA-Z_$] [a-zA-Z_0-9]*
FunctionalAssemblyExpression = Identifier '(' ( AssemblyItem ( ',' AssemblyItem )* )? ')'
AssemblyLocalDefinition = 'let' IdentifierOrList ':=' FunctionalAssemblyExpression
FunctionalAssemblyAssignment = IdentifierOrList ':=' FunctionalAssemblyExpression
IdentifierOrList = Identifier | '(' IdentifierList ')'
IdentifierList = Identifier ( ',' Identifier)*
AssemblyAssignment = '=:' Identifier
LabelDefinition = Identifier ':'
AssemblySwitch = 'switch' FunctionalAssemblyExpression AssemblyCase*
( 'default' AssemblyBlock )?
AssemblyCase = 'case' FunctionalAssemblyExpression AssemblyBlock
AssemblyFunctionDefinition = 'function' Identifier '(' IdentifierList? ')'
( '->' '(' IdentifierList ')' )? AssemblyBlock
AssemblyFor = 'for' ( AssemblyBlock | FunctionalAssemblyExpression)
FunctionalAssemblyExpression ( AssemblyBlock | FunctionalAssemblyExpression) AssemblyBlock
SubAssembly = 'assembly' Identifier AssemblyBlock
LinkerSymbol = 'linkerSymbol' '(' StringLiteral ')'
NumberLiteral = HexNumber | DecimalNumber
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+
一个AST转换,移除其中的for
,switch
和函数构建。结果仍由同一个解析器,但它不确定使用什么构造。如果添加仅跳转到并且不继续的jumpdests,则添加有关堆栈内容的信息,除非没有局部变量访问到外部作用域或栈高度与上一条指令相同。伪代码如下:
desugar item: AST -> AST =
match item {
AssemblyFunctionDefinition('function' name '(' arg1, ..., argn ')' '->' ( '(' ret1, ..., retm ')' body) ->
:
{
jump($<name>_start)
let $retPC := 0 let argn := 0 ... let arg1 := 0
$<name>_start:
let ret1 := 0 ... let retm := 0
{ desugar(body) }
swap and pop items so that only ret1, ... retm, $retPC are left on the stack
jump
0 (1 + n times) to compensate removal of arg1, ..., argn and $retPC
}
AssemblyFor('for' { init } condition post body) ->
{
init // cannot be its own block because we want variable scope to extend into the body
// find I such that there are no labels $forI_*
$forI_begin:
jumpi($forI_end, iszero(condition))
{ body }
$forI_continue:
{ post }
jump($forI_begin)
$forI_end:
}
'break' ->
{
// find nearest enclosing scope with label $forI_end
pop all local variables that are defined at the current point
but not at $forI_end
jump($forI_end)
0 (as many as variables were removed above)
}
'continue' ->
{
// find nearest enclosing scope with label $forI_continue
pop all local variables that are defined at the current point
but not at $forI_continue
jump($forI_continue)
0 (as many as variables were removed above)
}
AssemblySwitch(switch condition cases ( default: defaultBlock )? ) ->
{
// find I such that there is no $switchI* label or variable
let $switchI_value := condition
for each of cases match {
case val: -> jumpi($switchI_caseJ, eq($switchI_value, val))
}
if default block present: ->
{ defaultBlock jump($switchI_end) }
for each of cases match {
case val: { body } -> $switchI_caseJ: { body jump($switchI_end) }
}
$switchI_end:
}
FunctionalAssemblyExpression( identifier(arg1, arg2, ..., argn) ) ->
{
if identifier is function with n args and m ret values ->
{
// find I such that $funcallI_* does not exist
$funcallI_return argn ... arg2 arg1 jump()
pop (n + 1 times)
if the current context is `let (id1, ..., idm) := f(...)` ->
let id1 := 0 ... let idm := 0
$funcallI_return:
else ->
0 (m times)
$funcallI_return:
turn the functional expression that leads to the function call
into a statement stream
}
else -> desugar(children of node)
}
default node ->
desugar(children of node)
}
在操作码流生成期间,我们在一个计数器中跟踪当前的栈高,所以通过名称访问栈的变量是可能的。栈高在会修改栈的操作码后或每一个标签后进行栈调整。当每一个新局部变量被引入时,它都会用当前的栈高进行注册。如果要访问一个变量(或者拷贝其值,或者对其赋值),会根据当前栈高与变量引入时的当时栈高的不同来选择合适的DUP
或SWAP
指令。
伪代码:
codegen item: AST -> opcode_stream =
match item {
AssemblyBlock({ items }) ->
join(codegen(item) for item in items)
if last generated opcode has continuing control flow:
POP for all local variables registered at the block (including variables
introduced by labels)
warn if the stack height at this point is not the same as at the start of the block
Identifier(id) ->
lookup id in the syntactic stack of blocks
match type of id
Local Variable ->
DUPi where i = 1 + stack_height - stack_height_of_identifier(id)
Label ->
// reference to be resolved during bytecode generation
PUSHof label>
SubAssembly ->
PUSHof subassembly data>
FunctionalAssemblyExpression(id ( arguments ) ) ->
join(codegen(arg) for arg in arguments.reversed())
id (which has to be an opcode, might be a function name later)
AssemblyLocalDefinition(let (id1, ..., idn) := expr) ->
register identifiers id1, ..., idn as locals in current block at current stack height
codegen(expr) - assert that expr returns n items to the stack
FunctionalAssemblyAssignment((id1, ..., idn) := expr) ->
lookup id1, ..., idn in the syntactic stack of blocks, assert that they are variables
codegen(expr)
for j = n, ..., i:
SWAPi where i = 1 + stack_height - stack_height_of_identifier(idj)
POP
AssemblyAssignment(=: id) ->
look up id in the syntactic stack of blocks, assert that it is a variable
SWAPi where i = 1 + stack_height - stack_height_of_identifier(id)
POP
LabelDefinition(name:) ->
JUMPDEST
NumberLiteral(num) ->
PUSHas decimal and right-aligned>
HexLiteral(lit) ->
PUSH32as hex and left-aligned>
StringLiteral(lit) ->
PUSH328 encoded and left-aligned>
SubAssembly(assembly block) ->
append codegen(block) at the end of the code
dataSize() ->
assert that is a subassembly ->
PUSH32of code generated from subassembly >
linkerSymbol() ->
PUSH32 and append position to linker table
}