Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Entendendo os byte ModR/M e SIB.
Como já foi mencionado anteriormente o byte ModR/M é usado em algumas instruções para especificar o operando na memória ou registrador.
Em Assembly existem dois "tipos" de instruções que recebem dois operandos:
As que tem um operando registrador e imediato. Exemplo: mov eax, 123
As que tem um operando na memória ou dois operandos registradores. Exemplos: mov [ebx], 123
e mov eax, ebx
.
O primeiro tipo não precisa do byte ModR/M, pois o registrador destino é especificado nos 3 últimos bits do byte do opcode. Por exemplo o opcode B8
da instrução mov eax, 123
é o seguinte em binário: 10111000
Onde o número zero (000
) é o código para identificar o registrador EAX.
Um jeito mais simples de especificar esse campo no opcode sem precisar lidar com binário é simplesmente somar o opcode "base" (correspondente ao uso de AL/AX/EAX) mais o código do registrador. Por exemplo se a instrução B8
(B8 + 0
) corresponde a mov eax, 123
, então o opcode BB
(B8 + 3
) é mov ebx, 123
. E se eu quiser fazer mov bx, 123
basta adicionar o prefixo 66
à instrução.
Já as instruções do segundo tipo usam o byte ModR/M para definir o operando destino na memória (no caso de instruções sem o operando registrador) ou os dois operandos. Onde o byte ModR/M consiste nos três campos:
MOD
- Os primeiros 2 bits que definem o "modo" do operando R/M.
REG
- Os 3 próximos bits que definem o código do operando registrador.
R/M
- Os 3 últimos bits que definem o código do operando R/M.
O byte define 2 operandos:
Um operando que é sempre um registrador, definido no campo REG
.
Um operando que pode ser um registrador ou operando na memória.
Para que o campo R/M
defina também o código de um registrador, assim como o REG
, o valor 3 (11
em binário) deve ser usado no campo MOD
.
Um adendo sobre o byte ModR/M é que em algumas instruções o campo REG
é usado como uma extensão do opcode.
É o caso por exemplo das instruções inc dword [ebx]
(FF 03
) e dec dword [ebx]
(FF 0B
) que contém o mesmo byte de opcode mas fazem operações diferentes.
Repare como o campo R/M é necessário para especificar o operando na memória mas o REG fica "sobrando", por isso os engenheiros da Intel tomaram essa decisão minimamente confusa (vulgo gambiarra), afim de aproveitar dessa peculiaridade em instruções que precisam de um operando na memória mas não precisam de um operando registrador.
Para os demais valores do campo MOD
os seguintes endereçamentos são feitos de acordo com o valor de R/M
:
000
[BX+SI]
001
[BX+DI]
010
[BP+SI]
011
[BP+DI]
100
[SI]
101
[DI]
110
displacement 16-bit
111
[BX]
000
[BX+SI]
+ displacement 8-bit
001
[BX+DI]
+ displacement 8-bit
010
[BP+SI]
+ displacement 8-bit
011
[BP+DI]
+ displacement 8-bit
100
[SI]
+ displacement 8-bit
101
[DI]
+ displacement 8-bit
110
[BP]
+ displacement 8-bit
111
[BX]
+ displacement 8-bit
000
[BX+SI]
+ displacement 16-bit
001
[BX+DI]
+ displacement 16-bit
010
[BP+SI]
+ displacement 16-bit
011
[BP+DI]
+ displacement 16-bit
100
[SI]
+ displacement 16-bit
101
[DI]
+ displacement 16-bit
110
[BP]
+ displacement 16-bit
111
[BX]
+ displacement 16-bit
000
[eax]
001
[ecx]
010
[edx]
011
[ebx]
100
SIB
101
displacement 32-bit
110
[esi]
111
[edi]
000
[eax]
+ displacement 8-bit
001
[ecx]
+ displacement 8-bit
010
[edx]
+ displacement 8-bit
011
[ebx]
+ displacement 8-bit
100
SIB + displacement 8-bit
101
[ebp]
+ displacement 8-bit
110
[esi]
+ displacement 8-bit
111
[edi]
+ displacement 8-bit
000
[eax]
+ displacement 32-bit
001
[ecx]
+ displacement 32-bit
010
[edx]
+ displacement 32-bit
011
[ebx]
+ displacement 32-bit
100
SIB + displacement 32-bit
101
[ebp]
+ displacement 32-bit
110
[esi]
+ displacement 32-bit
111
[edi]
+ displacement 32-bit
Devido ao prefixo REX o campo R/M é estendido em 1 bit no modo de 64-bit.
0000
[rax/eax]
0001
[rcx/ecx]
0010
[rdx/edx]
0011
[rbx/ebx]
0100
SIB
0101
[rip/eip]
+ displacement 32-bit
0110
[rsi/esi]
0111
[rdi/edi]
1000
[r8/r8d]
1001
[r9/r9d]
1010
[r10/r10d]
1011
[r11/r11d]
1100
SIB
1101
[rip/eip]
+ displacement 32-bit
1110
[r14/r14d]
1111
[r15/r15d]
0000
[rax/eax]
+ displacement 8-bit
0001
[rcx/ecx]
+ displacement 8-bit
0010
[rdx/edx]
+ displacement 8-bit
0011
[rbx/ebx]
+ displacement 8-bit
0100
SIB + displacement 8-bit
0101
[rbp/ebp]
+ displacement 8-bit
0110
[rsi/esi]
+ displacement 8-bit
0111
[rdi/edi]
+ displacement 8-bit
1000
[r8/r8d]
+ displacement 8-bit
1001
[r9/r9d]
+ displacement 8-bit
1010
[r10/r10d]
+ displacement 8-bit
1011
[r11/r11d]
+ displacement 8-bit
1100
SIB + displacement 8-bit
1101
[r13/r13d]
+ displacement 8-bit
1110
[r14/r14d]
+ displacement 8-bit
1111
[r15/r15d]
+ displacement 8-bit
0000
[rax/eax]
+ displacement 32-bit
0001
[rcx/ecx]
+ displacement 32-bit
0010
[rdx/edx]
+ displacement 32-bit
0011
[rbx/ebx]
+ displacement 32-bit
0100
SIB + displacement 32-bit
0101
[rbp/ebp]
+ displacement 32-bit
0110
[rsi/esi]
+ displacement 32-bit
0111
[rdi/edi]
+ displacement 32-bit
1000
[r8/r8d]
+ displacement 32-bit
1001
[r9/r9d]
+ displacement 32-bit
1010
[r10/r10d]
+ displacement 32-bit
1011
[r11/r11d]
+ displacement 32-bit
1100
SIB + displacement 32-bit
1101
[r13/r13d]
+ displacement 32-bit
1110
[r14/r14d]
+ displacement 32-bit
1111
[r15/r15d]
+ displacement 32-bit
Os endereçamentos com R/M 100
(em 32-bit e 64-bit) são os que usam o byte SIB (exceto MOD 11
), que como já foi explicado anteriormente contém os campos Scale, Index e Base que são calculados de maneira equivalente a expressão:
Onde o campo scale são os 2 primeiros bits, onde seu valor numérico é equivalente aos seguintes fatores de escala:
00
- Não multiplica o index
01
- Multiplica o index por 2
10
- Multiplica o index por 4
11
- Multiplica o index por 8
Já os campos index e base contém 3 bits cada e os mesmos armazenam o código dos registradores que serão usados. Os bits dos campos no byte seguem a ordem que o próprio nome sugere. Como em: SSIIIBBB
.
Entendendo o código de máquina x86-64
O famigerado código de máquina (também chamado de linguagem de máquina), popularmente conhecido como "zeros e uns", são as instruções que o processador interpreta e executa. São basicamente números onde o processador decodifica esses números afim de executar determinadas operações identificadas pelas instruções.
Acho que boa parte das pessoas da área da computação sabem que processadores de computadores digitais funcionam com sinais elétricos com duas tensões diferentes: Uma alta (lá pelos 3v, mas pode variar de acordo com o processador) e uma baixa (perto de 0v), onde a tensão alta representa o 1 e a tensão baixa representa o 0.
Mas comumente é só isso o que as pessoas sabem sobre código de máquina. O objetivo deste capítulo é dar uma noção aprofundada de como funciona o código de máquina da arquitetura x86-64.
Cada arquitetura de processador (vulgo ISA, Instruction Set Architecture) têm um código de máquina distinto. Portanto as informações aqui são válidas para código de máquina x86 e x86-64. ARM, RISC-V etc. contém código de máquina que funciona de um jeito completamente diferente.
Antes de mais nada um pré-aviso: Sei que é romântico quando se fala de código de máquina meter um monte de zeros e uns (como: 10110100010
). Mas na vida real ninguém representa textualmente código de máquina em binário. Isso é normalmente feito em manuais ou ferramentas como disassemblers e debuggers usando hexadecimal.
Então ao pensar em código de máquina não pense nisso 10110100 00001110
mas sim nisso B4 0E
. Você é humano, pense como tal.
Comecei a desenvolver uma ferramenta exclusivamente para ser usada como auxílio para esse capítulo. Eu a chamei de x86-visualizer e seu intuito é você escrever uma instrução em Assembly e ela lhe exibir o código de máquina dividido em seus campos, assim facilitando o entendimento.
A ferramenta não está concluída então poucas instruções irão funcionar, todavia sugiro seu uso durante a leitura do capítulo afim de facilitar o entendimento da codificação das instruções.
Acesse o repositório dela aqui:
Também sugiro usar o ndisasm afim de fazer experimentações. Ele é um disassembler que vem junto com o nasm e já foi utilizado anteriormente no livro.
Campo immediate na instrução do código de máquina.
O campo immediate (valor "imediato") pode ter 1, 2, ou 4 bytes de tamanho. Ele é o operando numérico presente em algumas instruções. Exemplo:
Essa instrução em código de máquina fica: B8 44 33 22 11
Onde B8
é o opcode da instrução e 44 33 22 11
o valor imediato (0x11223344
). Lembrando que a arquitetura x86 é little-endian, portanto o valor imediato fica em little-endian na instrução.
O tamanho desse campo é definido pelo atributo operand-size, portanto ao usar o prefixo 66
o seu tamanho pode alternar na instrução entre 16-bit e 32-bit. Sobre instruções com operandos de 8-bit, como mov al, 123
, existem opcodes específicos para operandos nesse tamanho portanto o prefixo não é usado nessas instruções. E obrigatoriamente o immediate terá 8-bit de tamanho.
Outros dois exemplos seriam mov ax, 0x1122
e mov al, 0x11
. Onde o primeiro tem o código de máquina 66 B8 22 11
em modo de 32-bit, e em modo de 16-bit fica igual só que sem o prefixo 66
.
Já a segunda instrução terá o código de máquina B0 11
em qualquer modo de operação, já que ela independe do operand-size.
Entendendo o opcode da instrução.
Como já foi dito antes existem opcodes cujo os 3 últimos bits são usados para identificar o registrador usado na instrução. Opcodes nesse estilo de codificação são usados para instruções que só precisam usar um registrador. Por exemplo mov eax, 123
cujo o opcode é B8
.
Já em instruções que usam o byte ModR/M os dois bits menos significativos do opcode tem um significado especial, que são chamados de bit D (direction bit) e S (size bit). Conforme ilustração:
A função do bit D é indicar a direção para onde a operação está sendo executada. Se do REG para o R/M ou vice-versa. Repare nas instruções abaixo e seus respectivos opcodes:
Convertendo os opcodes 8B
e 89
para binário dá para notar um fato interessante:
A única diferença entre os opcodes é que em um o bit D está ligado e no outro não. Quando o bit D está ligado o campo REG é usado como operando destino e o campo R/M usado como fonte. E quando ele está desligado é o inverso: o campo R/M é o destino e o REG é o fonte. Obviamente o mesmo também se aplica se o R/M também for um registrador.
Por exemplo a instrução xor eax, eax
pode ser escrita em código de máquina como 31 C0
ou 33 C0
. Como no campo REG e no campo R/M são os mesmos registradores não faz diferença qual é o fonte e qual é o destino, a operação executada será a mesma. Usando um disassembler como o ndisasm dá para notar isso:
O bit S é usado para definir o tamanho do operando, onde:
0
-> Indica que o operando é de 8-bit
1
-> Indica que o operando é do tamanho do operand-size.
Repare por exemplo a instrução 30 C0
: