Como vamos da montagem para o código de máquina (geração de código)

Existe uma maneira fácil de visualizar a etapa entre a montagem do código para o código de máquina?

Por exemplo, se você abrir sobre um arquivo binário no bloco de notas, verá uma representação textualmente formatada do código de máquina. Eu presumo que cada byte (símbolo) que você vê é o caractere ascii correspondente para seu valor binário?

Mas como vamos do assembly para o binário, o que está acontecendo nos bastidores ??

Resposta

Olhe a documentação do conjunto de instruções e você encontrará entradas como esta de um microcontrolador de imagem para cada instrução:

exemplo de instrução addlw

A linha “encoding” informa como essa instrução se parece em binário. Neste caso, ele sempre começa com 5 unidades, depois um bit indiferente (que pode ser um ou zero), e os “k” representam o literal que você está adicionando.

O os primeiros bits são chamados de “opcode” e são exclusivos para cada instrução. A CPU basicamente olha para o opcode para ver que instrução é, então sabe decodificar os “k” s como um número a ser adicionado.

É tedioso, mas não é tão difícil de codificar e decodificar. Eu tive uma aula de graduação em que tínhamos que fazer isso manualmente em exames.

Para realmente fazer um arquivo executável completo, você também precisa fazer coisas como alocar memória, calcular desvios de ramificação e colocá-lo em um formato como ELF , dependendo do seu sistema operacional.

Resposta

Os opcodes de montagem têm, na maior parte, uma correspondência um-para-um com as instruções de máquina subjacentes. Portanto, tudo o que você precisa fazer é identificar cada opcode 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). Em seguida, você repete o processo para cada opcode adicional no arquivo de origem.

Claro, é preciso mais do que isso para criar um arquivo executável que será carregado e executado corretamente em um sistema operacional, e a maioria dos montadores decentes fazem tem alguns recursos adicionais além do simples mapeamento de opcodes para instruções de máquina (como macros, por exemplo).

Resposta

O primeiro algo que você precisa é algo como este arquivo . Este é o banco de dados de instruções para processadores x86 conforme usado pelo montador NASM (que ajudei a escrever, embora não as partes que realmente traduzem as 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 isso significa é que ele descreve a instrução ADD. Existem múltiplas variantes desta instrução, e a específica que está sendo descrita aqui é a variante que pega um registro de 32 bits ou 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 é:

 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 EAX e o 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 de operandos. Se você não encontrar uma correspondência, esse é um erro que precisa ser apresentado ao usuário (“combinação ilegal de opcode e operandos” ou semelhante é o texto usual).

Assim que “vermos obteve 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 descreve como gerar a instrução em código de máquina necessária:

  • O mi é uma descrição dos operandos: um operando a modr/m (registro ou memória) (o que significa que “precisaremos acrescentar um modr/m byte a o final da instrução, ao qual veremos mais tarde) e um uma instrução imediata (que será usada na descrição da instrução).
  • O próximo é hle. Isso identifica como tratamos o prefixo de “bloqueio”. Não usamos “lock”, então o ignoramos.
  • O próximo é o32. Isso nos diz que se estivermos montando o código para um 16- formato de saída de bits, a instrução precisa de um prefixo de substituição do tamanho do operando.Se estivéssemos produzindo saída de 16 bits, produziríamos o prefixo agora (0x66), mas irei presumir que não estamos e continuaremos.
  • O próximo é 83. Este é um byte literal em hexadecimal. Nós o geramos.
  • O próximo é /0. Isso especifica alguns bits extras que precisaremos no bytem modr / m e nos faz gerá-los. O byte modr/m é usado para codificar registradores ou referências indiretas à memória. Temos um único operando, um registro. O registro tem um número, que é especificado em outro arquivo de dados :

     eax REG_EAX reg32 0  
  • Verificamos se reg32 concorda com o tamanho necessário da instrução do banco de dados original (ele faz). O 0 é o número do registrador. Um modr/m byte é uma estrutura de dados especificada pelo processador, que se parece com esta:

      (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)  
  • Como estamos trabalhando com um registro, o campo mod é 0b11.

  • O campo reg é o número do registro que estamos usando, 0b000
  • Como há apenas um registro nesta instrução, precisamos preencher o campo rm com algo. É para isso que serviam os dados extras especificados em /0, então colocamos isso no campo rm, 0b000.
  • O modr/m byte é, portanto, 0b11000000 ou 0xC0. Produzimos isso.
  • O próximo é ib,s. Isso especifica um byte imediato com sinal. Observamos os operandos e observamos que temos um valor disponível. Nós o convertemos em um byte assinado e geramos (42 => 0x2A).

A instrução montada completa é, portanto: 0x83 0xC0 0x2A. Envie-a para o seu módulo de saída, junto com uma nota de que nenhum dos bytes constitui referências de memória (o módulo de saída pode precisar saber se houver).

Repita para todas as instruções. Acompanhe os rótulos para saber o que inserir quando eles “forem referenciados. Adicione recursos para macros e diretivas que são passadas para seus módulos de saída de arquivo de objeto. E é basicamente assim que um montador funciona.

Comentários

  • Obrigado. Ótima explicação, mas não deve ‘ ser ” 0x83 0xC0 0x2A ” em vez de ” 0x83 0xB0 0x2A ” porque 0b11000000 = 0xC0
  • @Kamran – $ 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. 🙂

Resposta

Na prática, um assembler geralmente não produz diretamente algum executável binário, mas alguns arquivo objeto (para ser alimentado posteriormente para o vinculador ). No entanto, há exceções (você pode usar alguns montadores para produzir diretamente algum executável binário ; eles são incomuns).

Primeiro, observe que muitos montadores hoje são programas de software livre . Portanto, baixe e compile em seu computador o código-fonte código de GNU as (uma parte de binutils ) e de nasm . Em seguida, estude o código-fonte deles. A propósito, eu recomendo usar o Linux para essa finalidade (é um sistema operacional muito amigável ao desenvolvedor e de software livre).

O arquivo objeto produzido por um montador contém notavelmente um segmento de código e instruções de realocação . Ele é organizado em um formato de arquivo bem documentado, que depende do sistema operacional. No Linux, esse formato (usado para arquivos de objeto, bibliotecas compartilhadas, core dumps e executáveis) é ELF . Esse arquivo 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 e Loaders para obter mais informações.

O segmento de código em tal arquivo de objeto contém código de máquina com lacunas (a ser preenchido, com a ajuda de informações de realocação, pelo vinculador). O código de máquina (realocável) gerado por um montador é obviamente específico para um conjunto de instruções arquitetura .Os ISAs de x86 ou x86-64 (usados na maioria dos processadores de laptop ou desktop) são terrivelmente complexos em seus detalhes. Mas um subconjunto simplificado, chamado y86 ou y86-64, foi inventado para fins de ensino. Leia slides sobre eles. Outras respostas a esta pergunta também explicam um pouco disso. Você pode querer ler um bom livro sobre Arquitetura de Computadores .

A maioria dos montadores está trabalhando em duas passagens , a segunda emitindo realocação ou corrigindo parte da saída da primeira passagem. Eles usam agora as técnicas usuais de análise (então leia talvez O Livro do Dragão ).

Como um executável é iniciado pelo kernel do SO (por exemplo, como a execve chamada de sistema funciona no Linux ) é uma questão diferente (e complexa). Geralmente configura algum espaço de endereço virtual (no processo fazendo isso execve (2) …) então reinicialize o estado interno do processo (incluindo registros modo de usuário ). Um vinculador dinâmico , como ld-linux.so (8) no Linux, pode estar envolvido no tempo de execução. Leia um bom livro, como Sistema operacional: três peças fáceis . O wiki OSDEV também fornece 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.

Comentários

  • Com relação aos formatos de arquivo de objeto, para um iniciante I ‘ d recomendo olhar para o formato RDOFF produzido pelo NASM. Isso foi intencionalmente projetado para ser o mais simples possível e ainda funcionar em uma variedade de situações. A fonte NASM inclui um vinculador e um carregador para o formato. (Divulgação completa – eu projetei e escrevi tudo isso)

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *