[linux-x86] Aceitando uma conexão TCP
Na parte anterior conseguimos criar uma função reutilizável (imprime) que escreve qualquer string no terminal. Agora vamos dar um passo maior: fazer nosso programa conversar pela rede! O objetivo dessa parte é fazer nosso programa aceitar uma conexão TCP e mandar uma mensagem para quem se conectou a ele.
Como funciona uma conexão TCP?
Antes de sair escrevendo código, vamos entender como funciona uma comunicação TCP entre dois computadores (ou dois programas na mesma máquina). O modelo é chamado de cliente-servidor:
- O servidor cria um socket e o associa a uma porta (ex: 8081)
- O servidor coloca o socket em modo de “escuta” (listen)
- O servidor espera (accept) até alguém conectar
- Um cliente pede conexão nessa porta
- O servidor recebe um novo file descriptor que representa a conexão com o cliente
- Ambos podem enviar (
write/send) e receber (read/recv) dados por esse fd
Tudo isso no Linux é feito através de chamadas de sistema! No total, usaremos 4 novas syscalls:
| Syscall | ID | O que faz |
|---|---|---|
socket |
41 | Cria um socket e retorna um fd |
bind |
49 | Associa o socket a uma porta |
listen |
50 | Coloca o socket em modo de escuta |
accept |
43 | Espera uma conexão e retorna o fd do cliente |
Além dessas, usaremos a close (id 3) para fechar os sockets, e a write (id 1) que já conhecemos para enviar dados ao cliente.
Você pode conferir todas as syscalls em Linux x64
Estrutura sockaddr_in
Para usar bind, precisamos passar um endereço que descreve em qual porta e interface de rede o socket vai escutar. Essa estrutura se chama sockaddr_in e em C tem essa cara:
struct sockaddr_in {
sa_family_t sin_family; // 2 bytes - família de endereço (AF_INET = 2)
in_port_t sin_port; // 2 bytes - porta em network byte order
struct in_addr sin_addr; // 4 bytes - endereço IP (INADDR_ANY = 0)
unsigned char sin_zero[8]; // 8 bytes - padding (sempre zero)
};
No total, a estrutura tem 16 bytes. Vamos montar ela em assembly na seção .data:
section .data
sockaddr_in:
dw 2 ; sin_family = AF_INET (IPv4)
dw 0x911F ; sin_port = htons(8081) — explicado abaixo!
dd 0 ; sin_addr.s_addr = INADDR_ANY (aceita conexões de qualquer IP)
dq 0 ; sin_zero — padding de 8 bytes zerados
O que é htons e network byte order?
A porta 8081 em hexadecimal é 0x1F91. Porém, a rede manda todos os números em big-endian (byte mais significativo primeiro), enquanto seu processador x86 trabalha em little-endian (byte menos significativo primeiro). A função htons (host-to-network-short) simplesmente inverte os bytes:
Porta 8081 = 0x1F91
Big-endian (rede): 0x1F 0x91
Little-endian (x86): 0x91 0x1F
Como o NASM grava palavras (dw) em little-endian, precisamos escrever os bytes já invertidos para que na memória fiquem na ordem correta para a rede. Por isso usamos 0x911F ao invés de 0x1F91. Dessa forma, os bytes na memória ficarão [0x1F, 0x91], que é exatamente o que a rede espera.
Sempre que for mudar a porta no código, lembre-se de inverter os bytes! Porta 3000 =
0xBB8→dw 0xB80B
Criando o socket
Vamos começar adicionando a chamada socket no início do nosso _start. A syscall socket recebe 3 argumentos:
-
RDI = domain (família do protocolo:
AF_INET= 2 para IPv4) -
RSI = type (tipo de socket:
SOCK_STREAM= 1 para TCP) - RDX = protocol (protocolo específico: 0 para deixar o kernel decidir)
O retorno em RAX será o file descriptor do socket criado.
section .text
global _start
_start:
; Criar socket — socket(AF_INET, SOCK_STREAM, 0)
mov rax, 41 ; syscall socket
mov rdi, 2 ; AF_INET = IPv4
mov rsi, 1 ; SOCK_STREAM = TCP
mov rdx, 0 ; protocolo padrão
syscall
; RAX agora contém o fd do nosso socket
; Vamos guardar ele em R12 para usar depois
mov r12, rax
Associando o socket à porta 8081 (bind)
Agora precisamos dizer ao kernel que esse socket deve escutar na porta 8081. A syscall bind (49) recebe:
- RDI = fd do socket (nosso R12)
-
RSI = ponteiro para
sockaddr_in(a estrutura que montamos) -
RDX = tamanho do
sockaddr_in(16 bytes)
; bind(socket_fd, &sockaddr_in, 16)
mov rax, 49 ; syscall bind
mov rdi, r12 ; fd do socket
mov rsi, sockaddr_in ; endereço da estrutura
mov rdx, 16 ; sizeof(sockaddr_in)
syscall
; Se RAX for negativo, algo deu errado
; Por simplicidade, vamos seguir sem verificar erros aqui
Colocando o socket em modo de escuta (listen)
Com o socket associado à porta, colocamos ele em modo de escuta com a syscall listen (50):
- RDI = fd do socket
- RSI = backlog (quantas conexões pendentes podem ficar na fila enquanto não atendemos)
; listen(socket_fd, 10)
mov rax, 50 ; syscall listen
mov rdi, r12 ; fd do socket
mov rsi, 10 ; backlog = 10 conexões na fila
syscall
Aceitando uma conexão (accept)
Agora a parte mais legal: esperar alguém conectar! A syscall accept (43) irá bloquear nosso programa até que um cliente conecte na porta 8081. Quando isso acontecer, ela retorna um novo fd em RAX que representa a conexão com o cliente.
- RDI = fd do socket (o que está em modo listen)
- RSI = ponteiro para estrutura de endereço do cliente (podemos passar 0 se não quisermos saber)
- RDX = ponteiro para o tamanho da estrutura (podemos passar 0 também)
; accept(socket_fd, NULL, NULL)
mov rax, 43 ; syscall accept
mov rdi, r12 ; fd do socket (listening)
mov rsi, 0 ; não precisamos do endereço do cliente
mov rdx, 0 ; não precisamos do tamanho
syscall
; RAX agora tem o fd da conexão com o cliente
mov r13, rax ; guarda o fd do cliente em R13
Enviando uma mensagem para o cliente
Agora que temos um fd representando a conexão com o cliente (em R13), podemos usar a syscall write (1) que já conhecemos para mandar uma mensagem. Mas precisamos adaptar nossa função imprime: ela sempre escreve no stdout (fd=1), e precisamos escrever no fd do cliente.
Vamos criar uma nova função enviar para escrever em qualquer fd. A syscall write espera exatamente esta ordem de argumentos:
- RDI = fd
- RSI = ponteiro do buffer
- RDX = tamanho
Então faz sentido criar nossa função usando a mesma ordem: enviar(fd, string) recebe o fd em RDI e a mensagem em RSI. A única coisa que a função precisa fazer antes de chamar write é contar quantos bytes existem na string terminada em zero:
enviar:
; Entrada: RDI = fd destino, RSI = ponteiro da string
push rdi
push rsi
push rdx
; Contar caracteres da string em RSI
mov rdx, 0 ; contador de caracteres
.proxchar:
cmp byte [rsi + rdx], 0 ; compara o byte atual com 0 (null terminator)
je .chamawrite ; se for 0, terminou a string
inc rdx ; senão, incrementa o contador
jmp .proxchar
.chamawrite:
mov rax, 1 ; syscall write
syscall
pop rdx
pop rsi
pop rdi
ret
Como usamos a mesma ordem de argumentos da syscall write, não precisamos trocar registradores antes do syscall.
Comparação com
imprime: a funçãoimprimeque criamos antes recebeimprime(string)em RDI e sempre escreve no stdout. Aenviarrecebeenviar(fd, string)e escreve no fd que passarmos. Poderíamos reescreverimprimecomoenviar(1, string), mas manteremos ambas por clareza.
Enviando resposta ao cliente
Com a função enviar pronta, podemos mandar uma mensagem para o cliente:
mov rdi, r13 ; fd do cliente
mov rsi, mensagem ; nossa mensagem
call enviar
; Fechar conexão com o cliente
mov rax, 3 ; syscall close
mov rdi, r13 ; fd do cliente
syscall
; Fechar socket do servidor
mov rax, 3 ; syscall close
mov rdi, r12 ; fd do servidor
syscall
; Sair
mov rax, 60
mov rdi, 0
syscall
Nosso programa completo
Juntando tudo, nosso helloworld.asm fica assim:
section .text
global _start
enviar:
; envia(fd, string) => RDI=fd, RSI=string
push rdi
push rsi
push rdx
mov rdx, 0
.proxchar:
cmp byte [rsi + rdx], 0
je .chamawrite
inc rdx
jmp .proxchar
.chamawrite:
mov rax, 1 ; syscall write
syscall
pop rdx
pop rsi
pop rdi
ret
_start:
; 1. Criar socket — socket(AF_INET, SOCK_STREAM, 0)
mov rax, 41 ; syscall socket
mov rdi, 2 ; AF_INET
mov rsi, 1 ; SOCK_STREAM
mov rdx, 0 ; protocolo
syscall
mov r12, rax ; guarda socket fd
; 2. Associar à porta — bind(socket_fd, &addr, 16)
mov rax, 49 ; syscall bind
mov rdi, r12 ; socket fd
mov rsi, sockaddr_in
mov rdx, 16 ; sizeof(sockaddr_in)
syscall
; 3. Modo escuta — listen(socket_fd, 10)
mov rax, 50 ; syscall listen
mov rdi, r12
mov rsi, 10 ; backlog
syscall
; 4. Esperar conexão — accept(socket_fd, NULL, NULL)
mov rax, 43 ; syscall accept
mov rdi, r12
mov rsi, 0
mov rdx, 0
syscall
mov r13, rax ; guarda fd do cliente
; 5. Enviar mensagem ao cliente
mov rdi, r13 ; fd do cliente
mov rsi, mensagem
call enviar
; 6. Fechar conexão com o cliente
mov rax, 3 ; syscall close
mov rdi, r13
syscall
; 7. Fechar socket servidor
mov rax, 3
mov rdi, r12
syscall
; 8. Sair
mov rax, 60
mov rdi, 0
syscall
section .data
sockaddr_in:
dw 2 ; sin_family = AF_INET
dw 0x911F ; sin_port = htons(8081)
dd 0 ; sin_addr.s_addr = INADDR_ANY
dq 0 ; sin_zero (8 bytes)
mensagem:
db "Ola da baixaria TCP!", 0x0D, 0x0A, 0
; ^CR ^LF ^terminador nulo
Repare que no final da mensagem adicionamos 0x0D, 0x0A (CR+LF — carriage return + line feed). Isso é o equivalente ao \r\n do C e garante que a quebra de linha funcione em qualquer cliente (navegadores, netcat, curl etc).
Testando!
Compile e execute:
make clean
make
./helloworld
O programa vai travar, esperando alguém conectar. Isso é o comportamento normal do accept — ele é bloqueante! Em outro terminal, use o netcat (ou nc) para conectar:
nc localhost 8081
Se tudo deu certo, você verá a mensagem Ola da baixaria TCP! no terminal do netcat, e o programa assembly irá fechar em seguida. Dá pra testar com o curl também:
curl http://localhost:8081
Dica: se o programa reclamar “Address already in use” (endereço já em uso), é porque a porta 8081 ainda está presa por uma execução anterior. Espere alguns segundos ou mude a porta. Isso acontece porque não configuramos
SO_REUSEADDR(faremos isso mais tarde).
Entendendo o fluxo
Vamos revisar o que cada passo fez:
- socket() → criou um “ponto de comunicação” no kernel, retornando um fd (provavelmente 3, já que 0, 1 e 2 são stdin/stdout/stderr)
- bind() → disse “esse socket vai usar a porta 8081 em qualquer interface de rede”
- listen() → colocou o socket em modo servidor, pronto para receber conexões
- accept() → dormiu até um cliente conectar, e retornou um novo fd exclusivo para aquela conexão
- write() → enviou dados através do fd do cliente
- close() × 2 → fechou a conexão com o cliente e depois o socket servidor
- exit() → encerrou o programa
Salvando o progresso
git add helloworld.asm
git commit -am "Agora aceitamos conexoes TCP. Baixaria networked!"
Conclusão da segunda parte
Agora nosso programa não apenas sai e escreve no terminal — ele conversa pela rede! Já aceitamos uma conexão TCP e enviamos dados para o cliente. Dominamos 5 novas chamadas de sistema: socket, bind, listen, accept e close.
Na próxima parte, vamos transformar isso em um servidor HTTP de verdade: ler a requisição do cliente (GET / HTTP/1.1) e retornar uma página HTML completa!