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:

  1. Enviar os headers fixos (sem Content-Length)
  2. Contar e enviar o corpo HTML
  3. Usar Connection: close para 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_tudo faz apenas uma syscall write. Para respostas pequenas isso funciona bem no nosso teste local, mas um servidor de produção deveria verificar o retorno de write e 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 de localhost.

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:

  1. Adicionar mais rotasGET /about retornando uma página “Sobre”, GET /time retornando a hora do servidor
  2. Servir arquivos do disco — usar a syscall open (id 2) para abrir arquivos e sendfile (id 40) para enviá-los
  3. Logging — imprimir no terminal cada requisição recebida (IP, método, caminho, código de resposta)
  4. Parser de HTTP mais robusto — extrair headers, tratar POST, lidar com query strings
  5. Multi-threading — usar clone (id 56) para atender múltiplos clientes simultaneamente
  6. 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!