Você está na página 1de 10

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

Código em C e de montagem para true e false


true false

int main(void) int main(void)


{ {
return 0; return 1;
} }

.text .text
.globl _main .globl _main
_main: _main:
pushl %ebp pushl %ebp
movl %esp, %ebp movl %esp, %ebp
subl $8, %esp subl $8, %esp
movl $0, %eax movl $1, %eax
leave leave
ret ret
.subsections_via_symbols .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ímbolo
main em C transforma-se em _main para 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, %eax ou movl $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 $ gcc -m32 -c false.s


$ otool -t true.o $ otool -t false.o
true.o: false.o:
(__TEXT,__text) section (__TEXT,__text) section
00000000 55 89 e5 83 ec 08 b8 00 00 00 00 c9 c3 00000000 55 89 e5 83 ec 08 b8 01 00 00 00 c9 c

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 $ otool -tv false.o


(__TEXT,__text) section (__TEXT,__text) section
_main: _main:
00000000 pushl %ebp 00000000 pushl %ebp
00000001 movl %esp,%ebp 00000001 movl %esp,%ebp
00000003 subl $0x08,%esp 00000003 subl $0x08,%esp
00000006 movl $0x00000000,%eax 00000006 movl $0x00000001,%eax
0000000b leave 0000000b leave
0000000c ret 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.

Soma dos n primeiros inteiros


somatorio.c somatorio.s

.text
.globl _somatorio
_somatorio:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
int somatorio(int n) movl $0, -12(%ebp)
{ movl $0, -16(%ebp)
int soma = 0; jmp L2
int i; L3:
movl -16(%ebp), %eax
for (i = 0; i < n; i++) addl %eax, -12(%ebp)
soma = soma + i; incl -16(%ebp)
L2:
return soma; 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:

pushl e movl

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.

leave e ret

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.

Organização da pilha em
somatorio()
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.

Implementação de sync
sync.c sync.s

#include <unistd.h> .text


.globl _main
int main(void) _main:
{ pushl %ebp
sync(); movl %esp, %ebp
return 0; 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 de n
fatorial.c fatorial.s
int fatorial(int n) .text
{ .globl _fatorial
if (n == 0) _fatorial:
return 1; pushl %ebp
else movl %esp, %ebp
return n * fatorial(n-1); 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 e movl

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

leave e ret

Epílogo da subrotina.

Organização da pilha em
fatorial()
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

Código de montagem otimizado


true.s false.s

.text .text
.globl _main .globl _main
_main: _main:
pushl %ebp pushl %ebp
movl %esp, %ebp movl %esp, %ebp
xorl %eax, %eax movl $1, %eax
leave leave
ret ret
.subsections_via_symbols .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.

Código objeto otimizado


true.o false.o

true.o: false.o:
(__TEXT,__text) section (__TEXT,__text) section
00000000 55 89 e5 31 c0 c9 c3 00000000 55 89 e5 b8 01 00 00 00 c9 c3

true.o: false.o:
(__TEXT,__text) section (__TEXT,__text) section
_main: _main:
00000000 pushl %ebp 00000000 pushl %ebp
00000001 movl %esp,%ebp 00000001 movl %esp,%ebp
00000003 xorl %eax,%eax 00000003 movl $0x00000001,%eax
00000005 leave 00000008 leave
00000006 ret 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

Código de montagem com omissão do ponteiro de frame


true false

.text .text
.globl _main .globl _main
_main: _main:
subl $12, %esp subl $12, %esp
movl $0, %eax movl $1, %eax
addl $12, %esp addl $12, %esp
ret ret
.subsections_via_symbols .subsections_via_symbols

true.o: false.o:
(__TEXT,__text) section (__TEXT,__text) section
00000000 83 ec 0c b8 00 00 00 00 83 c4 0c c3 00000000 83 ec 0c b8 01 00 00 00 83 c4 0c c3

true.o: false.o:
(__TEXT,__text) section (__TEXT,__text) section
_main: _main:
00000000 subl $0x0c,%esp 00000000 subl $0x0c,%esp
00000003 movl $0x00000000,%eax 00000003 movl $0x00000001,%eax
00000008 addl $0x0c,%esp 00000008 addl $0x0c,%esp
0000000b ret 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

Código de montagem com otimização e omissão do ponteiro de frame


true false

.text .text
.globl _main .globl _main
_main: _main:
xorl %eax, %eax movl $1, %eax
ret ret
.subsections_via_symbols .subsections_via_symbols

true.o: false.o:
(__TEXT,__text) section (__TEXT,__text) section
00000000 31 c0 c3 00000000 b8 01 00 00 00 c3

true.o: false.o:
(__TEXT,__text) section (__TEXT,__text) section
_main: _main:
00000000 xorl %eax,%eax 00000000 movl $0x00000001,%eax
00000002 ret 00000005 ret

Referências

Intel® 64 and IA-32 Architectures Software Developer's Manuals


Mac OS X Assembler Reference
Introduction to Mac OS X ABI Function Call Guide
IA-32 assembly on Mac OS X de Fabien Sanglard

Você também pode gostar