Linguagem de Máquina x86 em Macintosh
© 2009 Rudá Moura
Este artigo tem como objetivo ensinar a linguagem de montagem ou de máquina (assembly em Inglês) da arquitetura Intel x86/i386/IA-32 através de exemplos traduzidos da linguagem C.
Como pré-requisito é necessário ter um mínimo de conhecimento da família x86, por exemplo, entender que esta é uma arquitetura CISC de 32 bits ou que é do tipo little-endian e seus registradores. O código de montagem destes exemplos foram produzidos utilizando-se o compilador GCC versão 4.0 em um Macintosh com Mac OS X.
Entrada em procedimento e retorno de resultado: true e false
Os comandos comandos do Unix true e false retornam ao shell do usuário uma condição de sucesso para o comando true (valor 0) ou então de falha para o comando false (valor 1).
A partir dos programas true.c e false.c codificados em C e traduzidos para código de montagem, poderemos conhecer como é a entrada de um procedimento (prólogo) e a saída do procedimento (epílogo) com o retorno de um valor inteiro.
| true.c | false.c |
|---|---|
int main(void)
{
return 0;
}
|
int main(void)
{
return 1;
}
|
Geração do código de montagem
O montador (assembler em Inglês) é um programa que traduz a partir de um fonte o código objeto em binário (arquivo com extensão .o) ou diretamente em um programa executável. No Macintosh o montador é o próprio GCC que chama o utilitário as.
Para obtermos o arquivo de montagem de true, fazemos gcc -S true.c, o resultado é armazenado em true.s, e de forma semelhante para o comando false, conforme podemos ver abaixo.
| true.s | false.s |
|---|---|
.text .globl _main _main: pushl %ebp movl %esp, %ebp subl $8, %esp movl $0, %eax leave ret .subsections_via_symbols |
.text .globl _main _main: pushl %ebp movl %esp, %ebp subl $8, %esp movl $1, %eax leave ret .subsections_via_symbols |
Diretrizes do montador
Os símbolos .text, .globl, .subsections_via_symbols são pseudo-instruções e realmente não fazem parte da linguagem x86. Estes símbolos são diretrizes para instruir o montador a tomar uma determinada ação ou comportamento.
Vejamos em detalhes estas diretrizes que constam no código.
.text.globlmain() do C em código de montagem é identificado através de _main e obrigatoriamente deve ser global, como em .globl _main..subsections_via_symbolsConvenções de chamada (ABI)
Antes de comentar o efeito das instruções x86 é pertinente entender os princípios básicos das chamadas entre procedimentos, denominado de ABI (application binary interface em Inglês).
As instruções do x86 com o sufixo l (como pushl) operam em valores de 32 bits (4 bytes), o sufixo w opera em valores de 16 bits (2 bytes) e sufixo b com 1 byte (8 bits). Os operadores das instruções encontram-se na ordem origem, destino pois o GCC utiliza a convenção AT&T, nos manuais da Intel a ordem dos operadores está invertida destino, origem.
O registrador ESP (stack pointer em Inglês) aponta para o topo da pilha de chamada de procedimentos e o registrador EBP (frame pointer em Inglês) aponta para a base desta pilha. A região delimitada por EBP e ESP é um frame de chamada e contém as variáveis locais e demais informações temporárias do procedimento.
Por tradição o topo da pilha está “em um endereço alto” e a pilha “cresce para baixo”, portanto empilhar um inteiro de 32 bits (push) significa subtrair 4 bytes (32 bits) de ESP para então mover este conteúdo na posição de memória que ESP aponta.
A manipulação de elementos indexados dentro na pilha de chamadas, como por exemplo o 2ª inteiro seguinte, o 5º inteiro anterior, etc. é comum e para isto utiliza-se o registrador EBP, pois o registrador ESP não permite indexação.
Uma referência com índice negativo do EBP, como em -16(%ebp), indexa um elemento que está dentro do frame do procedimento chamado (o callee em Inglês). Já uma referência com índice positivo do EBP, como em 8(%ebp), indexa um elemento dentro do frame de quem fez a chamada (o caller em Inglês).
O registrador EAX contém o valor de retorno da chamada de procedimento. Esta regra é válida para valores inteiros e ponteiros, outros tipos de dados utilizam outras convenções, como por exemplo retornar o valor dentro da própria pilha.
Interpretação do código de montagem
Vejamos agora as ações que as instruções x86 do código de true e false realizam.
_main:pushl %ebpmovl %esp, %ebpsubl $8, %espmovl $0, %eax ou movl $1, %eaxmain().leaveretAs instruções leave e ret correspondem ao epílogo do procedimento. Pode-se substituir a instrução leave por estas instruções, de igual significado:
mov %ebp, %esp
pop %ebpDe forma análoga, seria perfeitamente válido substituir as instruções
pushl %ebp movl %esp, %ebp subl $8, %esp
do prólogo por uma única instrução enter $8, $0 de igual efeito, porém este não é o comportamento do GCC na versão 4.0.
Código objeto
A partir do código de montagem de true.s, podemos compilar para um objeto binário x86 através do comando gcc -c true.s e como resultado o arquivo true.o é gerado, ou então compilar diretamente para o arquivo executável true através do comando gcc true.s -o true.
O código em binário pode ser visualizado em hexadecimal utilizando-se o comando otool -t true.o (ou false.o) e o resultado é exibido abaixo.
| true.o | false.o |
|---|---|
true.o: (__TEXT,__text) section 00000000 55 89 e5 83 ec 08 b8 00 00 00 00 c9 c3 |
false.o: (__TEXT,__text) section 00000000 55 89 e5 83 ec 08 b8 01 00 00 00 c9 c3 |
Por curiosidade, um programa executável possui código adicional ou “de cola” além do que está contido nos arquivos de objetos, este código é responsável por iniciar (chamado de start) e finalizar o programa.
Desmontagem de código objeto
O processo inverso da montagem, isto é, a partir do objeto binário traduzir de volta para instruções de máquina, é denominado de desmontagem (disassembly em Inglês).
No Mac OS X o utilitário otool com algumas diretrizes é capaz de realizar esta tarefa. Para desmotar o código objeto de true faz-se otool -t -v true.o. O resultado pode ser visualizado abaixo.
| true.o | false.o |
|---|---|
(__TEXT,__text) section _main: 00000000 pushl %ebp 00000001 movl %esp,%ebp 00000003 subl $0x08,%esp 00000006 movl $0x00000000,%eax 0000000b leave 0000000c ret |
(__TEXT,__text) section _main: 00000000 pushl %ebp 00000001 movl %esp,%ebp 00000003 subl $0x08,%esp 00000006 movl $0x00000001,%eax 0000000b leave 0000000c ret |
Este código está organizado a partir do endereço 0 em termos relativos de memória e respeita o tamanho de cada instrução. O código objeto pode ser realocado para qualquer posição de memória conforme desejar o programa de linkedição.
Note que o símbolo global _main aponta para o endereço 0 e que as constantes estão todas escritas em hexadecimal.
Gerando código de montagem com otimização
Vejamos agora o resultado no código de montagem de true e false ao passarmos a diretriz de otimização -Os do GCC. Este parâmetro instrui compilador para utilizar o máximo possível de otimizações e também o menor código.
Como os nossos exemplos são triviais, não será possível ver todas as otimizações que o GCC pode fazer.
| true.s | false.s |
|---|---|
.text .globl _main _main: pushl %ebp xorl %eax, %eax movl %esp, %ebp leave ret .subsections_via_symbols |
.text .globl _main _main: pushl %ebp movl $1, %eax movl %esp, %ebp leave ret .subsections_via_symbols |
A primeira otimização que observarmos em true.s está no uso da instrução xor %eax, %eax para atribuir o valor zero ao registrador EAX. Esta é uma instrução de dois bytes que faz uso da propriedade do ou exclusivo de retorna zero se os argumentos forem iguais.
A economia em bytes da instrução pode ser visualizada diretamente no código objeto de true.o se comparado com o código de false.o mais convencional.
true.o: (__TEXT,__text) section 00000000 55 31 c0 89 e5 c9 c3 |
false.o: (__TEXT,__text) section 00000000 55 b8 01 00 00 00 89 e5 c9 c3 |
O compilador foi experto o suficiente para verificar que a função main() não faz uso de variáveis locais ou chamadas de procedimento e que portanto não é necessário pré-alocar espaço na pilha. Como resultado desta otimização, nenhuma instrução subl $8, %esp foi gerada.
| true.o | false.o |
|---|---|
(__TEXT,__text) section _main: 00000000 pushl %ebp 00000001 xorl %eax,%eax 00000003 movl %esp,%ebp 00000005 leave 00000006 ret |
(__TEXT,__text) section _main: 00000000 pushl %ebp 00000001 movl $0x00000001,%eax 00000006 movl %esp,%ebp 00000008 leave 00000009 ret |
Código otimizado com omissão do ponteiro de frame
Existe muita curiosidade em saber o que faz a diretriz de otimização -fomit-frame-pointer do GCC. Em termos simples, ela instrui ao compilador para utilizar o registrador EBP como de uso geral ao invés de apontador de frame.
Como consequência desse parâmetro, as operações com o frame podem ficar um pouco mais trabalhosas e por outro lado ganha-se um registrador adicional para fins genéricos.
Esta técnica de otimização é interesante para a arquitetura x86, que dispõe de poucos registradores se comparado a arquiteturas do tipo RISC, agraciadas com mais registadores de uso geral.Vejamos então como o utilitário true é gerado código de máquina com as diretrizes de otimização vistas até agora.
| gcc -S -fomit-frame-pointer true.c | gcc -S -Os -fomit-frame-pointer true.c |
|---|---|
.text
.globl _main
_main:
subl $12, %esp
movl $0, %eax
addl $12, %esp
ret
.subsections_via_symbols
|
.text
.globl _main
_main:
xorl %eax, %eax
ret
.subsections_via_symbols
|
Não há referências ao registrador EBP (ponteiro de frame) nestes códigos e o código objeto gerado ficou muito reduzido, conforme podemos observar na desmontagem do código.
| -fomit-frame-pointer | -Os -fomit-frame-pointer |
|---|---|
(__TEXT,__text) section 00000000 83 ec 0c b8 00 00 00 00 83 c4 0c c3 (__TEXT,__text) section _main: 00000000 subl $0x0c,%esp 00000003 movl $0x00000000,%eax 00000008 addl $0x0c,%esp 0000000b ret |
(__TEXT,__text) section 00000000 31 c0 c3 (__TEXT,__text) section _main: 00000000 xorl %eax,%eax 00000002 ret |
É importante frisar que o uso do parâmetro -fomit-frame-pointer inviabiliza a utilização de um depurador (GDB) com o código objeto. Não é recomendável também utilizar otimizações no processo de depuração.
Chamadas de funções externas: sync()
O comando sync do Unix força o sistema operacional a grava em disco todos os dados que estão mantidos em caches do sistema. O código deste utilitário nos permite demonstrar como é a chama de um procedimento externo em código de montagem.
O fonte em C é simples, pois a função do sistema sync() não admite parâmetros e nem mesmo retorna um valor.
| sync.c | sync.s |
|---|---|
#include <unistd.h>
int main(void)
{
sync();
}
| .text .globl _main _main: pushl %ebp movl %esp, %ebp subl $8, %esp call L_sync$stub leave ret .section __IMPORT,__jump_table,symbol_stubs, \ self_modifying_code+pure_instructions,5 L_sync$stub: .indirect_symbol _sync hlt ; hlt ; hlt ; hlt ; hlt .subsections_via_symbols |
As instruções pushl, movl e subl formam o prólogo do procedimento e conforme explicado anteriormente, são equivalentes a uma única instrução enter $8, $0. As instruções leave e ret dão forma ao epílogo do procedimento.
Uma chamada de procedimento externo é realizada através da instrução call L_sync$stub. Antes de transferir o controle de execução para o endereço do rótulo L_sync$stub, guarda-se na pilha o endereço de retorno da instrução que segue à call.
Utiliza-se a diretriz do montador .indirect_symbol para indicar que L_sync$stub é um símbolo externo e está localizado em outro arquivo objeto ou fonte. É tarefa do linkeditor resolver essas dependências de símbolos.
Desmontando o código objeto de sync.o obtemos:
(__TEXT,__text) section 00000000 55 89 e5 83 ec 08 e8 02 00 00 00 c9 c3 (__TEXT,__text) section _main: 00000000 pushl %ebp 00000001 movl %esp,%ebp 00000003 subl $0x08,%esp 00000006 calll 0x0000000d ; symbol stub for: _sync 0000000b leave 0000000c ret
O controle de execução da instrução call não é necessariamente transferido para o endereço 0x0000000d, o linkeditor pode rescrever este endereço para o local verdadeiro em memória na qual o código objeto de _sync está localizado.
Em um Macintosh o código de _sync está presente na biblioteca libSystem.dylib. Utilizamos então o comando otool -tV -p _sync /usr/lib/libSystem.dylib para obter o código desmontado.
(__TEXT,__text) section ___sync: 00059fb0 movl $0x00000024,%eax 00059fb5 calll __sysenter_trap 00059fba jae 0x00059fca 00059fbc calll 0x00059fc1 00059fc1 popl %edx 00059fc2 movl 0x0014d0ff(%edx),%edx 00059fc8 jmp *%edx 00059fca ret 00059fcb nop
Para a chamada de procedimentos com argumentos, os parâmetros são postos diretamente na pilha antes de usar a instrução call. O exemplo de implementação para função de fatorial demonstra como fazer esse tipo de chamada.
Somatório e estrutura de controles
Agora apresentamos o código em C para uma função que calcula o somatório dos n primeiros inteiros. O objetivo com este exemplo é ilustrar em linguagem de montagem o uso das variáveis locais e laços de repetição com condição.
| somatorio.c | somatorio.s |
|---|---|
int somatorio(int n)
{
int i;
int soma = 0;
for (i = 0; i < n; i++)
soma = soma + i;
return soma;
}
|
.text .globl _somatorio _somatorio: pushl %ebp movl %esp, %ebp subl $24, %esp movl $0, -12(%ebp) movl $0, -16(%ebp) jmp L2 L3: movl -16(%ebp), %eax leal -12(%ebp), %edx addl %eax, (%edx) leal -16(%ebp), %eax incl (%eax) L2: movl -16(%ebp), %eax cmpl 8(%ebp), %eax jl L3 movl -12(%ebp), %eax leave ret .subsections_via_symbols |
Em uma visão de alto nível, o trecho antes de L3 trata de iniciar as variáveis locais, em L2 temos a parte que avalia se o laço for deve continuar (i < n) ou então sair do procedimento, entre L2 e L3 temos o efeito do laço em si (a soma) e também a parte que incrementa o laço (i++).
Vejamos agora em detalhes.
pushl/movl/sublmovl $0, -12(%ebp)-12(%ebp).movl $0, -16(%ebp)-16(%ebp).jmp L2L3: movl -16(%ebp), %eaxleal -12(%ebp), %edxEDX = &soma em C.addl %eax, (%edx)*EDX += EAX em C.leal -16(%ebp), %eaxincl (%eax)L2: movl -16(%ebp), %eaxcmpl 8(%ebp), %eax8(%ebp) com o valor corrente de i (EAX).addl %eax, (%edx)jl L3movl -12(%ebp), %eaxA instrução LEA (Load Effective Address em inglês) carrega o endereço de origem (como em -16(%ebp)) para um destino, como um registrador.
A instrução de comparação cmpl A, B modifica adequadamente as flags CF, OF, SF, ZF, AF conforme os argumentos A e B.
Procedimentos recursivos: fatorial
O código em C para a função fatorial de n. Este exemplo demonstra como o uso de uma pilha de chamada permite implementar recursão. Outro assunto explorado com este exemplo é a codificação em linguagem de máquina do condicional se/então/senão.
| fatorial.c | fatorial.s |
|---|---|
int fatorial(int n)
{
if (n == 0)
return 1;
else
return n * fatorial(n-1);
}
|
.text .globl _fatorial _fatorial: pushl %ebp movl %esp, %ebp subl $40, %esp cmpl $0, 8(%ebp) jne L2 movl $1, -12(%ebp) jmp L4 L2: movl 8(%ebp), %eax decl %eax movl %eax, (%esp) call _fatorial movl %eax, %edx imull 8(%ebp), %edx movl %edx, -12(%ebp) L4: movl -12(%ebp), %eax leave ret .subsections_via_symbols |
O Fluxograma do procedimento é o que segue com o código de montagem abaixo.
_fatorial: pushl/movl/sublcmpl $0, 8(%ebp)jne L2movl $1, -12(%ebp)jmp L4L2: movl 8(%ebp), %eaxdecl %eaxmovl %eax, (%esp)call _fatorialimull 8(%ebp), %edx_fatorial.movl %edx, -12(%ebp)L4: movl -12(%ebp), %eaxleave/retReferências