Caríssimos leitores, o presente artigo pretende expor um pouco dos segredos desta linguagem de baixo nível, aquela mais próxima do código máquina. Como fator de motivação, é sabido que quando existe a necessidade de perceber o funcionamento de um executável, por exemplo um jogo de computador, a solução passa pela análise do código assembly. A partir dele, é possível alterar e personalizar seus processos e procedimentos, mas existe a necessidade de compreensão do código.
Este artigo não tem objetivo de aprofundar a matéria, apenas oferecer uma introdução de modo a facilitar a compreensão dos artigos de engenharia reversa do presente blog.
O artigo está organizado em diferentes Tópicos.
– Distinção entre Assembler e Assembly.
– Tópico 1 : Registos.
– Tópico 2: Tipos de Dados.
– Tópico 3: As instruções e a Pilha.
– Tópico 4: Instruções e Flags.
– Tópico 5: Instruções Lógicas.
– Tópico 6: Implementação de um programa e sua análise.
 

Assembler ou Assembly


Antes de mais convém esclarecer os conceitos de assembler e de assembly. Um erro muito comum nos indivíduos experts em informática, é falarem de assembler referindo-se à linguagem de programação, mas não, assembler é o nome dado ao programa de computador que gera o ficheiro binário a partir do código assembly.
De uma forma ainda mais simplificada, assembler : “Programa para desenvolver e compilar assembly“, assembly : “é o nome dado à linguagem de programação”.

Ferramentas de Trabalho para o Artigo

-Emu 80×86: Um assembler para criação do código-fonte. Possuí uma excelente interface e diversos módulos para debbuging.

Tópico 1: Registos

De uma forma geral, pode dizer-se que o assembly usa variáveis globais. Estas variáveis são úteis para a chamada de vários processos, como por exemplo:
– “Verificar se o utilizador digitou algo no buffer do teclado.
– “Informar o sistema que é pretendido escrever no buffer de saída para o monitor.
Estes são os dois casos mais simples e fáceis de perceber, na verdade, isto é um tudo mais complicado.
Os registos mais importantes e comuns são os listados em seguida.
1. AX – O acumulador. Compreende AH e AL, os bytes alto e baixo de AX
2. BX – A base. Compreende BH e BL, são usados como apontadores registos.
3. CX – O contador. Compreende CH e CL, é usado genericamente em ciclos iterativos (loops). 
4. DX – O deslocamento. Este é similar aos registos base (BX). Compreende DH e DL
Estes registos são definidos como gerais, pois permitem armazenar informação. Convém salientar que são todos registos de 16 bits, portanto, significa que apenas é possível guardar um inteiro positivo de 0 a 65535, ou um inteiro com sinal de -32768 até 32768.
Como mencionado, estes são registos de 16 bits. Em seguida uma imagem representativa.
Acima foi dito que AH e AL são os bytes alto e baixo, isto é, mais e menos significativos de AX. Na verdade, olhando para a imagem fica fácil perceber o porquê.
Sendo estes registos de 16 bits, por vezes são necessários registos de 32 bits e até 64 bits, conforme as necessidades. O principio passa maioritariamente pelo mesmo conceito.
Em seguida é apresentada a evolução dos registos de 16 bits para 32 bits.

1. AX – EAX
2. BX – EBX
3. CX – ECX
4. DX – EDX

 
Apenas existe a mudança de E, refere-se a extended, isto é, registo extendido. O conceito é quase o mesmo do anterior. É possível observar uma imagem que representa essa mudança em seguida.
Mais uma vez parece intuitivo, isto funciona de igual forma para os outros registos. Voltando ao registo AX, que foi o escolhido antecipadamente, é proposto um desafio. Se for pretendido armazenar 0A4Ch em AX como ficará armazenada a informação?
Nota.: No assemby a notação usada é a hexadecimal, daí o uso do valor 0A4C(hexadecimal).
Solução: AH receberá 0A e AL receberá 4C (AH=0A e AL=4C).
Dito isto, se verificar a imagem referente ao registo AX é intuitivo que tanto AH como AL contêm 8 bits de espaço reservado disponíveis na memória. Portanto, a junção dos dois (AX + AH) permite obter o registo AX de 16 bits.
De uma forma bastante simplista é possível afirmar que cada “valor” do número: 0A4C vale 4 bits, um nibble. (Definição de nibble em http://en.wikipedia.org/wiki/Nibble.)

0 – 4 bits.
A – 4 bits.
4 – 4 bits.
C – 4 bits.

Visualizando novamente a imagem referente ao AX e voltando a olhar para a solução do problema proposto, ficou claro esta subdivisão do registo na memória e o porquê de 0A ficar armazenado em AH. Na verdade, 0A são 8 bits, o tamanho total de AH.

Caso ainda persistam dúvidas, reparar no próximo caso.

AH= 03h (00000011)
AL= 10h (00010000)

Qual é o valor de AX? Basta combinar os valores de AH e AL.

AH + AL = 03h + 10h = 0310h
AX= 0310h

Para mais informação ver a tabela em: (http://pt.wikipedia.org/wiki/Inteiro_(ciência_da_computação)

Os restantes registos funcionam mais ou menos dentro desta lógica. Em seguida são apresentados os registos mais importantes.

5. CS – O segmento de código. O bloco de memória onde o código é armazenado.
6. DS – O segmento de dados. A área na memória onde os dados são armazenados.
7. ES – O segmento extra. Apenas outro segmento de dados.
8. SS – O segmento de pilha. Aqui o CPU (Central Processing Unit) armazena os endereços remotos das sub-rotinas.
10. SI– O índice de fonte. Frequentemente usado para movimentações de blocos de instruções. Este é um apontador que, com um segmento, geralmente DS, é usado pela CPU para leitura.
11. DI – O índice de destino. Novamente um apontador, que com um segmento, geralmente ES, é usado para escrita pela CPU.
12. SP – O apontador da pilha. Comummente usado com o segmento da pilha.

Estes e muitos outros são registos do assembly. Não existe a necessidade saber todos eles, visto que não é suposto programar em assembly, apenas perceber blocos de assembly, nomeadamente nos próximos artigos de engenharia reversa. Contudo, é suposto solidificar esta base de conhecimento, tão importante no contexto da engenharia reversa.

Tópico 2: Tipos de Dados

Aproveitando a ideia, é importante referir que no assembly, como noutra qualquer linguagem de programação, existem diferentes tipos de dados. Os mais usuais são os seguintes.

1. Byte : Conjunto de 8 bits.
2. Word: Conjunto de 16 bits.
3. Dword: Conjunto de 32 bits.
4. Qword: Conjunto de 64 bits.

É possível perceber que é sempre a dobrar o tipo de dados (bits) do anterior.

Tópico 3: As Instruções e a Pilha

Neste ponto é sabido que existem registos e diferentes tipos de dados no assembly. Uma possível analogia é comparar registos como prateleiras do supermercado, cada uma apenas possuí um certo tipo de produtos, mas dentro desses produtos existem diversas gamas.
Para o uso dos registos são necessárias instruções. Em seguida segue uma listagem das mais comuns. Convém referir, que estas poderão variar entre diferentes tipos de CPU.

1. MOV <destino> , <valor>MOVE. Esta instrução permite mover um valor para uma determinada posição da memória.

Ex: MOV AX, 13h.

Desta forma, 13h (10 em decimal) é movido para o registo AX. Antes da instrução, AX continha o valor zero, neste momento possuí 13h. É evidente que todas aquelas operações feitas no “Tópico 1” relacionadas com o AX, nomeadamente: “apenas foi usado o byte mais significativo de AX para armazenar o valor 13h (8 bits)“, se tornam agora mais perceptíveis.

2. INT <número> – Interrupção. Esta instrução gera uma interrupção no sistema.

Ex: INT 10h.

Neste caso era gerada uma interrupção 10h (16 em decimal). A interrupção lançada depende do conteúdo do registo AH, entre outras coisas. Por exemplo, se AX=13h e a interrupção 10h fosse gerada, o vídeo (modo gráfico) seria colocado no modo 320x200x256.

Mais precisamente:

AH seria igual a 00 – seleciona a sub-função do modo, e AL seria igual a 13h – modo gráfico 230x200x256.
Contudo, se AH=2h, e a interrupção 16h fosse gerada, isso instruiria a CPU para checkar se alguma tecla pressionada estava no buffer do teclado.
Se AH=2h, e BH = 0h e a interrupção 10h fosse gerada, então a CPU movia o cursor para a posição X em DL e posição Y em DH.
Como é possível perceber neste momento, os registos quase que são os ingredientes para fazer um bolo.

3. ADD <destino> , <valor> -Adiciona. Esta instrução soma um número ao valor armazenado em destino.

Ex: MOV AX, 0h; AX agora é igual a 0h.
ADD AX, 5h; AX agora é igual a 5h.
ADD AX, 10h; AX agora é igual a 15h.

4. SUB <destino>, <valor> – Subtrai. Esta instrução faz o inverso da adição.

Ex: MOV AX, 13h; AX agora é igual a 13h (19 decimal).
SUB AX, 5h ; AX agora é igual a 0Eh (14 decimal).

5. DEC <registo> -Decrementa. Esta instrução tem a função de decrementar, por exemplo, um registo.

Ex: MOV AX, 13h; AX agora é igual a 13h.
DEC AX; AX agora é igual a 12h.

6. INC <registo> – Incrementa. É o inverso da operação anterior.

7. CALL <procedimento> – Chama uma sub-função.

Era possível passar umas boas horas a listar e descrever as instruções do assembly, mas tal não irá acontecer. Quando existe a necessidade de perceber um programa em assembly é necessário o analista usar a ferramenta Internet e procurar/perceber o que cada uma das instruções faz. Estas instruções, como já mencionado no presente artigo, variam de CPU para CPU, mas fazem mais ou menos as mesmas operações.

-Pilhas

Duas das instruções que normalmente são usadas e que aparecem com regularidade em código assembly são as seguintes.

1. PUSH <registo>Push, coloca algo na pilha.
2. POP <registo>Pop, retira algo da pilha.

O conceito de pilha é bastante simples. Para não entrar em demasiados detalhes é apresentada a seguinte imagem, ilustrando uma pilha.
Quando é colocado um byte na pilha ele fica armazenado na base da pilha. O próximo registo ficaria na posição acima a SP. O último byte a entrar é sempre o primeiro a sair. Este é o conceito geral de pilha e é assim que funciona através das instruções push e pop.

Na prática funciona como na listagem apresentada em seguida, com todos os passos efetuados devidamente comentados.

Tópico 4: Instruções e Flags

No tópico anterior foram vistas algumas das instruções mais usuais no assembly, mas de forma propositada, foram deixadas duas das mais importantes, a fim de serem discutidas no presente tópico. A razão pela qual estas instruções foram descartadas do tópico anterior, foi porque estas fazem uso de flags. Sem mais demora, estas instruções são o CMP (comparação de dois valores) e os JUMPS (saltar para determinado endereço de memória (offset)). Esta instrução possuí imensas instruções JUMP associadas.
1. CMP AX , BX – Comparação. Compara o registo AX com o registo BX e reflete o valor da comparação (maior, menor ou igual) nas flags. É possível imaginar as flags como uma tabela, será visto mais adiante. Como se percebe, após uma instrução de CMP é normal que apareça um JUMP, e a lógica associada é muito simples. Se o valor da comparação for superior, então o salto (JUMP) é efetuado para determinada posição, se o valor for menor para outra posição e por aí em diante. O valor desta comparação é então armazenado na tabela de flags, daí a sua real importância.

2. Comparações sem sinal. 


Em seguida segue uma tabela com todas as comparações sem sinal e o valor de cada flag.
Não será exemplificado o uso destas instruções. Este será um tema a abordar no primeiro artigo de engenharia reversa. Como já referido, este artigo apenas serve como suporte à linguagem e de certa forma, ajudar o leitor a perceber o funcionamento (“por baixo”) de qualquer ficheiro executável. (Ver mais informação no blog: http://infptavares.blogspot.pt/2013/12/os-segredos-de-um-exe-pe-file-portable.html .)

3. Comparações com sinal.

Em seguida segue uma tabela com todas as comparações com sinal e o valor de cada flag.

4. Comparações menos comuns

Abaixo segue uma listagem de algumas comparações menos comuns e o valor de cada flag.

Para despoletar cada uma das instruções acima mencionadas, a CPU analisa de forma individual as flags da tabela, e verifica o bit que elas armazenam. Consoante o seu valor é disparado o respetivo salto (jump). Em seguida é apresentada a tabela de flags do assembly.

5. Flags

Legenda:

SFFlag de Sinal.
ZF Flag de Zero.
AF Flag Auxiliar.
PF Flag de Paridade.
CF Flag de Carry.

Convém lembrar que existem muitas mais, mas estas são as usuais.

Tópico 5: Instruções Lógicas

Completando a família das instruções do assembly, existe um tipo de instruções que ainda não foram discutidas no artigo, as instruções lógicas. Elas são bastante usadas. Em seguida segue uma tabela com as seguintes intruções.
Este tipo de instruções são vistas com frequência antes das instruções de comparação (CMP). Por vezes, existe a necessidade de as efetuar. Das instruções listadas, a mais frequente costuma ser o XOR.

Tópico 6: Implementação de um programa e sua análise

O presente tópico apresenta a implementação de um pequeno exercício exemplo na linguagem assembly. Antes de avançar com problemas concretos de engenharia reversa, convém lidar e praticar exercícios deste tipo, de forma a calcificar o conhecimento adquirido até este ponto. Neste sentido, foi proposto o famoso exercício Hello World, onde cada passo é devidamente comentado. Para a sua implementação é usado o software referido no início do artigo.

1. Criar um novo projeto no Emu80x86.

Para a criação de um novo projeto neste ambiente de desenvolvimento e emulador, apenas é necessário aceder ao menu file > new e selecionar a opção exe template. Podia ter sido escolhido um com template, mas como os próximos tutoriais de engenharia reversa são perante ficheiros executáveis (.exe), foi optado o seu uso neste mini tutorial. Segue uma imagem abaixo a demonstrar esse processo.

2. Código gerado por defeito.

Consequentemente à criação do ficheiro, o IDE (Integrated development environment) gera algum código por defeito, nomeadamente as secções do assembly. Este foi um assunto que não foi mencionado no artigo, visto não haver grande necessidade em fazê-lo. Será discutido sem grande detalhe abaixo. Em seguida é apresentada a janela de código gerada pelo IDE.
Como é fácil de perceber, o IDE comentou o local onde será escrito o código. Antes de mais, foi feito um comentário às linhas geradas, por forma a perceber todas as instruções. Segue a imagem em seguida.
O presente ficheiro segue então a configuração padrão de qualquer ficheiro vulgar escrito em assembly. Segue abaixo um protótipo ainda mais base.

Observando-o é fácil criar uma relação com o projecto gerado pelo IDE. Para os mais curiosos segue em seguida uma listagem a descrever cada uma das secções.

a) DOSSEG : Diz à CPU como organizar o segmento.
b) MODEL : Declara o modelo a usar. (Poderá explorar mais sobre este ponto se achar necessário.)
c) STACK : Qual o tamanho de pilha a alocar.
d) DATA : O que vai conter o segmento de dados.
e) CODE : O que vai conter o segmento de código.
f) START: Início do código.
g) END START: Fim do código.

Posto isto, estão lançados os ingredientes para fazer o bolo, neste caso, escrever o código exemplo.

Foi criada uma sub-função _proc: com o código acima. Penso não ser necessário estar a referir cada uma das linhas, pois elas estão comentadas.
Por fim, de maneira a organizar o documento foi chamada a sub-função através da instrução CALL, como já tinha sido estudado uns tópicos acima.
Antes de terminar, convém referir que este artigo é de caráter importantíssimo para os próximos artigos sobre engenharia reversa.
Em anexo é disponibilizado o projecto .asm para download.

Boa continuação!


3 Replies to “Introdução ao Assembly

  1. There is clearly a bundle to realize about this. I suppose you made some good points in features also.

  2. Wonderful job. I really enjoyed what you had to say, and more than that, how you presented it. Too cool!

Comments are closed.