En el tutorial anterior, aprendimos cómo un programa lee de la memoria hacia los registros de la máquina virtual (VM) sBPF. Ahora, ampliaremos ese modelo mostrando cómo los programas invocan la funcionalidad del entorno de ejecución de Solana a través de syscalls, con los argumentos de las syscalls suministrados mediante registros.
Un syscall es una API expuesta y ejecutada por el entorno de ejecución de Solana que los programas llaman para realizar operaciones que no pueden llevar a cabo por sí mismos, como la generación de logs y la invocación entre programas (cross-program invocation).
Así es como funciona:
- El programa carga valores en los registros de argumentos (
r1ar5) si elsyscallespera argumentos. - Tu programa ejecuta la instrucción
syscally transfiere el control al entorno de ejecución de Solana. El manejador delsyscalllee de los registros en los que cargaste valores. - El entorno de ejecución realiza la operación solicitada y luego devuelve el control al programa.

Solana proporciona syscalls para diferentes propósitos como la generación de logs, operaciones criptográficas, invocación entre programas, acceso a sysvars y operaciones de memoria. Para este tutorial, nos centraremos en los syscalls de logging.
syscalls para logging
Los programas invocan syscalls de logging para imprimir valores durante la ejecución. Existen cinco syscalls de logging, las cuales se enumeran a continuación. Discutiremos cada una de ellas en detalle en una sección posterior.
fn sol_log_(message: *const u8, len: u64): Estesyscallimprime texto UTF-8 en el log del programa.fn sol_log_data(data: *const u8, data_len: u64): Registra datos de bytes arbitrarios en el log del programa.fn sol_log_64_(arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64): Estesyscallregistra cinco valores enteros de 64 bits.fn sol_log_pubkey(pubkey_addr: *const u8): Registra una clave pública en el log del programa.fn sol_log_compute_units_(): Estesyscallregistra el número de unidades de cómputo restantes en el punto donde se ejecuta. No toma argumentos.
Cuatro de estos syscalls esperan argumentos. El programa carga esos argumentos en los registros antes de invocar el syscall y pasar el control al entorno de ejecución. sol_log_compute_units_ no toma argumentos, ya que solo consulta el estado interno del entorno de ejecución.
Al igual que los opcodes en la EVM, cada syscall tiene un coste de cómputo documentado en el código fuente del cliente. Mientras que syscalls como sol_log_64_ tienen costes de unidades de cómputo fijos, syscalls como sol_log_ que procesan datos de longitud variable tienen costes que dependen del tamaño de sus entradas. Los desarrolladores pueden usar el syscall sol_log_compute_units_ para medir la cantidad de unidades de cómputo consumidas.
A continuación, configuraremos nuestro entorno para experimentar con la carga de datos de la memoria a los registros y su registro con syscalls.
Configuración
Asegúrate de que solana-test-validator esté en ejecución. Completa estos pasos de configuración:
- Crea una carpeta llamada
syscalls - Crea un archivo
syscalls/syscalls.asmpara el código ensamblador - Crea un archivo
syscalls/instructions.jsonpara los datos de la transacción y añade el JSON a continuación. En nuestras demostraciones, no necesitaremos cuentas. Nuestro enfoque estará en elinstruction_data. Actualizaremos el contenido deinstruction_dataa lo largo del camino:
{
"accounts": [],
"program_id": "HTpqQdG7f44su3QsV3HHurraR1ZNjHAdArCy3qHKyKBC",
"instruction_data": []
}
Usaremos el mismo comando de los tutoriales anteriores y modificaremos solo las rutas de los archivos para nuestro directorio de syscalls.
agave-ledger-tool program run syscalls/syscalls.asm --limit 200000 --trace syscalls/trace.txt --ledger test-ledger --input syscalls/instructions.json
Ahora que nuestra configuración está completa, vamos a mostrar cómo ejecutar syscalls en ensamblador sBPF.
Cómo ejecutar syscalls en ensamblador sBPF
Un syscall en ensamblador sBPF sigue la plantilla a continuación. Debes cargar los valores de los argumentos en los registros antes de invocar un syscall.
... ; instructions that copy arguments into registers
syscall <syscall_name>
Usaremos esta plantilla en nuestro código ensamblador a lo largo de este artículo.
En la siguiente sección, llamaremos al syscall sol_log_ usando ensamblador sBPF.
Logging de cadenas de texto con sol_log_ 1/5
El syscall sol_log_ toma un puntero a una cadena de mensaje en memoria como primer argumento y la longitud del mensaje como segundo argumento, y luego registra el mensaje como texto UTF-8 en el log del programa. La definición en Rust del syscall sol_log_ es el siguiente código:
fn sol_log_(message: *const u8, len: u64)
Demostremos cómo registrar el mensaje “Hello world” en ensamblador sBPF usando sol_log_ en 3 pasos:
- Pasaremos la representación en bytes decimales ASCII de “Hello world” como datos de instrucción al programa.
- El entorno de ejecución copiará los datos de instrucción en la región de memoria de entrada, y nuestro código ensamblador lo leerá usando
r1como puntero base ya que las entradas se cargan enr1al inicio del programa. - Luego, registraremos el mensaje “Hello world”.
Primero, actualiza el instruction_data en el archivo instructions.json con la representación en bytes decimales ASCII de “Hello world”:
{
"accounts": [],
"program_id": "HTpqQdG7f44su3QsV3HHurraR1ZNjHAdArCy3qHKyKBC",
"instruction_data": [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
}
Ten en cuenta que el syscall sol_log_ espera que r1 contenga un puntero a la cadena de mensaje en la memoria y que r2 contenga su longitud (la cadena “Hello world” tiene 11 bytes de longitud).
A continuación se muestra un programa que imprimirá un mensaje “Hello world” usando el syscall sol_log_. Cópialo en el archivo syscalls/syscalls.asm.
mov64 r3, r1 ; Copy r1 (0x400000000) to r3 to preserve base pointer
add64 r1, 16 ; r1 now points to first byte of "Hello world" (0x400000010)
ldxdw r2, [r3+8] ; Load instruction data length (11) from memory into r2
syscall sol_log_ ; Invoke syscall: r1=message pointer, r2=length
exit
Expliquemos cada instrucción en detalle:
Línea 1/4, primera instrucción: mov64 r3, r1
Esta instrucción copia la dirección de inicio de la región de memoria de entrada (0x400000000) desde r1 hacia r3 para preservarla. Preservamos este valor en r3 porque lo necesitamos para leer la longitud de los datos de instrucción más adelante.
Línea 2/4, segunda instrucción: add64 r1, 16
El propósito de esta instrucción es almacenar el puntero a los datos de instrucción en r1, que es lo que espera el syscall sol_log_.
Esta instrucción avanza r1 más allá del conteo de cuentas y la longitud de los datos de instrucción en la memoria para que apunte al primer byte de los datos de instrucción, que en este ejemplo es el inicio de la cadena “Hello world”.
El campo del conteo de cuentas siempre está presente en memoria, incluso cuando no hay cuentas en la instrucción:
- Cuando existen cuentas, al conteo de cuentas le siguen los metadatos de las cuentas, las claves públicas y los datos de las cuentas.
- Cuando no hay cuentas, el conteo de cuentas es seguido inmediatamente por la longitud de los datos de instrucción.
En nuestro caso sin cuentas, los primeros 16 bytes contienen solo el conteo de cuentas (8 bytes) y la longitud de los datos de instrucción (8 bytes).

Por lo tanto, r1 apunta al primer byte de los datos de instrucción en la memoria y se convierte en 0x400000000 + 16 (16 es 0x10 en hexadecimal) = 0x400000010. Dado que ya conocemos la ubicación de memoria de destino, también es posible cargar el puntero del mensaje “Hello world” en r1 usando esta instrucción lddw r1, 0x400000010. Usamos la instrucción add64 porque es más idiomático navegar por estructuras de memoria dinámica de forma relativa.
Línea 3/4, tercera instrucción: ldxdw r2, [r3+8]
El propósito de esta instrucción es cargar la longitud de los datos de instrucción en r2, que es lo que espera el syscall sol_log_ como su segundo argumento.
Recuerda, preservamos el puntero base en r3 en la primera instrucción específicamente con este propósito.
La instrucción ldxdw carga un valor de 8 bytes de la memoria. El campo de longitud de los datos de instrucción está ubicado 8 bytes después del puntero base, por lo que su dirección es 0x400000000 + 8 = 0x400000008. Por lo tanto, ldxdw r2, [r3+8] carga el valor de 8 bytes almacenado en 0x400000008 en r2.
En nuestro ejemplo, esto carga el valor 11 (la longitud de “Hello world”) en r2.
Línea 4/4, cuarta instrucción: syscall sol_log_
Esto invoca el syscall sol_log_ pasándole r1 y r2 como primer y segundo argumento, respectivamente.
Ahora que entendemos cómo funciona el programa, ejecuta el programa con el agave-ledger-tool. El resultado debería registrar el mensaje “Hello world”.

Si estás escribiendo programas de Solana en Rust nativo o Anchor, normalmente usarás la macro msg! en lugar de llamar directamente a sol_log_. La macro msg! de Solana es un envoltorio (wrapper) alrededor del syscall sol_log_.
macro_rules! msg {
($msg:expr) => {
$crate::sol_log($msg)
};
($($arg:tt)*) => ($crate::sol_log(&format!($($arg)*)));
}
Además, puedes llamar directamente al syscall sol_log en tu programa de Rust en Anchor importándolo directamente desde el crate solana_program como se muestra a continuación:
use anchor_lang::solana_program::log::sol_log;
sol_log("Hello world");
Logging de datos binarios con sol_log_data 2/5
Este syscall es similar al syscall sol_log_. La diferencia es que registra datos binarios codificados en base64 en el log del programa en lugar de texto UTF-8.
Esto es lo que sucede cuando se invoca el syscall sol_log_data:
- El entorno de ejecución lee un arreglo de descriptores de porciones (slice descriptors) de la memoria. Un descriptor de porción es un registro en memoria que le indica al entorno de ejecución dónde comienza un búfer de bytes y cuántos bytes contiene.
- El entorno de ejecución usa el puntero almacenado en cada descriptor para ubicar el búfer de bytes en la memoria,
- Luego lee y registra el número de bytes especificados por el campo de longitud del descriptor como una salida codificada en base64.
Cada descriptor se almacena como 16 bytes en la memoria: un puntero de 8 bytes hacia los datos y un campo de longitud de 8 bytes.

Definición de la función sol_log_data y requisitos de registros
Aquí está la firma de sol_log_data:
fn sol_log_data(data: *const u8, data_len: u64)
El parámetro data es un *const u8 que el entorno de ejecución trata como un puntero a un arreglo de descriptores de porciones de 16 bytes (puntero, longitud). El parámetro data_len especifica cuántos descriptores deben leerse.
El syscall sol_log_data espera que:
r1contenga una dirección de memoria que apunte a un arreglo de descriptores de porcionesr2contenga el número de porciones en el arreglo
Logging de “Hello” como datos binarios
Demostremos cómo registrar la cadena “Hello” como datos binarios usando sol_log_data.
Primero, actualiza el instruction_data en el archivo instructions.json con la representación en bytes decimales ASCII de “Hello”:
{
"accounts": [],
"program_id": "HTpqQdG7f44su3QsV3HHurraR1ZNjHAdArCy3qHKyKBC",
"instruction_data": [72, 101, 108, 108, 111]
}
Para registrar “Hello” con sol_log_data, necesitamos un descriptor de porción en la pila (stack). Primero extraeremos el puntero a ‘Hello’ y su longitud de la memoria de entrada, almacenaremos ambos valores en la pila para formar el descriptor, y luego cargaremos la dirección del descriptor en r1 y el conteo de porciones en r2 como se muestra en el código a continuación. Copia el código en el archivo syscalls/syscalls.asm, prestando atención a los comentarios:
ldxdw r2, [r1+8] ; Load instruction data length from memory
add r1, 16 ; Advance r1 to instruction data start
stxdw [r10-16], r1 ; Store pointer on stack (descriptor field 1)
stxdw [r10-8], r2 ; Store length on stack (descriptor field 2)
mov r1, r10 ; Copy stack pointer to r1
add r1, -16 ; Adjust r1 to descriptor address
mov r2, 1 ; Load immediate value 1 (slice count)
syscall sol_log_data ; invoking the sol_log_data syscall
exit
Ejecuta el programa con agave-ledger-tool:

La salida muestra SGVsbG8=, que es la codificación en base64 de “Hello”. Esto demuestra que sol_log_data registró correctamente nuestros datos binarios.
Así es como puedes usar el syscall sol_log_data en Anchor.
use anchor_lang::solana_program::log::sol_log_data;
let a = b"hello";
let b = b"world";
sol_log_data(&[a, b]);
Logging de enteros con sol_log_64_ 3/5
El syscall sol_log_64_ registra cinco valores de 64 bits durante la ejecución del programa. No es necesario pasar los cinco argumentos al syscall sol_log_64_. Si no cargas un valor en un registro de argumento, el syscall sol_log_64_ registra cualquier valor que ya esté en ese registro (el cual podría ser 0 o datos sobrantes). Para ser explícito, establece los registros no utilizados en 0. La firma de la función sol_log_64 en Rust se ve así:
fn sol_log_64_(arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64)
En ensamblador sBPF, cargaremos cada uno de los argumentos en los registros de argumentos del r1 al r5. Los syscalls solo obtienen sus argumentos de los registros de argumentos.
Ten en cuenta que sol_log_64_ imprime los valores en formato hexadecimal.
Aquí tienes un ejemplo de cómo pasaremos 5 valores u64 al syscall sol_log_64_:
mov64 r1, 1
mov64 r2, 2
mov64 r3, 3
mov64 r4, 4
mov64 r5, 5
syscall sol_log_64_
exit
Para registrar menos valores, establece explícitamente los registros no utilizados en 0:
mov64 r1, 1
mov64 r2, 0
mov64 r3, 0
mov64 r4, 0
mov64 r5, 0
syscall sol_log_64_
exit
Para demostrar cómo cargar un único valor de 64 bits de la memoria hacia un registro y registrarlo, actualizaremos el campo instruction_data de nuestro archivo en ejecución instructions.json con una representación de 8 bytes del número 5 como ejemplo:
{
"accounts": [],
"program_id": "HTpqQdG7f44su3QsV3HHurraR1ZNjHAdArCy3qHKyKBC",
"instruction_data": [5, 0, 0, 0, 0, 0, 0, 0]
}
Luego podemos cargar los datos de instrucción de la memoria avanzando r1 más allá del conteo de cuentas (8 bytes) y la longitud de los datos de instrucción (8 bytes) como hicimos anteriormente con el syscall sol_log_.
ldxdw r1, [r1 + 16]
syscall sol_log_64_
exit
Cuando ejecutemos este código, obtendremos el siguiente resultado:

A diferencia de sol_log_, Anchor no reexporta sol_log_64_ desde el crate solana_program.
Logging de claves públicas con sol_log_pubkey 4/5
El syscall sol_log_pubkey registra una clave pública de Solana durante la ejecución del programa. Toma un único argumento: un puntero a una clave pública de 32 bytes en la memoria.
A diferencia de los otros syscalls de logging, sol_log_pubkey no toma un argumento de longitud. El entorno de ejecución lee exactamente 32 bytes comenzando desde la dirección proporcionada en r1 e interpreta esos bytes como una clave pública.
Para demostrar cómo funciona esto en ensamblador sBPF, registraremos el program_id de nuestro archivo de entrada JSON en ejecución porque es una clave pública. Actualiza el archivo instructions.json de modo que solo nos quede el program_id. Esta entrada de instrucción se cargará en la memoria cuando ejecutemos el programa:
{
"accounts": [],
"program_id": "HTpqQdG7f44su3QsV3HHurraR1ZNjHAdArCy3qHKyKBC",
"instruction_data": []
}
Luego copia el código a continuación en syscalls/syscalls.asm:
add64 r1, 16
syscall sol_log_pubkey
exit
Como ya sabemos, a la entrada del programa, r1 apunta al inicio de la región de memoria de entrada. En el código anterior, sumamos 16 a r1 para omitir:
- 8 bytes del conteo de cuentas
- 8 bytes de la longitud de los datos de instrucción (el campo de longitud de los datos de instrucción siempre está presente en el diseño de la memoria, independientemente de si la instrucción contiene bytes reales de datos de instrucción, de forma similar al campo del conteo de cuentas)
Dado que no hay datos de instrucción, el siguiente elemento en el diseño de la memoria después de la longitud de los datos de instrucción es el program_id. r1 contiene un puntero al program_id en la memoria.
Ejecuta el programa. Tu salida debería parecerse a la captura de pantalla a continuación. La clave pública del program_id debería coincidir con la clave pública del programa en nuestra entrada.

Seguimiento del uso de cómputo con sol_log_compute_units_ 5/5
El syscall sol_log_compute_units_ registra el número de unidades de cómputo restantes en el punto exacto donde se ejecuta. No toma argumentos, a diferencia de los otros syscalls que discutimos. Cuando se ejecuta el syscall sol_log_compute_units_, el entorno de ejecución consulta el presupuesto de cómputo actual e imprime el saldo restante en el log del programa.
Todos los costes de cómputo son aplicados por el entorno de ejecución. Algunos syscalls cuestan una cantidad fija de unidades de cómputo (por ejemplo, el syscall sol_log_64 cuesta 100 CU), mientras que otros cuestan una cantidad que depende de sus entradas; un ejemplo es el syscall sol_log_, ya que acepta cadenas con longitudes variables.
El syscall sol_log_compute_units_ es una herramienta de logging de medición que te ayuda a observar los costes de cómputo durante el desarrollo.
Logging manual de las unidades de cómputo utilizadas durante la ejecución del programa
Para registrar el coste de cómputo de una transacción específica, necesitaremos calcular el coste de cada uno de los opcodes involucrados en la transacción. Usemos el syscall sol_log_64_ como ejemplo:
ldxdw r1, [r1 + 16]
syscall sol_log_64_
exit
Primero necesitamos determinar el coste de cada uno de los opcodes; aunque las unidades de cómputo de todos los opcodes en ensamblador no están documentadas, seremos capaces de determinarlas en breve. Los opcodes syscall, ldxdw, exit cuestan 1 unidad de cómputo cada uno, mientras que sol_log_64 cuesta 100 CU (definido en este struct). Entonces, para calcular el coste en unidades de cómputo de esta transacción, sumaremos este coste, que será 1 + 1 + 1 + 100 = 103 unidades de cómputo.
En un programa real con cientos o miles de opcodes, calcular el coste total de cómputo manualmente sería poco práctico. Ahí es donde entra sol_log_compute_units_. Podemos usar sol_log_compute_units_ para registrar cuántas unidades de cómputo quedan después de una ejecución.
Logging de las unidades de cómputo utilizadas durante la ejecución del programa con sol_log_compute_units_
Ya que sabemos que el límite máximo de unidades de cómputo es 1,400,000 y el syscall sol_log_compute_units_ devuelve las unidades de cómputo restantes. Cuando ejecutamos nuestro programa usando el syscall sol_log_compute_units_, restamos las unidades de cómputo restantes de las unidades de cómputo máximas (1,400,000). La diferencia equivale a las unidades de cómputo consumidas por nuestro programa.
Ten en cuenta que el syscall sol_log_compute_units_ en sí mismo consume 100 unidades de cómputo, por lo que debemos tener esto en cuenta en nuestro cálculo.
Demostremos cómo podemos hacer esto usando ensamblador. Copia el código a continuación en tu archivo syscalls/syscalls.asm.
ldxdw r1, [r1 + 16]
syscall sol_log_64_
syscall sol_log_compute_units_
exit
Cuando ejecutemos el programa anterior con la herramienta agave-ledger-tool, obtendremos el siguiente resultado:

En la captura de pantalla, vemos 1,399,797 unidades de cómputo restantes.
Total consumido: 1,400,000 - 1,399,797 = 203 CU
Pero este 203 incluye el coste de sol_log_compute_units_ en sí mismo (100 CU) y su instrucción syscall (1 CU), los cuales debemos excluir. Tampoco incluye la instrucción exit (1 CU) que se ejecuta después del log.
Coste real del programa = 203 - 100 (log syscall) - 1 (instrucción syscall) + 1 (exit) = 103 CU
Esto coincide con nuestro cálculo manual anterior.
También puedes usar este syscall directamente en tus programas de Solana importándolo del crate solana_program:
solana_program::log::sol_log_compute_units()
Para aprender más sobre la optimización de unidades de cómputo, consulta nuestro tutorial anterior sobre Optimización de Unidades de Cómputo (Compute Unit Optimization).
Aquí tienes una tabla que resume los cinco syscalls de logging que hemos discutido:
| Syscall | Argumentos | Qué registra | Coste de cómputo |
|---|---|---|---|
sol_log_ |
r1: puntero a la cadena UTF-8 en memoria, r2: longitud en bytes |
Texto UTF-8 como cadena legible | Variable (depende de la longitud de la cadena) |
sol_log_data |
r1: puntero a un arreglo de descriptores de porciones, r2: número de porciones |
Datos binarios codificados en base64 | Variable (depende de los datos) |
sol_log_64_ |
r1 a r5: cinco enteros de 64 bits |
Hasta cinco enteros en formato hexadecimal | 100 CU (fijo) |
sol_log_pubkey |
r1: puntero a una clave pública de 32 bytes en memoria |
Clave pública en formato base58 estándar de Solana | 100 CU (fijo) |
sol_log_compute_units_ |
Ninguno | Unidades de cómputo restantes en el punto de ejecución | 100 CU (fijo) |
Este artículo es parte de una serie de tutoriales sobre desarrollo en Solana