Neste artigo, exploramos vários conceitos de baixo nível (compilação e layout, tempos de execução primitivos, assembler e muito mais) por meio do prisma da arquitetura RISC-V e de seu ecossistema. Sou desenvolvedor web, não faço nada no trabalho, mas é muito interessante para mim, é daí que veio o artigo! Junte-se a mim nesta jornada agitada para as profundezas do caos de baixo nível.
Primeiro, vamos falar um pouco sobre o RISC-V e a importância dessa arquitetura, configurar a cadeia de ferramentas RISC-V e executar um programa C simples em hardware RISC-V emulado.
Conteúdo
- O que é o RISC-V?
- Configurando ferramentas QEMU e RISC-V
- Olá RISC-V!
- Abordagem ingênua
- Levantando a cortina -v
- Pesquise nossa pilha
- Layout
- Pare com isso!
Hammertime! Tempo de execução!
- Depurar, mas agora de verdade
- O que vem a seguir?
- Opcional
O que é o RISC-V?
O RISC-V é uma arquitetura de conjunto de instruções gratuita. O projeto teve origem na Universidade da Califórnia em Berkeley em 2010. Um papel importante em seu sucesso foi desempenhado pela abertura de código e liberdade de uso, muito diferente de muitas outras arquiteturas. Tome o ARM: para criar um processador compatível, você deve pagar uma taxa antecipada
de US $ 1 milhão a US $ 10 milhões e também pagar royalties de 0,5 a 2% nas vendas . Um modelo livre e aberto torna o RISC-V uma opção atraente para muitos, inclusive para startups que não podem pagar uma licença para um ARM ou outro processador, para pesquisadores acadêmicos e (obviamente) para a comunidade de código aberto.
O rápido crescimento da popularidade do RISC-V não passou despercebido. A ARM
lançou um site que tentou (sem êxito) destacar os supostos benefícios do ARM sobre o RISC-V (o site já está fechado). O projeto RISC-V é suportado por
muitas grandes empresas , incluindo Google, Nvidia e Western Digital.
Configurando ferramentas QEMU e RISC-V
Não podemos executar o código no processador RISC-V até que configuremos o ambiente. Felizmente, isso não requer um processador RISC-V físico; em vez disso, adotamos o
qemu . Siga as
instruções para a instalação do
seu sistema operacional . Eu tenho o MacOS, então basta digitar um comando:
Convenientemente, o
qemu
vem com
várias máquinas prontas para qemu-system-riscv32 -machine
(consulte a opção
qemu-system-riscv32 -machine
).
Em seguida, instale o
OpenOCD para
ferramentas RISC-V e RISC-V.
Faça o download de montagens prontas das ferramentas RISC-V OpenOCD e RISC-V
aqui .
Nós extraímos os arquivos para qualquer diretório, eu tenho
~/usys/riscv
. Lembre-se disso 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
Defina as variáveis de ambiente
RISCV_OPENOCD_PATH
e
RISCV_PATH
para que outros programas possam encontrar nossa cadeia de ferramentas. Isso pode parecer diferente dependendo do sistema operacional e do shell: adicionei os caminhos ao
~/.zshenv
.
Crie um link simbólico em
/usr/local/bin
para esse arquivo executável, para que você possa executá-lo a qualquer momento sem especificar o caminho completo para
~/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>/bin/riscv64-unknown-elf-gcc
.
E pronto, temos um kit de ferramentas RISC-V em funcionamento! Todos os nossos executáveis, como
riscv64-unknown-elf-gcc
,
riscv64-unknown-elf-gdb
,
riscv64-unknown-elf-ld
e outros, estão em
~/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>/bin/
.
Olá RISC-V!
Atualização de 26 de maio de 2019:
Infelizmente, devido a um erro no RISC-V QEMU, o programa freedom-e-sdk 'olá mundo' no QEMU não funciona mais. Um patch foi lançado para resolver esse problema, mas, por enquanto, pule esta seção. Este programa não será necessário nas seções subseqüentes do artigo. Acompanho a situação e atualizo o artigo após corrigir o erro.
Veja este comentário para mais informações.Com as ferramentas configuradas, vamos executar o simples programa RISC-V. Vamos começar clonando o repositório 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 tradição , vamos começar com o programa 'Hello, world' do repositório
freedom-e-sdk
. Usamos o
Makefile
pronto que eles fornecem para compilar este programa no modo de depuração:
make PROGRAM=hello TARGET=sifive-hifive1 CONFIGURATION=debug software
E execute no QEMU:
qemu-system-riscv32 -nographic -machine sifive_e -kernel software/hello/debug/hello.elf Hello, World!
Este é um ótimo começo. Você pode executar outros exemplos no
freedom-e-sdk
. Depois disso, escreveremos e tentaremos depurar nosso próprio programa em C.
Abordagem ingênua
Vamos começar com um programa simples que adiciona infinitamente dois números.
cat add.c int main() { int a = 4; int b = 12; while (1) { int c = a + b; } return 0; }
Queremos executar este programa e a primeira coisa que precisamos compilá-lo para o processador RISC-V.
Isso cria o arquivo
a.out
, cujo padrão é o
gcc
para arquivos executáveis. Agora execute este arquivo no
qemu
:
Escolhemos a máquina
riscv-qemu
qual o
riscv-qemu
veio originalmente .
Agora que nosso programa é executado dentro do QEMU com o servidor GDB no
localhost:1234
, nos conectamos a ele com o cliente RISC-V GDB a partir de um terminal separado:
E nós estamos dentro do GDB!
Este GDB foi configurado como "--host = x86_64-apple-darwin17.7.0 --target = riscv64-unknown-elf". │
Digite "show configuration" para obter detalhes da configuração. │
Para obter instruções sobre relatórios de erros, consulte: │
<http://www.gnu.org/software/gdb/bugs/>. │
Encontre o manual do GDB e outros recursos de documentação on-line em: │
<http://www.gnu.org/software/gdb/documentation/>. │
│
Para obter ajuda, digite "ajuda". │
Digite "palavra apropriada" para procurar comandos relacionados à "palavra" ... │
Lendo símbolos de a.out ... │
(gdb)
Podemos tentar executar os comandos
run
ou
start
do arquivo executável
a.out
no GDB, mas no momento isso não funcionará por um motivo óbvio. Nós compilamos o programa como
riscv64-unknown-elf-gcc
, para que o host seja executado na arquitetura
riscv64
.
Mas há uma saída! Essa situação é uma das principais razões para a existência do modelo cliente-servidor do GDB. Podemos pegar o arquivo executável
riscv64-unknown-elf-gdb
e, em vez de iniciá-lo no host, especificar um destino remoto (servidor GDB). Como você se lembra, nós apenas iniciamos o
riscv-qemu
e nos disse para iniciar o servidor GDB no
localhost:1234
. Basta conectar-se a este servidor:
(gdb) destino remoto: 1234 │
Depuração remota usando: 1234
Agora você pode definir alguns pontos de interrupção:
(gdb) b main Breakpoint 1 at 0x1018e: file add.c, line 2. (gdb) b 5
E, finalmente, especifique GDB
continue
(comando abreviado
c
) até chegarmos ao ponto de interrupção:
(gdb) c Continuing.
Você notará rapidamente que o processo não termina de forma alguma. Isso é estranho ... não devemos chegar imediatamente ao ponto de interrupção
b 5
? O que aconteceu

Aqui você pode ver vários problemas:
- A interface do usuário do texto não pode encontrar a fonte. A interface deve exibir nosso código e quaisquer pontos de interrupção próximos.
- O GDB não vê a linha de execução atual (
L??
) e exibe o contador 0x0 ( PC: 0x0
).
- Algum texto na linha de entrada, que na sua totalidade se parece com isso:
0x0000000000000000 in ?? ()
0x0000000000000000 in ?? ()
Combinado com o fato de que não podemos alcançar o ponto de interrupção, esses indicadores indicam: fizemos
algo errado. Mas o que?
Levantando a cortina -v
Para entender o que está acontecendo, você precisa dar um passo atrás e falar sobre como nosso simples programa C realmente funciona. A função
main
faz uma adição simples, mas o que é realmente? Por que deveria ser chamado de
main
, não de
origin
ou
begin
? De acordo com a convenção, todos os arquivos executáveis começam a ser executados com a função
main
, mas que mágica fornece esse comportamento?
Para responder a essas perguntas, repita nossa equipe do GCC com o sinalizador
-v
para obter uma saída mais detalhada do que realmente está acontecendo.
riscv64-unknown-elf-gcc add.c -O0 -g -v
Como a saída é grande, não exibiremos a listagem inteira. É importante observar que, embora o GCC seja formalmente um compilador, o padrão é compilar (para limitar-se à compilação e montagem, você deve especificar o sinalizador
-c
). Por que isso é importante? Bem, dê uma olhada no fragmento da saída detalhada do
gcc
:
# O comando `gcc -v` atual gera caminhos completos, mas esses são bastante
# long, então finja que essas variáveis existem.
# $ RV_GCC_BIN_PATH = / Usuários / twilcock / usys / riscv / riscv64-unknown-elf-gcc- <data> - <versão> / 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'
Entendo que mesmo de forma abreviada isso é muito, então deixe-me explicar. Na primeira linha, o
gcc
executa o programa
collect2
, transmite os argumentos
crt0.o
,
crtbegin.o
e
crtend.o
, os
-lgcc
e
--start-group
. A descrição do collect2 pode ser encontrada
aqui : em resumo, o collect2 organiza várias funções de inicialização na inicialização, fazendo o layout em uma ou mais passagens.
Assim, o GCC compila vários arquivos
crt
com o nosso código. Como você pode imaginar,
crt
significa 'C runtime'.
Aqui está descrito em detalhes para que cada
crt
destina, mas estamos interessados em
crt0
, que faz uma coisa importante:
"É esperado que este objeto [crt0] contenha o caractere _start
, que indica a autoinicialização do programa."
A essência do “bootstrap” depende da plataforma, mas geralmente envolve tarefas importantes, como configurar um quadro de pilha, passar argumentos de linha de comando e chamar
main
. Sim,
finalmente encontramos a resposta para a pergunta: é
_start
chama nossa função principal!
Pesquise nossa pilha
Resolvemos um enigma, mas como isso nos aproxima do objetivo original - executar um programa C simples em
gdb
? Resta resolver vários problemas: o primeiro deles está relacionado a como o
crt0
configura nossa pilha.
Como vimos acima, o padrão do
gcc
é o
crt0
. Os parâmetros padrão são selecionados com base em vários fatores:
- Objetivo trigêmeo correspondente à estrutura do sistema
machine-vendor-operatingsystem
. Nós temos riscv64-unknown-elf
- Arquitetura de destino,
rv64imafdc
- Alvo ABI,
lp64d
Geralmente tudo funciona bem, mas não para todos os processadores RISC-V. Como mencionado anteriormente, uma das tarefas do
crt0
é configurar a pilha. Mas ele não sabe exatamente onde a pilha deve estar para nossa CPU (
-machine
)? Ele não pode fazer isso sem a nossa ajuda.
No
qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -kernel a.out
usamos a máquina
virt
. Felizmente, o
qemu
facilita o despejo de informações da máquina em um despejo
dtb
(blob da árvore de dispositivos).
É difícil ler dados dtb porque é basicamente um formato binário, mas existe um utilitário de linha de comando
dtc
(compilador de árvore de dispositivos) que pode converter o arquivo em algo mais legível.
O arquivo de saída é
riscv64-virt.dts
, onde vemos muitas informações interessantes sobre
virt
: o número de núcleos de processador disponíveis, a localização da memória de vários dispositivos periféricos, como UART, a localização da memória interna (RAM). A pilha deve estar nessa memória, então procure-a com
grep
:
grep memory riscv64-virt.dts -A 3 memory@80000000 { device_type = "memory"; reg = <0x00 0x80000000 0x00 0x8000000>; };
Como você pode ver, este nó possui 'memória' especificada como
device_type
. Aparentemente, encontramos o que estávamos procurando. Pelos valores dentro de
reg = <...> ;
Você pode determinar onde o banco de memória é iniciado e qual o seu comprimento.
Na
especificação devicetree, vemos que a sintaxe
reg
é um número arbitrário de pares
(base_address, length)
. No entanto, existem quatro significados dentro de
reg
. Estranho, não existem dois valores suficientes para um banco de memória?
Novamente, a partir da especificação devicetree (procure a propriedade
reg
), descobrimos que o número de células
<u32>
para especificar o endereço e o comprimento é determinado pelas propriedades
#address-cells
e
#size-cells
no nó pai (ou no próprio nó). Esses valores não são especificados em nosso nó de memória e o nó de memória pai é simplesmente a raiz do arquivo. Vamos procurar por esses valores:
head -n8 riscv64-virt.dts /dts-v1/; / { #address-cells = <0x02>; #size-cells = <0x02>; compatible = "riscv-virtio"; model = "riscv-virtio,qemu";
Acontece que o endereço e o comprimento exigem dois valores de 32 bits. Isso significa que com
reg = <0x00 0x80000000 0x00 0x8000000>;
nossa memória começa
0x00 + 0x80000000 (0x80000000)
e ocupa
0x00 + 0x8000000 (0x8000000)
bytes, ou seja, termina em
0x88000000
, o que corresponde a 128 megabytes.
Layout
Usando
qemu
e
dtc
encontramos os endereços de RAM na máquina virtual virtual. Também sabemos que o
gcc
compõe
crt0
por padrão, sem configurar a pilha conforme necessário. Mas como usar essas informações para eventualmente executar e depurar o programa?
Como
crt0
não nos convém, existe uma opção óbvia: escreva seu próprio código e componha-o com o arquivo de objeto que obtivemos após compilar nosso programa simples. Nosso
crt0
precisa saber onde a parte superior da pilha é iniciada para inicializá-la corretamente. Poderíamos
crt0
valor
0x80000000
diretamente para
crt0
, mas essa não é uma solução muito adequada, levando em consideração as alterações que possam ser necessárias no futuro. E se quisermos usar outra CPU, como
sifive_e
, com características diferentes no emulador?
Felizmente, não somos os primeiros a fazer essa pergunta e já existe uma boa solução. O vinculador GNU
ld
permite definir o caractere disponível em nosso
crt0
. Podemos definir o símbolo
__stack_top
adequado para diferentes processadores.
Em vez de escrever seu próprio arquivo vinculador do zero, faz sentido pegar o script padrão com
ld
e modificá-lo um pouco para suportar caracteres adicionais. O que é um script vinculador?
Aqui está uma boa descrição :
O principal objetivo do script do vinculador é descrever como as seções do arquivo são correspondidas na entrada e na saída e controlar o layout da memória do arquivo de saída.
Sabendo disso, vamos copiar o script do vinculador padrão
riscv64-unknown-elf-ld
para um novo arquivo:
cd ~/usys/riscv
Este arquivo possui
muitas informações interessantes, muito mais do que podemos discutir neste artigo. Saída detalhada com a
--Verbose
inclui informações sobre a versão
ld
, arquiteturas suportadas e muito mais. É bom saber tudo isso, mas essa sintaxe é inaceitável no script do vinculador, portanto, abra um editor de texto e exclua tudo o que é supérfluo do arquivo.
vim riscv64-virt.ld
# Remova tudo acima e incluindo a linha =============
GNU ld (GNU Binutils) 2.32
Emulações suportadas:
elf64lriscv
elf32lriscv
usando script vinculador interno:
=====================================================
/ * Script para -z combreloc: combina e ordena seções de realocação * /
/ * Copyright (C) 2014-2019 Free Software Foundation, Inc.
A cópia e distribuição deste script, com ou sem modificação,
são permitidos em qualquer meio sem royalties, desde que os direitos autorais
aviso e este aviso são preservados. * /
OUTPUT_FORMAT ("elf64-littleriscv", "elf64-littleriscv",
"elf64-littleriscv")
... restante do script do vinculador ...
Depois disso, execute o comando
MEMORY para determinar manualmente onde
__stack_top
estará. Localize a linha que começa com
OUTPUT_ARCH(riscv)
, ela deve estar na parte superior do arquivo e adicione o comando
MEMORY
abaixo:
OUTPUT_ARCH(riscv) /* >>> Our addition. <<< */ MEMORY { /* qemu-system-risc64 virt machine */ RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 128M } /* >>> End of our addition. <<< */ ENTRY(_start)
Criamos um bloco de memória chamado
RAM
, para o qual a leitura (
r
), a gravação (
w
) e o armazenamento do código executável (
x
) são permitidos.
Ótimo, definimos um layout de memória que corresponde às especificações de nossa máquina
virt
RISC-V. Agora você pode usá-lo. Queremos colocar nossa pilha na memória.
Você precisa definir o caractere
__stack_top
. Abra o script do vinculador (
riscv64-virt.ld
) em um editor de texto e adicione algumas linhas:
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 você pode ver, definimos
__stack_top
usando
o comando PROVIDE . O símbolo estará acessível a partir de qualquer programa associado a esse script (supondo que o próprio programa não determine algo com o nome
__stack_top
). Defina
__stack_top
como
ORIGIN(RAM)
. Sabemos que esse valor é
0x80000000
mais
LENGTH(RAM)
, que é de 128 megabytes (
0x8000000
bytes). Isso significa que nosso
__stack_top
definido como
0x88000000
.
Por uma questão de brevidade, não listarei o arquivo vinculador inteiro
aqui ; você pode vê-lo
aqui .
Pare com isso! Hammertime! Tempo de execução!
Agora, temos tudo o que precisamos para criar nosso próprio tempo de execução C. Na verdade, essa é uma tarefa bastante simples, eis o arquivo
crt0.s
inteiro:
.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
Atrai imediatamente um grande número de linhas que começam com um ponto. Este é um arquivo para o assembler
as
. Linhas com pontos são chamadas
diretivas assembler : fornecem informações para assembler. Este não é um código executável, como as instruções do RISC-V assembler, como
jal
e
add
.
Vamos analisar o arquivo linha por linha. Trabalharemos com vários registradores RISC-V padrão; portanto, confira
esta tabela , que abrange todos os registradores e sua finalidade.
.section .init, "ax"
Conforme indicado no
manual do GNU assembler 'as' , esta linha diz ao assembler para inserir o seguinte código na seção
.init
, que é alocada (
a
) e executável (
x
). Esta seção é outra
convenção comum para a execução de código no sistema operacional. Trabalhamos em hardware puro sem sistema operacional, portanto, no nosso caso, essas instruções podem não ser absolutamente necessárias, mas, em qualquer caso, é uma boa prática.
.global _start _start:
.global
disponibiliza o seguinte caractere para
ld
. Sem isso, o link não funcionará, porque o
ENTRY(_start)
no script do vinculador aponta para o símbolo
_start
como o ponto de entrada para o arquivo executável. A próxima linha informa ao assembler que estamos iniciando a definição do caractere
_start
.
_start: .cfi_startproc .cfi_undefined ra ...other stuff... .cfi_endproc
Essas diretivas
.cfi
informam sobre a estrutura do quadro e como lidar com ele. As
.cfi_startproc
e
.cfi_endproc
sinalizam o início e o fim de uma função, e
.cfi_undefined ra
informa ao assembler que o registro
ra
não deve ser restaurado para qualquer valor que ele contenha antes do
_start
.
.option push .option norelax la gp, __global_pointer$ .option pop
Essas diretivas
.option
alteram o comportamento do assembler de acordo com o código quando você precisa aplicar um conjunto específico de opções.
Aqui está uma descrição detalhada de por que o uso de
.option
neste segmento é importante:
... como possivelmente relaxamos o endereçamento de seqüências para sequências mais curtas em relação ao GP, o carregamento inicial do GP não deve ser enfraquecido e deve ser algo como isto:
.option push .option norelax la gp, __global_pointer$ .option pop
para que, após o relaxamento, você obtenha o seguinte código:
auipc gp, %pcrel_hi(__global_pointer$) addi gp, gp, %pcrel_lo(__global_pointer$)
em vez de simples:
addi gp, gp, 0
E agora a última parte do nosso
crt0.s
:
_start: ...other stuff... la sp, __stack_top add s0, sp, zero jal zero, main .cfi_endproc .end
Aqui podemos finalmente usar o símbolo
__stack_top
, que trabalhamos duro para criar.
A pseudo-instrução la
(endereço de carregamento) carrega o valor
__stack_top
no registrador
sp
(ponteiro da pilha), configurando-o para uso no restante do programa.
Em seguida,
add s0, sp, zero
adiciona os valores dos registradores
sp
e
zero
(que na verdade é o registrador
x0
com uma referência rígida a 0) e coloca o resultado no registrador
s0
. Este é um
registro especial que é incomum em vários aspectos. Primeiro, é um “registro persistente”, ou seja, é salvo quando chamadas de função. Em segundo lugar,
s0
às vezes atua como um ponteiro de quadro, o que concede a cada função um pequeno espaço na pilha para armazenar os parâmetros passados para essa função. Como as chamadas de função funcionam com os ponteiros de pilha e quadro é um tópico muito interessante que você pode dedicar facilmente a um artigo separado, mas, por enquanto, saiba que em nosso tempo de execução é importante inicializar o ponteiro de quadro
s0
.
Em seguida, vemos o
jal zero, main
declaração
jal zero, main
. Aqui
jal
significa pular e vincular. A instrução espera operandos na forma de
jal rd (destination register), offset_address
. Funcionalmente,
jal
grava o valor da próxima instrução (registrador
pc
mais quatro) em
rd
e, em seguida, define o registro
pc
valor atual do
pc
mais o endereço de deslocamento com
extensão de sinal , efetivamente "chamando" esse endereço.
Como mencionado acima,
x0
fortemente vinculado ao valor literal 0 e a gravação nele é inútil.
Portanto, pode parecer estranho usarmos um registro como o registro de destino zero
, que os montadores do RISC-V interpretam como um registro x0
. Afinal, isso significa uma transição incondicional para offset_address
. Por que fazer isso, porque em outras arquiteturas há uma instrução explícita para uma transição incondicional?Esse padrão estranho jal zero, offset_address
é realmente otimização inteligente. O suporte para cada nova instrução significa um aumento e, consequentemente, um aumento no custo do processador. Portanto, quanto mais simples o ISA, melhor. Em vez de poluir o espaço de instruções com duas instruções jal
e unconditional jump
, a arquitetura RISC-V suporta apenas jal
, e saltos incondicionais são suportados jal zero, main
.O RISC-V possui muitas dessas otimizações, a maioria das quais toma a forma das chamadas pseudo - instruções . Os montadores sabem como traduzi-los em instruções reais de hardware. Por exemplo, j offset_address
montadores RISC-V convertem pseudo- instruções para saltos incondicionais em jal zero, offset_address
. Para obter uma lista completa das pseudo instruções oficialmente suportadas , consulte a especificação RISC-V (versão 2.2) . _start: ...other stuff... jal zero, main .cfi_endproc .end
Nossa última linha é a diretiva assembler .end
, que simplesmente marca o final do arquivo.Depurar, mas agora de verdade
Tentando depurar um programa C simples em um processador RISC-V, resolvemos muitos problemas. Primeiro, usei qemu
e dtc
encontrei nossa memória na máquina virtual virt
RISC-V. Em seguida, usamos essas informações para controlar manualmente a alocação de memória em nossa versão do script padrão do vinculador riscv64-unknown-elf-ld
, o que nos permitiu determinar com precisão o símbolo __stack_top
. Em seguida, use este símbolo em sua própria versão crt0.s
, que define os nossos índices de pilha e globais, e, finalmente, chamou a função main
. Agora você pode atingir seu objetivo e começar a depurar nosso programa simples no GDB.Lembre-se aqui é o próprio programa C: cat add.c int main() { int a = 4; int b = 12; while (1) { int c = a + b; } return 0; }
Compilando e vinculando: riscv64-unknown-elf-gcc -g -ffreestanding -O0 -Wl,--gc-sections -nostartfiles -nostdlib -nodefaultlibs -Wl,-T,riscv64-virt.ld crt0.s add.c
Aqui indicamos muito mais sinalizadores do que da última vez, então vamos ver os que não descrevemos antes.-ffreestanding
informa ao compilador que a biblioteca padrão pode não existir , portanto, não há necessidade de fazer suposições sobre sua presença obrigatória. Esse parâmetro não é necessário ao iniciar o aplicativo em seu host (no sistema operacional), mas, neste caso, não é, portanto, é importante informar o compilador dessas informações.-Wl
- Uma lista de sinalizadores separados por vírgula para passar ao vinculador ( ld
). Aqui, --gc-sections
significa "seções de coleta de lixo" e ld
é instruído a remover seções não utilizadas após a vinculação. Bandeiras -nostartfiles
, -nostdlib
e -nodefaultlibs
o ligador não processa os arquivos de inicialização do sistema padrão (por exemplo, padrãocrt0
), implementações padrão do sistema stdlib e bibliotecas vinculadas padrão do sistema padrão. Como temos nosso próprio script crt0
e vinculador, é importante passar esses sinalizadores para que os valores padrão não entrem em conflito com nossas preferências do usuário.-T
indica o caminho para o script do vinculador, o que é simples no nosso caso riscv64-virt.ld
. Por fim, especificamos os arquivos que queremos compilar, compilar e compor: crt0.s
e add.c
. Como antes, o resultado é um arquivo completo e pronto para executar, chamado a.out
.Agora execute o nosso novo executável novíssimo em qemu
:
Agora execute gdb
, lembre-se de carregar os símbolos de depuração a.out
, especificando-o com o ú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)
Em seguida, conecte nosso cliente gdb
ao servidor gdb
que lançamos como parte do comando qemu
: (gdb) target remote :1234 │ Remote debugging using :1234
Defina um ponto de interrupção no principal: (gdb) b main Breakpoint 1 at 0x8000001e: file add.c, line 2.
E inicie o programa: (gdb) c Continuing. Breakpoint 1, main () at add.c:2
A partir da saída fornecida, é claro que atingimos com sucesso o ponto de interrupção na linha 2! Isso também é visível na interface de texto, finalmente temos a linha correta L
, o valor PC:
é L2
e PC:
- 0x8000001e
. Se você fez tudo como no artigo, a saída será mais ou menos assim: a
partir de agora, você pode usá-lo gdb
como de costume: -s
para ir para a próxima instrução, info all-registers
para verificar os valores dentro dos registros enquanto o programa é executado, etc. Experimente o seu prazer ... nós, é claro , trabalhou muito para isso!O que vem a seguir?
Hoje conseguimos muito e, espero, aprendemos muito! Eu nunca tive um plano formal para este e outros artigos, apenas segui o que era mais interessante para mim a cada momento. Portanto, não tenho certeza do que acontecerá a seguir. Eu gostei especialmente da imersão profunda nas instruções jal
, portanto, talvez no próximo artigo tomemos como base o conhecimento adquirido aqui, mas substitua-o por add.c
algum programa em puro montador RISC-V. Se você tiver algo específico que gostaria de ver ou tiver alguma dúvida, abra tickets .Obrigado pela leitura! Espero encontrar no próximo artigo!Opcional
Se você gostou do artigo e quer saber mais, confira a apresentação de Matt Godbolt intitulada “Bits entre bits: como entramos no main ()” da conferência CppCon2018. Ela aborda o assunto um pouco diferente do que estamos aqui. Realmente boa palestra, veja por si mesmo!