在 Assembly x86 中构建 Web 服务器,第五部分,最终服务器
没有前面的内容,通过大会的基础,onde possível entender alguns conceitos tais como Tipos de registradores,堆栈,循环,FLAGS等,tudo sendofeito com通过GDB调试。
Agora 构建的 Web 服务器非常简单,可以将 HTML 转换为“Hello, World”。元 é chegarmos nisto:
该对象的处理过程包括 Web 基础、套接字、TCP 和 HTTP 的传递,以及在 Assembly x86 上探索实践概念的过程。
议程
网站建筑
在网络服务中,可以使用HTTP 协议,通过 TCP/IP 传输网络进行传输。
Estas mensagens são enviadas entre different dispositivos conectados a uma rede, que pode ser privada (local) or publica.定期进行 HTTP 通信,并通过 2 个配置发送客户端和服务器端的数据。
Vamos brevemente falar de cada um destes conceitos。
客户-服务
Numa arquitetura cliente-servidor,temos 2 dispositivos conectados a uma rede de de computadores:
对于服务器网络,客户需要实现与服务器的连接,因此需要满足以下条件,并且需要将服务器开发移交至服务器,然后再进行连接。
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 order?
这是OSI 通信模型的一部分。
OSI模型
OSI 是一种参考不同通信方式的模型,可与不同的通信方式相结合,从而实现通信方式和通信方式的通信。
- 网络摄像机:响应网络传输信息,例如蓝牙、无线电频率、卡波斯等
- 重新连接的相机:响应帧解码和消息编码,与 meio 数码设备或 meio 数码设备进行交互,反之亦然
- 网络协议:定义网络协议、互联网协议、IP (互联网协议)
网络、公共计算机、全球网络和互联网上的网络传输
- 交通运输:对运输特征的响应,这定义了运输的标准和规则。例如,TCP传输控制协议的具体工作原理
- 会议和展示的技巧:如何确定信息的标准,确定不同配置之间的关系,以及展示信息的格式
- 应用程序:应用程序的具体格式定义为“应用程序”,例如 HTTP(超文本传输协议)、FTP、SSH 等协议
有一个问题:如何将通信模型重新转换为操作系统的实际程序?
这是套接字和 TCP的重要时刻。
套接字和 TCP
数字计算机、所有程序都封装在 uma estrutura chamada processo中,与前部的声音相同。
在客户端服务器上安装客户端,在计算机上运行进程,并在服务器上获取信息,在进程中使用专有标识符,或PID:
隔离进程的定义、进程间通信的不同格式的定义(例如 IPC 或进程间通信)、管道、文件系统文件、文件描述符和 UNIX 套接字。
建立“类 UNIX”系统的传奇故事,主要针对 GNU/Linux
您可以使用 UNIX套接字上的 comunicarem 计算机来处理2 个进程。计算机的处理过程与通信有何不同?
在Berkeley Sockets中,定义了使用套接字进行通信的 API,在不同的套接字上可以使用不同的计算程序,或者可以在本地使用,也可以在Internet上使用不同的套接字。
本文介绍了 TCP,即通过套接字进行通信的协议。 Portanto,用于与服务器通信的客户端,可精确建立通信端点,作为基本套接字,与网络嵌套,并使用TCP 套接字。
Estes 插座 são abertos tanto do lado do cliente, Quanto no server.没有服务器,estes 套接字são mapeados em decritores de arquivos, querepresentam um número especial e reservado, também chamado de porta de comunicação :
好吧,Leandro,Consegui entender o conceito de sockets e TCP。是否可以在网络上设置格式?
Com vocês, o HTTP。
HTTP
HTTP 是应用程序中最重要的信息格式协议。
HTTP 是一个关于网络文件的定义,它是不同网站上的基本文档。
在网络中,标题格式、主题内容、主题内容的顺序包括元数据和主题、主题选项和相关内容,以及主题主要内容,包括 HTML、CSS 和 Javascript。
在集市上,我们可以通过网络进行交流。这是一个简单的 Web 服务器示例,它是最基础的,并且在接下来的部分中已经足够了,可以在 Assembly x86 上开发 Web 服务器。
Como funciona um servidor web
与前面的内容一致,通过 TCP 套接字操作来配置网络。
通过系统调用(系统调用)进行操作,没有系统操作,portanto,para darmos início ao server,vamos entender como devem ser criados os sockets a nível do OS。
4 个系统调用 para o resgate
服务器操作术语的 fazer 4 系统调用概要如下:
socket
一个系统调用套接字,响应于通信端点的响应,并返回与端点相关的描述符 (fd)。
libc、套接字和编号为 41 的套接字和后续关联:
int socket(int domain, int type, int protocol)
Lembrando que estamos utilizando arquitetura x86_64,ou x64
绑定
绑定属性名称和端口 ao 套接字之前。系统调用 libc 响应编号 49 和以下项:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
一个系统调用listen marca o socket criado(precisa ser do tipostream,no caso TCP)para aceitar conexões。连续编号 50 并在 C 上进行后续操作:
int listen(int sockfd, int backlog)
接受
系统调用接受承认没有套接字的客户端连接,并重新连接了特定于客户端的套接字。这是一个系统调用的原则、程序和程序,以便继续执行新客户和建立的新客户。
编号为 288 的参考号为:
int accept(int sockfd, struct *addr, int addrlen, int flags)
总结一下,Web 服务器的基本原理,独立的程序、程序语言或技术,以及 4 个系统调用。
无论是 Express、Rails、Django 或 NGINX 服务,都可以使用 baixo dos panos 的系统:套接字、绑定、监听和接受
Sem mais delongas, vamos ver como tudo isto se aplica naquilo que importa para esta saga: assembly。
Um server modesto em Assembly
Montar 作为程序集上的 Web 服务器系统调用,但在整个过程中是困难的。 Para começar, vamos fazer a primeira syscall, que é a socket。
Criando o socket
系统调用的编号和 libc 的相关名称的前面部分
Iniciamos 定义为常量,apenas 作为系统调用套接字的必要条件:
global _start
; syscalls constants
%define SYS_socket 41
; other constants
%define AF_INET 2
%define SOCK_STREAM 1
%define SOCK_PROTOCOL 0
此处,vamos reservar 1 byte 是resb 1指“reservar 1 byte”。该字节可用于设备或参考描述符或套接字的编号。
如果没有初始化或字节的价值,则没有必要.data在接下来的时间中使用该传奇的时刻,就像在接下来的那样.bss。
- 接下来
.data,请注意以下事项 - 接下来
.bss,我将不再进行初始化
section .bss
sockfd: resb 1
记忆布局的相关内容:
想象一下,下一章.bss又一章.data,或者,我会在下一章中记忆更多的内容.data。
Agora,请注册者按照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
- 域:代表通信领域。不能使用 AF_INET,也不能使用 IPv4,并且必须符合glibc的特定要求
- 类型:代表通信类型,不适用 SOCK_STREAM 顺序、机密、双工和连接。符合 glibc é 1 的价值
- 协议:美国可以使用特定的协议。 AF_INET 和 SOCK_STREAM 的默认值是 0,表示 TCP 套接字的设置
Lembrando 是 UNIX 家族中存在的套接字,但其功能与 IP 无关。可以使用 UNIX 组合套接字 SOCK_STREAM,可以与 AF_INET (IPv4) 家族组合使用 SOCK_STREAM(字节段,双工),也可以与 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
执行系统调用、返回功能的命令、表示符合文件要求的描述符、注册 RAX 的设备(与 chamada 的协议):
(gdb) i r rax
rax 0x3 3
(gdb) next
(gdb) x &sockfd
0x402000 <sockfd>: 0x00000003
您可以使用一个系统调用来指定sockfd套接字的编号。
使用strace执行:
$ strace ./live
execve("./live", ["./live"], 0x7ffca20187e0 /* 24 vars */) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
exit(0) = ?
+++ exited with 0 +++
没有错误,耶!
Vamos para a próxima 系统调用。
Fazendo 绑定无插槽
Agora 是一个渲染的时刻,也是一个与 este 套接字通信端点的端口。它提供系统调用绑定。
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 querepresenta o descriptor do socket criado
sockaddr **addr *
代表内存存储的连接器,它是一个数据包,包含:家庭、端口、ip_address、sin_zero、 sin_zero 和 preenchimento 字节的填充。
Para arquitetura x64,esta estrutura deve conter 16 字节,总计:
- 协议家族中的 2 个字节
-
2 字节端口
-
IP 渲染的 4 个字节
-
8 字节 de padding para o sin_zero、 ou seja 、 preencher os 8 bytes Restantes com ZERO
addrlen : sockaddr 的地址,即 16 个字节
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
通过 GDB 验证,可以使用sockaddr以下命令来配置系统调用所需的参数sockaddr *addr:
# Breakpoint na syscall de bind
(gdb) break 38
(gdb) run
(gdb) x &sockaddr
0x402000 <family>: 0xb80b0002
Sebuscarmos 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 个字节代表一个 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
如果服务器响应无 IP 响应0.0.0.0,则 4 字节的代理将为零:
(gdb) x /4xb (void*) &sockaddr+4
0x402004 <ip_address>: 0x00 0x00 0x00 0x00
E, por fim, os 8 个字节restantes 代表sin_zero , todos preenchidos com 0:
(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 +++
哎哟!重新启动后,出现 0 次错误,但出现了一些小问题。修复编号为3000 的端口,SIM 为47115,符合 vemos em htons(47115)。
系统调用绑定中隐含的htons字节序是使用转换器的功能来执行的,它是在重新使用之前对字节进行编程的。如果使用大端字节序,可以将其顺序转换为大端字节序(不包括 x86_64、小端字节序)。
Entretanto htons(47115) não é o valor que queremos。 O que precisamos é que o mapeamendo seja htons(3000)。这是什么?
十六进制表示的值是3000和0x0BB8,在没有 GBD 的情况下,它的值是小端字节反转的字节数,是0xB80B。十进制0xB80B数字 é 47115 !!!!!! Aí que está o Problema!
说明逆变器没有任何程序字节,可以通过发送或发送命令来实现正确的功能。
....
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
执行 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 +++
高超!可以通过系统调用绑定执行相关参数,包括htons(3000),返回0,但是没有任何错误。
准备接收连接
大致步骤包括准备连接器插座、基础知识和功能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表示没有套接字的“悬垂”连接的数量。 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!听听 funcionou lindamente,最后,éuma função muito simples。 Agora 无法连接客户端。
与客户的合作时刻
啊,伟大的时刻。 Vamos montar 作为系统调用接受的指令,与 libc 中的函数一起使用,接收套接字作为主要参数和选项。
%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
通过 GDB 执行程序,系统调用的结果将被执行:
# Breakpoint na syscall de socket
(gdb) break 55
(gdb) run
(gdb) next
该程序与套接字系统调用相关,并负责内核响应。如果内核响应程序继续执行,则可以使用 HTTP 客户端精确实现,并且可以使用curl:
$ curl localhost:3000
修复程序并继续执行。 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 differenterente (RAX contém 4 和 sockfd contém 3)。在一份文档中,描述了代表特定客户和服务商的新套接字通信方式的描述符名称。
Vamos 移动器或 RAX 的价值,用于保护插座,可以使用 RAX 的新功能来接受外部系统调用:
mov r8, rax ; client socket
服务人员和联络人的回复
重要的是要与客户端连接的套接字进行处理并响应请求。
Vamos 实现了 subrotina .write,为客户提供了响应(套接字):
%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
没有示例,假设 HTTP 响应字符串是根据记忆定义的.data。
CR(回车)、LF(换行)表示
\r\nHTTP 协议定义的常量和分隔符
Agora,定义了一个 subrotina .close,与客户联系:
%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 执行程序或 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 seguir foi feito o bind
- 听完
- e por fim,o接受ficou bloqueado a espera de uma requisição
Em outra janela, vamos fazer a requisição:
$ curl localhost:3000
<h1>Hello, World!</h1>
E 没有服务者,a saída do strace 没有最终的 ficou assim:
write(4, "HTTP/1.1 200 OK\r\nContent-Type: t"..., 86) = 86
close(4) = 0
exit(0) = ?
+++ exited with 0 +++
发送回复邮件write、发送邮件、发送邮件close或发送邮件exit。
Como não ficar feliz?
Mas o servidor deve ficar em Loop,não?
Sim,或服务程序开发循环,portanto ao invés de fazer o jmp .exit,fazemos jmp .acceptna ú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,在服务器终端上,与客户端连接时,没有启动循环,并且与系统调用接受的新连接有关。
服务器最终代码:
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
执行以下命令:
$ 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
这个传奇故事不存在,所以我们不能在网络浏览器中执行,最后建立网络服务器,不是吗?
结论
增加网络服务器的最终构建模式。 Aqui aprendemos conceitos sobre sockets, TCP e HTTP, com umapitada level de HTML.
Fala aí,quem não já conhecia a tag H1 do HTML? kk
这些术语称为重新套接字、绑定、监听和接受程序集的系统调用。
无论如何,这都是传奇故事的一部分,但我们无法通过线程进行交互,也可以将内存中的内存分配为线程。
敬请关注!
罗德里戈·贡萨尔维斯·德·布兰科 (Rodrigo Gonçalves de Branco)的Agradecimentos重新审视了艺术的严谨性
参考资料
使用 Bash 构建 Web 服务器
https://dev.to/leandronsp/series/19120
OSI 模型
https://en.wikipedia.org/wiki/OSI_model
TCP
https://en.wikipedia.org/wiki/Transmission_Control_Protocol
Berkeley 套接字
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








