Como vamos do assembly ao código da máquina (geração de código)
16
Existe uma maneira fácil de visualizar a etapa entre a montagem do código no código da máquina?
Por exemplo, se você abrir um arquivo binário no bloco de notas, verá uma representação formatada em texto do código da máquina. Suponho que cada byte (símbolo) que você vê é o caractere ASCII correspondente ao seu valor binário?
Mas como vamos da montagem para a binária, o que está acontecendo nos bastidores?
Veja a documentação do conjunto de instruções e você encontrará entradas como esta em um microcontrolador de pic para cada instrução:
A linha "codificação" diz como é essa instrução em binário. Nesse caso, ele sempre começa com 5, depois um bit de não se preocupa (que pode ser um ou zero), e os "k" s representam o literal que você está adicionando.
Os primeiros bits são chamados de "opcode" e são únicos para cada instrução. A CPU basicamente olha o código de operação para ver qual é a instrução e, em seguida, sabe decodificar os "k" como um número a ser adicionado.
É entediante, mas não tão difícil de codificar e decodificar. Eu tive uma aula de graduação onde tínhamos que fazer isso manualmente nos exames.
Para criar um arquivo executável completo, você também precisa alocar memória, calcular desvios de ramificação e colocá-lo em um formato como ELF , dependendo do sistema operacional.
Os opcodes de montagem têm, na maior parte, uma correspondência individual com as instruções subjacentes da máquina. Portanto, tudo o que você precisa fazer é identificar cada código de operação na linguagem assembly, mapeá-lo para a instrução de máquina correspondente e gravar a instrução de máquina em um arquivo, junto com seus parâmetros correspondentes (se houver). Você repete o processo para cada código de operação adicional no arquivo de origem.
É claro que é preciso mais do que isso para criar um arquivo executável que seja carregado e executado adequadamente em um sistema operacional, e a maioria dos montadores decentes tem alguns recursos adicionais além do simples mapeamento de códigos de operação para instruções da máquina (como macros, por exemplo).
A primeira coisa que você precisa é algo como este arquivo . Este é o banco de dados de instruções para processadores x86, conforme usado pelo montador NASM (que eu ajudei a escrever, embora não as partes que realmente traduzem instruções). Vamos escolher uma linha arbitrária do banco de dados:
ADD rm32,imm8 [mi: hle o32 83 /0 ib,s] 386,LOCK
O que isto significa é que descreve a instrução ADD. Existem várias variantes desta instrução, e a específica que está sendo descrita aqui é a variante que pega um registro de 32 bits ou um endereço de memória e adiciona um valor imediato de 8 bits (ou seja, uma constante incluída diretamente na instrução). Um exemplo de instrução de montagem que usaria esta versão é esta:
add eax, 42
Agora, você precisa pegar sua entrada de texto e analisá-la em instruções e operandos individuais. Para a instrução acima, isso provavelmente resultaria em uma estrutura que contém a instrução ADD, e uma matriz de operandos (uma referência ao registro EAXe ao valor 42). Depois de ter essa estrutura, você percorre o banco de dados de instruções e encontra a linha que corresponde ao nome da instrução e aos tipos dos operandos. Se você não encontrar uma correspondência, é um erro que precisa ser apresentado ao usuário ("combinação ilegal de opcode e operandos" ou similar é o texto usual).
Depois de obter a linha do banco de dados, olhamos para a terceira coluna, que para esta instrução é:
[mi: hle o32 83 /0 ib,s]
Este é um conjunto de instruções que descrevem como gerar a instrução de código de máquina necessária:
A mié uma descrição dos operandos: um operando modr/m(registrador ou memória) (o que significa que precisaremos acrescentar um modr/mbyte ao final da instrução, que veremos mais adiante) e um uma instrução imediata (que irá ser usado na descrição da instrução).
O próximo é hle. Isso identifica como lidamos com o prefixo "lock". Nós não usamos "lock", então nós o ignoramos.
O próximo é o32. Isso nos diz que, se estivermos montando código para um formato de saída de 16 bits, a instrução precisará de um prefixo de substituição de tamanho de operando. Se estivéssemos produzindo uma saída de 16 bits, produziríamos o prefixo now ( 0x66), mas assumirei que não estamos e continuamos.
O próximo é 83. Este é um byte literal em hexadecimal. Nós produzimos isso.
O próximo é /0. Isso especifica alguns bits extras que precisaremos no modr / m bytem e nos faz gerá-lo. O modr/mbyte é usado para codificar registradores ou referências indiretas de memória. Temos um único operando, um registro. O registro possui um número, especificado em outro arquivo de dados :
eax REG_EAX reg32 0
Verificamos que reg32concorda com o tamanho exigido da instrução no banco de dados original. O 0é o número do registro. Um modr/mbyte é uma estrutura de dados especificada pelo processador, com a seguinte aparência:
(most significant bit)
2 bits mod - 00 => indirect, e.g. [eax]
01 => indirect plus byte offset
10 => indirect plus word offset
11 => register
3 bits reg - identifies register
3 bits rm - identifies second register or additional data
(least significant bit)
Porque estamos trabalhando com um registro, o modcampo é 0b11.
O regcampo é o número do registro que estamos usando,0b000
Como há apenas um registro nesta instrução, precisamos preencher o rmcampo com algo. É para isso que servem os dados extras especificados /0, então colocamos isso no rmcampo 0b000,.
O modr/mbyte é, portanto, 0b11000000ou 0xC0. Nós produzimos isso.
O próximo é ib,s. Isso especifica um byte imediato assinado. Observamos os operandos e observamos que temos um valor imediato disponível. Nós o convertemos em um byte assinado e o produzimos ( 42=> 0x2A).
A instrução montada completa é, portanto: 0x83 0xC0 0x2A. Envie-o para o seu módulo de saída, juntamente com uma nota de que nenhum dos bytes constitui referência de memória (o módulo de saída pode precisar saber se o faz).
Repita para todas as instruções. Acompanhe os rótulos para saber o que inserir quando forem referenciados. Adicione recursos para macros e diretivas que são passadas para os módulos de saída do arquivo de objeto. E é basicamente assim que um montador funciona.
Na prática, um assembler geralmente não produz diretamente algum executável binário , mas algum arquivo de objeto (a ser alimentado posteriormente ao vinculador ). No entanto, existem exceções (você pode usar alguns montadores para produzir diretamente algum executável binário; eles são incomuns).
Primeiro, observe que muitos montadores são hoje programas de software livre . Então faça o download e compile no seu computador o código fonte do GNU como (uma parte do binutils ) e do nasm . Em seguida, estude o código fonte. BTW, eu recomendo usar o Linux para esse fim (é um SO muito amigável para desenvolvedores e de software livre).
O arquivo de objeto produzido por um assembler contém notavelmente um segmento de código e instruções de realocação . Está organizado em um formato de arquivo bem documentado, que depende do sistema operacional. No Linux, esse formato (usado para arquivos de objetos, bibliotecas compartilhadas, dumps principais e executáveis) é ELF . Esse arquivo de objeto é posteriormente inserido no vinculador (que finalmente produz um executável). As realocações são especificadas pela ABI (por exemplo, x86-64 ABI ). Leia o livro de Levine, Linkers and Loaders, para mais.
O segmento de código desse arquivo de objeto contém código de máquina com orifícios (a serem preenchidos, com a ajuda de informações de realocação, pelo vinculador). O código de máquina (relocável) gerado por um assembler é obviamente específico para uma arquitetura de conjunto de instruções . Os ISAs x86 ou x86-64 (usados na maioria dos processadores para laptop ou desktop) são extremamente complexos em seus detalhes. Mas um subconjunto simplificado, chamado y86 ou y86-64, foi inventado para fins de ensino. Leia os slides sobre eles. Outras respostas a esta pergunta também explicam um pouco disso. Você pode ler um bom livro sobre Arquitetura de Computadores .
A maioria das montadoras está trabalhando em duas passagens , a segunda emitindo realocação ou corrigindo parte da saída da primeira passagem. Agora eles usam técnicas usuais de análise (então talvez leia The Dragon Book ).
PS. Sua pergunta é tão ampla que você precisa ler vários livros sobre ela. Eu dei algumas referências (muito incompletas). Você deve encontrar mais deles.
Em relação aos formatos de arquivo de objeto, para um iniciante, recomendo examinar o formato RDOFF produzido pelo NASM. Isso foi intencionalmente projetado para ser o mais simples possível realisticamente e ainda funcionar em várias situações. A fonte NASM inclui um vinculador e um carregador para o formato. (Divulgação completa - eu projetei e escreveu tudo isso)
$ cat > test.asm bits 32 add eax,42 $ nasm -f bin test.asm -o test.bin $ od -t x1 test.bin 0000000 83 c0 2a 0000003
... sim, você está certo. :)Na prática, um assembler geralmente não produz diretamente algum executável binário , mas algum arquivo de objeto (a ser alimentado posteriormente ao vinculador ). No entanto, existem exceções (você pode usar alguns montadores para produzir diretamente algum executável binário; eles são incomuns).
Primeiro, observe que muitos montadores são hoje programas de software livre . Então faça o download e compile no seu computador o código fonte do GNU como (uma parte do binutils ) e do nasm . Em seguida, estude o código fonte. BTW, eu recomendo usar o Linux para esse fim (é um SO muito amigável para desenvolvedores e de software livre).
O arquivo de objeto produzido por um assembler contém notavelmente um segmento de código e instruções de realocação . Está organizado em um formato de arquivo bem documentado, que depende do sistema operacional. No Linux, esse formato (usado para arquivos de objetos, bibliotecas compartilhadas, dumps principais e executáveis) é ELF . Esse arquivo de objeto é posteriormente inserido no vinculador (que finalmente produz um executável). As realocações são especificadas pela ABI (por exemplo, x86-64 ABI ). Leia o livro de Levine, Linkers and Loaders, para mais.
O segmento de código desse arquivo de objeto contém código de máquina com orifícios (a serem preenchidos, com a ajuda de informações de realocação, pelo vinculador). O código de máquina (relocável) gerado por um assembler é obviamente específico para uma arquitetura de conjunto de instruções . Os ISAs x86 ou x86-64 (usados na maioria dos processadores para laptop ou desktop) são extremamente complexos em seus detalhes. Mas um subconjunto simplificado, chamado y86 ou y86-64, foi inventado para fins de ensino. Leia os slides sobre eles. Outras respostas a esta pergunta também explicam um pouco disso. Você pode ler um bom livro sobre Arquitetura de Computadores .
A maioria das montadoras está trabalhando em duas passagens , a segunda emitindo realocação ou corrigindo parte da saída da primeira passagem. Agora eles usam técnicas usuais de análise (então talvez leia The Dragon Book ).
Como um executável é iniciado pelo kernel do SO (por exemplo, como a
execve
chamada do sistema funciona no Linux) é uma questão diferente (e complexa). Geralmente ele configura algum espaço de endereço virtual (no processo que executa esse execve (2) ...) e reinicializa o estado interno do processo (incluindo registros no modo de usuário ). Um vinculador dinâmico - como ld-linux.so (8) no Linux - pode estar envolvido em tempo de execução. Leia um bom livro, como Sistema operacional: Three Easy Pieces . O wiki do OSDEV também está fornecendo informações úteis.PS. Sua pergunta é tão ampla que você precisa ler vários livros sobre ela. Eu dei algumas referências (muito incompletas). Você deve encontrar mais deles.
fonte