Linguagem de Máquina x86 em Macintosh
© 2009 Rudá Moura

Este artigo tem como objetivo ensinar a linguagem de montagem ou de máquina (assembly em Inglês) da arquitetura Intel x86/i386/IA-32 através de exemplos traduzidos da linguagem C.

Como pré-requisito é necessário ter um mínimo de conhecimento da família x86, por exemplo, entender que esta é uma arquitetura CISC de 32 bits ou que é do tipo little-endian e seus registradores. O código de montagem destes exemplos foram produzidos utilizando-se o compilador GCC versão 4.0 em um Macintosh com Mac OS X.

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 programas true.c e false.c codificados em C e traduzidos para código de montagem, poderemos conhecer como é a entrada de um procedimento (prólogo) e a saída do procedimento (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;
}

Geração do código de montagem

O montador (assembler em Inglês) é um programa que traduz a partir de um fonte o código objeto em binário (arquivo com extensão .o) ou diretamente em um programa executável. No Macintosh o montador é o próprio GCC que chama o utilitário as.

Para obtermos o arquivo de montagem de true, fazemos gcc -S true.c, o resultado é armazenado em true.s, e de forma semelhante para o comando false, conforme podemos ver abaixo.

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

Diretrizes do montador

Os símbolos .text, .globl, .subsections_via_symbols são pseudo-instruções e realmente não fazem parte da linguagem x86. Estes símbolos são diretrizes para instruir o montador a tomar uma determinada ação ou comportamento.

Vejamos em detalhes estas diretrizes que constam no código.

.text
o conteúdo que segue é código de montagem para ser traduzido. O termo texto (text em Inglês) é usado tradicionalmente para designar código puro de máquina.
.globl
informa que o símbolo é global e procedimentos externos (em outros arquivos) podem fazer uso deste símbolo. O main() do C em código de montagem é identificado através de _main e obrigatoriamente deve ser global, como em .globl _main.
.subsections_via_symbols
instrui ao montador que partes de código não utilizados por outros procedimentos podem ser excluídos. Esta é uma diretriz para otimização de espaço mas de pouca serventia para o propósito deste artigo.

Convenções de chamada (ABI)

Antes de comentar o efeito das instruções x86 é pertinente entender os princípios básicos das chamadas entre procedimentos, denominado de ABI (application binary interface em Inglês).

As instruções do x86 com o sufixo l (como pushl) operam em valores de 32 bits (4 bytes), o sufixo w opera em valores de 16 bits (2 bytes) e sufixo b com 1 byte (8 bits). Os operadores das instruções encontram-se na ordem origem, destino pois o GCC utiliza a convenção AT&T, nos manuais da Intel a ordem dos operadores está invertida destino, origem.

O registrador ESP (stack pointer em Inglês) aponta para o topo da pilha de chamada de procedimentos e o registrador EBP (frame pointer em Inglês) aponta para a base desta pilha. A região delimitada por EBP e ESP é um frame de chamada e contém as variáveis locais e demais informações temporárias do procedimento.

Por tradição o topo da pilha está “em um endereço alto” e a pilha “cresce para baixo”, portanto empilhar um inteiro de 32 bits (push) significa subtrair 4 bytes (32 bits) de ESP para então mover este conteúdo na posição de memória que ESP aponta.

A manipulação de elementos indexados dentro na pilha de chamadas, como por exemplo o 2ª inteiro seguinte, o 5º inteiro anterior, etc. é comum e para isto utiliza-se o registrador EBP, pois o registrador ESP não permite indexação.

Uma referência com índice negativo do EBP, como em -16(%ebp), indexa um elemento que está dentro do frame do procedimento chamado (o callee em Inglês). Já uma referência com índice positivo do EBP, como em 8(%ebp), indexa um elemento dentro do frame de quem fez a chamada (o caller em Inglês).

O registrador EAX contém o valor de retorno da chamada de procedimento. Esta regra é válida para valores inteiros e ponteiros, outros tipos de dados utilizam outras convenções, como por exemplo retornar o valor dentro da própria pilha.

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 símbolo seguido de dois pontos é um rótulo (label em Inglês) e marca uma posição de memória. Um rótulo pode ser referenciado diretamente, se estiver no mesmo arquivo ou externamente, se este for indicado como global.
pushl %ebp
guarda a base do frame EBP do procedimento que fez a chamada (o frame anterior) na pilha.
movl %esp, %ebp
Move a base do frame EBP para o topo da pilha ESP.
subl $8, %esp
reserva espaço na pilha para dois elementos de 32 bits. Neste ponto do código temos o chamado prólogo do procedimento.
movl $0, %eax ou movl $1, %eax
carrega a constante 0 (ou 1 para o false) no registrador EAX. Este valor de retorno é repassado ao shell do usuário no final do procedimento main().
leave
volta o topo da pilha para a base da pilha, descartando o frame que foi utilizado e restaura a base do frame do procedimento que fez a chamada.
ret
desempilha o endereço de retorno do procedimento que fez a chamada e continua a execução no ponto da instrução seguinte.

As instruções leave e ret correspondem ao epílogo do procedimento. Pode-se substituir a instrução leave por estas instruções, de igual significado:

mov %ebp, %esp
volta o topo da pilha para a base, descartando o frame.
pop %ebp
desempilha a base do frame anterior.

De forma análoga, seria perfeitamente válido substituir as instruções

pushl %ebp
movl %esp, %ebp
subl $8, %esp

do prólogo por uma única instrução enter $8, $0 de igual efeito, porém este não é o comportamento do GCC na versão 4.0.

Código objeto

A partir do código de montagem de true.s, podemos compilar para um objeto binário x86 através do comando gcc -c true.s e como resultado o arquivo true.o é gerado, ou então compilar diretamente para o arquivo executável true através do comando gcc true.s -o true.

O código em binário pode ser visualizado em hexadecimal utilizando-se o comando otool -t true.o (ou false.o) e o resultado é exibido abaixo.

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 nos arquivos de objetos, este código é responsável por iniciar (chamado de start) e finalizar o programa.

Desmontagem de código objeto

O processo inverso da montagem, isto é, a partir do objeto binário traduzir de volta para instruções de máquina, é denominado de desmontagem (disassembly em Inglês).

No Mac OS X o utilitário otool com algumas diretrizes é capaz de realizar esta tarefa. Para desmotar o código objeto de true faz-se otool -t -v true.o. O resultado pode ser visualizado abaixo.

Desmontagem via otool
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

Este código está organizado a partir do endereço 0 em termos relativos de memória e respeita o tamanho de cada instrução. O código objeto pode ser realocado para qualquer posição de memória conforme desejar o programa de linkedição.

Note que o símbolo global _main aponta para o endereço 0 e que as constantes estão todas escritas em hexadecimal.

Gerando código de montagem com otimização

Vejamos agora o resultado no código de montagem de true e false ao passarmos a diretriz de otimização -Os do GCC. Este parâmetro instrui compilador para utilizar o máximo possível de otimizações e também o menor código.

Como os nossos exemplos são triviais, não será possível ver todas as otimizações que o GCC pode fazer.

Código de montagem com -Os
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 em true.s está no uso da instrução xor %eax, %eax para atribuir o valor zero ao registrador EAX. Esta é uma instrução de dois bytes que faz uso da propriedade do ou exclusivo de retorna zero se os argumentos forem iguais.

A economia em bytes da instrução pode ser visualizada diretamente no código objeto de true.o se comparado com o código de false.o mais convencional.

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 chamadas de procedimento 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.

Desmontagem 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

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

Existe muita curiosidade em saber o que faz a diretriz de otimização -fomit-frame-pointer do GCC. Em termos simples, ela instrui ao compilador para utilizar o registrador EBP como de uso geral ao invés de apontador de frame.

Como consequência desse parâmetro, as operações com o frame podem ficar um pouco mais trabalhosas e por outro lado ganha-se um registrador adicional para fins genéricos.

Esta técnica de otimização é interesante para a arquitetura x86, que dispõe de poucos registradores se comparado a arquiteturas do tipo RISC, agraciadas com mais registadores de uso geral.

Vejamos então como o utilitário true é gerado código de máquina com as diretrizes de otimização vistas até agora.

Código de montagem com otimização e omissão do ponteiro de frame
gcc -S -fomit-frame-pointer true.cgcc -S -Os -fomit-frame-pointer true.c
        .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 desmontagem do código.

-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 o uso do parâmetro -fomit-frame-pointer inviabiliza a utilização de um depurador (GDB) com o código objeto. Não é recomendável também utilizar otimizações no processo de depuração.

Chamadas de funções externas: 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();
}
	.text
.globl _main
_main:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$8, %esp
	call	L_sync$stub
	leave
	ret
	.section __IMPORT,__jump_table,symbol_stubs, \
self_modifying_code+pure_instructions,5
L_sync$stub:
	.indirect_symbol _sync
	hlt ; hlt ; hlt ; hlt ; hlt
	.subsections_via_symbols

As instruções pushl, movl e subl formam o prólogo do procedimento e conforme explicado anteriormente, são equivalentes a uma única instrução enter $8, $0. As instruções leave e ret dão forma ao epílogo do procedimento.

Uma chamada de procedimento externo é realizada através da instrução call L_sync$stub. Antes de transferir o controle de execução para o endereço do rótulo L_sync$stub, guarda-se na pilha o endereço de retorno da instrução que segue à call.

Utiliza-se a diretriz do montador .indirect_symbol para indicar que L_sync$stub é um símbolo externo e está localizado em outro arquivo objeto ou fonte. É tarefa do linkeditor resolver essas dependências de símbolos.

Desmontando o código objeto de sync.o obtemos:

(__TEXT,__text) section
00000000 55 89 e5 83 ec 08 e8 02 00 00 00 c9 c3

(__TEXT,__text) section
_main:
00000000	pushl	%ebp
00000001	movl	%esp,%ebp
00000003	subl	$0x08,%esp
00000006	calll	0x0000000d	; symbol stub for: _sync
0000000b	leave
0000000c	ret

O controle de execução da instrução call não é necessariamente transferido para o endereço 0x0000000d, o linkeditor pode rescrever este endereço para o local verdadeiro em memória na qual o código objeto de _sync está localizado.

Em um Macintosh o código de _sync está presente na biblioteca libSystem.dylib. Utilizamos então o comando otool -tV -p _sync /usr/lib/libSystem.dylib para obter o código desmontado.

(__TEXT,__text) section
___sync:
00059fb0        movl    $0x00000024,%eax
00059fb5        calll   __sysenter_trap
00059fba        jae     0x00059fca
00059fbc        calll   0x00059fc1
00059fc1        popl    %edx
00059fc2        movl    0x0014d0ff(%edx),%edx
00059fc8        jmp     *%edx
00059fca        ret
00059fcb        nop

Para a chamada de procedimentos 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 esse tipo de chamada.

Somatório e estrutura de controles

Agora apresentamos o código em C para uma função que calcula o somatório dos n primeiros inteiros. O objetivo com este 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 então sair do procedimento, 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++).

Vejamos agora em detalhes.

pushl/movl/subl
Criam um novo frame de chamada.
movl $0, -12(%ebp)
Atribui a soma o valor 0. A variável local soma é o 4º elemento da pilha (em relação ao topo) e por isso é indexado como -12(%ebp).
movl $0, -16(%ebp)
Atribui a i o valor 0. A variável local i é o 3º elemento da pilha (em relação ao topo) e por isso é indexado como -16(%ebp).
jmp L2
Pule para 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 EDX = &soma em C.
addl %eax, (%edx)
Adiciona i a soma, igual *EDX += EAX em C.
leal -16(%ebp), %eax
Guarde em EAX o endereço de i.
incl (%eax)
Incrementa o 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 8(%ebp) 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 L3 (i < n).
movl -12(%ebp), %eax
Guarda o resultado da soma no registrador (EAX) para o retorno do procedimento.
leave/ret
Descarta o frame e retorna.

A instrução LEA (Load Effective Address em inglês) carrega o endereço de origem (como em -16(%ebp)) para um destino, como um registrador.

A instrução de comparação cmpl A, B modifica adequadamente as flags CF, OF, SF, ZF, AF conforme os argumentos A e B.

Procedimentos recursivos: fatorial

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 do condicional se/então/senão.

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 do procedimento é o que segue com o código de montagem abaixo.

fluxograma
_fatorial: pushl/movl/subl
criam um novo frame.
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
descarta o frame e retorna.

Referências