Solidity語言學習筆記————38、Solidity彙編
Solidity彙編(Solidity Assembly)
Solidity定義了一個組合語言,可以不同Solidity一起使用。這個組合語言還可以嵌入到Solidity原始碼中,以內聯彙編的方式使用。下面我們將從內聯彙編如何使用著手,介紹其與獨立使用的組合語言的不同,最後再介紹這門組合語言。
內聯彙編(Inline Assembly)
通常我們通過庫程式碼,來增強語言我,實現一些精細化的控制,Solidity為我們提供了一種接近於EVM底層的語言,內聯彙編,允許與Solidity結合使用。由於EVM是棧式的,所以有時定位棧比較麻煩,Solidty的內聯彙編為我們提供了下述的特性,來解決手寫底層程式碼帶來的各種問題:
- 允許函式風格的操作碼:
mul(1, add(2, 3))
等同於push1 3 push1 2 add push1 1 mul
- 內聯區域性變數:
let x := add(2, 3) let y := mload(0x40) x := add(x, y)
- 可訪問外部變數:
function f(uint x) { assembly { x := sub(x, 1) } }
- 標籤:
let x := 10 repeat: x := sub(x, 1) jumpi(repeat, eq(x, 0))
- 迴圈:
for { let i := 0 } lt(i, x) { i := add(i, 1) } { y := mul(2, y) }
- switch語句:
switch x case 0 { y := mul(x, 2) } default { y := 0 }
- 函式呼叫:
function f(x) -> y { switch x case 0 { y := 1 } default { y := mul(x, f(sub(x, 1))) } }
下面將詳細介紹內聯編譯(inline assembly)語言。
警告 |
---|
需要注意的是內聯編譯是一種非常底層的方式來訪問EVM虛擬機器。他沒有Solidity提供的多種安全機制。 |
註解 |
---|
TODO:待補充內聯彙編的變數作用域的不同,尤其是使用含internal的函式的庫時所引入的複雜度。另外,還需補充,編譯器定義的符號(symbols)。 |
示例
下面的例子提供了一個庫函式來訪問另一個合約,並把它寫入到一個bytes
變數中。有一些不能通過常規的Solidity語言完成,內聯庫可以用來在某些方面增強語言的能力。
pragma solidity ^0.4.0;
library GetCode {
function at(address _addr) public view returns (bytes o_code) {
assembly {
// 取得程式碼的大小,這需要彙編
let size := extcodesize(_addr)
// 分配輸出位元組陣列
// 也可以不使用匯編完成——o_code = new bytes(size)
o_code := mload(0x40)
// new "memory end" including padding
mstore(0x40,到add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// 儲存長度在記憶體中
mstore(o_code, size)
// 實際取得程式碼,這需要彙編
extcodecopy(_addr, add(o_code, 0x20), 0, size)
}
}
}
內聯彙編在當編譯器沒辦法得到有效率的程式碼時非常有用。但需要留意的是內聯彙編寫起來是比較難的,因為編譯器不會進行一些檢查,所以你應該只在複雜的,且你知道你在做什麼的事情上使用它。pragma solidity ^0.4.16;
library VectorSum {
// 此函式效率較低,因為優化器當前無法
// 移除陣列訪問中的邊界檢查。
function sumSolidity(uint[] _data) public view returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i)
o_sum += _data[i];
}
// 我們知道我們只訪問陣列,所以我們可以避免檢查。
// 0x20需要新增到一個陣列中,因為第一個位置包含
// 陣列長度。
function sumAsm(uint[] _data) public view returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i) {
assembly {
o_sum := add(o_sum, mload(add(add(_data, 0x20), mul(i, 0x20))))
}
}
}
// 功能與上述相同,但完全使用內聯彙編。
function sumPureAsm(uint[] _data) public view returns (uint o_sum) {
assembly {
// 載入長度(前32位元組)
let len := mload(_data)
// 跳過長度欄位。
//
// 保持臨時變數,以便可以在適當位置增加。
//
// 注意:在這個彙編程式塊之後,遞增的資料會導致一個不可用的資料變數。
let data := add(_data, 0x20)
// Iterate until the bound is not met.
for
{ let end := add(data, len) }
lt(data, end)
{ data := add(data, 0x20) }
{
o_sum := add(o_sum, mload(data))
}
}
}
}
語法
內聯編譯語言也會像Solidity一樣解析註釋,字面量和識別符號。所以你可以使用//
和/**/
的方式註釋。內聯編譯的在Solidity中的語法是包裹在assembly { ... }
,下面是可用的語法,後續有更詳細的內容。
- 字面量。如
0x123
,42
或"abc"
(字串最多是32個字元) - 操作碼(指令的方式),如
mload sload dup1 sstore
,後面有可支援的指令列表 - 函式風格的操作碼,如
add(1, mlod(0)
- 標籤,如
name:
- 變數定義,如
let x := 7
、let x := add(y, 3)
或let x
(初始值為0) - 識別符號(標籤或內聯區域性變數或外部),如
jump(name)
,3 x add
- 賦值(指令風格),如,
3 =: x
。 - 函式風格的賦值,如
x := add(y, 3)
- 支援塊級的區域性變數,如
{ let x := 3 { let y := add(x, 1) } }
操作碼
這個文件不想介紹EVM虛擬機器的完整描述,但後面的列表可以做為EVM虛擬機器的指令碼的一個參考。
如果一個操作碼有引數(通過在棧頂),那麼他們會放在括號。需要注意的是引數的順序可以顛倒(非函式風格,後面會詳細說明)。用-
標記的操作碼不會將一個引數推到棧頂,而標記為*
的是非常特殊的,所有其它的將且只將一個推到棧頂。用F
、H
、B
或C
標記的操作碼分別來自Frontier、Homestead、Byzantium或Constantinople。Constantinople仍在計劃中,所有這樣的指令都會導致無效的指令異常。
在後面的例子中,mem[a...b)
表示成位置a
到位置b
(不包含)的memory位元組內容,storage[p]
表示在位置p
的strorage內容。
操作碼pushi
和jumpdest
不能被直接使用。
在語法中,操作碼被表示為預先定義的識別符號。
操作碼 | 說明 | ||
---|---|---|---|
stop | - | F | 停止執行,等同於return(0,0) |
add(x, y) | F | x + y | |
sub(x, y) | F | x - y | |
mul(x, y) | F | x * y | |
div(x, y) | F | x / y | |
sdiv(x, y) | F | x / y, for signed numbers in two’s complement | |
mod(x, y) | F | x % y | |
smod(x, y) | F | x % y, for signed numbers in two’s complement | |
exp(x, y) | F | x to the power of y | |
not(x) | F | ~x, every bit of x is negated | |
lt(x, y) | F | 1 if x < y, 0 otherwise | |
gt(x, y) | F | 1 if x > y, 0 otherwise | |
slt(x, y) | F | 1 if x < y, 0 otherwise, for signed numbers in two’s complement | |
sgt(x, y) | F | 1 if x > y, 0 otherwise, for signed numbers in two’s complement | |
eq(x, y) | F | 1 if x == y, 0 otherwise | |
iszero(x) | F | 1 if x == 0, 0 otherwise | |
and(x, y) | F | bitwise and of x and y | |
or(x, y) | F | bitwise or of x and y | |
xor(x, y) | F | bitwise xor of x and y | |
byte(n, x) | F | nth byte of x, where the most significant byte is the 0th byte | |
shl(x, y) | C | logical shift left y by x bits | |
shr(x, y) | C | logical shift right y by x bits | |
sar(x, y) | C | arithmetic shift right y by x bits | |
addmod(x, y, m) | F | (x + y) % m with arbitrary precision arithmetics | |
mulmod(x, y, m) | F | (x * y) % m with arbitrary precision arithmetics | |
signextend(i, x) | F | sign extend from (i*8+7)th bit counting from least significant | |
keccak256(p, n) | F | keccak(mem[p…(p+n))) | |
sha3(p, n) | F | keccak(mem[p…(p+n))) | |
jump(label) | - | F | jump to label / code position |
jumpi(label, cond) | - | F | jump to label if cond is nonzero |
pc | F | current position in code | |
pop(x) | - | F | remove the element pushed by x |
dup1 … dup16 | F | copy ith stack slot to the top (counting from top) | |
swap1 … swap16 | * | F | swap topmost and ith stack slot below it |
mload(p) | F | mem[p..(p+32)) | |
mstore(p, v) | - | F | mem[p..(p+32)) := v |
mstore8(p, v) | - | F | mem[p] := v & 0xff (only modifies a single byte) |
sload(p) | F | storage[p] | |
sstore(p, v) | - | F | storage[p] := v |
msize | F | size of memory, i.e. largest accessed memory index | |
gas | F | gas still available to execution | |
address | F | address of the current contract / execution context | |
balance(a) | F | wei balance at address a | |
caller | F | call sender (excluding delegatecall ) | |
callvalue | F | wei sent together with the current call | |
calldataload(p) | F | call data starting from position p (32 bytes) | |
calldatasize | F | size of call data in bytes | |
calldatacopy(t, f, s) | - | F | copy s bytes from calldata at position f to mem at position t |
codesize | F | size of the code of the current contract / execution context | |
codecopy(t, f, s) | - | F | copy s bytes from code at position f to mem at position t |
extcodesize(a) | F | size of the code at address a | |
extcodecopy(a, t, f, s) | - | F | like codecopy(t, f, s) but take code at address a |
returndatasize | B | size of the last returndata | |
returndatacopy(t, f, s) | - | B | copy s bytes from returndata at position f to mem at position t |
create(v, p, s) | F | create new contract with code mem[p..(p+s)) and send v wei and return the new address | |
create2(v, n, p, s) | C | create new contract with code mem[p..(p+s)) at address keccak256(. n . keccak256(mem[p..(p+s))) and send v wei and return the new address | |
call(g, a, v, in, insize, out, outsize) | F | call contract at address a with input mem[in..(in+insize)) providing g gas and v wei and output area mem[out..(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success | |
callcode(g, a, v, in, insize, out, outsize) | F | identical to call but only use the code from a and stay in the context of the current contract otherwise | |
delegatecall(g, a, in, insize, out, outsize) | H | identical to callcode but also keep caller and callvalue | |
staticcall(g, a, in, insize, out, outsize) | B | identical to call(g, a, 0, in, insize, out, outsize) but do not allow state modifications | |
return(p, s) | - | F | end execution, return data mem[p..(p+s)) |
revert(p, s) | - | B | end execution, revert state changes, return data mem[p..(p+s)) |
selfdestruct(a) | - | F | end execution, destroy current contract and send funds to a |
invalid | - | F | end execution with invalid instruction |
log0(p, s) | - | F | log without topics and data mem[p..(p+s)) |
log1(p, s, t1) | - | F | log with topic t1 and data mem[p..(p+s)) |
log2(p, s, t1, t2) | - | F | log with topics t1, t2 and data mem[p..(p+s)) |
log3(p, s, t1, t2, t3) | - | F | log with topics t1, t2, t3 and data mem[p..(p+s)) |
log4(p, s, t1, t2, t3, t4) | - | F | log with topics t1, t2, t3, t4 and data mem[p..(p+s)) |
origin | F | transaction sender | |
gasprice | F | gas price of the transaction | |
blockhash(b) | F | hash of block nr b - only for last 256 blocks excluding current | |
coinbase | F | current mining beneficiary | |
timestamp | F | timestamp of the current block in seconds since the epoch | |
number | F | current block number | |
difficulty | F | difficulty of the current block | |
gaslimit | F | block gas limit of the current block |
字面量(Literals)
你可以使用整數常量,通過直接以十進位制或16進位制的表示方式,將會自動生成恰當的pushi
指令。
assembly { 2 3 add "abc" and }
上面的例子中,將會先加2,3得到5,然後再與字串abc進行與運算。字串按左對齊儲存,且不能超過32位元組。
函式風格(Functional Style)
你可以在操作碼後接著輸入操作碼,它們最終都會生成正確的位元組碼。比如下面將會新增3
到memory中位置0x80
:
3 0x80 mload add 0x80 mstore
由於經常很難直觀的看到某個操作碼真正的引數,Solidity內聯編譯提供了一個函式風格的表示式,上面的程式碼與下述等同:mstore(0x80, add(mload(0x80), 3))
函式風格的表示式不能在內部使用指令風格,如1 2 mstore(0x80, add)
將不是合法的,必須被寫為mstore(0x80, add(2, 1))
。那些不帶引數的操作碼,括號可以忽略。
需要注意的是函式風格的引數與指令風格的引數是反的。如果使用函式風格,第一個引數將會出現在棧頂。
訪問外部函式與變數(Access to External Variables and Functions)
Solidity中的變數和其它識別符號,可以簡單的通過名稱引用。對於memory變數,這將會把地址而不是值推到棧上。Storage的則有所不同,由於對應的值不一定會佔滿整個storage槽位,所以它的地址由槽和實際儲存位置相對起始位元組偏移。要搜尋變數x
指向的槽位,使用x_slot
,得到變數相對槽位起始位置的偏移使用x_offset
。
在賦值中(見下文),我們甚至可以直接向Solidity變數賦值。
還可以訪問內聯編譯的外部函式:內聯編譯會推入整個的入口的label(應用虛擬函式解析的方式)。Solidity中的呼叫語義如下:
- 呼叫者推入
return label
,arg1
,arg2
, …argn
- 呼叫返回
ret1
,ret2
,…,retm
這個功能使用起來還是有點麻煩,因為堆疊偏移量在呼叫過程中基本上有變化,因此對區域性變數的引用將是錯誤的。
pragma solidity ^0.4.11;
contract C {
uint b;
function f(uint x) public returns (uint r) {
assembly {
r := mul(x, sload(b_slot)) // ignore the offset, we know it is zero
}
}
}
註解 |
---|
如果訪問小於256位的型別的變數(例如uint64 , address , bytes16 或 byte ),則不能對不屬於該型別編碼的位進行任何假設。特別是,不要假定它們為零。為了安全起見,在使用該資料之前,請始終正確清除資料:uint32 x = f(); assembly { x := and(x, 0xffffffff) /* now use x */ } 。清除有符號的型別,您可以使用signextend 操作碼。 |
標籤(Labels)
註解 |
---|
標籤被棄用。請使用函式、迴圈、if或switch語句來代替。 |
另一個在EVM的彙編的問題是jump
和jumpi
使用了絕對地址,可以很容易的變化。Solidity內聯彙編提供了標籤來讓jump跳轉更加容易。需要注意的是標籤是非常底層的特性,儘量使用內聯彙編函式,迴圈,Switch指令來代替。下面是一個求Fibonacci的例子:
{
let n := calldataload(4)
let a := 1
let b := a
loop:
jumpi(loopend, eq(n, 0))
a add swap1
n := sub(n, 1)
jump(loop)
loopend:
mstore(0, a)
return(0, 0x20)
}
需要注意的是自動訪問棧元素需要內聯者知道當前的棧高。這在跳轉的源和目標之間有不同棧高時將失敗。當然你也仍然可以在這種情況下使用jump,但你最好不要在這種情況下訪問棧上的變數(即使是內聯變數)。
此外,棧高分析器會一個操作碼接著一個操作碼的分析程式碼(而不是根據控制流),所以在下面的情況下,彙編程式將對標籤two
的堆疊高度產生錯誤的判斷:
{
let x := 8
jump(two)
one:
// Here the stack height is 2 (because we pushed x and 7),
// but the assembler thinks it is 1 because it reads
// from top to bottom.
// Accessing the stack variable x here will lead to errors.
x := 9
jump(three)
two:
7 // push something onto the stack
jump(one)
three:
}
宣告彙編-區域性變數(Declaring Assembly-Local Variables)
你可以通過let
關鍵字來定義在內聯彙編中有效的變數,實際上它只是在{...}
中有效。內部實現上是,在let
指令出現時會在棧上建立一個新槽位,來儲存定義的臨時變數,在塊結束時,會自動在棧上移除對應變數。你需要為變數提供一個初始值,比如0
,但也可以是複雜的函式表示式:
pragma solidity ^0.4.16;
contract C {
function f(uint x) public view returns (uint b) {
assembly {
let v := add(x, 1)
mstore(0x80, v)
{
let y := add(sload(v), 1)
b := y
} // y is "deallocated" here
b := add(b, v)
} // v is "deallocated" here
}
}
賦值(Assignments)
你可以向內聯區域性變數賦值,或者函式區域性變數。需要注意的是當你向一個指向memory或storage賦值時,你只是修改了對應指標而不是對應的資料。
有兩種方式的賦值方式:函式風格和指令風格。函式風格,比如variable := value
,你必須在函式風格的表示式中提供一個變數,最終將得到一個棧變數。指令風格=: variable
,值則直接從棧底取。以於兩種方式冒號指向的都是變數名稱。賦值的效果是將棧上的變數值替換為新值。
{
let v := 0 // functional-style assignment as part of variable declaration
let g := add(v, 2)
sload(10)
=: v // instruction style assignment, puts the result of sload(10) into v
}
註解 |
---|
指令風格賦值被deprecated。 |
If
if語句可用於有條件地執行程式碼。沒有“else”部分,如果需要多個備選方案,請考慮使用“switch”(見下文)
{
if eq(value, 0) { revert(0, 0) }
}
body的花括號是必須的。
Switch
你可以使用switch
語句來作為一個基礎版本的if/else
語句。它需要取一個值,用它來與多個常量進行對比。每個分支對應的是對應切爾西到的常量。與某些語言容易出錯的行為相反,控制流不會自動從一個判斷情景到下一個場景(即預設是break
的)。最後有個叫default
的兜底。
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
可以有的case不需要包裹到大括號中,但每個case需要用大括號的包裹。
迴圈
內聯彙編支援一個簡單的for風格的迴圈。for風格的迴圈的頭部有三個部分,一個是初始部分,一個條件和一個後疊加部分。條件必須是一個函式風格的表示式,而其它兩個部分用大括號包裹。如果在初始化的塊中定義了任何變數,這些變數的作用域會被預設擴充套件到迴圈體內(條件,與後面的疊加部分定義的變數也類似。因為預設是塊作用域,所以這裡是一種特殊情況)。
下面的示例計算記憶體中的一個區域的和。
{
let x := 0
for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
x := add(x, mload(i))
}
}
for迴圈也可以類似於while迴圈:簡單地保留初始化和迭代後的部分空。{
let x := 0
let i := 0
for { } lt(i, 0x100) { } { // while(i < 0x100)
x := add(x, mload(i))
i := add(i, 0x20)
}
}
函式
組合語言允許定義底層的函式。這些需要在棧上取引數(以及一個返回的程式碼行),也會將結果存到棧上。呼叫一個函式與執行一個函式風格的操作碼看起來是一樣的。
函式可以在任何地方定義,可以在定義的塊中可見。在函式內,你不能訪問一個在函式外定義的一個區域性變數。同時也沒有明確的return
語句。
如果你呼叫一個函式,並返回了多個值,你可以將他們賦值給一個元組,使用a, b := f(x)
或let a, b := f(x)
。
下面的例子中通過平方乘來實現一個指數函式。
{
function power(base, exponent) -> result {
switch exponent
case 0 { result := 1 }
case 1 { result := base }
default {
result := power(mul(base, base), div(exponent, 2))
switch mod(exponent, 2)
case 1 { result := mul(base, result) }
}
}
}
要注意的事(Things to Avoid)
內聯彙編使用中需要一個比較高的視野,但它又是非常底層的語法。函式呼叫,迴圈,switch被轉換為簡單的重寫規則,另外一個語言提供的是重安排函式風格的操作碼,管理了jump標籤,計算了棧高以方便變數的訪問,同時在塊結束時,移除塊內定義的塊內的區域性變數。特別需要注意的是最後兩個情況。你必須清醒的知道,組合語言只提供了從開始到結束的棧高計算,它沒有根據你的邏輯去計算棧高(譯者注:這常常導致錯誤)。此外,像交換這樣的操作,僅僅交換棧裡的內容,並不是變數的位置。
Solidity中的慣例
與EVM彙編不同,Solidity知道型別少於256位元組,如,uint24
。為了讓他們更高效,大多數的數學操作僅僅是把也們當成是一個256位元組的數字進行計算,高位的位元組只在需要的時候才會清理,比如在寫入記憶體前,或者在需要比較時。這意味著如果你在內聯彙編中訪問這樣的變數,你必須要手動清除高位的無效位元組。
Solidity以非常簡單的方式來管理記憶體:內部存在一個空間記憶體的指標在記憶體位置0x40
。如果你想分配記憶體,可以直接使用從那個位置的記憶體,並相應的更新指標。
記憶體的前64個位元組可以用作短期分配的“劃痕空間”。空閒記憶體指標(即0x60
開始)後的32位元組永遠為0,並用作空動態記憶體陣列的初始值。
Solidity中的記憶體陣列元素,總是佔用多個32位元組的記憶體(也就是說byte[]
也是這樣,但是bytes
和string
不是這樣)。多維的memory的陣列是指向memory的陣列。一個動態陣列的長度儲存在資料的第一個槽位,緊接著就是陣列的元素。
警告 |
---|
靜態大小的記憶體陣列沒有長度欄位,但是它在未來很快會被新增,以便在靜態和動態大小的陣列之間允許更好的可互換性,所以請不要依賴於此。 |
相關文章
- Solidity語言學習筆記————39、獨立彙編Solid筆記
- Solidity語言學習筆記————1、初識Solidity語言Solid筆記
- Solidity語言學習筆記————2、使用編譯器Solid筆記編譯
- Solidity語言學習筆記————36、 庫Solid筆記
- Solidity語言學習筆記————37、Using forSolid筆記
- Solidity語言學習筆記————4、常量Solid筆記
- Solidity語言學習筆記————33、事件(Events)Solid筆記事件
- Solidity語言學習筆記————12、陣列Solid筆記陣列
- Solidity語言學習筆記————34、繼承Solid筆記繼承
- Solidity語言學習筆記————43、安全考量Solid筆記
- Solidity語言學習筆記————42、提示和技巧Solid筆記
- Solidity語言學習筆記————28、純函式Solid筆記函式
- Solidity語言學習筆記————26、回退函式Solid筆記函式
- Solidity語言學習筆記————16、對映MappingSolid筆記APP
- Solidity語言學習筆記————32、建立合約Solid筆記
- Solidity語言學習筆記————41、記憶體佈局Solid筆記記憶體
- Solidity語言學習筆記————25、作用域和宣告Solid筆記
- Solidity語言學習筆記————27、檢視函式Solid筆記函式
- Solidity語言學習筆記————10、布林型、整型Solid筆記
- Solidity語言學習筆記————3、Remix的基本使用Solid筆記REM
- Solidity語言學習筆記————15、結構體StructSolid筆記結構體Struct
- Solidity語言學習筆記————18、字串和函式Solid筆記字串函式
- Solidity語言學習筆記————14、左值運算子Solid筆記
- Solidity語言學習筆記————9、左值運算子Solid筆記
- Solidity語言學習筆記————17、原始檔對映Solid筆記
- Solidity語言學習筆記————5、全域性變數Solid筆記變數
- Solidity語言學習筆記————30、函式過載Solid筆記函式
- Solidity語言學習筆記————35、抽象合約和介面Solid筆記抽象
- Solidity語言學習筆記————26、Assert, Require, Revert 和 ExceptionsSolid筆記UIException
- Solidity語言學習筆記————22、可見性和GettersSolid筆記
- Solidity語言學習筆記————20、函式修飾符Solid筆記函式
- Solidity學習筆記-2Solid筆記
- Solidity語言學習筆記————23、函式呼叫和賦值Solid筆記函式賦值
- Solidity語言學習筆記————24、輸入輸出引數Solid筆記
- Solidity語言學習筆記————13、固定大小位元組陣列Solid筆記陣列
- Solidity語言學習筆記————14、動態位元組陣列Solid筆記陣列
- Solidity語言學習筆記————8、運算子優先順序Solid筆記
- Solidity語言學習筆記————44、合約的後設資料Solid筆記