[linux-x86] Conclusões do servidor HTTP
Na parte anterior transformamos nosso programa em um servidor HTTP funcional, capaz de responder com HTML e diferenciar entre uma rota válida (200) e uma inválida (404). Porém ele ainda tem um problema grave: só atende um cliente e morre. Vamos resolver isso e dar os toques finais.
Atendendo múltiplas conexões com um loop
A solução é simples: depois de fechar a conexão com o cliente, voltamos para o accept ao invés de sair do programa. Para isso, transformamos o fluxo em um loop:
socket → bind → listen
↓
┌─ accept
│ ↓
│ read (requisição)
│ ↓
│ roteamento (200 ou 404)
│ ↓
│ write (resposta)
│ ↓
│ close (cliente)
│ ↓
└──(volta pro accept)
Em assembly, isso é feito com labels e jmp. Vamos reorganizar nosso _start:
_start:
; Inicialização (socket, bind, listen) — executado uma vez só
mov rax, 41
mov rdi, 2
mov rsi, 1
mov rdx, 0
syscall
mov r12, rax ; R12 = socket fd (servidor)
mov rax, 49
mov rdi, r12
mov rsi, sockaddr_in
mov rdx, 16
syscall
mov rax, 50
mov rdi, r12
mov rsi, 10
syscall
; Loop principal
.proximo_cliente:
; accept — bloqueia até alguém conectar
mov rax, 43
mov rdi, r12
mov rsi, 0
mov rdx, 0
syscall
mov r13, rax ; R13 = fd do cliente
; Tratar requisição
call tratar_cliente
; Fechar cliente e voltar pro accept
mov rax, 3
mov rdi, r13
syscall
jmp .proximo_cliente
Agora a função tratar_cliente contém a lógica de ler a requisição, rotear e responder. O programa nunca vai sair do loop a não ser que seja interrompido (Ctrl+C ou kill).
setsockopt: resolvendo “Address already in use”
Se você testar o servidor, matar com Ctrl+C e rodar de novo imediatamente, provavelmente vai receber um erro do bind. Isso acontece porque o kernel mantém a porta ocupada por alguns segundos após o programa fechar (estado TIME_WAIT do TCP). A solução é usar a syscall setsockopt (id 54) com a opção SO_REUSEADDR:
; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))
mov rax, 54 ; syscall setsockopt
mov rdi, r12 ; socket fd
mov rsi, 1 ; SOL_SOCKET (nível socket)
mov rdx, 2 ; SO_REUSEADDR (reusar endereço)
mov r10, reuse_opt ; ponteiro para o valor (1 = ativar)
mov r8, 4 ; sizeof(int) = 4 bytes
syscall
Precisamos adicionar na .data:
reuse_opt: dd 1 ; valor 1 (ativado) como double-word (4 bytes)
Adicionamos isso logo depois do socket() e antes do bind(). Isso normalmente permite reiniciar o servidor sem esperar o TIME_WAIT.
Para syscalls com mais de 3 argumentos, o 4º argumento vai em R10 e o 5º em R8. Fique atento a isso, pois é fácil errar!
Removendo Content-Length com Connection: close
Na parte anterior calculamos o Content-Length manualmente. Isso funciona, mas é frágil — se alterarmos o HTML, temos que recontar os bytes. Para simplificar, vamos usar a técnica de resposta em duas partes:
- Enviar os headers fixos (sem Content-Length)
- Contar e enviar o corpo HTML
- Usar
Connection: closepara que o navegador saiba que recebeu tudo
Esse é um truque válido do HTTP/1.1: com Connection: close, o navegador sabe que a resposta termina quando a conexão fecha. Assim não precisamos de Content-Length.
Se quisermos fazer direito em uma versão futura, podemos calcular o tamanho do corpo dinamicamente (reaproveitando nosso contador de strings) e injetar no header. A parte fácil é calcular o tamanho:
; Calcular Content-Length dinamicamente
; RDI = ponteiro do corpo HTML
; Retorna: RAX = tamanho
calc_tamanho:
push rdi
xor rax, rax ; zera RAX (contador)
.prox:
cmp byte [rdi], 0
je .fim
inc rax
inc rdi
jmp .prox
.fim:
pop rdi
ret
Mas injetar esse número no meio do header é mais trabalhoso (precisaríamos formatar inteiro para string). Vamos deixar esse aprimoramento para um projeto futuro e usar Connection: close por enquanto.
Código final completo
Assim como na parte anterior,
enviar_tudofaz apenas uma syscallwrite. Para respostas pequenas isso funciona bem no nosso teste local, mas um servidor de produção deveria verificar o retorno dewritee continuar enviando até completar o buffer.
section .text
global _start
enviar_tudo:
; enviar_tudo(fd, buffer, tamanho)
push rdi
push rsi
push rdx
mov rax, 1 ; syscall write
syscall
pop rdx
pop rsi
pop rdi
ret
tratar_cliente:
; Entrada: R13 = fd do cliente
; 1. Ler requisição
mov rax, 0 ; syscall read
mov rdi, r13
mov rsi, buffer
mov rdx, 4096
syscall
; 2. Verificar se é "GET / "
cmp byte [buffer], 'G'
jne .erro_404
cmp byte [buffer + 1], 'E'
jne .erro_404
cmp byte [buffer + 2], 'T'
jne .erro_404
cmp byte [buffer + 4], '/'
jne .erro_404
cmp byte [buffer + 5], ' '
jne .erro_404
.rota_ok:
mov rdi, r13
mov rsi, resposta_200
mov rdx, resposta_200_len
call enviar_tudo
ret
.erro_404:
mov rdi, r13
mov rsi, resposta_404
mov rdx, resposta_404_len
call enviar_tudo
ret
_start:
; 1. socket
mov rax, 41
mov rdi, 2
mov rsi, 1
mov rdx, 0
syscall
mov r12, rax
; 2. setsockopt (SO_REUSEADDR) — evita "Address already in use"
mov rax, 54
mov rdi, r12
mov rsi, 1 ; SOL_SOCKET
mov rdx, 2 ; SO_REUSEADDR
mov r10, reuse_opt ; &opt
mov r8, 4 ; sizeof(opt)
syscall
; 3. bind
mov rax, 49
mov rdi, r12
mov rsi, sockaddr_in
mov rdx, 16
syscall
; 4. listen
mov rax, 50
mov rdi, r12
mov rsi, 10
syscall
; 5. Loop principal: aceitar clientes indefinidamente
.proximo_cliente:
mov rax, 43
mov rdi, r12
mov rsi, 0
mov rdx, 0
syscall
mov r13, rax
call tratar_cliente
mov rax, 3
mov rdi, r13
syscall
jmp .proximo_cliente
section .data
sockaddr_in:
dw 2 ; AF_INET
dw 0x911F ; htons(8081)
dd 0 ; INADDR_ANY
dq 0 ; sin_zero
reuse_opt: dd 1 ; SO_REUSEADDR = ativado (4 bytes)
section .bss
buffer: resb 4096
section .rodata
resposta_404:
db "HTTP/1.1 404 Not Found", 0x0D, 0x0A
db "Content-Type: text/html", 0x0D, 0x0A
db "Connection: close", 0x0D, 0x0A
db 0x0D, 0x0A
db "<html><head><title>404</title></head>", 0x0D, 0x0A
db "<body bgcolor='black'><center>", 0x0D, 0x0A
db "<h1 style='color: red'>404 — Nao achei :(</h1>", 0x0D, 0x0A
db "</center></body></html>", 0x0D, 0x0A
resposta_404_len equ $ - resposta_404
resposta_200:
db "HTTP/1.1 200 OK", 0x0D, 0x0A
db "Content-Type: text/html", 0x0D, 0x0A
db "Connection: close", 0x0D, 0x0A
db 0x0D, 0x0A
db "<html>", 0x0D, 0x0A
db "<head><title>Baixaria HTTP</title></head>", 0x0D, 0x0A
db '<body bgcolor="black">', 0x0D, 0x0A
db '<center><h1 style="color: #00ff00">Ola mundo da baixaria!</h1></center>', 0x0D, 0x0A
db '<hr>', 0x0D, 0x0A
db '<center style="color: #00ff00">asm4noobs — feito em assembly puro no Linux x86-64!</center>', 0x0D, 0x0A
db '</body>', 0x0D, 0x0A
db '</html>', 0x0D, 0x0A
resposta_200_len equ $ - resposta_200
Testando o servidor em loop
Compile, execute, e agora o servidor não sai mais sozinho:
make clean && make && ./helloworld
Abra vários terminais e teste com curl repetidamente, ou abra várias abas no navegador:
# Terminal 1 — servidor rodando
./helloworld
# Terminal 2 — vários curls
curl http://localhost:8081/
curl http://localhost:8081/
curl http://localhost:8081/invalido
Todas as requisições serão atendidas em sequência! Para parar o servidor, use Ctrl+C.
Bônus: tente acessar de outro computador na mesma rede! Use
http://<seu-ip>:8081/no lugar delocalhost.
Resumo do que aprendemos
Da parte 0 à parte 4, saímos de um programa que dava segmentation fault para um servidor web funcional. Aqui está um resumo de todas as syscalls que usamos (Linux x86-64):
| Syscall | ID | Uso no projeto |
|---|---|---|
read |
0 | Ler requisição HTTP do cliente |
write |
1 | Enviar resposta para o cliente e mensagens para o terminal |
close |
3 | Fechar sockets e conexões |
socket |
41 | Criar socket TCP |
accept |
43 | Aguardar e aceitar conexão de cliente |
bind |
49 | Associar socket a uma porta |
listen |
50 | Colocar socket em modo servidor |
setsockopt |
54 | Configurar opções do socket (SO_REUSEADDR) |
exit |
60 | Encerrar o programa corretamente |
E os conceitos de assembly que dominamos:
- Seções (
.text,.data,.rodata,.bss) - Registradores, endereçamento e o modelo x86-64
- Instruções de movimento (
mov,xchg,lodsb) - Instruções aritméticas (
inc,dec,xor,cmp) - Saltos condicionais e incondicionais (
jmp,jz,jnz,je,jne) - Funções (
call/ret) e passagem de argumentos por registradores - Pilha (
push/pop) e preservação de registradores - Labels e cálculo de tamanhos (
equ,$) - Chamadas de sistema e convenção de chamada do kernel Linux
- Estruturas de dados em memória (sockaddr_in)
- Protocolos: TCP e HTTP/1.1
Indo além
Este tutorial cobriu apenas a base. Algumas ideias para expandir o projeto:
-
Adicionar mais rotas —
GET /aboutretornando uma página “Sobre”,GET /timeretornando a hora do servidor -
Servir arquivos do disco — usar a syscall
open(id 2) para abrir arquivos esendfile(id 40) para enviá-los - Logging — imprimir no terminal cada requisição recebida (IP, método, caminho, código de resposta)
- Parser de HTTP mais robusto — extrair headers, tratar POST, lidar com query strings
-
Multi-threading — usar
clone(id 56) para atender múltiplos clientes simultaneamente - Portar para outras plataformas — refazer o mesmo projeto em Windows, MacOSX e ARM64 (confira as outras seções do tutorial!)
Se você criar algo legal, contribua com o projeto! Guia de contribuição.
Salvando pela última vez (por enquanto)
git add helloworld.asm
git commit -am "Servidor HTTP finalizado! Loop, SO_REUSEADDR e um monte de baixaria"
Fim do tutorial Linux x86
Parabéns! Você construiu um servidor web do zero em assembly. Isso é algo que pouquíssimas pessoas podem dizer que fizeram. Você agora entende, em nível de máquina, exatamente o que acontece quando um navegador acessa um site — desde a criação do socket até o envio do HTML.
Agora que tal explorar as outras arquiteturas? Volte para a página inicial e escolha seu próximo desafio!