Na parte anterior conseguimos aceitar uma conexão TCP e mandar uma mensagem simples. Mas o objetivo desse tutorial é fazer um servidor web! Então chegou a hora de falarmos o protocolo HTTP e transformarmos nosso programa em um servidor que um navegador consegue entender.

Como funciona o HTTP?

HTTP (HyperText Transfer Protocol) é um protocolo de texto que roda sobre o TCP. A comunicação acontece assim:

  1. O cliente (navegador) conecta no servidor
  2. O cliente envia uma requisição em texto puro
  3. O servidor processa e envia uma resposta em texto puro
  4. A conexão é fechada (ou mantida, no caso de keep-alive)

Uma requisição HTTP típica tem esse formato:

GET / HTTP/1.1\r\n
Host: localhost:8081\r\n
User-Agent: curl/7.68.0\r\n
Accept: */*\r\n
\r\n

A primeira linha é a request line: <método> <caminho> <versão>. Depois vêm os headers (um por linha, formato Chave: Valor), e uma linha em branco (\r\n) indica o fim da requisição.

A resposta HTTP tem estrutura parecida:

HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
Content-Length: 123\r\n
Connection: close\r\n
\r\n
<html>...</html>

Primeiro vem a status line (versão código mensagem), depois os headers de resposta, uma linha em branco, e por fim o corpo da resposta (o HTML).

Lendo a requisição do cliente

A primeira novidade: precisamos ler o que o cliente enviou. No Linux, usamos a syscall read (id 0):

  • RDI = fd de onde ler
  • RSI = ponteiro para o buffer onde guardar os dados
  • RDX = quantos bytes no máximo queremos ler
  • Retorno em RAX = quantos bytes foram lidos (ou negativo se erro)

Vamos criar um buffer na seção .bss (Block Started by Symbol), que é uma seção para dados não inicializados — ocupa espaço na memória mas não no executável:

section .bss
    buffer: resb 4096       ; reserva 4096 bytes (4 KB)

E então chamar o read:

    mov rax, 0          ; syscall read
    mov rdi, r13        ; fd do cliente
    mov rsi, buffer     ; ponteiro do buffer
    mov rdx, 4096       ; até 4096 bytes
    syscall
    ; RAX = número de bytes lidos

Por enquanto, vamos apenas ler a requisição para tirar os dados do socket. Na sequência usaremos os primeiros bytes desse buffer para decidir qual resposta enviar.

Escrevendo a resposta HTTP

Para responder ao navegador, precisamos enviar os headers HTTP seguidos do conteúdo HTML. Vamos criar tudo como strings na seção .rodata:

section .rodata

headers_200:
    db "HTTP/1.1 200 OK", 0x0D, 0x0A
    db "Content-Type: text/html", 0x0D, 0x0A
    db "Connection: close", 0x0D, 0x0A
    db "Content-Length: 251", 0x0D, 0x0A
    db 0x0D, 0x0A    ; linha em branco = fim dos headers
    ; O corpo HTML vem logo em seguida!
    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!</center>', 0x0D, 0x0A
    db '</body>', 0x0D, 0x0A
    db '</html>', 0x0D, 0x0A

headers_200_len equ $ - headers_200    ; calcula o tamanho total enviado pelo write

Repare em alguns detalhes importantes:

  • As quebras de linha no HTTP são \r\n (0x0D + 0x0A, ou CR+LF). Não use apenas \n!
  • O Content-Length diz quantos bytes tem o corpo da resposta (depois da linha em branco). Tem que estar correto, senão o navegador vai ficar esperando mais dados ou cortar o HTML.
  • Usei a diretiva equ para calcular automaticamente quantos bytes serão enviados pelo write. $ significa “posição atual” e subtraindo o label inicial, temos o tamanho exato da resposta inteira (status line + headers + corpo).

Com isso, enviar a resposta completa é trivial:

    mov rdi, r13        ; fd do cliente
    mov rsi, headers_200
    mov rdx, headers_200_len
    call enviar_tudo

Precisamos de uma variação da nossa enviar que aceite um tamanho explícito (já que a resposta HTTP não tem terminador nulo):

Em um servidor robusto, precisaríamos repetir o write até todos os bytes serem enviados, porque sockets podem fazer escritas parciais. Para este exemplo pequeno em localhost, uma única chamada é suficiente para manter o código simples.

enviar_tudo:
    ; enviar_tudo(fd, buffer, tamanho) => RDI=fd, RSI=buffer, RDX=tamanho
    push rdi
    push rsi
    push rdx

    mov rax, 1          ; syscall write
    syscall

    pop rdx
    pop rsi
    pop rdi
    ret

Juntando tudo: o servidor HTTP

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

_start:
    ; 1. socket(AF_INET, SOCK_STREAM, 0)
    mov rax, 41
    mov rdi, 2
    mov rsi, 1
    mov rdx, 0
    syscall
    mov r12, rax

    ; 2. bind(socket_fd, &addr, 16)
    mov rax, 49
    mov rdi, r12
    mov rsi, sockaddr_in
    mov rdx, 16
    syscall

    ; 3. listen(socket_fd, 10)
    mov rax, 50
    mov rdi, r12
    mov rsi, 10
    syscall

    ; 4. accept(socket_fd, NULL, NULL)
    mov rax, 43
    mov rdi, r12
    mov rsi, 0
    mov rdx, 0
    syscall
    mov r13, rax        ; fd do cliente

    ; 5. Ler requisição do cliente
    mov rax, 0          ; syscall read
    mov rdi, r13        ; fd do cliente
    mov rsi, buffer
    mov rdx, 4096
    syscall
    ; Ignoramos o conteúdo da requisição por enquanto

    ; 6. Enviar resposta HTTP
    mov rdi, r13
    mov rsi, headers_200
    mov rdx, headers_200_len
    call enviar_tudo

    ; 7. Fechar conexão com o cliente
    mov rax, 3
    mov rdi, r13
    syscall

    ; 8. Fechar socket servidor
    mov rax, 3
    mov rdi, r12
    syscall

    ; 9. Sair
    mov rax, 60
    mov rdi, 0
    syscall


section .data

sockaddr_in:
    dw 2                ; AF_INET
    dw 0x911F           ; htons(8081)
    dd 0                ; INADDR_ANY
    dq 0                ; sin_zero

section .bss

buffer: resb 4096

section .rodata

headers_200:
    db "HTTP/1.1 200 OK", 0x0D, 0x0A
    db "Content-Type: text/html", 0x0D, 0x0A
    db "Connection: close", 0x0D, 0x0A
    db "Content-Length: 251", 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!</center>', 0x0D, 0x0A
    db '</body>', 0x0D, 0x0A
    db '</html>', 0x0D, 0x0A

headers_200_len equ $ - headers_200

IMPORTANTE: O Content-Length: 251 foi calculado manualmente contando os bytes do HTML (do <html> até </html> incluindo os \r\n). Se você alterar o HTML, recalcule esse valor! Na próxima parte vamos simplificar isso usando Connection: close sem Content-Length.

Testando com o navegador

Compile e execute:

make clean
make
./helloworld

Agora abra seu navegador e acesse:

http://localhost:8081

Você deverá ver uma página preta com o texto verde “Ola mundo da baixaria!” — exatamente o HTML que escrevemos! Dá pra testar com curl também:

curl -v http://localhost:8081

O -v (verbose) mostra os headers da resposta:

< HTTP/1.1 200 OK
< Content-Type: text/html
< Connection: close
< Content-Length: 251
<
<html>
<head><title>Baixaria HTTP</title></head>
...

Adicionando suporte a rotas (404)

Vamos melhorar: ler o caminho da requisição e retornar 404 se não for /. Primeiro, precisamos verificar se a requisição começa com GET / :

    ; Depois do read, verificar o buffer
    cmp dword [buffer], "GET "   ; compara os primeiros 4 bytes com "GET "
    jne .erro_400
    cmp word [buffer + 4], "/ "  ; compara bytes 4-5 com "/ "
    je  .rota_ok

.erro_400:
    ; Retornar 404
    ...
    jmp .fim_resposta

.rota_ok:
    ; Retornar 200 com o HTML
    ...
    jmp .fim_resposta

.fim_resposta:

Atenção com a ordem dos bytes! No NASM, "GET " é armazenado como bytes ‘G’ ‘E’ ‘T’ ‘ ‘ na memória. Como o x86 é little-endian, dword [buffer] lê os bytes na ordem: buffer[0], buffer[1], buffer[2], buffer[3]. A comparação cmp dword [buffer], "GET " funciona porque o NASM inverte automaticamente strings em constantes de múltiplos bytes. Mágico, né?

Se você não confia nessa mágica, podemos fazer o jeito seguro byte a byte:

    mov al, [buffer]
    cmp al, 'G'
    jne .erro_400
    mov al, [buffer + 1]
    cmp al, 'E'
    jne .erro_400
    mov al, [buffer + 2]
    cmp al, 'T'
    jne .erro_400
    mov al, [buffer + 4]
    cmp al, '/'
    jne .erro_400
    mov al, [buffer + 5]
    cmp al, ' '
    jne .erro_400
    ; É um GET / válido!

Implementação completa com rota 404

section .text
global _start

enviar_tudo:
    push rdi
    push rsi
    push rdx
    mov rax, 1
    syscall
    pop rdx
    pop rsi
    pop rdi
    ret

_start:
    mov rax, 41
    mov rdi, 2
    mov rsi, 1
    mov rdx, 0
    syscall
    mov r12, rax

    mov rax, 49
    mov rdi, r12
    mov rsi, sockaddr_in
    mov rdx, 16
    syscall

    mov rax, 50
    mov rdi, r12
    mov rsi, 10
    syscall

    mov rax, 43
    mov rdi, r12
    mov rsi, 0
    mov rdx, 0
    syscall
    mov r13, rax

    ; Ler requisição
    mov rax, 0
    mov rdi, r13
    mov rsi, buffer
    mov rdx, 4096
    syscall

    ; Verificar se é "GET / " (rota raiz)
    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
    jmp .finalizar

.erro_404:
    mov rdi, r13
    mov rsi, resposta_404
    mov rdx, resposta_404_len
    call enviar_tudo

.finalizar:
    mov rax, 3
    mov rdi, r13
    syscall

    mov rax, 3
    mov rdi, r12
    syscall

    mov rax, 60
    mov rdi, 0
    syscall


section .data
sockaddr_in:
    dw 2
    dw 0x911F
    dd 0
    dq 0

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 "Content-Length: 155", 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 a baixaria!</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 "Content-Length: 251", 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!</center>', 0x0D, 0x0A
    db '</body>', 0x0D, 0x0A
    db '</html>', 0x0D, 0x0A
resposta_200_len equ $ - resposta_200

Testando as rotas

Compile, execute e teste:

make clean
make
./helloworld

No navegador:

  • http://localhost:8081/ → página verde “Ola mundo da baixaria!”
  • http://localhost:8081/qualquercoisa → página vermelha “404 — Nao achei a baixaria!”

Com curl:

$ curl -v http://localhost:8081/
< HTTP/1.1 200 OK

$ curl -v http://localhost:8081/naoexiste
< HTTP/1.1 404 Not Found

Entendendo as seções

Nesse ponto do tutorial usamos todas as seções principais de um programa:

Seção Permissão O que colocamos
.text R+X (ler e executar) Código executável: funções, _start
.data R+W (ler e escrever) Dados inicializados mutáveis: sockaddr_in
.rodata R (somente leitura) Constantes: respostas HTTP
.bss R+W (ler e escrever) Dados não inicializados: buffer

Salvando o progresso

git add helloworld.asm
git commit -am "Servidor HTTP funcionando com rotas 200 e 404. A baixaria agora tem web!"

Conclusão da terceira parte

Conseguimos! Nosso programa agora é um servidor HTTP completo (ainda que minimalista): recebe requisições, faz roteamento básico, retorna headers e HTML apropriados. Aprendemos:

  • A syscall read para receber dados
  • O formato das requisições e respostas HTTP
  • A seção .bss para buffers não inicializados
  • A diretiva equ para calcular tamanhos automaticamente
  • Como comparar strings em assembly para fazer roteamento

Na próxima (e última) parte do tutorial Linux x86, vamos deixar nosso servidor atendendo várias conexões em sequência, melhorar a reinicialização do servidor, e fazer um fechamento do que aprendemos!

Conclusões do servidor Linux x86