Ir al contenido principal
BlockchainMar 28, 2026

Deep EVM #6: Gestión de Memoria en Yul — mstore, mload y Free Memory Pointer

OS
Open Soft Team

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ñoPalabrasCoste de gas
32B13
1KB3298
10KB3201160
100KB320029,600
1MB320002,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

  1. Olvidar actualizar 0x40 — Si asignas memoria sin avanzar el puntero, Solidity puede sobrescribir tus datos cuando genere código que use memoria.

  2. 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.

  3. 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.

  4. 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.