-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathinline_assembly_notes.txt
276 lines (218 loc) · 15 KB
/
inline_assembly_notes.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
INLINE ASSEMBLY NOTES
1. functional-style opcodes: mul(1, add(2, 3)) instead of push1 3 push1 2 add push1 1 mul
2. assembly-local variables: let x := add(2, 3) let y := mload(0x40) x := add(x, y)
3. access to external variables: function f(uint x) public { assembly { x := sub(x, 1) } }
4. labels: let x := 10 repeat: x := sub(x, 1) jumpi(repeat, eq(x, 0))
5. loops: for { let i := 0 } lt(i, x) { i := add(i, 1) } { y := mul(2, y) }
6. if statements: if slt(x, 0) { x := sub(0, x) }
7. switch statements: switch x case 0 { y := mul(x, 2) } default { y := 0 }
8. function calls: function f(x) -> y { switch x case 0 { y := 1 } default { y := mul(x, f(sub(x, 1))) } }
Warning:
Inline assembly is a way to access the Ethereum Virtual Machine at a low level. This discards several important safety features of Solidity.
SYNTAX
Assembly parses comments, literals and identifiers exactly as Solidity, so you can use the usual // and /* */ comments. Inline assembly is marked by assembly { ... } and inside these curly braces, the following can be used (see the later sections for more details)
1. literals, i.e. 0x123, 42 or "abc" (strings up to 32 characters)
2. opcodes (in “instruction style”), e.g. mload sload dup1 sstore, for a list see below
3. opcodes in functional style, e.g. add(1, mlod(0))
4. labels, e.g. name:
5. variable declarations, e.g. let x := 7, let x := add(y, 3) or let x (initial value of empty (0) is assigned)
6. identifiers (labels or assembly-local variables and externals if used as inline assembly), e.g. jump(name), 3 x add
7. assignments (in “instruction style”), e.g. 3 =: x
8. assignments in functional style, e.g. x := add(y, 3)
9. blocks where local variables are scoped inside, e.g. { let x := 3 { let y := add(x, 1) } }
OPCODES
Instruction Explanation
stop - F stop execution, identical to 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(<address> . 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
FUNCTIONAL STYLE VS INTRUCTION STYLE
Instruction style example:
3 0x80 mload add 0x80 mstore
Functional style example:
mstore(0x80, add(mload(0x80), 3))
Functional style expressions cannot use instructional style internally, i.e. 1 2 mstore(0x80, add) is not valid assembly, it has to be written as mstore(0x80, add(2, 1)). For opcodes that do not take arguments, the parentheses can be omitted.
Note that the order of arguments is reversed in functional-style as opposed to the instruction-style way. If you use functional-style, the first argument will end up on the stack top.
ACCESS TO EXTERNAL VARIABLES AND FUNCTIONS
Solidity variables and other identifiers can be accessed by simply using their name. For memory variables, this will push the address and not the value onto the stack. Storage variables are different: Values in storage might not occupy a full storage slot, so their “address” is composed of a slot and a byte-offset inside that slot. To retrieve the slot pointed to by the variable x, you used x_slot and to retrieve the byte-offset you used x_offset. Example:
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
}
}
}
Functions external to inline assembly can also be accessed: The assembly will push their entry label (with virtual function resolution applied).
DECLARING ASSEMBLY-LOCAL VARIABLES
You can use the let keyword to declare variables that are only visible in inline assembly and actually only in the current {...}-block. What happens is that the let instruction will create a new stack slot that is reserved for the variable and automatically removed again when the end of the block is reached. You need to provide an initial value for the variable which can be just 0, but it can also be a complex functional-style expression.
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
Assignments are possible to assembly-local variables and to function-local variables. Take care that when you assign to variables that point to memory or storage, you will only change the pointer and not the data.
There are two kinds of assignments: functional-style and instruction-style. For functional-style assignments (variable := value), you need to provide a value in a functional-style expression that results in exactly one stack value and for instruction-style (=: variable), the value is just taken from the stack top. For both ways, the colon points to the name of the variable. The assignment is performed by replacing the variable’s value on the stack by the new value.
{
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
}
CONTROL STRUCTURES
If:
The if statement can be used for conditionally executing code. There is no “else” part, consider using “switch” if you need multiple alternatives.
{
if eq(value, 0) { revert(0, 0) }
}
Switch:
You can use a switch statement as a very basic version of “if/else”. It takes the value of an expression and compares it to several constants. The branch corresponding to the matching constant is taken. Contrary to the error-prone behaviour of some programming languages, control flow does not continue from one case to the next. There can be a fallback or default case called default.
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
The list of cases does not require curly braces, but the body of a case does require them.
Loops:
Assembly supports a simple for-style loop. For-style loops have a header containing an initializing part, a condition and a post-iteration part. The condition has to be a functional-style expression, while the other two are blocks. If the initializing part declares any variables, the scope of these variables is extended into the body (including the condition and the post-iteration part).
The following example computes the sum of an area in memory.
{
let x := 0
for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
x := add(x, mload(i))
}
}
For loops can also be written so that they behave like while loops: Simply leave the initialization and post-iteration parts empty.
{
let x := 0
let i := 0
for { } lt(i, 0x100) { } { // while(i < 0x100)
x := add(x, mload(i))
i := add(i, 0x20)
}
}
Functions:
Assembly allows the definition of low-level functions. These take their arguments (and a return PC) from the stack and also put the results onto the stack. Calling a function looks the same way as executing a functional-style opcode.
Functions can be defined anywhere and are visible in the block they are declared in. Inside a function, you cannot access local variables defined outside of that function. There is no explicit return statement.
If you call a function that returns multiple values, you have to assign them to a tuple using a, b := f(x) or let a, b := f(x).
The following example implements the power function by square-and-multiply.
{
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
Inline assembly might have a quite high-level look, but it actually is extremely low-level. Function calls, loops, ifs and switches are converted by simple rewriting rules and after that, the only thing the assembler does for you is re-arranging functional-style opcodes, managing jump labels, counting stack height for variable access and removing stack slots for assembly-local variables when the end of their block is reached. Especially for those two last cases, it is important to know that the assembler only counts stack height from top to bottom, not necessarily following control flow. Furthermore, operations like swap will only swap the contents of the stack but not the location of variables.
So basically avoid working with the stack.
SOLIDITY VS EVM ASSEMBLY
In contrast to EVM assembly, Solidity knows types which are narrower than 256 bits, e.g. uint24. In order to make them more efficient, most arithmetic operations just treat them as 256-bit numbers and the higher-order bits are only cleaned at the point where it is necessary, i.e. just shortly before they are written to memory or before comparisons are performed. This means that if you access such a variable from within inline assembly, you might have to manually clean the higher order bits first.
Solidity manages memory in a very simple way: There is a “free memory pointer” at position 0x40 in memory. If you want to allocate memory, just use the memory from that point on and update the pointer accordingly.
Elements in memory arrays in Solidity always occupy multiples of 32 bytes (yes, this is even true for byte[], but not for bytes and string). Multi-dimensional memory arrays are pointers to memory arrays. The length of a dynamic array is stored at the first slot of the array and then only the array elements follow.
Warning: Statically-sized memory arrays do not have a length field, but it will be added soon to allow better convertibility between statically- and dynamically-sized arrays, so please do not rely on that.
STANDALONE ASSEMBLY
The assembly language described as inline assembly above can also be used standalone and in fact, the plan is to use it as an intermediate language for the Solidity compiler. In this form, it tries to achieve several goals:
1. Programs written in it should be readable, even if the code is generated by a compiler from Solidity.
2. The translation from assembly to bytecode should contain as few “surprises” as possible.
3. Control flow should be easy to detect to help in formal verification and optimization.
Assembly happens in four stages:
Parsing
Desugaring (removes switch, for and functions)
Opcode stream generation
Bytecode generation