[linux-riscv] Aceitando uma conexão TCP
Na parte anterior criamos funções reutilizáveis e aprendemos a gerenciar a stack manualmente no RISC-V. Agora vamos fazer nosso programa conversar pela rede!
Syscalls de rede no RISC-V
As syscalls de socket no RISC-V são as mesmas do asm-generic (comuns a várias arquiteturas modernas):
| Syscall | ID RISC-V | O que faz |
|---|---|---|
socket |
198 | Cria um socket |
bind |
200 | Associa a uma porta |
listen |
201 | Modo escuta |
accept |
202 | Aceita conexão |
setsockopt |
208 | Configura opções |
A convenção de chamada é a mesma: número da syscall em a7, argumentos em a0-a5, retorno em a0.
Estrutura sockaddr_in
Montamos a estrutura no .data:
.section .data
sockaddr_in:
.hword 2 # sin_family = AF_INET (IPv4) — halfword = 2 bytes
.hword 0x911F # sin_port = htons(8081) — bytes invertidos
.word 0 # sin_addr.s_addr = INADDR_ANY = 0.0.0.0 — word = 4 bytes
.dword 0 # sin_zero — padding 8 bytes
.hword= 2 bytes,.word= 4 bytes,.dword= 8 bytes no RISC-V 64. O mesmo conceito de network byte order do tutorial x86 se aplica:8081 = 0x1F91→ armazenamos0x911Fpara ficar em big-endian na memória.
Criando uma função enviar genérica
Vamos criar uma função que envia dados por qualquer fd. Diferente do x86, não faremos a contagem de strings dentro dela — receberemos o tamanho diretamente. Isso simplifica o código:
# enviar(fd, buffer, tamanho)
# a0 = fd, a1 = buffer, a2 = tamanho
enviar:
li a7, 64 # syscall write
ecall
ret
Simples assim! Como os argumentos já estão em a0, a1, a2 (que são exatamente o que write espera), nem precisamos mover registradores.
Nosso programa completo
.section .text
.global _start
enviar:
# a0=fd, a1=buffer, a2=tamanho
li a7, 64
ecall
ret
_start:
# 1. socket(AF_INET, SOCK_STREAM, 0)
li a7, 198 # syscall socket
li a0, 2 # AF_INET = IPv4
li a1, 1 # SOCK_STREAM = TCP
li a2, 0 # protocolo = 0
ecall
mv s0, a0 # s0 = socket fd
# 2. bind(socket_fd, &addr, 16)
li a7, 200 # syscall bind
mv a0, s0 # socket fd
la a1, sockaddr_in # ponteiro da struct
li a2, 16 # sizeof(sockaddr_in)
ecall
# 3. listen(socket_fd, 10)
li a7, 201 # syscall listen
mv a0, s0 # socket fd
li a1, 10 # backlog
ecall
# 4. accept(socket_fd, NULL, NULL)
li a7, 202 # syscall accept
mv a0, s0 # socket fd
li a1, 0 # addr = NULL
li a2, 0 # addrlen = NULL
ecall
mv s1, a0 # s1 = fd do cliente
# 5. Enviar mensagem ao cliente
mv a0, s1 # fd do cliente
la a1, mensagem # buffer
li a2, 22 # "Ola da baixaria TCP!\r\n" = 22 bytes
call enviar
# 6. Fechar cliente
li a7, 57 # syscall close
mv a0, s1 # fd do cliente
ecall
# 7. Fechar socket servidor
li a7, 57 # syscall close
mv a0, s0 # fd do servidor
ecall
# 8. Sair
li a7, 93 # syscall exit
li a0, 0
ecall
.section .data
sockaddr_in:
.hword 2 # AF_INET
.hword 0x911F # htons(8081)
.word 0 # INADDR_ANY
.dword 0 # sin_zero
mensagem:
.ascii "Ola da baixaria TCP!\r\n"
# CR (\r) = 0x0D, LF (\n) = 0x0A
Atenção ao
.asciivs NASM: No GNU assembler você pode usar\re\ndiretamente! Bem mais conveniente que0x0D, 0x0A.
Calculando o tamanho da mensagem
Assim como no x86, podemos usar subtração de labels para calcular o tamanho da mensagem automaticamente:
mensagem:
.ascii "Ola da baixaria TCP!\r\n"
mensagem_fim:
E usar no código:
la a1, mensagem # ponteiro da mensagem
la t0, mensagem_fim # ponteiro para logo depois da mensagem
sub a2, t0, a1 # tamanho = mensagem_fim - mensagem
No GNU assembler, um label também pode marcar o endereço logo depois de uma string. Subtraindo mensagem de mensagem_fim, temos o tamanho exato em bytes. Fazemos essa subtração em runtime porque os dois endereços são resolvidos pelo linker sem depender da ordem das seções no arquivo.
Código final com tamanho automático
.section .text
.global _start
enviar:
li a7, 64
ecall
ret
_start:
# socket
li a7, 198
li a0, 2
li a1, 1
li a2, 0
ecall
mv s0, a0
# bind
li a7, 200
mv a0, s0
la a1, sockaddr_in
li a2, 16
ecall
# listen
li a7, 201
mv a0, s0
li a1, 10
ecall
# accept
li a7, 202
mv a0, s0
li a1, 0
li a2, 0
ecall
mv s1, a0
# enviar
mv a0, s1
la a1, mensagem
la t0, mensagem_fim
sub a2, t0, a1
call enviar
# close(cliente)
li a7, 57
mv a0, s1
ecall
# close(servidor)
li a7, 57
mv a0, s0
ecall
# exit
li a7, 93
li a0, 0
ecall
.section .data
sockaddr_in:
.hword 2
.hword 0x911F
.word 0
.dword 0
mensagem:
.ascii "Ola da baixaria TCP!\r\n"
mensagem_fim:
Testando
Compile, execute com QEMU e conecte com netcat:
make clean
make
# Executa em background com QEMU
qemu-riscv64 ./helloworld &
pid=$!
sleep 0.5
# Conecta com netcat
nc localhost 8081
# Deve imprimir: Ola da baixaria TCP!
kill $pid 2>/dev/null
Importante: O QEMU em modo usuário faz bridge da rede para o host! As portas que você abrir no programa emulado estarão acessíveis no
localhostda sua máquina.
Se você tem hardware RISC-V nativo, pode simplesmente executar ./helloworld diretamente e testar com nc localhost 8081 em outro terminal.
Salvando o progresso
git add helloworld.S
git commit -am "Socket TCP funcionando no RISC-V! A baixaria agora eh multicore-friendly!"
Conclusão da terceira parte
Já aceitamos conexões TCP no RISC-V! O fluxo é idêntico ao x86, mas com números de syscall diferentes e a sintaxe do GNU assembler. Aprendemos:
- Syscalls de rede no RISC-V (198-208)
-
.hword,.word,.dwordpara estruturas de dados - Cálculo de tamanho com
.(ponto) do GNU assembler - QEMU faz bridge de rede automaticamente
Na próxima parte: transformar isso em um servidor HTTP!