En este artículo, exploramos varios conceptos de bajo nivel (compilación y diseño, tiempos de ejecución primitivos, ensamblador y más) a través del prisma de la arquitectura RISC-V y su ecosistema. Yo mismo soy desarrollador web, no hago nada en el trabajo, pero es muy interesante para mí, ¡de aquí es de donde vino el artículo! Únete a mí en este viaje agitado a las profundidades del caos de bajo nivel.
Primero, hablemos un poco sobre RISC-V y la importancia de esta arquitectura, configure la cadena de herramientas RISC-V y ejecute un programa simple C en hardware RISC-V emulado.
Contenido
- ¿Qué es RISC-V?
- Configuración de herramientas QEMU y RISC-V
- Hola RISC-V!
- Enfoque ingenuo
- Levantando la cortina -v
- Busca en nuestra pila
- Diseño
- Basta!
Hammertime! Tiempo de ejecución!
- Depurar pero ahora de verdad
- Que sigue
- Opcional
¿Qué es RISC-V?
RISC-V es una arquitectura de conjunto de instrucciones gratuitas. El proyecto se originó en la Universidad de California en Berkeley en 2010. La apertura del código y la libertad de uso desempeñaron un papel importante en su éxito, que era muy diferente de muchas otras arquitecturas. Tome ARM: para crear un procesador compatible, debe pagar una tarifa por adelantado
de $ 1 millón a $ 10 millones, y también pagar regalías de 0.5 a 2% sobre las ventas . Un modelo gratuito y abierto hace que RISC-V sea una opción atractiva para muchos, incluso para las nuevas empresas que no pueden pagar una licencia para un ARM u otro procesador, para investigadores académicos y (obviamente) para la comunidad de código abierto.
El rápido crecimiento en popularidad de RISC-V no pasó desapercibido. ARM
lanzó un sitio que intentó (sin éxito) destacar los supuestos beneficios de ARM sobre RISC-V (el sitio ya está cerrado). El proyecto RISC-V cuenta con el respaldo de
muchas grandes empresas , incluidas Google, Nvidia y Western Digital.
Configuración de herramientas QEMU y RISC-V
No podemos ejecutar el código en el procesador RISC-V hasta que configuremos el entorno. Afortunadamente, esto no requiere un procesador físico RISC-V; en cambio, tomamos
qemu . Siga las
instrucciones para instalar
su sistema operativo . Tengo MacOS, así que solo ingrese un comando:
Convenientemente,
qemu
viene con
varias máquinas listas para qemu-system-riscv32 -machine
(vea la
qemu-system-riscv32 -machine
).
A continuación, instale
OpenOCD para las
herramientas RISC-V y RISC-V.
Descargue los ensamblajes ya preparados de las herramientas RISC-V OpenOCD y RISC-V
aquí .
Extraemos los archivos a cualquier directorio, lo tengo
~/usys/riscv
. Recuérdelo para uso futuro.
mkdir -p ~/usys/riscv cd ~/Downloads cp openocd-<date>-<platform>.tar.gz ~/usys/riscv cp riscv64-unknown-elf-gcc-<date>-<platform>.tar.gz ~/usys/riscv cd ~/usys/riscv tar -xvf openocd-<date>-<platform>.tar.gz tar -xvf riscv64-unknown-elf-gcc-<date>-<platform>.tar.gz
Establezca las variables de entorno
RISCV_OPENOCD_PATH
y
RISCV_PATH
para que otros programas puedan encontrar nuestra cadena de herramientas. Esto puede verse diferente según el sistema operativo y el shell: agregué las rutas al
~/.zshenv
.
Cree un enlace simbólico en
/usr/local/bin
para este archivo ejecutable para que pueda ejecutarlo en cualquier momento sin especificar la ruta completa a
~/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>/bin/riscv64-unknown-elf-gcc
.
¡Y listo, tenemos un kit de herramientas RISC-V en funcionamiento! Todos nuestros ejecutables, como
riscv64-unknown-elf-gcc
,
riscv64-unknown-elf-gdb
,
riscv64-unknown-elf-ld
y otros, están en
~/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>/bin/
.
Hola RISC-V!
Parche del 26 de mayo de 2019:
Desafortunadamente, debido a un error en RISC-V QEMU, el programa freedom-e-sdk 'hello world' en QEMU ya no funciona. Se ha lanzado un parche para resolver este problema, pero por ahora, omita esta sección. Este programa no será necesario en las secciones posteriores del artículo. Rastreo la situación y actualizo el artículo después de corregir el error.
Vea este comentario para más información.Con las herramientas configuradas, ejecutemos el sencillo programa RISC-V. Comencemos clonando el repositorio SiFive
freedom-e-sdk :
cd ~/wherever/you/want/to/clone/this git clone --recursive https://github.com/sifive/freedom-e-sdk.git cd freedom-e-sdk
Por tradición , comencemos con el programa 'Hola, mundo' del repositorio
freedom-e-sdk
. Utilizamos el
Makefile
listo para usar que proporcionan para compilar este programa en modo de depuración:
make PROGRAM=hello TARGET=sifive-hifive1 CONFIGURATION=debug software
Y ejecutar en QEMU:
qemu-system-riscv32 -nographic -machine sifive_e -kernel software/hello/debug/hello.elf Hello, World!
Este es un gran comienzo. Puede ejecutar otros ejemplos desde
freedom-e-sdk
. Después de eso, escribiremos e intentaremos depurar nuestro propio programa en C.
Enfoque ingenuo
Comencemos con un programa simple que agrega infinitamente dos números.
cat add.c int main() { int a = 4; int b = 12; while (1) { int c = a + b; } return 0; }
Queremos ejecutar este programa, y lo primero que necesitamos para compilarlo para el procesador RISC-V.
Esto crea el archivo
a.out
, que por defecto
gcc
archivos ejecutables. Ahora ejecute este archivo en
qemu
:
Elegimos la máquina
virt
que
originalmente riscv-qemu
.
Ahora que nuestro programa se ejecuta dentro de QEMU con el servidor GDB en
localhost:1234
, nos conectamos con el cliente RISC-V GDB desde un terminal separado:
¡Y estamos dentro de GDB!
Este GDB se configuró como "--host = x86_64-apple-darwin17.7.0 --target = riscv64-unknown-elf". │
Escriba "show configuration" para obtener detalles de la configuración. │
Para obtener instrucciones de informe de errores, consulte: │
<http://www.gnu.org/software/gdb/bugs/>. │
Encuentre el manual de GDB y otros recursos de documentación en línea en: │
<http://www.gnu.org/software/gdb/documentation/>. │
│
Para obtener ayuda, escriba "ayuda". │
Escriba "apropos word" para buscar comandos relacionados con "word" ... │
Lectura de símbolos de a.out ... │
(gdb)
Podemos intentar ejecutar los comandos de
run
o
start
para el archivo ejecutable
a.out
en GDB, pero por el momento esto no funcionará por una razón obvia.
riscv64-unknown-elf-gcc
el programa como
riscv64-unknown-elf-gcc
, por lo que el host debería ejecutarse en la arquitectura
riscv64
.
¡Pero hay una salida! Esta situación es una de las principales razones de la existencia del modelo cliente-servidor de GDB. Podemos tomar el archivo ejecutable
riscv64-unknown-elf-gdb
y, en lugar de iniciarlo en el host, especifíquelo en algún destino remoto (servidor GDB). Como recordará, acabamos de comenzar
riscv-qemu
y nos dijo que
riscv-qemu
el servidor GDB en
localhost:1234
. Simplemente conéctese a este servidor:
(gdb) control remoto de destino: 1234 │
Depuración remota utilizando: 1234
Ahora puede establecer algunos puntos de interrupción:
(gdb) b main Breakpoint 1 at 0x1018e: file add.c, line 2. (gdb) b 5
Y finalmente, especifique GDB
continue
(comando abreviado
c
) hasta llegar al punto de interrupción:
(gdb) c Continuing.
Notará rápidamente que el proceso no termina de ninguna manera. Esto es extraño ... ¿no deberíamos llegar inmediatamente al punto de interrupción
b 5
? Que paso

Aquí puedes ver varios problemas:
- La interfaz de usuario de texto no puede encontrar la fuente. La interfaz debe mostrar nuestro código y cualquier punto de interrupción cercano.
- GDB no ve la línea de ejecución actual (
L??
) y muestra el contador 0x0 ( PC: 0x0
).
- Algún texto en la línea de entrada, que en su totalidad se ve así:
0x0000000000000000 in ?? ()
0x0000000000000000 in ?? ()
Combinados con el hecho de que no podemos alcanzar el punto de ruptura, estos indicadores indican: hicimos
algo mal. Pero que?
Levantando la cortina -v
Para comprender lo que está sucediendo, debe dar un paso atrás y hablar sobre cómo funciona realmente nuestro simple programa C debajo del capó. La función
main
hace una simple adición, pero ¿qué es realmente? ¿Por qué debería llamarse
main
, no
origin
o
begin
? Según la convención, todos los archivos ejecutables comienzan a ejecutarse con la función
main
, pero ¿qué magia proporciona este comportamiento?
Para responder a estas preguntas, repitamos nuestro equipo de GCC con el indicador
-v
para obtener una salida más detallada de lo que realmente está sucediendo.
riscv64-unknown-elf-gcc add.c -O0 -g -v
El resultado es grande, por lo que no veremos la lista completa. Es importante tener en cuenta que, aunque GCC es formalmente un compilador, también se predetermina a la compilación (para limitarse a la compilación y el ensamblaje, debe especificar el indicador
-c
). ¿Por qué es esto importante? Bueno, eche un vistazo al fragmento de la salida detallada de
gcc
:
# El comando real `gcc -v` genera rutas completas, pero esas son bastante
# largo, así que imagina que estas variables existen.
# $ RV_GCC_BIN_PATH = / Users / twilcock / usys / riscv / riscv64-unknown-elf-gcc- <date> - <version> / bin /
# $ RV_GCC_LIB_PATH = $ RV_GCC_BIN_PATH /../ lib / gcc / riscv64-unknown-elf / 8.2.0
$ RV_GCC_BIN_PATH /../ libexec / gcc / riscv64-unknown-elf / 8.2.0 / collect2 \
... truncado ...
$ RV_GCC_LIB_PATH /../../../../ riscv64-unknown-elf / lib / rv64imafdc / lp64d / crt0.o \
$ RV_GCC_LIB_PATH / riscv64-unknown-elf / 8.2.0 / rv64imafdc / lp64d / crtbegin.o \
-lgcc --start-group -lc -lgloss --end-group -lgcc \
$ RV_GCC_LIB_PATH / rv64imafdc / lp64d / crtend.o
... truncado ...
COLLECT_GCC_OPTIONS = '- O0' '-g' '-v' '-march = rv64imafdc' '-mabi = lp64d'
Entiendo que incluso en forma abreviada esto es mucho, así que déjenme explicarlo. En la primera línea,
gcc
ejecuta el programa
collect2
, pasa los argumentos
crt0.o
,
crtbegin.o
y
crtend.o
, los
-lgcc
y
--start-group
. La descripción de collect2 se puede encontrar
aquí : en resumen, collect2 organiza varias funciones de inicialización en el inicio, haciendo el diseño en una o más pasadas.
Por lo tanto, GCC compila varios archivos
crt
con nuestro código. Como puedes adivinar,
crt
significa 'C tiempo de ejecución'.
Aquí se describe en detalle para qué
crt
destinado cada
crt
, pero estamos interesados en
crt0
, que hace una cosa importante:
"Se espera que este objeto [crt0] contenga el carácter _start
, que indica el arranque del programa".
La esencia de la "rutina de carga" depende de la plataforma, pero generalmente involucra tareas importantes como configurar un marco de pila, pasar argumentos de línea de comando y llamar a
main
. Sí,
finalmente encontramos la respuesta a la pregunta: ¡es
_start
llama a nuestra función principal!
Busca en nuestra pila
Resolvimos un acertijo, pero ¿cómo nos acerca esto al objetivo original: ejecutar un programa C simple en
gdb
? Queda por resolver varios problemas: el primero de ellos está relacionado con cómo
crt0
configura nuestra pila.
Como vimos anteriormente, el valor predeterminado de
gcc
crt0
. Los parámetros predeterminados se seleccionan en función de varios factores:
- Triplete de destino correspondiente a la estructura del
machine-vendor-operatingsystem
. Lo tenemos riscv64-unknown-elf
- Arquitectura de destino,
rv64imafdc
- Target ABI,
lp64d
Por lo general, todo funciona bien, pero no para todos los procesadores RISC-V. Como se mencionó anteriormente, una de las tareas de
crt0
es configurar la pila. ¿Pero él no sabe exactamente dónde debería estar la pila para nuestra CPU (
-machine
)? No puede hacerlo sin nuestra ayuda.
En el
qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -kernel a.out
utilizamos la máquina
virt
. Afortunadamente,
qemu
facilita volcar la información de la máquina en un volcado
dtb
(blob de árbol de dispositivos).
Los datos Dtb son difíciles de leer porque es básicamente un formato binario, pero hay una utilidad de línea de comandos
dtc
(compilador del árbol de dispositivos) que puede convertir el archivo en algo más legible.
El archivo de salida es
riscv64-virt.dts
, donde vemos mucha información interesante sobre
virt
: la cantidad de núcleos de procesador disponibles, la ubicación de la memoria de varios dispositivos periféricos, como UART, la ubicación de la memoria interna (RAM). La pila debería estar en esta memoria, así que
grep
con
grep
:
grep memory riscv64-virt.dts -A 3 memory@80000000 { device_type = "memory"; reg = <0x00 0x80000000 0x00 0x8000000>; };
Como puede ver, este nodo tiene 'memoria' especificada como
device_type
. Aparentemente, encontramos lo que estábamos buscando. Por los valores dentro de
reg = <...> ;
Puede determinar dónde comienza el banco de memoria y cuál es su longitud.
En
la especificación del dispositivo, vemos que la sintaxis
reg
es un número arbitrario de pares
(base_address, length)
. Sin embargo, hay cuatro significados dentro del
reg
. Extraño, ¿no son dos valores suficientes para un banco de memoria?
Una vez más, a partir de la especificación del dispositivo (búsqueda de la propiedad
reg
) descubrimos que el número de
<u32>
para especificar la dirección y la longitud está determinado por las propiedades
#size-cells
#address-cells
y
#size-cells
en el nodo primario (o en el propio nodo). Estos valores no se especifican en nuestro nodo de memoria, y el nodo de memoria principal es simplemente la raíz del archivo. Veamos en él estos valores:
head -n8 riscv64-virt.dts /dts-v1/; / { #address-cells = <0x02>; #size-cells = <0x02>; compatible = "riscv-virtio"; model = "riscv-virtio,qemu";
Resulta que tanto la dirección como la longitud requieren dos valores de 32 bits. Esto significa que con
reg = <0x00 0x80000000 0x00 0x8000000>;
nuestra memoria comienza
0x00 + 0x80000000 (0x80000000)
y ocupa
0x00 + 0x8000000 (0x8000000)
bytes, es decir, termina en
0x88000000
, que corresponde a 128 megabytes.
Diseño
Usando
qemu
y
dtc
encontramos las direcciones RAM en la máquina virtual virt. También sabemos que
gcc
compone
crt0
por defecto, sin configurar la pila como necesitamos. Pero, ¿cómo usar esta información para eventualmente ejecutar y depurar el programa?
Como
crt0
no nos conviene, hay una opción obvia: escribir su propio código y luego componerlo con el archivo de objeto que obtuvimos después de compilar nuestro programa simple. Nuestro
crt0
necesita saber dónde comienza la parte superior de la pila para inicializarla correctamente. Podríamos
crt0
valor
0x80000000
directamente a
crt0
, pero esta no es una solución muy adecuada, teniendo en cuenta los cambios que puedan ser necesarios en el futuro. ¿Qué pasa si queremos usar otra CPU, como
sifive_e
, con diferentes características en el emulador?
Afortunadamente, no somos los primeros en hacer esta pregunta, y ya existe una buena solución. El enlazador GNU
ld
permite definir el carácter disponible en nuestro
crt0
. Podemos definir el símbolo
__stack_top
adecuado para diferentes procesadores.
En lugar de escribir su propio archivo de enlace desde cero, tiene sentido tomar el script predeterminado con
ld
y modificarlo un poco para admitir caracteres adicionales. ¿Qué es un script vinculador?
Aquí hay una buena descripción :
El propósito principal de la secuencia de comandos del vinculador es describir cómo las secciones del archivo se combinan en la entrada y la salida, y controlar el diseño de la memoria del archivo de salida.
Sabiendo esto,
riscv64-unknown-elf-ld
script de enlace predeterminado
riscv64-unknown-elf-ld
a un nuevo archivo:
cd ~/usys/riscv
Este archivo tiene
mucha información interesante, mucho más de lo que podemos discutir en este artículo. La salida detallada con la
--Verbose
incluye información sobre la versión
ld
, arquitecturas compatibles y mucho más. Todo esto es bueno saberlo, pero dicha sintaxis es inaceptable en el script del enlazador, así que abra un editor de texto y elimine todo lo superfluo del archivo.
vim riscv64-virt.ld
# Eliminar todo lo anterior e incluir la línea =============
GNU ld (GNU Binutils) 2.32
Emulaciones compatibles:
elf64lriscv
elf32lriscv
usando un script de enlazador interno:
===================================================
/ * Script para -z combreloc: combina y ordena las secciones de reubicación * /
/ * Copyright (C) 2014-2019 Free Software Foundation, Inc.
Copia y distribución de este script, con o sin modificación,
están permitidos en cualquier medio sin regalías siempre que los derechos de autor
aviso y este aviso se conservan. * /
OUTPUT_FORMAT ("elf64-littleriscv", "elf64-littleriscv",
"elf64-littleriscv")
... resto del script del enlazador ...
Después de eso, ejecute el comando
MEMORY para determinar manualmente dónde estará
__stack_top
. Localice la línea que comienza con
OUTPUT_ARCH(riscv)
, debe estar en la parte superior del archivo y agregue el comando
MEMORY
debajo:
OUTPUT_ARCH(riscv) /* >>> Our addition. <<< */ MEMORY { /* qemu-system-risc64 virt machine */ RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 128M } /* >>> End of our addition. <<< */ ENTRY(_start)
Creamos un bloque de memoria llamado
RAM
, para el cual se permite leer (
r
), escribir (
w
) y almacenar el código ejecutable (
x
).
Genial, hemos definido un diseño de memoria que coincide con las especificaciones de nuestra máquina
virt
RISC-V. Ahora puedes usarlo. Queremos poner nuestra pila en la memoria.
__stack_top
definir el carácter
__stack_top
. Abra su script de enlace (
riscv64-virt.ld
) en un editor de texto y agregue algunas líneas:
SECTIONS { /* Read-only sections, merged into text segment: */ PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x10000)); . = SEGMENT_START("text-segment", 0x10000) + SIZEOF_HEADERS; /* >>> Our addition. <<< */ PROVIDE(__stack_top = ORIGIN(RAM) + LENGTH(RAM)); /* >>> End of our addition. <<< */ .interp : { *(.interp) } .note.gnu.build-id : { *(.note.gnu.build-id) }
Como puede ver, definimos
__stack_top
usando
el comando __stack_top
. Se podrá acceder al símbolo desde cualquier programa asociado con este script (suponiendo que el programa en sí mismo no determine algo con el nombre
__stack_top
). Establezca
__stack_top
en
ORIGIN(RAM)
. Sabemos que este valor es
0x80000000
más
LENGTH(RAM)
, que es 128 megabytes (
0x8000000
bytes). Esto significa que nuestro
__stack_top
establecido en
0x88000000
.
Por brevedad, no enumeraré el archivo de enlace completo
aquí ; puede verlo
aquí .
Basta! Hammertime! Tiempo de ejecución!
Ahora tenemos todo lo que necesitamos para crear nuestro propio tiempo de ejecución de C. En realidad, esta es una tarea bastante simple, aquí está todo el archivo
crt0.s
:
.section .init, "ax" .global _start _start: .cfi_startproc .cfi_undefined ra .option push .option norelax la gp, __global_pointer$ .option pop la sp, __stack_top add s0, sp, zero jal zero, main .cfi_endproc .end
Inmediatamente atrae una gran cantidad de líneas que comienzan con un punto. Este es un archivo para ensamblador
as
. Las líneas con puntos se denominan
directivas de ensamblador : proporcionan información para el ensamblador. Este no es un código ejecutable, como las instrucciones del ensamblador RISC-V como
jal
y
add
.
Veamos el archivo línea por línea. Trabajaremos con varios registros RISC-V estándar, así que consulte
esta tabla , que cubre todos los registros y su propósito.
.section .init, "ax"
Como se indica en
el manual del ensamblador GNU 'como' , esta línea le dice al ensamblador que inserte el siguiente código en la sección
.init
, que se asigna (
a
) y el ejecutable (
x
). Esta sección es otra
convención común para ejecutar código dentro del sistema operativo. Trabajamos en hardware puro sin un sistema operativo, por lo que en nuestro caso tal instrucción puede no ser absolutamente necesaria, pero en cualquier caso, esta es una buena práctica.
.global _start _start:
.global
pone el siguiente personaje a disposición de
ld
. Sin esto, el enlace no funcionará, porque el
ENTRY(_start)
en el script del enlazador apunta al símbolo
_start
como el punto de entrada al archivo ejecutable. La siguiente línea le dice al ensamblador que estamos comenzando la definición del carácter
_start
.
_start: .cfi_startproc .cfi_undefined ra ...other stuff... .cfi_endproc
Estas directivas
.cfi
informan sobre la estructura del marco y cómo manejarlo. Las
.cfi_endproc
.cfi_startproc
y
.cfi_endproc
señalan el comienzo y el final de la función, y
.cfi_undefined ra
le dice al ensamblador que el registro
ra
no debe restaurarse a ningún valor que contenga antes de que se
_start
.
.option push .option norelax la gp, __global_pointer$ .option pop
Estas directivas
.option
cambian el comportamiento del ensamblador de acuerdo con el código cuando necesita aplicar un conjunto específico de opciones.
Aquí hay una descripción detallada de por qué es importante el uso de
.option
en este segmento:
... dado que posiblemente relajemos el direccionamiento de secuencias a secuencias más cortas en relación con el GP, la carga inicial del GP no debería debilitarse y debería ser algo como esto:
.option push .option norelax la gp, __global_pointer$ .option pop
para que después de relajarte obtengas el siguiente código:
auipc gp, %pcrel_hi(__global_pointer$) addi gp, gp, %pcrel_lo(__global_pointer$)
en lugar de simple:
addi gp, gp, 0
Y ahora la última parte de nuestros
crt0.s
:
_start: ...other stuff... la sp, __stack_top add s0, sp, zero jal zero, main .cfi_endproc .end
Aquí finalmente podemos usar el símbolo
__stack_top
, que trabajamos mucho para crear.
La __stack_top
la
(dirección de carga) carga el valor
__stack_top
en el registro
sp
(puntero de pila), configurándolo para su uso en el resto del programa.
Luego
add s0, sp, zero
agrega los valores de los registros
sp
y
zero
(que en realidad es el registro
x0
con una referencia fija a 0) y coloca el resultado en el registro
s0
. Este es un
registro especial que es inusual en varios aspectos. En primer lugar, es un "registro persistente", es decir, se guarda cuando se llama a la función. En segundo lugar,
s0
veces actúa como un puntero de cuadro, lo que le da a cada función llamar un pequeño espacio en la pila para almacenar los parámetros pasados a esta función. Cómo funcionan las llamadas de función con los punteros de pila y marco es un tema muy interesante que puede dedicar fácilmente a un artículo separado, pero por ahora, solo sepa que en nuestro tiempo de ejecución es importante inicializar el puntero de marco
s0
.
A continuación vemos el
jal zero, main
declaración
jal zero, main
. Aquí
jal
significa salto y enlace. La instrucción espera operandos en forma de
jal rd (destination register), offset_address
. Funcionalmente,
jal
escribe el valor de la siguiente instrucción (registro de
pc
más cuatro) en
rd
, y luego establece el registro de
pc
valor de
pc
actual más la dirección de desplazamiento con
extensión de signo , efectivamente "llamando" a esta dirección.
Como se mencionó anteriormente,
x0
estrechamente vinculado al valor literal 0, y escribir en él es inútil.
Por lo tanto, puede parecer extraño que usemos un registro como registro de destino zero
, que los ensambladores RISC-V interpretan como un registro x0
. Después de todo, esto significa una transición incondicional a offset_address
. ¿Por qué hacer esto, porque en otras arquitecturas hay una instrucción explícita para una transición incondicional?Este patrón extraño jal zero, offset_address
es en realidad una optimización inteligente. El soporte para cada nueva instrucción significa un aumento y, en consecuencia, un aumento en el costo del procesador. Por lo tanto, cuanto más simple sea el ISA, mejor. En lugar de contaminar el espacio de instrucciones con dos instrucciones jal
y unconditional jump
, la arquitectura RISC-V solo es compatible jal
y se admiten saltos incondicionales jal zero, main
.RISC-V tiene muchas de estas optimizaciones, la mayoría de las cuales toman la forma de las llamadas pseudoinstrucciones . Los ensambladores saben cómo traducirlos en instrucciones reales de hardware. Por ejemplo, j offset_address
los ensambladores RISC-V traducen pseudoinstrucciones para saltos incondicionales a jal zero, offset_address
. Para obtener una lista completa de pseudo instrucciones oficialmente compatibles , consulte la especificación RISC-V (versión 2.2) . _start: ...other stuff... jal zero, main .cfi_endproc .end
Nuestra última línea es la directiva del ensamblador .end
, que simplemente marca el final del archivo.Depurar pero ahora de verdad
Intentando depurar un programa C simple en un procesador RISC-V, resolvimos muchos problemas. Primero, usamos qemu
y dtc
encontramos nuestra memoria en la máquina virtual virt
RISC-V. Luego usamos esta información para controlar manualmente la asignación de memoria en nuestra versión del script predeterminado del enlazador riscv64-unknown-elf-ld
, lo que nos permitió determinar con precisión el símbolo __stack_top
. Luego usamos este símbolo en nuestra propia versión crt0.s
, que configura nuestra pila y punteros globales, y finalmente llamamos a la función main
. Ahora puede lograr su objetivo y comenzar a depurar nuestro sencillo programa en GDB.Recordemos aquí está el programa C en sí: cat add.c int main() { int a = 4; int b = 12; while (1) { int c = a + b; } return 0; }
Compilar y vincular: riscv64-unknown-elf-gcc -g -ffreestanding -O0 -Wl,--gc-sections -nostartfiles -nostdlib -nodefaultlibs -Wl,-T,riscv64-virt.ld crt0.s add.c
Aquí indicamos muchas más banderas que la última vez, así que repasemos las que no hemos descrito antes.-ffreestanding
le dice al compilador que la biblioteca estándar puede no existir , por lo que no es necesario hacer suposiciones sobre su disponibilidad obligatoria. Este parámetro no es necesario al iniciar la aplicación en su host (en el sistema operativo), pero en este caso no lo es, por lo tanto, es importante informar al compilador de esta información.-Wl
- Una lista de banderas separadas por comas para pasar al enlazador ( ld
). Aquí, --gc-sections
significa "secciones de recolección de basura", y ld
se le indica que elimine las secciones no utilizadas después del enlace. Flags -nostartfiles
, -nostdlib
y -nodefaultlibs
dígale al enlazador que no procese los archivos de inicio del sistema estándar (por ejemplo, predeterminadocrt0
), implementaciones estándar de stdlib del sistema y bibliotecas vinculadas predeterminadas del sistema estándar. Tenemos nuestro propio script crt0
y enlazador, por lo que es importante pasar estos marcadores para que los valores predeterminados no entren en conflicto con nuestras preferencias de usuario.-T
indica la ruta a nuestro script de enlazador, que es simple en nuestro caso riscv64-virt.ld
. Finalmente, especificamos los archivos que queremos compilar, compilar y componer: crt0.s
y add.c
. Como antes, el resultado es un archivo completo y listo para ejecutarse llamado a.out
.Ahora ejecute nuestro nuevo ejecutable completamente nuevo en qemu
:
Ahora ejecute gdb
, recuerde cargar los símbolos de depuración para a.out
, especificándolo con el último argumento: riscv64-unknown-elf-gdb --tui a.out GNU gdb (GDB) 8.2.90.20190228-git Copyright (C) 2019 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "--host=x86_64-apple-darwin17.7.0 --target=riscv64-unknown-elf". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from a.out... (gdb)
Luego, conecte nuestro cliente gdb
al servidor gdb
que lanzamos como parte del comando qemu
: (gdb) target remote :1234 │ Remote debugging using :1234
Establecer un punto de interrupción en main: (gdb) b main Breakpoint 1 at 0x8000001e: file add.c, line 2.
Y comienza el programa: (gdb) c Continuing. Breakpoint 1, main () at add.c:2
¡De la salida dada está claro que alcanzamos con éxito el punto de interrupción en la línea 2! Esto también es visible en la interfaz de texto, finalmente tenemos la línea correcta L
, el valor PC:
es L2
y PC:
- 0x8000001e
. Si hizo todo como en el artículo, la salida será algo como esto: a
partir de ahora, puede usarlo gdb
como de costumbre: -s
para ir a la siguiente instrucción, info all-registers
para verificar los valores dentro de los registros a medida que se ejecuta el programa, etc. Experimente para su placer ... nosotros, por supuesto ¡Trabajé mucho por esto!Que sigue
¡Hoy hemos logrado mucho y espero haber aprendido mucho! Nunca tuve un plan formal para este y otros artículos, solo seguí lo que era más interesante para mí en todo momento. Por lo tanto, no estoy seguro de lo que sucederá después. Me gustó especialmente la inmersión profunda en las instrucciones jal
, por lo que tal vez en el próximo artículo tomaremos como base el conocimiento adquirido aquí, pero lo reemplazaremos con add.c
algún programa en ensamblador RISC-V puro. Si tiene algo específico que le gustaría ver o tiene alguna pregunta, abra las entradas .Gracias por leer! ¡Espero encontrarme en el próximo artículo!Opcional
Si le gustó el artículo y quiere saber más, consulte la presentación de Matt Godbolt titulada “Bits Between Bits: How We Get into main ()” de la conferencia CppCon2018. Ella aborda el tema un poco diferente de lo que estamos aquí. Muy buena conferencia, ¡compruébalo por ti mismo!