[linux-riscv] Fazendo Hello World
Na parte anterior fizemos nosso programa RISC-V apenas sair. Agora vamos fazer algo útil: escrever Olá, mundo! no terminal.
A syscall write no RISC-V
A syscall write no RISC-V Linux tem o número 64 e espera três argumentos:
- a0 = file descriptor (1 = stdout)
- a1 = ponteiro para o buffer (a string)
- a2 = quantidade de bytes a escrever
.section .rodata
helloworld:
.ascii "Ola, mundo!\n" # 12 bytes
# Não tem terminador nulo automático!
helloworld_end: # label para calcular o tamanho
No GNU assembler,
.asciicria uma string sem terminador nulo. Se quisesse com terminador, usaria.asciz. Mas no nosso tutorial usaremos.ascii+ tamanho explícito (como odbdo NASM).
Calculamos o tamanho com subtração de labels:
li a7, 64 # syscall write
li a0, 1 # fd = stdout
la a1, helloworld # ponteiro para a string
li a2, 12 # tamanho = 12 bytes
ecall
laé uma pseudo-instrução que carrega o endereço de um label. Ela é expandida pelo assembler emlui+addi(ouauipc+addidependendo da distância).
Juntando com o exit
.section .text
.global _start
_start:
# write(stdout, helloworld, 12)
li a7, 64 # syscall write
li a0, 1 # stdout
la a1, helloworld
li a2, 12
ecall
# exit(0)
li a7, 93 # syscall exit
li a0, 0
ecall
.section .rodata
helloworld:
.ascii "Ola, mundo!\n"
Compile e teste:
make clean
make
make run
# Deve imprimir: Ola, mundo!
Funções em RISC-V
Assim como no x86, podemos criar funções com call/ret. No RISC-V, usamos jal (jump and link) e jalr para chamadas e ret para retorno.
jal salva o endereço de retorno no registrador ra (x1) e salta para o label. ret é uma pseudo-instrução que expande para jalr x0, ra, 0.
Vamos criar uma função imprime:
.section .text
.global _start
imprime:
# Sempre escreve helloworld no stdout
li a7, 64
li a0, 1
la a1, helloworld
li a2, 12
ecall
ret
_start:
jal imprime # chama imprime (salva retorno em ra)
jal imprime # chama de novo
jal imprime # e de novo
li a7, 93
li a0, 0
ecall
.section .rodata
helloworld:
.ascii "Ola, mundo!\n"
Diferença x86 vs RISC-V: No x86 usamos
call/ret. No RISC-V usamosjal/jalr/ret. A pseudo-instruçãoreté equivalente ajalr x0, 0(ra)(oujalr zero, ra, 0).
Contando caracteres dinamicamente
No x86 usamos lodsb para carregar bytes sequencialmente. RISC-V não tem essa instrução — precisamos fazer manualmente com lb (load byte).
Primeiro, adicionamos um terminador nulo na string (usando .byte):
helloworld:
.ascii "Ola, mundo!\n"
.byte 0 # terminador nulo
Agora criamos um contador de string:
# Função: conta_str
# Entrada: a0 = ponteiro da string
# Saída: a0 = número de caracteres (sem o terminador nulo)
conta_str:
mv t0, a0 # t0 = ponteiro da string
li t1, 0 # t1 = contador
1:
lb t2, 0(t0) # carrega byte em t2
beqz t2, 2f # se for 0, terminou
addi t1, t1, 1 # contador++
addi t0, t0, 1 # avança ponteiro
j 1b # volta pro loop
2:
mv a0, t1 # retorna o contador em a0
ret
Algumas novidades do RISC-V aqui:
-
mv rd, rs— pseudo-instrução para copiar registrador (expande paraaddi rd, rs, 0). -
lb t2, 0(t0)— carrega um byte do endereço emt0+ offset 0 parat2. -
beqz rs, label— branch if equal to zero (pseudo parabeq rs, x0, label). -
addi rd, rs, imm— soma imediato. -
j label— jump incondicional. - Labels numéricos
1:e referências1b(backward) /1f(forward) — conveniente para loops locais!
RISC-V não tem registrador de flags! Enquanto o x86 usa
cmp+jz(baseado na flag ZF), o RISC-V compara dois registradores diretamente combeq(branch equal),bne(branch not equal),blt(less than), etc.
Função imprime genérica
Vamos criar uma função imprime que recebe uma string qualquer em a0:
imprime:
# Salva registradores que vamos usar
addi sp, sp, -32 # reserva 32 bytes para manter sp alinhado em 16 bytes
sd ra, 24(sp) # salva ra (porque vamos chamar conta_str)
sd s0, 16(sp) # salva s0
sd s1, 8(sp) # salva s1
mv s0, a0 # s0 = ponteiro da string
call conta_str # a0 = tamanho da string
mv s1, a0 # s1 = tamanho
li a7, 64 # syscall write
li a0, 1 # stdout
mv a1, s0 # buffer = string
mv a2, s1 # tamanho
ecall
ld ra, 24(sp) # restaura ra
ld s0, 16(sp) # restaura s0
ld s1, 8(sp) # restaura s1
addi sp, sp, 32 # libera a stack
ret
Aqui está a maior diferença: RISC-V não tem
push/pop! Gerenciamos a stack manualmente:
addi sp, sp, -32— move o stack pointer para baixo (cresce para baixo) mantendo alinhamento de 16 bytes.sd reg, offset(sp)— salva registrador na stack (store doubleword, 8 bytes).ld reg, offset(sp)— restaura registrador da stack (load doubleword).addi sp, sp, 32— restaura o stack pointer.
Convenção de chamada RISC-V
No RISC-V, os registradores são divididos em:
| Registrador | Nome ABI | Preservado? | Uso |
|---|---|---|---|
| x1 | ra | Não (caller-saved) | Endereço de retorno |
| x2 | sp | Sim (callee-saved) | Stack pointer |
| x5-x7 | t0-t2 | Não | Temporários |
| x8 | s0/fp | Sim | Salvo / frame pointer |
| x9 | s1 | Sim | Salvo |
| x10-x17 | a0-a7 | Não | Argumentos de função |
| x18-x27 | s2-s11 | Sim | Salvos |
| x28-x31 | t3-t6 | Não | Temporários |
Registradores s0-s11 devem ser preservados pela função chamada (se forem usados). Registradores t0-t6 e a0-a7 podem ser livremente alterados. Como usamos s0 e s1, salvamos eles na stack.
Código completo
.section .text
.global _start
conta_str:
mv t0, a0
li t1, 0
1:
lb t2, 0(t0)
beqz t2, 2f
addi t1, t1, 1
addi t0, t0, 1
j 1b
2:
mv a0, t1
ret
imprime:
addi sp, sp, -32
sd ra, 24(sp)
sd s0, 16(sp)
sd s1, 8(sp)
mv s0, a0
call conta_str
mv s1, a0
li a7, 64
li a0, 1
mv a1, s0
mv a2, s1
ecall
ld ra, 24(sp)
ld s0, 16(sp)
ld s1, 8(sp)
addi sp, sp, 32
ret
_start:
la a0, helloworld
call imprime
la a0, msg2
call imprime
li a7, 93
li a0, 0
ecall
.section .rodata
helloworld:
.ascii "Ola, mundo!\n"
.byte 0
msg2:
.ascii "QQ eu to fazendo aqui?\n"
.byte 0
Compile e teste:
make clean
make
make run
Saída esperada:
Ola, mundo!
QQ eu to fazendo aqui?
Salvando o progresso
git add helloworld.S
git commit -am "Hello World no RISC-V! Funcao imprime generica e contador de strings"
Conclusão da segunda parte
Já dominamos o básico do RISC-V: syscalls, funções com call/ret, stack manual, e contagem de strings. A sintaxe é diferente do x86, mas os conceitos são os mesmos — uma vez que você entende uma arquitetura, migrar para outra é questão de aprender a nova “sintaxe”!
Na próxima parte: sockets e TCP no RISC-V!