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:

  1. O servidor cria um socket e o associa a uma porta (ex: 8081)
  2. O servidor coloca o socket em modo de “escuta” (listen)
  3. O servidor espera (accept) até alguém conectar
  4. Um cliente pede conexão nessa porta
  5. O servidor recebe um novo file descriptor que representa a conexão com o cliente
  6. 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 = 0xBB8dw 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ção imprime que criamos antes recebe imprime(string) em RDI e sempre escreve no stdout. A enviar recebe enviar(fd, string) e escreve no fd que passarmos. Poderíamos reescrever imprime como enviar(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:

  1. 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)
  2. bind() → disse “esse socket vai usar a porta 8081 em qualquer interface de rede”
  3. listen() → colocou o socket em modo servidor, pronto para receber conexões
  4. accept() → dormiu até um cliente conectar, e retornou um novo fd exclusivo para aquela conexão
  5. write() → enviou dados através do fd do cliente
  6. close() × 2 → fechou a conexão com o cliente e depois o socket servidor
  7. 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!

Fazendo um servidor HTTP