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.

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.

Implementações em C de true e false
true.cfalse.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
Código de montagem para true.c e false.c
true.sfalse.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
o conteúdo que segue é código de montagem para ser traduzido. O termo texto (text) é tradicionalmente 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 procedimentos externos. A função main() em C para o código de montagem transforma-se no símbolo _main.
.subsections_via_symbols
instrui ao montador que partes de código não utilizados por outros procedimentos podem ser excluídas. Ignore esta diretriz para o propósito deste artigo.

Convenção de Chamada (ABI)

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

Prólogo e epílogo

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.
subl $8, %esp
Reserva espaço na pilha para dois elementos de 4 bytes (dois inteiros). Neste ponto do código realizamos o prólogo da subrotina.
movl $0, %eax ou movl $1, %eax
Carrega a constante 0 (ou 1 para o false) no registrador EAX. Este é o valor de retorno de main() 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.

$ 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
Código objeto
true.ofalse.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
Remontagem
true.ofalse.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
Remontagem com o GDB
truefalse
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.

Depurando com o GDB
$ 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.

Código de montagem com otimização
true.sfalse.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.

Código objeto otimizado
true.ofalse.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.

Remontagem dos objetos otimizados
true.ofalse.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
Código de montagem de true com otimização e omissão do ponteiro de frame
-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.

Remontagem das otimizações
-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.

Soma dos n primeiros inteiros
somatorio.csomatorio.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++).

Função somatório

Vejamos agora em detalhes.

pushl/movl/subl
Prólogo.
movl $0, -12(%ebp)
Atribui ao local soma o valor 0.
movl $0, -16(%ebp)
Atribui ao local i o valor 0.
jmp L2
Vá para o rótulo L2.
L3: movl -16(%ebp), %eax
Guarda em EAX o valor corrente de i
leal -12(%ebp), %edx
Guarda em EDX o endereço de soma, igual a EDX = &soma em C.
addl %eax, (%edx)
Adiciona i a soma, igual a *EDX += EAX em C.
leal -16(%ebp), %eax
Guarde em EAX o endereço de i.
incl (%eax)
Incrementa i.
L2: movl -16(%ebp), %eax
Guarda em EAX o valor corrente de i
cmpl 8(%ebp), %eax
Compara o parâmetro n armazenado na pilha com o valor corrente de i (EAX).
addl %eax, (%edx)
Adiciona EAX (o valor atual de i) ao conteúdo do endereço apontado por EDX (soma).
jl L3
Se for menor o resultado, vá para o rótulo L3 (i < n).
movl -12(%ebp), %eax
Guarda o resultado da soma em EAX como valor de retorno.
leave/ret
Epílogo.

A 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.

Organização da pilha
MemóriaInformação
EBP+8Parâmetro n
EBP+4EIP salvo
EBPEBP salvo
EBP-4?
EBP-8?
EBP-12Variável soma
EBP-16Variá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.

Implementação de sync
sync.csync.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
Programa sync

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.

Função fatorial de n
fatorial.cfatorial.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.

fluxograma
_fatorial: pushl/movl/subl
Prólogo.
cmpl $0, 8(%ebp)
Compara o parâmetro n com valor zero.
jne L2
Se não for zero, vai para L2.
movl $1, -12(%ebp)
Guarda o valor 1 na pilha.
jmp L4
Vai para L4.
L2: movl 8(%ebp), %eax
Guarda n em EAX.
decl %eax
Decrementa n.
movl %eax, (%esp)
Guarda n no topo da pilha.
call _fatorial
Chama o procedimento _fatorial com o novo n.
movl %eax, %edx
Guarda o resultado da chamada (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 pilha
L4: movl -12(%ebp), %eax
Guarda em EAX o resultado do procedimento
leave/ret
Epílogo.
Organização da pilha
MemóriaInformação
EBP+8Parâmetro n
EBP+4EIP salvo
EBPEBP salvo
EBP-4?
EBP-8?
EBP-12Variá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.

Registradores de Uso geral
NomeFunçãoPreservadoPartes
EAXValor de retorno, acumuladorNãoAX (16 bits), AH e AL (8 bits)
EDXDividendo, uso geralNãoDX (16 bits), DH e DL (8 bits)
ECXContador para loops ou strings, uso geralNãoCX (16 bits), CH e CL (8 bits)
EBXBase de PIC, uso geralSim (PIC)BX (16 bits), BH e BL (8 bits)
EBPFrame Pointer, uso geralSim (FP)BP (16 bits)
ESIFonte de dados, uso geralSimSI (16 bits)
EDIDestino de dados, uso geralSimDI (16 bits)
ESPStack PointerSimSP (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.

Registradores de Segmentos
NomeFunção
CSSegmento de código
DSSegmento de dados
SSSegmento de pilha
ESSegmento extra (de dados)
FSSegmento extra (de dados)
GSSegmento extra (de dados)

Flags de status e controle do sistema.

EFLAGS
31-22212019181716 151413-12111009080706050403020100
0IDVIPVIFACVMRF 0NTIOPLOFDFIFTFSFZF0AF0PF1CF

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.

Instruction Pointer
NomeFunção
EIPEndereç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.

Ponto Flutuante
NomeFunção
ST0Valor de retorno
ST1-ST7Uso geral

Os registradores SIMD de 64 bits (MM0-7) e 128 bits (XMM0-7) não são preservados entre chamadas de subrotinas.

SIMD de 64 bits e 128 bits
NomeReferência
MM0-MM7Intel MMX
XMM0-XMM7Extensõ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