
Cómo escribir programas de Solana con ensamblador SBPF
Tabla de contenido
- Arquitectura de la máquina virtual sBPF
- Arquitectura del conjunto de instrucciones de sBPF
- Modelo de memoria de sBPF
- Syscalls de Solana en sBPF
- Configura tu entorno
- Configura tu proyecto
- Ejemplo de memo en ensamblador sBPF
- Compila e implementa tu programa
- Interactúa con tu programa
- Validación de entradas
- Comprobación de límites de memoria
- Gestión de registros
- Seguridad aritmética
- Validación de parámetros de syscall
Actualmente, en el ecosistema de Solana se libra una carrera paralela por llevar la optimización de programas al nivel más bajo posible.
En términos generales, bibliotecas como Pinocchio están revolucionando el desarrollo en Rust y consiguiendo mejoras de varios órdenes de magnitud en la eficiencia computacional. Mientras tanto, en el nivel más bajo posible, un grupo de desarrolladores dedicados, unidos por su desprecio mutuo hacia el compilador, lleva esto un paso más allá. En lugar de escribir programas de Solana en lenguajes compilados como Rust o C, se enfocan en crear meticulosamente bytecode a mano para extraer el máximo rendimiento de cada instrucción.
Estas mejoras de bajo nivel solo son posibles cuando damos instrucciones directas a la VM en su lenguaje nativo: ensamblador sBPF, la variante propia de Solana del filtro de paquetes de Berkeley extendido (eBPF), el bytecode que se utiliza y ejecuta en cada programa on-chain.
Escribir ensamblador sBPF brinda a los desarrolladores acceso directo a la interfaz de más bajo nivel de la máquina virtual de Solana. Aunque el compilador de Rust y LLVM intentan optimizar, la sintaxis del lenguaje no siempre es lo bastante explícita o carecen del contexto necesario para tomar mejores decisiones de compilación. Por eso, suelen generar bytecode subóptimo frente al de un desarrollador experto con el control total sobre cada instrucción que permite el ensamblador.
Aunque este nivel adicional de control sacrifica ergonomía, el ahorro en el uso de unidades de cómputo y en el tamaño del binario —y, por tanto, en la renta— es significativo. Este ahorro cobra especial importancia en operaciones muy disputadas, competitivas y críticas para el rendimiento.
Al mismo tiempo, también puede argumentarse que no todos los programas deberían escribirse en ensamblador.
Aunque la situación ha mejorado drásticamente, históricamente las herramientas han sido limitadas. Más importante aún, las mejoras de rendimiento suelen implicar la considerable desventaja de verificar manualmente la corrección y aumentar los costos de auditoría. Esto se debe a la falta de herramientas automatizadas y a que la sintaxis es más difícil de leer, escribir y comprender.
Por otro lado, también puede decirse que los lenguajes compilados son una caja negra que oculta las decisiones del compilador. Por eso, la transparencia y el control adicionales del ensamblador suelen revelar aspectos que no se ven fácilmente al trabajar con lenguajes compilados. De hecho, la gran mayoría de los avances recientes de rendimiento en nuestros SDK de Rust surgieron al descubrir que podíamos crear a mano bytecode más eficiente que el compilador.
En este artículo, aprenderás:
- Qué es el ensamblador sBPF y cómo brinda control directo sobre la máquina virtual
- La evolución del filtro de paquetes de Berkeley a eBPF y por qué Solana lo adoptó
- La arquitectura de máquina virtual, el conjunto de instrucciones y el modelo de memoria de sBPF
- Cómo configurar tu entorno de desarrollo y compilar programas sBPF
- Programación paso a paso en ensamblador mediante un ejemplo práctico de memo
- Consideraciones de seguridad esenciales al escribir código de bajo nivel
¿Qué es el ensamblador?
El ensamblador es una variante legible por humanos del código máquina: el lenguaje de programación de más bajo nivel que se corresponde directamente con el conjunto de instrucciones de una CPU o VM.
En lugar de variables y funciones, el ensamblador opera con registros (ubicaciones de almacenamiento rápidas y temporales en la CPU), direcciones de memoria (ubicaciones físicas en la RAM o el disco) y operaciones fundamentales, como cargar (leer de la memoria), almacenar (guardar en la memoria), realizar cálculos aritméticos y ejecutar saltos (flujo de control).
Cada instrucción de ensamblador se corresponde de forma unívoca con una instrucción equivalente en código máquina.
Esta correspondencia unívoca permite a los programadores controlar con precisión lo que ejecuta el procesador, incluidos los registros que almacenan datos, cómo se accede a la memoria y la secuencia exacta de operaciones.
A diferencia de los lenguajes de alto nivel, donde una sola función puede generar decenas de instrucciones, el ensamblador ofrece total transparencia y control sobre el comportamiento de la máquina, sin abstracciones opacas.
¿Qué son Berkeley Packet Filter (BPF) y eBPF?
Berkeley Packet Filter (BPF) surgió en 1992 como una máquina virtual para filtrar eficientemente paquetes de red en kernels Unix. El BPF original utilizaba un conjunto de instrucciones sencillo y una arquitectura basada en registros que permitía ejecutar código de forma segura en un entorno aislado dentro del kernel.
Extended Berkeley Packet Filter (eBPF) modernizó este concepto y pasó de ser un filtro de paquetes a una máquina virtual de propósito general. eBPF introdujo una arquitectura de 64 bits, más registros y conjuntos de instrucciones más completos. Esto permitió ejecutar programas complejos de forma segura en el espacio del kernel para redes, seguridad y supervisión de sistemas.
Solana adoptó eBPF porque ofrecía un entorno de ejecución seguro y probado con aislamiento integrado. Este aislamiento evita que los programas accedan a recursos del sistema, bloqueen nodos o interfieran con otros programas. A su vez, la ejecución determinista garantiza que todos los validadores produzcan resultados idénticos.
Además, la arquitectura basada en registros y la cadena de herramientas madura lo hicieron ideal para ejecutar código on-chain con alto rendimiento. El backend existente de LLVM también permitía a los desarrolladores compilar desde lenguajes de alto nivel como Rust.
Arquitectura de la máquina virtual sBPF
Cuando se ejecuta un programa de Solana, el entorno de ejecución carga el bytecode sBPF en la memoria, realiza una verificación estática para garantizar su seguridad —comprueba bucles infinitos, accesos no válidos a memoria y el uso correcto de las instrucciones— y después lo ejecuta dentro de la máquina virtual.
La VM proporciona un entorno de ejecución controlado de 64 bits. Los programas se ejecutan completamente aislados del sistema anfitrión y de otros programas, y todo acceso a recursos se gestiona mediante el entorno de ejecución.
Arquitectura del conjunto de instrucciones de sBPF
sBPF opera con once registros de 64 bits (r0-r10). r10 funciona como puntero de marco de solo lectura y r0 como registro de retorno.
Las instrucciones siguen un formato uniforme, con códigos de operación que especifican las operaciones (aritmética, lógica, acceso a memoria y saltos) y operandos que indican registros de origen o destino, desplazamientos o valores inmediatos.
Las principales categorías de instrucciones incluyen operaciones de ALU (suma, resta y operaciones bit a bit), operaciones de memoria (carga y almacenamiento) y flujo de control (saltos condicionales e incondicionales).
Modelo de memoria de sBPF
Los programas sBPF operan dentro de una disposición estructurada de memoria: una pila de 4 KB para variables locales y llamadas a funciones, un heap para asignaciones dinámicas, datos del programa de solo lectura que contienen el bytecode y las constantes, y regiones de datos de cuentas asignadas a cuentas de Solana a las que el programa puede acceder durante la ejecución.
Se comprueban los límites de todos los accesos a memoria y los programas no pueden acceder fuera de sus regiones designadas.
Syscalls de Solana en sBPF
Los programas sBPF no pueden acceder directamente a los recursos del sistema ni realizar operaciones de E/S. En su lugar, solicitan servicios mediante syscalls, instrucciones especiales que transfieren el control al entorno de ejecución de Solana.
En el ensamblador sBPF, las syscalls se invocan mediante la instrucción call y un símbolo de llamada que el compilador modifica para convertirlo en un destino de llamada durante el ensamblado. Actualmente, las syscalls se invocan mediante reubicaciones dinámicas basadas en texto: un complejo sistema de tablas de búsqueda de cadenas que asigna símbolos a un hash Mumur3 de 32 bits durante la compilación JIT. Sin embargo, existe una propuesta activa para reemplazarlo por syscalls estáticas y simplificar drásticamente las convenciones de llamada. Cuando se invoca una syscall, los argumentos se pasan mediante los registros del 1 al 5. El registro 5 a veces actúa como desbordamiento de pila y los valores de retorno se escriben en r0.
Entre las syscalls comunes se incluyen las operaciones de memoria (sol_memcpy, sol_memcmp), las funciones criptográficas (hashing y verificación de firmas), el registro de eventos y las invocaciones entre programas.
Aunque todas las instrucciones sBPF normales tienen un costo de 1 CU, los cálculos de unidades de cómputo para las syscalls se gestionan de manera muy distinta. Cuando se añade una syscall al protocolo, se mide su rendimiento para determinar un costo base de invocación (por ejemplo, para una invocación CPI es de 1000). En algunos casos, también se aplica un costo variable adicional según la cantidad de datos consumidos.
Tutorial de ensamblador SBPF
Configura tu entorno
Tradicionalmente, escribir ensamblador sBPF requería toda la cadena de herramientas de Solana: un proceso pesado, complejo y dependiente de la plataforma.
Por este motivo, Dean Little creó el SDK de sBPF, que ofrece una solución integral para iniciar, crear, compilar, probar e implementar programas sBPF.
Puedes instalar el SDK en cualquier sistema operativo mediante Cargo:
cargo install --git https://github.com/blueshift-gg/sbpf.gitAntes de entrar en el código, también se recomienda instalar la extensión de ensamblador sBPF para VS Code, que ofrece resaltado de sintaxis, autocompletado y detección de errores.
Configura tu proyecto
Crea la estructura de un proyecto nuevo con:
sbpf init <name_of_the_project>Esto crea un proyecto con pruebas de Rust mediante Mollusk.
Si prefieres crear una estructura con pruebas de TypeScript, puedes usar lo siguiente para inicializarla:
sbpf init <name_of_the_project> --ts-tests Ejemplo de memo en ensamblador sBPF
Es difícil imaginar un programa más sencillo que un memo. Precisamente por eso es la introducción perfecta al ensamblador sBPF.
El programa hace una sola cosa: recibe los datos de instrucción que le envíes y los registra en la blockchain. Sin cuentas ni lógica compleja, solo control puro a nivel de instrucción sobre la máquina virtual de Solana.
Comencemos examinando el programa completo:
.equ NUM_ACCOUNTS, 0x00
.equ DATA_LEN, 0x08
.equ DATA, 0x10
.globl entrypoint
entrypoint:
ldxdw r0, [r1+NUM_ACCOUNTS]
ldxdw r2, [r1+DATA_LEN]
add64 r1, DATA
call sol_log_
exitDefine tus constantes
El programa comienza definiendo tres constantes que representan la estructura de la región de entrada serializada del entorno de ejecución de Solana:
.equ NUM_ACCOUNTS, 0x00 // Account count offset
.equ DATA_LEN, 0x08 // Data length offset
.equ DATA, 0x10 // Data start offsetEstos desplazamientos corresponden a las ubicaciones de memoria donde el entorno de ejecución de Solana empaqueta los datos de instrucción.
Cuando la VM llama a nuestro programa, nos entrega un búfer estructurado en el registro r1. Estas constantes nos permiten navegar por esa estructura.
Herramientas como sbpf.xyz pueden calcular automáticamente estos desplazamientos según la disposición de los datos de tu cuenta y de tus instrucciones.
Crea el punto de entrada
A continuación, creamos el punto de entrada y la validación de nuestro programa.
.globl entrypoint
entrypoint:
ldxdw r0, [r1+NUM_ACCOUNTS]La directiva de punto de entrada .globl indica al enlazador que haga visible globalmente el símbolo del punto de entrada. El entorno de ejecución de Solana busca este símbolo para saber dónde comenzar a ejecutar tu programa. Dato curioso: aunque normalmente lo llamamos punto de entrada, en realidad puede tener cualquier nombre.
La primera instrucción utiliza una ingeniosa técnica de validación específica del ensamblador. Como r0 es nuestro registro de retorno y cualquier valor de retorno distinto de 0 funciona como código de error, cargar el número de cuentas directamente en r0 obliga al programa a finalizar con un código de error distinto de cero si se proporcionan más de 0 cuentas.
Esto nos permite omitir manualmente cualquier cuenta proporcionada para validar el desplazamiento de nuestros datos de instrucción en la VM.
Invoca la syscall sol_log
Finalmente, podemos ejecutar la syscall sol_log_:
ldxdw r2, [r1+DATA_LEN] ; Load memo length
add64 r1, DATA ; Point r1 to memo data
call sol_log_ ; Log the memo
exit ; Exit with r0 valueLa syscall sol_log_ espera la longitud del mensaje en r2 y un puntero al mensaje que se registrará en r1. Por suerte, en la región de entrada serializada, el entorno de ejecución antepone automáticamente a los datos de instrucción un contador de longitud de 64 bits. Por tanto, podemos invocar la syscall sol_log_ de esta forma:
- Cargar en r2 el valor del desplazamiento DATA_LEN
- Hacer que r1 apunte al desplazamiento donde comienzan nuestros datos de instrucción
Cuando nuestros dos registros apuntan a los valores correctos, simplemente invocamos la syscall y después salimos con el valor que contenga r0.
Compila e implementa tu programa
Puedes compilar tu programa con el ensamblador integrado propio de SBPF, escrito por Claire Fan. Es un binario de Rust de 5 MB que reemplaza la cadena de herramientas LLVM de más de 2 GB que tendrías que usar con las herramientas de la plataforma Solana.
Para ejecutar una compilación, simplemente usa:
sbpf buildDespués de compilar tu programa, puedes implementarlo con:
sbpf deployInteractúa con tu programa
Para probar tu programa con las pruebas de la estructura generada, puedes ejecutar sbpf test. Si lo prefieres, puedes ejecutar el flujo completo con sbpf e2e para compilar, implementar y probar con un solo comando.
Consideraciones de seguridad en el ensamblador sBPF
Escribir código ensamblador implica asumir toda la responsabilidad por la seguridad: no hay un compilador que detecte tus errores. Cada instrucción afecta directamente la seguridad del programa, por lo que estos principios fundamentales son esenciales.
Validación de entradas
Los programas en ensamblador deben validar manualmente todas las entradas. Comprueba siempre la cantidad de cuentas, la longitud de los datos y el tamaño de los búferes antes de usarlos.
Por ejemplo, en nuestro programa de memo, cargar la cantidad de cuentas en r0 creó una validación automática: si se proporcionaban cuentas por error, el programa fallaba. Para procesar datos, comprueba que las longitudes estén dentro de los rangos esperados antes de acceder a la memoria.
Comprobación de límites de memoria
sBPF no proporciona comprobación automática de límites. Antes de acceder a arreglos o búferes, comprueba manualmente que tus operaciones de lectura y escritura se mantengan dentro de los límites asignados. Una simple comprobación de límites antes de acceder a la memoria puede evitar bloqueos y corrupción de datos:
jgt r2, MAX_LENGTH, error # Check if length exceeds limit
ldxb r3, [r1+r2] # Safe to load if check passesGestión de registros
Los registros contienen el estado crítico del programa. Cuando llames a funciones o syscalls, conserva los valores importantes guardándolos en la pila o en otros registros.
El puntero de marco (r10) y los valores de retorno de r0 requieren especial atención. Corromperlos puede bloquear tu programa o crear vulnerabilidades de seguridad.
Seguridad aritmética
La detección manual de desbordamientos es crucial para las operaciones aritméticas. Antes de sumar valores grandes, comprueba si el resultado podría superar los límites de 64 bits.
Las operaciones de división requieren comprobaciones explícitas de cero para evitar errores de ejecución.
Validación de parámetros de syscall
Las syscalls esperan parámetros válidos y fallarán si las entradas no lo son. Antes de llamar a una syscall, asegúrate de que los registros contengan punteros, longitudes y valores correctos. Los parámetros no válidos no solo provocan fallos, sino que también consumen unidades de cómputo innecesariamente.
Conclusión
El ensamblador sBPF no es para todos, y ese es precisamente el punto.
La mayoría de los desarrolladores debería seguir usando Rust y dejar que el compilador se encargue de las optimizaciones. Sin embargo, para quienes optimizan hasta la última unidad de cómputo o crean infraestructura crítica para el rendimiento, el ensamblador ofrece algo que ningún lenguaje de alto nivel puede brindar: control total.
Aquí cubrimos los fundamentos, desde cómo encaja sBPF en la arquitectura de Solana hasta la creación de tu primer programa de memo.
El ejemplo del memo puede parecer trivial, pero demuestra los principios fundamentales que usarás en programas más complejos:
- Manipulación directa de registros
- Gestión manual de memoria
- Manejo explícito de syscalls
Estas mejoras tienen un costo: cambias las redes de seguridad por velocidad y las abstracciones por control. Deberías elegir ensamblador cuando ya hayas optimizado tu código Rust y aún necesites más rendimiento, cuando crees infraestructura donde cada microsegundo importa o cuando necesites hacer algo que el compilador simplemente no puede optimizar bien.
Para todo lo demás, evítate el dolor de cabeza y sigue usando Rust.
Las herramientas están mejorando, la comunidad está creciendo y las ventajas de rendimiento hablan por sí solas. Pero recuerda: “un gran poder conlleva una gran responsabilidad para no romper nada”.
Si quieres leer más contenido sobre cómo usar el ensamblador sBPF, consulta el curso de introducción al ensamblador en Blueshift y pon a prueba tus habilidades con algunos de los desafíos disponibles allí.
Artículos relacionados
Suscríbete a Helius
Mantente al día con las novedades del desarrollo en Solana y recibe actualizaciones cuando publiquemos


