Escolar Documentos
Profissional Documentos
Cultura Documentos
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.
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.
Parâmetros 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.
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.
.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.
Vejamos agora as ações que as instruções x86 do código de true e false realizam.
_main:
pushl %ebp
Move a base do frame para o topo da pilha. Neste ponto do código realizamos o
prólogo da subrotina.
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:
popl %ebp
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.
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:
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.
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.
.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++).
pushl e movl
Prólogo da subrotina.
jmp L2
incl (%eax)
Incrementa i.
jl L3
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
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.
/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.
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
Prólogo da subrotina.
jne L2
jmp L4
Guarda n em EAX.
decl %eax
Decrementa n.
call _fatorial
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
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.
.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.
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
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.
.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.
.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