Probablemente pocos especialistas de TI necesiten explicar qué es Memtest86 +, tal vez ya se haya convertido más o menos en el estándar para probar RAM en una PC. Cuando en una de las partes anteriores me encontré con una barra de memoria rota que viene incluida con la placa, (junto con una netbook con DDR2 habilitado) parecía una solución obvia. Otra pregunta es que allí, en principio, el funcionamiento inestable del sistema era visible a simple vista. En casos más complicados, escuché que, además del "toque" banal de las celdas de memoria hasta el infinito, esta herramienta utiliza algunos patrones de datos especiales en los que es más probable que se detecten errores en la operación DDR. En general, algo maravilloso, es una pena que incluso en el nombre que dice: 86 - "Solo para sistemas compatibles con x86". O no?
Debajo del corte, verá mis intentos de portar MemTest86 + v5.1 a RISC-V y el subtotal. Spoiler: se mueve!
DESCARGO DE RESPONSABILIDAD: el proyecto resultante fue probado mínimamente específicamente por mí en un ensamblaje RocketChip específico en un tablero específico. La precisión y la seguridad (especialmente en otros sistemas) no están garantizadas. Use bajo su propio riesgo. En particular, las áreas de memoria reservadas actualmente no se procesan de ninguna manera si caen en el rango de RAM.
Como ya dije, no hace mucho tiempo compré una placa base con Cyclone IV en AliExpress, pero la memoria contenía errores. Afortunadamente, una de las características importantes de esta placa fue el uso de módulos DDR2 SO-DIMM convencionales, lo mismo que en mi viejo netbook. Sin embargo, sería interesante obtener, por así decirlo, una solución autohospedada para probar los módulos de memoria (y, de hecho, también el controlador). La posibilidad de depurar mis errores en las condiciones de mala memoria no era del todo agradable. Especialmente sin esperar una solución rápida y mentalmente preparándome para posponer la reescritura completa en otro ensamblador por un tiempo indefinidamente largo, abrí un artículo de Wikipedia sobre Memtest86 + y de repente vi "Escrito en: C y ensamblaje" en la tarjeta. Hmm, es decir, él, aunque "... 86", pero no está escrito completamente en ensamblador? Esto es alentador. Solo queda entender la relación.
Entonces, vaya a memtest.org y descargue la versión 5.01 bajo GPL2. Para facilitar el desarrollo, lo volví a cargar en GitHub. Afortunadamente, justo en el archivo fuente, nos recibe el archivo README.background , titulado
La anatomía y fisiología de Memtest86-SMP
Explica con cierto detalle (e incluso con imágenes en forma de arte ASCII) la operación de alto nivel del código. Al comienzo del documento, vemos un diseño binario , que consta de bootsect.o
, setup.o
, head.o
y algunos memtest_shared
. Es fácil ver que estos tres archivos de objetos se obtienen de las fuentes de ensamblador correspondientes. A primera vista, todo lo demás está escrito en C! No está mal, no está mal ...
Como resultado, copié el Makefile
a Makefile.arch
y comencé a reescribir todo, e intentar tirar lo que no corresponde. En primer lugar, por supuesto, necesitaba una cadena de herramientas para RISC-V, que, afortunadamente, todavía ha estado conmigo desde los experimentos anteriores. Al principio pensé en hacer un puerto para la arquitectura de 32 bits, pero luego recordé que se había cargado un procesador de 64 bits en la placa, y tenía la riscv64-
con el prefijo riscv64-
.
Digresión de letras: por supuesto, lo primero fue estudiar el tema de la compatibilidad del código de 32 y 64 bits. Como resultado, la especificación para la parte no privilegiada de la ISA (Arquitectura del conjunto de instrucciones) que se encuentra en el párrafo 1.3 RISC-V ISA Overview
declaración 1.3 RISC-V ISA Overview
:
La principal ventaja de separar explícitamente las ISA base es que cada ISA base puede optimizarse para sus necesidades sin necesidad de admitir todas las operaciones necesarias para otras ISA base. Por ejemplo, RV64I puede omitir instrucciones y CSR que solo son necesarios para hacer frente a los registros más estrechos en RV32I. Las opciones RV32I pueden usar el espacio de codificación, de lo contrario reservado para instrucciones que solo son requeridas por variantes de espacio de direcciones más amplias.
También quiero señalar que es probable que la cadena de herramientas con el prefijo riscv64-
recopile fácilmente el código de 32 bits si la arquitectura de destino se selecciona correctamente, más sobre eso más adelante.
En el proceso de portabilidad, tiene sentido mantener estos documentos a mano:
Configuración de compilación
Comencemos por aceptar: quiero obtener un puerto adecuado para portar más a arquitecturas que no sean x86 y RISC-V. También propongo lanzar disquetes de arranque y otros detalles de x86 fuera de la construcción multiplataforma.
Lo que finalmente tenemos: hay tres archivos ensambladores: bootsect.S
, setup.S
y head.S
Los primeros dos son necesarios solo al inicio, y el tercero se necesita más adelante cuando se traslada a otra área de memoria. El hecho es que para probar la memoria "debajo de uno mismo", el código de prueba primero debe moverse a un nuevo lugar. Estos archivos se recopilan en ELF, de donde se toman secciones de código, datos, etc. Además, está ensamblado en forma de PIC (código independiente de posición), al principio incluso me sorprendió: aunque el código es independiente (es decir, sin un núcleo, libc, etc.), utiliza características tan avanzadas.
Además, en el Makefile se encuentran periódicamente parámetros que especifican la arquitectura: -march=i486
, -m32
y similares. Necesito escribir algo así y luego como un tonto . La situación con la arquitectura RISC-V es algo como esto: hay rv64
rv32
y rv64
(como, todavía hay los embebidos más truncados y rv128 reservados para el futuro, pero no estamos muy interesados en ellos), y el nombre ISA se forma asignando letras a este prefijo extensiones: i
- el conjunto de instrucciones de enteros básico, m
- multiplicación y división de enteros, ... Por supuesto, me gustaría hacer rv64i
, pero Memtest86 difícilmente será portado fácilmente a la arquitectura sin multiplicación. Es cierto que parece que el compilador simplemente generará llamadas de función en lugar de instrucciones "problemáticas", pero existe el riesgo de quedarse con un rendimiento muy reducido (sin mencionar el hecho de que estas funciones deberán escribirse o llevarse a algún lado).
También necesitará la línea ABI. En principio, los conceptos básicos de la convención de convocatoria ya se describen en el Volume I
especificado en el "Manual del programador de ensamblaje RISC-V", así que haré algo como
$ riscv64-linux-gnu-gcc-9 -mabi=help riscv64-linux-gnu-gcc-9: error: unrecognized argument in option '-mabi=help' riscv64-linux-gnu-gcc-9: note: valid arguments to '-mabi=' are: ilp32 ilp32d ilp32e ilp32f lp64 lp64d lp64f riscv64-linux-gnu-gcc-9: fatal error: no input files compilation terminated.
Y sin pensarlo lp64
, tomaré lp64
. Mirando hacia el futuro, diré que con este ABI, los archivos de encabezado de la biblioteca estándar no funcionaron, así que tomé lp64f
y ARCH "actualicé" a rv64imf
. Sin pánico, no planeo usar realmente punto flotante en mi puerto.
Como de alguna manera no quería profundizar en la escritura de scripts de enlazador multiplataforma y, por lo tanto, no pude encontrar de inmediato las claves de ld , decidí seguir head.S
con el archivo de ensamblador head.S
, aferrándome al resto de las funciones usando memtest_shared.arch.lds
. Descarté una indicación del formato de salida y la arquitectura (después de todo, es más fácil cambiarlo de una variable en el Makefile), y también comenté temporalmente DISCARD
al final, no pudiendo averiguar qué secciones específicas de información de depuración necesitaba. (Mirando hacia el futuro: información de depuración fina, pero .rela
tuvo que agregarse) En términos generales, la versión x86 enfatizó la necesidad de encajar en 64k - Espero que esto esté de alguna manera relacionado con las características del modo real y no nos preocupe en RISC-V . Como resultado, el objeto compartido con el PIC se recopilará, ya que en el original, el código y los datos que se cargarán en la memoria se extraerán de él.
Recopilamos ... y la compilación recae en el primer archivo reloc.c
, aparentemente, está tomado de algunos ld-linux.so
y es responsable de apoyar la tabla de compensación global, etc. de acuerdo con las convenciones de llamadas para x86. Resultó que requería trabajar directamente con registros usando insertos de ensamblador. Pero estamos en RISC-V: se creó originalmente para admitir PIC de forma nativa, así que no dude en lanzar reloc.c
. Además, todavía había inserciones, a veces bastante largas. Afortunadamente, estaban en el código de prueba inmediatamente después del código C comentado, que optimizan (a partir de ellos hice de nuevo piezas completas de código intercambiadas por la directiva del preprocesador) o algo dependiente de la plataforma, sin el cual, en casos extremos, puedo (probablemente) hacer (como encender / apagar el caché, restar el CPUID, etc.). Finalmente, hubo algunas cosas como la llamada rdtsc
, que yo rdtsc
, sin grandes problemas, puse en un encabezado dependiente de la plataforma y lo implementé de acuerdo con la documentación en RISC-V.
Como resultado, obtuvimos el directorio arch/i386
, donde se movía una gran cantidad de código de soporte PCI, leía información de los conjuntos de chips, definiciones específicas de plataforma de direcciones mapeadas en memoria, etc. Además, el principio de la función test_start
que test_start
, que es el punto de entrada desde la setup.S
al código C. Cuánto tiempo, breve, pero comentando todo lo que es posible y realizando todo lo que no se puede comentar bajo RISC-V (como la setup.S
y el código para trabajar con puerto serie en la implementación de SiFive), obtuve el arch/riscv
, con el que todo estaba más o menos compilado.
Aquí me veo obligado a aclarar que los experimentos en sí se realizaron parcialmente antes de la redacción del artículo, por lo que una secuencia específica de acciones puede contener una cierta cantidad de "ficción artística". Sin embargo, intento al menos realizar la presentación de tal manera que, en cualquier caso, represente uno de los caminos posibles (soy programador, lo recuerdo) . Entonces, veamos cómo comenzar todo.
Corriendo en hierro
Desde experimentos anteriores, todavía tengo un "soporte" polvoriento del Raspberry Pi, conectado a la placa de depuración. Los cables proporcionan UART, JTAG y un adaptador con una tarjeta SD. Un cierto procesador RV64 con un controlador DDR2 está cosido en la memoria de configuración. Como en tiempos anteriores, enciendo la "frambuesa", abro dos sesiones SSH antes, una de las cuales reenvía el puerto TCP 3333 para conectar gdb a OpenOCD. En una de las sesiones, inicio minicom para ver UART, en otra, openocd para depurar desde el host a través de JTAG. Enciendo la alimentación de la placa, y se ejecutan mensajes en la consola sobre cómo se cargan los datos de la SD.
Ahora puedes ejecutar el comando:
riscv64-unknown-elf-gdb \ -ex 'target remote 127.0.0.1:3333' \ -ex 'restore /path/to/memtest_shared.bin binary 0x80010000' \ -ex 'add-symbol-file /path/to/memtest_shared 0x80010000' -ex 'set $pc=0x80010000'
Las opciones -ex
gdb que finja que el usuario ha ingresado estos comandos desde la consola:
- el primero establece una conexión con OpenOCD
- el segundo copia el contenido del archivo host especificado a la dirección especificada
- el tercero explica a gdb que la información sobre el código fuente debe tomarse de este archivo, teniendo en cuenta el hecho de que se descargó en esta dirección (y no lo que se indica en sí mismo)
- nota: tomamos los caracteres del archivo ELF y cargamos el binario "en bruto"
- finalmente, el cuarto traduce a la fuerza el puntero de comando actual a nuestro código
Desafortunadamente, no todo funciona sin problemas, y aunque las líneas de código en el depurador se muestran correctamente, pero en todas las variables globales: ceros. De hecho, si ejecutamos un comando de la forma p &global_var
en gdb, p &global_var
, vemos la dirección de acuerdo con la dirección de descarga inicial (tengo 0x0
), que no se especifica usando add-symbol-file
. Como una muleta, pero una solución muy simple, simplemente agregué 0x80010000
a la dirección especificada manualmente y miré el contenido de la memoria a través de x/x 0xADDR
. De hecho, sería posible indicar temporalmente la dirección de inicio correcta en el script del vinculador, que en este momento coincidirá con la dirección de descarga en esta configuración de prueba .
Características de la reubicación en arquitecturas modernas.
Bueno, cómo descargar el código de alguna manera lo descubrimos, lo comenzamos. No funciona La depuración paso a paso muestra que caemos durante el funcionamiento de la función switch_to_main_stack
; parece que todavía está intentando utilizar el valor no publicado de la dirección del símbolo correspondiente a la pila de trabajo.
De todos modos, el primer volumen de documentación nos informa sobre diferentes pseudoinstrucciones y su trabajo con PIC activado y desactivado:

Como puede ver, el principio general es que las direcciones en la memoria se cuentan a partir de la instrucción actual, con la primera agregando la parte superior del desplazamiento y la siguiente add
pulir los bits de orden inferior. Difícilmente ayuda declarar una variable global como
struct vars * const v = &variables;
Por lo tanto, tomamos la documentación RISC-V ELF psABI con descripciones de los tipos de reubicaciones y escribimos la parte específica de la plataforma para reloc.c
. Aquí debe tenerse en cuenta que el archivo original, aparentemente, fue tomado del código multiplataforma. Allí, incluso en lugar de especificar una profundidad de bits específica, se ElfW(Addr)
macros del tipo ElfW(Addr)
, Elf32_Addr
a Elf32_Addr
o Elf64_Addr
. Sin embargo, no en todas partes, es por eso que los agregamos donde no están en el código general (así como en el arch/riscv/reloc.inc.c
, después de todo, para RISC-V no hay ningún sentido especial para estar vinculado a una profundidad de bits específica, donde no es requerido)
Como resultado, switch_to_main_stack
comenzó a pasar (no sin las instrucciones del ensamblador dependiente de la plataforma, por supuesto). El depurador muestra variables globales todavía torcidas. Bueno, está bien :(
Definición de hardware
Por supuesto, para las pruebas sería posible usar constantes codificadas en lugar del código de definición del equipo que se arrojó, pero para cada ensamblaje de procesador específico, la reconstrucción de memtest es incluso demasiado costosa para los estándares de mi aplicación. Por lo tanto, actuaremos "como adultos serios". Afortunadamente, en RISC-V (y probablemente en la mayoría de las arquitecturas modernas) es habitual que el gestor de arranque pase un puntero al Blob de árbol de dispositivos , que es una versión compilada de la descripción DTS como esta:
zeowaa-1gb.dts /dts-v1/; / { #address-cells = ^_^lt gt^_^; #size-cells = ^_^lt gt^_^; compatible = "freechips,rocketchip-unknown-dev"; model = "freechips,rocketchip-unknown"; chosen { bootargs = "console=ttySIF0,125200 debug loglevel=7"; }; firmware { sifive,uboot = "YYYY-MM-DD"; }; L16: aliases { serial0 = &L8; }; L15: cpus { #address-cells = ^_^lt gt^_^; #size-cells = ^_^lt�gt^_^; timebase-frequency = ^_^ltgt^_^; L5: cpu@0 { device_type = "cpu"; clock-frequency = ^_^lt�gt^_^; compatible = "sifive,rocket0", "riscv"; d-cache-block-size = ^_^lt gt^_^; d-cache-sets = ^_^lt@gt^_^; d-cache-size = ^_^ltကgt^_^; d-tlb-sets = ^_^lt gt^_^; d-tlb-size = ^_^lt gt^_^; i-cache-block-size = ^_^lt gt^_^; i-cache-sets = ^_^lt@gt^_^; i-cache-size = ^_^ltကgt^_^; i-tlb-sets = ^_^lt gt^_^; i-tlb-size = ^_^lt gt^_^; mmu-type = "riscv,sv39"; next-level-cache = <&L10>; reg = <0x0>; riscv,isa = "rv64imafdc"; status = "okay"; timebase-frequency = ^_^ltgt^_^; tlb-split; L3: interrupt-controller { #interrupt-cells = ^_^lt gt^_^; compatible = "riscv,cpu-intc"; interrupt-controller; }; }; }; L10: ram@80000000 { device_type = "memory"; reg = <0x0 0x80000000 0x0 0x40000000>; reg-names = "mem"; }; L14: soc { #address-cells = ^_^lt gt^_^; #size-cells = ^_^lt gt^_^; compatible = "freechips,rocketchip-unknown-soc", "simple-bus"; ranges; L1: clint@2000000 { compatible = "riscv,clint0"; interrupts-extended = <&L3 3 &L3 7>; reg = <0x2000000 0x10000>; reg-names = "control"; }; L2: debug-controller@0 { compatible = "sifive,debug-013", "riscv,debug-013"; interrupts-extended = <&L3 65535>; reg = <0x0 0x1000>; reg-names = "control"; }; L9: gpio@64002000 { #gpio-cells = ^_^lt gt^_^; #interrupt-cells = ^_^lt gt^_^; compatible = "sifive,gpio0"; gpio-controller; interrupt-controller; interrupt-parent = <&L0>; interrupts = <3 4 5 6 7 8>; reg = <0x64002000 0x1000>; reg-names = "control"; }; L0: interrupt-controller@c000000 { #interrupt-cells = ^_^lt gt^_^; compatible = "riscv,plic0"; interrupt-controller; interrupts-extended = <&L3 11 &L3 9>; reg = <0xc000000 0x4000000>; reg-names = "control"; riscv,max-priority = ^_^lt gt^_^; riscv,ndev = ^_^lt gt^_^; }; L6: rom@10000 { compatible = "sifive,maskrom0"; reg = <0x10000 0x2000>; reg-names = "mem"; }; L8: serial@64000000 { compatible = "sifive,uart0"; interrupt-parent = <&L0>; clocks = <&tlclk>; interrupts = ^_^lt gt^_^; reg = <0x64000000 0x1000>; reg-names = "control"; }; L7: spi@64001000 { #address-cells = ^_^lt gt^_^; #size-cells = ^_^lt�gt^_^; compatible = "sifive,spi0"; interrupt-parent = <&L0>; interrupts = ^_^lt gt^_^; reg = <0x64001000 0x1000>; clocks = <&tlclk>; reg-names = "control"; L12: mmc@0 { compatible = "mmc-spi-slot"; disable-wp; reg = <0x0>; spi-max-frequency = ^_^lt gt^_^; voltage-ranges = <3300 3300>; }; }; tlclk: tlclk { #clock-cells = ^_^lt�gt^_^; clock-frequency = ^_^lt gt^_^; clock-output-names = "tlclk"; compatible = "fixed-clock"; }; }; };
Solía analizar archivos ELF, pero ahora estoy nuevamente convencido con FDT (árbol de dispositivos planos): estas especificaciones amables están escritas por personas buenas y afectuosas (¡Aún así, ellos mismos lo analizan!) y analizar dichos archivos (al menos hasta que necesite procesar entradas no confiables) no plantea problemas particulares. Así que aquí: al comienzo del archivo hay una estructura de encabezado simple que contiene el número mágico 0xd00dfeed
y algunos campos más. Estamos interesados en el desplazamiento del "árbol plano" off_dt_struct
y la tabla de filas off_dt_strings
. En realidad, también necesita procesar off_mem_rsvmap
, que enumera las áreas de memoria que es mejor evitar. Todavía los ignoro (no están en mi tablero), pero no repito esto en casa .
En principio, el procesamiento no es particularmente difícil: solo necesita caminar sobre un árbol plano de acuerdo con los tokens. Hay tres tokens clave :
Bueno, en general, eso es todo: revisamos esta sección, sin olvidar observar la alineación de 4 bytes. Ah, sí, una mosca en la pomada: los números en FDT están en formato big endian, por lo que hacemos una función simple
static inline uint32_t be32(uint32_t x) { return (x << 24) | (x >> 24) | ((x & 0xff0000) >> 8) | ((x & 0xff00) << 8); }
Como resultado, en riscv_entry
primero que debe hacer es analizar FDT y la parte de head.S
que es responsable de transferir el control a riscv_entry
parece a esto
.globl startup_32 # -- ... startup_32: lla sp, boot_stack_top mv s0, a0 # s0, s1 -- callee-saved mv s1, a1 # ... .bss # jal _dl_start # mv a0, s0 mv a1, s1 j riscv_entry
En el registro a0
nos da una identificación de hart (hart es algo así como un flujo de hardware en la terminología RISC-V). Todavía no lo uso, tendría que resolverlo en un caso de subproceso único. En a1
gestor de arranque coloca un puntero al FDT. Lo pasamos a la función void riscv_entry(ulong hartid, uint8_t *fdt_address)
.
Ahora, con el advenimiento de la parsilka FDT en mi código, la secuencia de carga del tablero se hizo así:
Y desde el lado de gdb, se agrega el comando
Resultó que, además de las inserciones de ensamblador, también hay direcciones de memoria conocidas. Por ejemplo SCREEN_ADR
(exactamente así, con una D
), que indica el área correspondiente a lo que se muestra en la pantalla. Cuando me encontré con esto, simplemente coloqué con un gesto amplio todo lo que se refiere a él en #if HAS_SCREEN
, y luego lo #if HAS_SCREEN
ciegas durante mucho tiempo. Ya pensé manualmente, de vez en cuando, volcar todo esto en la consola, pero luego noté que el mismo código generaba muchas secuencias de escape en el puerto serie. Resultó que todo ya había sido escrito antes que nosotros, solo necesita colocar las definiciones con mayor precisión, y aquí está, la interfaz familiar (aunque en blanco y negro) en la ventana de minicom. (Por el momento, HAS_SCREEN no se utiliza en absoluto; acabo de iniciar la matriz dummy_con
para cambiar el código original como mínimo).
Depuración en QEMU
Así que depuré todo en un tablero real, y durante algún tiempo, ni siquiera a ciegas. Pero todo se ralentiza en JTAG: ¡horror! Bueno, al final, todo debería funcionar en hardware real, pero sería bueno depurar en QEMU. Después de un cierto número de experimentos, algo resultó ser una muleta, pero muy similar a trabajar con una tabla:
$ qemu-system-riscv64 -M help Supported machines are: none empty machine sifive_e RISC-V Board compatible with SiFive E SDK sifive_u RISC-V Board compatible with SiFive U SDK spike_v1.10 RISC-V Spike Board (Privileged ISA v1.10) (default) spike_v1.9.1 RISC-V Spike Board (Privileged ISA v1.9.1) virt RISC-V VirtIO Board (Privileged ISA v1.10)
Observamos qué placas QEMU está lista para emular. Estoy interesado en hardware compatible con sifive_u
.
$ qemu-system-riscv64 -M sifive_u,dumpdtb -m 1g # - QEMU on -- strace $ ls -l on -rw-rw-r-- 1 trosinenko trosinenko 1923 19 20:14 on $ dtc -I dtb < on > on.dts # $ vim on.dts # bootargs $ dtc < on.dts > on.dtb <stdout>: Warning (clocks_property): /soc/ethernet@100900fc:clocks: cell 0 is not a phandle reference <stdout>: Warning (clocks_property): /soc/ethernet@100900fc:clocks: cell 1 is not a phandle reference <stdout>: Warning (clocks_property): /soc/ethernet@100900fc:clocks: cell 2 is not a phandle reference <stdout>: Warning (interrupts_extended_property): /soc/interrupt-controller@c000000:interrupts-extended: cell 0 is not a phandle reference <stdout>: Warning (interrupts_extended_property): /soc/interrupt-controller@c000000:interrupts-extended: cell 2 is not a phandle reference <stdout>: Warning (interrupts_extended_property): /soc/clint@2000000:interrupts-extended: cell 0 is not a phandle reference <stdout>: Warning (interrupts_extended_property): /soc/clint@2000000:interrupts-extended: cell 2 is not a phandle reference
Ahora tenemos un blob de árbol de dispositivo "fijo". Sin cambiar la configuración de la VM (¡muletas!), Ejecute:
qemu-system-riscv64 \ -M sifive_u -m 1g \ -serial stdio \ -s -S
-serial stdio
redirige el puerto serie a la consola, porque las secuencias de escape se utilizarán activamente. Las opciones -s -S
aumentan gdbserver y crean una VM para pausar, respectivamente. Puede descargar el código utilizando el loader
, pero luego debe reiniciar QEMU cada vez.
Puedes conectarte usando
riscv64-unknown-elf-gdb \ -ex 'target remote 127.0.0.1:1234' \ -ex 'restore /path/to/on.dtb binary 0x80100000' \ -ex 'restore /path/to/memtest_shared.bin binary 0x80020000' \ -ex 'add-symbol-file memtest_shared 0x80100000' \ -ex 'set $a1=0x80020000' \ -ex 'set $pc=0x80100000'
Como resultado, ¡todo funciona más que inteligentemente!
Principio general de trabajo
, , , Memtest86+ btrace
, , ( , QEMU):

, , memtest . , (, trap): , , QEMU - ! «» Illegal instruction
, . mcause
(?), — mepc
(?), — mtval
( ?), .

, :
head.S:
# # = 0 --- , # , , ... lla t1, _trap_entry csrw mtvec, t1 # ... _trap_entry: csrr a0, mcause csrr a1, mepc csrr a2, mtval jal riscv_trap_entry
, calling convention, . memtest, HiFive_U-Boot, Volume II
:
arch.c:
static const char *errors[] = { "Instruction address misaligned", "Instruction access fault", "Illegal instruction", "Breakpoint", "Load address misaligned", "Load access fault", "Store/AMO address misaligned", "Store/AMO access fault", ^_^quot quot^_^, ^_^quot quot^_^, ^_^quot quot^_^, ^_^quot quot^_^, "Instruction page fault", "Load page fault", ^_^quot quot^_^, "Store/AMO page fault", }; void riscv_trap_entry(ulong cause, ulong epc, ulong tval) { char buf[32]; cprint(12, 0, "EXCP: "); if (cause < sizeof(errors) / sizeof(errors[0])) { cprint(12, 8, errors[cause]); } else { itoa(buf, cause); cprint(12, 8, buf); } cprint(13, 0, "PC: "); hprint3(13, 8, epc, 8); cprint(14, 0, "Addr: "); hprint3(14, 8, tval, 8); HALT(); }
— « » . , «» , , , .
: . , memtest : : « , , . ». : do_test
main.c
2, ( ), — «» , memtest. , run_at
, memtest _start
_end
( «» ), - spinlock' goto *addr;
. , , «» , «».
, bss
— _dl_start
, riscv_entry
, trap entry. , : L1I-, . , fence.i
.
, Memtest86+ — , barrier_s
. , . , , .
, : . : . : , - (Own Address, ) . , , . . - . , x86 , , uint64_t
0x80000002
. , : , load/store x86 , — . , QEMU , « , ».
, , — unaligned access ..
, , RocketChip, — QEMU, , , RocketChip — unaligned access trap, QEMU « ».
«misaligned» ,
Changed description of misaligned load and store behavior. The specification now allows visible misaligned address traps in execution environment interfaces, rather than just mandating invisible handling of misaligned loads and stores in user mode. Also, now allows access exceptions to be reported for misaligned accesses (including atomics) that should not be emulated.
, , — , user-mode code , . . , , . , — - machine mode . , rdtsc
(x86) rdtime
(rv64), trap, . , , memory-mapped .
: , low_test_addr
( ), , fdt . , , low_test_addr
, , 2 high_test_adr
… , — : head.S
initial_load_addr
, riscv_entry
move_to_correct_addr
:
static void move_to_correct_addr(void) { uintptr_t cur_start = (uintptr_t)&_start; uintptr_t cur_end = (uintptr_t)&_end; if (cur_start == low_test_addr || cur_start == high_test_adr) {
, — , memtest , RAM - . RISC-V , v->plim_lower
.
, «» , -, — test.c
ulong
( unsigneg long
), 32- x86 uint32_t
, « 64 » uint64_t
. «!!! Good: ffffffff Real: ffffffff Bad bits: 00000000». ? - -1, 32 1. , , 0… , : , ulong
( uint32_t
), ( uintptr_t
). , . , uint64_t
4. RISC-V , C, , — UB. memtest UBSan. , , UBSan trap-on-error JTAG.
, memtest - , , U-Boot.
: mkimage
U-Boot Linux :
mkimage -A riscv -O linux -T kernel -C none \ -a 0x80000000 -e 0x80000000 \ -n memtest -d memtest.bin memtest.uboot
SD-
run mmcsetup; run fdtsetup; fdt set /chosen bootargs "console=ttyS0"; fatload mmc 0:1 82000000 memtest.uboot; bootm fdt; bootm 82000000 - ${fdtaddr}
( , run
— ).
: FDT: 0xbffb7c80
. , : ffffffff
, . , ( ), : HiFive_U-Boot :
theKernel(machid, (unsigned long)images->ft_addr);
,
void (*theKernel)(int arch, uint params);
, , , , 32 , head.S
:
li t0, 0xffffffffL and a1, a1, t0
, , - , , , :
- x86. — review
- SMP RISC-V
arch/
-test.c
RISC-V ( -O0
!)