Assembly x86 em Macintosh
© 2009 Rudá Moura
Este artigo tem como objetivo ensinar assembly (linguagem de montagem ou de máquina) da arquitetura Intel x86/i386/IA-32, a partir de exemplos de programas em C.
Os códigos de montagens destes exemplos foram produzidos no compilador GCC versão 4.2 em um MacBook rodando o Mac OS X Snow Leopard.
Prólogo e Epílogo: programas 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 fontes true.c e false.c codificados em C e traduzidos para código de montagem, poderemos entender como se faz a entrada de subrotina (prólogo) e a saída de subrotina (epílogo) com o retorno de um valor inteiro.
| true.c | false.c |
|---|---|
int main(void)
{
return 0;
}
|
int main(void)
{
return 1;
}
|
O assembler (montador) é o programa que traduz, a partir de um fonte, para o código objeto em binário com extensão .o ou diretamente em programa executável. No Mac OS X, o assembler é o próprio GCC, que conduz internamente o utilitário as.
Vamos compilar para o assembly com o GCC os exemplos acima, o resultado é armazenado em true.s e false.s.
$ gcc -S true.c $ gcc -S false.c
| 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 |
Os argumentos das instruções do x86 encontram-se na ordem instrução origem, destino, pois o GCC utiliza a convenção da AT&T, já nos manuais da Intel a ordem está invertida, é instrução destino, origem.
Utiliza-se o sufixo l (long word), como em pushl, para operar com valores de 32 bits (4 bytes), o sufixo w (word) corresponde a valores de 16 bits (2 bytes) e sufixo b (byte) com um byte apenas (8 bits).
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 realmente não fazem parte das instruções do x86.
Vejamos estas diretrizes que constam no código.
.text.globl _main_main é declarado como global e portanto é visível a procedimentos externos. A função main() em C para o código de montagem transforma-se no símbolo _main..subsections_via_symbolsConvenção de Chamada (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, que é denominado de ABI – Application Binary Interface.
O registrador ESP (Stack Pointer) aponta para o topo (limite atual) da pilha de subrotinas. O registrador EBP (Frame Pointer) contém a base atual dessa pilha. A região delimitada por EBP e ESP é um frame de chamada.
Um frame contém área de linkagem (registrador EIP salvo) para o retorno a quem chamou, a base do frame anterior (registrador EBP salvo) e área de rascunho, com registradores salvos e variáveis locais à subrotina.
Por tradição da Intel, a pilha está alinhada em 16 bits, o seu topo está em um endereço alto de memória e cresce para baixo. Empilhar um inteiro de 32 bits significa subtrair 4 bytes (32 bits) de ESP e então mover este conteúdo na posição de memória que ESP aponta.
A manipulação de elementos indexados dentro na pilha é sempre através do registrador EBP, pois o ESP não permite esta operação. O primeiro parâmetro para a subrotina está em 8(%ebp), o segundo parâmetro em 12(%ebp), a regra geral para os parâmetros é 4n+8(%ebp).
Variáveis locais são indexadas através de índices negativos em EBP.
Para o retorno de subrotinas, o registrador EAX contém o valor escalar que se deseja retornar. Esta regra é válida para escalares do tipo inteiro 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:global é visível para procedimentos externos.pushl %ebpmovl %esp, %ebpsubl $8, %espmovl $0, %eax ou movl $1, %eaxmain() ao sistema operacional.leaveretAs instruções leave e ret correspondem ao epílogo da subrotina, a instrução leave tem o mesmo efeito que:
movl %ebp, %esp
popl %ebpGerando 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.
$ gcc -c true.s $ gcc true.o -o true $ gcc -c false.s $ gcc false.o -o false
O código em binário pode ser visualizado em hexadecimal utilizando-se o comando otool, conforme os comandos abaixo.
$ otool -t true.o $ otool -t false.o
| 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 no arquivo de objeto, este código (o start) é responsável por iniciar e finalizar o programa através de main().
Código objeto remontado
O processo inverso da montagem, isto é, a partir do objeto binário reproduzir de volta as instruções de máquina, é denominado de disassembly (remontagem).
No Mac OS X, o utilitário otool é capaz de realizar esta tarefa. Para remontar o código objeto de true e false faz-se:
$ otool -tv true.o $ otool -tv false.o
| 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 |
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.
Com o utilitário GDB podemos visualizar código de máquina.
$ gdb true (gdb) disassemble main $ gdb false (gdb) disassemble main
| true | false |
|---|---|
Dump of assembler code for function main: 0x00001f86 <main+0>: push %ebp 0x00001f87 <main+1>: mov %esp,%ebp 0x00001f89 <main+3>: sub $0x8,%esp 0x00001f8c <main+6>: mov $0x0,%eax 0x00001f91 <main+11>: leave 0x00001f92 <main+12>: ret End of assembler dump. |
Dump of assembler code for function main: 0x00001f86 <main+0>: push %ebp 0x00001f87 <main+1>: mov %esp,%ebp 0x00001f89 <main+3>: sub $0x8,%esp 0x00001f8c <main+6>: mov $0x1,%eax 0x00001f91 <main+11>: leave 0x00001f92 <main+12>: ret End of assembler dump. |
Melhor ainda, com o GDB é possível depurar diretamente código de montagem. Para isto, a fase de linkedição necessita incluir o código de start, que está em crt1.o, e da biblioteca padrão C, que no Mac OS X está na libSystem.dylib, além dos símbolos de depuração.
$ gcc -S true.c $ as -g true.s -o true.o $ ld /usr/lib/crt1.o -lSystem true.o -o true $ gdb true (gdb) list main 1 .text 2 .globl _main 3 _main: 4 pushl %ebp 5 movl %esp, %ebp 6 subl $8, %esp 7 movl $0, %eax 8 leave 9 ret 10 .subsections_via_symbols (gdb) start Breakpoint 1 at 0x1fa7: file true.s, line 4. Starting program: /Users/ruda/true Reading symbols for shared libraries +. done Breakpoint 1, _main () at true.s:5 5 movl %esp, %ebp (gdb) print $pc $1 = (void (*)()) 0x1fa7 <_main+1> Current language: auto; currently asm (gdb) print $sp $2 = (void *) 0xbffffa68 (gdb) print $fp $3 = (void *) 0xbffffaa8
Gerando código de montagem com otimização
Vejamos agora o resultado no código de montagem de true e false ao passarmos uma diretriz de otimização do GCC, este parâmetro instrui ao compilador para utilizar o máximo possível de otimizações e também o menor código possível.
$ gcc -Os -S true.c $ gcc -Os -S false.c
Como os nossos exemplos são triviais, não é possível conhecer todas as otimizações que o GCC é capaz de 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 está na instrução xorl %eax, %eax para atribuir o valor zero ao registrador EAX. Esta é uma instrução de dois bytes que utliza a propriedade A xor A = 0.
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 chamada de subrotina 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 |
Gerando código com omissão do ponteiro de frame
Existe muita curiosidade em saber o que faz a diretriz do GCC omit-frame-pointer, ela 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 fins geral.
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.$ gcc -fomit-frame-pointer -S true.c $ gcc -fomit-frame-pointer -Os -S true.c
| -fomit-frame-pointer | -fomit-frame-pointer -Os |
|---|---|
.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 remontagem 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 a omissão do ponteiro de frame inviabiliza a utilização do GDB e que depurar código otimizado pode apresentar comportamento diferente do esperado.
Locais e Estrutura de controles: Somatório
Agora 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 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 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++).
Vejamos agora em detalhes.
pushl/movl/sublmovl $0, -12(%ebp)movl $0, -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), %eaxaddl %eax, (%edx)jl L3movl -12(%ebp), %eaxA instrução lea de Load Effective Address carrega o endereço de uma origem, tal como -16(%ebp), para um registrador ou local de memória.
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 função externa: 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();
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 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:
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 0x00000000 0000000b movl $0x00000000,%eax 00000010 leave 00000011 ret
O controle de execução da instrução calll não é necessariamente transferido para o endereço 0x00000000, o linkeditor vai rescrever este endereço para o local verdadeiro em memória na qual o código objeto de _sync está carregado em memória.
No Mac OS X o código de _sync está presente na biblioteca libSystem.dylib.
$ otool -tV -p _sync /usr/lib/libSystem.dylib
/usr/lib/libSystem.dylib: (__TEXT,__text) section _sync: 00097844 movl $0x00000024,%eax 00097849 calll __sysenter_trap 0009784e jae 0x0009785e 00097850 calll 0x00097855 00097855 popl %edx 00097856 movl 0x0010f073(%edx),%edx 0009785c jmp *%edx 0009785e ret
Para a chamada de subrotinas 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.
Procedimentos recursivos: Fatorial de n
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 de um condicional if/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: 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/ret| 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 |
Referências
Histórico de Revisões