Desenvolvimento

13 dez, 2012

Emulação de hardware – CPU e memória

Publicidade

Existem dezenas de plataformas de hardware (embora algumas pessoas digam que existe apenas uma – o computador). Cada uma possui suas próprias vantagens e desvantagens. Por exemplo, Intel é a plataforma mais usada para desktops, ARM e MIPS são amplamente utilizadas em sistemas embarcados, e assim por diante. Às vezes, uma necessidade pode surgir para testar/depurar código executável escrito para uma plataforma a que você tem acesso. Por exemplo, e se você tem tem que executar código ARM enquanto usa um desktop baseado na Intel? Na maioria dos casos, isso não é um problema, devido a uma grande quantidade disponível de emuladores de plataforma (como QEMU e muitos outros). No entanto, apesar de o QEMU ser uma ferramenta bastante poderosa, existem alguns casos em que ela não é útil (pelo menos não sem algumas modificações).

Nota para nerds:

Sim, existem esses casos – se você ainda não viu um, não significa que eles não existem. O código neste artigo é apenas para demonstração – verifique se os erros podem ser omitidos. Ele pode ser não otimizado. Sim, pode haver maneiras melhores.

Seja forçado por suas necessidades atuais ou apenas por diversão, você pode querer escrever seu próprio emulador para qualquer plataforma existente (ou não existente). Você pode dar uma olhada neste artigo para ver como um CPU simplista pode ser projetado e implementado. No entanto, a CPU é apenas uma pequena (embora, importante) parte do seu emulador. Há muitas outras coisas das quais você tem que cuidar, como memória, dispositivos IO etc. É claro que a complexidade da implementação depende do quão isolado você quer que seu emulador seja.

Como você pode entender a partir do título deste artigo, vamos nos concentrar na interface CPU to Memory (RAM). Pode ser uma boa ideia definir a quantidade de memória que o seu emulador deve suportar (definir a largura da linha de endereço) com antecedência. Por exemplo, se você for dar suporte para no máximo 64 kB, então um modo de endereçamento de 16 bits seria suficiente. Nesse caso, você pode simplesmente alocar uma área de memória contínua e acessá-la diretamente. No entanto, e se você planeja oferecer suporte para 1 ou 2 gigabytes, ou até mesmo mais? Embora isso não seja necessariamente usado, sua arquitetura pode implicar isso. Você definitivamente não gostaria de fazer uma alocação enorme. Especialmente se o software que você estiver planejando executar usa um pouco de memória no espaço menor endereço, um pouquinho na parte superior e em si é carregado em algum lugar no meio. Se essa é a situação, então você deve implementar uma espécie de mecanismo de paginação, que só aloca páginas para os endereços que estão sendo usados.

Paginação

Vamos fazer algumas definições para lidar com páginas:

#define PAGE_SIZE 0x1000 // You may choose to use other size
#define PAFE_MASK 0x0FFF // This depends on the value of PAGE_SIZE

typedef struct _page_t
{
struct _page_t*   previous, next;
unsigned long     base;  // Address in the emulated memory represented by this page
unsigned int      flags; //Whatever flags you want your pages to have
unsigned char*    mem;   // Pointer to the actual allocated memory
}page_t;

O mecanismo é bastante semelhante ao mecanismo de paginação usado hoje, exceto que você não precisa usar tabelas de páginas, já que na maioria das vezes uma simples lista linkada de páginas é o suficiente, e você não está mapeando a memória virtual para a física, mas o mapeando a memória emulada para a memória virtual, que é acessível para o emulador.

previous e next – aponta para outras estruturas page_t na lista linkada de páginas;

base – endereço menor de memória emulada representado por esta página;

flags – quaisquer atributos que você gostaria de ter em suas páginas (por exemplo, é gravável ou executável etc.);

mem – aponta para a área de memória realmente alocada pelo emulador.

Utilizar esse mecanismo irá reduzir o uso geral de memória, já que você teria que alocar apenas as áreas de memória usadas pelo software que está sendo executado em seu emulador.

Gerenciamento da página

É claro depende de você como gerenciar esse tipo de paginação, mas, para mim, pode ser uma boa ideia implementar um conjunto de funções para gerenciar a lista classificada (por base) linkadas de páginas:

page_t* memory_page_alloc(void);

Essa função simplesmente retornaria um ponteiro para uma estrutura alocada page_t. Não se esqueça de alocar a área de memória real de PAGE_SIZE e armazenar um ponteiro para ela em page_t->mem.

void  memory_page_release(page_t** pg);

Essa função libera todos os recursos alocados para uma página. Isso inclui a memória que realmente representa a página e é apontada por page_t->mem e a estrutura page_t si.

int  memory_page_add(page_t** page_list, unsigned long base);

Essa função é responsável pela alocação de uma nova página, o que representaria a memória começando na base e sua inserção na lista linkada de páginas.

*page_list – ponteiro para a primeira página da lista linkada de páginas;

base – início do endereço da memória emulada de tamanho PAGE_SIZE.

Seu valor de retorno deve dizer se uma página foi adicionada ou ocorreu um erro durante as alocações de memória.

Emulação de Memory Access

Por não estarmos falando de um array consistente, mas sim várias áreas de memória separadas (a partir do ponto de vista do emulador), faz sentido escrever algumas funções que executariam operações de leitura/gravação de/para a memória emulada.

int memory_read_byte(page_t* pg_list, unsigned long address, unsigned char* byte);

Essa função é responsável por ler um único byte da memória emulada apontada por address. O byte lido é devolvido no local apontado por byte. Ele percorre a lista linkada de páginas à procura de uma página onde page_t->base <= address && (page_t->base + PAGE_SIZE) > address. Se não houver essa página, então ele ou aloca e a adiciona à lista de páginas e, em seguida, executa a operação de leitura, ou simplesmente retorna o erro (o que pode ser útil para emular violações de acesso à memória). Cabe a você definir o comportamento dessa função em tal situação. Na verdade, você pode definir uma flag interna para ativar/desativar alocações de página automáticas.

int memory_write_byte(page_t* pg_list, unsigned long address, unsigned char byte);

Essa função é quase idêntica à anterior, exceto que ela escreve um único byte para a memória emulada. Seu comportamento deve ser o mesmo de memory_read_byte.

Definitivamente, não é bom ser apenas capaz de transferir um byte de cada vez, então você é mais do que bem-vindo para implementar funções de transferências maiores. No entanto, você precisa ter cuidado nos casos em que essa transferência envolve duas páginas e verificar que ambas são alocadas (ou seja, acessíveis).

Claro que há muitas coisas a mais para emular, como dispositivos IO, possivelmente adaptadores de rede, mas a memória é o mais importante. Mas isso vai muito além do âmbito deste artigo.

Espero que este artigo tenha sido informativo. Vejo você no próximo.

***

Texto original disponível em http://syprog.blogspot.com.br/2012/08/emulation-of-hardware-cpu-memory.html