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.

ABI

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
truefalse
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

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

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

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
(__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.

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.csomatorio.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:

Função somatório
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ó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 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.

Programa sync
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

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

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ó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

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

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

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
truefalse
	.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
Código de montagem com otimização e omissão do ponteiro de frame
truefalse
	.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

Referências