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.
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).
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).
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.
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.
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.
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
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 |
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
.globl _main
_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
Vejamos agora as ações que as instruções x86 do código de true e false realizam.
_main:
global
é visível externamente.pushl %ebp
movl %esp, %ebp
subl $8, %esp
movl $0, %eax
ou movl $1, %eax
main()
e consequentemente, ao sistema operacional.leave
ret
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
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
.
$ 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.
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.
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:
pushl
e movl
subl $24, %esp
movl $0, -12(%ebp)
movl $0, -16(%ebp)
jmp L2
L3: movl -16(%ebp), %eax
addl %eax, -12(%ebp)
incl (%eax)
L2: movl -16(%ebp), %eax
cmpl 8(%ebp), %eax
jl L3
movl -12(%ebp), %eax
leave
e ret
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 |
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.
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
: pushl
e movl
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
imull 8(%ebp), %edx
_fatorial
.movl %edx, -12(%ebp)
L4: movl -12(%ebp), %eax
leave
e 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 |
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 |
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 |