Deep EVM #6: Gestión de Memoria en Yul — mstore, mload y Free Memory Pointer
Engineering Team
La memoria de la EVM en detalle
La memoria de la EVM es un arreglo de bytes lineal que comienza vacío y se expande bajo demanda. A diferencia del storage que persiste entre transacciones, la memoria existe solo durante la ejecución de un call frame específico. Cuando una llamada retorna, su memoria desaparece.
Tres opcodes gestionan la memoria:
- MSTORE(offset, value) — Escribe 32 bytes en la posición offset
- MLOAD(offset) — Lee 32 bytes desde la posición offset
- MSTORE8(offset, value) — Escribe 1 byte en la posición offset
El free memory pointer
Solidity reserva los primeros 128 bytes (0x00-0x7f) de memoria para propósitos especiales:
- 0x00-0x3f — Scratch space para operaciones de hashing
- 0x40-0x5f — Free memory pointer (puntero de memoria libre)
- 0x60-0x7f — Slot cero (inicializado a 0x00)
- 0x80+ — Inicio de la memoria libre
El free memory pointer en 0x40 es el mecanismo de “asignación” de Solidity. Cada vez que Solidity necesita memoria (para arrays, structs, strings, o ABI encoding), lee el puntero, usa esa posición, y avanza el puntero.
assembly {
// Leer el free memory pointer actual
let ptr := mload(0x40)
// ptr == 0x80 al inicio de la ejecución
// Asignar 64 bytes
mstore(0x40, add(ptr, 64))
// Ahora puedes usar ptr a ptr+63 libremente
mstore(ptr, 42)
mstore(add(ptr, 32), 99)
}
Expansión de memoria cuadrática
El coste de gas de la memoria no es lineal. Sigue esta fórmula:
coste = 3 * palabras + (palabras² / 512)
Donde palabras = ceil(tamaño_maximo / 32). Esto significa:
| Tamaño | Palabras | Coste de gas |
|---|---|---|
| 32B | 1 | 3 |
| 1KB | 32 | 98 |
| 10KB | 320 | 1160 |
| 100KB | 3200 | 29,600 |
| 1MB | 32000 | 2,048,000 |
Nota cómo el coste explota: 1KB cuesta ~100 gas, pero 1MB cuesta más de 2 millones. Esta es una defensa deliberada contra el uso excesivo de memoria.
ABI encoding manual en Yul
Una de las optimizaciones más comunes es codificar datos para llamadas externas manualmente:
function transferOptimizado(address token, address to, uint256 amount) internal {
assembly {
let ptr := mload(0x40)
// Selector de transfer(address,uint256)
mstore(ptr, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
// Parámetro 1: address to
mstore(add(ptr, 4), to)
// Parámetro 2: uint256 amount
mstore(add(ptr, 36), amount)
// Llamar al token
let success := call(gas(), token, 0, ptr, 68, 0, 32)
if iszero(success) {
revert(0, 0)
}
}
}
Esto evita el overhead de ABI encoding de Solidity y puede ahorrar 200-500 gas por llamada.
Patrones comunes de memoria
Copiar calldata a memoria
assembly {
let size := calldatasize()
let ptr := mload(0x40)
calldatacopy(ptr, 0, size)
mstore(0x40, add(ptr, size))
}
Buffer de retorno dinámico
assembly {
// Construir un array dinámico de uint256 en memoria
let ptr := mload(0x40)
let length := 5
mstore(ptr, length) // Primer slot: longitud del array
// Llenar elementos
for { let i := 0 } lt(i, length) { i := add(i, 1) } {
mstore(add(add(ptr, 32), mul(i, 32)), mul(i, i)) // i²
}
// Retornar como bytes
let totalSize := add(32, mul(length, 32))
return(ptr, totalSize)
}
Hashing eficiente
assembly {
// Usar el scratch space (0x00-0x3f) para hashing
// No necesita actualizar el free memory pointer
mstore(0x00, clave)
mstore(0x20, slotBase)
let hash := keccak256(0x00, 0x40)
}
Trampas comunes
-
Olvidar actualizar 0x40 — Si asignas memoria sin avanzar el puntero, Solidity puede sobrescribir tus datos cuando genere código que use memoria.
-
Alineación — MLOAD y MSTORE operan en límites de 32 bytes. Leer de una posición no alineada te da datos de ambos lados de la frontera.
-
Expansión innecesaria — Cada vez que accedes a una posición de memoria más alta que el máximo anterior, pagas el coste de expansión. Reutiliza posiciones de memoria cuando sea posible.
-
Interferencia con Solidity — Si mezclas Yul y Solidity, el código de Solidity asume que el free memory pointer es correcto. Corrompirlo causa bugs silenciosos y devastadores.
Conclusión
La gestión de memoria en Yul requiere disciplina — no hay recolector de basura, no hay checks de límites, y los errores son silenciosos. Pero dominar mstore, mload y el free memory pointer te da control preciso sobre uno de los recursos más importantes de la EVM, permitiendo optimizaciones significativas en ABI encoding, hashing y construcción de datos de retorno.