Solidity and inline assembly

Elizabeth
Shyft Network
Published in
6 min readJul 31, 2018

Assembly language is a low-level language used to communicate directly with the processor. A high level language such as C, Go, or Java is compiled down to assembly before execution. An interesting feature of Solidity is its support for inline assembly. Assembly is used to interact directly with the EVM using opcodes. Assembly gives you much more control, enabling you to execute logic that may not be possible with just Solidity. Here are the docs for inline assembly in Solidity. I suggest keeping them open while reading this article for reference.

Assembly language uses instructions or opcodes such as PUSH, POP, ADD to act directly on items in memory. The EVM is a stack machine. If you don’t know what a stack machine is, here is an amazing series of posts detailing stack machines. A stack is a first-in-last-out data structure. The two main stack operations are PUSH and POP. PUSH puts an item on the top of the stack; POP removes the item on the top of the stack. The item most recently pushed onto the stack is the first that gets popped off.

A stack machine is a type of processor in which all operands are stored on a stack. A stack machine still has memory and registers for the PC (program counter) and SP (stack pointer). Not everything is stored on the stack, only operands are. Examining the following piece of assembly code:

PUSH 3
PUSH 5
ADD

The operations or opcodes are PUSH and ADD. The operands are 3 and 5. Let’s do a little visual of the stack. What happens here is:

PUSH 3 
|__3__|
PUSH 5
|__5__|
|__3__|
ADD
|__8__|

When the ADD opcode is called, no operands are passed to it. The machine knows implicitly that the operands it needs are on the top of the stack. ADD needs two operands, so it performs the ADD operation on the two top stack items.

This implicit passing of operands allows stack machine code to compile to a smaller size than register machine code. For more of a look at stack machines vs. register machines, I’d suggest this article and this interesting piece comparing processor performance between the two.

There are two main types of architectures used for processors: register machines and stack machines. Register machines are processors in which all operands are stored in quick-access fixed-byte-size memory. Registers are generally 32-bit. The size of the operand storage is referred to as the word size. A 32-bit processor has 1 word = 32 bits. The EVM has a 32-byte or 256-bit word size. This means every item in the stack or in memory has a size of 32 bytes.

For a look at a simplified virtual stack machine, checkout my mini stack machine implementation in C.

Let’s look at assembly in solidity. Say we want a function to add two uints and store the sum in memory. Normally, we would write:

function add(uint a, uint b) {
uint sum = a + b;
}

With inline assembly, we would write:

function add(uint a, uint b){
assembly {
let sum := add(a, b)
mstore(mload(0x40), sum)
}
}

Firstly, the syntax within an assembly block is different. We assign values to variables using := and we need to directly use opcodes instead of high-level syntax like + and uint . We declare variables by writing let x := 5 , let x := add(a,b) .

let sum := add(a, b) declared a variable called sum then executes the add opcode on a and b and saves the result in sum .

mstore(mload(0x40), sum)has a bit more going on. Let’s explore mload(0x40) first. According to the solidity docs, mload(p) returns mem[p..(p+32)]. mload(0x40) then returns the value stored in memory at address 0x40. In solidity, address 0x40 is reserved as a free memory pointer. The content of address 0x40 is the next free memory address.

mstore(p,v) sets mem[p..(p+32)] := v. In other words, mstore stores the value v at the memory address p. p..(p+32) means the entire word or block of memory at p is filled by v.

Putting these together, mstore(mload(0x40), sum) stores sum inside the next free memory location.

Let’s try returning the sum:

function add(uint a, uint b) returns (uint) {
assembly {
let sum := add(a, b)
mstore(0x0, sum)
return(0x0, 32)
}
}

return(p,s) is specified in the docs as `end execution, return data mem[p..(p+s)]`. This code stores sum at address 0x0, then returns the value stored at 0x0 from byte 0 to 32 (the entire word). uint is an alias for uint256, which is 256bits = 32 bytes, so we need to return the entire 32 bytes. Address 0x0 is reserved for return values, so it’s okay to put sum there.

Alternatively, we could declare sum in the function return:

function add(uint a, uint b) returns (uint sum) {
assembly {
let sum := add(a, b)
}
}

Let’s try reading call data to see how it looks. Call data is what’s sent to the contract when you wish to interact with it. Call data is formatted as a byte array. The first 4 bytes is reserved for the function signature and the rest is the parameters. For example, the function signature of function add(uint a, uint b) would be the first four bytes ofkeccak256("add(uint,uint)") . The EVM checks for a matching function signature in the contract, then calls that function using the following call data.

function callMe() returns (bytes4) {
assembly {
calldatacopy(0x0, 0, 4)
return(0x0, 4)
}
}

When we call callMe() , it performs a calldatacopy(memAddr, start, end) . In this example, we copy bytes 0 to 4 of the calldata to memory address 0x0. It then returns 4 bytes stored at address 0x0. What is returned is actually the function signature. It looks like this:0xe73620c3 . It is formatted as a hexidecimal literal with 8 characters. Remember, each character in a hexidecimal is half a byte (a byte can be valued up to 2⁸-1, a hex character can be valued up to 15, which is 2⁴-1.) Two characters is one byte, so 8 characters is 4 bytes.

Let’s modify the function a little to accept a uint, read the call data, and return it.

function callMe(uint a) returns (bytes32) {
assembly {
calldatacopy(0x0, 4, 36)
return(0x0, 32)
}
}

First, we changed the function to take uint a and return bytes32 . calldatacopy now copies bytes 4 to 36 of the call data to address 0x0. Bytes 4 to 36 of the calldata is theuint a we passed in. The function then returns the 32 bytes we wrote at 0x0. The function returns uint a , formatted as a bytes32.

Let’s make a function that will format the call data for callMe(uint a) . This function will accept uint a and return bytes . Bytes is a byte array; it is an alias for byte[].

function callFormatter(uint a) returns (bytes) {
bytes4 sig = bytes4(keccak256("callMe(uint)"));

assembly {
mstore(0x0, sig)
mstore(add(0x0, 4), a)
return(0x0,36)
}
}

First, we create the create the function signature. We hash callMe(uint) and cast it as a bytes4. Note we don’t put the parameter names in the function signature, just the types. Then, we store sig at address 0x0. We store a beside it, at 0x0 + 4. Remember, the signature is 4 bytes, so we need to add the 4 byte offset. We then return 36 bytes stored at 0x0, which is our formatted call data.

Next in the series: external function calls! The only way to get a return value from an external call is to use assembly.

--

--