Skip to content

Latest commit

 

History

History
140 lines (89 loc) · 7.49 KB

File metadata and controls

140 lines (89 loc) · 7.49 KB
description
Explicando os atributos das instruções da arquitetura x86.

Atributos

Você já deve ter reparado que as instruções têm mais informações do que nós explicitamos nelas. Por exemplo a instrução mov eax, [0x100] implicitamente acessa a memória a partir do segmento DS, além de que magicamente a instrução tem um tamanho específico de operando sem que a gente diga a ela.

Todas essas informações implícitas da instrução são especificadas a partir de atributos que tem determinados valores padrões que podem ser modificados. Os três atributos mais importantes para a gente entender é o operand-size, a_ddress-size_ e segment.

{% hint style="info" %} O opcode é um byte do código de máquina que especifica a operação a ser executada pelo processador. Em algumas instruções mais alguns bits de outro byte da instrução em código de máquina é utilizado para especificar operações diferentes, que é o campo REG do byte ModR/M. Como o já citado far call por exemplo. {% endhint %}

Operand-size

Em protected mode nós podemos acessar operandos de 32, 16 ou 8 bits. O que define o tamanho do operando na instrução é o atributo operand-size.

Instruções que lidam com operandos de 8 bits tem opcodes próprios só para eles. Mas as instruções que lidam com operandos de 16 e 32 são as mesmas instruções, mudando somente o atributo operand-size.

Vamos fazer um experimento com o código abaixo:

{% code title="tst.asm" %}

bits 32

mov ah,  bh
mov eax, ebx

{% endcode %}

Compile esse código sem especificar qualquer formatação para o NASM, assim ele irá apenas colocar na saída as instruções que escrevemos:

$ nasm tst.asm -o tst

Depois disso use o ndisasm especificando para desmontar instruções como de 32 bits, e depois, como de 16 bits. A saída ficará como no print abaixo:

Repare que tanto em 32 quanto 16 bits a instrução mov ah, bh não muda. Porém as instruções mov eax, ebx e mov ax, bx são a mesma instrução.

Só o que muda de um para outro é o operand-size. Enquanto em 32-bit por padrão o operand-size é de 32 bits, em 16-bit ele é de 16-bit. Por isso que se dizemos para o disassembler que as instruções são de 16-bit ele desmonta a instrução como mov ax, bx. Porque é de fato essa operação que o processador em modo de 16-bit iria executar, não é um erro do disassembler.

E isso não vale só para registradores mas também para operandos imediatos e operandos em memória. Vamos fazer outro experimento:

{% code title="tst.asm" %}

bits 32

mov eax, 0x11223344

{% endcode %}

Os comandos:

$ nasm tst.asm -o tst
$ ndisasm -b32 tst
$ ndisasm -b16 tst

A saída fica assim:

Entendendo melhor a saída do ndisasm:

  • A esquerda fica o raw address da instrução em hexadecimal, que é um nome bonitinho para o índice do primeiro byte da instrução dentro do arquivo (contando a partir de 0).
  • No centro fica o código de máquina em hexadecimal. Os bytes são mostrados na mesma ordem em que estão no arquivo binário.
  • Por fim a direita o disassembly das instruções.

Repare que quando dizemos para o ndisasm que as instruções são de 32-bit ele faz o disassembly correto e mostra mov eax, 0x11223344. Porém quando dizemos que é de 16-bit ele desmonta mov ax, 0x3344 seguido de uma instrução que não tem nada a ver com o que a gente escreveu.

Se você prestar atenção no código de máquina vai notar que nosso operando imediato 0x11223344 está bem ali em little-endian logo após o byte B8 (o opcode). Porque é assim que operandos imediatos são dispostos no código de máquina, o valor imediato faz parte da instrução.

Agora no segundo caso quando dizemos que são instruções de 16-bit a instrução não espera um operando de 4 bytes mas sim 2 bytes. Por isso o disassembler considera isto aqui como a instrução:

B8 44 33

Os bytes 22 11 ficam sobrando e acabam sendo desmontados como se fossem uma instrução diferente. Na prática o processador também executaria o código da mesma maneira que o ndisasm o desmontou, um dos motivos do porque código de modos de processamento diferentes não são compatíveis entre si.

{% hint style="info" %} Em 64-bit o operand-size também tem 32 bits de tamanho por padrão. {% endhint %}

Address-size

O atributo de address-size define o modo de endereçamento. O tamanho padrão do offset acompanha a largura do barramento interno do processador (ou o tamanho do Instruction Pointer).

Quando o processador está em modo de 16-bit pode-se usar endereçamento de 16 ou 32 bits. O mesmo vale para modo de 32-bit onde se usa por padrão 32 bits de endereçamento mas dá para usar modo de endereçamento de 16 bits.

Já em 64-bit o address-size é de 64 bits por padrão, mas também é possível usar endereçamento de 32 bits.

{% hint style="info" %} Apesar do offset e RIP no submodo de 64-bit serem de 64 bits (8 bytes) de tamanho, na prática o barramento de endereço do processador tem apenas 48 bits (6 bytes) de tamanho.

Os dois bytes mais significativos de RIP não são usados e devem sempre estarem zerados. Endereços acima de 0x0000FFFFFFFFFFFF não são válidos em x86-64. {% endhint %}

Mas o atributo não muda somente o tamanho do offset mas todo ele devido ao fato de haver diferenças entre o modo de endereçamento de 16-bit e de 32-bit. Observe o disassembly no print:

A instrução mov byte [bx], 42 compilada para 16-bit não altera apenas o tamanho do registrador, quando está em 32-bit, mas também o registrador em si. Isso acontece devido as diferenças de endereçamento já explicadas neste livro em A base→Endereçamento.

Agora observe a instrução mov byte [ebx], 42 compilada para 32-bit:

Desta vez a diferença entre 32-bit e 64-bit foi unicamente relacionado ao tamanho. Mas agora um último experimento: mov byte [r12], 42. Desta vez com um registrador que não existe uma versão menor em 32-bit.

Existem duas diferenças: o registrador mudou para ESP e um byte 41 ficou sobrando antes da instrução. Dando um pouco de spoiler do próximo tópico do livro, o byte que sobrou ali é o prefixo REX que não existe em 32-bit e por isso foi interpretado como outra instrução.

Segment

Como explicado no tópico que fala sobre registradores de segmentos algumas instruções fazem o endereçamento em determinados segmentos. O atributo de segmento padrão é definido de acordo com qual registrador é usado como base no endereçamento.

Registrador base Segmento
RIP CS
SP/ESP/RSP SS
BP/EBP/RBP SS
Qualquer outro registrador DS

Exemplos:

mov eax, [rbx]  ; Lê do endereço DS:RBX
mov eax, [rbp]  ; Lê do endereço SS:RBP

{% hint style="info" %} Determinadas instruções usam segmentos específicos, como é o caso da movsb. Onde ela acessa DS:RSI e ES:RDI. {% endhint %}