Assembly de x86 em Macintosh
Rudá Moura
Setembro de 2009
Este artigo tem como objetivo ensinar linguagem de montagem ou de máquina (assembly) da arquitetura Intel x86/i386/IA-32, a partir de exemplos de programas em C.
Os códigos de montagens destes exemplos foram produzidos com o 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 traduzir 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, como em pushl (Long Word ou Doubleword), para operar com valores de 4 bytes (32 bits), o sufixo w (Word) corresponde a valores de 2 bytes (16 bits) e sufixo b (Byte) a apenas um byte (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.
.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 tem alinhamento de 16 bytes, o seu topo está em um endereço alto de memória e cresce para baixo. Empilhar um inteiro significa subtrair 4 bytes de ESP e então mover este conteúdo na posição de memória que ESP aponta.
A manipulação de elementos indexados na pilha é sempre através do registrador EBP, pois o ESP não permite esta operação. Por exemplo, o primeiro parâmetro para a chamar uma subrotina está em 8(%ebp), o segundo parâmetro em 12(%ebp), a regra geral é 4n+8(%ebp).
Variáveis locais são indexadas através de índices negativos em EBP, pois estão dentro do frame da subrotina atual que foi chamada (callee) e conforme citado anteriormente, os parâmetros estão no frame da subrotina que fez a chamada (caller).
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.
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 externamente.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, observe que a instrução leave tem o mesmo efeito que:
movl %ebp, %esppopl %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 também visualizar o 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/src/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 |
(gdb) info reg eax 0x1 1 ecx 0x6d 109 edx 0x7 7 ebx 0xbffffb2c -1073743060 esp 0xbffffa68 0xbffffa68 ebp 0xbffffaa8 0xbffffaa8 esi 0xbffffadc -1073743140 edi 0xbffffad4 -1073743148 eip 0x1fa7 0x1fa7 <_main+1> eflags 0x246 582 cs 0x17 23 ss 0x1f 31 ds 0x1f 31 es 0x1f 31 fs 0x0 0 gs 0x37 55 |
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 | false.o |
|---|---|
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 é interessante para a arquitetura x86, que dispõe de poucos registradores, se comparado a arquiteturas do tipo RISC, agraciadas com mais registradores.
$ 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 _sync. Antes de transferir o controle de execução para o endereço do rótulo _sync, guarda-se na pilha o endereço de retorno da instrução que segue à call.
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 call 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, conforme podemos ver abaixo.
$ 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 uma chamada com parâmetros.
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 |
Apêndice
Registradores de 32 bits de uso geral. Os registradores EAX, EDX, ECX não são preservados entre chamadas de subrotinas.
| Nome | Função | Preservado | Partes |
|---|---|---|---|
| EAX | Valor de retorno, acumulador | Não | AX (16 bits), AH e AL (8 bits) |
| EDX | Dividendo, uso geral | Não | DX (16 bits), DH e DL (8 bits) |
| ECX | Contador para loops ou strings, uso geral | Não | CX (16 bits), CH e CL (8 bits) |
| EBX | Base de PIC, uso geral | Sim (PIC) | BX (16 bits), BH e BL (8 bits) |
| EBP | Frame Pointer, uso geral | Sim (FP) | BP (16 bits) |
| ESI | Fonte de dados, uso geral | Sim | SI (16 bits) |
| EDI | Destino de dados, uso geral | Sim | DI (16 bits) |
| ESP | Stack Pointer | Sim | SP (16 bits) |
Os registradores de segmentos são seletores de segmentos de 16 bits. O Mac OS X utiliza o conceito de memória plana (flat) sem segmentação, portanto os registradores de segmento não são utilizados pelo sistema operacional.
| Nome | Função |
|---|---|
| CS | Segmento de código |
| DS | Segmento de dados |
| SS | Segmento de pilha |
| ES | Segmento extra (de dados) |
| FS | Segmento extra (de dados) |
| GS | Segmento extra (de dados) |
Flags de status e controle do sistema.
| 31-22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13-12 | 11 | 10 | 09 | 08 | 07 | 06 | 05 | 04 | 03 | 02 | 01 | 00 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | ID | VIP | VIF | AC | VM | RF | 0 | NT | IOPL | OF | DF | IF | TF | SF | ZF | 0 | AF | 0 | PF | 1 | CF |
ID Flag (ID), Virtual Interrupt Pending (VIP), Virtual Interrupt Flag (VIF), Alignment Check (AC), Virtual-8086 mode (VM), Resume Flag (RF), Nested Task (NT), I/O Privilege Level (IOPL), Overflow Flag (OF), Direction Flag (DF), Interrupt Enable Flag (IF), Trap Flag (TF), Sign Flag (SF), Zero Flag (ZF), Auxiliary Carry Flag (AF), Parity Flag (PF), Carry Flag (CF).
O registrador EIP de 32 bits é o que se costuma chamar de Contador de programa.
| Nome | Função |
|---|---|
| EIP | Endereço da próxima instrução a executar |
Registradores de Ponto Flutuante não são preservados entre chamadas de subrotinas, mas são zerados na estrada e saída da subrotina.
| Nome | Função |
|---|---|
| ST0 | Valor de retorno |
| ST1-ST7 | Uso geral |
Os registradores SIMD de 64 bits (MM0-7) e 128 bits (XMM0-7) não são preservados entre chamadas de subrotinas.
| Nome | Referência |
|---|---|
| MM0-MM7 | Intel MMX |
| XMM0-XMM7 | Extensões SSE, SSE2, SSE3 e SSSE3 |
Referências
Este artigo é inspirado no Apêndice A do livro Computer Organization and Design: the Hardware/Software Interface. 2ª edição.
Histórico de Revisões
© MMIX