Ir para conteúdo

Vitor Mob

Membros
  • Postagens

    3
  • Registro em

  • Última visita

2 Seguidores

Últimos Visitantes

569 visualizações

Conquistas de Vitor Mob

4

Reputação

  1. Introdução Olá, neste artigo irei abordar a emulação do Intel 8080, com tópicos sobre lógica, instruções e rotinas. O projeto completo do código está no meu Github. ? Para emular o Intel 8080, usei C++, linguagem com a qual estou familiarizado com a sintaxe. Neste artigo irei demonstrar trechos de códigos com instruções usadas pelo Intel 8080 e explicar a lógica por trás. Requisitos mínimos para compreender o artigo incluem C++ e conhecimentos sobre o funcionamento de processadores (flags de estado, registradores, instruções). História Estudando sobre processadores, me veio a dúvida: Como a lógica de um processador é implementada?. Para compreender isso, precisei estudar um pouco sobre Verilog, porém meu intuito era fazer a emulação de um processador, então decidi emular o Intel 8080, que foi o segundo microprocessador de 8 bits da Intel, tendo como antecessor i8008 e sucessor o i8085. O Intel 8080 possui 65536 bytes de memória disponível e 7 registradores de 8 bits (A, B, C, D, E, H e L). Ele obtém as operações que vai executar a partir de pares de registradores de 16 bits (HL, BC e DE) e utiliza 5 flags de estado (PF, SF, AC, CF e ZF). Vamos ver agora como avancei nesse desenvolvimento. ? O que é emulação? Da Wikipedia: "Na computação, um emulador é um software que reproduz as funções de um determinado ambiente, a fim de permitir a execução de outros softwares sobre ele. Pode ser pela transcrição de instruções de um processador alvo para o processador no qual ele está rodando, ou pela interpretação de chamadas para simular o comportamento de um hardware específico. O emulador também é responsável pela simulação dos circuitos integrados ou chips do sistema de hardware em um software. Basicamente, um emulador expõe as funções de um sistema para reproduzir seu comportamento, permitindo que um software criado para uma plataforma funcione em outra. Também são disponíveis emuladores de consoles de vídeo games". Para construir um emulador, então, precisamos reproduzir o funcionamento de algo. No caso, do Intel 8080. Este tem várias partes que precisam ser reproduzidas (registradores, pilha de memória, flags, etc), mas precisamos começar de algum lugar, né? Vamos em frente! Desenvolvimento Memória / Stack Vamos começar pela pilha (stack). O processador precisa colocar dados nela com a instrução PUSH e remover/recuperar com a instrução POP. Abaixo minha implementação: #define MAX_MEMORY 0x10000L inline byte_t memory[MAX_MEMORY]; void Instructions::push(word_t data16) { SP -= 2; memory::write_memory_word(SP, data16); } word_t Instructions::pop() { word_t mem = memory::read_memory_word(SP); SP += 2; return mem; } Podemos notar, quando efetuamos o push(), que o registrador SP (Stack Pointer) é decrementado em duas unidades. Analogamente, SP é incrementado em duas unidades quando a função pop() é chamada. Dois registradores que são essenciais para o uso da stack, o SP, e o PC : (SP)Stack Pointer Operações de empilhamento são realizadas por várias das instruções, e facilitar a execução de sub-rotinas e tratamento de interrupções de programa. Especifica quais endereços as operações de stack irão operar por meio de um registro especial acessível de 16 bits chamado Ponteiro da Pilha(SP), (PC) Program Counter É um registrador de 16 bits que é aceito acessível ao programador e cujo conteúdo indique o endereço da próxima instrução a ser executada. Nota: O Intel 8080 endereça a memória em 16-bits através do registrador de Stack Pointer (SP). O tipo word_t é uma palavra-chave para o tipo uint16_t. O mesmo vale para byte_t e uint8_t. Flags As flags de estado indicam resultados de operações. No Intel 8080, são elas: Sign (S), definida quando o resultado é negativo. Zero (Z), definida quando a operação tem como resultado zero. Parity (P), definida quando o número de bits 1 em uma operação é par. Carry (C), definida se a última operação de adição resultou em um transbordo ou se a última operação de subtração exigiu um empréstimo. Auxiliary Carry (AC ou H), usada para aritmética com binary-coded decimal (BCD). Para implementar as flags, utilizei operadores de bitwise. Por exemplo, segue a implementação da instrução ADD, que faz uma operação de adição: void Instructions::add(word_t data16) { word_t work16 = A + data16; A = work16 & 0xff; CF = (work16 & 0x100) != 0; AC = ((A >> 7) & 0x1) != 1; SF = (A & 0x80) != 0; ZF = (A == 0); PF = parity(A); } Nota: Utilizei o registrador A (Acumulador) para somar o argumento da instrução ADD. Falarei mais sobre os registradores mais a frente no texto. Vamos à explicação da configuração das flags no contexto da instrução ADD: CF recebe o resultado de uma operação de bitwise, pegando os 8 bits menos significativos e verificando se o resultado é 1. AC recebe o resultado de uma operação de bitwise movendo 7 bits a direita e verifica se é 0. SF pega os bits mais significativos e verificar se o resultado é 1. ZF verifica se o registrador A é 0, ou seja, se a nossa adição resultou em um valor 0. PF depende do resultado da função parity(), explicada abaixo. Flag de Paridade Para implementar a flag de paridade, escrevi uma função que percorre os 8 bits mais significativos do meu registrador e faz a contagem de quantos bits 1 foram encontrados. Caso essas operações de bitwise retornem verdadeiro, a flag é setada para 1. Caso contrário, 0. Segue a função: inline static bool parity(byte_t n) { word_t p_count = 0; for (int i = 0; i <= 8; i++) p_count += (n >> i) & 1; return !(p_count&1); } Registradores O Intel 8080 possui sete registradores de 8-bits, sendo A o registrador principal, na sequência temos B, C, D, E, H e L. Para trabalhar com números de 16-bits, o Intel 8080 acessa os pares de registradores HL, DE e BC. O par HL é tipicamente usado para endereçar a memória (H = high e L = low). Algumas instruções também permitem que o par de registradores HL seja usado como um acumulador de 16 bits. Implementação do par HL: void Instructions::set_hl(word_t data16) { L = data16 & 0xff; H = data16 >> 8; } Instruções Sem dúvidas meu maior desafio foi na implementação das instruções do processador. De acordo com o manual Intel 8080 Programmers, este processador possui 78 instruções de categorias variadas: aritméticas com binários e decimais, lógicas, de operação com um byte de dados, saltos, etc. Segue alguns exemplos de conjuntos de instruções: Adição ADD (Add Register or Memory to Accumulator) ADC (Add Register or Memory to Accumulator With Carry) ACI (Add Immediate to Accumulator With Carry) DAD (Double Add) Subtração SUB (Subtract Register or Memory) SBB (Subtract Register or Memory From Accumulator With Borrow) SUI (Subtract Immediate From Accumulator) SBI (Subtract Immediate From Accumulator With Borrow) CMP (Compare Register or Memory With Accumulator) CPI (Compare Immediate With Accumulator Direct Addressing Instructions) Rotação RLC (Rotate Accumulator Left) RRC (Rotate Accumulator Right) RAL (Rotate Accumulator Left Through Carry) RAR (Rotate Accumulator Right Through Carry) Lógicas ANA (Logical and Register or Memory With Accumulator) ORA (Logical or Register or Memory With Accumulator) XRA (Logical Exclusive-Or Register or Memory With Accumulator (Zero Accumulator)) ANI (And Immediate With Accumulator) ORI(Or Immediate With Accumulator) XRI (Exclusive-Or Immediate With Accumulator) Eu utilizei uma construção switch/case em C++ para implementar o reconhecimento das instruções do Intel 8080, no método i8080:execute_opcode(). Nele, fiz um grande switch para testar os opcodes de cada instrução que decidi emular, utilizando esta página como referência. Vejamos algumas implementações. Nos exemplos abaixo, data16 é um valor imediato. A instrução SUB deve fazer A = A - data16. Flags afetadas: CF, AC, SF, ZF e PF. Segue a implementação: void Instructions::sub(word_t data16) { word_t work16 = A - data16; A = work16 & 0xff; AC = ((A >> 7) & 0x1) != 1; CF = (work16 & 0x100) != 0; ZF = (A == 0); SF = (A & 0x80) != 0; PF = parity(A); } Já a instrução RAL deve rotacionar os bits de um número uma única vez para a esquerda. Apenas a flag CF é afetada: void Instructions::ral() { bool flag = CF; CF = A >> 0x07; A = ( A << 0x01 ) | flag; } A instrução ANA faz um AND bit-a-bit entre A e o byte passado como argumento. Flags afetadas: CF, AC, ZF, SF e PF. Ficou assim: void Instructions::ana(word_t data16) { byte_t work16 = A & data16; CF = work16 >> 8; AC = ((A | data16) & 0x08) != 0; ZF = (work16 & 0xff) == 0; SF = (work16 & 0x80) != 0; PF = parity(work16 & 0xff); A = work16; } Essas são algumas das implementações de instruções que fiz. No total, foram 19 instruções emuladas, além de operações na stack. Para mais detalhes, testes e acesso completo ao código fonte, basta acessar o repositório do projeto no Github. Contribuições e feedbacks são muito bem vindos! Valeu! ?
  2. Vitor Mob

    maProc

    maProc é um inspetor de processos para Linux. Com ele é possível inspecionar a memória de um processo, alterá-la, além de controlar o processo em si. O autor do projeto é o @Vitor Mob. ? Releases e código-fonte disponíveis no repositório do Github.
  3. Vitor Mob

    elfparser-ng

    A análise de executáveis Linux é uma área em constante crescimento. Pensando nisso, pegamos um projeto que estava sem atualizações há 7 anos chamado elfparser e fizemos nosso próprio fork (um novo projeto que começa com uma cópia idência ao original) com autorização do autor. Chamado de elfparser-ng, o programa abre um arquivo ELF e o disseca, de modo a ser um bom aliado numa primeira análise do binário. O elfparser-ng também emite um score onde tenta inferir se o binário é malicioso ou não. Nossa versão conta com algumas melhorias já: Editor hexadecimal. Botão de reset. Abas arrastáveis. Screenshot do elfparser-ng em funcionamento na linha de comando: Repositório do projeto no Github: elfparser-ng.
  4. Incrível, atualmente estudando RE, e não sabia desta técnica, ansioso para as próximas publicações !!!
×
×
  • Criar Novo...