No artigo anterior, passamos pelos fundamentos de Assembly, onde foi possível entender alguns conceitos básicos tais como tipos de registradores, stack, loops, FLAGS etc, tudo sendo feito com debugging via GDB.
Agora, vamos de fato construir um web server muito simples que devolve um HTML com a frase "Hello, World". A meta é chegarmos nisto:
O processo para chegarmos a este objetivo consiste em cobrir fundamentos de Web, passando por sockets, TCP e HTTP, enquanto vamos explorando conceitos práticos em Assembly x86.
Agenda
Arquitetura Web
Para criar um servidor web, precisamos manipular mensagens HTTP, que são transportadas via camada de transporte TCP/IP através de uma rede.
Estas mensagens são enviadas entre diferentes dispositivos conectados a uma rede, que pode ser privada (local) ou pública. Regularmente, comunicação HTTP é feita entre 2 dispositivos, sendo um deles o cliente e outro o servidor.
Vamos brevemente falar de cada um destes conceitos.
Cliente-servidor
Numa arquitetura cliente-servidor, temos 2 dispositivos conectados a uma rede de computadores:
Para um servidor web, é necessário que o cliente realize uma conexão com o servidor, em seguida faça uma requisição, pelo que o servidor deve devolver uma resposta e, por último, fechar a conexão.
Mas como esta mensagem deve ser enviada? Quem garante a entrega? E caso ocorra falha de sinal na camada física (cabeamento de rede), como assegurar que cada "pacote" da mensagem seja entregue em ordem?
É pra isto que foi criado o modelo de comunicação OSI.
Modelo OSI
OSI é um modelo de referência para comunicação entre diferentes dispositivos através de diferentes redes, que estabelece um conjunto de camadas que vai desde a camada física até a camada de formato de mensagens.
- Camada física: responsável pelo tráfego de informações através de meios físicos, tais como bluetooth, frequência de rádio, cabos etc
- Camada de enlace de rede: responsável pela decodificação e codificação de mensagens em frames, do meio físico para o meio digital e vice-versa
- Camada de rede: é aqui que definimos protocolos de rede, tais como o protocolo de internet, também conhecido como IP (Internet Protocol)
Na web, os dados trafegam geralmente através de uma rede de computadores pública, global e descentralizada, neste caso a Internet
- Camada de transporte: camada responsável por características de entrega, tais como definir critérios de confiabilidade e ordem dos pacotes de mensagens. Por exemplo, nesta camada temos o protocolo de controle de transmissão, ou TCP
- Camada de sessão e apresentação: aqui vão critérios de informações que podem ser vinculadas a uma determinada conexão entre diferentes dispositivos, bem como o formato de apresentação das informações na rede
- Camada de aplicação: nesta camada, temos a definição do formato de mensagens em um nível mais "aplicacional", como por exemplo protocolo HTTP (Hypertext Transfer Protocol), FTP, SSH entre outros
Entretanto fica aqui uma questão: como que todo esse modelo de comunicação em rede se converte em algo prático num programa dentro de um sistema operacional?
Chegou o momento de falar sobre sockets e TCP.
Sockets e TCP
Num computador, todos os programas são encapsulados dentro de uma estrutura chamada processo, como vimos em artigos anteriores.
Quando falamos em cliente na aquitetura cliente-servidor, estamos falando de um processo rodando dentro de um computador, e o mesmo vale para o servidor, onde cada processo tem seu próprio identificador, ou PID:
Sabendo que processos são isolados, foram definidas diferentes formas de comunicação entre processos (também conhecido como IPC, ou inter process communication), tais como pipes, arquivos do filesystem, descritores de arquivos e UNIX sockets.
Estamos baseando a saga em sistema "UNIX-like", mais especificamente GNU/Linux
Ou seja, temos ciência que é possível fazer 2 processos dentro de um mesmo computador se comunicarem através de UNIX sockets. Mas como fazer dois processos em computadores distintos se comunicarem?
Entramos então em Berkeley Sockets, que define uma API comum de comunicação utilizando sockets, onde diferentes sockets podem estar no mesmo computador, ou em uma mesma rede local, ou até mesmo em redes diferentes dentro da Internet.
É aqui que temos a introdução ao TCP, que é um protocolo de comunicação via sockets. Portanto, para fazer um cliente se comunicar com um servidor, é preciso estabelecer endpoints de comunicação, que são basicamente sockets, e neste caso para a web, vamos utilizar sockets TCP.
Estes sockets são abertos tanto do lado do cliente, quanto no servidor. No servidor, estes sockets são mapeados em descritores de arquivos, que representam um número especial e reservado, também chamado de porta de comunicação:
Ok Leandro, consegui entender o conceito de sockets e TCP. Mas qual deveria ser o formato da mensagem na web?
Com vocês, o HTTP.
HTTP
HTTP é um protocolo de formato de mensagem que faz parte da camada de aplicação.
Com HTTP, a mensagem é definida seguindo padrões de hipertexto, que são basicamente documentos que podem ter ligações com outros documentos em sites diferentes.
Na web, o padrão segue um formato de headline, que contém o tipo de pedido, seguido de quebra de linhas com cabeçalhos de metadados e por fim, opcionalmente e dependendo do tipo de pedido, um corpo com a mensagem principal contendo majoritariamente HTML, CSS e Javascript.
Até agora, passamos por conceitos que formam a web. Como nosso exemplo de web server é bastante simples, estes fundamentos já são o suficiente para entrarmos na próxima seção, que é de fato escrever o web server em Assembly x86.
Como funciona um servidor web
Conforme vimos na seção anterior, arquitetura web passa por manipulação de sockets TCP.
Tal manipulação é feita via chamadas de sistema (syscalls) no sistema operacional, portanto, para darmos início ao servidor, vamos entender como devem ser criados os sockets a nível do OS.
4 syscalls para o resgate
Resumidamente temos que fazer 4 syscalls para termos um server operante, que são:
socket
A syscall socket é responsável por criar um endpoint de comunicação de rede e retornar um descritor de arquivo (fd) relativo ao endpoint criado.
Na libc, socket é referenciada pelo número 41 e tem a seguinte assinatura:
int socket(int domain, int type, int protocol)
Lembrando que estamos utilizando arquitetura x86_64, ou x64
bind
bind atribui nome e porta ao socket previamente criado. Esta syscall na libc responde pelo número 49 e tem a assinatura a seguir:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
listen
A syscall listen marca o socket criado (precisa ser do tipo stream, no caso TCP) para aceitar conexões. É conhecida pelo número 50 e tem a seguinte assinatura em C:
int listen(int sockfd, int backlog)
accept
A syscall accept admite uma conexão de um cliente no socket e cria um novo socket de conexão específico para aquele cliente. Esta syscall, a princípio, bloqueia o programa e só continua a execução quando uma nova conexão com novo cliente é estabelecida.
É referenciada pelo número 288 e tem a seguinte assintaura:
int accept(int sockfd, struct *addr, int addrlen, int flags)
Em resumo, tudo o que precisamos para criar um web server, independente do programa, linguagem de programação ou tecnologia, é de chamar estas 4 syscalls.
Não se engane, o teu servidor Express, Rails, Django ou NGINX, faz estas chamadas de sistema por baixo dos panos: socket, bind, listen e accept
Sem mais delongas, vamos ver como tudo isto se aplica naquilo que importa para esta saga: assembly.
Um server modesto em Assembly
Montar as syscalls para o web server em Assembly não é tão difícil quanto parece. Para começar, vamos fazer a primeira syscall, que é a socket.
Criando o socket
Como de costume, vamos montar as instruções de acordo com o manual e tabela de syscalls.
Já vimos na seção anterior quais são os números das syscalls e suas respectivas assinaturas na libc
Iniciamos definindo as constantes, apenas as necessárias para a syscall socket:
global _start
; syscalls constants
%define SYS_socket 41
; other constants
%define AF_INET 2
%define SOCK_STREAM 1
%define SOCK_PROTOCOL 0
Após isto, vamos reservar 1 byte com a diretiva resb 1
que significa "reservar 1 byte". Este byte será utilizado para armazenar o número do descritor de arquivo que referencia o socket que vai ser criado.
Como não queremos inicializar o valor deste byte, não vamos colocar na seção .data
como temos utilizado até o momento na saga, mas sim na seção .bss
.
- Na seção
.data
, ficam apenas dados inicializados - Na seção
.bss
, ficam os dados não-inicializados
section .bss
sockfd: resb 1
Vamos relembrar o layout de memória:
Como vemos na imagem, a seção .bss
vem a seguir a seção .data
, ou seja, fica em endereços de memória mais altos que a seção .data
.
Agora, vamos montar os registradores seguindo a convenção de chamada e a ordem dos parâmetros da função socket na libc:
section .text
_start:
.socket:
; int socket(int domain, int type, int protocol)
mov rdi, AF_INET
mov rsi, SOCK_STREAM
mov rdx, SOCK_PROTOCOL
mov rax, SYS_socket
syscall
mov [sockfd], rax
.exit:
mov rdi, 0
mov rax, 60
syscall
- domain: representa o domínio de comunicação. No caso queremos usar AF_INET, que significa IPv4, e tem o valor 2 conforme especificado no glibc
- type: representa o tipo de comunicação, que no caso vamos usar SOCK_STREAM que é sequencial, confiável, duplex e baseado em conexão. O valor conforme glibc é 1
- protocol: esta opção é usada no caso da utilização de um protocolo em específico. Neste caso, vamos deixar o valor como 0 que é o default para AF_INET e SOCK_STREAM, indicando que se trata de um socket TCP
Lembrando que existem sockets da família UNIX que não funcionam na camada de rede IP. É possível combinar socket UNIX com SOCK_STREAM, mas neste caso estamos combinando a família AF_INET (IPv4) com o tipo SOCK_STREAM (segmento de bytes, duplex), e esta combinação faz este socket ser TCP. Para mais detalhes sobre sockets, sugiro a leitura de um artigo que escrevi sobre UNIX Sockets
Vamos confirmar com GDB?
# Breakpoint na linha <syscall>
(gdb) break 22
(gdb) run
# Confirmando que os registradores estão com os valores corretos
# antes da execução da syscall...
(gdb) i r rdi rsi rdx rax
rdi 0x2 2
rsi 0x1 1
rdx 0x0 0
rax 0x29 41
# Confirmando que `sockfd` continua com o valor zerado
(gdb) x &sockfd
0x402000 <sockfd>: 0x00000000
(gdb) next
Após a execução da syscall, podemos ver que o retorno da função, que representa o descritor de arquivo conforme documentação, está armazenado no registrador RAX (de acordo com a convenção de chamada):
(gdb) i r rax
rax 0x3 3
(gdb) next
(gdb) x &sockfd
0x402000 <sockfd>: 0x00000003
Ou seja, após a syscall, temos em sockfd
o número do socket que acabou de ser criado.
Executando com strace:
$ strace ./live
execve("./live", ["./live"], 0x7ffca20187e0 /* 24 vars */) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
exit(0) = ?
+++ exited with 0 +++
Sem erros, yay!
Vamos para a próxima syscall.
Fazendo bind no socket
Agora, é o momento de atribuir um endereço e uma porta como endpoint de comunicação para este socket. É para isto que serve a syscall bind.
Analisando a função:
; int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
Podemos ver que um dos argumentos é um ponteiro para uma struct na memória. Vamos entender melhor cada argumento.
sockfd
Em sockfd vai o inteiro que representa o descritor do socket criado
sockaddr **addr*
Representa o ponteiro para o endereço de memória que contém uma estrutura de dados que, de acordo com este guia, contempla: family, port, ip_address, sin_zero, onde sin_zero é apenas padding de preenchimento de bytes.
Para arquitetura x64, esta estrutura deve conter 16 bytes no total, onde:
- 2 bytes são para a família de protocolo
2 bytes para a porta
4 bytes para o endereço de IP
8 bytes de padding para o sin_zero, ou seja, preencher os 8 bytes restantes com ZERO
addrlen: tamanho do sockaddr, e já sabemos que são 16 bytes
Uma vez entendidos os parâmetros da função, vamos montar a chamada.
%define SYS_bind 49
; Data types in asm
; (db) byte => 1 byte
; (dw) word => 2 bytes
; (dd) doubleword => 4 bytes
; (dq) quadword => 8 bytes
section .data
sockaddr:
family: dw AF_INET ; 2 bytes
port: dw 0x0BB8 ; 2 bytes (representa a porta 3000)
ip_address: dd 0 ; 4 bytes
sin_zero: dq 0 ; 8 bytes
.bind:
; int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
mov rdi, [sockfd]
mov rsi, sockaddr
mov rdx, 16
mov rax, SYS_bind
syscall
Ao validar com GDB, podemos ver que o sockaddr
está armazenando a estrutura necessária para ser enviada no parâmetro sockaddr *addr
da syscall:
# Breakpoint na syscall de bind
(gdb) break 38
(gdb) run
(gdb) x &sockaddr
0x402000 <family>: 0xb80b0002
Se buscarmos os 2 primeiros bytes, confirmamos que é o valor 2 (repare que está invertido pois é o padrão little-endian da aquitetura x86_64:
(gdb) x /2xb &sockaddr
0x402000 <family>: 0x02 0x00
Quanto à porta, queremos que o server responda no número 3000. Portanto, verificamos que os próximos 2 bytes representam a porta:
# Em hexadecimal, 3000 equivale a 0x0BB8, mas por causa do formato
# little-endian da arquitetura x86_64, estamos visualizando 0xB80B
(gdb) x /2xb (void*) &sockaddr+2
0x402002 <port>: 0xb8 0x0b
Queremos também que o servidor responda no endereço de IP 0.0.0.0
, então os próximos 4 bytes estarão todos a zero:
(gdb) x /4xb (void*) &sockaddr+4
0x402004 <ip_address>: 0x00 0x00 0x00 0x00
E, por fim, os 8 bytes restantes representando sin_zero, todos preenchidos com zero:
(gdb) x /8xb (void*) &sockaddr+8
0x402008 <sin_zero>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Vamos executar com strace:
$ strace ./live
execve("./live", ["./live"], 0x7ffd51ed4650 /* 24 vars */) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(47115), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
exit(0) = ?
+++ exited with 0 +++
Ouch! Apesar da função bind ter retornado 0 indicando que não houve erros, temos um pequeno problema. Repare que a porta não está sendo mapeada para o número 3000, e sim para 47115, conforme vemos em htons(47115).
Entendendo a implicação de endianess na syscall bind
htons
é uma função de rede utilizada para converter a ordem dos bytes do programa antes de serem utilizados na rede. Como a internet utiliza big-endian, esta função converte a ordem utilizada na arquitetura (no caso da x86_64, little-endian) para o formato big-endian da rede.
Entretanto htons(47115) não é o valor que queremos. O que precisamos é que o mapeamendo seja htons(3000). Por quê isto está acontecendo?
O valor que colocamos em hexadecimal representando 3000 é 0x0BB8, mas se prestarmos atenção no GBD, o valor de fato armazenado está com os bytes invertidos para little-endian, que é 0xB80B. Ocorre que 0xB80B
em decimal é 47115!!!!!! Aí que está o problema!
Precisamos então inverter os bytes no programa, e assim sendo o valor que será passado para a função htons fica corrigido.
....
section .data
sockaddr:
family: dw AF_INET ; 2 bytes
port: dw 0xB80B ; 2 bytes (aqui invertemos os bytes)
ip_address: dd 0 ; 4 bytes
sin_zero: dq 0 ; 8 bytes
....
E analisando novamente com GDB:
# Agora sim, apesar de estar invertido, é exatamente este valor que
# queremos que seja passado para htons: 0x0BB8 em decimal é 3000
(gdb) x /2xb (void*) &sockaddr+2
0x402002 <port>: 0x0b 0xb8
Executando novamente com strace:
$ strace ./live
execve("./live", ["./live"], 0x7ffd51ed4650 /* 24 vars */) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(3000), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
exit(0) = ?
+++ exited with 0 +++
Superb! Podemos ver que a syscall bind foi executada com os parâmetros corretamente, inclusive o htons(3000)
, então retornando 0, que indica que não houve qualquer erro.
Preparando para receber conexões
Próximo passo consiste em preparar o socket para receber conexões, que basicamente é chamar a função listen
:
%define SYS_listen 50
%define BACKLOG 2
.listen:
; int listen(int sockfd, int backlog)
mov rdi, [sockfd]
mov rsi, BACKLOG
mov rax, SYS_listen
syscall
Onde BACKLOG significa a quantidade de conexões "pendentes" no socket. Executamos com strace e:
$ strace ./live
execve("./live", ["./live"], 0x7ffe6b4eea30 /* 24 vars */) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(3000), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 2) = 0
exit(0) = ?
+++ exited with 0 +++
Que noite maravilhosa! Listen funcionou lindamente, afinal, é uma função muito simples. Agora, hora de aceitar conexões de clientes no socket.
Chegou o momento de aceitar clientes
O grande momento chegou. Vamos montar as instruções da syscall accept, que de acordo com a função em libc, recebe um socket como primeiro argumento e os demais são opcionais.
%define SYS_accept 288
.accept:
; int accept(int sockfd, struct *addr, int addrlen, int flags)
mov rdi, [sockfd]
mov rsi, 0 ; não precisa estabelecer um addr
mov rdx, 0 ; não precisa do tamanho uma vez que não há addr
mov r10, 0
mov rax, SYS_accept
syscall
Se executarmos com GDB, podemos ver que o resultado da syscall fica bloqueado até que uma conexão seja feita:
# Breakpoint na syscall de socket
(gdb) break 55
(gdb) run
(gdb) next
O programa está parado na syscall de socket, aguardando resposta do kernel. Para que o kernel responda e o programa continue a execução, é preciso realizar um pedido usando um HTTP client, e neste caso vamos usar o curl:
$ curl localhost:3000
Repare que o programa continuou a execução. Vamos ver a resposta que está em RAX:
(gdb) i r rax
rax 0x4 4
# Um número diferente do sockfd, que é o socket criado pelo server
(gdb) x &sockfd
0x402010 <sockfd>: 0x00000003
Podemos ver que é um número diferente (RAX contém 4 e sockfd contém 3). De acordo com a documentação, este é o número do descritor que representa um novo socket criado para comunicação entre um cliente específico e o servidor.
Vamos mover o valor de RAX para R8, apenas para preservar o socket, uma vez que RAX será usado novamente por outras syscalls de accept:
mov r8, rax ; client socket
Resposta do servidor e fechamento da conexão
Uma outra coisa importante a se fazer é fechar a conexão com este socket do cliente depois de ter processado e respondido a requisição.
Vamos implementar a subrotina .write
, que escreve a resposta na conexão (socket) do cliente:
%define SYS_write 1
%define CR 0xD
%define LF 0xA
section .data
response:
headline: db "HTTP/1.1 200 OK", CR, LF
content_type: db "Content-Type: text/html", CR, LF
content_length: db "Content-Length: 22", CR, LF
crlf: db CR, LF
body: db "<h1>Hello, World!</h1>"
responseLen: equ $ - response
section .text
...
.write:
; int write(int fd, buffer *bf, int bfLen)
mov rdi, r8
mov rsi, response
mov rdx, responseLen
mov rax, SYS_write
syscall
ret
No exemplo acima, assumimos que a string de resposta HTTP aponta para uma estrutura na memória, definida em .data
.
Atenção para CR (carriage return), LF (line feed) que são constantes que representam
\r\n
que são separadores de linhas definidos pelo protocolo HTTP
Agora, definir a subrotina .close
, que fecha a conexão com o cliente:
%define SYS_close 3
section .text
...
.close:
; int close(int fd)
mov rdi, r8
mov rax, SYS_close
syscall
ret
Ligando tudo no accept:
section .text
....
.accept:
; int accept(int sockfd, struct *addr, int addrlen, int flags)
mov rdi, [sockfd]
mov rsi, 0 ; não precisa estabelecer um addr
mov rdx, 0 ; não precisa do tamanho uma vez que não há addr
mov r10, 0
mov rax, SYS_accept
syscall
mov r8, rax ; client socket
call .write ; escreve no socket
call .close ; fecha o socket
jmp .exit ; termina o programa
E agora, vamos executar o programa com strace:
$ strace ./live
execve("./live", ["./live"], 0x7ffd811567c0 /* 24 vars */) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(3000), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 2) = 0
accept4(3, NULL, NULL, 0
- primeiro foi feita a syscall socket
- a seguir foi feito o bind
- depois o listen
- e por fim, o accept ficou bloqueado a espera de uma requisição
Em outra janela, vamos fazer a requisição:
$ curl localhost:3000
<h1>Hello, World!</h1>
E no servidor, a saída do strace no final ficou assim:
write(4, "HTTP/1.1 200 OK\r\nContent-Type: t"..., 86) = 86
close(4) = 0
exit(0) = ?
+++ exited with 0 +++
Escreveu a resposta com write
, fechou a conexão com close
, e depois terminou o programa com exit
.
Como não ficar feliz?
Mas o servidor deve ficar em loop, não?
Sim, o servidor deve ficar em loop, portanto ao invés de fazer o jmp .exit
, fazemos jmp .accept
na última linha da procedure:
...
.accept:
; int accept(int sockfd, struct *addr, int addrlen, int flags)
mov rdi, [sockfd]
mov rsi, 0 ; não precisa estabelecer um addr
mov rdx, 0 ; não precisa do tamanho uma vez que não há addr
mov r10, 0
mov rax, SYS_accept
syscall
mov r8, rax ; client socket
call .write
call .close
jmp .accept ; <-- MUDANÇA AQUI, mantém o server em loop infinito
Assim, o server nunca termina, e quando uma conexão com um cliente é fechada, voltamos no início do loop e ficamos a espera de nova conexão na syscall accept.
Código final do server:
global _start
%define SYS_socket 41
%define SYS_bind 49
%define SYS_listen 50
%define SYS_accept 288
%define SYS_write 1
%define SYS_close 3
%define AF_INET 2
%define SOCK_STREAM 1
%define SOCK_PROTOCOL 0
%define BACKLOG 2
%define CR 0xD
%define LF 0xA
; Data types in asm
; byte => 1 byte
; word => 2 bytes
; doubleword => 4 bytes
; quadword => 8 bytes
section .data
sockaddr:
family: dw AF_INET ; 2 bytes
port: dw 0xB80B ; 2 bytes (47115 big endian becomes 3000 little endian)
ip_address: dd 0 ; 4 bytes
sin_zero: dq 0 ; 8 bytes
sockaddrLen: equ $ - sockaddr
response:
headline: db "HTTP/1.1 200 OK", CR, LF
content_type: db "Content-Type: text/html", CR, LF
content_length: db "Content-Length: 22", CR, LF
crlf: db CR, LF
body: db "<h1>Hello, World!</h1>"
responseLen: equ $ - response
section .bss
sockfd: resb 1
section .text
_start:
.socket:
; int socket(int domain, int type, int protocol)
mov rdi, AF_INET
mov rsi, SOCK_STREAM
mov rdx, SOCK_PROTOCOL
mov rax, SYS_socket
syscall
mov [sockfd], rax
.bind:
; int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
mov rdi, [sockfd]
mov rsi, sockaddr
mov rdx, sockaddrLen
mov rax, SYS_bind
syscall
.listen:
; int listen(int sockfd, int backlog)
mov rdi, [sockfd]
mov rsi, BACKLOG
mov rax, SYS_listen
syscall
.accept:
; int accept(int sockfd, struct *addr, int addrlen, int flags)
mov rdi, [sockfd]
mov rsi, 0 ; não precisa estabelecer um addr
mov rdx, 0 ; não precisa do tamanho uma vez que não há addr
mov r10, 0
mov rax, SYS_accept
syscall
mov r8, rax ; client socket
call .write
call .close
jmp .accept
.write:
; int write(int fd, buffer *bf, int bfLen)
mov rdi, r8
mov rsi, response
mov rdx, responseLen
mov rax, SYS_write
syscall
ret
.close:
; int close(int fd)
mov rdi, r8
mov rax, SYS_close
syscall
ret
Executando tudo com strace e temos:
$ strace ./live
execve("./live", ["./live"], 0x7fff9fde7840 /* 24 vars */) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(3000), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 2) = 0
accept4(3, NULL, NULL, 0) = 4
write(4, "HTTP/1.1 200 OK\r\nContent-Type: t"..., 86) = 86
close(4) = 0
accept4(3, NULL, NULL, 0) = 4
write(4, "HTTP/1.1 200 OK\r\nContent-Type: t"..., 86) = 86
close(4) = 0
accept4(3, NULL, NULL, 0) = 4
write(4, "HTTP/1.1 200 OK\r\nContent-Type: t"..., 86) = 86
close(4) = 0
accept4(3, NULL, NULL, 0
No lado do cliente:
$ curl localhost:3000
<h1>Hello, World!</h1>
$ curl localhost:3000
<h1>Hello, World!</h1>
$ curl localhost:3000
<h1>Hello, World!</h1>
Com vocês, o web browser
Esta saga não teria nenhuma graça se não fosse pra ser executada em um web browser, afinal estamos falando de um web server, não?
Conclusão
Incrivelmente chegamos no final da construção de um modesto web server. Aqui aprendemos conceitos sobre sockets, TCP e HTTP, com uma pitada leve de HTML.
Fala aí, quem não já conhecia a tag H1 do HTML? kk
Para além de termos visto sobre as syscalls de rede socket, bind, listen e accept em Assembly.
Ainda não chegamos ao fim da saga, pelo que no próximo artigo iremos abordar a criação de threads e aprender sobre alocação dinâmica de memória para as threads.
Stay tuned!
Agradecimentos a Rodrigo Gonçalves de Branco por ter revisado este artigo com o devido rigor
Referências
Building a web server in Bash
https://dev.to/leandronsp/series/19120
OSI Model
https://en.wikipedia.org/wiki/OSI_model
TCP
https://en.wikipedia.org/wiki/Transmission_Control_Protocol
Berkeley Sockets
https://en.wikipedia.org/wiki/Berkeley_sockets
HTTP
https://en.wikipedia.org/wiki/HTTP
struct sockaddr_in
https://www.gta.ufrj.br/ensino/eel878/sockets/sockaddr_inman.html
Top comments (3)
Lembrei da época do DOS 5 quando eu tive que criar pacotes IP/IPX/UDP para enviar dados de monitoramento de equipamentos pela rede Novell e converter num servidor Windows NT. O detalhe é que não existiam funções no DOS para isso. Eu tive que usar uma API da Novell Network e outra da Lantastic para entender como definir os frames dos pacotes de rede (endereçamentos, flags, tipos, tamanhos, TTL, etc e a área de dados). Tudo num misto de programas assembly, C, Turbo Pascal e Delphi. Bons tempos aqueles onde a gente tirava "água de pedra", ou melhor, transformava um 386 num servidor web e aplicações que funcionavam 24/7 durante anos.
Muito bom seu artigo, gostei.
opa, valeu Plinio! relatos de quem é raiz hein, mto bom, experiência boa essa aí
Que incrível! Estou acompanhando a saga e estou achando o máximo! Obrigado, Leandro, por esse conteúdo fantástico