Hi,
Hoje eu acordei cedo e fiquei um bom tempo sem nada para fazer. Para não perder esse tempo, tomei a decisão de escrever algo. Procurando um assunto, me deparei com o FASM, que há muito tempo não utilizava. Então, tive a idéia de escrever algo relacionado a sockets em ASM. É exatamente isso que veremos hoje =)
Antes de começarmos, irei disponibilzar o link para download do compilador utilizado:
http://flatassembler.net/fasmw167.zip (http://flatassembler.net/fasmw167.zip)
Eu, particularmente, gosto muito do FASM, pois além de ser apresentar como um compilador bem simples e leve, nos permite aplicar funções da API do Windows entre outros recursos com muita facilidade.
Baixe o arquivo e descompacte-o. Eu criei a pasta "C:\FASM" para descompactar os arquivos.
Após extrair os arquivos, abra o arquivo "FASMW.EXE". A partir daí, já podemos introduzir nosso código =)
Vamos ver a estrutura básica de um programa WIN32:
format PE GUI 4.0
include "c:\fasm\include\win32ax.inc"
start:
invoke MessageBox,0,"HI!","LOL!",0x40
invoke ExitProcess,0
data import
library kernel32,'KERNEL32.DLL'
user32,'USER32.DLL'
import kernel32,\
ExitProcess,'ExitProcess'
import user32,\
MessageBox,'MessageBoxA'
end data
O que o programa acima faz? Lolz, apenas mostra uma mensagem e encerra imediatamente após ser carregado.
Vamos desmembrar o código:
format PE GUI 4.0
Define que o programa é para Windows.
include "c:\fasm\include\win32ax.inc"
Inclui o cabeçalho do Windows. Algumas funções vitais estão incluídas nele.
Podemos optar pelo uso do header "win32a.inc" ao invés deste. Mas qual seria a diferença? Bem, utilizando o header "win32a.inc" teremos que abrir mão de algumas facilidades ao programar, como por exemplo passar strings diretamente a funções não necessitando declarar variáveis. Um exemplo:
WIN32AX.INC
MessageBox,0,"HI!","LOL",0
-----------------------------------
WIN32A.INC
MessageBox,0,msg,0,tit,0
msg db "HI!",0
tit db "LOL",0
Note que "c:\fasm\include" é o diretório onde os headers estão contidos. Lembre-se de mudar para o diretório correto, caso necessário.
start:
invoke ExitProcess,0
É o ponto de entrada do programa. Invocamos (chamamos) a função MessageBox para exibir a mensagem e, logo após, ExitProcess para encerrar o programa.
data import
library kernel32,'KERNEL32.DLL'
user32,'USER32.DLL'
import kernel32,\
ExitProcess,'ExitProcess'
import user32,\
MessageBox,'MessageBoxA'
end data
A parte acima é destinada à importação de funções e bibliotecas.
library kernel32,'KERNEL32.DLL'
user32,'USER32.DLL'
O programa importa as DLL's "KERNEL32.DLL" e "USER32.DLL".
import kernel32,\
ExitProcess,'ExitProcess'
O programa importa a função "ExitProcess" dentro da DLL "KERNEL32".
import user32,\
MessageBox,'MessageBoxA'
O programa importa a função "MessageBox" dentro da DLL "USER32".
Para fazer o teste, salve o código como "exemplo.asm", e em seguida, vá até o menu RUN -> Execute.
Após uma pequena base, vamos ver a estrutura básica de um programa que utiliza sockets:
format PE GUI 4.0
include "c:\fasm\include\win32ax.inc"
start:
sair:
invoke ExitProcess,0
data import
library kernel32,'KERNEL32.DLL',\
user32,'USER32.DLL',\
winsock,'WSOCK32.DLL'
import kernel32,\
ExitProcess,'ExitProcess'
import user32,\
MessageBox,'MessageBoxA'
import winsock,\
WSAStartup,'WSAStartup',\
socket,'socket',\
connect,'connect',\
accept,'accept',\
bind,'bind',\
listen,'listen',\
send,'send',\
sendto,'sendto',\
recv,'recv',\
recvto,'recvto',\
closesocket,'closesocket',\
inet_addr,'inet_addr',\
htons,'htons',\
gethostbyname,'gethostbyname',\
WSACleanup,'WSACleanup'
end data
wsadata WSADATA
sock dd 0
sock_addr sockaddr_in
size_addr dd 16
Além das DLLS já utilizadas anteriormente, importamos a DLL "WSOCK32.DLL" - biblioteca do WINSOCK.
Vejamos as funções importadas:
import winsock,\
WSAStartup,'WSAStartup',\
socket,'socket',\
connect,'connect',\
accept,'accept',\
bind,'bind',\
listen,'listen',\
send,'send',\
sendto,'sendto',\
recv,'recv',\
recvto,'recvto',\
closesocket,'closesocket',\
inet_addr,'inet_addr',\
htons,'htons',\
gethostbyname,'gethostbyname',\
WSACleanup,'WSACleanup'
QuoteWSAStartup - utilizada para inicializar o winsock;
socket - criar um socket;
connect - conectar-se a um host;
accept - aceitar uma conexão;
bind - configurar socket localmente;
listen - escuta em uma porta;
send - enviar dados para uma conexão ativa;
sendto - enviar dados para um host com conexão não necessariamente ativa (geralmente protocolo UDP);
recv - receber dados de uma conexão ativa;
recvto - receber dados de um host com conexão não necessariamente ativa (geralmente protocolo UDP);
closesocket - fechar socket;
inet_addr - transformar um IP de string (xxx.xxx.xxx.xxx) para valor numérico;
htons - transformar uma porta em network byte;
gethostbyname - resolver um hostname;
WSACleanup - finalizar winsock;
Embora existam outras funções, estas são as principais.
Abaixo, nós temos a declaração das variáveis que serão utilizadas para trabalhar com sockets:
wsadata WSADATA
sock dd 0
sock_addr sockaddr_in
size_addr dd 16
Quotewsadata - variável auxiliar para inicializar o winsock;
sock - nome atribuído ao socket;
sock_addr - struct que contém as configurações do socket: porta, host e família;
size_addr - contém o tamanho (bytes) da estrutura sockaddr_in.
Irei dividir o artigo em duas partes: esta, onde iremos abordar o protocolo TCP e uma segunda, onde o protocolo UDP será abordado.
Para começar, vamos ver como criar um socket simples:
start:
invoke WSAStartup,0x101,addr wsadata
cmp eax,-1
je erro_wsa
invoke socket,AF_INET,SOCK_STREAM,0
cmp eax,-1
je erro_socket
mov [sock],eax
invoke WSACleanup
jmp sair
erro_wsa:
invoke MessageBox,0,"Ocorreu um erro ao inicializar o winsock","Erro",0x10
jmp sair
erro_socket:
invoke MessageBox,0,"Ocorreu um erro ao criar o socket","Erro",0x10
jmp sair
sair:
invoke ExitProcess,0
Veja o trecho seguinte:
invoke WSAStartup,0x101,addr wsadata
cmp eax,-1
je erro_wsa
Temos a incialização do winsock neste bloco. O processo é feito pela função WSAStartup:
Quoteinvoke WSAStartup,VERSAO,PTWSADATA
VERSAO:
versão do winsock que será utilizada. (0x101 ou 101h = 1.1).
PTWSADATA:
ponteiro para uma variável do tipo WSADATA;
Em caso de erro, a função retorna o valor -1 em EAX.
Note que se o valor de EAX for -1, o programa pula para "erro_wsa".
Se você observar bem, o segundo parâmetro da função exige um ponteiro para uma variável do tipo WSADATA. É por este mesmo motivo que utilizamos a instrução "addr" antes da variável "wsadata" - para passar o seu endereço de memória para o ponteiro =).
A criação do socket é feita no seguinte bloco de código:
invoke socket,AF_INET,SOCK_STREAM,0
cmp eax,-1
je erro_socket
A função socket é a responsável por criá-lo. Sua sintaxe:
Quoteinvoke socket,FAMÍLIA,PROTOCOLO,TIPO
FAMÍLIA:
seria a família cuja a qual o socket pertence. A família AF_INET é utilizada para protocolos relacionados à internet.
PROTOCOLO:
protocolo a usar. No artigo veremos dois deles: SOCK_STREAM (protocolo TCP) e SOCK_DGRAM (protocolo UDP).
TIPO:
valor opcional. Geralmente utilizado em RAW_SOCKETS. No artigo, seu valor é usado como ZERO.
Novamente, caso um erro ocorra ao criar o socket, o registrador EAX terá o valor -1. Caso um erro de fato ocorra, o programa pula para "erro_socket". Na ausência de erro, o valor de EAX passa a ser o número de identificação do socket.
Caso a inicialização e criação de sockets não apresentarem erros, atribuímos à variável do socket, o valor apropriado para o novo socket:
mov [sock],eax
invoke WSACleanup
jmp sair
Obs:: para passar um valor de um registrador para uma variável fazemos:
mov [nome_var],REGISTRADOR
Note que apenas criamos um socket, finalizamos o winsock e encerramos o programa.
As mensagens de erro são chamadas da seguinte forma:
erro_wsa: ' Erro ao incializar
invoke MessageBox,0,"Ocorreu um erro ao inicializar o winsock","Erro",0x10
jmp sair
erro_socket: ' Erro ao criar socket
invoke MessageBox,0,"Ocorreu um erro ao criar o socket","Erro",0x10
jmp sair
Repare que os dois trechos chamam pelo label "sair", que contém:
sair:
invoke ExitProcess,0
Ou seja, finaliza o programa.
Aguardando e recebendo conexões
-------------------------------------------
Abaixo nós temos um socket que aguarda e aceita conexões na porta 1234:
start:
invoke WSAStartup,0x101,addr wsadata
cmp eax,-1
je erro_wsa
invoke socket,AF_INET,SOCK_STREAM,0
cmp eax,-1
je erro_socket
mov [sock],eax
mov [sin.sin_family],AF_INET
mov [sin.sin_addr],0
invoke htons,1234
mov [sin.sin_port],ax
invoke bind,[sock],addr sin,[size_addr]
cmp eax,-1
je erro_bind
invoke listen,[sock],1
cmp eax,-1
je erro_listen
invoke accept,[sock],0,0
mov [sock],eax
invoke MessageBox,0,"Um pedido de conexão foi feito =)","Lol",0x40
invoke closesocket,[sock]
invoke WSACleanup
jmp sair
erro_wsa:
invoke MessageBox,0,"Ocorreu um erro ao inicializar o winsock","Erro",0x10
jmp sair
erro_socket:
invoke MessageBox,0,"Ocorreu um erro ao criar o socket","Erro",0x10
jmp sair
erro_bind:
invoke MessageBox,0,"Ocorreu um erro ao configurar o socket localmente","Erro",0x10
jmp sair
erro_listen:
invoke MessageBox,0,"Ocorreu um erro ao colocar o socket na escuta","Erro",0x10
jmp sair
sair:
invoke ExitProcess,0
O programa cria o socket, coloca-o aguardando na porta 1234, mostra uma mensagem quando um pedido de conexão é feito, fecha o socket e encerra (lol!).
Vamos destacar os pontos importantes:
mov [sin.sin_family],AF_INET
mov [sin.sin_addr],0
invoke htons,1234
mov [sin.sin_port],ax
O trecho acima é responsável pela configuração dos seguintes parâmetros: família do socket, porta e host.
mov [sin.sin_family],AF_INET -> ajusta a família: sin.sin_family = AF_INET
mov [sin.sin_addr],0 -> zera o host permitindo-o ficar em escuta.
invoke htons,1234
mov [sin.sin_port],ax
Primeiramente, chamamos a função htons() para transformar o valor 1234 (porta) para network byte - valor requerido pela estrutura. A função htons() retorna um valor do tamanho WORD, isto é, 16 bits que é armazenado no registrador AX. O membro
"sin.sin_port" também possui 16 bits, portanto, atribuímos a ele o valor em AX.
invoke bind,[sock],addr sin,[size_addr]
cmp eax,-1
je erro_bind
As instruções acima fazem com que o socket seja configurado localmente, isto é, o prepara para ficar no modo de escuta (listen).
QuoteA função bind() é responsável por tal. Sua sintaxe é:
invoke bind,SOCKET,PTADDR,sizeADDR
SOCKET:
variável que representa o nosso socket;
PTADDR:
ponteiro para a estrutura sockaddr_in;
sizeADDR:
tamanho da estrutura sockaddr_in (que é igual a 16).
A função retorna em EAX o valor -1 caso o processo falhe. Se, de fato falhar, o programa pula para "erro_bind".
Note que passamos o terceiro argumento da função bind() através da variável "size_addr".
O socket é verdadeiramente colocado em escuta através da função listen():
invoke listen,[sock],1
cmp eax,-1
je erro_listen
A sintaxe é:
Quoteinvoke listen,SOCKET,NUM
SOCKET:
variável do socket;
NUM:
número de conexões que serão escutadas.
Grande parte das funções relacionadas a sockets retorna, em EAX, o valor -1 quando um erro ocorre. Com a função listen() não é diferente. Desta vez, o label "erro_listen" seria chamado.
invoke accept,[sock],0,0
A linha acima faz com que o socket fique escutando na porta 1234 até que um pedido de conexão seja feito.
A sintaxe é:
Quoteinvoke accept,SOCKET,NOVO_SOCKADDR,PTsizeADDR
SOCKET:
socket que está em modo listen;
NOVO_SOCKADDR:
parâmetro opcional. Especifica uma nova estrutura sockaddr_in contendo informações sobre o host remoto;
NOVO_SOCKADDR:
também opcional. Ponteiro para uma variável contendo o tamanho da estrutura sockaddr_in.
A função retorna -1 na ocorrência de falhas ou retorna um novo valor para o socket que contém novas informações sobre a conexão.
Após a conexão ter sido feita, devemos associar novamente um valor ao socket, desta vez, o novo valor retornado em EAX:
mov [sock],eax
invoke MessageBox,0,"Um pedido de conexão foi feito =)","Lol",0x40
Já sabemos que após uma conexão ter sido aceita, uma mensagem é mostrada.
Simplesmente fechamos o socket com:
Quoteinvoke closesocket,[sock]
Onde "sock" é a variável do socket ;)
Conectando-se
-------------------------------------------
O código abaixo mostra um exemplo de um programa que se conecta na porta 1234 do host local:
start:
invoke WSAStartup,0x101,addr wsadata
cmp eax,-1
je erro_wsa
invoke socket,AF_INET,SOCK_STREAM,0
cmp eax,-1
je erro_socket
mov [sock],eax
mov [sin.sin_family],AF_INET
invoke inet_addr,"127.0.0.1"
mov [sin.sin_addr],eax
invoke htons,1234
mov [sin.sin_port],ax
invoke connect,[sock],addr sin,[size_addr]
cmp eax,-1
je erro_con
invoke MessageBox,0,"Conectado!","Lol",0x40
invoke closesocket,[sock]
invoke WSACleanup
jmp sair
erro_wsa:
invoke MessageBox,0,"Ocorreu um erro ao inicializar o winsock","Erro",0x10
jmp sair
erro_socket:
invoke MessageBox,0,"Ocorreu um erro ao criar o socket","Erro",0x10
jmp sair
erro_con:
invoke MessageBox,0,"Ocorreu um erro ao se conectar","Erro",0x10
jmp sair
sair:
invoke ExitProcess,0
Os pontos importantes:
invoke inet_addr,"127.0.0.1"
Chamamos pela função "inet_addr" responsável por transformar um IP no formato STRING é valor numérico - valor requerido pela estrutura.
Este valor é retornado em EAX, por isso, fazemos:
mov [sin.sin_addr],eax -> Define o IP remoto a se conectar
Novamente, temos a especificação da porta:
invoke htons,1234
mov [sin.sin_port],ax
Desta vez, porém, essa será utilizada na conexão com o computador remoto.
invoke connect,[sock],addr sin,[size_addr]
cmp eax,-1
je erro_con
Tentamos nos conectar ao host através da função connect() cuja sintaxe é:
Quoteinvoke connect,SOCKET,PTSockAddr,sizeADDR
SOCKET:
variável do socket;
PTSockAddr:
ponteiro para a estrutura sockaddr_in;
SizeAddr:
tamanho da estrutura (16).
Um possível erro faria com que o valor -1 fosse retornado em EAX. No fato, o label "erro_con" seria chamado.
As linhas abaixo apenas mostram uma mensagem e fecha o socket, respectivamente:
invoke MessageBox,0,"Conectado!","Lol",0x40 ; Mostra uma notificação caso o programa se conecte ao host
invoke closesocket,[sock]
Note que especificamos o host pelo seu IP. Contudo, podemos obter este IP resolvendo o DNS de um determinado host.
Em outras palavras, quero dizer que não podemos fazer por exemplo:
invoke inet_addr,"www.google.com.br"
Temos que resolver o host "www.google.com.br" em seguida obter seu IP. Isto é feito através da função gethostbyname().
Esta função requere uma nova estrutura - a estrutura HOSTENT. Iremos inclui-la junto às variáveis:
wsadata WSADATA
sock dd 0
sock_addr sockaddr_in
size_addr dd 16
host hostent ; Declaração da estrutura HOSTENT
Vejamos agora o exemplo:
start:
invoke WSAStartup,0x101,addr wsadata
cmp eax,-1
je erro_wsa
invoke socket,AF_INET,SOCK_STREAM,0
cmp eax,-1
je erro_socket
mov [sock],eax
mov [sin.sin_family],AF_INET
invoke gethostbyname,"localhost"
cmp eax,0
je erro_host
mov eax,[eax+12]
mov eax,[eax]
mov eax,[eax]
mov [sin.sin_addr],eax
invoke htons,1234
mov [sin.sin_port],ax
invoke connect,[sock],addr sin,[size_addr]
cmp eax,-1
je erro_con
invoke MessageBox,0,"Conectado!","Lol",0x40
invoke closesocket,[sock]
invoke WSACleanup
jmp sair
erro_wsa:
invoke MessageBox,0,"Ocorreu um erro ao inicializar o winsock","Erro",0x10
jmp sair
erro_socket:
invoke MessageBox,0,"Ocorreu um erro ao criar o socket","Erro",0x10
jmp sair
erro_con:
invoke MessageBox,0,"Ocorreu um erro ao se conectar","Erro",0x10
jmp sair
erro_host:
invoke MessageBox,0,"Ocorreu um erro ao resolver o host","Erro",0x10
jmp sair
sair:
invoke ExitProcess,0
Vejamos:
invoke gethostbyname,"localhost"
cmp eax,0
je erro_host
Nós tentamos resolver o host "localhost". Caso a função gethostbyname() não consiga resolver o host, é retornado o valor ZERO em EAX, fazendo com que o programa pule para "erro_host".
A sintaxe seria:
Quotegethostbyname,HOSTNAME
Onde "HOSTNAME" é o host que se tenta resolver =)
A seqüência abaixo é responsável por obter o IP do host:
mov eax,[eax+12] ; Move para EAX o membro h_addr_list - o que contém a lista de IPs para o hostname
mov eax,[eax] ; Obtém o primeiro IP - principal
mov eax,[eax] ; Atribui o valor numérico deste IP a EAX
Já que temos o IP em EAX já convertido para o valor requerido, fazemos:
mov [sin.sin_addr],eax ; Agora o membro sin_addr contém o IP do host "localhost" ;)
A partir daí, podemos utilizar a função connect() para estabelecer uma conexão:
invoke connect,[sock],addr sin,[size_addr]
Recebendo e enviando
-------------------------------------------
Para receber e enviar dados através de um socket, utilizamos, respectivamente, as funções send() e recv().
O envio de dados é muito simples, veja:
invoke send,[sock],"LOL",3,0
O programa enviaria "LOL" para o computador remoto.
Sintaxe:
Quoteinvoke send,SOCKET,DADOS,TAM,0
SOCKET:
o nosso socket;
DADOS:
dados a serem enviados;
TAM:
número de bytes a serem enviados;
FLAGS:
valores opcionais, geralmente deixado como ZERO.
Outra maneria de utilizar a função send() é passando um buffer como argumento, veja:
invoke send,[sock],Mensagem,3,0
; Na seção da variáveis:
Mensagem db "Lol",0
Para receber dados, devemos declarar um buffer na seção das variáveis:
buffer db 512 dup(0)
O buffer acima pode armazenar 512 bytes =)
Na seção do código, fazemos:
invoke recv,[sock],buffer,512,0
Sintaxe:
Quoteinvoke recv,SOCKET,BUFFER,TAM,FLAGS
SOCKET:
o nosso socket de novo 
BUFFER:
buffer que irá armazenar os dados;
TAM:
tamanho deste buffer;
FLAGS:
valores opcionais, geralmente deixado como ZERO.
A função recv retorna em EAX o valor -1 na ocorrência de falhas, como a perda da conexão por exemplo, ou retorna o número de bytes recebidos.
Um exemplo:
invoke recv,[sock],buffer,512,0
invoke MessageBox,0,buffer,"O computador remoto enviou",0x40
; Nas declarações de variáveis:
buffer db 512 dup(0)
O programa acima, assim que recebesse dados do socket, mostraria-os.
Muito bem, termino de abordar algo bem básico sobre sockets em ASM e espero que tenha entendido alguma coisa do que eu escrevi =)
Na próxima parte, iremos abordar o protocolo UDP.
Bye.
Excelente cara, e ninguém pra comentar... mto bom mesmo.
Excelente.. ótimo.. bom a logica deu pra entender. agora o código ja eh mais complicado.
Antigo o post mais sendo do Dark vale a pena comentar em qualquer circunstancia.
Realmente ninguém comenta em coisas assim e realmente não sei o porque.
Parabéns Dark, sabe que falo que sou um admirador nato teu toda vez que falamos sobre codar. rs
bjxx ..