Assembly de x86 em Macintosh
Introdução
Este artigo tem como objetivo ensinar a linguagem de máquina ou de montagem (assembly) da arquitetura Intel x86/i386/IA-32, a partir de exemplos de programas em C, traduzidos para a linguagem de montagem. Nos exemplos apresentados, utilizou-se o compilador GCC 4.2.1 em um MacBook Intel com Mac OS X 10.6.4.
O presente artigo foi inspirado no Apêndice A do livro Computer Organization and Design: the Hardware/Software Interface. 2ª edição. David A. Patterson e John L. Hennessy.
Arquitetura
O Intel x86 é uma arquitetura CISC de 32 bits. Os registradores de uso geral (todos de 32 bits) são EAX, EBX, ECX, EDX, ESI e EDI. O apontador ou contador de instruções é o EIP, os registradores ESP e EBP gerenciam a pilha de dados. O registrador EFLAGS é uma coleção de sinalizadores de status e controle
O Intel x86 possui ainda registradores de 16 bits para segmentos de memória CS, DS, SS, ES, FS, GS. Registradores para ponto flutuante ST0–ST7 (32 bits), vetores SIMD (Single Instruction, Multiple Data) MM0–MM7 (64 bits) e XMM0-XMM7 (128 bits).
O Mac OS X segue o modelo ILP32, na qual inteiros longos (long int) e ponteiros (void*) são de 32 bits. O compilador GCC utiliza a convenção da AT&T, as instruções encontram-se na ordem instrução origem, destino (guarde bem isso). Nos manuais da Intel a ordem é invertida para instrução destino, origem.
Utiliza-se o sufixo l (long word ou double word), como em pushl EAX, para instruções que operam com valores de 32 bits, o sufixo w (word) corresponde a valores de 16 bits e sufixo b (byte) para valores de 8 bits. Quando não for especificado um sufixo, assume-se valores de 32 bits para a operação.
Convenção de Chamadas (ABI)
Antes de entender o efeito das instruções x86 é pertinente conhecer os princípios básicos que regem as chamadas entre subrotinas e retorno de valores, denominado em inglês de Application Binary Interface (ABI).
Frame de chamada
O registrador ESP (Stack Pointer) aponta para o limite atual (topo) da pilha de subrotinas. O topo da pilha está em um endereço alto de memória (digamos, em 0xbffffa50) e cresce para baixo. Empilhar um inteiro significa subtrair 4 bytes de ESP e depois mover conteúdo para a posição de memória que ESP aponta.
O registrador EBP (Frame Pointer) contém a base atual da pilha (por exemplo, 0xbffffa58). A região delimitada por EBP e ESP é uma área de rascunho para variáveis locais e temporáris da subrotina, registradores que devem ser salvos e algum preenchimento (padding). Um frame de chamada contém ainda uma área de linkagem (o EIP salvo) para o retorno a instrução seguinte de quem chamou e a base do frame anterior (o EBP salvo).
Parâmetros da subrotina
A manipulação de valores dentro da pilha se faz através do registrador EBP, pois o registrador ESP não permite acesso indexado. Quando uma subrotina (caller) chama uma outra subrotina, os parâmetros são inseridos na pilha e o primeiro parâmetro encontra-se em 8(%ebp), o segundo parâmetro em 12(%ebp), a regra geral é 4n+8(%ebp) para os n parâmetros inteiros da subrotina.
Variáveis locais
As variáveis locais são inseridas na pilha seguindo a ordem de sua declaração, e estão disponíveis através de índices negativos de EBP, pois fazem parte do frame da subrotina que foi chamada (callee). O endereço indicado por -12(%ebp) contém a primeira variável local declarada na subrotina e -16(%ebp) a segunda declaração, a forma geral é -4n-12(%ebp) para as n variáveis inteiras declaradas na subrotina.
Retorno de valores
Se o valor de retorno for um inteiro ou um ponteiro, este é posto no registrador EAX, para valores de ponto flutuante o retorno estará contido no registrador ST0. Outros tipos de dados utilizam outras convenções, verifique na seção de referências aonde obter mais informações.
Alinhamento da pilha
Como requisito de quem efetua uma chamada de subrotina para as funções do Mac OS X, deve-se obrigatoriamente deixar a pilha alinhada em 16 bytes, que é o tamanho do maior tipo de dados SIMD. Este alinhamento é uma características do Macintosh Intel para otimização de chamadas.
Quando não se alinha a pilha, o sintoma característico é a ocorrência de falha de segmentação (segmentation fault). Através do depurador GDB ou do Crash Reporter, é possível identificar a presença do símbolo misaligned_stack_error_ sinalizando o erro de alinhamento na pilha.
Thread 0 Crashed: Dispatch queue: com.apple.main-thread 0 libSystem.B.dylib 0x984bd0f0 misaligned_stack_error_ + 0 1 sync 0x00001f7d main + 11 2 sync 0x00001f69 start + 53
Prólogo e Epílogo de Subrotinas
Os 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 fontes de true.c e false.c, traduzidos para o código de montagem, poderemos entender como se faz a entrada de subrotina (o prólogo) e a saída de subrotina (o epílogo) com o retorno de um valor inteiro.
O programa montador (assembler) traduz o código fonte da máquina com extensão .s para o código objeto binário com extensão .o ou diretamente em um programa executável no formato Mach-O. No Mac OS X, o assembler é o próprio GCC, que conduz internamente o utilitário as.
Vamos instruir o GCC a gerar apenas o código de montagem dos programas em C.
$ gcc -m32 -S true.c $ gcc -m32 -S false.c
| true | false |
|---|---|
int main(void)
{
return 0;
}
|
int main(void)
{
return 1;
}
|
.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 diretrizes que vão instruir o montador a tomar uma determinada ação ou comportamento. Estes símbolos são pseudo-instruções e na realidade não fazem parte do conjunto de instruções do x86.
.text- O conteúdo que segue é código de montagem para ser traduzido. O termo texto (text) neste contexto é utilizado para designar código puro de máquina.
.globl _main- Informa que o símbolo
_mainé declarado como global e portanto visível a subrotinas externas. O símbolomainem C transforma-se em_mainpara o código de máquina. .subsections_via_symbols- Instrui ao montador que partes de código não utilizados por outros procedimentos podem ser excluídas. É seguro ignorar esta diretriz para o propósito do artigo.
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:- Um nome seguido de dois pontos é um rótulo (label) e referencia uma posição em memória. Um rótulo
globalé visível externamente. pushl %ebp- Guarda a base do frame de quem fez a chamada (o frame anterior) na pilha.
movl %esp, %ebp- Move a base do frame para o topo da pilha. Neste ponto do código realizamos o prólogo da subrotina.
subl $8, %esp- Reserva espaço na pilha.
movl $0, %eaxoumovl $1, %eax- Carrega a constante 0 (ou 1 para o false) no registrador EAX. Este é o valor de retorno da subrotina
main()e consequentemente, ao sistema operacional. leave- Volta o topo da pilha para a base do frame para descarta-lo. Restaura a base do frame da subrotina que fez a chamada.
ret- Restaura o endereço de retorno da subrotina que fez a chamada e continua a execução neste endereço.
As instruções leave e ret correspondem ao epílogo da subrotina, observe que a instrução leave tem o mesmo efeito que:
movl %ebp, %esp- Volta o topo da pilha para a base, descartando o frame.
popl %ebp- Desempilha a base do frame anterior.
Gerando o código objeto
A partir do código de montagem de true.s, podemos gerar um objeto binário x86 no arquivo true.o, ou então compilar diretamente para o arquivo executável true. O código em binário pode ser visualizado em hexadecimal utilizando-se o comando otool.
$ gcc -m32 -c true.s $ otool -t true.o true.o: (__TEXT,__text) section 00000000 55 89 e5 83 ec 08 b8 00 00 00 00 c9 c3 |
$ gcc -m32 -c false.s $ otool -t false.o 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 no arquivo de objeto, este código (o start) é responsável por iniciar e finalizar o programa através de main(). O formato dos arquivos executáveis no Mac OS X é chamado de Mach-O.
Código objeto remontado
O processo inverso da montagem, isto é, a partir do objeto binário reconstruir de volta as instruções de máquina, é denominado de remontagem (disassembly). No Mac OS X, o utilitário otool é capaz de realizar esta tarefa, para a remontagem do código objeto de true e false faz-se:
$ otool -tv true.o (__TEXT,__text) section _main: 00000000 pushl %ebp 00000001 movl %esp,%ebp 00000003 subl $0x08,%esp 00000006 movl $0x00000000,%eax 0000000b leave 0000000c ret |
$ otool -tv false.o (__TEXT,__text) section _main: 00000000 pushl %ebp 00000001 movl %esp,%ebp 00000003 subl $0x08,%esp 00000006 movl $0x00000001,%eax 0000000b leave 0000000c ret |
Note que o símbolo global _main aponta para o endereço 0 e as constantes estão todas escritas em hexadecimal. O código objeto pode ser realocado para qualquer posição de memória, conforme desejar o programa de linkedição ld reescrevendo esse endereço.
Variáveis Locais & Estrutura de Controle
Apresentamos o código em C para uma função que calcula o somatório dos n primeiros inteiros. O objetivo deste exemplo é ilustrar, em linguagem de montagem, o uso de variáveis locais e laços de repetição com condição.
| somatorio.c | somatorio.s |
|---|---|
int somatorio(int n)
{
int soma = 0;
int i;
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 addl %eax, -12(%ebp) incl -16(%ebp) 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 sair da subrotina, 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++).
Em detalhes, temos que:
pushlemovl- Prólogo da subrotina.
subl $24, %esp- Abre espaço na pilha.
movl $0, -12(%ebp)- Atribui a variável local soma o valor 0.
movl $0, -16(%ebp)- Atribui a variável local i o valor 0.
jmp L2- Salto incondicional para o rótulo L2.
L3: movl -16(%ebp), %eax- Guarda em EAX o valor atual de i.
addl %eax, -12(%ebp)- Faz soma = soma + i.
incl (%eax)- Incrementa i.
L2: movl -16(%ebp), %eax- Guarda em EAX o valor atual de i
cmpl 8(%ebp), %eax- Compara o parâmetro n armazenado na pilha com o valor corrente de i (EAX).
jl L3- Se for menor o resultado (i < n), salte para o rótulo L3.
movl -12(%ebp), %eax- Guarda o resultado da soma em EAX como valor de retorno.
leaveeret- Epílogo da subrotina.
A instrução de comparação cmpl A, B modifica adequadamente as flags CF, OF, SF, ZF, AF do registrador EFLAGS conforme os argumentos A e B.
| Memória | Informação |
|---|---|
| EBP+8 | Parâmetro n |
| EBP+4 | EIP salvo |
| EBP | EBP salvo |
| EBP-4 | ? |
| EBP-8 | ? |
| EBP-12 | Variável soma |
| EBP-16 | Variável i |
| EBP-20 | ? |
| EBP-24 (= ESP) | Topo |
Chamada de Subrotina
O comando do Unix sync força todos os caches do sistema operacional a serem gravados em disco. Este comando chama a função de biblioteca sync(), que por sua vez realiza uma chamada de sistema de mesmo nome. O fonte em C deste utilitário, nos permitir demonstrar como é feita a chama de uma subrotina externa. O código é simples, pois a função de biblioteca sync() não admite parâmetros e nem mesmo retorna um valor.
| sync.c | sync.s |
|---|---|
#include <unistd.h>
int main(void)
{
sync();
return 0;
}
|
.text
.globl _main
_main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
call _sync
movl $0, %eax
leave
ret
.subsections_via_symbols
|
Uma chamada externa de subrotina é realizada através da instrução call _sync. Antes de transferir o controle de execução para o endereço do símbolo externo _sync, guarda-se na pilha o endereço de retorno (o EIP) da instrução que segue a chamada, neste caso o endereço de movl $0, %eax.
Desmontando o código objeto de sync.o, obtemos:
sync.o: (__TEXT,__text) section 00000000 55 89 e5 83 ec 08 e8 f5 ff ff ff b8 00 00 00 00 00000010 c9 c3 |
sync.o: (__TEXT,__text) section _main: 00000000 pushl %ebp 00000001 movl %esp,%ebp 00000003 subl $0x08,%esp 00000006 calll 0x100000000 0000000b movl $0x00000000,%eax 00000010 leave 00000011 ret |
O controle de execução da instrução call não é transferido para o endereço 0x100000000, é tarefa do linkeditor rescrever este endereço para o local verdadeiro em memória na qual o código objeto de _sync estará previamente carregado. Isto é feito durante a carga do programa para execução.
No Mac OS X o código de _sync está presente na biblioteca libSystem.dylib, que é equivalente as bibliotecas libc e a libm de outros sistemas.
$ otool -arch i386 -tV -p _sync /usr/lib/libSystem.dylib
/usr/lib/libSystem.dylib: (__TEXT,__text) section _sync: 00097540 movl $0x00000024,%eax 00097545 calll __sysenter_trap 0009754a jae 0x0009755a 0009754c calll 0x00097551 00097551 popl %edx 00097552 movl 0x0011036f(%edx),%edx 00097558 jmp *%edx 0009755a ret
Para a chamada de subrotinas com argumentos, os parâmetros são postos diretamente na pilha, conforme descrito anteriormente, na seção ABI. O exemplo de implementação da função de fatorial demonstra como fazer a chamada com um parâmetro.
Subrotinas Recursivas (Fatorial)
Este exemplo demonstra como o uso de uma pilha de dados permite chamadas recursivas, para isto, utilizaremos o código em C da função fatorial de n. Outro assunto explorado com este exemplo é a codificação em linguagem de máquina do condicional if/then/else.
| 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 da subrotina é o que segue, com o código de montagem abaixo.
_fatorial:pushlemovl- Prólogo da subrotina.
subl $40, %esp- Abre espaço na pilha.
cmpl $0, 8(%ebp)- Compara o parâmetro n com valor zero.
jne L2- Se não for zero (n≠0), salta para L2.
movl $1, -12(%ebp)- Guarda o valor 1 em uma variável temporária (temp).
jmp L4- Salto incondicional para L4.
L2: movl 8(%ebp), %eax- Guarda n em EAX.
decl %eax- Decrementa n.
movl %eax, (%esp)- Guarda n no topo da pilha, para ser o parâmetro da chamada que segue.
call _fatorial- Chama o procedimento _fatorial, com o novo n.
- movl %eax, %edx
- Guarda o resultado da subrotina (EAX) em EDX
imull 8(%ebp), %edx- Multiplica o valor original de n com o retorno da chamada de
_fatorial. movl %edx, -12(%ebp)- Guarda o resultado na variável temporária.
L4: movl -12(%ebp), %eax- Salva em EAX o resultado do procedimento
leaveeret- Epílogo da subrotina.
| Memória | Informação |
|---|---|
| EBP+8 | Parâmetro n |
| EBP+4 | EIP salvo |
| EBP | EBP salvo |
| EBP-4 | ? |
| EBP-8 | ? |
| EBP-12 | Variável temp |
| EBP-16 | ? |
| EBP-20 | ? |
| EBP-24 | ? |
| EBP-28 | ? |
| EBP-32 | ? |
| EBP-36 | ? |
| EBP-40 (=ESP) | Topo |
Otimizações do Compilador e Código de Montagem
Vejamos agora o resultado no código de montagem de true e false, quando passamos a diretriz -Os do GCC, que instrui ao compilador a gerar código com otimização e o menor possível. Como os nossos exemplos são triviais, não será possível conhecer todas as otimizações que o GCC é capaz de fazer.
$ gcc -m32 -Os -S true.c $ gcc -m32 -Os -S false.c
| true.s | false.s |
|---|---|
.text .globl _main _main: pushl %ebp movl %esp, %ebp xorl %eax, %eax leave ret .subsections_via_symbols |
.text .globl _main _main: pushl %ebp movl %esp, %ebp movl $1, %eax leave ret .subsections_via_symbols |
A otimização que observarmos está na instrução xorl %eax, %eax de true.s, que é jeito mais curto, com apenas dois bytes, para atribuir o valor zero ao registrador EAX. Este truque tem como base a propriedade A xor A = 0.
O compilador foi experto o suficiente para verificar que a função main() não faz uso de variáveis locais ou chamada de subrotina, portanto não é necessário pré-alocar espaço na pilha. Como resultado desta otimização, nenhuma instrução subl $xx, %esp está presente.
| true.o | false.o |
|---|---|
true.o: (__TEXT,__text) section 00000000 55 89 e5 31 c0 c9 c3 |
false.o: (__TEXT,__text) section 00000000 55 89 e5 b8 01 00 00 00 c9 c3 |
true.o: (__TEXT,__text) section _main: 00000000 pushl %ebp 00000001 movl %esp,%ebp 00000003 xorl %eax,%eax 00000005 leave 00000006 ret |
false.o: (__TEXT,__text) section _main: 00000000 pushl %ebp 00000001 movl %esp,%ebp 00000003 movl $0x00000001,%eax 00000008 leave 00000009 ret |
Código com omissão do ponteiro de frame
A diretriz do GCC -fomit-frame-pointer instrui ao compilador para desligar o uso de ponteiro de frame e utilizar o EBP como um registrador de uso geral. Como consequência desse parâmetro, as operações com o frame podem ficar um pouco mais trabalhosas, mas por outro lado, ganha-se um registrador adicional para uso geral.
Esta técnica de otimização é interessante para a arquitetura x86, que dispõe de poucos registradores se comparado a arquiteturas do tipo RISC, agraciadas com muitos registradores. Vejamos o resultado da diretriz.
$ gcc -m32 -fomit-frame-pointer -S true.c $ gcc -m32 -fomit-frame-pointer -S false.c
| true | false |
|---|---|
.text .globl _main _main: subl $12, %esp movl $0, %eax addl $12, %esp ret .subsections_via_symbols |
.text .globl _main _main: subl $12, %esp movl $1, %eax addl $12, %esp ret .subsections_via_symbols |
true.o: (__TEXT,__text) section 00000000 83 ec 0c b8 00 00 00 00 83 c4 0c c3 true.o: (__TEXT,__text) section _main: 00000000 subl $0x0c,%esp 00000003 movl $0x00000000,%eax 00000008 addl $0x0c,%esp 0000000b ret |
false.o: (__TEXT,__text) section 00000000 83 ec 0c b8 01 00 00 00 83 c4 0c c3 false.o: (__TEXT,__text) section _main: 00000000 subl $0x0c,%esp 00000003 movl $0x00000001,%eax 00000008 addl $0x0c,%esp 0000000b ret |
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 remontagem do código. É importante frisar que a omissão do ponteiro de frame pode inviabilizar a utilização do GDB e que depurar código otimizado pode apresentar comportamento diferente do esperado.
Para finalizar, vamos ativar otimização e omissão do ponteiro de frame para ver o resultado em código de montagem.
$ gcc -m32 -Os -fomit-frame-pointer -S true.c $ gcc -m32 -Os -fomit-frame-pointer -S false.c
| true | false |
|---|---|
.text .globl _main _main: xorl %eax, %eax ret .subsections_via_symbols |
.text .globl _main _main: movl $1, %eax ret .subsections_via_symbols |
true.o: (__TEXT,__text) section 00000000 31 c0 c3 true.o: (__TEXT,__text) section _main: 00000000 xorl %eax,%eax 00000002 ret |
false.o: (__TEXT,__text) section 00000000 b8 01 00 00 00 c3 false.o: (__TEXT,__text) section _main: 00000000 movl $0x00000001,%eax 00000005 ret |