RISC-V desde cero

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


  1. ¿Qué es RISC-V?
  2. Configuración de herramientas QEMU y RISC-V
  3. Hola RISC-V!
  4. Enfoque ingenuo
  5. Levantando la cortina -v
  6. Busca en nuestra pila
  7. Diseño
  8. Basta! Hammertime! Tiempo de ejecución!
  9. Depurar pero ahora de verdad
  10. Que sigue
  11. 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:

# also available via MacPorts - `sudo port install qemu` brew install qemu 

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 .

 # I put these two exports directly in my ~/.zshenv file - you may have to do something else. export RISCV_OPENOCD_PATH="$HOME/usys/riscv/openocd-<date>-<version>" export RISCV_PATH="$HOME/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>" # Reload .zshenv with our new environment variables. Restarting your shell will have a similar effect. source ~/.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 .

 # Symbolically link our gcc executable into /usr/local/bin. Repeat this process for any other executables you want to quickly access. ln -s ~/usys/riscv/riscv64-unknown-elf-gcc-8.2.0-<date>-<version>/bin/riscv64-unknown-elf-gcc /usr/local/bin 

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

 # -O0 to disable all optimizations. Without this, GCC might optimize # away our infinite addition since the result 'c' is never used. # -g to tell GCC to preserve debug info in our executable. riscv64-unknown-elf-gcc add.c -O0 -g 

Esto crea el archivo a.out , que por defecto gcc archivos ejecutables. Ahora ejecute este archivo en qemu :

 # -machine tells QEMU which among our list of available machines we want to # run our executable against. Run qemu-system-riscv64 -machine help to list # all available machines. # -m is the amount of memory to allocate to our virtual machine. # -gdb tcp::1234 tells QEMU to also start a GDB server on localhost:1234 where # TCP is the means of communication. # -kernel tells QEMU what we're looking to run, even if our executable isn't # exactly a "kernel". qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -kernel a.out 

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:

 # --tui gives us a (t)extual (ui) for our GDB session. # While we can start GDB without any arguments, specifying 'a.out' tells GDB # to load debug symbols from that file for the newly created session. riscv64-unknown-elf-gdb --tui a.out 

¡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 # this is the line within the forever-while loop. int c = a + b; Breakpoint 2 at 0x1019a: file add.c, line 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:

  1. 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.
  2. GDB no ve la línea de ejecución actual ( L?? ) y muestra el contador 0x0 ( PC: 0x0 ).
  3. 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).

 # Go to the ~/usys/riscv folder we created before and create a new dir # for our machine information. cd ~/usys/riscv && mkdir machines cd machines # Use qemu to dump info about the 'virt' machine in dtb (device tree blob) # format. # The data in this file represents hardware components of a given # machine / device / board. qemu-system-riscv64 -machine virt -machine dumpdtb=riscv64-virt.dtb 

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.

 # I'm running MacOS, so I use Homebrew to install this. If you're # running another OS you may need to do something else. brew install dtc # Convert our .dtb into a human-readable .dts (device tree source) file. dtc -I dtb -O dts -o riscv64-virt.dts riscv64-virt.dtb 

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 # Make a new dir for custom linker scripts out RISC-V CPUs may require. mkdir ld && cd ld # Copy the default linker script into riscv64-virt.ld riscv64-unknown-elf-ld --verbose > riscv64-virt.ld 

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_addresses 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 jaly unconditional jump, la arquitectura RISC-V solo es compatible jaly 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_addresslos 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 qemuy dtcencontramos nuestra memoria en la máquina virtual virtRISC-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-sectionssignifica "secciones de recolección de basura", y ldse le indica que elimine las secciones no utilizadas después del enlace. Flags -nostartfiles, -nostdliby -nodefaultlibsdí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 crt0y enlazador, por lo que es importante pasar estos marcadores para que los valores predeterminados no entren en conflicto con nuestras preferencias de usuario.

-Tindica 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.sy 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:

 # -S freezes execution of our executable (-kernel) until we explicitly tell # it to start with a 'continue' or 'c' from our gdb client qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -S -kernel a.out 

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 gdbal servidor gdbque 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 L2y PC:- 0x8000001e. Si hizo todo como en el artículo, la salida será algo como esto: a



partir de ahora, puede usarlo gdbcomo de costumbre: -spara ir a la siguiente instrucción, info all-registerspara 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.calgú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!

Source: https://habr.com/ru/post/454208/


All Articles