Parte 6: Portando o MemTest86 + para o RISC-V


Provavelmente poucas pessoas de TI precisam explicar o que é o Memtest86 + - talvez ele já tenha se tornado mais ou menos o padrão no teste de RAM em um PC. Quando em uma das partes anteriores me deparei com uma barra de memória quebrada que acompanha a placa, ela (junto com um netbook habilitado para DDR2) parecia uma solução óbvia. Outra questão é que ali, em princípio, a operação instável do sistema era visível a olho nu. Em casos mais complicados, ouvi dizer que, além do banal "toque" das células de memória até o infinito, essa ferramenta usa alguns padrões de dados especiais nos quais os erros na operação de DDR têm mais probabilidade de serem detectados. Em geral, uma coisa maravilhosa, é uma pena que mesmo no nome esteja escrito: 86 - "Somente para sistemas compatíveis com x86". Ou não?


Abaixo do corte, você verá minhas tentativas de portar o MemTest86 + v5.1 para o RISC-V e o subtotal. Spoiler: ele se move!


AVISO LEGAL: o projeto resultante foi minimamente testado especificamente por mim em uma montagem RocketChip específica em um quadro específico. Precisão e segurança (especialmente em outros sistemas) não são garantidas. Use por sua conta e risco. Em particular, as áreas de memória atualmente reservadas não são processadas de forma alguma se estiverem dentro do intervalo de RAM.


Como eu já disse, há pouco tempo comprei uma placa-mãe com o Cyclone IV no AliExpress, mas a memória era de buggy. Felizmente, uma das características importantes desta placa foi o uso de módulos DDR2 SO-DIMM convencionais - o mesmo que no meu antigo netbook. No entanto, seria interessante obter, por assim dizer, uma solução auto-hospedada para testar módulos de memória (e, de fato, também o controlador). A perspectiva de depurar meus erros nas condições de memória ruim não era de todo agradável. Especialmente sem esperar por uma solução rápida e mentalmente me preparando para adiar a reescrita completa em outro montador por tempo indeterminado, abri um artigo da Wikipedia no Memtest86 + e de repente vi “Escrito em: C e montagem” no cartão. Hmm, isto é, ele, embora "... 86", mas não está escrito inteiramente em assembler? Isso é encorajador. Resta apenas entender o relacionamento.


Então, vá para memtest.org e faça o download da versão 5.01 na GPL2. Para facilitar o desenvolvimento, eu o recarreguei no GitHub. Felizmente, no arquivo de origem, somos recebidos pelo arquivo README.background , intitulado


Anatomia e Fisiologia do Memtest86-SMP

Explica com alguns detalhes (e mesmo com imagens na forma de arte ASCII) a operação de alto nível do código. No início do documento, vemos um layout binário , composto por bootsect.o , setup.o , head.o e alguns memtest_shared . É fácil ver que esses três arquivos de objeto são obtidos das fontes correspondentes do assembler. À primeira vista, tudo o resto está escrito em C! Nada mal, nada mal ...


Como resultado, copiei o Makefile para Makefile.arch e comecei a reescrever tudo, e tentei jogar fora o que não corresponde. Antes de tudo, é claro, eu precisava de uma cadeia de ferramentas para o RISC-V, que, felizmente, ainda está comigo desde os experimentos anteriores. No começo, pensei em criar uma porta para arquitetura de 32 bits, mas depois lembrei que um processador de 64 bits foi carregado na placa e eu tinha a riscv64- com o prefixo riscv64- .


Digressão lírica: é claro, a primeira coisa foi estudar a questão da compatibilidade do código de 32 e 64 bits. Como resultado, a especificação para a parte não privilegiada do ISA (Instruction Set Architecture) encontrada no parágrafo 1.3 RISC-V ISA Overview declaração 1.3 RISC-V ISA Overview :


A principal vantagem de separar explicitamente os ISAs de base é que cada ISA de base pode ser otimizado para suas necessidades sem exigir o suporte de todas as operações necessárias para outros ISAs de base. Por exemplo, o RV64I pode omitir instruções e CSRs necessários apenas para lidar com os registros mais restritos no RV32I. As opções do RV32I podem usar espaço de codificação, caso contrário, reservado para instruções exigidas apenas por variantes mais amplas do espaço de endereço.

Também quero observar que a cadeia de ferramentas com o prefixo riscv64- provavelmente coletará facilmente o código de 32 bits se a arquitetura de destino for selecionada corretamente - mais sobre isso mais tarde.


No processo de portabilidade, faz sentido manter esses documentos à mão:



Configuração de compilação


Vamos começar concordando: desejo obter uma porta adequada para portar outras arquiteturas que não sejam x86 e RISC-V. Também proponho lançar disquetes de inicialização e outros detalhes específicos do x86 da compilação de plataforma cruzada.


Em última análise, o que temos: existem três arquivos assembler: bootsect.S , setup.S e head.S Os dois primeiros são necessários apenas na inicialização, e o terceiro é necessário mais tarde ao se mudar para outra área de memória. O fato é que, para testar a memória "debaixo de si", o código de teste deve primeiro se mover para um novo local. Arquivos Sich são coletados no ELF, a partir do qual seções de código, dados etc. são retiradas. Além disso, ele é coletado na forma de PIC (Código Independente de Posição) - a princípio, fiquei surpreso: embora o código seja autônomo (ou seja, sem um kernel, libc, etc.), ele usa recursos avançados.


Além disso, os parâmetros que definem a arquitetura periodicamente são encontrados no Makefile: -march=i486 , -m32 e similares. Eu preciso escrever algo assim, e depois como um otário . A situação com a arquitetura RISC-V é mais ou menos assim: existem rv64 e rv64 (como, ainda há as incorporadas e rv128 mais truncadas reservadas para o futuro, mas não estamos muito interessadas nelas), e o nome ISA é formado atribuindo letras a esse prefixo extensões: i - o conjunto inteiro básico de instruções, m - multiplicação e divisão de números inteiros ... É claro que eu gostaria de fazer o rv64i , mas o Memtest86 dificilmente será facilmente portado para a arquitetura sem multiplicação. É verdade que parece que o compilador simplesmente gera chamadas de função em vez de instruções "problemáticas", mas existe o risco de permanecer com desempenho bastante reduzido (sem mencionar que essas funções precisarão ser gravadas ou levadas a algum lugar).


Você também precisará da linha ABI. Em princípio, os princípios básicos da convenção de chamada já estão descritos no Volume I especificado no "Manual do Programador de Montagem RISC-V", portanto, farei 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. 

E sem pensar lp64 , eu vou pegar lp64 . Olhando para o futuro, direi que, com essa ABI, os arquivos de cabeçalho da biblioteca padrão não funcionaram, por isso peguei lp64f e o ARCH "atualizou" para rv64imf . Sem pânico, não pretendo realmente usar ponto flutuante na minha porta.


Como de alguma forma eu não queria me aprofundar na escrita de scripts de vinculador de plataforma cruzada - e, portanto, não consegui encontrar imediatamente as chaves para ld , decidi seguir com o arquivo head.S do assembler, agarrando-me ao restante das funções usando memtest_shared.arch.lds . Deitei uma indicação do formato e da arquitetura de saída (afinal, é mais fácil alterá-lo de uma variável no Makefile) e também comentei temporariamente o DISCARD no final, não sendo capaz de descobrir quais seções específicas das informações de depuração eu precisava. (Olhando para o futuro: informações detalhadas sobre depuração, mas a .rela precisava ser adicionada) De um modo geral, a versão x86 enfatizava a necessidade de caber em 64k - espero que isso esteja de alguma forma relacionado aos recursos do modo real e não nos preocupe no RISC-V . Como resultado, o objeto compartilhado com o PIC será coletado, como no original, o código e os dados que serão carregados na memória serão removidos.


Nós coletamos ... e a compilação cai no primeiro arquivo reloc.c - aparentemente, é retirado de algum ld-linux.so e é responsável por dar suporte à Tabela Global de Compensações, etc. de acordo com as convenções de chamada para x86. Aconteceu que era necessário trabalhar diretamente com registradores usando inserções de montador. Mas estamos no RISC-V - ele foi originalmente criado para oferecer suporte nativo ao PIC, portanto, sinta-se livre para lançar reloc.c . Além disso, ainda havia inserções, às vezes bastante longas. Felizmente, eles estavam no código de teste imediatamente após o código C comentado, que eles otimizam (a partir deles, novamente fiz trechos de código completos alternados pela diretiva de pré-processador) ou algo dependente da plataforma, sem o qual, em casos extremos, eu posso (provavelmente) do (como ligar / desligar o cache, subtrair o CPUID, etc.). Finalmente, houve algumas coisas como a chamada rdtsc , que eu rdtsc , sem grandes problemas, coloquei em um cabeçalho dependente da plataforma e o implementei de acordo com a documentação no RISC-V.


Como resultado, obtivemos o diretório arch/i386 , onde uma grande quantidade de código de suporte PCI se moveu, leu informações dos chipsets, definições específicas da plataforma de endereços mapeados na memória, etc. Além disso, o início da função test_start , que é o ponto de entrada da setup.S para o código C. Quanto tempo, curto, mas comentando tudo o que é possível e realizando tudo o que não pode ser comentado no RISC-V (como a setup.S e o código para trabalhar com porta serial na implementação do SiFive), obtive o arch/riscv , com o qual tudo foi mais ou menos compilado.


Aqui sou forçado a esclarecer que os experimentos foram realizados parcialmente antes da redação do artigo, de modo que uma sequência específica de ações possa conter uma certa quantidade de "ficção artística". No entanto, tento conduzir pelo menos a apresentação de tal forma que, de qualquer forma, represente um dos caminhos possíveis (sou programador, lembro disso) . Então, vamos ver como começar tudo.


Correndo em ferro


Desde experimentos anteriores, ainda tenho um suporte empoeirado do Raspberry Pi, conectado à placa de depuração. Os fios fornecem UART, JTAG e um adaptador com um cartão SD. Um determinado processador RV64 com um controlador DDR2 é costurado na memória de configuração. Como nos tempos anteriores, eu ligo o “raspberry”, abro duas sessões SSH antes dele, uma das quais encaminha a porta 3333 TCP para conectar o gdb ao OpenOCD. Em uma das sessões, inicio o minicom para assistir ao UART, em outra - openocd para depuração do host via JTAG. Ligo a alimentação do quadro - e mensagens no console sobre como ele carrega dados do SD executado.


Agora você pode executar o 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' 

as opções -ex gdb a fingir que o usuário inseriu esses comandos no console:


  • o primeiro estabelece uma conexão com o OpenOCD
  • o segundo copia o conteúdo do arquivo host especificado para o endereço especificado
  • o terceiro explica ao gdb que as informações sobre o código-fonte devem ser extraídas desse arquivo, levando em consideração o fato de que ele foi baixado nesse endereço (e não o que é indicado nele)
    • nota: pegamos os caracteres do arquivo ELF e carregamos o binário "bruto"
  • finalmente, o quarto converte à força o ponteiro de comando atual para o nosso código

Infelizmente, nem tudo corre absolutamente bem e, embora as linhas de código no depurador sejam exibidas corretamente, mas em todas as variáveis ​​globais - zeros. De fato, se executarmos um comando no formato p &global_var no gdb, veremos o endereço de acordo com o endereço de download inicial (eu tenho 0x0 ), que não é especificado usando add-symbol-file . Como muleta, mas como uma solução muito simples, simplesmente adicionei 0x80010000 ao endereço especificado manualmente e examinei o conteúdo da memória através de x/x 0xADDR . De fato, seria possível indicar temporariamente o endereço inicial correto no script do vinculador, que no momento coincidirá com o endereço de download nessa configuração de teste .


Recursos de realocação em arquiteturas modernas


Bem, como fazer o download do código, de alguma forma, descobrimos - começamos. Não funciona A depuração passo a passo mostra que caímos durante a operação da função switch_to_main_stack - parece que ele ainda está tentando usar o valor não relacionado do endereço do símbolo correspondente à pilha de trabalho.


Mesmo assim, o primeiro volume de documentação nos fala sobre diferentes pseudo-instruções e seu trabalho com o PIC ligado e desligado:


Algumas pseudo-instruções do RISC-V


Como você pode ver, o princípio geral é que os endereços na memória são contados a partir da instrução atual, com o primeiro adicionando a parte superior do deslocamento e o próximo add polindo os bits de ordem inferior. Dificilmente ajuda declarar uma variável global como


 struct vars * const v = &variables; 

Portanto, pegamos a documentação do RISC-V ELF psABI com descrições dos tipos de realocações e escrevemos a parte específica da plataforma para reloc.c . Aqui deve-se notar que o arquivo original, aparentemente, foi retirado do código de plataforma cruzada. Lá, mesmo em vez de especificar uma profundidade de bits específica, ElfW(Addr) macros do tipo ElfW(Addr) , Elf32_Addr para Elf32_Addr ou Elf64_Addr . No entanto, em todos os lugares, no entanto, é por isso que os adicionamos onde não estão no código geral (assim como no código arch/riscv/reloc.inc.c - afinal, para o RISC-V, não há um sentido especial a ser vinculado a uma profundidade de bits específica, onde não há necessário).


Como resultado, switch_to_main_stack começou a passar (não sem as instruções do montador dependente da plataforma, é claro). O depurador mostra variáveis ​​globais ainda tortas. Bem, ok :(


Definição de Hardware


Obviamente, para testes, seria possível usar constantes codificadas em vez do código de definição de equipamento descartado, mas para cada conjunto específico de processador, a reconstrução do memtest é muito cara pelos padrões do meu aplicativo. Portanto, agiremos "como adultos sérios". Felizmente, no RISC-V (e provavelmente na maioria das arquiteturas modernas), é habitual que o gerenciador de inicialização transmita um código ao Device Tree Blob , que é uma versão compilada da descrição do 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&#0;gt^_^; timebase-frequency = ^_^lt󴉀gt^_^; L5: cpu@0 { device_type = "cpu"; clock-frequency = ^_^lt&#0;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 = ^_^lt󴉀gt^_^; 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&#0;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&#0;gt^_^; clock-frequency = ^_^lt gt^_^; clock-output-names = "tlclk"; compatible = "fixed-clock"; }; }; }; 

Costumava analisar arquivos ELF, mas agora estou novamente convencido do FDT (árvore plana de dispositivos): essas especificações são escritas por pessoas atenciosas (Ainda assim, eles mesmos analisam!) e analisar esses arquivos (pelo menos até que você precise processar entradas não confiáveis) não apresenta problemas específicos. Então aqui: no início do arquivo, existe uma estrutura de cabeçalho simples que contém o número mágico 0xd00dfeed e mais alguns campos. Estamos interessados ​​no deslocamento da "árvore plana" off_dt_struct e da tabela de linhas off_dt_strings . Na verdade, você também precisa processar off_mem_rsvmap , que enumera áreas da memória que devem ser evitadas. Ainda os ignoro (eles não estão no meu quadro), mas não o repito em casa .


Em princípio, o processamento não é particularmente difícil: você só precisa andar em uma árvore plana de acordo com os tokens. Existem três tokens principais :


  • FDT_BEGIN_NODE - nos dados extras imediatamente a seguir, vem o nome do elemento da subárvore na forma de uma sequência terminada em nulo. Basta adicionar o nome à pilha
  • FDT_END_NODE - a subárvore acabou, remova o elemento da pilha
  • FDT_PROP - aqui está um pouco mais complicado: é seguido por uma estrutura, seguida por len bytes de dados extras. O nome da "variável" está no nome do deslocamento na tabela de cadeias
     struct { uint32_t len; uint32_t nameoff; } 

Bem, em geral, é tudo: passamos por esta seção, sem esquecer de observar o alinhamento em 4 bytes. Ah, sim, uma mosca na pomada: os números no FDT estão no formato big endian, então criamos uma função simples


 static inline uint32_t be32(uint32_t x) { return (x << 24) | (x >> 24) | ((x & 0xff0000) >> 8) | ((x & 0xff00) << 8); } 

Como resultado, no riscv_entry primeira coisa a fazer é analisar o FDT e a parte do head.S responsável pela transferência do controle para o riscv_entry é algo parecido com isto


  .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 

No registro a0 hart id é passado para nós (hart é algo como um fluxo de hardware na terminologia RISC-V) - eu ainda não o uso, eu precisaria descobrir isso em um caso de thread único. Em a1 carregador de inicialização coloca um ponteiro para o FDT. Passamos para a função void riscv_entry(ulong hartid, uint8_t *fdt_address) .


Agora, com o advento da FDT parsilka no meu código, a sequência de carregamento da placa ficou assim:


  • ligue a energia
  • aguarde o console de inicialização U
  • insira comandos para preparar o FDT correto. Em particular, a /chosen/bootargs command /chosen/bootargs armazena a linha de comando do kernel. Tudo o que eu retiro do FDT - intervalo de RAM, endereço UART, ... - pode e deve ser deixado como está
     run fdtsetup fdt set /chosen bootargs "console=ttyS0 btrace" 
  • usando o comando fdt addr , descubra o endereço de download do FDT, se você não procurou

E do lado do gdb, o comando é adicionado


  • -ex 'set $a1=0xfdtaddr'

Saída de informações na tela


Como se viu, além das inserções do assembler, também existem endereços de memória conhecidos. Por exemplo, SCREEN_ADR (exatamente assim, com um D ), que aponta para a área correspondente ao que é exibido na tela. Quando me deparei com isso, simplesmente coloquei com um gesto amplo tudo o que se refere a ele em #if HAS_SCREEN e depois #if HAS_SCREEN cegamente por um longo tempo. Eu já pensei manualmente, de vez em quando, em despejar tudo isso no console, mas notei que o mesmo código dolorosamente muitas seqüências de escape saem para a porta serial. Aconteceu que tudo já havia sido escrito antes de nós, basta colocar as definições com mais precisão - e aqui está, a interface familiar (embora em preto e branco) na janela do minicom! (No momento, o HAS_SCREEN não é usado - iniciei a matriz dummy_con para alterar o código original no mínimo.)


Depurando no QEMU


Então, depurei tudo em um quadro real e já há algum tempo - nem mesmo às cegas. Mas tudo diminui a velocidade no JTAG - horror! Bem, no final, tudo deve funcionar em hardware real, mas seria bom depurar no QEMU. Após várias experiências, algo acabou sendo uma muleta, mas muito semelhante ao trabalho com uma prancha:


 $ 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) 

Examinamos quais placas o QEMU está pronto para emular. Estou interessado em hardware compatível com 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 

Agora temos um blob da árvore de dispositivos "fixo". Sem alterar a configuração da VM (muletas!), Execute:


 qemu-system-riscv64 \ -M sifive_u -m 1g \ -serial stdio \ -s -S 

-serial stdio redireciona a porta serial para o console, porque as seqüências de escape serão ativamente usadas. As opções -s -S aumentam o gdbserver e criam uma VM para pausar, respectivamente. Você pode baixar o código usando o loader , mas é necessário reiniciar o QEMU sempre.


Você pode conectar 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, tudo funciona mais do que com inteligência!


Princípio geral do trabalho


, , , Memtest86+ btrace , , ( , QEMU):


modo btrace


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


Instrução ilegal


, :


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) { //  ,     return; } if (cur_start == initial_load_addr && (cur_start - low_test_addr) < (cur_end - cur_start) ) { //   " ":   , //           //     ,    ,   //     ... serial_echo_print("FIRST STARTUP RELOCATION...\n"); void *temp_addr = (((uintptr_t)&_end >> 12) + 1) << 12; run_at(temp_addr, 0); } else { // ,    --- ,  . serial_echo_print("FINAL STARTUP RELOCATION...\n"); run_at(low_test_addr, 0); } } 

, — , 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 !)

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


All Articles