Você está na página 1de 215

Dicas - C e Assembly

para arquitetura x86-64


Verso 0.33.9
2014-2017 por Frederico Lamberti Pissarra
3 de fevereiro de 2017
Ttulo: C & Assembly para arquitetura x86-64
Autor: Frederico Lamberti Pissarra
Ano de publicao: 2016
Este material protegido pela licena GFDL 1.3:
C & Assembly para arquitetura x86-64
Copyright (C) 2014-2016 by Frederico Lamberti Pissarra

A permisso de cpia, distribuio e/ou modificao deste documento


garantida sob os termos da licena GNU Free Documentation License,
Version 1.3 ou verso mais recente publicada pela Free Software
Foundation.

O texto integral da licena pode ser lido no link


https://www.gnu.org/licenses/fdl.html.

importante, no entanto, advertir ao leitor que este material encontra-se em processo de produo.
Trata-se, ento, de um rascunho e no reflete o material finalizado.

Figura da capa: DIE da arquitetura Intel Ivy Bridge


ndice
Introduo 1
Organizao do livro 2
Sistemas Operacionais: Linux vs Windows 2
Sobre C e C++ 2
Sobre a linguagem Assembly 3
A melhor rotina possvel 4
Cdigos-fonte deste livro 5
Arquiteturas de processadores Intel 6
No confunda arquitetura com modo de operao 7
Avisos finais sobre o livro 7
Captulo 1: Introduo ao processador 9
Modos de operao 9
O que significa proteo? 10
O que significa paginao? 10
Proteo e segmentos, no modo protegido em 32 bits 10
Onde fica a tabela de descritores? 13
Seletores e descritores de segmentos no modo x86-64 13
Descritores no modo x86-64 14
Cache de descritores 15
Proteo no modo x86-64 15
Captulo 2: Interrompemos nossa programao... 17
O que uma interrupo? 17
As Interrupes especiais: Excees, Faltas e Abortos 17
Existem dois tipos de interrupes de hardware diferentes 18
Interrupes so sempre executadas no kernelspace! 18
Faltas importantes para performance 19
Sinais: Interrupes no user space 19
Um exemplo de uso de sinais, no Linux 21
No use signal(), use sigaction() 21
Existe mais sobre sinais do que diz sua v filosofia... 22
Captulo 3: Resolvendo dvidas frequentes sobre a Linguagem C 23
Headers e Mdulos: Uma questo de organizao 23
Chamadas de funes contidas no mesmo mdulo 24
Tamanho de inteiros e ponteiros 25
No prudente confiar nos tamanhos dos tipos default 25
Existe diferena entre declarar e definir um smbolo 26
Diferena entre declarao e uso 27
Parnteses 27
Escopo dos parmetros de funes 27
Prottipos de funes 28
Problemas com algumas estruturas com mscaras de bits 29
Finalmente: Ponteiros! 31
Declarando e usando ponteiros 32
Problemas com ponteiros para estruturas 34
Ponteiros e strings 34
Diferenas entre declarar ponteiros e arrays contendo strings 35
Mais ponteiros e arrays 36
Declarando e inicializando arrays, estruturas e unions 36
Ponteiros, funes e a pilha 38
Entendendo algumas declaraes malucas usando ponteiros 39
Utilidade de ponteiros para funes 40
A biblioteca padro: libc 41
A libc faz um monte de coisas por baixo dos panos 43
Cuidados ao usar funes da libc 44
Disparando processos filhos 44
Captulo 4: Resolvendo dvidas sobre a linguagem Assembly 47
O processador pode ter bugs 47
A mesma instruo pode gastar mais tempo do que deveria 48
Nem todas as instrues gastam tempo 49
A pilha e a red zone 49
Prefixos 50
Captulo 5: Misturando C e Assembly 53
Convenes de chamada (x86-64) 53
Convenes de chamada (i386) 54
Funes com nmero de parmetros varivel no x86-64 56
Pilha, nos modos i386 e x86-64 56
Um detalhe sobre o uso de registradores de 32 e 64 bits 57
Exemplo de funo em assembly usando a conveno de chamada 57
Aviso sobre retorno de funes com tipos complexos 58
O uso da pilha 60
Variveis locais e a pilha 61
O compilador no usa todas as instrues 62
Detalhes interessantes sobre a instruo NOP e o prefixo REP 63
LOOP e LOOPNZ so diferentes, mas REPNZ e REP so a mesma coisa! 65
Assembly inline 65
Operador de indireo (ponteiros) em assembly 68
Usando a sintaxe Intel no assembly inline do GCC 69
Problemas com o assembler inline do GCC 69
Usando NASM 70
Captulo 6: Ferramentas 73
GNU Linker (ld) 73
Obtendo informaes com objdump 75
GNU Debugger 75
Configurando o GDB 76
Duas maneiras de listar cdigo assembly no gdb 76
Listando registradores 77
Examinando a memria com o GDB 78
Obtendo listagens em assembly usando o GCC 78
Sobre os endereos em listagens em assembly obtidas com objdump ou GCC 81
Usando o make: Fazendo um bolo, usando receitas 82
Captulo 7: Medindo performance 87
Ciclos de mquina e ciclos de clock 87
Contar ciclos de mquina no fcil 87
Como medir? 88
Aumentando a preciso da medida 89
Mas, o gcc possui funes intrnsecas para executar CPUID e RDTSC! 91
Melhorando a medio de performance 92
O clculo do ganho de performance 92
Quando um ganho vale pena? 93
Usando perf para medir performance 94
Captulo 8: Otimizaes automticas 95
Nveis de otimizao 95
Common Subexpression Elimination (CSE) 95
Desenrolamento de loops 96
Movendo cdigo invariante de dentro de loops (Loop Invariant Code Motion) 96
Eliminao de cdigo morto (Dead Code Elimination) 97
Eliminao de armazenamento morto (Dead Store Elimination) 97
Previso de saltos (Branch Prediction) 97
Simplificaes lgicas 99
Simplificao de funes recursivas 100
Auto vetorizao (SSE) 101
Otimizaes atravs de profiling 101
Captulo 9: Caches 103
A estrutura dos caches 103
Determinando o tamanho de uma linha 104
O dilema do alinhamento 105
Dica para usar melhor os caches... 106
Audaciosamente indo onde um byte jamais esteve... 107
Funes inline e a saturao do cache 107
Captulo 10: Memria Virtual 109
Virtualizao de memria 109
Espaos de endereamento 109
Paginao 109
Paginao e swapping 110
Tabelas de paginao no modo x86-64 111
As entradas da Page Table 112
As extenses PAE e PSE 112
Translation Lookaside Buffers 113
A importncia de conhecer o esquema de paginao 113
Tabelas de pginas usadas pelo userspace 114
Alocando memria: malloc e mmap 115
Um exemplo de injeo de cdigo, usando pginas 116
Quanta memria fsica est disponvel? 118
Captulo 11: Threads! 121
Multitarefa preemptiva e cooperativa 121
Mltiplos processadores 122
Como o scheduler chamado? 123
Finalmente, uma explicao de porque SS no zero no modo x86-64! 123
Na prtica, o que uma thread? 123
Criando sua prpria thread usando pthreads 124
Criando threads no Windows 125
Parar uma thread na marra quase sempre no uma boa ideia 127
Nem todas as threads podem ser mortas 127
Threads, ncleos e caches 128
Trabalhar com threads no to simples quanto parece 128
Evitando race conditions 129
Threads e bibliotecas 130
Threads e Bloqueios 131
O que significa isso tudo? 131
Tentando evitar o chaveamento de contextos de tarefas 131
Usando o OpenMP 132
OpenMP no mgico 133
Compilando e usando OpenMP 133
OpenCL e nVidia CUDA 134
Captulo 12: Ponto flutuante 135
Preciso versus Exatido 135
O que ponto flutuante? 135
Estrutura de um float 135
Analogia com notao cientfica 136
Valores especiais na estrutura de um float. 137
Pra que os valores denormalizados existem? 138
Intervalos entre valores 138
Um problema de base 139
O conceito de valor significativo 140
Regras da matemtica elementar nem sempre so vlidas 141
Que tal trabalhar na base decimal, ao invs da binria? 142
A preciso decimal de um tipo float ou double 142
Comparando valores em ponto flutuante 143
No compare tipos de ponto flutuante diferentes 144
Evite overflows! 144
Ponto fixo 145
Modo x86-64 e ponto flutuante 146
O tipo long double 146
Captulo 13: Instrues Estendidas 149
SSE 149
Funes intrinsecas para SSE. 150
Exemplo do produto escalar 151
Uma otimizao que falhou o produto vetorial 152
Uma otimizao bem sucedida: Multiplicao de matrizes 154
E quando ao AVX? 155
Outras extenses teis: BMI e FMA 155
Captulo 14: Dicas e macetes 159
Valores booleanos: Algumas dicas interessantes em C 159
Quanto mais as coisas mudam... 160
INC e DEC so lerdas! 160
No faa isso!!! 160
Aritmtica inteira de multipla preciso 161
Voc j no est cansado disso? 162
Otimizao de preenchimento de arrays 164
Tentando otimizar o RFC 1071 check sum. E falhando... 166
Previso de saltos e um array de valores aleatrios 168
Usar raiz quadrada, via SSE, parece estranho... 170
Gerando nmeros aleatrios 171
Captulo 15: Misturando Java e C 173
Porque no uma boa ideia misturar C com ambientes gerenciados 173
Garbage Collection, no Java 174
Misturando com um exemplo simples... 175
Usando strings 178
Usando arrays unidimensionais 180
Usando objetos 181
Chamando mtodos do prprio objeto 182
Acessando membros de dados do prprio objeto 184
De volta aos arrays: Usando mais de uma dimenso 184
Captulo 16: Usando Python como script engine 187
Contagem de referncias 187
Instanciando o python 187
Carregando um mdulo 188
Executando um mdulo simples 189
Apndice A: System calls 191
Consideraes sobre o uso da instruo SYSCALL 191
Onde obter a lista de syscalls do Linux? 192
Usar syscalls no Windows no uma boa idia! 193
Apndice B: Desenvolvendo para Windows usando GCC 195
Usando o MinGW no Linux 195
Usando o MinGW no Windows 195
As bibliotecas mingwm10.dll e msvcrt.dll 196
Limitaes do MinGW 196
Assembly com o MinGW e NASM 196
Windows usa codificao de caracteres de 16 bits, internamente 196
Vale a pena desenvolver aplicaes inteiras em assembly para Windows? 197
Importando DLLs no MinGW-w64 198
Apndice C: Built-ins do GCC 201

Apndice D: Mdulos do Kernel 205


Anatomia de um mdulo simples 205
Introduo
Em 1994 publiquei uma srie de 26 captulos sobre linguagem assembly numa rede de troca de
mensagens conhecida como RBT. Na poca a Internet ainda era restrita ao crculo acadmico e
inacessvel a ns, meros mortais. Era tambm uma poca em que o processador top de linha no
passava do 286 e, s na segunda metade dos anos 90, tive acesso a um 386DX (40 MHz), onde
comecei a explorar os novos registradores e instrues em 32 bits. O curso de assembly, como
ficou conhecido, para minha surpresa, fez um tremendo sucesso depois que a RBT deixou de existir,
sendo citado at hoje em fruns especializados e copiado entre estudantes universitrios (alguns me
relataram) pas fora. O curso me colocou em contato com muita gente e, assim me dizem, tido
como uma espcie de manual definitivo (embora no seja!). Acontece que tudo fica velho e
obsoleto. Algumas das dicas e truques contidos no curso no valem mais ou no so mais usados
em lugar nenhum, mesmo pelos melhores compiladores. Por exemplo, hoje meio difcil acessar os
recursos de placas de vdeo diretamente e ningum mais usa os device drivers HIMEM.SYS e
EMM386.EXE.
Durante anos pensei em atualizar o curso. S que no queria publicar algo para o completo
novato ou repetir a estrutura do texto original. No quero ensinar C e Assembly pra ningum.
Existem bons manuais, livros e tutoriais disponveis por ai, tanto em forma textual quanto em vdeo,
no Youtube, basta fazer uma pesquisa no Google! Ao invs disso, este livro uma coletnea de
artigos que escrevi em alguns blogs e sites especializados desde a poca do curso e com
algumas novidades. uma coleo de dicas e macetes, bem como a tentativa de mastigar um
pouco alguns conceitos que podem ser obtidos facilmente no Wikipedia, s que numa linguagem
tecnicamente mais escabrosa para o estudante. o caso de captulos que falam de cache, memria
virtual e threads, por exemplo.
Mesmo que a minha inteno de facilitar o entendimento sobre tpicos to ridos seja bem
sucedida, devo alert-lo para alguns detalhes: Primeiro, este material no bsico. Algum
entendimento prvio sobre as linguagens C e Assembly necessrio. Segundo, a inteno no
esmiuar as entranhas de um sistema operacional ou de seu ambiente de janelas preferido. Voc
ver muita informao resumida sobre interrupes, gerenciamento de memria, threads, mas o
assunto abordado aqui est longe de ser completo. Um bom comeo para mergulhar nesses assuntos
so os manuais de desenvolvimento de software da Intel1 e da AMD2. Mas, essas no so as nicas
referncias bibliogrficas importantes. Voc poder querer ler um bocado sobre chipsets (legados,
por exemplo, como o PIC 8259A3 e o PIT 82534, ou os mais modernos, com o I/O APIC 82093A5),
bem como listagens dos mapeamentos de portas de I/O6. Esse material essencial para o
entendimento da arquitetura dos PCs, especialmente no que concerne o mapeamento da memria.
J para a linguagem C existem, alm de tutoriais online, bons livros traduzidos para o portugus,
bem como as especificaes ISO da linguagem (que recomendo fortemente que voc se
familiarize!).

1 IA-32 & Intel 64 Software Development Manuals: http://goo.gl/UbssTF.


2 AMD64 Architecture Programmer's Manuals: http://goo.gl/oIBnVD.
3 Programmable Interrupt Controller 8259A Datasheet: https://goo.gl/jCu2sy.
4 Programmable Interval Timer 8253 Datasheet: http://goo.gl/9zGsvF.
5 Advanced I/O Programmable Interrupt Controller 82093A Datasheet: http://goo.gl/44ntD7.
6 Eis uma listagem, em formato texto: http://goo.gl/cF82AV.

1
Organizao do livro
Os captulos 1 e 2 do o embasamento para entendimento sobre recursos do processador. Nos
captulos 3 e 4 mostro algumas dicas sobre C e Assembly, respectivamente, para tentar resolver,
espero que definitivamente, dvidas que frequentemente surgem aos meus leitores e amigos. Sigo
falando sobre como misturar cdigos em C e Assembly. Falo tambm alguma coisa sobre a
mistura com Java, no captulo 15, e Python, no captulo 16. Mesmo isto no sendo o foco desse
material.
O captulo 6 fala alguma coisa sobre as ferramentas do ofcio para o desenvolvedor. No um
material extensivo, s uma introduo.
Os captulo 7 e 8 comeam a mostrar alguma coisa sobre performance. O captulo 7 especialmente
interessante: Ele mostra uma maneira de medir a performance de suas rotinas. E voc encontrar, no
apndice B, duas funes escritas em assembly, que possibilitam uma medio mais precisa.
A partir do nono captulo at o dcimo terceiro so mostrados recursos importantes para
performance, existentes no seu processador. O entendimento sobre caches, memria virtual e
threads particularmente importante se voc quer arrancar at o ltimo ciclo de mquina de
performance em suas aplicaes.
No captulo 12 esqueo um pouco sobre performance e exploro alguns conceitos sobre operaes
em ponto flutuante, no sentido de mostrar que nem tudo o que parece. O 13 captulo estende o
12, mostrando as extenses SSE, AVX e FMA.
O antepenltimo captulo dedicado a dicas em geral.
E, no final, os apndices do informaes adicionais que podem ser teis para consulta e uso.

Sistemas Operacionais: Linux vs Windows


Na maioria do tempo falarei sobre Linux aqui. Isso no significa que dicas e macetes no possam
ser aplciadas a outros sistemas, como Windows e OS/X. Acontece que o nico sistema operacional
onde podemos colocar as mos em suas entranhas e realizar uma avaliao quase que cirrgica o
Linux7.
A no ser que voc tenha o cdigo fonte do Windows mais recente ou do OS/X (que baseado no
FreeBSD, por falar nisso), certos recursos no podem ser conhecidos diretamente. Quaisquer
explicaes envolvendo recursos internos do Windows, por exemplo, seria meramente especulativa.
A documentao existente sobre a API boa (disponvel na MSDN Library8, por exemplo), mas
superficial do ponto de vista das entranhas do sistema. Acredito que usar a imaginao com base em
informaes vindas de especulao algo que pode ser feito na interpretao de contos de fadas,
no num material tcnico...
Na medida do possvel mostrarei dicas que envolvem tambm o Windows. Embora o OS/X tambm
seja proprietrio, mais fcil encontrar paralelos com o sistema que lhe deu origem (FreeBSD).
Assim, dentre os sistemas operacionais mais famosos este livro restrito a apenas dois: Linux e
Windows, com nfase no primeiro.

Sobre C e C++
De acordo com o ttulo desde livro, este material tambm restrito a duas linguagens de
programao: C e Assembly. As poucas referncias a C++, Java e C# estaro l apenas para ilustrar

7 Ok, voc tambm pode fazer isso com as variaes free do BSD!
8 Acessvel via https://msdn.microsoft.com/library.

2
algum ponto. As nicas excees so os captulos sobre a mistura de Java e Python com C.
meio difcil falar sobre misturar cdigo C com Java sem falar em Java, n?
De maneira geral, no lido aqui com C++ e linguagens mais moderninhas, ou orientadas objetos
por questes de convico. Em essncia, elas so a mesma coisa que C. Tm algumas coisinhas a
mais (classes, sobrecarga de operadores, funes virtuais, etc) que podem ser implementadas
cuidadosamente por um programador com boa experincia em C. Outro motivo para no falar sobre
C++ que ele mais complicado de entender em baixo nvel. O leitor que quiser usar assembly
junto ao seu cdigo C++ deve prestar ateno e tomar cuidado com muitos detalhes. No so
poucos! Para citar algumas:
Name Mangling Os nomes de funes, em C++, costumam ser codificados de uma
maneira diferente do que feito em C. Em C uma funo do tipo: int f(int); tem o nome de
f, numa listagem do cdigo equivalente em assembly. Em C++, o nome uma coisa
maluca como _Z1fi ou algo que o valha (no vou me ater conveno de nomenclatura de
funes em C++ aqui... isso muda de compilador para compilador). O verbo To mangle,
em ingls, pode ser literalmente traduzido para mutilar. O que diz muito sobre C++;
Funes membro de classes As funes membro, no estticas, de uma classe recebem
um ponteiro adicional, escondido, chamado 'this'. Este ponteiro aponta para a instncia do
objeto da classe e passado, escondido, para todas as funes (de novo: no estticas).
Funes membro virtuais Toda chamada a uma funo membro virtual feita com dupla
indireo. Isto , uma referncia a um objeto um ponteiro que aponta para uma tabela
contendo ponteiros para as funes. Explicarei isso, brevemente, no captulo sobre
misturas;
Existem mais algumas diferenas que devem ser estudadas por quem quer usar assembly com C++
e pretendo dedicar um captulo s para te mostrar porque acredito que orientao objetos no
uma boa ideia... Corro o risco de criar uma guerra entre os entusiastas, j que afirmo que um
programa escrito em C costuma ser mais performtico e mais simples do que um escrito em C++
devido a menor complexidade do cdigo gerado pelo compilador, no segundo caso. Esses so, em
essncia, os meus motivos pela preferncia ao C, em vez de C++.
Essas consideraes no querem dizer que C++ seja uma perfeita droga. Ao contrrio: O
compilador C++ faz um excelente trabalho de otimizao, assim como o compilador C. Mas,
existem complexidades e abstraes, que facilitam a vida do programador e, ao mesmo tempo,
causam a criao de cdigo menos que timo do ponto de vista de cdigo equivalente em C e
Assembly otimizados. Isso quer dizer que o desenvolvedor C++ deve ter preocupao redobrada se
quiser cdigos de excelente performance... O desenvolvedor C tambm tem que faz-lo, mas a
preocupao menor...

Sobre a linguagem Assembly


Quanto a linguagem assembly, existem muitos sabores que o leitor pode escolher. Cada tipo de
processador tem o seu. Este texto lida apenas com os modos i386 e x86-64 dos processadores da
famlia Intel. No lido aqui com as plataformas IA-64 (tambnm da Intel), ou ARM e ARM-64, por
exemplo (o ltimo est em voga, hoje em dia, graas aos dispositivos mveis e devices como o
Raspberry PI e smartphones). Isso importante porque nessas outras arquiteturas temos, alm da
linguagem assembly diferente, regras de otimizaes e organizao de hardware tambm
completamente diferentes.
Assembly, aqui, tambm no usada como linguagem principal de desenvolvimento e recomendo
fortemente que voc no tente us-la como tal. Hoje em dia acho uma verdadeira loucura

3
desenvolver aplicaes inteiras nessa linguagem. At os sistemas operacionais so complexos o
suficiente para no permitirem uma abordagem produtiva nesse sentido e, alm do mais, os
compiladores de nvel maior, como o caso de C, fazem um trabalho muito bom do ponto de vista
da otimizao de cdigo.
bom notar que os nicos compiladores com grande flexibilidade de otimizaes so,
necessariamente, C e C++. Isso particularmente vlido com o uso de compiladores do projeto
GNU: gcc e g++9. Compiladores de outras linguagens como C#, Java, Pascal... possuem algumas
poucas opes de otimizaes (geralmente apenas uma opo do tipo -optimize), mas no a
mesma flexibilidade. Outros compiladores C/C++ j foram bons, um dia, como o caso da variao
contida no Visual Studio. Hoje, a maioria das opes de otimizao foram extirpadas para tornarem
o compilador mais amigvel plataforma .NET e sua Intermediate Language. Dessa forma,
mesmo para Windows, continuo recomendando o GCC... Uma outra possibilidade o clang, projeto
open source criado pela Apple. O clang , essencialmente, a mesma coisa que o GCC com algumas
features extras que, do ponto de vista do desenvolvedor em linguagem C, so incuas, na maioria
das vezes.
Claro que o trabalho de otimizao pode ser feito pelo desenvolvedor esperto puramente em
assembly, mas o mesmo desenvolvedor pode dar um tiro no prprio p com sua esperteza e criar
cdigo cuja performance ser pior do que a gerada pelo compilador C. J tive experincias
dolorosas nesse sentido. O lema No existe melhor otimizador do que aquele que est entre suas
orelhas10 continua vlido e, em alguns casos, assembly a escolha definitiva para atingir o objetivo
de ter um cdigo rpido, s que nem sempre isso funciona bem! Na maioria das vezes recomendo
confiana (adotando precaues) no trabalho do compilador aliado ao projeto cuidadoso de
algoritmos, inclusive com um bom apoio da literatura extensa sobre o assunto. Um bom algoritmo,
muitas vezes, cria bons cdigos finais e dispensa otimizaes mais hardcore. No deixe de ler The
Art of Computer Programming de Donald E. Knuth e Algorithms in C de Robert Sedgewick11.
Mesmo achando uma coisa de maluco desenvolver aplicaes inteiras em Assembly, no posso
deixar de dizer que o conhecimento dessa linguagem e da arquitetura de seu processador (e do PC)
so essenciais para atingir alta performance. Observar o que seu compilador preferido gerou e
melhorar o cdigo de forma que voc tenha certeza de que conseguiu o melhor resultado s
possvel ao analis-lo ao nvel do Assembly. E, s vezes, criar uma ou outra rotina crtica
diretamente em Assembly poupa tempo e torna seu cdigo mais simples, alm de rpido.
Outro aviso sobre esse livro que certos conceitos fundamentais da linguagem so assumidos como
conhecidos. Uma discusso mais profunda sobre Assembly pode ser encontrada num outro livro:
Linguagem Assembly para i386 e x86-64, deste mesmo autor que vos escreve, leitor!

A melhor rotina possvel


Alguns leitores me perguntam, de tempos em tempos, qual o melhor jeito de fazer alguma coisa.
Entendo que por melhor querem dizer o mais veloz, no sentido de que exetem o mais rpido
possvel. Bem No existe tal coisa... O que existe um conjunto de fatores que levaro a sua
rotina a ser mais rpida do que outra equivalente e isso s pode ser determinado de trs formas:
1. Experincia;
2. Experimentao;

9 O compilador do Visual Studio teve grande parte de suas opes de otimizao retiradas da linha de comando.
Nesse sentido, ele no gera o melhor cdigo possvel.
10 Li isso num livro de Michael Abrash: Zen of Code Optimization.
11 Sedgewick tambm tem publicado um excelente livro chamado somente de Algorithms, onde todos os exemplos so
em Java. Deixando Java de lado, vale muito a pena l-lo.

4
3. Medio.
Por experincia quero dizer a familiaridade do desenvolvedor com a linguagem e com o
ambiente. Somente essa convivncia poder te dizer qual jeito que criar rotinas mais rpidas ou
mais lentas... A experimentao essencial, at mesmo para o sucesso do primeiro item. J
medio sempre um passo necessrio. Usar uma tcnica que sempre deu certo no passado no
garantia de que ela dar certo hoje. necessrio medir a performance de vrios casos para
determinar qual o mais rpido. Mais adiante neste livro apresentarei uma rotina simples para
medir a quantidade de ciclos de clock gastos por uma rotina. essencial que essas medies
sejam feitas em cdigos crticos, mesmo que voc ache que sua rotina seja a mais rpida
possvel...

Cdigos-fonte deste livro


Todo cdigo-fonte completo que voc ler neste texto comea com uma linha de comentrio dizendo
o nome do arquivo que usei. Com isso voc pode copi-lo e compil-lo. Essa conveno
particularmente importante quando temos cdigos que possuem diversos mdulos ou arquivos.
Se preferir, alguns dos cdigos mais importantes esto disponveis no GitHub, no endereo
http://github.com/fredericopissarra/book-srcs.
No decorrer do livro chamo de mdulos cada um dos arquivos com extenso .c ou .asm, ou
seus arquivos objetos equivalentes. Um programa completo pode ser composto de diversos mdulos
que so compilados separadamente e depois linkados para formar o arquivo executvel. Tambm
conveniente que voc aprenda alguma coisa a respeito do utilitrio make, isto , como construir
um makefile. Um makefile uma receita que o utilitrio make usa para compilar e linkar todos os
mdulos do seu programa e construir o produto final... Mais adiante dou uma palinha sobre o
make, s pra dar um gostinho da coisa.
Avisos: Sobre makefiles: Os comandos de uma receita tm que ser precedidos de um
caractere '\t' (tab). Nas listagens, neste livro, esse caractere no est l. Ao copiar e colar
voc obter erros ao chamar o 'make'.
Sobre as listagens em C e Assembly: O editor de textos usado para confeccionar esse livro
substitui as aspas por dois caracteres especiais... A aspa de abertura diferente da aspa de
fechamento. Observe: . Ao copiar o cdigo e tentar compil-lo, obter erros, com toda
certeza... Tentei acertar esse problema nos cdigos e acredito que consegui. Mas, esteja
avisado desse possvel problema!
No uso IDEs para desenvolver minhas aplicaes, apenas o bom e velho vim, os compiladores e
muito material de referncia (manuais, livros, manpages etc). Este um dos motivos porque voc
no ver screenshots de janelas, receitas de configurao de meu ambiente de desenvolvimento
favorito, etc. Este livro sobre desenvolvimento em C usando Assembly, para o modo x86-64 do
processador Intel, com uma pitada de informaes sobre hardware e sobre algumas entranhas do
processador ou do PC. Ele no um livro sobre IDEs.
J que no uso IDEs e elas me do alergia (assim como o Windows!), nas listagens com vrios
mdulos, por motivos de brevidade, indicarei o incio do novo arquivo atravs de uma linha que
lembra um picote, em modo texto:
-----%<----- corte aqui -----%<-----

Assim, voc saber onde termina um arquivo e comea outro. O exemplo abaixo mostra um mdulo
em C chamado hello.c, um makefile (chamado de Makefile) e a linha de comando usada para
compilar o projeto:

5
/* hello.c */
#include <stdio.h>

int main(int argc, char *argv[])


{
printf("Hello, world!\n");
return 0;
}
-----%<----- corte aqui -----%<-----
# Makefile

hello: hello.o
$(CC) -o $@ $^

hello.o: hello.c
$(CC) -O3 -c -o $@ $<
-----%<----- corte aqui -----%<-----
$ make
cc -O3 -c -o hello.o hello.c
cc -o hello hello.o
$ ./hello
Hello, world!

No caso de linhas de comando, os comandos digitveis aparecero em negrito. E, j que meu foco
Linux, o prompt sempre um '$' ou '#' (se o nvel de acesso exige o usurio 'root'). Voc poder
ver um prompt C:\> para linhas de comando especficas para Windows, mas ser raro.
Outro recurso, no que se refere aos cdigos fonte, o uso de reticncias para dizer que no estou
mostrando um cdigo completo, mas um fragmento. Isso indica que mais alguma coisa deve ser
feita na listagem para que ela seja compilvel, mas isso no interessante no momento, j que
quero mostrar apenas a parte significativa do cdigo. Um exemplo o caso onde posso mostrar um
loop usando uma varivel que deve ser inicializada em algum outro lugar, s que para efeitos de
explicao, isso suprfluo:
/* Essas reticncias dizem que mais alguma coisa necessria aqui! */

/* Estamos, no momento, interessados apenas neste loop!


bviamente os ponteiros 'dp' e 'sp' foram inicializados em algum outro lugar! */
while (*dp++ = *sp++);

/* E aqui tem mais cdigo... */


Tento, tambm, manter os cdigos fonte contidos numa nica pgina ou separado entre pginas de
uma maneira menos fragmentada possvel, para facilitar a leitura.
Voc tambm reparou que comentrios tem cor deferente, certo?

Arquiteturas de processadores Intel


O termo arquitetura usado de forma bastante ampla neste livro. Existem dois conceitos
fundamentais: Quando os termos i38612 (ou IA-32) e x86-64 (ou Intel 64) so usados, refiro-me
aos processadores derivados da famlia 80x86 que suportam e operam nos modos de 32 e 64 bits,
respectivamente. O outro conceito est atrelado tecnologia usada no processador. Existem vrias e
elas tm nomes engraados: Nehalem, NetBurst, Sandy Bridge, Ivy Bridge, Haswell etc, para citar
apenas os da Intel.

12 Usarei i386 ao invs de IA-32 aqui, mesmo que i386 seja especfico para uma arquitetura de processadores
(80386SX e 80386DX).

6
Figura 1: Arquiteturas dos processadores Intel
Saber sobre essas arquiteturas til, por exemplo, porque foi apenas na NetBurst que a Intel
introduziu o modo de 64 bits.
Em essncia, este livro aplica-se a todas as tecnologias, desde o i386 at a mais recente (Broadwell,
na poca em que escrevo isso), mas meu foco est mais nas arquiteturas vindas depois da NetBurst,
especialmente na Haswell. Meus ambientes de teste atuais so baseados nessa ltima arquitetura.
No estranho que um livro que se prope a falar sobre arquitetura de 64 bits recorra a tecnologias
de 32? Acontece que o modo x86-64 , na realidade, o mesmo modo i386 com extenses de 64 bits
e com alguns recursos extirpados. Quase tudo o que vale para 32 bits, vale para 64. O contrrio
que no se aplica...
Assim, quando falo da arquitetura i386, estou falando de todas os processadores Intel depois do
i386, inclusive. Quando falo de x86-64, todos os processadores depois da arquitetura NetBurst,
inclusive, so o objeto de estudo.

No confunda arquitetura com modo de operao


Neste livro, o termo i386 aplica-se tanto a arquitetura quanto a um modo de operao dos
processadores da famlia x86.
Quando ler modo i386, isso significa que o processador estar trabalhando em modo protegido
de 32 bits. O outro modo de operao citado na documentao da Intel IA-32e. Este o modo
de operao de 64 bits ou, de maneira mais precisa, o modo de 32 bits com extenses (da o 'e') para
64 bits. Neste livro chamo esse modo de x86-64, aproveitando a nomenclatura da AMD. Acho
x86-64 mais sexy do que IA-32e. O modo x86-64 tambm chamado de amd64, este ltimo
usado na nomeao de pacotes do Linux.
No texto voc poder ver uma mistura de nomenclaturas nesse sentido... IA-32e, amd64 e x86-64
so usados de forma intercambivel para significar a mesma coisa: o modo de 64 bits. Pode ser que
voc encontre IA-32 e i386 tambm, que querem dizer a mesma coisa.

Avisos finais sobre o livro


Todo esse livro fala somente sobre a unidade central de processamento (CPU) ou processador,
para os ntimos. Mostrarei muito pouco (se mostrar!), ou apenas um resumo, sobre GPUs
(Graphical Processing Unit), chipsets e outros dispositivos contidos no seu computador.
Voc tambm vai reparar que uso o termo plataforma Intel no texto, mesmo sabendo que a AMD
um grande competidor, a Intel firmou-se como padro de facto com relao a esse tipo de
processador... Isso no significa que a AMD fique atrs. Alis, recomendo que voc estude tambm

7
os manuais de desenvolvimento de software da AMD (que so mais mastigveis).
S mais uma coisa que voc vai reparar Uso a notao KiB, MiB, GiB, TiB e PiB para quilo,
mega giga, tera e peta bytes ao invs das tradicionais kB, MB etc. O motivo simples:
Os prefixos k, M, G, T e P so potncias de 10, enquanto os padres ISO/IEC 80000-13
e IEEE 1541-2002 padronizam os prefixos kibi, mebi, gibi, tebi e pebi como sendo
multiplicadores na base 2, respectivamente 2, 2, 2, 2 e 2.
Ainda, o B, maisculo indica byte e o minsculo, b, indicar bit, como em 15 Mib/s (15
mebibits por segundo).
Ao invs de kibi continuarei chamando de quilo s para no causar maior estranheza ou
gozao ( de comer?).

8
Captulo 1: Introduo ao
processador
Quem pretende desenvolver software para a plataforma Intel no deveria estar interessado em um
modelo de processador especfico (i7, i5, i3, Core2 Duo, Pentium, 486, 386 etc), mas nos pontos
comuns entre eles. No entanto, quanto mais voc mergulhar em detalhes, ver que no existe tal
coisa de arquitetura comum. A cada par de anos a Intel apresenta uma nova arquitetura nomeada a
partir de algum lugar ou cidade norte-americana (Sandy Bridge, Ivy Bridge, Haswell etc) que possui
uma srie de novos recursos. A arquitetura Haswell, por exemplo, possui quase o dobro de poder de
processamento que sua irm mais nova, a arquitetura Sandy Bridge, sem contar com algumas
novidades.
Sem levar em conta a arquitetura, o que pretendo mostrar neste captulo so informaes que
possam ser usadas para atingir a programao de alta performance na plataforma x86-64, se o leitor
estiver preocupado com isso... A nfase, como ser em todo o livro, o modo protegido de 64
bits.

Modos de operao
Processadores da famlia Intel 80x86, a partir do 80286, podem trabalhar em diversos modos. S
estamos interessados nos processadores que suportem o modo x86-64. Este o modo protegido,
paginado, de 32 bits com extenses para 64 bits. importante perceber que no existe um modo
de 64 bits, mas uma extenso ao modo de 32. Note, tambm, os termos protegido e paginado.
Uma breve explicao sobre proteo e paginao dada nos tpicos seguintes.
Existem outros modos de operao do processador que so interessantes e usados, em casos muito
especficos, mas que no explorarei aqui. Por exemplo, o processador inicia sua vida (depois do
reset ou power up) no modo real. Esse um modo de 16 bits, onde o processador s enxerga 1 MiB
de memria e um monte de recursos mais complicados, do modo protegido, so simplificados. o
modo usado pelo antigo MS-DOS e pelo Windows 3.1... A vida era simples e boa naqueles tempos.
Hoje ela ficou complicada e cheia de necessidades. Por isso, o modo real usado apenas pela BIOS
e por uma parcela nfima do bootstrap do seu sistema operacional.
Sim! A BIOS opera em modo real de 16 bits! E por isso que sistemas operacionais como Linux e
Windows no a usam nunca! Do mesmo jeito, um programinha escrito para MS-DOS no
funcionar nesses sistemas diretamente. Para execut-los, se voc ainda tiver alguma pea de museu
dessas, ter que usar um emulador como o DOSBox, uma mquina virtual com o MS-DOS instaldo
ou um modo de operao especial do processador chamado Virtual8086.
Outro modo menos conhecido o SMM (System Management Mode). Para o desenvolvedor de
aplicaes esse modo irrelevante, j que dedicado para sistemas operacionais e, mesmo assim,
raramente usado, de acordo com minhas observaes. Voc poder achar mais informaes sobre
SMM nos manuais da Intel.
Os nicos modos que nos interessam sero chamados, neste livro, de i386 e x86-64. O primeiro o
tradicional modo protegido de 32 bits e o segundo o modo estendido de 64. Esse estendido
importante porque existem dois modos x86-64: O compatible e o long. O primeiro (compatible) ,
essencialmente, o modo i386 com suporte aos registradores de 64 bits. Todo o resto funciona do
mesmo jeito que no modo i386. J o segundo modo (long) funciona como um modo completamente
novo, onde algumas caractersticas malucas do modo i386 no existem. A maioria dos sistemas
operacionais ditos de 64 bits, baseados em processadores Intel, da famlia 80x86, usam o modo

9
long e, portanto, no falarei do modo compatible aqui.
O modo long o que chamo aqui de modo x86-64.

O que significa proteo?


Num ambiente onde vrios processos podem estar sendo executados de forma concorrente
importante que um no possa interferir no outro. Por interferncia podemos entender que um
processo no pode ter acesso aos dados e cdigo de outro. Pelo menos no diretamente.
Proteo, neste sentido, a infraestrutura oferecida pelo processador para isolarmos processos.
Atravs da proteo podemos, por exemplo, ter cdigos e dados do kernel completamente isolados
de cdigos e dados de um programa do usurio em execuo e, ainda, um programa do usurio no
tem como acessar recursos de outro programa. Um no sabe que o outro existe. Assim, cada
processo tem o seu prprio espao de memria, protegido.
Veremos que apenas o kernel, ou o sistema operacional, tm como acessar todo e qualquer recurso
de processos, mesmo que no sejam os seus prprios. Mas o contrrio no vale.

O que significa paginao?


Ao invs do processador trabalhar apenas com a memria fsica, contida nos seus pentes de
memria, enxergando-a como um grande array, a paginao permite dividi-la em blocos pequenos
chamados de pginas. Mais do que isso: Paginao nos permite mapear mais memria do que a
que existe, fisicamente, fazendo um malabarismo que descreverei l no captulo sobre memria
virtual.
Atravs do sistema de paginao temos dois espaos de memria 13: O espao fsico e o espao
linear (ou Virtual). O espao fsico aquele onde um endereo um ndice que especifica
diretamente na memria. J um espao linear est relacionado a uma outra forma de
endereamento: Neste espao, um endereo linear um valor que precisa ser traduzido para um
endereo fsico. Um endereo linear especifica uma entrada num conjunto de tabelas que contm
descritores de pginas. Este endereo, linear, tambm contm o deslocamento dentro de uma
pgina. Ou seja, um endereo linear uma maneira de endereamento indireta.
Qual a utilidade de quebrarmos o espao fsico em pginas? que no esquema de traduo de
um endereo linear para um endereo fsico temos que usar mapas ou tabelas que indicam se
uma pgina est presente na memria fsica ou no e onde, no espao fsico ela est. Se a pgina
no existir fisicamente, o sistema operacional tem a opo de trocar (swap) uma pgina existente
por uma no existente, aproveitando o espao fsico e fazendo de conta que temos mais memria do
que est instalada no seu sistema. Essas trocas geralmente so feitas entre o espao de memria
fsica ocupada pela pgina e algum lugar no disco (HD). No caso do Linux comum que uma
partio de swap seja usada para esse fim. No caso do Windows, um arquivo de swap geralmente
usado.
A paginao um modo opcional no modo i386, mas obrigatrio no modo x86-64.

Proteo e segmentos, no modo protegido em 32 bits


Se voc j estudou assembly para a famlia 80x86, j viu que existem seis registradores seletores
de segmentos (CS, DS, ES, SS, FS e GS). No modo real, de 16 bits, o contedo desses
registradores fornece um endereo base de 20 bits que aponta para um segmento de 64 KiB de
tamanho. Isso feito deslocando o contedo de um desses registradores para a esquerda em 4 bits,
13 Na verdade so 3: O espao Lgico, o Virtual e o Fsico.

10
que a mesma coisa que multiplicar o valor por 16, efetivamente adicionando esses bits extras
(zerados) direita do valor contido nesses registradores. De posse desse endereo base adicionamos
um deslocamento e obtemos um endereo lgico de 20 bits:

No modo protegido a coisa um pouco mais complicada. Esses registradores no contm o


endereo base de um segmento. Eles contm um ndice que seleciona uma entrada em uma tabela
de descritores.

Figura 2: Estrutura de um selector de


segmentos.

O diagrama acima mostra a estrutura de um seletor. Temos 13 bits de ndice, 1 bit que nos diz o
tipo de seletor (que no importa agora) e dois bits que nos dizem o privilgio do seletor (que
explico mais adiante). Com 13 bits, um seletor pode selecionar uma das 8192 entradas da tabela de
descritores.
A tabela de descritores contm entradas que descrevem (da o nome!) segmentos de memria que
podem ser selecionados pelo seletor (de novo, da o nome!). Cada entrada nessa tabela chamada
de descritor e tm mais ou menos a seguinte estrutura simplificada14:

Figura 3: Forma resumida de um descritor.

No modo i386, sem usarmos o recurso de paginao, um bloco (segmento) de memria descrito
num descritor pode ter at 4 GiB (depende do campo tamanho do descritor) e estar localizado em
qualquer lugar da memria fsica (de acordo com o campo endereo base). O campo tipo nos
diz para que finalidade o segmento de memria ser usado (dados, cdigo, pilha e outras estruturas
usadas pelo processador). O campo flags tambm tm essa finalidade.
O campo privilgio contm um valor que indica qual o nvel de acesso que um processo precisa
ter para usar esse descritor. Esse campo privilgio, num descritor, chamado de DPL (Descriptor
Priviledge Level). No confundir com o campo privilgio num seletor: L ele chamado de RPL
(Requestor Priviledge Level Nvel de privilgio do requisitante). O DPL diz ao processador qual o
privilgio necessrio para acessar o segmento e o RPL nos diz qual o privilgio que o seletor est
requisitando.
S que ambos os privilgios precisam ser comparados com o nvel de privilgio corrente (CPL, ou
Current Priviledge Level). CPL o privilgio com o qual o processador est trabalhando no
momento e mantido no seletor de segmento de cdigo (registrador CS). O CPL tambm
mantido, como cpia, no registrador SS (Stack Selector) porque o processo exige o uso da pilha em
instrues como CALL e RET, bem como no tratamento de interrupes Digo cpia porque o
campo de privilgio do seletor SS , de fato, RPL, mas precisa ter o mesmo nvel do CPL.
Eis um exemplo dos CPL, DPL e RPL em uso: Suponha que a seguinte instruo esteja preste a ser
executada:
mov eax,[0x400104]

14 A estrutura no assim. Este um esquema para facilitar a compreenso.

11
Suponhamos que o CPL seja 3 (O seletor de cdigo, CS, aponta para algum lugar do userspace) e
suponha tambm que o RPL do seletor DS seja, tambm, 3. Este RPL (de DS) ser checado contra o
DPL na entrada da tabela de descritores correspondente ao ndice contido no seletor e, contra o
CPL. No caso, se o DPL tambm for 3, ento o processador poder executar a instruo, caso
contrrio ele causar um segmentation fault ou um general protection error.
Neste caso, todos os trs nveis de privilgio (CPL, RPL e DPL) precisam ser 3, j que este menor
privilgio possvel. Um cdigo executado (CPL) no privilgio 3 no pode requisitar, via seletor de
dados (RPL de DS) acesso a um descritor com privilgio maior (se DPL for menor que 3).
A mesma coisa acontece com a referncia ao endereo linear formado pelo endereo base contido
na tabela de descritores, de acordo com o ndice em DS, e o offset 0x400104, fornecido na
instruo: Se o RPL de DS, o DPL do descritor e o CPL no baterem, teremos tambm um
segmentation fault ou GPL. Por baterem eu no quero dizer que precisem ser iguais. Se o CPL for
0, basicamente os privilgios no sero checados, j que o processador, executando instrues neste
nvel, ter acesso total a qualquer coisa.
Como esses privilgios tm apenas 2 bits de tamanho, eles podem variar entre 0 e 3, onde o nvel
de privilgio 0 o mais poderoso e o 3 o menos. A regra que privilgios com valores menores so
mais privilegiados. Um processo rodando com CPL 2 pode acessar recursos descritos com DPL 2
ou 3, acessados via seletores com RPL 2 ou 3 (mas um seletor com RPL 3 no pode usar um
descritor com DPL 2!). A ordem do poder do privilgio pode, inicialmente, parecer estranha, mas
pense numa corrida: Quem chega em 1 lugar o vencedor. Os que chegam em 2 ou 3, por
definio, so perdedores. Como a contagem comea de zero, privilgios desse valor so sempre
mais poderosos.
O nvel de privilgio 0, por ser mais poderoso, permite a execuo de todas as instrues do
processador. J os privilgios menores tem limitaes significativas. A execuo de algumas
instrues privilegiadas tambm causar segmentation fault. Se tentarmos executar uma instruo
LGDT, por exemplo, e estivermos no userspace, teremos problemas Geralmente, o kernel de um
sistema operacional e seus mdulos, rodam no nvel de privilgio zero (system ou kernel mode).
Os programas criados por voc rodaro no nvel 3 (user mode). A figura abaixo mostra uma
possibilidade de distribuio de privilgios. Na prtica apenas os nveis 0 e 3 so usados por causa
do sistema de paginao (memria virtual), que discutirei mais tarde.

Figura 4: "Anis" de privilgio.

Outra nomenclatura que voc pode achar para cdigos e dados que estejam presentes na regio de
memria descrita com nvel de privilgio zero kenelland (a terra do kernel) ou kernelspace. Se
estiverem no privilgio 3 pertencero ao userland (a terra do usurio) ou userspace.

12
Onde fica a tabela de descritores?
O processador no possui uma regio de memria interna que mantm a tabela de descritores. Essa
tabela tem que ser colocada na memria fsica. Cada descritor, no modo i386, ocupa exatamente 8
bytes e, como temos 8192 descritores (lembre-se dos 13 bits do ndice num seletor), ento uma
tabela ocupa exatamente 64 KiB na memria fsica.
Quando o modo protegido inicializado, carregamos o endereo inicial da tabela de descritores
globais num registrador especial chamado GDTR (Global Descritor Table Register)15. O
processador usa esse endereo como base para localizar as entradas na tabela de descritores, de
acordo com o ndice contido num seletor. A primeira entrada da tabela sempre zerada e
considerada invlida. Esse um descritor NULO16. Um ndice zero num seletor apontar para esse
descritor e, qualquer acesso memria usando esse seletor causar um segmentation fault ou GPF
(General Protection Fault). As demais entradas da tabela descrevem regies da memria linear para
uso especfico... A configurao depende do kernel. O sistema operacional pode, por exemplo, usar
a entrada de ndice 1 para descrever um segmento de cdigo com DPL igual a zero, o ndice 2 para
um segmento de dados e o ndice 3 para um segmento de pilha, ambos com o mesmo privilgio, no
kernelspace. Todos esses 3 descritores, bviamente, estaro disponveis apenas para o kernel (que
usa CPL e RPL zerados nos seletores). Outras entradas na tabela podero descrever segmentos com
DPL=3 que podero ser executados, lidos e gravados atravs de seletores com RPL ou CPL=3 (ou
CPL com maior privilgio que 3).
Outras tabelas, como as tabelas de pginas, descritas no captulo sobre memria virtual seguem o
mesmo princpio: Elas so contidas na memria fsica e apontadas por algum registrador de controle
especial.
A descrio de regies da memria no est restrita a apenas cdigos e dados. Existem descritores
especiais, chamados descritores de sistema. Entre eles, call gates, task gates, task segment status
etc.

Seletores e descritores de segmentos no modo x86-64


O modo x86-64 um modo meio esquisito em alguns aspectos. At ento, tanto no modo real
quanto no modo i386, os seletores fazem exatamente o que eles foram feitos para fazer: Selecionam
um segmento de memria... No modo x86-64 eles quase no servem para nada!
Os registradores CS, DS, ES, FS, GS e SS continuam existindo, mas o nico que realmente tem
alguma utilidade o CS porque guarda, em seu interior, o CPL e um bit dizendo em que modalidade
do modo x86-64 o cdigo ser executado Existem dois: O long mode e o compability mode. No
modo compatvel o processo comporta-se do mesmo jeito que o modo i386, exceto pelo fato de que
temos acesso aos registradores extendidos de 64 bits.
Os demais seletores esto desabilitados no modo x86-64 puro. Se eles estiverem todos zerados (e,
portanto, invlidos) ou com qualquer outro valor sero simplesmente ignorados:
/* readsel.c */
#include <stdio.h>

#define GET_SELECTOR(s) \
__asm__ __volatile__ ( "movw %%" #s ",%%ax" : "=a" ((s)) )

unsigned short cs, ds, es, fs, gs, ss;

15 Existe uma outra tabela de descritores chamada Local Descriptor Table. Por motivos de simplicidade vou ignorar a
existncia dessa tabela na explicao que segue...
16 Essa uma exigncia da arquitetura Intel para o modo protegido e no tem NADA haver com ponteiros NULL.

13
void main(void)
{
GET_SELECTOR(cs);
GET_SELECTOR(ds);
GET_SELECTOR(es);
GET_SELECTOR(fs);
GET_SELECTOR(gs);
GET_SELECTOR(ss);

printf("CS=0x%04X, DS=0x%04X, ES=0x%04X, FS=0x%04X, GS=0x%04X, SS=0x%04X\n",


cs, ds, es, fs, gs, ss);
}
-----%<----- corte aqui -----%<-----
$ gcc -o readsel readsel.c
$ ./readsel
CS=0x0033, DS=0x0000, ES=0x0000, FS=0x0000, GS=0x0000, SS=0x002B

No exemplo acima, note que DS, ES, FS e GS esto zerados. Mesmo assim o programa foi capaz de
ler/escrever na memria, atravs do seletor DS! O registrador CS contm um valor e, de acordo com
o diagrama da estrutura de um seletor que mostrei antes, ele aponta para o ndice 6 da tabela de
descritores globais (TI=0) e requisita o privilgio no userspace (RPL=3). O seletor SS parece
desafiar o que foi dito antes. Ele contm um valor vlido (ndice 12 e RPL=3), mas isso est ai
apenas para controle interno do sistema operacional e no tem o mnimo significado para os nossos
programas. Quer dizer: no usado pelo processador.
Se voc executar o programinha acima no modo i386 obter valores vlidos para todos os seletores
e alter-los poder levar ao erro de segmentation fault. No modo x86-64 puro, podemos alterar, sem
medo, DS e ES. Os registradores FS e GS so especiais no sentido de que podem usar um descritor
como offset para um endereo linear, dependendo da configurao do processador. Isso no usado
no userspace e no nos interessa, no momento.
Neste ponto voc pode estar se perguntanto onde diabos foi parar a proteo, ou seja, os privilgios,
no modo x86-64? Se os seletores no tem uso nesse modo, incluindo o RPL, como o processador
sabe quais regies pode ou no acessar se no h comparao com o DPL? Neste modo, o CPL
continua sendo mantido internamente em CS, mas os privilgios das regies de memria so
mantidas nos descritores de pginas! O modo de paginao obrigatrio no modo x86-64 por esse
motivo...

Descritores no modo x86-64


Dos descritores usados por seletores, apenas os descritores de segmentos de cdigo so necessrios
no modo x86-64 e apenas o seletor CS considerado, os demais esto desabilitados.
As excees so os seletores FS e GS que podem ser usados para apontar para endereos base nos
descritores para os quais apontam. O seletor GS particularmente interessante porque, no
kernelspace, podemos usar a instruo SWAPGS que troca o endereo base pelo contedo da
MSR17 IA32_KERNEL_GS_BASE (0xC0000102). Isso permite manter o endereo de estruturas
acessveis apenas pelo sistema operacional.
Ambos seletores, FS e GS, raramente aparecero em seus cdigos no userspace. A possvel exceo
para o uso de FS no Windows: Na verso Win32 o seletor FS usado pelo mecanismo chamado
SEH (Structured Exception Handler), em blocos try...catch, em C++, quando o programa deixa o
sistema operacional tratar algumas excees (diviso por zero, por exemplo). Mas, suspeito que no
modo x86-64 isso no seja feito desse jeito. De qualquer maneira, eu mesmo raramente vi FS e GS
usados no userspace. De fato, raramente vi qualquer um dos seletores usados explicitamente.

17 MSRs so registradores especiais mantidos pelo processador. A sigla vm de Machine Specific Register.

14
Cache de descritores
Sempre que um seletor carregado um descritor copiado para uma parte invisvel e inacessvel do
seletor. Alm dos 16 bits visveis de um seletor, temos a estrutura de um descritor atrelada a ele,
ou seja, seu limite, endereo base e bits de controle, DPL... Assim, sempre que usarmos um seletor
o processador no tem que recarregar o descritor novamente.
Do ponto de vista do desenvolvedor isso suprfluo, j que o processador far esse caching de
qualquer forma e essas informaes adicionais so invisveis.

Proteo no modo x86-64


Desconsiderando a paginao, no modo x86-64 o processador assume que o endereo base sempre
zero e o tamanho do segmento corresponde a todo o espao enderevel possvel. Em teoria
podemos acessar at 16 EiB (exabytes), ou bytes, de memria. Na prtica, as arquiteturas atuais
permitem acesso de at 4 PiB (petabytes), ou bytes18.
Mesmo que os campos de endereo base e o tamanho no sejam usados nos descritores, os
privilgios e outros flags ainda so usados para o seletor CS.
Os seletores FS e GS, alm do uso citado anteriormente, podem ser usados tambm para
armazenarem um valor de 64 bits a ser usado como offset numa instruo. Por exemplo, se
tivermos, no userspace:
lea rdi,[fs:rbx+rax*2]

Neste caso, se FS aponta para um descritor de onde a instruo obter o endereo base e a somar
ao contedo de RBX e ao dobro de RAX, colocando o valor calculado em RDI.
Se tentarmos usar prefixos de seletores, sem que sejam FS ou GS, em referncias memria, eles
so sumariamente ignorados ou, no pior caso, causaro uma exceo de instruo indefinida
(undefined instruction), j que alguns prefixos no esto definidos para o modo x86-64:
mov rax,[cs:rbx] ; Isso no est disponvel no assembly x86-64 e,
; causa uma exceo!

J que seletores no so usados, as instrues LDS, LES, LSS, LFS e LGS no existem no modo
x86-64 claro que ainda podemos carregar os seletores via instruo MOV.

18 Os bits excedentes tm que ser, necessariamente, uma cpia do bit 51, ou seja, um endereo no modo x86-64 tem
sinal!

15
16
Captulo 2: Interrompemos nossa
programao...
Citei excees e faltas no captulo anterior ao falar de segmentation fault, por exemplo. O
mecanismo que o processador usa para o tratamento de erros , em essncia, o mesmo que ele usa
para o tratamento de interrupes. Mas, o que uma interrupo? E qual a diferena entre uma
interrupo e uma exceo ou falta?

O que uma interrupo?


O processador est constantemente executando cdigo. Tanto o cdigo do programa executado no
userspace quanto o cdigo contido no kernel. No existe tal coisa como processamento parado ou
tempo ocioso nos processadores19... Isso uma abstrao usada pelo sistema operacional ou
ambientes grficos. Em resumo: O processador est executando cdigo o tempo todo, sem parar...
Durante a execuo de um programa certos circuitos (por exemplo, o do teclado) podem pedir
CPU que o processamento normal seja interrompido para executar uma rotina de tratamento de
servio (ISR, Interrupt Service Routine). Essa rotina vai lidar com as necessidades do dispositivo
que requisitou a ateno e, ao terminar, fazer com que o fluxo de processamento retorne ao normal.
O teclado faz isso mudando o estado de um sinal eltrico, no processador, do nvel baixo para o
nvel alto (as interrupes, no PC, so requisitadas na subida no pulso). Esse sinal a chamada
IRQ (Interrupt ReQuests).
Para suportar diversas IRQs diferentes, o seu computador tem um chip que interpreta os pedidos de
interrupo dos dispositivos e os entrega CPU juntamente com o nmero da requisio. Existem,
atualmente, 15 IRQs diferentes e elas so numeradas de IRQ0 at IRQ15, onde IRQ2 no usada.
O nmero da requisio tambm est relacionado com a resoluo de prioridade (IRQ1 tem mais
prioridade do que IRQ7, por exemplo. No exemplo do circuito do teclado, ele est conectado
IRQ1.
Quando voc digita uma tecla, o controlador do teclado (KBC KeyBoard Controller) faz um
pedido de interrupo IRQ1 ao controlador programvel de interrupes (PIC). O PIC
programado de forma tal que, quando o processador reconhece o pedido (via pino INT# do
processador), ele recebe um nmero correspondente entrada da tabela de interrupes que deve
ser usada para chamar a rotina de tratamento de servio... Da, o processador pra tudo o que est
fazendo, salta para a ISR correspondente e, no final dessa rotina, indica ao PIC que tratou a
interrupo escrevendo um comando EOI (End Of Interruption) num registrador especial do
controlador. Logo em seguida, executa uma instruo IRET que far o processador retomar a
execuo normal.
Assim como os descritores de segmentos, existe uma tabela que descreve cada uma das 256
entradas possveis de interrupes. Essa tabela chamada de IDT (Interrupt Descritors Table).
No modo protegido as 32 entradas iniciais da IDT so reservadas para uso interno do processador.
Essas entradas correspondem s excees, faltas e abortos. As demais so livres para uso tanto por
IRQs quanto por interrupes por software (via instruo INT).

As Interrupes especiais: Excees, Faltas e Abortos

19 Bem... no bem assim! Mas, continue lendo, ok?

17
Algumas interrupes so causadas no por uma sinalizao feita por um circuito externo, mas pelo
prprio processador. o caso de faltas como General Protection Fault, Page Fault ou Stack
Overflow Fault. Se certas condies esperadas pelo processador forem violadas, ele interrompe o
processamento normal e desvia o fluxo de processamento para tratadores especiais, que lidaro com
essas faltas.
Existem trs tipos especiais de interrupes de falhas: Excees, Faltas e Abortos... A diferena
entre os trs termos que um aborto algo mais drstico que uma falta, que mais drstico que
uma exceo. Um erro de diviso por zero causar uma exceo. Um erro de validao de
privilgios causar uma falta. Mas, existem erros que colocam o processador em um estado
instvel. Esses so os abortos, que geralmente no podem ser tratados (e levam Blue Screen Of
Death ou a um Kenel Panic).
Abortos s interessam ao sistema operacional. Estamos interessados apenas em algumas faltas e
excees.

Existem dois tipos de interrupes de hardware diferentes


Vimos, acima, o que uma IRQ. Essas interrupes podem ser mascaradas (ou desabilitadas)
facilmente. O flag IF no registrador RFLAGS toma conta disso, do lado do processador. Se o flag
IF estiver zerado, o processador no aceitar nenhuma IRQ.
Para mascarar IRQs s usar a instruo CLI, zerando o flag IF. Para habilitar as IRQs, basta
executar a instruo STI, fazendo IF ser setado.
Existe tambm uma interrupo de hardware que no pode ser mascarada. Ela chamada NMI
(Non Maskable Interrupt) e est associada a um conjunto de erros que o seu computador pode
enfrentar e so verificados por algum circuito externo... Geralmente no estamos muito interessados
em NMIs.
Ateno: Quando zeramos o flag IF, o processador no aceitar as IRQs, mas isso no significa que
o PIC no continue recebendo-as. Em certos casos o sistema operacional pode precisar mascarar
interrupes no prprio PIC, alm de zerar o flag IF. A mesma coisa acontece com a NMI... O
processador sempre obrigado a aceitar NMIs, mas podemos desabilitar o sinal eltrico em outro
circuito do PC para inibi-lo (estranhamente, o chip controlador do teclado permite fazer isso!).

Interrupes so sempre executadas no kernelspace!


No possvel criar rotinas de tratamento de interrupes no ring 3. Todas as interrupes so de
respo sabilidade do kernel. Assim, quando o processador recebe uma requisio de interrupo, o
par de registradores SS:RSP , o registrador RFLAGS e tambm o par CS:RIP so empilhados, nesta
ordem, e o controle passado para o tratador de interrupo 20. Quando a interrupo acabar, o
kernel ter que devolver o controle ao seu cdigo (que tem pilha prpria e foi interrompido com os
flags num estado conhecido).
Algumas excees e faltas colocam na pilha, depois de empilhar CS:RIP, um cdigo de erro. o
caso de General Protection Fault e Page Fault. O formato desse cdigo de erro depende da falta
(ou exceo). Uma GPF, por exemplo, recebe um erro diferente que uma Page Fault.

20 SS tambm empilhado por motivos de compatibilidade. Lembre-se ele no usado no modo x86-64.

18
Figura 5: Pilha, antes e depois de uma interrupo ou exceo com
cdigo de erro.

Quando o tratador encontra uma instruo IRET ele recupera os contedos empilhados, na ordem
em que os foram empilhados, saltando para o CS:RIP contido na pilha. Isso mais ou menos como
RET funciona, com alguns registradores a mais.
responsabilidade das rotinas de tratamento de interrupes preservarem o contedo dos
registradores de uso geral (exceto RSP) e recuper-los antes de sair da interrupo.

Faltas importantes para performance


A falta mais importante, do ponto de vista da performance, a Page Fault. bom lembrar que uma
pgina uma regio de 4 KiB na memria mapeada numa tabela que permite o uso de memria
virtual.
Essa falta acontece de acordo com uma das cinco condies abaixo:
A pgina no est presente na memria fsica e, portando, precisa ser mapeada;
O nvel de privilgio corrente (CPL) menos privilegiado do que o descrito para a pgina;
Tentar escrever em uma pgina read-only;
RIP aponta para uma pgina marcada como no executvel;
Os bits reservados, no mapa de pginas, so diferentes de zero.
Conhecer essas regras interessante, mas o mais importante saber que, sempre que h um Page
Fault o processador interrompido e um tratador executado. Isso afetar outras regies do
processador como caches, por exemplo.
Dos motivos para a falta, o primeiro o mais importante. atravs dele que o processo de page
swapping feito. Assim, quanto mais faltas de pgina, provavelmente o sistema operacional estar
mapeando pginas possivelmente fazendo gravaes e leituras em disco tambm. No de
espantar que page faults possam ser o motivo de uma grande perda de performance. Temos que
arrumar um jeito de evit-las...

Sinais: Interrupes no user space


Seu cdigo em C pode implementar rotinas de tratamento de sinais. Esses sinais so interrupes
no fluxo normal de operao do seu processo, isto , seu programa pra o que est fazendo para
atender um sinal e, quando a rotina de tratamento finalizada, ele pode continuar o que estava
fazendo (ou, em alguns casos, abortar o processo).
Sinais so usados em ambientes POSIX. O Windows, por exemplo, no implementa sinais21...
21 Bem.... pelo menos a implementao no to boa. O MSDN indica que a funo signal() implementada para
sinais como SIGINT, SIGABRT, SIGFPE, SIGILL, SIGSEGV e SIGTERM e s. Mesmo assim, SIGINT no
suportado pelo Windows (embora signal() o permita). Outros sinais importantes como SIGKILL, SIGCHLD e

19
Um sinal conhecido por um valor inteiro apelidado por SIGxxx. Abaixo temos uma lista com
alguns dos sinais mais usados. Note que todo sinal, se no for tratado, tem um comportamento
padro:
Sinal Descrio Comportamento padro
SIGINT O usurio usou Ctrl+C no terminal. Trmino do processo.
SIGKILL Recebido quando o processo matado. Trmino do processo.
SIGSEGV Uma referncia invlida memria foi Trmino do processo.
feita.
SIGTERM Pedido de trmino do processo. Trmino do processo.
SIGSTOP Pedido de suspenso (parada) do Suspenso do processo.
processo (Ctrl+Z, no terminal?).
SIGUSR1, SIGUSR2 Sinais definidos pelo usurio. Trmino do processo.
SIGCHLD Processo filho (forked) foi terminado. Ignorado.
SIGALRM Sinal do timer. til para criar timeouts Trmino do processo.
em processos. Esse sinal programado
via syscall alarm().
Tabela 1: Alguns sinais mais conhecidos

Os sinais SIGKILL e SIGSTOP no podem ter tratadores, ou seja, o comportamento padro fixo
para esses sinais. SIGKILL particularmente drstico. Ele mata o processo no importa o que esteja
acontecendo. comum, quando o usurio quer matar um processo de maneira definitiva, que use:
$ kill -9 8132

Neste exemplo, o comando kill envia um sinal SIGKILL (9) para o processo com PID 8132. O
correto seria enviar o sinal SIGTERM e s se o processo no terminar, enviar SIGKILL:
$ kill -SIGTERM 8132
espera um pouco
$ ps -eo pid | grep 8132 && kill -SIGKILL 8132

O motivo de SIGKILL no poder ser tratado porque ele precisa mesmo ter o poder de terminar o
processo. Os demais sinais podem ter tratadores que, se no chamarem a funo exit antes de seu
trmino, faro com que seu programa continue rodando de onde foi interrompido.
Suponha que voc queira desabilitar o funcionamento do Ctrl+C. Basta fazer algo assim:
signal(SIGINT, SIG_IGN);

O smbolo SIG_IGN um tratador especial, pr concebido, que ignora o sinal. Existe tambm o
smbolo SIG_DFL, que indica o uso de um tratador default para o sinal. Nada impede que voc crie
sua prpria rotina de tratamento. Elas tm sempre o seguinte prottipo:
void sighandler(int signal);

Onde, claro, sighandler pode ser outro nome de funo... Um exemplo, com relao ao SIGINT,
se o usurio digitar Ctrl+C durante a execuo de seu processo, poderia ser este:

SIGALRM so invlidos no Windows.

20
static void sigint_handler(int signal)
{
printf(\nUsurio pediu interrupo!\n
EU NO DEIXO!\n);
}


/* Em algum lugar do seu programa, registramos o manipulador para SIGINT: */
signal(SIGINT, sigint_handler);

simples assim... S preciso alert-lo que a funo signal() obsoleta. O mtodo preferido para
registrar manipuladores de sinais usando a funo sigaction(). Esta funo permite um ajuste fino
do registro e manipulao de sinais. Ela permite registro de manipuladores que tm acesso a mais
informaes sobre o sinal e, inclusive, a possibilidade de bloquear outro sinais enquanto o tratador
estiver em execuo.
Em essncia, sinais so interrupes...

Um exemplo de uso de sinais, no Linux


Algumas funes no retornam at que alguma coisa acontea. Essas funes so ditas
bloqueadas pelo sistema operacional. Por exemplo, por default a funo recv(), que l um
conjunto de caracteres vindos de um socket, no retorna at que tenha bytes lidos nos buffers do
driver de rede... Uma maneira de criar uma rotina com suporte o recurso de timeout, usando recv(),
esta:
int alarm = 0;

/* Manipulador do sinal. */
int sigalarm(int signal) { alarm = 1; }


/* Assinala o handler de interrupo para SIGALRM. */
signal(SIGALRM, sigalarm);

/* Pede ao kernel para gerar um SIGALRM para o nosso processo em


'timeout_in_seconds' segundos. */
alarm(timeout_in_seconds);

/* Neste ponto o seu processo pode ser bloqueado. */


len = recv(fd, buffer, sizeof(buffer), 0);

/* Pede ao kernel para ignorar o pedido anterior, se a funo acima


no for bloqueada. */
alarm(0);

/* Se o alarme foi dado, faz algo! */


if (alarm)
{

/* trata erro de timeout aqui */

}

Ao assinalar o manipulador sigalarm() ao sinal SIGALRM, quando este ocorrer, o processo no


ser terminado (comportamento default do SIGALRM), mas o processo bloqueado vai terminar e
retornar... Logo depois de recv() colocamos um alarm(0) para dizer ao kernel que, se o SIGALRM
no foi enviado, no o envie mais!
Se o sinal foi enviado, setamos a varivel alarm para 1, indicando que o tempo limite j passou.

No use signal(), use sigaction()


A funo signal() obsoleta e no deve mais ser usada. Ela ainda existe por motivos de

21
compatibilidade. O correto, hoje em dia, usar a funo sigaction(), que bem mais flexvel. Ela
permite ajustar alguns flags que informam, por exemplo, se a funo bloqueada que foi
interrompida deve ser reiniciada ou no... Ainda, podemos ter tratadores de signais que recebem
mais informaes, tornando-os ainda mais flexveis. Ao invs de um tratador (handler), podemos ter
uma ao (sigaction), onde a funo receber o nmero do sinal, um ponteiro para uma estritura do
tipo siginfo_t, contendo um monte de informaes e um terceiro argumento (geralmente no usado).
A funo signal(), na realidade, usa sigaction() para fazer a sua mgica, mas o tipo de interrupo
(se permite restart ou no da funo bloqueadora, por exemplo), depende do sinal em si, no de
algum ajuste cuidadoso que voc possa fazer... Assim, use sigaction() para um melhor fine tunning
s comportamento do tratador do sinal.

Existe mais sobre sinais do que diz sua v filosofia...


Isso ai em cima apenas um aperitivo sobre sinais. Sinais podem ser bloquados, mascarados.
Podemos tratar sinais em threads, etc... Consulte um bom livro sobre desenvolvimento para Unix
como o material de Richard W. Stevens (Advanced Programming in the UNIX Environment, Third
Edition, por exemplo).

22
Captulo 3: Resolvendo dvidas
frequentes sobre a Linguagem C
Antes de cair de boca no assembly conveniente tentar acabar, de vez, com algumas dvidas que
estudantes de linguagem C tm. As principais, pelo que posso perceber, so sobre parametrizao de
funes e o uso de ponteiros. Aqui vai uma discusso sobre esses itens, que fazem o programador
novato pensar que C complicada e cheia de armadilhas.

Headers e Mdulos: Uma questo de organizao


Alguns de meus leitores tm a dvida recorrente sobre o porque da existncia de arquivos com
extenso .h... Num cdigo fonte em C comum termos diversos arquivos com extenso .c
(chamados de mdulos) e arquivos com extenso .h (chamados de headers ou cabealhos).
Os mdulos sero compilados, quase sempre separadamente, e depois linkados para montarem o
executvel ou biblioteca final. Os arquivos header existem para organizar melhor os cdigos fonte.
Eles no so bibliotecas de funes.
Repito e enfatizo: Arquivos header no so bibliotecas!
Bibliotecas existem em dois sabores: Estticas e dinmicas. Bibliotecas estticas so conjuntos de
funes que sero linkadas ao seu cdigo fonte formando um programa monoltico, ou seja, todas as
funes da biblioteca esttica so incorporados na imagem binria contida no arquivo executvel.
Bibliotecas dinmicas tambm so linkadas, mas o seu programa as carrega do disco, quando
precisa delas.
No caso do Linux, as bibliotecas estticas esto arquivadas num arquivo com extenso .a 22. J as
bibliotecas dinmicas ficam em arquivos com extenso .so chamados de shared object. Voc
pode listar esses arquivos em diretrios como /lib e /usr/lib.
No caso do Windows, bibliotecas estticas tm extenso .lib e as dinmicas so as velhas
conhecidas DLLs. comum que DLLs estejam armazenadas em arquivos com extenso .dll, mas
alguns arquivos com extenso .exe tambm so DLLs disfaradas (como o kernel, a GDI e a API
para o usurio). Windows ainda disfara DLLs em outras extenses (.ocx, por exemplo).
Se um arquivo header no uma biblioteca, o que ele ? Esses arquivos geralmente contm
declaraes, prottipos de funes, macros, constantes definidas para o preprocessador, tipos,
externs etc. Mas, no contm as definies de funes. Isso fica nas bibliotecas, externas ao seu
programa, ou nos mdulos, em arquivos .c. E a estrutura dos headers costuma ser a seguinte:
/* header.h */
#ifndef __HEADER_INCLUDED__
#define __HEADER_INCLUDED__


/* declaraes, definies, macros, prottipos, etc */

#endif

O motivo da definio do smbolo __HEADER_INCLUDED__, no exemplo acima, deve-se a


possibilidade de que o programador inclua o header diversas vezes ou, pior, o inclua de maneira
circular. Por exemplo:

22 A extenso .a vem de archive. O termo arquivado no foi escolhido levianamente na sentena.

23
/* header1.h */
#include header2.h
-----%<----- corte aqui -----%<-----
/* header2.h */
#include header1.h

Ao realizar as declaraes dos headers desse jeito, somente se os smbolos estiverem definidos,
garantimos que essas declaraes sero feitas apenas uma nica vez, no importa quantos #include
voc use.
Costumo nomear esses smbolos como __filename_INCLUDED__, onde filename s possui o
nome do arquivo, sem o .h. Como esses smbolos so annimos (no possuem valores) e valem
apenas para o preprocessador, eles no sero exportados para o executvel final e, ao usar o
filename, sei que ele ser definido apenas para header especfico. Claro que voc pode usar o
padro que seja mais conveniente para seu projeto. De fato, algumas IDEs usam alguns padres
malucos. O importante a caracterstica nica na nomeao dos smbolos.
Nada te impede de definir funes dentro de um header, mas essa no uma boa prtica... Existem
excees regra: Algumas funes intrnsecas nos headers do GCC definem funes inline
dentro de headers (veja cpuid.h, por exemplo). Eu recomendo que voc no faa isso em seus
cdigos. Acompanhar onde algumas declaraes so feitas, em projetos grandes, j complicado o
suficiente sem que se quebre uma regra to bsica.

Chamadas de funes contidas no mesmo mdulo


Num cdigo como abaixo voc espera que a funo f seja chamada pela funo g, mas no isso
que geralmente acontece:
/* teste.c */

int f(int x) { return x + x; }


int g(int x) { return x * f(x); }

Em casos como esse o compilador tende a criar uma funo f, que pode ser chamada por funes
contidas em outros mdulos, e incorpor-la, ou seja, codific-la inline na funo g.
Com isso voc acaba com duas cpias do mesmo cdigo: Uma que pode ser chamada e outra que
foi incorporada. Veja como fica, em assembly:
f:
lea eax,[rdi+rdi]
ret

g:
lea eax,[rdi+rdi] ; Deveria ser call f.
imul eax,edi
ret

Aqui no h grandes problemas, mas imagine que f seja uma rotina grande com umas 200
instrues. Ao invs de um simples CALL em g, teramos duas cpias de f. Isso, algumas vezes,
inaceitvel.
Para evitar esse comportamento voc pode fazer duas coisas: Mudar o atributo da funo f ou
codificar essa funo em um outro mdulo. Funes definidas em mdulos diferentes so,
necessariamente, chamadas via CALL, a no ser que sejam marcadas como inline (mas, fique
ciente, essa marca apenas uma dica!).
Para mudar um atributo de uma funo, no GCC, basta atrelar a declarao __attribute__((attr)) no
incio da declarao, onde attr um dos atributos que o GCC aceita. No caso, estamos interessados
no atributo noinline:

24
__attribute__((noinline)) int f(int x) { return x+x; }
int g(int x) { return x*f(x); }

Tamanho de inteiros e ponteiros


Se voc j usou C com Windows, deve ter se perguntado: Pra que diabos existe o tipo 'long'? Afinal,
no Windows, 'long' e 'int' tm exatamente o mesmo tamanho e a mesma semntica (ambos so
inteiros de 32 bits de tamanho). Isso tambm vlido para o modo 32 bits da maioria dos sistemas
operacionais. No modo x86-64 a coisa mais complicada...
Existem quatro modelos de uso dos tipos inteiros para o modo x86-64, dependendo do sistema
operacional em uso. Elas so conhecidas por siglas: IL32P64 (ou LLP64), LP64 (ou I32LP64),
ILP64 e SILP64. Nessas siglas o 'I' corresponde ao tipo 'int', 'L' ao 'long' e 'P' 'Ponteiro'. I32LP64
significa int de 32 bits; long e ponteiros de 64 bits, por exemplo.
Eis uma tabela mostrando as diferenas, em bits, entre os tipos dos quatro modelos e quem os usa
(exclu os ponteiros j que nos quatro modelos eles tm sempre 64 bits de tamanho):
Modelo short int long Sistema Operacional
IL32P64 16 32 32 Windows
I32LP64 16 32 64 POSIX ABI
ILP64 16 64 64 HAL (Hello, Ave!)
SILP64 64 64 64 UNICOS
Tabela 2: Modelos de inteiros e sistemas operacionais

A maioria dos sistemas operacionais de 64 bits atuais, que so baseados em POSIX, diferenciam os
tipos 'long' e 'int'. Mesmo assim, o tipo 'long long' foi incorporado na especificao de C para
garantir que tenhamos um tipo explcito relacionado com 64 bits. O modelo I32LP64 torna esse
novo tipo obsoleto. Ainda, a especificao da linguagem C preocupou-se com essas diferenas.
Para isso existe o header stdint.h que define apelidos para tipos como: int8_t, int16_t, int32_t e
int64_t e seus derivados unsigned, colocando um 'u' na frente do nome do tipo (exemplo: uint64_t).
Se voc quer criar cdigos que cruzem plataformas recomendvel que use esses tipos.
Os modelos ILP64 e SILP64 so usados por sistemas obscuros (HAL Computer Systems [Ave! O
que voc est fazendo, Ave?!] e UNICOS, segundo o wikipedia). No estranho que, no caso do
modelo SILP64, 'short' e 'long' tenham o mesmo tamanho?

No prudente confiar nos tamanhos dos tipos default


Talvez voc tenha se acostumado com as novas arquiteturas de processadores onde os tipos char,
short, int, long e long long tenham tamanhos definidos. No uma boa idia confiar nisso se voc
pretende criar cdigo para vrias plataformas.
Em primeiro lugar, no existem tipos short, long e long long. Essas so variaes de tamanho do
tipo int. Em segundo lugar, esses tamnhos variam de acordo com o processador. Eis um exemplo na
tabela seguir:

25
Tipo Z-80 8086 386 e superiores
char 8 8 8
short int 8 8? 16
int 8 16? 32
long int 16 16 3223
Tabela 3: Tamanho de tipos por processador

Num processador de 8 bits como o Z-80 quase todos os tipos de inteiros tm 8 bits de tamanho. Faz
sentido, porque trata-se de um processador de 8 bits! J no 8086, provavelmente o tipo int tem 16
bits e essa ser a diferena entre short e long... J no 386 o tipo short int diferente do char.
Marquei com ? Os tamanhos, em bits, que no tenho mais certeza (faz tempo que no lido com
esses processadores).
essencial que voc consulte a documentao do compilador para o processador alvo ou faa uso
do header limits.h que contm constantes como CHAR_MAX, SHORT_MAX, INT_MAX e
LONG_MAX, explicitando o mximo valor que pode ser armazenado nesses tipos. Alis, sequer
podemos supor que um char tenha sempre 8 bits... Em computadores antigos (alguns mainframes,
por exemplo, um char pode muito bem ter 7 bits). Para isso limits.h fornece a constante
CHAR_BITS.
Outro detalhe o uso do operador sizeof. Ele sempre lhe dar o tamanho em bytes de um tipo, at
mesmo ponteiros. Sendo um operador, s vezes ele no pode ser usado no pr-processador do
compilador. Fazer algo como mostrado abaixo causar erro de pr-compilao:
#if sizeof(char *) != 8

#endif

Da a importncia dos smbolos definidos em limits.h.

Existe diferena entre declarar e definir um smbolo


Primeiro, vamos a uma definio de smbolo: Todos os nomes que voc d as variveis, funes e
tipos so genericamente conhecidos como smbolos. Um smbolo um nome ou apelido.
A principal coisa que voc deveria entender sobre a linguagem C (e C++) que existe uma
diferena essencial entre declarar algo e definir algo. Quando voc faz:
unsigned long x;

Voc est declarando um smbolo chamado x que ser usado para conter um valor inteiro, longo
e sem sinal. Repare que voc no est dizendo para o compilador qual esse valor, apenas est
declarando: Ei, compilador! Separa um espao para um inteiro longo sem sinal e o chame de 'x'!
Quando atribui um valor, voc define o contedo da varivel. Simples assim...
No caso de variveis comuns, como mostrado acima, o smbolo 'x' automaticamente definido
como contendo um valor aleatrio, imprevisvel.
Existe tambm um atalho onde voc pode declarar e definir um smbolo ao mesmo tempo:
int x = 2;

Isso um atalho! O que voc est realmente fazendo declarar o smbolo x como sendo do tipo
23 Como j sabemos, no modelo I32LP64, no modo x86_64, este tamanho pode ser de 64 bits!

26
int e, logo em seguida, definindo o seu contedo como tendo o valor 2.
Ento no parece haver muita diferena entre declarar e definir, certo? A diferena est nos casos
onde no possvel definir um smbolo porque j foi definido em outro lugar. ai que entram os
arquivos header. Nesses arquivos comum existirem declaraes de funes e variveis que sero
definidas em algum outro lugar... A declarao abaixo:
int printf(const char *fmt, ...);

Informa ao compilador que, em algum lugar, existe uma funo chamada printf que toma um
ponteiro para um char, constante, e uma lista varivel de parmetros, retornando um int. No
contexto do uso dessa funo no necessrio saber onde ela foi definida. Esse um tipo de
declarao que voc encontra em headers como stdio.h.

Diferena entre declarao e uso


Quando voc usa operadores de ponteiros, existe uma diferena entre declar-lo e us-lo. As duas
coisas, abaixo, so diferentes:
int *p = &x; /* declarando um ponteiro que conter o endereo da varivel 'x'. */

*p = 10; /* Usando o operador de indireo para colocar 10 dentro do


endereo contido na varivel 'p' (declarada, acima, como ponteiro!). */

A primeira linha declara o ponteiro 'p' (usando o '*') e inicializa a varivel 'p' com o endereo de 'x'.
A mesma coisa pode ser feita assim:
int *p;
p = &x;

O ponto que, na declarao, o '*' no um operador, mas um artifcio sinttico, usado para
declarar ponteiros. J no uso da varivel p, o asterisco um operador de indireo. uma
operao que mandamos o compilador fazer com a varivel p... Falo mais sobre ponteiros
adiante...

Parnteses
Pode parecer ridculo falar de parnteses, mas eles tm vrios significados num cdigo fonte em C.
Eles podem ser usados para especificar parmetros de funes, podem ser usados para forar a
ordem de clculo numa expresso, podem ser usados para adequar um tipo de varivel a outro,
podem ser usados para realizar uma chamada a uma funo e podem ser usados para resolver
expresses que so ambguas:
int f(int); /* Declarao de prottipo. Funo aceita um parmetro do tipo 'int'. */
int (*p)[10]; /* Declara ponteiro de array para 10 'int's.
Sem os parnteses teramos um array de ponteiros. */
z = (x + 3) * y; /* Sem os parnteses o 3 seria multiplicado ao 'y'! */
i = (int)c; /* Converte a varivel 'c' para o tipo 'int'. */
f(2); /* Chama a funo f passando o valor 2 como parmetro. */

Nos dois primeiros casos temos declaraes onde os parnteses so necessrios. Nos dois ltimos,
temos operadores diferentes (casting e chamada de funo) e no terceiro caso temos uma resoluo
de ambiguidade.

Escopo dos parmetros de funes


Uma funo em C pode receber parmetros vindos de outras funes que a chamam. Uma funo
sempre chamada por outra funo. Assim, temos a funo chamadora e a funo chamada. No

27
h mistrio nisso.
Toda funo pode receber parmetros. E h um detalhe importante: Parmetros de funes so
encarados como se fossem variveis locais prpria funo e s podem ser alterados dentro dela.
Veja um exemplo de uma simples funo que preenche um buffer com zeros:
/* Primeira verso da funo.
Usa os parmetros como se fossem inalterveis. */
void fillzeros1(char *ptr, size_t size)
{
size_t i;

for (i = 0; i < size; i++)


ptr[i] = 0;
}
-----%<----- corte aqui -----%<-----
/* Segunda verso.
Usa os parmetros como variveis locais. */
void fillzeros2(char *ptr, size_t size)
{
while (size--)
ptr[size] = 0;
}

fcil provar que os parmetros so locais funo:


size_t s;
char buffer[1024];

s = sizeof(buffer);
fillzeros2(buffer, s);

printf("O valor de 'size' %d\n", s);

No fim das contas o fragmento de rotina, acima, imprimir o valor 1024, mesmo que 'size' seja
alterado de dentro da funo fillzeros2. Isso condizente com o fato de que poderamos chamar a
funo passando um valor constante, ao invs de uma varivel:
fillzeros(buffer, 1024);

O segundo parmetro, neste caso, uma constante que, por definio, no pode ser alterada. Mas,
dentro da funo esse valor colocado na varivel local 'size'.
Se essa explicao no bastar, historicamente parmetros so passados pela pilha, ou seja, uma
cpia dos parmetros so empilhados e usados pela funo. As variveis e constantes originais,
passados funo no so tocados por ela.

Prottipos de funes
comum, num cdigo mais complexo escrito em C ou C++, que voc veja declaraes de funes
de maneira incompleta. Isso serve para avisar o compilador o que est por vir sem que voc tenha
que definir uma funo com antecedncia. Provavelmente isso no nenhuma novidade para voc.
O detalhe que, s vezes, essas declaraes incompletas podem parecer meio confusas:
extern void qsort(void *, size_t, size_t, int (*)(const void *, const void *));

Onde esto os nomes dos parmetros na declarao acima? Onde est o corpo da funo? E esse
quarto parmetro maluco? No caso especfico do prottipo do qsort estamos dizendo ao compilador
que a funo tomar quatro parmetros. O primeiro um ponteiro genrico (void *), os outros dois
so do tipo size_t e o quarto um ponteiro para uma funo que toma dois parmetros const void
* e devolver um inteiro. Essa declarao tambm diz que qsort no retorna valores (void) e
definida em algum outro lugar, talvez numa biblioteca.

28
O que essa declarao no diz qual sero os nomes das variveis locais associadas aos parmetros!
isso! No h nenhuma informao adicional para a funo nessa declarao. Os prottipos s
existem para dizer ao compilador: Eu ainda no sei quem esse cara, mas ele se parece com isso
ai!. Fornecer o nome dos parmetros opcional.
Prottipos, em minha opinio, so construes excelentes para organizar cdigo. Costumo us-los
para colocar minhas funes em uma ordem lgica em meus cdigos-fonte. Por exemplo:
#include <stdio.h>
#include <stdlib.h>

/* Nesse meu cdigo eu uso uma funo calc(), mas ela


ser definida somente DEPOIS da funo main(). */
int calc(int);

int main(int argc, char *argv[])


{
int valor;

if (argc != 2)
{
fprintf(stderr, "Uso: test <valor>\n");
return 1;
}

valor = atoi(argv[1]);

/* A funo main() precisa saber como a funo calc() para


poder us-la! Dai o prottipo, l em cima. */
printf("O triplo do valor %d %d\n", valor, calc(valor));

return 0;
}

/* Finalmente, calc() definida aqui. */


int calc(int valor) { return (valor + valor); }

claro que no faz nenhum mal colocar o nome dos parmetros no prottipo. Alis, at uma boa
ideia, j que o compilador far testes de sintaxe entre a declarao (do prottipo) e a definio da
funo. Mas, ao mesmo tempo, ao no colocar nomes de parmetros, voc se libera de ter que
certificar-se que todos os arquivos que contenham as definies de suas funes tenham que ter, em
algum outro lugar, prottipos com parmetros nomeados exatamente como na definio. Eu prefiro
no nomear parmetros nos prottipos. Digamos que essa uma prtica tradicional entre os
programadores que lidam com a linguagem C...

Problemas com algumas estruturas com mscaras de bits


A especificao da linguagem C nos diz que o uso de mscaras de bits algo dependente de
implementao. Ao fazer algo assim:
struct mystruc_s {
unsigned x:6;
unsigned y:27;
};

Voc pode esperar que o primeiro bit do membro 'y' seja colocado imediatamente depois do ltimo
bit do membro 'x'. Em algumas arquiteturas isso pode ser verdade, mas no quando falamos das
arquiteturas Intel e do compilador GCC (e alguns outros). A estrutura acima produzir dois
DWORDs, onde os 6 bits inferiores estaro no primeiro DWORD, atribudo ao membro 'x', e os 27
bits seguintes no segundo DWORD, atribudo ao membro 'y'.
Para evitar essa diviso, podemos pedir ao compilador para compactar os bits:

29
struct mystruc_s {
unsigned x:6;
unsigned y:27;
} __attribute__((packed));

Agora a estrutura ter exatamente 5 bytes. Os 33 bits estaro compactados, o mais prximos
possvel, colocando os primeiros 32 bits dentro de um DWORD e o bit que sobra no ltimo BYTE.
Na maioria das vezes usar mscaras de bits no uma grande ideia. O compilador vai gerar muito
cdigo para arranjar os valores desejados. Imagine que voc use a estrutura compactada, acima, e
queira ler os valores de x e y:
struct mystruct_s s = { 2, 3 };
unsigned a, b;


a = s.x;
b = s.y;
-----%<----- corte aqui -----%<-----
; Cdigo gerado pelo compilador...

; O arranjo binrio dos valores x e y da estrutura (msb -> lsb):


; ???????y yyyyyyyy yyyyyyyy yyyyyyyy yyxxxxxx
; | |
; bit 31 bit 0

; L o primeiro byte e isola x.


movzx eax,byte ptr [s]
and eax,0x3F
mov [a],eax ; coloca em 'a'.

movzx eax,byte ptr [s]


movzx ecx,byte ptr [s+1]

; coloca os 2 primeiros bits de y no lugar certo.


shr al,6
movzx eax,al

; coloca os 8 bits do prximo byte de y no lugar certo.


sal rcx,2
or rcx,rax

; coloca os prrimos 16 bits de y no lugar certo.


movzx eax,byte ptr [s+2]
sal rax,10
or rax,rcx
movzx ecx,byte ptr [s+3]
sal rcx,18
or rcx,rax

; e finalmente coloca o ltimo bit de y no lugar certo.


movzx eax,byte ptr [s+4]
and eax,1
sal rax,26
or rax,rcx

; armazena o valor de y.
mov [b],eax

Que cdigo horrvel! S como comparao, eis a implementao que eu faria:

30
; L 64 bits da memria...
movzx rax,byte ptr [s]
mov rcx,rax ; Guarda valor lido em RCX.

; Isola os 6 bits inferiores.


and eax,0x3f
mov [a],eax

; Pega os 27 bits restantes.


shr rcx,6
and ecx,0x7ffffff
mov [b],ecx

Mas o ponto que, ao usar mapas de bits, o seu cdigo vai ficar enorme e, claro, um tanto lento.
Isso especialmente vlido com estruturas compactadas. Observe agora o cdigo equivalente com a
estrutura sem o atributo packed (gerado pelo compilador):
mov eax,[s]
and eax,0x3f
mov [a],eax

mov eax,[s+4]
and eax,0x7ffffff
mov [b],eax

bem parecido com minha implementao, no ? O motivo de usar EAX ao invs de RAX, que
a estrutura no tem 64 bits de tamanho... Mas, na verdade, j que dados tambm so alinhados, no
h problemas em ler um QWORD onde temos apenas 33 bits (um bit a mais que um DWORD). Por
isso minha rotina pode ser um pouquinho mais rpida que a gerada pelo compilador...
Mesmo assim, sem a compactao da estrtura, o cdigo do compilador fica, bviamente, mais
simples e rpido em relao ao cdigo anterior.
Siga a dica: Evite campos de bits sempre que puder.

Finalmente: Ponteiros!
Uma das coisas que causa certo desespero s pessoas que tiveram o primeiro contato com
linguagens como C e C++ so as criaturas do inferno conhecidas como ponteiros. Espero poder
infundir um pouco de autoconfiana e mostrar que ponteiros no mordem, so quase uns anjinhos, e
que so um dos recursos mais teis que outras linguagens fazem questo de esconder.
Sempre que voc ler a palavra ponteiro pode, sem medo, substitu-la por endereo de memria,
ou simplesmente endereo. E saiba que todo smbolo, exceto por macros e definies feitas no
nvel do preprocessador, um ponteiro, mesmo que no parea. O fragmento de cdigo, abaixo,
exemplifica:
int x;


x = 10;

Quando declaramos a varivel global 'x' estamos pedindo ao compilador que crie um espao na
memria com o tamanho de um int (4 bytes) e apelide o endereo inicial desse espao de 'x'. Isso
quer dizer que 'x' s existe no seu cdigo fonte e dentro do compilador. O nome real dessa varivel
o seu endereo. Assim, quando atribumos o valor 10 varivel 'x' o compilador entende que
dever gravar o valor 10 no endereo cujo apelido 'x'. Ele faz, mais ou menos isso:
mov dword ptr [0x601024],10

Neste exemplo, o endereo 0x601024 um exemplo de onde o compilador pode decidir colocar a

31
varivel, na memria.
Mesmo em assembly, que uma linguagem de nvel maior que a linguagem de mquina,
podemos usar a metfora da varivel. Sem nos preocuparmos muito com a sintaxe do cdigo
abaixo, podemos usar um smbolo como apelido para um endereo, como se fosse uma varivel:

section .bss
x: resd 1 ; 'x' um smbolo que equivale a um
; endereo de memria da sesso de dados onde
; reservamos o espao para um dword.

section .text


mov dword [x],10 ; Coloca o valor de 32 bits '10' no endereo
; apelidado por 'x'.

Declarando e usando ponteiros


Em C o conceito de ponteiro um pouco mais especializado. Trata-se de uma varivel que
contm um endereo, ao invs de um valor. Uma varivel do tipo ponteiro faz referncia a um dado
contido na memria, de maneira indireta, isto , essa varivel especial (o ponteiro) contm um
endereo que aponta para um dado do tipo declarado. Quando fazemos algo assim:
int * p;

Dizemos que a varivel 'p' ser usada para conter um endereo para alguma regio da memria (ou
seja, um ponteiro!) que possui um dado do tamanho de um int. Mas, ateno: A declarao acima
no inicializa a varivel! Um exemplo melhor est no grfico abaixo:

Figura 6: Ponteiro que aponta para uma varivel

Aqui o compilador escolheu o endereo 0x601432 para a varivel 'x' e o endereo 0x601024 para a
varivel 'p'. S que essa ltima um ponteiro que foi inicializada com o endereo de 'x' (via
operador &).
Note... a varivel p, e no *p!
Por questo de estilo, prefiro declarar ponteiros usando o '*' prximo ao nome da varivel. Isso
serve apenas para embelezar o cdigo. Se voc preferir separar a declarao do jeito que fiz
acima, por motivos de clareza, fique vontade...
Conforme vimos, em relao aos modelos IL32P64 e I32LP64, na arquitetura x86-64, todo
ponteiro, no importa o tipo associado a ele, tem exatamente 8 bytes de tamanho (o tamanho de um
QWORD, ou seja 64 bits).
Eis um exemplo simples de declarao e uso de um ponteiro:

32
#include <stdio.h>

int x = 10;

int main(int argc, char *argv[])


{
/* declarao de ponteiro 'p' definido como tendo o endereo de 'x'. */
int *p = &x;

printf("%lu\n%d\n", p, *p);

return 0;
}

O cdigo acima imprimir um valor aleatrio um endereo escolhido pelo compilador e o


valor 10. Mas, o que significa esse '*p' usado como parmetro na chamada de printf?
Diferente da declarao da varivel, esse outro '*' um operador de derreferncia ou indireo24.
O conceito que uma varivel do tipo ponteiro contm uma referncia (o endereo) usada para
obter o dado apontado e, ao usar o operador '*', voc diz ao compilador: pegue o dado cujo
endereo est no ponteiro.
Repare que existem dois momentos em que lidamos com ponteiros:
1. A declarao/definio: Onde reservamos o espao para a varivel;
2. O uso: Quando usamos o operador de indireo para obter o dado apontado.
A declarao de um ponteiro feita como qualquer outra varivel, exceto que o tipo seguido de
um '*' (ou, o nome da varivel precedido pelo asterisco, dependendo de como voc encara). E,
tambm, como para qualquer varivel, tudo o que o compilador far alocar espao suficiente para
caber um endereo.
O tipo associado ao ponteiro usado em operaes aritimticas envolvendo o ponteiro. Ponteiros
so valores inteiros e as mesmas regras da aritimtica, vlidas para unsigned longs, so vlidas para
eles: Podemos incrementar ou decrementar um ponteiro; podemos somar dois ponteiros (embora
isso no seja l muito til!); podemos somar um valor inteiro a um ponteiro; podemos subtrair dois
ponteiros (isso til!) ou uma constante...
No exemplo abaixo assumo que o ponteiro 'p' foi inicializado em algum outro lugar. Esse fragmento
serve para responder a pergunta: O que acontece quando incrementamos um ponteiro declarado
como sendo do tipo int?
/* Declarei como 'extern' para dizer que esse ponteiro foi inicializado em
algum outro lugar... */
extern int *p;

p++;

Vamos supor que o endereo contido em 'p' seja 0x601040. Depois de increment-lo, no
obteremos 0x601041, mas sim 0x601044! Isso acontece por causa do tipo associado ao ponteiro.
Um int possui 4 bytes de tamanho (32 bits), ento o ponteiro incrementado de 4 em 4. Se
tivssesmos ponteiros de tipos mais complexos, como no exemplo:
/* Essa estrutura tem 52 bytes de tamanho. */
struct vertice_s {
float x, y, z, w;
float nx, ny, nz;
float r, g, b, a;
float s, q;
};

24 Chamarei de operador de indireo daqui pra frente, pois dereferncia parece ser uma palavra inexistente na
lngua portuguesa.

33
struct vertice_s *vrtxp, *p;

p = vrtxp + 10;

Assumindo que 'vrtxp' tenha sido inicializado em algum outro lugar, o ponteiro 'p' conter o
endereo contido em 'vrtxp' mais 520 bytes, j que o tipo 'struct vertice_s' tem 52 bytes de tamanho.
Ento, recapitulando:
char *p; /* isso uma declarao de um ponteiro 'p' no inicializado. */
char *p = &s; /* Isso a declarao de um ponteiro 'p' inicializado com o
endereo de 's'. */
p = &s; /* Isso coloca o 'endereo de' 's' no ponteiro 'p'. */
*p = 'a'; /* Isso coloca a constante 'a' no endereo contido no ponteiro 'p'. */
p++; /* Isso incrementa o endereo contido em 'p'. */

Problemas com ponteiros para estruturas


H um problema com o uso do operador de indireo e a notao de estruturas (e unioes). O
operador de membro de dados '.' tem maior precedncia do que o operador de indireo '*'. Por
causa disso o cdigo abaixo no faz o que voc espera:
struct S {
int x;
};

/* Apenas um exemplo de inicializao de ponteiro para estrutura. */


struct S *p = &s;

/* Isso no faz o que voc pensa que faz! */


*p.x = 3;

Acabaremos com algum erro de compilao. de se supor que *p.x signifique a derreferncia do
ponteiro 'p' seguida da obteno do membro de dados 'x', mas, como '.' tem precedncia maior, o
compilador entende que 'p.x' o ponteiro. O que falso!
Existem duas maneiras de resolver isso: Podemos usar parnteses para acabar com a ambiguidade:
(*p).x = 3; /* Essa a sintaxe correta! */

Ou, desde o padro ANSI da linguagem C existe um atalho. O operador '->' usado para obter um
membro de uma estrutura atravs de um ponteiro. Ou seja, o que est esquerda de '->' deve ser
sempre um ponteiro para uma estrutura e direita, um membro de dados. A mesma linha, acima,
fica assim:
p->x = 3;

A coisa funciona do mesmo jeito para membros de unies.

Ponteiros e strings
Voc j deve ter topado com uma string, em C. So aquelas constantes cercadas por aspas duplas
("). Sinto dizer, j que a contradio na prxima sentena vai dar um n no seu crebro:
Uma string no uma string! um array de chars terminado com o byte 0 (zero)!
O que ocorre que as funes da biblioteca padro da linguagem C interpretam arrays de chars
terminados com zero como se fossem strings. Isso torna a linguagem flexvel, j que podemos usar
qualquer array como se fosse uma string, basta fazer um casting de qualquer ponteiro para 'char *',
e nos certificarmos que, em algum ponto, o array contenha um byte zero.
Fica bastante bvio que strings no existem, como um tipo primitivo, em C, j que no existem

34
operadores especializados para lidar com elas. Ou voc as trata como arrays, diretamente, ou lida
com elas atravs de funes da biblioteca padro como strcpy, strcat, strdup etc.

Diferenas entre declarar ponteiros e arrays contendo strings


Existem alguns jeitos de declararmos e inicializarmos strings:
char *str1 = "Hello";
char str2[] = "Hello";
char str3[] = { 'H', 'e', 'l', 'l', 'o', '\0' };

Sempre que topar com uma constante do tipo string, cercada por aspas duplas, o compilador
traduzir isso para um array contendo os caracteres individuais e um '\0' (zero) adicional. Assim, a
primeira declarao a de um ponteiro que conter o endereo do array que contm 6 bytes ('H', 'e',
'l', 'l', 'o' e '\0').
As declaraes de str2 e str3 so equivalentes entre si. Elas dizem para o compilador que os 6 bytes
devem ser alocados na regio de dados da memria e inicializados com 'H', 'e', 'l', 'l', 'o' e '\0'.
Depois disso os smbolos str2 e str3 contero os endereos iniciais dos arrays.
Ateno: A declarao abaixo no coloca o valor zero no final do array. A constante tem 6
bytes (contando com o zero final), mas o array foi explicitamente declarado como tendo 5
chars (bytes):
char str4[5] = "Hello";

O compilador colocar os 5 primeiros bytes da constante no array, descatar o sexto byte e,


talvez, emita um aviso sobre isso.
Existe uma diferena entre declarar um ponteiro que aponta para uma constante e um array
inicializado com uma. E sutil: No primeiro caso, a constante no pode ser alterada porque, afinal
de contas, uma constante! No caso dos arrays a constante usada para inicializar o contedo da
memria reservada para eles e pode, em runtime, ser modificada.
Para ilustrar, eis um programa que falhar miseravelmente, explodindo o famigerado
Segmentantion Fault em nossas caras, provavelmente porque constantes, quando so armazenadas
na memria, ficam em uma sesso read-only do cdigo:
/* strtok.c */
#include <stdio.h>
#include <string.h>

/* Isto um ponteiro para uma constante! */


char *s = "Hello, world!";

int main(int argc, char *argv[])


{
char *p;

p = strtok(s, ", "); /* Ocorrer um 'segmentation fault' aqui! */


while (p != NULL)
{
printf("%s\n", p);
p = strtok(NULL, ", ");
}

return 0;
}
-----%<----- corte aqui -----$<-----

35
$ gcc -g -o strtok strrok.c
$ gdb ./strtok
(gdb) r
Starting program: strtok

Program received signal SIGSEGV, Segmentation fault.


strtok () at ../sysdeps/x86_64/strtok.S:190
190 ../sysdeps/x86_64/strtok.S: No such file or directory.
(gdb) bt
#0 strtok () at ../sysdeps/x86_64/strtok.S:190
#1 0x000000000040056f in main (argc=1, argv=0x7fffffffe1c8) at strtok.c:11

O problema com esse programa que strtok espera poder escrever no array apontado por 's' e no
consegue. Se voc mudar a declarao de 's' para:
char s[] = "Hello, world!";

Tudo funcionar perfeitamente.

Mais ponteiros e arrays


Assim como com ponteiros, existe uma diferena entre declarao e uso. No uso de arrays, o
operador de indexao '[]' usado para obter um item de um array , na verdade, um atalho para
aritmtica de ponteiros.
Esse operador toma um ponteiro como base e um ndice que somado a esse ponteiro para obter um
endereo. De posse desse endereo calculado, o dado acessado. Os dois usos, abaixo so
equivalentes:
int x[10];

y = x[3]; /* 'x' um ponteiro e 3 um deslocamento. */
y = *(x + 3); /* a mesma coisa!!! */

O nome da varivel do array 'x' um ponteiro para o primeiro item do array. Podemos chamar esse
smbolo de base do array. O ndice, dentro de '[]', o deslocamento adicionado a essa base,
levando em conta o tipo do ponteiro, para obter o endereo do item desejado. Por isso, para
inicializar um ponteiro apontando para o primeiro item de um array, podemos sempre fazer de uma
das duas maneiras:
int x[10];
int *p;

p = x; /* 'p' aponta para o incio do array. */


p = &x[0]; /* a mesma coisa!!! */

Ou seja, se voc no se sente confortvel em usar o nome de um array como sendo o ponteiro para o
item de ndice 0 (zero), ento pode sempre pegar o endereo do item zero explicitamente. a
mesma coisa.
O fato de que o operador '[]' seja uma notao de ponteiros escondida permite um uso esquisito da
notao de arrays graas propriedade comutativa da adio... J que 'x+3' a mesma coisa que
'3+x', os dois usos abaixo so exatamente idnticos:
y = x[3]; /* 'x' a base e '3' o deslocamento. /
y = 3[x]; /* '3', agora, a base e 'x' o deslocamento. */

pouco provvel que voc obtenha algum erro de compilao. Talvez um aviso... talvez...

Declarando e inicializando arrays, estruturas e unions


Neste tpico quero deixar registrado um comportamento til, nos compiladores C, que est

36
especificado e, portanto, garantido que sempre funcione. Ao declarar e definir apenas um item de
um array, estrutura ou union, o compilador automaticamente preencher o restante dos itens (no
inicializados no cdigo fonte) com zeros:
/* test.c */
#include <stdio.h>

struct mystruct_s {
int x;
int y;
int z;
};

struct mystruct_s s = { 1 }; /* define apenas 'x'. */


int a[3] = { 2 }; /* define apenas a[0]. */

int main(int argc, char *argv[])


{
printf("s = {%d, %d, %d};\n"
"a = [%d, %d, %d];\n",
s.x, s.y, s.z,
a[0], a[1], a[2]);

return 0;
}
-----%<---- corte aqui ---%<-----
$ gcc -o test test.c
$ ./test
s = {1, 0, 0};
a = [2, 0, 0];

Viu s? Definimos apenas o primeiro item da estrutura declarada como 's' e o primeiro item do array
'a', mas o compilador automaticamente zerou os itens restantes.
Ainda, no caso do GCC, existe uma extenso para inicializaes de estruturas e unions que pode ser
til. Podemos explicitar qual membro ser inicializado. Substitua a inicializao da estrutura 's',
acima, por:
struct mystruct_s s = { .z = 1 };

E voc obter s = { 0, 0, 1 }. A mesma coisa pode ser feita com um array, basta explicitar o ndice
do item a ser inicializado:
int a[3] = { [1] = 2 }; /* Inicializa o item [1] com 2 e o resto com zeros.
ndices de arrays sempre comeam com [0]. */

Esses recursos so extenses que esto disponveis na especificao C99, mas especificaes mais
antigas (C89, por exemplo) no as implementam.
Outra maneira de atribuir valores a um array usando literais compostos. Esse um recurso que
foi introduzido como extenso no GCC e tornou-se padronizado na especificao ISO C11.
Confesso que um macete que no costumo usar e at acho meio esquisito. Consiste em informar
o array literal precedido por um type casting. Assim:
double *p = (double []){ 1, 2, 3 };

O casting necessrio por que o compilador pode ficar confuso se ele no for informado. Outra
considerao sutil ao usar esse macete que criamos um ponteiro para um array constante. Isso
diferente de declarar:
double p[] = { 1, 2, 3 };

Isto , usar literais compostos podem causar o mesmo problema que temos ao declarar strings
contantes atribudas a ponteiros, ao invs de arrays.

37
Os literais compostos podem ser usados para passar arrays literais para funes:
extern int f(int *x);


x = f((int []){ 1, 2, 3});

De qualquer maneira, acho essa extenso muito esquisita...

Ponteiros, funes e a pilha


Ponteiros so muito teis quando trata-se de passar parmetros para funes por referncia, ao
invs de por valor. Especialmente se estamos falando de estruturas complexas. Suponha que
tenhamos uma estrutura com vrios itens, como:
struct vectice_s {
float x, y, z, w;
float nx, ny, nz;
float r, g, b, a;
float s, q;
};

Essa estrutura contm 13 floats e tem o tamanho total de 52 bytes (cada float tem 4 bytes de
tamanho). Agora, repare nas declaraes de funes abaixo:
extern int CheckNormal(struct vertice_s v);
extern int CheckNormal2(struct vertice_s *vp);

Se chamarmos a primeira funo todos os 52 bytes da estrutura sero empilhados antes da chamada.
De fato, 56 bytes da pilha sero usados, j que na arquitetura x86-64 todos os itens na pilha devem
ser alinhados por QWORDs (de 8 em 8 bytes).
Se usarmos a segunda funo, tudo o que ser passado para a funo CheckNormal2 sero os 8
bytes que correspondem ao tamanho de um ponteiro. Esses 8 bytes podem nem mesmo serem
empilhados, de acordo com a conveno de chamada que mostrarei no prximo captulo. Nas
declaraes acima, o primeiro caso um exemplo de passagem por valor. O segundo, passagem por
referncia.
Passagem por valor, especialmente de estruturas complexas, pode aumentar a presso sobre a pilha.
Na linguagem C muito comum dividir o trabalho em funes e realizar diversas chamadas... Se
para cada funo chamada empilharmos, digamos, 56 bytes, e tivermos 50 chamadas acumuladas, o
uso da pilha ser de, aproximadamente, 32 KiB (32000 bytes, exatamente, alm dos 56 bytes de
parmetros empilhados temos que contar tambm com o endereo de retorno das funes
chamadoras!). No parece grande coisa, no ? Acontece que 32 KiB o tamanho de todo o cache
L1 para cada ncleo, em certas arquiteturas. E, num ambiente multithreaded, podemos ter pilhas
menores que isso. Quero dizer que, com uso intenso da pilha, teremos problemas em outras reas...
A pilha usada para conter outras coisas alm de parmetros de funes e endereos de retorno.
No estamos contando aqui com variveis locais que no puderam ser mantidas em registradores,
bem como variveis temporrias e valores de registradores que precisam ser preservados entre
chamadas... Ou seja, se usarmos passagem de parmetros por valor de estruturas complexas, num
programa grande, corremos o risco de exaurir a pilha25, tomando um erro de stack overflow na
cara, e abortar o programa no meio de um processamento crtico.

25 Mas no necessrio preocupar-se demais com a pilha... Os sistemas operacionais modernos alocam bastante
espao para elas no userspace. Cerca de 1 MiB alocado para ela... E a pilha ainda pode crescer. S que isso vale
para a thread principal do processo. As thread secundrias normalmente so inicializadas com uma pilha de
tamanho fixo e pequeno.

38
Entendendo algumas declaraes malucas usando ponteiros
Se no bastasse todas as explicaes acima, que afugentam os novatos, temos o verdadeiro terror na
flexibilidade e na sintaxe usada por C e C++ com a declarao de ponteiros. Afinal, voc pode
querer um ponteiro simples para um tipo primitivo ou pode querer algo como um ponteiro para um
array de ponteiros de funes. Comecemos por algo simples. Declarar um array de ponteiros:
int *iArray[3]; /* array de 3 ponteiros do tipo int. */

Mas, e se quisssemos declarar um ponteiro para um array de 3 ints? Notou a diferena? No caso
acima temos 3 ponteiros num array e agora quero um ponteiro para um array de 3 itens. Para obter
isso preciso enganar o compilador, usando parnteses:
int (*iArray)[3]; /* 'iArray' um ponteiro para um array de 3 ints. */

Graas aos parnteses dizemos ao compilador que iArray o ponteiro para um array. No caso
anterior o 'int *' o tipo do array, no segundo, 'int'.
Eis outro exemplo, no caso de declarao de funes:
int *f(void); /* funo que retorna ponteiro do tipo int. */
int (*f)(void); /* Ponteiro (no inicializado) para uma funo que retorna int. */

Outro tipo de declarao que voc pode precisar a de ponteiro para ponteiro:
char **pp; /* 'pp' um ponteiro que aponta para um ponteiro que aponta para um char. */

Voc pode ver a utilidade disso quando temos um array de strings:


/* astr um array de ponteiros. */
char *astr[] = { "hello", "world", NULL };
char **pp;

/* 'pp' aponta para o primeiro item do array acima. */


pp = astr;

/* Percorre o array 'astr' usando o ponteiro 'pp'. */


while (*pp != NULL)
{
printf("%s\n", *pp);
printf("O primeiro char da string '%c'\n", **pp);
pp++;
}

Aqui fao 'pp' apontar para o primeiro item do array 'astr', composto de 3 ponteiros para char. '*pp'
nos d, ento, o endereo inicial de cada string, individualmente. Se usssemos '**pp', obteramos o
primeiro caractere da string apontada por '*pp'.

Figura 7: Estrutura usando ponteiro para ponteiros.

A utilidade desse tipo de construo vem do fato que se um ponteiro pode ser usado para apontar
para um item de um array, ento se usarmos um array de ponteiros podemos ter uma estrutura
bidimensional ou um array de arrays26.
26 Isso bem diferente de um array bidimensional no contexo da linguagem C. O que chamo de array de arrays
um ponteiro para um array de ponteiros. H uma distino sutil.

39
O que acabo de descrever o que ocorre com o segundo parmetro da funo main. Quando voc
executa seu programa, a linha de comando divida em strings (cada string um array de chars) e os
ponteiros para essas strings so colocados num array. A funo main recebe o ponteiro para esse
array de ponteiros. por isso que declarar 'argv' como 'char *argv[]' ou 'char **argv' ,
essencialmente, a mesma coisa:
int main(int argc, char *argv[]); /* Esta a forma cannica da declarao de 'main' */
int main(int argc, char **argv); /* Essa a mesma coisa que a declarao acima. */

Com relao a outras declaraes malucas, podemos ter algumas bem complicadas:
int * (*f[10])(int); /* array de 10 ponteiros para funes que retornam ponteiros para int. */

Pode ser desafiante criar declaraes para, por exemplo, um array de ponteiros de funes que
retornam ponteiros para arrays de funes... Recomendo que voc evite esses tipos de maluquices.

Utilidade de ponteiros para funes


Uma dvida que um leitor me apresentou foi sobre os ponteiros para funes. Acho que ficou claro
que os compiladores usam apelidos para endereos de coisas que esto localizadas na memria.
Funes no so diferentes.
Em assembly uma chamada para um procedimento, via instruo CALL, usa como parmetro o
endereo de entrada do procedimento. E como ponteiro apenas uma forma mais rebuscada de
falarmos de endereo, podemos dizer que CALL usa um ponteiro. Em C podemos declarar um
ponteiro para uma funo e cham-la atravs deste ponteiro:
/* funcptr declarada como um ponteiro para uma funo. */
int (*funcptr)(int);

/* Esta , de fato, a definio de uma funo. */


int f(int x) { return x + x; }

int main(int argc, char *argv[])


{
/* Atribui ao ponteiro 'funtptr' o endereo do
ponto de entrada da funo 'f'. */
funcptr = f;

/* Chama a funo 'f' indiretamente, usando o ponteiro 'funcptr'. */


printf("O dobro de %d %d\n", 2, funcptr(2));

return 0;
}

O prprio smbolo 'f' um ponteiro, se voc pensar bem... ele o endereo do ponto de entrada da
funo.
Qual a utilidade desse artifcio? Suponha que voc tenha duas ou mais funes que fazem a
mesma coisa, mas que so otimizadas para processadores diferentes (ou, usando extenses
diferentes do processador). Por exemplo, trs funes que preencham buffers com zero... uma
escrita em C puro, uma que use SSE e outra escrita em assembly puro. No seu cdigo voc quer
chamar uma delas, atravs da mesma chamada, de acordo com uma escolha feita na inicializao da
aplicao:
/* Prottipos para as 3 funes disponveis. */
extern void zerofill_c(void *, size_t);
extern void zerofill_sse(void *, size_t);
extern void zerofill_asm(void , size_t);

/* Ponteiro para uma funo. */


void (*zerofill_ptr)(void *, size_t);

40
/* Rotina de inicializao chamada no incio do seu cdigo. */
void init(void)
{
if (sse_present)
zerofill_ptr = zerofill_sse;
else if (can_use_asm)
zerofill_ptr = zerofill_asm;
else
zerofill_ptr = zerofill_c;
}

int main(int argc, char *argv[])


{

/* init vai decidir qual das 3 funes usar. */
init();

/* Chama zerofill_xxx Via ponteiro! */


zerofill_ptr(buffer, sizeof(buffer));


return 0;
}

A biblioteca padro: libc


Toda vez que voc compila e linka um cdigo em C, leva junto smbolos da a biblioteca padro
libc. No Linux toda biblioteca comea com o nome lib, seguida do que ela contm. libc contm,
alm das funes da biblioteca padro, as rotinas de inicializao e finalizao de seus programas.
Diferente do que voc pode ter aprendido, a funo main no a primeira coisa a ser executada no
seu cdigo. A primeira funo executada chama-se _start. O diagrama abaixo mostra algumas das
funes que so chamadas antes (e depois) de main:

Figura 8: Inicializao de um programa em C

Procure interpretar esse grfico de chamadas como uma rvore binria: _start chama
__lib_start_main, que chama __libc_csu_init e depois main e depois, na ordem, _init, _gmon_start,
frame_dummy, __do_global_ctors_aux e cada um dos construtores registrados no array
constructors[1..n]. Depois dessas inicializaes todas a funo main finalmente chamada. Quando
ela sai, exit executado e ele executa todos as funes registradas com atexit, depois as registradas
no array finiarray e os destrutores...
S para ilustrar, eis o cdigo fonte completo de um helloworld, totalmente escrito em assembly, que
pode ser executado e no usa a libc27:

27 O padro de chamada para syscalls no modo 32 bits um pouco diferente do modo 64 bits. Em ambos os modos
poderamos usar a instruo 'int 0x80', mas a instruo syscall mais indicada. Ao usar syscall a parametrizao das
funes diferente do que ao usar 'int 0x80'.

41
; helloworld.asm
bits 64
section .data

msg: db "Hello, world!", 10


len equ $ - msg

section .text

global _start
_start:
mov rax,1 ; sys_write syscall
mov rdi,1 ; STDOUT
mov rsi,msg
mov rdx,len
syscall

mov eax,60 ; sys_exit syscall


xor rdi,rdi ; cdigo de erro de sada.
syscall
-----%<----- corte aqui -----%<-----
$ nasm -f elf64 helloworld.asm -o helloworld.o
$ ld -s helloworld.o -o helloworld
$ ./helloworld
Hello, world!
$ ls -l helloworld
-rwxrwxr-x 1 user user 512 Dez 29 21:43 helloworld

O smbolo _start usado pelo linker, por default, como ponto de partida para o executvel. No
significa que todo executvel tem que ter esse smbolo. Poderamos usar a opo '-e' do linker para
estipular um ponto de entrada diferente:
$ ld -e _mystart -s myprog.o -o myprog

Nos programas em C a libc o motivo pelo qual seu cdigo no ter menos que 8 KiB de tamanho.
Repare que a verso em puro assembly tem apenas 512 bytes. A biblioteca padro faz muito
trabalho de inicializao e finalizao.
De uma forma geral, a funo _libc_csu_init responsvel pela execuo de construtores... Eles
no so coisas restritas ao C++. Graas ao esquema de atributos do GCC podemos criar funes que
sero executadas antes de main, como se fossem construtores de objetos, no C++. Da mesma
forma, existem funes de finalizao, ou destrutores que so chamados quando o cdigo em C
encerrado:
/* test.c */
#include <stdio.h>

void __attribute__((constructor)) ctor(void)


{
printf("Funo chamada antes de main().\n");
}

void __attribute__((destructor)) dtor(void)


{
printf("Funo chamada depois de main().\n");
}

int main(int argc, char *argv[])


{
printf("Hello!\n");
return 0;
}
-----%<----- corte aqui -----%<-----
$ gcc -o test test.c
$ ./test
Funo chamada antes de main().
Hello!
Funo chamada depois de main().

42
O cdigo de inicializao a poro da libc que linkada de forma esttica. O restante das funes,
como printf, por exemplo, so linkadas de forma dinmica. O GCC assume a opo -lc por
default28.

A libc faz um monte de coisas por baixo dos panos


Eis uma comparao com o cdigo helloworld.asm, mostrado anteriormente, e o clssico hello.c
Para compar-los vou usar a ferramenta strace que mostra apenas chamadas syscalls. Se voc quiser
ver, inclusive, chamadas bibliotecas, use o utilitrio ltrace com a opo '-S':
$ ls -l
total 24
-rwxrwxr-x 1 user user 8510 Fev 14 14:53 hello
-rw-rw-r-- 1 user user 63 Fev 14 14:53 hello.c
-rwxrwxr-x 1 user user 512 Fev 14 14:58 helloworld
-rw-rw-r-- 1 user user 331 Fev 14 14:57 helloworld.asm

$ cat hello.c
/* Eis o nosso simples hello.c */
#include <stdio.h>
void main(void) { puts("Hello, world!"); }

$ strace ./helloworld
execve("./helloworld", ["./helloworld"], [/* 69 vars */]) = 0
write(1, "Hello, world!\n", 14) = 14
_exit(0) = ?
+++ exited with 0 +++

$ strace ./hello
execve("./hello", ["./hello"], [/* 69 vars */]) = 0
brk(0) = 0x120c000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8fbc1cd000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=113021, ...}) = 0
mmap(NULL, 113021, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8fbc1b1000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\320\37\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1845024, ...}) = 0
mmap(NULL, 3953344, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f8fbbbe7000
mprotect(0x7f8fbbda2000, 2097152, PROT_NONE) = 0
mmap(0x7f8fbbfa2000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3,
0x1bb000) = 0x7f8fbbfa2000
mmap(0x7f8fbbfa8000, 17088, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0)
= 0x7f8fbbfa8000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8fbc1b0000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8fbc1ae000
arch_prctl(ARCH_SET_FS, 0x7f8fbc1ae740) = 0
mprotect(0x7f8fbbfa2000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ) = 0
mprotect(0x7f8fbc1cf000, 4096, PROT_READ) = 0
munmap(0x7f8fbc1b1000, 113021) = 0
fstat(1, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8fbc1cc000
write(1, "Hello, world!\n", 14) = 14
exit_group(14) = ?
+++ exited with 14 +++

Pera ai! Porque execve est sendo chamada? Acontece que Linux no cria um processo do nada.
Todos os processos so forks do processo init e depois feita a chamada para execve para substituir
o novo processo pela imagem contida no arquivo executvel...
Depois disso, a primeira coisa que voc nota a tentativa de abrir um arquivo de configurao
28 A opo -l precisa apenas da poro do nome da biblioteca que segue 'lib' e precede '.so' ou '.a'. Por exemplo, se
voc precisar usar funes da biblioteca libORbit-2.so, s precisar especificar -lORbit-2 para o GCC.

43
chamado ld.so.nohwcap. Isso existe porque algumas bibliotecas podem ter sido pr-carregadas e
esto atreladas a features especficas do seu processador. Se /etc/ld.so.nohwcap existir e seu
contedo for diferente de '0', ento a libc vai tomar alguns cuidados ao carregar essas bibliotecas
especiais.
A seguir, a libc consulta a configurao de ld.so.preload para que ela no tente carregar as
bibliotecas listadas nesse arquivo de configurao (j que elas j esto carregadas!).
O arquivo ld.so.cache contm bibliotecas candidatas a j estarem carregadas e prontas para ser
usadas. A libc verifica isso tambm...
Durante a carga uma srie de alocaes de redimensionamentos de pginas privadas ao processo so
feitas e a biblioteca libc.so carregada. At ento o que foi executado eram syscalls.
A chamada a arch_prctl inicializa o seletor FS para o processo (possivelmente usado durante
chaveamento de contextos de tarefas).
Finalmente, depois de mais alguns ajustes a syscall write chamada para imprimir nossa string e
exit chamada.
Esse trabalho todo um dos motivos pelos quais desenvolver aplicaes inteiras em assembly
impraticvel.... A libc contm um enorme conjunto de features que facilitam muito nossas vidas.
Sem ela voc teria que criar tudo isso manualmente, medida que necessitar.
A vantagem da libc que essas inicializaes so feitas apenas uma vez, bem como rotinas de
finalizao se necessrias. Uma vez que o cdigo em main executado, todo o controle est com
a sua aplicao. claro, de tempos em tempos voc cede esse controle libc ou a outras
bibliotecas, como pthread, por exemplo. Mas um preo muito pequeno para pagar em troca de
performance, estabilidade e disponibilidade de ferramentas interessantes.

Cuidados ao usar funes da libc


As funes da libc so construdas para funcionar de acordo com certas condies. Por exemplo, as
funes strtok, setlocale, fcloseall, readdir, tmpnam e gethostbyname no so seguras para serem
usadas em threads. Elas so marcadas como MT-Unsafe na documentao da biblioteca (pena que
isso no seja explcito nas manpages). Alm da insegurana quando a multithreading, muitas
funes no so seguras para serem usadas em tratadores de sinais ou, pior, no so seguras para
serem interrompidas por tratadores de sinais. Essas so marcadas como AS-Unsafe (de
assynchonous signal).
Existem ainda aquelas que no so seguras para serem usadas em tratadores de cancelamento de
threads. Essas so marcadas como AC-Unsafe (de assynchronous cancelation).
Consulte o manual da GNU libc29 para maiores detalhes.

Disparando processos filhos


Outra dvida frequncia a respeito do Linux como fazer para que seu programa execute outro
programa. No Windows temos a funo CreateProcess e, parece, existe algo semelhante no Linux,
que faz parte da biblioteca padro (libc): As funes exec.
Antes de falar de exec preciso explicar que existe uma funo genrica chamada system que no
deve ser usada! Essa funo bem simples: Voc passa uma string contendo a linha de comando
desejada e executa a funo. O valor de retorno o cdigo de retorno da linha de comando ou -1,

29 Baixe o pdf neste link: http://www.gnu.org/software/libc/manual/pdf/libc.pdf

44
em caso de erro30. Acontece que system tem o potencial de causar graves problemas de segurana e
no recomendado por diversos advisories especializados. O mtodo correto de disparar um novo
processo atravs do par de funes fork e exec.
Forking a ao de criar uma cpia do processo atual de forma que ambos os processos
continuaro a execuo a partir do fork. A funo fork retornar um de trs valores:
-1, se o novo processo no pode ser criado a partir do processo atual;
0 retornado para o processo filho;
Um valor positivo retornado para o processo pai, contendo o PID do processo filho.
A funo fork tem um efeito colateral interessante: O processo filho herda todos os dados,
descritores de arquivos, e outros recursos do pai. Esses recursos s divergiro do pai quando forem
modificados pelo filho.
A partir da criao do processo filho com base no processo pai, queremos que esse novo processo
seja substitudo pela carga de um novo executvel. Isso feito por exec. Ele sobrepe o processo.
O cdigo para executar um processo filho arbitrrio, dessa maneira, esse:
static char const * const arg = "./proc2";
pid_t pid;


if ((pid = fork()) == -1)
{
fprintf(stderr, "ERRO: No foi possvel carregar o processo filho!");
exit(EXIT_FAILURE); /* Ou outra instruo para desistir da criao do processo filho. */
}

/* Se for o filho, substitui o processo carregando um novo executvel.


Note que o primeiro parmetro igual ao segundo. O primeiro parmetro
o arquivo que ser executado. O segundo parmetro equivalente a argv[0] e
assim por diante. */
if (pid == 0)
execl(arg, arg, NULL);

/* o pai continua aqui */


Mesmo depois da execuo de execl (ou derivados), alguns atributos do processo original so
preservados, afinal o processo ainda um fork do processo original, s com a imagem binria
modificada. Mas a maioria desses atributos so substitudos, ou seja, so descartados. Como o
mapeamento de memria, os dispatches e mscaras de signals. Em essncia, como se todos os
atributos fossem recriados para a nova imagem binria. Consulte a documentao de execve para
mais detalhes.

30 Embora existam diferenas entre sistemas POSIX! No Linux, por exemplo, recomendvel que se use o macro
WEXITSTATUS para obter o valor real de retorno.

45
Captulo 4: Resolvendo dvidas
sobre a linguagem Assembly
sempre bom ter em mente que linguagem de mquina a nica linguagem que seu processador
entende. Todas as outras existem para que voc, programador, tenha condies de dizer ao
computador o que ele deve fazer.
Essas outras linguagens precisam ser traduzidas e, no processo de traduo, alguma inteno pode
ser perdida, mal interpretada.
Com assembly nos aproximamos bastante da linguagem de mquina entendida pelo processador.

O processador pode ter bugs


Encare seu processador como se fosse um pequeno computador que est executando,
constantemente, um programa. Esse programa feito para decodificar instrues e execut-las. S
que, como em qualquer programa, ele pode conter bugs.
Para saber quais bugs seu processador tem necessrio saber qual a verso s software que ele
est rodando. Isso feito fazendo uma consulta via instruo CPUID:
/* version.c */
#include <stdio.h>
#include <string.h>
#include <cpuid.h>

struct version_s {
unsigned stepping:4;
unsigned model:4;
unsigned family:4;
unsigned type:2;
unsigned :2;
unsigned exmodel:4;
unsigned exfamily:8;
};

static const char *processor_types[] =


{ "Original OEM", "Overdrive", "Dual", "Reserved" };

const char *getBrandString(void);

void main(void)
{
unsigned a, b, c, d;
int type, family, family2, model, stepping;

__cpuid(1, a, b, c, d);
type = ((struct version_s *)&a)->type;
family = family2 = ((struct version_s *)&a)->family;
model = ((struct version_s *)&a)->model;
stepping = ((struct version_s *)&a)->stepping;

if (family == 6 || family == 15)


{
if (family == 15)
family2 += ((struct version_s *)&a)->exfamily;
model += ((struct version_s *)&a)->exmodel << 4;
}

47
printf("Seu processador: \"%s\"\n"
"\tTipo: %s\n"
"\tFamlia: 0x%02X\n"
"\tModelo: 0x%02X\n"
"\tStepping: 0x%02X\n",
getBrandString(),
processor_types[type],
family2, model, stepping);
}

const char *getBrandString(void)


{
static char str[49];
unsigned *p;
size_t idx;
unsigned a, b, c, d;
int i;

p = (unsigned *)str;
for (i = 2; i <= 4; i++)
{
__cpuid(0x80000000 + i, a, b, c, d);
*p++ = a;
*p++ = b;
*p++ = c;
*p++ = d;
}

*(char *)p = '\0';

/* Retorna string a partir do ponto onde no h espaos. */


return (const char *)str + strspn(str, " ");
}
-----%<----- corte aqui -----%<------
$ gcc -o version version.c
$ ./version
Seu processador: "Intel(R) Core(TM) i5-3570 CPU @ 3.40GHz"
Tipo: Original OEM
Famlia: 0x06
Modelo: 0x3A
Stepping: 0x09

Encare os campos famlia, modelo e stepping como se fosse a verso do software do seu
processador. No exemplo acima, em uma de minhas mquinas de teste, temos um i5 cuja verso
6.58.9. Se voc tiver o mesmo brand de processador (i5-3570) pode ser que seu stepping seja
diferente. Para a verso 6.58, se o stepping for maior que 9 ento o seu processador tem menos bugs
que o meu...

A mesma instruo pode gastar mais tempo do que deveria


Tomemos o exemplo de uma instruo muito simples:
add r/m64,imm64

r/m64 significa que o operando do lado esquerdo aceita um registrador (r) ou referncia
memria (m) de 64 bits. O operando do lado direito, imm64, diz que essa instruo aceita um
valor imediato, um valor.
O manual de otimizao da Intel nos diz que essa instruo tem latncia de 1 ciclo de mquina, mas
isso depende se estamos usando registrador ou memria do lado esquerdo. As duas instrues
abaixo tem latncia diferentes:
add rax,1 ; Gasta 1 ciclo
add qword [rdx],1 ; Gasta 2 ciclos

Quando usamos referncia memria um ciclo adicional gasto porque o valor de 64 bits tm que
ser lido, somado ao valor 1 e depois gravado de volta. Isso no acontece quando o operando um

48
registrador.
Existem, ainda, algumas idiossincrasias... As instrues abaixo consomem tempos diferentes,
mesmo que no haja o ciclo de leitura-operao-escrita:
cmp rax,[rsi] ; Gasta 2 ciclos.
cmp [rsi],rax ; Gasta 3 ciclos.

Ambas as instrues comparam (subtraem) o contedo de um registrador (RAX) com o contedo da


memria (apontada por RSI). Mas, j que a segunda instruo faz referncia no primeiro operando,
ela gasta 1 ciclo extra.
Para ter uma ideia intuitiva sobre o gasto de tempo de uma rotina, bom assumir essa regra:
Referncias memria adicionam pelo menos um ciclo de mquina e, se for do lado destino da
instruo, mais um.

Nem todas as instrues gastam tempo


Esse um conceito difcil de aceitar. Mas verdadeiro. Algumas instrues so executadas de tal
maneira, em certas circunstncias, que elas no gastam tempo algum.
H algum tempo os processadores podem executar duas ou mais instrues ao mesmo tempo (e no
estou falando de threads aqui!). Se uma instruo gasta 10 ciclos e outra gasta 1 ciclo, a ltima
parece no ter gastado tempo algum. Se a sequncia abaixo for executada em paralelo:
; As duas instrues gastam 10 ciclos, no total!
imul ebx ; Gasta uns 10 ciclos.
mov ecx,esi ; Gasta s 1 ciclo.

O gasto de tempo da segunda instruo est incorporado no gasto da primeira. Especialmente


porque ela no depende de nada da primeira... No exemplo, IMUL multiplica EAX por EBX e
coloca o resultado no par de registradores EDX:EAX. Note que o MOV que segue usa ECX e ESI.
Outro exemplo de MOV que no gasta tempo o acesso a uma regio da memria mapeada para o
Local APIC. Segundo a documentao da Intel, essa regio mapeada internamente e no consome
nenhum ciclo de leitura por parte do processador... Esses MOVs podem ser considerados como
instantneos.

A pilha e a red zone


Voc ver, no prximo captulo, que variveis locais e temporrias, usadas por suas funes em C,
podem ser reservadas na pilha. Mostrarei como isso funciona por l, mas necessrio explicar um
conceito, usado no modo x86-64, chamado de zona vermelha (red zone).
Esse conceito s existe no modo x86-64 e definido no POSIX ABI... Ele afirma que os 128 bytes
depois do topo da pilha (depois que foi alocado espao para as variveis locais) no pode ser usado
pelo kernel. Essa regra til porque o kernel usa a pilha do processo para manter informaes sobre
a thread a qual ele pertence.
Ao criar essa zona desmilitarizada, digamos assim, o kernel d um espao de trabalho confortvel
para a thread do processo e permite que fragmentos de cdigo chamados prlogo e eplogo sejam
desnecessrios...
No modo x86-64 possvel compilar o kernel para que ele no respeite a red zone, mas isso tem o
potencial de tornar o sistema operacional instvel, no que concerne s threads...

49
Prefixos
Algumas instrues, por causa do uso dos registradores, so codificadas com tamanho maior do que
voc espera. Eis um exemplo:
00000000: 31C0 xor eax,eax
00000002: 4831C0 xor rax,rax

Esse byte 0x48 na frente dos dois bytes que compem um XOR EAX,EAX o prefixo REX. Ele
indica que a instruo usa registradores estendidos. No exemplo acima as duas instrues fazem
exatamente a mesma coisa: zeram RAX.
Outros prefixos existem. Algumas instrues especiais possuem o prefixo 0x0F. Outras so prefixos
de repetio, como REP, REPZ e REPNZ:
00000000: A5 movsd
00000001: F3A5 rep movsd
00000003: F3A5 repnz movsd
00000005: F2A5 repz movsd

O prefixo REPNZ o mesmo usado pelo REP (so a mesma coisa). J o REPZ um prefixo
diferente. Alm do prefixo temos a instruo MOVSD (0xA5).
Existem mais prefixos. Se sua instruo usa um ponteiro e um seletor de segmento diferente de DS,
ela ser prefixada, indicando o seletor usado. O mesmo vale quando usar um seletor diferente de SS
para ponteiros com endereo-base usando RSP ou RBP.
Alm desses prefixos corriqueiros, algumas extenses do processador usam prefixos especiais.
Existe um prefixo XOP (eXtended OPeration), disponvel para extenses AVX. E existem extenses
que so dicas para o processador, raramente usadas. Mas, dois prefixos especiais podem ser muito
frequentes: 0x66 e 0x67.
O processador espera encontrar operandos de 32 bits, nos modos i386 e x86-64. Se voc usar
operandos de 16 bits, como os registradores AX, BX, CX, , ele colocar o prefixo 0x66 (operand
override):
00000000: 66678B06 mov ax,[esi]
00000004: 678B06 mov eax,[esi]

Isso significa que a maioria das instrues que lidam com o tipo short podem ter um byte a mais e,
por esse motivo, o compilador prefere lidar com ints, nem que seja apenas no cdigo final.
O prefixo 0x67 o address override. No modo x86-64, sempre que temos uma indireo como
[ESI], o processador prefere usar registradores de 64 bits, afinal um endereo cannico tem 52 bits
de tamanho. Da, se usssemos [RSI] na instruo acima o prefixo 0x67 no seria usado:
00000000: 8B06 mov eax,[rsi] ; Operandos e endereos do tamanho esperados.
00000002: 678B06 mov eax,[esi] ; Operando de 32 bits, mas endereo em 32 bits!
00000005: 668B06 mov ax,[rsi] ; Operando de 16 bits, mas endereo em 64!
00000008: 66678B06 mov ax,[esi] ; Operando de 16 e endereo de 32!
0000000C: 488B06 mov rax,[rsi] ; Operando de 64 e endereo de 64 (prefixo REX).
0000000F: 67488B06 mov rax,[esi] ; Operando de 64 (REX) e endereo de 32 (0x67).

O importante perceber que se seus operandos foram de 32 bits e os ponteiros de 64, nenhum
prefixo adicionado instruo.
E, por estranho que parea, o uso de operandos de 8 bits no acrescenta prefixos, a no ser que o
outro operando seja uma indireo usando ponteiros de 32 bits:
00000000: 8A06 mov al,[rsi]

Ateno! O prefixo REX no fixado em 0x48. De fato, existe mais que um desses prefixos,

50
dependendo do formato da instruo. Mas, os prefixos 0x66 e 0x67 so fixos e suas semnticas so
as citadas.
A dica aqui a de que voc deve evitar o uso de tipos de 16 ou 64 bits tanto quanto possvel,
mesmo no modo x86-64, para evitar a adio de prefixos REX ou 0x66. O prefixo 0x67 raramente
usado pelo compilador C porque ele respeita o tamanho dos ponteiros. Ao usar um tipo short voc
pode estar poupando espao no cache L1D, mas estar colocando mais presso onde mais interessa,
no seu cdigo (ou seja, no cache L1I)!
Dito isso voc perceber que, mesmo que use o tipo short, o GCC tende a usar registradores de 32
bits atravs de instrues como MOVZX (para unsigned short) e MOVSX (para signed short). Isso
no poupa espao em relao a usar um registrador de 16 bits, que ter o cdigo prefixado com
0x66, mas usar um prefixo 0x0F... O prefixo 0x0F especial. Ele faz parte da instruo e no l
um prefixo e, por isso, no tem o potencial a inserir ciclos na execuo. O GCC, ao encontrar o
cdigo abaixo, tende a gerar cdigo como segue:
short x = *(short *)p;
-----%<----- corte aqui -----%<-----
000000??: 0FB706 movsz eax,word [rsi] ; Assumindo que RSI contm o endereo em p.

Como vimos anteriormente, a instruo MOV AX,[RSI] tem 3 bytes de tamanho, com um prefixo
0x66. A instruo MOV, neste caso, tende a ser mais lenta que MOVSX.
claro que nem sempre o GCC usar essa tcnica. Por exemplo, o cdigo pode precisar da parte
superior de EAX, mas ele tem preferncia por fazer isso.

51
52
Captulo 5: Misturando C e
Assembly
Existem duas formas de misturar funes escritas em C e em assembly. No primeiro modo usando
o que se chama de assembler inline. Assembler (com 'er') significa montador. Inline porque o
cdigo em linguagem assembly (com 'y') ser misturado com o cdigo em C.
O segundo modo usando um assembler externo, como o MASM, GAS ou NASM. Usarei esse
ltimo por sua simplicidade e portabilidade (alm do que, free software).
Antes de comearmos a baguna importante entender como o compilador C organiza as funes
para que possamos usar o mesmo padro com as listagens em assembly, usando um assembler.

Convenes de chamada (x86-64)


Toda funo em C assume uma ordem em que registradores so usados ou dados sero empilhados,
formando os parmetros que a funo receber, bem como quais registradores retornaro valores e
quais devem ser preservados entre chamadas. A isso se d o nome de conveno de chamada.
Na arquitetura x86-64 a conveno usada depende do sistema operacional. No Windows de um
jeito e nos sistemas baseados em POSIX (Linux, OS/X, Unix etc) de outro. No caso dos sistemas
baseados em POSIX a interface binria de aplicaes (ABI, Applications Binary Interface) dita que
os parmetros sejam passados para uma funo usando os registradores RDI, RSI, RDX, RCX, R8 e
R9, nessa sequncia. Se houverem valores em ponto flutuante, os registradores XMM0 at XMM7
devem ser usados, tambm nessa sequncia. Parmetros adicionais so passados pela pilha. Os
valores de retorno so colocados no registrador RAX (ou XMM0, no caso de ponto flutuante). Os
registradores RBP, RBX e os registradores entre R12 at R15 devem ser sempre preservados entre
chamadas.
Com o Windows a coisa no to flexvel. Os registradores RCX, RDX, R8 e R9 so usados, na
sequncia, ou XMM0 at XMM3, em caso de ponto flutuante. Os demais parmetros so passados
pela pilha. Da mesma forma que na POSIX ABI, RAX (ou XMM0, no caso de ponto flutuante)
deve ser usado como valor de retorno. A preservao de registradores, alm dos especificados pela
POSIX ABI, tambm engloba RSI, RDI e os registradores de XMM6 at XMM15.
Arquitetura Parmetros Retorno Devem ser preservados
POSIX ABI RDI, RSI, RDX,
Inteiros RAX
RCX, R8 e R9
RBX, RBP, R12 at R15.
Ponto
XMM0 at XMM7 XMM0
flutuante
Windows Inteiros RCX, RDX, R8 e R9 RAX RBX, RBP, RSI, RDI, R12
Ponto at R15.
XMM0 at XMM3 XMM0 XMM6 at XMM15
flutuante
Tabela 4: Registradores usados na conveno de chamada x86-64

Se houverem mais parmetros do que registradores disponveis, os valores sero empilhados. O


empilhamento feito da direita para a esquerda, seguindo o padro da linguagem C, por causa da
possibilidade de termos um nmero varivel de parmetros numa funo:

53
int f(int x, ...);

No prottipo acima o compilador no tem como saber, de antemo, o nmero de parmetros. Se


chamarmos a funo desta maneira:
f(1,2,3,4,5,6,7,8);

O compilador colocar os primeiros 6 parmetros nos registradores, de acordo com a conveno, e


os dois restantes na pilha. Empilhando31 primeiro o valor 8 e depois o 7:
mov edi,1
mov esi,2
mov edx,3
mov ecx,4
mov r8d,5
mov r9d,6
sub rsp,16 ; Ajusta RSP para empilhar os dois dwords.
mov dword ptr [rsp],7 ; Note que 7 est no topo da pilha.
mov dword ptr [rsp+8],8
call f
add rsp,16 ; Limpa a pilha.

Outro exemplo til para melhor compreenso da conveno de chamada x86-64 quando temos
uma funo assim:
void f(int x, float y, int z);

O parmetro x ser passado atravs do registrador RDI, o y atravs de XMM0 e z pelo RSI. Isso
quer dizer que RSI o segundo parmetro inteiro da sequncia, mesmo que o segundo parmetro
real seja um float.

Convenes de chamada (i386)


Este livro dedicado ao uso do modo x86-64, mas interessante saber como o modo i386 lida com
a conveno de chamada... Na verdade, existem pelo menos 3 delas:
Conveno Descrio
cdecl Todos os argumentos da funo so empilhados da direita para esquerda.
fastcall Os dois primeiros argumentos so passados pelos registradores ECX e EDX,
respectivamente. O restante empilhado da direita para esquerda, como em cdecl.
stdcall Variao da antiga conveno pascal. Os argumentos so passados da direita para
esquerda, pela pilha. Ainda, a funo chamada responsvel por limpar a pilha
antes de sair.
Tabela 5: Convenes de chamada no modo 32 bits.

Assim como na conveno usada no modo x86-64, o registrador EBX deve sempre ser conservado.
A conveno cdecl a conveno padro para qualquer compilador C. As outras, se usadas, devem
ser explicitamente informadas.
No caso do Windows comum observar declaraes como esta:
int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int);

Esse CALLBACK na declarao da funo , certamente um macro que substitudo por __stdcall.
Historicamente, todas as funes da API do Windows, exceto uma, seguem a conveno PASCAL

31 Repare: Mantive a pilha alinhada por qword, colocando os valores de 32 bits em espaos de 64. Isso necessrio
para manter a performance!

54
(ou stdcall). A exceo a funo wsprintf, que aceita nmero varivel de parmetros... O que s
possvel se o empilhamento for feito da direita para esquerda. Assim, as funes da Win32 API
esperam que os argumentos sejam empilhados da esquerda para a direita e que a funo chamadora
limpe a pilha.
Limpar a pilha significa somente colocar o ESP na posio inicial, antes do empilhamento dos
parmetros e da chamada da funo.
Quanto aos valores de retorno, todas as convenes acima usam o registrador EAX e, s vezes,
tambm o registrador EDX. O par EDX:EAX usado quando estamos lidando com tipos com mais
de 64 bits de tamanho.
Para entender a conveno padro cdecl, eis um exemplo:
int f(int a, int b) { return a + b; }

O cdigo gerado pela funo acima pode aparecer de vrias formas. A mais provvel, se a mxima
otimizao for usada, a que vem a seguir:
f: mov eax,[esp+4] ; Pega 'a'.
add eax,[esp+8] ; soma com 'b'.
ret

Lembre-se que sempre que um valor colocado na pilha, o ponteiro de pilha (Stack Pointer)
decrementado e depois o valor gravado no lugar apontado. Como a funo f usa a conveno
cdecl, sabemos que 'b' empilhado primeiro, seguido por 'a'. Depois dos dois empilhamentos a
instruo call usada para chamar a funo, o que causa o empilhamento do endereo de retorno
funo chamadora. por isso que usamos [esp+4] para obter o valor de 'a' (o ltimo valor
empilhado), j que ESP aponta para uma posio da pilha que contm o endereo de retorno...

Figura 9: Pilha, antes e depois de chamar a


funo com a conveno cdecl.

Na figura acima, 'a' o valor contido no argumento a, passado para a funo. Assim como 'b' o
valor do argumento b. A chamada feita assim:
x = f(1,2);
-----%<----- corte aqui -----%<-----
; Cdigo equivalente... note a ordem do empilhamento...
push dword 2 ; Empilha 2
push dword 1 ; Empilha 1
call f
sub esp,8 ; Limpa a pilha.

O cdigo em assembly, acima, apenas um exemplo didtico. mais provvel que seu compilador
faa algo assim:
add esp,8 ; Aloca 2 'ints' na pilha.
mov dword ptr [esp+4],2 ; Coloca os 2 ints na ordem esperada.
mov dword ptr [esp],1
call f ; chama f.
sub esp,8 ; limpa a piha.

55
Outros detalhes dignos de nota so:
O tipo float passado como se fosse um tipo int, ou seja, o contedo binrio da varivel
empilhado no mesmo espao de 32 bits que seria usado por um int. Note que no h uso de
registradores SSE, automaticamente, como na conveno usada pela arquitetura x86-64;
Tipos com 64 bits de tamanho so passados atravs do empilhamento de dois valores de 32
bits. A poro mais significativa empilhada primeiro. O motivo que ao acessar esses
valores via ponteiros, de dentro da funo, a ordem little endian tem que continuar sendo
obedecida;
Tipos com menos de 32 bits (char e short) so convertidos para o equivalente de 32 bits
antes de serem empilhados;
Como no h possibilidade de usar o par de registradores RDX:RAX no modo 32 bits, o tipo
estendido __int128 tende a no estar disponvel.
Neste ponto voc pode se perguntar: Por que a conveno fastcall no a preferida dos
compiladores no modo i386?. Afinal, usar registradores nos dois parmetros iniciais evita, pelo
menos, dois empilhamentos! Acontece que fastcall no usado por nenhum sistema operacional e
os criadores de compiladores esto livres para implementar essa conveno como bem os convier.
o caso do Borland C++ Builder (ou Embarcadero C++ Builder, como preferirem!)... Neste
compilador h a tendncia de usar 3 registradores ao invs de 2: EAX, EDX e ECX, nesta ordem,
numa conveno chamada por eles de register. Como fastcall no , nem de perto, padronizada,
ento prudente evit-la. A no ser que voc saiba realmente o que est fazendo...
De todo modo, a conveno padronizada do modo x86-64 superior s convenes mostradas aqui.
Os pontos de interesse nesse tpico so: A conveno de chamada no modo i386 diferente da
usada no modo x86-64 (o que as torna incompatveis); e, mesmo no modo x86-64, alguns
parmetros podem ser passados pela pilha, do mesmo jeito que na conveno cdecl.

Funes com nmero de parmetros varivel no x86-64


Na conveno cdecl os parmetros so passados pela pilha, mas no x86-64, os 6 primeiros so
passados por registradores. Como que fica o caso de chamadas para funes que possuem nmero
varivel de parmetros? Suponha que tenhamos a declarao de f, abaixo, e uma chamada:
extern void f(int x, ...);

f(1,2,3,4);
-----%<----- corte aqui -----%<-----
extern f

g:
mov ecx,4
mov edx,3
mov esi,2
mov edi,1
call f

Quer dizer, a conveno de chamada x86-64 continua valendo, inclusive para funes com nmero
de parmetros indefinido...

Pilha, nos modos i386 e x86-64


No sei se ficou evidente. Pilhas, nesses dois modos, comportam-se de maneiras diferentes. No
modo i386 cada entrada na pilha tem 32 bits de tamanho. Isso quer dizer que ESP ser
incrementado ou decrementado de 4 em 4 bytes. J no modo x86-64, alm de usarmos o registrador

56
estendido RSP, cada entrada na pilha tem 64 bits de tamanho, ou seja, quando RSP for movido,
ser de 8 em 8 bytes.

Um detalhe sobre o uso de registradores de 32 e 64 bits


Na arquitetura x86-64 os registradores de 32 bits so um subconjunto dos registradores de 64 bits.
EAX, por exemplo, a poro dos 32 bits menos significativos do registrador RAX. No caso dos
registradores estendidos, R8 at R15, para acessar os 32 bits inferiores basta usar um sufixo: R8D
at R15D, onde 'D' significa DWORD. Se usar o sufixo 'W' estaremos acessando os 16 bits menos
significativos 'W' de WORD.
O detalhe que quero mostrar que, ao usar EAX, ao invs de RAX, os 32 bits superiores do
registrador de RAX so automaticamente zerados. Isso se deve ao fato que a instruo que lida com
EAX a mesma que lida com RAX. Por exemplo:
; test.asm
bits 64
section .text
global f:function

f:
mov rax,1
mov eax,1
ret
-----%<----- corte aqui -----%<-----
$ nasm -f elf64 -l test.s -o test.o test.asm; cat test.s
1 bits 64
2 section .text
3
4 global f:function
5
6 f:
7 00000000 B801000000 mov rax,1
8 00000005 B801000000 mov eax,1
9 0000000A C3 ret

Repare que o microcdigo (op) das duas instrues MOV so idnticas. Elas s sero diferentes se
RAX for inicializado com um valor maior que 32 bits.
Claro que o que se aplica a EAX e RAX, aplica-se a todos os outros registradores de uso geral
(RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8 at R15).

Exemplo de funo em assembly usando a conveno de chamada


Eis as listagens de uma simples funo, em C e o cdigo gerado pelo compilador, em Assembly:
/* simple.c */
int f(int x, int y, int z)
{
return x * y + z;
}
-----%<----- corte aqui -----%<-----
; simple.s gerado pelo compilador
; Entrada:
; RDI = x
; RSI = y
; RDX = z
; Sada:
; EAX
f:
imul edi,esi ; RDI = EDI*ESI (x = x*y)
lea eax,[rdi+rdx] ; EAX = EDI+EDX (return x+z)
ret

Ao ser chamada pelo cdigo em C, a funo 'f' receber o valor de 'x' pelo registrador RDI, 'y' pelo

57
RSI e 'z' pelo RDX, de acordo com a conveno. RAX usado para devolver o resultado que, no
caso, do tipo 'int'. Ento a rotina preocupa-se apenas com a DWORD menos significativa de RAX
(ou seja, EAX). O compilador usar EDI, ESI e EAX porque especificamos o tipo 'int'.
Visual C++ usar a conveno da Microsoft, substituindo RDI por RCX, RSI por RDX e o RDX
por R8:

f proc near
imul ecx,edx
lea eax,[rcx+r8]
ret
f endp

Aviso sobre retorno de funes com tipos complexos


Funes podem retornar tipos complexos como structs e unions, mas no uma boa idia retornar
esses tipos por valor... Considere o que acontece quando retornamos valores de tipos primitivos
como int, por exemplo...
O valor retornado uma cpia do valor contido dentro da funo. No cdigo:
int f(int x)
{
int temp = x + x;

return temp;
}

No estamos retornando a varivel temp, mas uma cpia de seu conteudo, para o chamador. A
mesma coisa acontece com estruturas retornadas por valor, como no exemplo:
struct myStruc {
int count;
int array[5];
};

struct myStruc f(int x)


{
struct myStruc ms = { x, { 0, 1, 2, 3, 4 } };

return ms;
}
-----%<----- corte aqui -----%<-----
f:
mov rax,rdi
mov [rdi],esi ; ms.count = x;
mov [rdi+4],0
mov [rdi+8],1
mov [rdi+12],2
mov [rdi+16],3
mov [rdi+20],4
ret

Repare: A funo continua retornando RAX, mas, desssa vez, retorn um ponteiro para a estrutura
que dever ter sido alocada pela funo chamadora de f. Um ponteiro para essa estrutura passado
pelo registrador RDI, da mesma forma que seria feito se a funo fosse escrita assim:

58
struct myStruc *f(struct myStruc *p, int x)
{
p->count = x;
p->array[0] = 0;
p->array[1] = 1;
p->array[2] = 2;
p->array[3] = 3;
p->array[4] = 4;
return p;
}

Este parece ser um recurso interessante, mas d uma olhada no cdigo gerado pela rotina abaixo:
struct myStruc {
int count;
int x[512];
};

struct myStruc f(int x)


{
int i;
struct myStruc ms = { x };

for (i = 0; i < 512; i++)


ms.x[i] = x+1;

return ms;
}
-----%<----- corte aqui -----%<-----
f:
push rbp
mov edx,2052
mov ebp,esi ; Guarda o parmetro x.
push rbx
xor esi,esi ; Vai preencher a estrutura com zeros.
mov rbx,rdi ; Guarda o ponteiro da estrutura escondida.
sub rsp,2072 ; Aloca espao para a estrutura na pilha.
mov rdi,rsp
call memset

lea r8d,[rbp+1] ; r8d = x + 1;


lea rdx,[rsp+4] ; rdx aponta para o ncio de x[].
lea rcx,[rsp+2052] ; rcx aponta para o fim de x[].
.L1:
mov [rdx],r8d ; escreve em x[i].
add rdx,4 ; avana i.
cmp rdx,rcx ; chegou ao fim?
jne .L1 ; no? Continua preenchendo.

mov rsi,rsp ; rsi aponta para a estrutura na pilha.


mov rdi,rbx ; rdi aponta para estrutura escondida.
mov edx,2052 ; Vamos copiar 2052 bytes.
mov [rsp],ebp ; Escreve o parmetro x em count.
call memcpy ; Faz a cpia.
add rsp,2072 ; Limpa a pilha.

mov rax,rbx ; Retorna o ponteiro para a estrutura escondida.


pop rbx
pop rbp
ret

Continuamos obtendo RAX como resposta, contendo o ponteiro escondido passado para a funo,
mas repare como ela ficou tremendamente mais complicada do que deveria! A funo aloca
2052 bytes (os 512 int's do array e o count, da estrutura) mais 20 bytes (?!). Ela preenche todo o
array com zeros (usando memset), coloca em R8D o valor de x, passado para a funo e usa RDX e
RCX nas iteraes do loop, preenchendo o contedo da pilha com os valores desejados... Logo
depois, memcpy copia toda a estrutura para a regio da memria dada pelo ponteiro escondido e
retorna esse ponteiro em RAX.
E esse um cdigo simples! s vezes o compilador se v obrigado a fazer uso do seletor de
segmento FS para conter um endereo base e torna todo o cdigo praticamente ilegvel, sem contar

59
pouco performtico!
Agora, compare o cdigo anterior com este:
struct myStruc *f(struct myStruc *ptr, int x)
{
int i;

ptr->count = x;
for (i = 0; i < 512; i++)
ptr->x[i] = x+1;
return ptr;
}
-----%<----- corte aqui ------%<-----
f:
mov rax,rdi
mov [rdi],esi
xor edx,edx
lea ecx,[rsi+1]
.L1:
mov [rax+4*rdx],ecx
add rdx,4
cmp rdx,2048
jne .L1
ret

Este cdigo faz a mesma coisa (literalmente) que o anterior sem preencher o array x com zeros e
sem fazer uma cpia para uma estrutura escondida nos parmetros. Ela menor e mais rpida!
Deixo essa dica: Jamais retorne estruturas e unies por valor!

O uso da pilha
A pilha usada para receber parmetros que no caibam num registradores ou se a quantidade de
parmetros for maior que uma certa quantidade, de acordo com a conveno de chamada. Repare
que, para POSIX ABI, existem 5 registradores que suportam parmetros inteiros e 8 registradores
que suportam parmetros em ponto flutuante. Se, por exemplo, tivssemos uma funo declarada
como:
int f(int, int, int, int, int, int, float);

Os primeiros cinco parmetros sero passados via RDI, RSI, RDX, R8 e R9, mas o sexto 'int' ser,
necessariamente, colocado na pilha. O ltimo parmetro ser passado por XMM0.
Outro uso para a pilha o armazenamento temporrio e de variveis locais (que no caibam nos
registradores disponveis para a rotina).
Quando o compilador tenta preservar valores contidos em registradores na pilha e recuper-los
depois, raramente usar instrues como PUSH e POP. O motivo que essas instrues tomam,
pelo menos, um ciclo de mquina adicional na manipulao do registrador RSP. Suponha que o
compilador queira preservar RBX, RCX e RDX na pilha para recuper-los depois. O cdigo no
otimizado padro poderia ser mais ou menos assim:
push rbx /* salva RBX, RCX e RDX na pilha. */
push rcx
push rdx

pop rdx /* Recupera RBX, RCX e RDX da pilha. */
pop rcx
pop rbx

Cada uma das instrues PUSH subtrai 8 do registrador RSP e depois move o contedo do
registrador sendo empurrado, usando RSP como ponteiro. O cdigo equivalente do primeiro
PUSH seria mais ou menos assim:

60
sub rsp,8
mov [rsp],rbx

No caso no cdigo que precisa empurrar vrias coisas para a pilha (e depois pux-las de volta),
o compilador geralmente resolve fazer a subtrao (e depois a adio) uma s vez:
; Ajusta o RSP para 24 bytes 'para baixo'. Note que 24
; 3 vezes o tamanho de cada um dos registradores.
;
; O motivo de serem guardados 'de trs para frente' que
; a pilha cresce 'para baixo'.

sub rsp,24
mov [rsp+16],rbx
mov [rsp+8],rcx
mov [rsp],rdx

mov rdx,[rsp]
mov rcx,[rsp+8]
mov rbx,[rsp+16]
add rsp,24

Em teoria, a primeira rotina, com os PUSHs e POPs, gastaria cerca de 12 ciclos, enquanto a rotina
acima gastaria apenas 8.

Variveis locais e a pilha


Vimos como os argumentos de uma funo so passados, seja por registradores, seja pela pilha, mas
e quanto as variveis locais?
Se voc compilar seu cdigo com algum nvel de otimizao, o compilador tentar manter variveis
locais dentro de registradores o tempo todo. Nem sempre ele consegue. Da, a pilha tambm usada
para armazenar essas variveis. A pilha usada porque, quando a funo termina, feita a
limpeza da pilha. Isso significa que as variveis locais tambm estaro perdidas para a funo
chamadora.
Segundo as convenes de chamada, os argumentos so empilhados (de trs para frente, por
default) seguido do endereo de retorno, quando for feito um call. Cada empilhamento feito
decrementando o registrador RSP e usando-o como ponteiro para armazenamento do valor
empilhado... As variveis locais so alocadas depois do endereo de retorno. Se [RSP+4] a
derreferncia ao ponteiro para o primeiro argumento da funo, na pilha, [RSP-4] a derreferncia
uma varivel local, alocada na pilha. Considere o seguinte fragmento de cdigo:
#include <stdio.h>

extern int g(void);

void f(void)
{
int x = g(); // Chama g() para inicializar x.

printf("%p\n", &x); // Note que tomamos o endereo de x aqui.


}
-----%<----- corte aqui -----%<-----
.section .data

fmt:
db '%p',10,0

.section .text

extern g
extern printf

61
f:
call g
lea rsi,[rsp-4] // Vamos passar o ponteiro para pritnf.
// Ento, coloca-o em RSI (2 argumento de printf).
mov [rsi],eax // EAX contm o resultado retornado por g().
// Armazena EAX na varivel local, na pilha.
mov rdi,fmt // RDI o primeiro argumento de printf().
call printf // Chama printf().
ret

No exemplo acima, o resultado da chamada funo g(), no registrador EAX, armazenado na


pilha, em [RSP-4], porque a funo printf precisa do ponteiro da varivel local x. Ainda, segundo a
conveno de chamada, os dois argumentos de printf devem ser passados, pela ordem, em RDI e
RSI. Por isso calculei RSI antes de us-lo como ponteiro.
Como regra geral, toda varivel local que seu cdigo precisa acessar atravs de ponteiros colocada
na pilha...
Mesmo para aqueles casos onde o compilador consegue manter as variveis locais em registradores,
a regio de armazenamento local (abaixo do endereo de retorno) pode ser usado como
armazenamento temporrio. Suponha que sua funo use R12 e a funo chamada tambm o use.
necessrio guardar o contedo de R12 no espao local, chamar a funo e depois recuper-lo:
mov [rsp-8],r12
call func
mov r12,[rsp-8]

A conveno de chamada usada pela arquitetura x86-64 prev que alguns registradores podem ser
descartados entre chamadas, mas outros devem ser preservados (lembra?). O artifcio de usar a
regio de armazenamento local da pilha um jeito de fazer isso. Outro jeito seria manter variveis
globais no segmento de dados, o que aumenta o tamanho do cdigo e tem a possibilidade de poluir
os caches32.

O compilador no usa todas as instrues


Se voc der uma olhada no volume 2 dos manuais de desenvolvimento de software da Intel para os
modos i386 e x86-64 poder encontrar instrues como XLAT, JECXZ, XCHG, LOOP e outras
com nomes interessantes. Algumas parecem bem teis, mas depois de olhar para muitas listagens
em assembly geradas pelo seu compilador C, perceber que ele no as usa... NUNCA! O motivo?
Performance.
Algumas dessas instrues mais especializadas so realmente lentas. Mais lentas do que um cdigo
que faz a mesma coisa usando instrues mais discretas. Nos exemplos abaixo, a primeira rotina
mais rpida que a segunda:
; primeira rotina...
.L1:
; faz alguma coisa aqui
sub rcx,1
jnz .L1
-----%<----- corte aqui -----%<-----
; segunda rotina...
.L2:
; faz alguma coisa aqui
loop .L2

A instruo LOOP faz exatamente a mesma coisa que SUB/JNZ acima mas, surpreendentemente,
mais lenta!

32 Sobre caches, veremos mais frente.

62
A listagem abaixo mostra outro caso onde uma instruo especializada era mais lenta que a
sequncia de instrues mais discreta:
; primeira rotina...
bzero:
mov rcx,rsi
xor al,al
.L1:
mov [rdi],al
inc rdi
dec rcx
jnz .L1
ret
-----%<----- corte aqui -----%<-----
; segunda rotina...
bzero:
mov rcx,rsi
xor al,al
rep stosb
ret

Como no caso anterior, a primeira rotina faz a mesma coisa que a segunda. Mas a primeira era mais
rpida. Somente nas arquiteturas mais recentes (se no me engano, a partir da Sandy Bridge), o uso
do prefixo REP com as instrues de manipulao de blocos STOSB e MOVSB ficaram mais
rpidas...
A instruo XCHG outro caso interessante... Aparentemente ela bem til: Troca o contedo entre
dois registradores (ou registrador e memria). As duas listagens parciais, abaixo, seriam
equivalentes:

; usando xchg para trocar RAX pelo contedo de memria
xchg rax,[var1]

-----%<----- corte aqui -----%<-----

mov rbx,[var1]
mov [var1],rax
mov rax,rbx

Exceto que a segunda listagem usa o registrador RBX como armazenamento temporrio e a
primeira no o faz. Nesse caso XCHG parece ser bem atraente, pois faz o que 3 MOV fazem, sem
usar um registrador extra. Se fosse s isso ela seria, de fato, muito interessante!
Acontece que XCHG, quando um dos parmetros uma referncia memria, tambm modifica o
estado do sinal LOCK# da CPU. Este sinal informa os outros processadores que o barramento de
endereos e dados esto travados e no podem ser modificados. Isso garante que XCHG ir ler-
modificar-gravar a memria sem interferncias de outros processadores. Com isso, a instruo gasta
mais ciclos de mquina do que deveria, ficando mais lenta que os 3 'MOV's.
xchg rax,[rdi] ; Essa instruo usa o prefixo LOCK automaticamente.
xchg rdx,rcx ; Essa instruo no usa o prefixo LOCK automtico.

XCHG muito boa quando temos que fazer trocas entre registradores. Estranhamente o compilador
nunca a usa!
Existem ainda instrues mais complexas que no usadas pelo compilador: Seja pela baixa
performance, pela inutilidade ou pelo uso muito especfico (existem instrues que s funcionam
no ring 0, por exemplo).

Detalhes interessantes sobre a instruo NOP e o prefixo REP


NOP a instruo que no faz nada, s gasta tempo. Ela bastante usada para alinhar cdigo.

63
Tipicamente NOP uma instruo que gasta apenas um byte no segmento de cdigo, mas a Intel a
estendeu para que possa usar mais espao, se necessrio, gastando a mesma quantidade de ciclos de
clock.
Esses novos NOPs permitem operadores e so conhecidos como hinted nops. Eis exemplos da
listagem obtida pelo NASM33:
1 bits 64
2 section .text
3
4 00000000 90 nop
5 00000001 660F1F00 nop word [rax]
6 00000005 660F1F0400 nop word [rax+rax]
7 0000000A 660F1F040500000000 nop word [nosplit rax+0]
8 00000013 0F1F00 nop dword [rax]
9 00000016 0F1F0400 nop dword [rax+rax]
10 0000001A 0F1F040500000000 nop dword [nosplit rax+0]
11 00000022 480F1F00 nop qword [rax]
12 00000026 480F1F0400 nop qword [rax+rax]
13 0000002B 480F1F040500000000 nop qword [nosplit rax+0]

Todas essas variaes gastam apenas um nico ciclo de clock, mas tm tamanhos diferentes. Repare
que podemos usar NOPs de 1, 3, 4, 5, 8 e 9 bytes. Algumas dessas verses usam ou o prefixo 0x66
ou o prefixo 0x48. O prefixo 0x66 diz ao processador que operando ter 16 bits de tamanho.
O prefixo REX (entre 0x48 e 0x4f) usado no modo de 64 bits para indicar que o operando tem, de
fato, 64 bits de tamanho. O tamanho default de operandos, na arquitetura x86-64, DWORD (32
bits). Por isso NOP DWORD [RAX] no tem prefixo REX, mas NOP QWORD [RAX] tem.
No se preocupe com o prefixo REX ou o prefixo 0x66. Os compiladores tomam conta disso...
Para obter NOPs de 2 bytes podemos usar a instruo XCHG:
xchg ax,ax ; Isso ser traduzido para 66 90, no micro-cdigo.
; como se colocssemos o prefixo 66 antes do NOP.

Obter NOPs de 6 e 7 bytes de tamanho mais complicado, mas possvel.


De fato, o NASM implementa alguns apelidos para esses tipos de NOPs: HINT_NOPn, onde n pode
ser um valor entre 1 e 63.
Outra instruo que pode ser usada para emular um NOP LEA:
lea rax,[rax]
lea rax,[nosplit rax+0]

Embora essas instrues tendam a realizar uma operao aritmtica (sem afetar quaisquer flags), o
processador percebe que nada est sendo feito, de fato, e elas s gastaro 1 ciclo de mquina.
Quanto ao prefixo REP, s vezes, o compilador o coloca na frente de instrues onde ele no se
aplica, como no exemplo:
rep ret

O que o compilador tenta fazer aqui tambm alinhamento. Se ele usasse NOP para alinhar o RET,
gastaria um ciclo de mquina adicional desnecessariamente... Ao usar REP, que s funciona com
instrues de manipulao de bloco (MOVS, STOS, LODS, CMPS) e no afeta quaisquer outras,
ele evita isso.

33 O modificador nosplit no endereo efetivo indica ao NASM que este no dever otimizar a expresso.

64
LOOP e LOOPNZ so diferentes, mas REPNZ e REP so a mesma coisa!
E, por falar em manipulao de blocos, existem dois tipos de prefixo REP: Um que testa se o flag
ZF zero e outro se ele um. Ao usar REP voc est usando REPNZ... Mas, cuidado que com
instrues que no afetam ZF, usa-se REP.
Usando as instrues de bloco com REPNZ ou REPZ o processador vai testar o contedo do
registrador RCX contra zero, para determinar a quantidade mxima de repeties. Depois ele vai
executar a instruo, decrementar RCX, incrementar/decrementar (dependendo do flag DF) RSI
e/ou RDI (dependendo da instruo), testar o flag ZF e repetir tudo de novo, dependendo do
prefixo. A instruo abaixo, pode ser escrita, em pseudo-cdigo, como:
repnz scasb
-----%<----- corte aqui -----%<-----
/* pseudo-cdigo */
extern struct flags eflags;

void repnz_scasb(unsigned long rcx, void *rdi, char al)


{
while (rcx != 0)
{
rcx--;

eflags.zf = 0;
if (*rdi == al)
{
eflags.zf = 1;
break;
}

if (!eflags.df)
rdi++;
else
rdi--;
}
}

LOOP, LOOPZ e LOOPNZ, por outro lado, so diferentes entre si. A primeira instruo testa
somente se RCX zero ou no, decrementando-o antes de saltar... As outras duas testam o contedo
do flag ZF antes de decrementarem RCX.

Assembly inline
Dentro de seu cdigo em C voc pode usar cdigos em assembly. Isso funciona na maioria dos
compiladores C/C++, mas a sintaxe pode variar de compilador para compilador. Como estamos
usando o GCC, a sintaxe pode parecer um tanto confusa:
__asm__ __volatile__ (
"string_com_cdigo_asm"
: [sada]
: [entrada]
: [registradores modificados]
);

A palavra reservada __asm__ bastante bvia, mas porque esse bloco deve ser marcado como
__volatile__? Acontece que mesmo um cdigo em assembly pode ser otimizado pelo GCC. Ao
marcar o cdigo como volatile dizemos ao compilador que este no deve realizar otimizaes.
Mas, ateno! Marcar o bloco como volatile apenas um pedido ao compilador que ele pode
ignorar.
Outra coisa: Voc pode usar as palavras reservadas asm e volatile sem os underscores:
asm volatile ( "string" : [sada] : [entrada] : [registros preservados] );

65
S uso __asm__ e __volatile__ porque a documentao do GCC assim sugere que se faa, para
evitar conflitos de namespace. Mas, se voc der uma olhada no cdigo fonte do kernel do Linux,
por exemplo, ver essa ltima forma, visualmente mais confortvel, sendo usada...
Em relao aos parmetros contidos no bloco, a string com cdigo asm uma grande string
contendo, obviamente, o cdigo em assembly. A separao de linhas pode ser feita com um '\n' ou
';'. Particularmente, prefiro o ltimo, mesmo que ele bagunce um pouco as listagens em assembly
obtidas a partir do GCC... S necessrio tomar cuidado com a plataforma onde esses cdigos
assembly inline sero compilados... O GAS (assembler usado para compilar o assembly inline) usa
';' como incio de comentrio em algumas plataformas. Assim, usar '\n' mais seguro.
Toda funo tm parmetros de entrada, valores de retorno e recursos que precisam ser preservados.
Um cdigo em assembly inline no diferente. Depois da string contendo o cdigo em si, seguem
trs listas opcionais. Na primeira temos uma lista os valores de sada, na segunda os valores de
entrada e na terceira a lista os registradores que devem ser preservados.
Cada item da listas de valores de entrada e de valores de sada contm uma string que descreve
como o parmetro dever ser interpretado. Por exemplo:
unsigned int lo, hi, val;


__asm__ __volatile__ (
"movl %2,%%eax;"
"movl %%rax,%%rdx;"
"shrl 32,%%rdx"
: "=a" (lo), "=d" (hi) /* lista de sadas */
: "r" (val) /* lista de entradas */
: "rbx", "rcx" /* lista de preservao. */
);

Aqui temos a lista de sada que descreve que o valor em EAX (definido pela string =a) ser
colocado na varivel 'lo' (porque 'lo' definida como 'unsigned int') e o valor em EDX ser
colocado na varivel 'hi' (veja o =d), tambm dependendo do tamanho de 'hi'.
Segue a lista de variveis de entrada, que diz ao compilador que o contedo da varivel 'val' ser
colocado em um registrador, escolha do GCC (graas string r), antes da execuo do cdigo
em assembly.
E, finalmente, a lista de registradores que devem ser preservados. No caso, RBX e RCX. Um aviso
necessrio quanto a essa lista de preservao... O GCC no sabe quais registradores sero
modificados dentro da string do cdigo no assembly inline, mas ele sabe que precisar usar os
registradores e/ou memria definidos nos descritores de sada e entrada. Ento, na lista de
preservao precisamos listar apenas aqueles registradores que nosso cdigo altera que no
estejam nas outras duas listas.
Alm de preservao de registradores, existem outros dois registradores especiais que podem ser
usados nesse pedao do bloco do assembly inline. Trata-se de cc e memory. O pseudo
registrador cc , na verdade, o registrador EFLAGS. J memory uma dica ao compilador
quando o seu cdigo faz alteraes na memria que no so previsveis, do ponto de vista do
compilador... Por exemplo, se voc usar instrues de bloco como SCAS ou MOVS, prudente
colocar memory na lista de preservao.
Os descritores de entrada e sada so meio estranhos, no? Os caracteres usados nessas strings
variam de processador para processador, mas, de forma geral, para a arquitetura Intel (tanto i386
quanto x86-64) a tabela abaixo mostra os principais:

66
Descritor Significado
a, b, c, d, D e S Respectivamente os registradores EAX, EBX, ECX, EDX, EDI e ESI ou seus
derivados (de 16 ou 32 bits).
r Um registrador qualquer, escolha do compilador.
m Uma referncia memria
g Registrador, memria ou constante. O compilador escolhe.
i ou n Valor inteiro (imediato).
f Um registrador do x87, escolhido pelo compilador.
t O registrador st(0) do x87.
u O registrador st(1) do x87.
A O par de registradores EDX:EAX ou DX:AX34.
x Registrador SSE escolha do compilador.
Yz O registrador XMM0.
Tabela 6: Lista dos principais descritores de entra/sada para assembly inline

No caso da lista da descrio de sada, se usada, necessrio colocar um = ou um + antes do


descritor. No caso de usar um =, dizemos ao compilador que o descritor ser usado apenas para
gravao (apenas como sada). O + diz que o descritor pode tambm ser lido pelo cdigo em
assembly. Normalmente usamos apenas =.
Descritores como 'r', 'g' e 'x' so interessantes porque permitem que voc abstraia seu cdigo do uso
de um registrador especfico, deixando o compilador escolher qual usar.
No exemplo dado anteriormente, o cdigo assembly receber o contedo da varivel 'val' em um
registrador qualquer, escolha do compilador; ao terminar, o contedo de EAX ser colocado em
'lo' e EDX em 'hi'; e os registradores RBX e RCX sero preservados (se necessrio!). simples
assim.
O compilador sabe que deve usar EAX ao invs de RAX, AX, AH ou AL, por causa do tamanho da
varivel associada ao descritor. No exemplo a varivel 'val' do tipo int e, portanto, EAX
suficiente para comport-la. Assim, os descritores a, b, c, d, D e S selecionam um dos
registradores de uso geral de acordo com o tamanho e tm esses nomes genricos.
Um meio de acessar, dentro do cdigo assembly, um desses parmetros descritos atravs do
nmero de ordem em que o descritor aparece em ambas as listas. Pense nesses descritores como se
eles estivessem num array... No exemplo anterior o descritor associado com a varivel 'lo' tem a
posio 0 (zero), a posio 1 a do descritor associado varivel 'hi' e o descritor de entrada,
associado com a varivel 'val' tem a posio 2.
Nosso cdigo obtm o valor da varivel 'val' assim:
movl %2,%%eax;

Esse '%2' diz ao assembler inline que deve colocar, em seu lugar, o contedo do item 2 da lista de
descritores. Note que o descritor declarado como r e, portanto o compilador far de tudo para
substitu-lo por um registrador.
Esse macete o responsvel pelo fato de registradores terem que ser prefixados com dois '%'.

34 Note que 'A' no lida com o par RDX:RAX.

67
Alm do uso do ndice de um descritor dentro do cdigo assembly, podemos usar esses ndices nos
prprios descritores. Por exemplo: Suponha que queiramos multiplicar um valor por 13, em
assembly. Poderamos fazer algo assim:
long val, result;

__asm__ __volatile__ (
"cqo;"
"movl $13,%%ecx;"
"idiv %%rcx"
: "=a" (result) /* A sada ser copiada de RAX para result.
: "0" (val) /* A entrada ser copiada de val para RAX.
: "rcx" /* Informa que RCX foi alterado na rotina tambm!
);

Eu disse que os descritores so strings, certo? E strings podem ter mais que um caracter... possvel
refinarmos o descritor combinando as descries acima com outras:
Descritores adicionais Significado
I Inteiro na faixa de 0..31.
J Inteiro na faixa de 0..63
K Inteiro na faixa de -128..127 (8 bits)
N Inteiro na faixa de 0..255 (8 bits)
G Constante de 80 bits (x87)
C Constante (ponto flutuante) para ser usada em instruo SSE
e Constante de 32 bits sinalizada.
Z Constante de 32 bits sem sinal.
Tabela 7: Descritores adicionais que podem ser combinados com outros.

Assim, poderamos usar um descritor dN como entrada que um valor pode ser ser compilado
como o registrador DL (ou DH) ou um valor inteiro. o caso de:
__asm__ __volatile__ ( "outb %%al,%1" : : "a" (value), "dN" (port) );

Se fizermos port ser 0x70, por exemplo, o compilador pode escolher compilar a instruo acima
como OUT 0x70,AL ao invs de OUT DX,AL. Ento, fique atento: Os descritores no somente
informam entradas e sadas, mas tm o potencial de modificar o prprio cdigo em assembly...

Operador de indireo (ponteiros) em assembly


Assim como em C, assembly tambm possui uma sintaxe particular para lidar com ponteiros. Vale
lembrar dois detalhes: Primeiro, ponteiro s um outro nome para endereo e, em segundo
lugar, o operador de indexao de arrays '[]', em C, apenas uma notao diferente para lidar
tambm com ponteiros. Com assembly a mesma coisa. O operador de indireo do assembly '[]'.
Isso particularmente importante quando usamos o NASM. As duas declaraes abaixo, que fazem
referncia mesma varivel, tm significados diferentes:
mov rax,var1 ; RAX receber o endereo de 'var1'.
mov rax,[var1] ; RAX receber o valor contido no endereo de 'var1'.

Essa diferena fica evidente quando usamos um registrador como ponteiro. Suponha que RDI tenha
o endereo de uma varivel. Para acessar o contedo desse endereo temos que usar o operador de
indireo '[]' cercando o nome do registrador:

68
mov rax,[rdi] ; RAX receber o 'long' cujo endereo dado por RDI.

Usando a sintaxe Intel no assembly inline do GCC


Por default o GCC suporta o estilo AT&T da linguagem assembly. Neste estilo, a ordem dos
operadores numa instruo invertida. Ainda, a instruo em si carrega um modificador dizendo o
tamanho dos operandos. Por exemplo:
movl $4,%eax ; a mesma coisa que 'mov eax,4'.

O 'l', depois de 'mov', significa long. O valor 4, precedido de um '$', uma constante que ser
colocada no EAX.
Outra chatice do estilo AT&T a notao usada para referencias memria. Ao invs de usar o
operador de indireo '[]' e a notao Intel:
[BASE + NDICE*MULTIPLICADOR + DESLOCAMENTO]

Que parece-me ser mais intuitiva que o padro usado no sabor AT&T, que usa notao diferente.
Algo assim:
DESLOCAMENTO(BASE, NDICE, MULTIPLICADOR)

Veja a diferena nas duas instrues, idnticas:


mov rax,[rbx + rsi*2 + 3] ; padro Intel.
movq 3(%rbx,%rsi,2),%rax ; padro AT&T

A segunda instruo requer mais ateno para ser entendida do que a primeira, no? Suspeito que a
sintaxe AT&T deriva do padro do assembly dos antigos processadores MC68000.
Para usar o estilo Intel, parecido com o estilo usando no NASM e em outros assemblers, no
assembler inline do GCC, basta colocar seu cdigo entre as diretivas .intel_syntax e
.att_syntax. Eis um exemplo simples que copia uma varivel para outra:
unsigned v1=3, v2;

/* Estilo AT&T (default) */


__asm__ __volatile__ (
"movl %1,%%eax;"
"movl %%eax,%0;"
: "=g" (v2) : "g" (v1) : "rax"
);

/* Estilo Intel */
__asm__ __volatile__ (
".intel_syntax;"
"mov eax,%1;"
"mov %0,eax;"
".att_syntax;"
: "=g" (v2) : "g" (v1) : "rax"
);

necessrio dizer ao assembler inline que o estilo AT&T deve ser retomado, seno voc obter
erros de compilao depois do bloco __asm__.

Problemas com o assembler inline do GCC


Lembre-se que o GCC tende a otimizar todo o cdigo que ele traduz, inclusive o cdigo contido no
bloco __asm__. Tentamos evitar isso com o uso de __volatile__, mas nem sempre o compilador nos
obedece. Isso pode ser bastante problemtico para aqueles que gastaram horas refinando uma rotina
para tentar obter a menor quantidade de ciclos de mquina possvel e verem seu trabalho jogado no

69
lixo pelo compilador (ou, pior, nem perceber que o compilador te sacaneou!).
Repare que o prprio esquema de passagem de parmetros para o bloco __asm__ implica que
alguma otimizao ser feita, quer voc queira, quer no.
Todo o sentido de ter um assembler inline que voc pode colocar seus cdigos j otimizados, em
assembly, junto com seu cdigo C, observando as regras do compilador... Se o compilador vai
otimizar seu cdigo de qualquer maneira, ento, pra que usar assembly?
Recomendo que mantenha seus cdigos com a menor quantidade possvel de assembly inline. Se
quiser colocar uma rotina em assembly no seu programa, use um assembler externo como o NASM.
Reserve o assembler inline para aquelas rotinas que fazem algo que o compilador no faria com
facilidade. Um exemplo o cdigo fonte do kernel do Linux: Assembly inline s usado l para
cdigos que o compilador normalmente no gera. Por exemplo, usando instrues que s esto
disponveis no nvel mais privilegiado.

Usando NASM
Ao invs de usar o assembler inline, criar nossas rotinas em assembly em um cdigo fonte separado,
oferece tambm suas vantagens. Em primeiro lugar, no h dependncia dos recursos de otimizao
do GCC (ou de seu compilador C/C++ preferido). Outra coisa interessante que o NASM cross
platform. Isso quer dizer que podemos criar cdigos em assembly para serem usados no Linux,
OS/X, Windows e outros ambientes com pouca ou nenhuma modificao no cdigo original35.
A preferncia pelo NASM que ele um desses compiladores free software que esto
disponveis em qualquer plataforma (assim como o GCC) e oferece uma sintaxe parecida, com
diferenas interessantes, em relao ao MASM (Macro Assembler). Existem outros sabores de
assemblers, como o YASM e o FASM, bem como o MASM e TASM (esses ltimo, disponveis
apenas para Windows!). Voc pode preferir usar o GAS (GNU Assembler), mas prepare-se para a
confuso da sintaxe AT&T. Prefiro uma sintaxe mais parecida possvel com a da Intel.
Um cdigo escrito para o NASM bem simples. Existem diretivas para controle do compilador e
do cdigo em si. Um programa tpico, para o NASM mais ou menos assim:
; triple.asm
bits 64 ; Vamos usar x86-64.
section .text ; Aqui comea o 'segmento' de cdigo.

global f:function ; O label 'f' ser exportado como ponto de entrada de uma funo.

; A funo abaixo equivale a:


; int f(int x) { return 3*x; }
f:
mov eax,edi
add eax,eax
add eax,edi
ret

Basicamente existem quatro tipos de sections ou segmentos: Cdigo, dados inicializados,


dados no inicializados e constantes. Estes so especificados na diretiva SECTION (ou
SEGMENT) como '.text', '.data', '.bss' ou '.rodata', respectivamente. A sigla BSS originria de
Block Started by Symbol. Isso quer dizer que o segmento .bss no faz parte da imagem binria do
seu programa contido no arquivo em disco... Esse segmento criado e preenchido com zeros pela
biblioteca libc... Dados no inicializados, em C, so sempre inicializados com zeros!
Diferente de outros assemblers, no existem diretivas especficas para demarcar blocos de
procedimentos ou funes alm da declarao da SECTION .text. No MASM (Macro Assembler) a
35 Isso no l muito verdadeiro. Sistemas operacionais tm esquemas diferentes. Por exemplo, como j vimos, na
conveno de chamada!

70
listagem acima seria escrita mais ou menos assim (para i386):
.586p
.model flat,stdcall

.code

; Exporta o procedimento 'f'.


public f

; No MASM necessrio usar PROC e ENDP para demarcar uma funo.


f proc near
mov eax,edi ; RDI o primeiro parmetro tambm na conveno da Microsoft!
add eax,eax
add eax,edi
ret
f endp

end

Note os proc near e endp...


No caso do NASM um ponto de entrada de funo simplesmente um smbolo como qualquer
outro. A nica coisa que voc precisa fazer declarar o smbolo como global se for us-lo em
outro lugar e, opcionalmente, dizer que este smbolo est relacionado a uma funo (o atributo
:function, depois do nome da funo na diretiva global, no exemplo). Da, para usar a funo no
seu cdigo em C, basta declarar o prottipo como 'extern' e depois linkar tudo. Eis um exemplo:
# Makefile
test: test.o triple.o
gcc -o $@ $^

test.o: test.c
gcc -O3 -march=native -c -o $@ $<

triple.o: triple.asm
nasm -f elf64 -o $@ $<
-----%<----- corte aqui -----%<-----
/* test.c */
#include <stdio.h>

/* Funo definida em triple.asm, l em cima. */


extern int f(int);

int main(int argc, char *argv[])


{
printf("3*23 = %d\n", f(23));
return 0;
}

O compilador vai criar um arquivo objeto contendo a imagem binria do seu cdigo fonte e os
smbolos que foram publicados. No caso do Linux o formato do arquivo objeto tem que ser ELF.
No caso do Windows, o formato WIN6436. Os formatos default do NASM so ELF32 e WIN32,
para Linux e Windows, respectivamente. Por isso voc ter que usar a opo '-f' do assembler para
gerar o arquivo objeto compatvel com o modo de 64 bits.
Podemos ter problemas com o formato escolhido... Os formatos WIN32 ou WIN64, por exemplo,
no suportam atributos nos smbolos exportados. Se voc tentar compilar o triple.asm, como est,
no formato WIN64, e obter:
$ nasm -f win64 triple.asm -o triple.o
triple.asm:9: error: COFF format does not support any special symbol types

Para evitar isso voc pode retirar o atributo 'function' na diretiva 'global'. Esses atributos existem
36 Linux usa o padro Executable and Linkable Format (ELF). Windows usa o Common Object File Format (COFF).
O formato COFF foi definido pelo POSIX e adotado pela Microsoft com o surgimento do Win32. No Linux o
formato ELF foi elaborado como um avano sobre o COFF. Outros sistemas UNIX podem usar COFF tambm.

71
para fins de otimizao e documentao feitas pelo linker, especialmente para o formato ELF.
Aparentemente no tm quaisquer usos no format COFF.

72
Captulo 6: Ferramentas
Eis algumas ferramentas teis para o desenvolvedor, incluindo debugger, profiler, criao de
projetos e anlise de cdigo.

GNU Linker (ld)


comum usarmos o GCC para compilar e, depois, linkar os diversos mdulos num nico arquivo
executvel ou biblioteca. Esse um atalho conveniente para o desenvolvedor, no entanto, o que
ocorre por debaixo dos panos uma chamada ao linker (ld).
O linker faz algumas coisas: Ele sabe quais segmentos (ou sesses) esto contidas no arquivo
alvo (executvel ou biblioteca) e sabe como esses segmentos devem ser organizados. Ele tambm
mantm alguns smbolos especiais (_start, _end, etc) onde, por exemplo, o smbolo _start o ponto
de entrada default para execuo. Outra tarefa do linker misturar os blocos de dados e cdigo
contidos nas sesses dos arquivos objeto parciais, bem como resolver smbolos extern de um
arquivo objeto, fazendo referncia na definio desse smbolo definido em outro arquivo objeto (ou
mostrar um erro se no encontrar a definio do smbolo em lugar algum!).
Mas, onde essas informaes so definidas?
O linker precisa de um script que contenha essas definies. Se voc compilar um pequeno
programa exemplo dizendo ao linker par mostrar o que ele est fazendo (-Wl,--verbose), ver uma
srie de informaes do linker, incluindo os tipos de alvos suportados e o script default usado.
Esse script grande e no vale a pena ser reproduzido aqui. Ao invs disso, eis um script do linker
customizado para o cdigo de boot (16 bits) do kernel do Linux:
/*
* setup.ld
*
* Linker script for the i386 setup code
*/
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)

SECTIONS
{
. = 0;
.bstext : { *(.bstext) }
.bsdata : { *(.bsdata) }

. = 495;
.header : { *(.header) }
.entrytext : { *(.entrytext) }
.inittext : { *(.inittext) }
.initdata : { *(.initdata) }
__end_init = .;

.text : { *(.text) }
.text32 : { *(.text32) }

. = ALIGN(16);
.rodata : { *(.rodata*) }
.videocards : {
video_cards = .;
*(.videocards)
video_cards_end = .;
}

. = ALIGN(16);
.data : { *(.data*) }

73
.signature : {
setup_sig = .;
LONG(0x5a5aaa55)
}

. = ALIGN(16);
.bss :
{
__bss_start = .;
*(.bss)
__bss_end = .;
}

. = ALIGN(16);
_end = .;

/DISCARD/ : { *(.note*) }

/*
* The ASSERT() sink to . is intentional, for binutils 2.14 compatibility:
*/
. = ASSERT(_end <= 0x8000, "Setup too big!");
. = ASSERT(hdr == 0x1f1, "The setup header has the wrong offset!");
/* Necessary for the very-old-loader check to work... */
. = ASSERT(__end_init <= 5*512, "init sections too big!");
}

Esse script define um conjunto de segmentos (sesses) que estaro contidas no arquivo alvo
(vmlinuz?!), a ordem em eles aparecero no arquivo depois dos cdigos linkados e um conjunto de
smbolos. Os segmentos so .bstext, .bsdata, .header, .entrytext, .inittext, .initdata, .text, .text32,
.rodata, .videocards, .data, .signature, e .bss. Os segmentos .bstext e .bsdata comeam no endereo
virtual 0 (zero). O '.', no script, um contador de endereos. O segmento .header comea no
endereo virtual 495 (0x1EF) e os demais segmentos o seguem...
Os smbolos definidos no script so _start (a diretita ENTRY diz que esse o ponto de entrada do
programa linkado), __end_init (que ser o endereo do final do segmento de dados .initdata),
__bss_start e __bss_end (que so definidos para que os cdigos de inicializao tenham a chance de
zerar todo o segmento) e _end que o endereo final da imagem binria37.
Embora o script acima seja customizado para o kernel, no userspace o linker tem um script default
onde um monte de segmentos e smbolos definidos para arquios executveis e bibliotecas. Por
exemplo, para executveis o linker usa um script que define o endereo virtual de carga inicial em
0x400000. No caso de shared libraries o endereo inicial no definido, j que necessrio que
esse tipo de arquivo contenha cdigo independente de posio (PIC = Position Independent Code).
Para ver o script que o linker est usando, chame-o com a opo verbose, na hora de linkar... Se
voc usa o poprio GCC para linkar objetos, use a opo -Wl,--verbose.
Para executveis, j que os smbolos _start e _end so definidos pelo script default do linker38,
podemos acess-los em nossos programas facilmente:
/* Mostra endereos de incio e fim dos segmentos do nosso programa */
#include <stdio.h>

/* O tipo dos smbolos irrelevante. Para obter o endereo do smbolo


sempre necessrio usar o operador &. Poderamos declar-los como
void *, por exemplo. */
extern unsigned long _start; /* Ponto de entrada */
extern unsigned long __executable_start, _end; /* Incio e fim dos segmentos. */

37 De posse de smbolos como __executable_start e _end voc poderia calcular o tamanho total da memria em uso,
mas no deixe de considerar que sua aplicao pode fazer uso de alocao dinmica!
38 Obviamente, os smbolos _start e __executable_start no so definidos para bibliotecas.

74
void main(void)
{
printf("Endereo do incio: %p\n"
"Endereo do fim: %p\n"
"Ponto de entrada: %p\n"
"Endereo de main(): %p\n,
&__executable_start, &_end, &_start, main);
}
-----%<----- corte aqui -----%<-----
$ gcc -o test test.c
$ ./test
Endereo do incio: 0x400000
Endereo do fim: 0x600960
Ponto de entrada: 0x40042b
Endereo de main(): 0x400506

Para obter o endereo linear do smbolo necessrio usar o operador &, como voc pode ver...
Se voc quiser ver o conjunto de smbolos definidos em seu executvel (ou num arquivo objeto)
pode usar o utilitrio objdump com a opo -t (desde que os smbolos no tenham sido extirpados
do executvel final com a opo -s do linker).

Obtendo informaes com objdump


O utilitrio objdump pode listar uma srie de informaes interessantes sobre arquivos objeto,
incluindo executveis e bibliotecas. Lista de segmentos (opo -h), lista de smbolos (opo -t) e at
mesmo o disassembly dos cdigos contidos no arquivo (opes -d, -D ou -S). A opo -d faz o
disassembly apenas das sesses executveis (isso definido no script do linker!), j a opo -D
tenta disassemblar todas as sesses. A opo -S usada caso o seu programa tenha informaes
de debugging embutidas: Alm do disassembly essa opo listar o cdigo fonte correspondente
tambm.
No caso do disassembly, podemos usar uma opo -M para dizer o sabor que queremos usar. O
sabor default o formato AT&T. Para o sabor Intel use -M intel. Note tambm que as opes
-S e -d listaro todas as sesses executveis e todos os smbolos contidos no arquivo.

GNU Debugger
Seus cdigos quase nunca se comportam como voc espera. Por mais cuidado que se tenha, bugs
so quase sempre includos naquelas rotinas que voc pensa serem perfeitas. At o momento que
voc compila e executa o programa e percebe que ele no est fazendo o que deveria.
Para consertar o cdigo podemos usar um debugger (literalmente, um tirador de bugs). Um dos
mais poderosos e free o GNU Debugger (GDB).
Usar o GDB exige alguma preparao: A primeira coisa que voc ter que fazer inserir, no seu
cdigo executvel, as informaes de debugging. Isso feito usando a opo '-g' durante a
compilao. til tambm no usar as otimizaes automticas do compilador. Essas otimizaes
podem extirpar parte do cdigo que voc quer debugar, tornando a sesso de debugging mais
complicada. Lembre-se que, nessa etapa, o que queremos fazer retirar erros de nosso programa...
Ainda no queremos faz-lo ficar rpido ou eficiente. Assim, usar a opo '-O0' tambm til (j
que, por default, o compilador usa '-O1').
No mostrarei aqui tcnicas de debugging. Existem publicaes mais interessantes do que eu
poderia escrever em poucos tpicos. O que nos interessa usar o GDB como disassembler e alguns
comandos para obter informaes do cdigo.

75
Configurando o GDB
Prefiro a sintaxe Intel ao invs da sintaxe AT&T nas listagens de cdigos em assembly. Para
configurar o GDB, basta usar o comando:
set disassembly-flavor intel

O chato que toda vez que for usar o gdb voc deveria digitar essa linha. Felizmente, existe uma
maneira de executar certos comandos automaticamente sempre que o gdb for iniciado. Basta
coloc-los no arquivo de configurao ~/.gdbinit:
$ echo 'set disassembly-flavor intel' > ~/.gdbinit39

Duas maneiras de listar cdigo assembly no gdb


Suponha que voc queira listar o famoso cdigo hello, world:
/* test.c */
#include <stdio.h>

int main(int argc, char *argv[])


{
printf("Hello, world.\n");
return 0;
}
-----%<----- corte aqui ----%<-----
$ gcc -g -O0 test.c -o test

Uma vez compilado com a opo -g do gcc, voc pode carregar o executvel e list-lo usando o
comando disassemble (ou a verso abreviada: disas):
$ gdb ./test

(gdb) disas main
Dump of assembler code for function main:
0x0000000000400400 <+0>: sub rsp,0x8
0x0000000000400404 <+4>: mov edi,0x4005cc
0x0000000000400409 <+9>: call 0x4003e0 <puts@plt>
0x000000000040040e <+14>: xor eax,eax
0x0000000000400410 <+16>: add rsp,0x8
0x0000000000400414 <+20>: ret
End of assembler dump.

GDB fornecer uma listagem contendo o endereo das instrues, o offset relativo a partir do incio
da funo (porque, no modo x86-64, possvel usar um modo de endereamento relativo ao
registrador RIP) e as instrues. Acontece que GDB permite listar mais detalhes! Se quisermos
listar o cdigo fonte, em C, que gerou a listagem acima, basta usarmos a opo /m:
(gdb) disas /m main
Dump of assembler code for function main:
4 {
0x0000000000400400 <+0>: sub rsp,0x8
5 printf("Hello\n");
0x0000000000400404 <+4>: mov edi,0x4005cc
0x0000000000400409 <+9>: call 0x4003e0 <puts@plt>
6 return 0;
7 }
0x000000000040040e <+14>: xor eax,eax
0x0000000000400410 <+16>: add rsp,0x8
0x0000000000400414 <+20>: ret

Tambm podemos acrescentar o contedo binrio das instrues usando a opo /r:

39 Repare que o arquivo ~/.gdbinit pode j existir em seu sistema. Se este for o caso, mofidique-o com seu editor de
textos favorito.

76
(gdb) disas /r main
Dump of assembler code for function main:
0x0000000000400400 <+0>: 48 83 ec 08 sub rsp,0x8
0x0000000000400404 <+4>: bf cc 05 40 00 mov edi,0x4005cc
0x0000000000400409 <+9>: e8 d2 ff ff ff call 0x4003e0 <puts@plt>
0x000000000040040e <+14>: 31 c0 xor eax,eax
0x0000000000400410 <+16>: 48 83 c4 08 add rsp,0x8
0x0000000000400414 <+20>: c3 ret

Listando registradores
Uma vez que sua sesso de debugging est em andamento, voc pode listar os registradores com o
comando info. Existem duas maneiras: info reg e info all-reg. Nem todos os registradores so
listados. No primeiro caso voc obter os registradores de uso geral, RAX, RBX, RCX, RDX, RSI,
RDI, RBP, RSP, R8 at R15, RIP e EFLAGS (no RFLAGS! Mas no importa, j que este ,
essencialmente EFLAGS com os 32 bits superiores zerados!). No segundo caso, os registradores de
ponto flutuante e SIMD (SSE ou AVX, dependendo de sua arquitetura) so listados tambm:
#include <stdio.h>

int f(int x)
{
return 2*x;
}

int main(int argc, char *argv[])


{
printf("%d\n", f(2));

return 0;
}

Compilando e iniciando nossa sesso de debugging:


$ gcc -O1 -g test.c -o test
$ gdb test
Reading symbols from test...done.
(gdb) b f
Breakpoint 1 at 0x40050c: file test.c, line 5.
(gdb) r
Starting program: test
Breakpoint 1, f (x=2) at test.c:5
5 return 2*x;

(gdb) info reg


rax 0x7ffff7dd9ec8 140737351884488
rbx 0x0 0
rcx 0x0 0
rdx 0x7fffffffe1c8 140737488347592
rsi 0x7fffffffe1b8 140737488347576
rdi 0x2 2
rbp 0x7fffffffe0b0 0x7fffffffe0b0
rsp 0x7fffffffe0b0 0x7fffffffe0b0
r8 0x7ffff7dd8300 140737351877376
r9 0x7ffff7deb310 140737351955216
r10 0x0 0
r11 0x7ffff7a6fdb0 140737348304304
r12 0x400400 4195328
r13 0x7fffffffe1b0 140737488347568
r14 0x0 0
r15 0x0 0
rip 0x400513 0x400513 <f+7>
eflags 0x206 [ PF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0

77
(gdb) disas f
Dump of assembler code for function f:
=> 0x000000000040050c <+0>: lea eax,[rdi+rdi*1]
0x000000000040050f <+3>: ret
End of assembler dump.

Como era esperado, graas conveno de chamada, o registrador RDI contm o primeiro
parmetro da chamada para a funo f() - marcado em vermelho para chamar sua ateno. Repare
que no disassembler da funo o breakpoint est marcado com =>.
Outra maneira de inspecionar o contedo de um registrador usando o comando print e o nome do
registrador precedido com $, como $rax.

Examinando a memria com o GDB


Alm do disassembler e da possibilidade de examinar o contedo dos registradores, o comando x
do GDB nos permite eXaminar uma regio da memria. Basta fornecer o formato, a quantidade e o
endereo (ou o smbolo) desejado. Com o mesmo cdigo acima, poderamos ver o contedo da
pilha quando o breakpoint alcanado:
$ gdb test
Reading symbols from test...done.
(gdb) b f
Breakpoint 1 at 0x40050c: file test.c, line 5.
(gdb) r
Starting program: test
Breakpoint 1, f (x=2) at test.c:5
5 return 2*x;
(gdb) x/32bx $rsp
0x7fffffffdde0: 0x00 0xde 0xff 0xff 0xff 0x7f 0x00 0x00
0x7fffffffdde8: 0x54 0x05 0x40 0x00 0x00 0x00 0x00 0x00
0x7fffffffddf0: 0xe8 0xde 0xff 0xff 0xff 0x7f 0x00 0x00
0x7fffffffddf8: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00

Usei o registrador RSP como endereo base para o exame, pedindo para examinar 32 bytes e
mostr-los em hexadecimal (x/32bx). Podemos usar as letras 'b', 'w', 'h' e 'q' para BYTE, WORD,
DWORD (a letra 'd' usada para decimal) e QWORD, respectivamente. O 'x' final diz que os
valores devem ser impressos em hexadecimal.
Podemos usar um tamanho e a letra 's' para mostrar o bloco em formato de string. As letras 'd' e 'u'
mostram os dados em decimal com e sem sinal, respectivamente. A letra 't' mostra os valores em
binrio (a letra devido a palavra TWO). A outra formatao interessante 'f', para ponto flutuante
(embora, para mim, no esteja claro qual o formato real: float, double ou long double?).

Obtendo listagens em assembly usando o GCC


Um recurso muito til para anlise do seu prprio cdigo pedir ao compilador que gere uma
listagem em assembly. No GCC voc pode fazer isso usando a opo '-S'. No Visual C++ basta usar
a opo '-Fa' com o CL.EXE.
Minha preferncia, no caso do GCC, gerar listagens no estilo Intel, ao invs do estilo AT&T. Isso
pode ser obtido adicionando a opo '-masm=intel'. E o GCC ainda permite acrescentar comentrios
listagem gerada usando-se a opo '-fverbose-asm'. Outra coisa importante dizer ao compilador
qual arquitetura ser usada para otimizao. Isso feito atravs da opo '-march'. Eu prefiro usar -
march=native para que o GCC use, como processador alvo, o da mquina em que o cdigo foi
compilado. Se seu processador suporta a extenso AVX ou AVX-512, por exemplo, o GCC usar as
instrues estendidas.
Num executvel de produo voc pode querer escolher um processador ou arquitetura especfica.
Consulte a documentao do GCC para saber quais esto disponveis.

78
Eis um exemplo de gerao de listagem assembly, usando o GCC, para o seguinte cdigo:
/* list.c */
#include <malloc.h>

/* A estrutura de nosso 'n'. */


struct node_s {
struct node_s *next;
void *data;
size_t size;
};

/* Insere um n depois do n dado por node_ptr. */


struct node_s *InsertNodeAfter(struct node_s *node_ptr, void *data, size_t size)
{
struct node_s *newnode;

if ((newnode = (struct node_s *)malloc(sizeof(struct node_s))) != NULL)


{
newnode->next = node_ptr->next;
newnode->data = data;
newnode->size = size;
node_ptr->next = newnode;
}

return newnode;
}
-----%<----- corte aqui -----%<-----
$ gcc -O3 -march=native -S -masm=intel -fverbose-asm list.c

A listagem abaixo, compilada para um processador i7-4770, fica mais ou menos assim (alis, para
todos os modelos de processadores que suportam x86-64):
; Entrada: RDI = node_ptr,
; RSI = data
; RDX = size
; Retorna RAX.
InsertNodeAfter:
; Salva os parmetros em R12, RBP e RBX porque esses registradores tm que
; ser preservados entre chamadas de funes. A funo malloc(), abaixo, pode
; modificar RDI, RSI e RDX!
push r12
mov r12,rsi # data, data
push rbp
mov rbp,rdx # size, size
push rbx
mov rbx,rdi # node_ptr, node_ptr

; Aloca 24 bytes (sizeof(struct node_s)).


mov edi,24
call malloc

test rax,rax # new_node


je .insert_exit ; Se RAX == NULL, sai.

mov rdx,[rbx] # D.2521, node_ptr_5(D)->next


mov [rax+8],r12 # newnode_4->data, data
mov [rax+16],rbp # newnode_4->size, size
mov [rax],rdx # newnode_4->next, D.2521
mov [rbx],rax # node_ptr_5(D)->next, newnode

.insert_exit:
pop rbx
pop rbp
pop r12
ret

Os comentrios mais claros so colocados pelo GCC e os em vermelho so meus. Note que o
significado de uma instruo ou referncia memria comentada com o nome da varivel, do
cdigo original em C, referenciada ou um nome esquisito ( o caso de D.2521, acima) usado para
uma varivel temporria.
Outra maneira pedir ao compilador que use o GNU Assembler (GAS) para gerar uma listagem

79
mista contendo o seu cdigo fonte como comentrios. Isso feito adicionando as opes -g e -Wa,-
ahlnd, no gcc, e redirecionando a sada para um arquivo texto. Eis o mesmo exemplo:
$ gcc -g -O3 -march=native -masm=intel -Wa,-ahlnd -c list.c > list.lst

necessrio o uso da opo -g para que o GNU Assembler tenha como mesclar o cdigo fonte, em
C, com a listagem em assembly. A linha de comado acima criar algo assim:
$ cat list.lst
1 .file "list.c"
2 .intel_syntax noprefix
3 .text
4 .Ltext0:
5 .p2align 4,,15
6 .globl InsertNodeAfter
8 InsertNodeAfter:
9 .LFB24:
10 .file 1 "ins.c"
1:ins.c **** /* insafter.c */
2:ins.c **** #include <malloc.h>
3:ins.c ****
4:ins.c **** /* A estrutura de nosso 'n'. */
5:ins.c **** struct node_s {
6:ins.c **** struct node_s *next;
7:ins.c **** void *data;
8:ins.c **** size_t size;
9:ins.c **** };
10:ins.c ****
11:ins.c **** /* Insere um n depois do n dado por node_ptr. */
12:ins.c **** struct node_s *InsertNodeAfter(struct node_s *node_ptr, void *data,
size_t size)
13:ins.c **** {
11 .loc 1 13 0
12 .cfi_startproc
13 .LVL0:
14 0000 4154 push r12
15 .cfi_def_cfa_offset 16
16 .cfi_offset 12, -16
17 0002 4989F4 mov r12, rsi
18 0005 55 push rbp
19 .cfi_def_cfa_offset 24
20 .cfi_offset 6, -24
21 0006 4889D5 mov rbp, rdx
22 0009 53 push rbx
23 .cfi_def_cfa_offset 32
24 .cfi_offset 3, -32
25 .loc 1 13 0
26 000a 4889FB mov rbx, rdi
14:ins.c **** struct node_s *newnode;
15:ins.c ****
16:ins.c **** if ((newnode =
(struct node_s *)malloc(sizeof(struct node_s))) != NULL)
27 .loc 1 16 0
28 000d BF180000 mov edi, 24
28 00
29 .LVL1:
30 0012 E8000000 call malloc
30 00
31 .LVL2:
32 0017 4885C0 test rax, rax
33 001a 7411 je .L6
17:ins.c **** {
18:ins.c **** newnode->next = node_ptr->next;
34 .loc 1 18 0
35 001c 488B13 mov rdx, QWORD PTR [rbx]
19:ins.c **** newnode->data = data;
36 .loc 1 19 0
37 001f 4C896008 mov QWORD PTR [rax+8], r12
20:ins.c **** newnode->size = size;
38 .loc 1 20 0
39 0023 48896810 mov QWORD PTR [rax+16], rbp
18:ins.c **** newnode->data = data;
40 .loc 1 18 0
41 0027 488910 mov QWORD PTR [rax], rdx

80
21:ins.c **** node_ptr->next = newnode;
42 .loc 1 21 0
43 002a 488903 mov QWORD PTR [rbx], rax
44 .L6:
22:ins.c **** }
23:ins.c ****
24:ins.c **** return newnode;
25:ins.c **** }
45 .loc 1 25 0
46 002d 5B pop rbx
47 .cfi_def_cfa_offset 24
48 .LVL3:
49 002e 5D pop rbp
50 .cfi_def_cfa_offset 16
51 .LVL4:
52 002f 415C pop r12
53 .cfi_def_cfa_offset 8
54 .LVL5:
55 0031 C3 ret
56 .cfi_endproc
57 .LFE24:
59 .Letext0:
60 .file 2 "/usr/lib/gcc/x86_64-linux-gnu/4.8/include/stddef.h"
61 .file 3 "/usr/include/x86_64-linux-gnu/bits/types.h"
62 .file 4 "/usr/include/libio.h"
63 .file 5 "/usr/include/stdio.h"
64 .file 6 "/usr/include/malloc.h"

A listagem em assembly exatamente a mesma obtida com a opo -S. No entanto, a listagem do
cdigo origiral tambm apresentada e de uma forma bem confusa (repare nos nmeros das
linhas!), j que a enfase no cdigo em assembly... As otimizaes feitas pelo compilador tendem a
tornar a leitura do cdigo em C meio esquisita. Pelo menos temos comentrios dizendo o que est
sendo feito com o cdigo e os ops.
Neste ponto voc pode se perguntar qual a diferena de usar -march=native, afinal de contas? Em
certos casos o compilador pode escolher usar, por exemplo, os registradores YMM (512 bits, ou 32
bytes). Se tivssemos mais um valor de 64 bits na estrutura e quisssemos zer-la completamente, o
compilador poderia escolher fazer algo assim:
vpxor xmm0, xmm0, xmm0
vmovdqu [rdi],ymm0
vzeroupper

Ao invs de algo mais tradicional:


xor eax,eax
mov [rdi],rax
mov [rdi+8],rax
mov [rdi+16],rax
mov [rdx+24],rax

Sobre os endereos em listagens em assembly obtidas com objdump ou GCC


Considere o programinha abaixo:
/* test.c */
int x = 3;

int f(int a) { return a*x; }

Ao compil-lo e obtermos a listagem em assembly (com os cdigos em hexadecimal), obtemos algo


assim:
$ gcc -O3 -masm=native -c test.c -o test.o
$ objdump -d -M intel test.o

Disassembly of section .text:
0000000000000000 <f>:

81
0: 8b 05 00 00 00 00 mov eax,DWORD PTR [rip+0x0]
6: 0f af c7 imul eax,edi
9: c3

Marquei em vermelho o pedao da instruo MOV problemtica... Essa instruo est, claramente,
obtendo o valor da varivel x, que foi declarada como global... Mas, como que o compilador sabe
que o endereo dela 0x0?
Ele no sabe! Para esse mdulo (este arquivo objeto) a varivel global x colocada no segmento
.data logo no incio, ou seja, no offset 0... Por isso a listagem coloca o offset 0 na instruo. Isso
pode ser observado com:
$ objdump -t test.o
test.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 test.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .text.unlikely 0000000000000000 .text.unlikely
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g F .text 000000000000000a f
0000000000000000 g O .data 0000000000000004 x

Nessa listagem da tabela de smbolos temos, em cada linha, o endereo virtual do smbolo, um flag
que diz se o smbolo local (l) ou global (g), outro flag que diz se o smbolo para o
debugger (d), uma referncia a um arquivo (f), uma funo (F) ou outra coisa (O mas pode ser
um espao tambm)... Segue a section onde o smbolo se encontra, o tamanho do smbolo em bytes
e, finalmente, o nome.
Note que x um smbolo genrico (do tipo O) que se encontra no offset 0, global, localiza-se
no segmento .data e tem 4 bytes de tamanho. Mas esse offset relativo ao segmento .data deste
mdulo (test.o).
Por esse motivo, os endereos contidos em listagem assembly (geradas pelo compilador) devem ser
entendidos como sendo relativos ao mdulo ao qual pertence. trabalho do linker atribuir um
offset condizente com o programa final depois de agrupar todos os segmentos dos mdulos.

Usando o make: Fazendo um bolo, usando receitas


Ao invs de compilar seus cdigos um a um, chamando o gcc e o nasm vrias vezes, pode-se criar
uma receita para cozinhar o bolo todo de uma maneira mais automtica. Isso feito com o
utilitrio make.
Com ele voc cria um arquivo que contm, literalmente, receitas. Elas so compostas de regras e
aes. Voc s precisa encadear as regras para que uma dependa da outra e as aes sero feitas na
ordem correta. Um exemplo, para compilar o famoso helloworld.c:
helloworld: helloworld.o
gcc -o $@ $^

helloworld.o: helloworld.c
gcc -c -o $@ $<

Ateno: A linha de comando tem que comear com um caractere '\t' (um tab).
Aqui temos duas receitas. A primeira receita a mais importante porque estabelece a cadeia de
dependncias... No exemplo acima, a primeira receita nos diz que helloworld depende de
helloworld.o. Se helloworld.o existir ento, para fazer helloworld, a linha de comando contendo o
gcc ser chamada para linkar o arquivo objeto, criando o executvel. Os macros $@ e $^ so

82
atalhos para o alvo ($@) e as dependncias ($^). O make substituir esses macros com os nomes de
arquivos corretos, contidos na receita.
Note que a primeira receita depende de helloworld.o. A segunda receita nos diz como ele feito. Ele
depende de helloworld.c. A linha de comando abaixo dessa receita faz o helloworld.o, que a
dependncia de helloworld.
A diferena entre $< e $^ que o primeiro contm os nomes das dependncias separadamente. O
segundo contm todas as dependncias numa nica string. Isso til num Makefile assim:
test: test1.o test2.o test3.o
gcc -o $@ $^

%.o: %.c
gcc -c -o $@ $<

Aqui, todo arquivo com extenso '.o' ser feito a partir de um arquivo correspondente com
extenso '.c'. Ou seja, o gcc ser chamado vrias vezes, uma para cada arquivo '.c' no diretrio
corrente. Uma vez que tenhamos 'teste1.o', 'teste2.o' e 'teste3.o', o gcc ser chamado, de novo, para
linkar os 3 objetos.
Outra coisa interessante do make que as linhas de comando das receitas podem ser omitidas. O
utilitrio sabe o que fazer, na maioria dos casos. Se crissemos algo como:
test: test1.o test2.o test3.o
gcc -o $@ $^

%.o: %.c

A sintaxe '%.o' e '%.c' o equivalente do wildcard '*.o' e '*.c'.

No script acima, o compilador gcc ser chamado com a opo -c para criar os objetos que
dependem dos mdulos em C. Para customizar esse comportamento podemos alterar algumas
variveis de ambiente. A varivel CC nos diz qual o compilador C que vai ser usado. Por default,
make usa 'cc', que um link simblico para gcc.

As opes de compilao esto na varivel CFLAGS. Assim, se quisssemos compilar nossos


cdigos com a mxima otimizao, poderamos fazer:
CFLAGS += -O3 -march=native
OBJECTS = test1.o test2.o test3.o

test: $(OBJECTS)
$(CC) -o $@ $^

%.o: %.c

A lista de objetos do qual 'test' depende foi colocada numa varivel, bem como as opes '-O3
-march=native' foram adicionadas varivel preexistente CFLAGS. Ao usar $(var) fazemos uma
substituio literal da varivel no comando ou na receita.
O make tambm permite receitas que tenham como alvo um falso arquivo. Por exemplo:
CFLAGS += -O3 -march=native
OBJECTS = test1.o test2.o test3.o

test: $(OBJECTS)
$(CC) -o $@ $^

%.o: %.c

clean:
rm *.o

83
O alvo clean no a receita principal e no est na lista de dependncias. Ele jamais ser executado
quando chamarmos make sem parmetros. Mas, podemos chamar make passando o alvo desejado:
$ make clean

prudente informar ao make que esse um alvo de araque (phony target) usando a diretiva
.PHONY:
CFLAGS += -O3 -march=native
OBJECTS = test1.o test2.o test3.o

# Diz ao make que 'clean' um alvo 'impostor'.


.PHONY: clean

test: $(OBJECTS)
$(CC) -o $@ $^

%.o: %.c

clean:
rm *.o

Esses phony targets so teis para realizarmos operaes diferentes e at mesmo quanto nosso
script exige mltiplos alvos. Lembre-se que a primeira receita a que estabelece toda a cadeia de
dependncias. Assim, se tivermos que compilar um 'hello' e um 'bye', por exemplo, poderamos
fazer algo assim:
# Diz ao make que 'clean' um alvo 'impostor'.
.PHONY: all clean

# Esta a receita principal. Ela estabelece a dependncia


# das receitas 'hello' e 'bye'.
all: hello bye

hello: hello.o
$(CC) -o $@ $^

bye: bye.o
$(CC) -o $@ $^

%.o: %.c

clean:
rm *.o

A receita principal, 'all', estabelece que 'hello' e 'bye' tm que ser construdos primeiro. Cada um
deles depende de um arquivo objeto que, por sua vez, so construdos pela receita '%.o: %.c'. A
receita 'clean' est ai para permitir fazer uma limpeza do diretrio onde os arquivos foram
compilados.
Quanto as receitas em si, as aes podem ter mais que uma linha. Cada linha executada num shell
diferente. Por exemplo, se tivssemos a receita para 'hello' assim:
hello: hello.o
$(CC) -o $@ $^ -lm
strip -s $@

O linker ser chamado num shell e o 'strip' em outro, na sequncia. Isso pode ser problemtico se
voc quiser, por uma questo de estilo, usar mais que uma linha. No make, usar vrias linhas para
serem interpretadas pelo mesmo shell exige o uso do caractere '\' no final da linha. Ainda, j que
make tem macros comeando com $, se seu comando usa variveis de ambiente voc deve fazer as
substituies usando $$:
hello: hello.o
$(CC) -o $@ $^ -lm
# As linhas abaixo so executadas num mesmo shell.

84
if [ -f $@ ]; then \
echo Arquivo $@ compilado com sucesso em $$PWD. \
fi

85
86
Captulo 7: Medindo performance
Existem algumas interpretaes para a palavra performance, aplicveis ao contexto da execuo
de software. A nica interpretao que interessa aqui sinnima de velocidade.
Ao dizer velocidade voc pode pensar isso como o nmero de instrues que o processador
executar numa unidade de tempo ou, ainda, quanto tempo o processador leva para executar um
conjunto de instrues. uma aproximao lgica. Infelizmente, impraticvel.

Ciclos de mquina e ciclos de clock


Todo o tempo gasto na execuo de uma nica instruo composto de um conjunto de ciclos
chamado de ciclos de mquina. Um ciclo de mquina , por sua vez, composto de um conjunto de
ciclos de clock e um ciclo de clock perodo da forma de onda quadrada usada como base de
temporizao do processador. So os famosos giga-hertz da sua CPU. No meu caso, meu
processador um i7-4770 com 3.4 GHz de velocidade, o que significa que cada ciclo de clock
acontece em cerca de 0,29 ns40.
Um ciclo de mquina, em mdia, toma cerca de 4 ciclos de clock 41. Embora, hoje em dia, a
performance seja medida em termos de ciclos de clock porque os processadores modernos
multiplicam o clock, internamente, continuarei usando o termo ciclos de mquina no decorrer
deste texto.
Instrues diferentes gastam quantidades diferentes de ciclos de mquina. Instrues simples
costumam gastar apenas um ciclo de mquina. Instrues mais complicadas (como CALL), gastam
alguns ciclos (que pode chegar a algumas dezenas de ciclos).

Contar ciclos de mquina no fcil


Antigamente conseguamos calcular, de antemo, quantos ciclos de mquina uma rotina gastaria.
Bastava contar os ciclos de mquina de instrues individuais, j que elas eram lidas, decodificadas
e executadas uma depois da outra, numa sequncia bem definida.
A Intel usou um conceito de arquitetura superescalar que foi implementada nos antigos
processadores da srie 29000 da AMD e nos seus prprios processadores da srie i960 para acelerar
o ciclo de leitura, decodificao e execuo. O termo escalar aqui sinnimo de sequencial... O
super indica que os processadores, desde ento, realizam os processos busca, decodificao e
execuo de instrues em paralelo. Enquanto uma instruo est sendo lida da memria (do cache,
no caso) outra est sendo decodificada e outra est sendo executada...
A coisa complicou ainda mais, nas novas arquiteturas, com a reordenao automtica de instrues.
Esses novos processadores podem mudar a ordem do ciclo de execues para garantir que a menor
quantidade de ciclos de mquina seja gasto...
Temos outros fatores que afetam essa contagem de ciclos: Emparelhamento de instrues,
reordenao de acessos memria, efeitos relativos ao cache, threads, paginao etc. Tudo isso
pode atrasar bastante a execuo de instrues.
A ocorrncia da maioria dessas interferncias so difceis de prever. Isso deixa a contagem de
ciclos fora de questo. As nicas coisas que podemos fazer : usar a intuio e medir o tempo!

40 Um nanossegundo equivale bilionsima parte de um segundo.


41 Isso um chute muito mal chutado... uma base de comparao muito fraca, mas til, para meus propsitos.

87
Intuio, neste caso, olhar para uma rotina (em assembly) e avaliar, aproximadamente, o tempo
gasto com base na complexidade das instrues. Isso til ao analisar ou desenvolver um rascunho
da rotina desejada.
Como em qualquer coisa, nossa intuio pode estar totalmente errada. Precisamos de evidncias. E
a maneira mais simples de sabermos quantos ciclos, em mdia, uma rotina vai gastar medindo.
Nesse captulo te mostro como medir esse gasto e como calcular o ganho ou perda de performance
de uma rotina.

Como medir?
Para determinar a velocidade de uma funo necessrio medir a quantidade de ciclos de mquina
que esto sendo gastos. Existem duas maneiras de fazermos isso:
1. Via hardware: Usando equipamentos como ICEs (In Circuit Emulators) e/ou Analisadores
Lgicos;
2. Via software: Usando contadores internos, RTC (Real Time Clock o relgio do
computador) ou algum profiler especializado como o Intel VTune.
ICEs42 e Analisadores Lgicos so caros e exigem conhecimento e experincia com eletrnica. Para
a maioria de ns, pobres mortais, esses equipamentos tm curso proibitivos.
Nos processadores atuais existem recursos de medio de performance chamados de performance
counters. S que eles s esto disponveis para o cdigo executado pelo kernel. Nossas aplicaes,
no userspace, no tm acesso a esses contadores.
Felizmente, desde os processadores Pentium, existe um contador que est disponvel em todos os
privilgios de execuo. Trata-se do Timestamp Counter (TSC). Ele conta quantos ciclos de clock j
aconteceram desde o ltimo reset (ou power up) do processador.
Para acessar o contedo desse contador basta executar a instruo RDTSC. Ela devolve a contagem
no par de registradores EDX:EAX. Mesmo no modo 64 bits esse par de registradores usado ao
invs de apenas RAX.
Para ter uma ideia do valor que pode ser mantido pelo contador, se um processador funciona com
clock de 3 GHz, podemos obter, sem perigo de overflow, quase 195 anos de contagem desde o
momento que o processador foi colocado em funcionamento!
O exemplo abaixo mostra como obter o valor, usando a funo intrnseca __rdtsc:
/* rdtsc1.c */
#include <stdio.h>
#include <x86intrin.h>

/* _rdtsc() tem o prottipo:

unsigned long long _rdtsc(void); */


int main(int argc, char *argv[])
{
printf("Ciclos: %lu\n", __rdtsc());
return 0;
}
-----%<----- corte aqui -----%<-----
$ gcc -O3 -o rdtsc1 rdtsc1.c
$ ./rdtsc1
Ciclos: 82384266127

garantido que a instruo RDTSC retorne um valor nico, toda vez que for chamada (mesmo que

42 Existe uma instruo no documentada nos processadores Intel (mas, documentada nos processadores AMD)
chamada IceBP. Trata-se de um breakpoint por hardware, mas nem vale a pena estud-la!

88
seja chamada em threads diferentes):

Aumentando a preciso da medida


Os processadores modernos tendem a executar instrues fora de ordem. Pode parecer estranho,
j que o conceito de um programa , justamente, execuo de instrues de forma sequencial, uma
depois da outra. S que, algumas vezes, no faz muita diferena, do ponto de vista funcional, que
uma sequncia de instrues seja alterada. E essa modificao de ordem pode resultar num aumento
considervel de performance. Eis um exemplo. Considere o pequeno fragmento de cdigo abaixo:
loop:
mov al,[rsi]
mov [rdi],al ; dependncia de AL, acima.
inc rdi
inc rsi
dec rcx
jnz loop

Lembre-se que podemos ter duas ou mais instrues sendo executadas ao mesmo tempo no
mesmo processador lgico. No exemplo acima bvio que as duas primeiras instrues tero que
ser executadas sequencialmente porque a segunda depende da atualizao de AL, feita na primeira
instruo. O processador, esperto como , poder reordenar as instrues assim:
loop:
mov al,[rsi]
inc rsi ; Repare que essa instruo foi 'reordenada'.
mov [rdi],al
inc rdi
dec rcx
jnz loop

Se o processador tem a capacidade de executar duas instrues simultaneamente43, evidente que o


segundo loop gasta 3 ciclos de mquina, enquanto o primeiro executa em 4. Isso nos d um
aumento de performance de 25% (ou seja, o segundo loop executado em 75% do tempo do
primeiro).
J que instrues podem ser reordenadas na tentativa de aumentar a performance, o cdigo sob teste
pode ser reordenado, inclusive colocando instrues antes ou depois das leituras do contador de
ciclos de clock. evidente que isso nos dar medida errada do gasto de ciclos daquilo que
queremos testar. Felizmente, podemos usar instrues que serializam o processador. O termo
serializar significa que o processador esperar que todas as instrues pendentes sejam executadas
e, s ento, continuar o processamento.
A maioria das instrues que serializam o processador s podem ser executadas em nveis altos de
privilgio (ring 0), mas existem algumas que so teis no userspace. o caso de instrues como
CPUID, LFENCE, SFENCE e MFENCE. A instruo CPUID tambm serializa o processador.
As instrues LFENCE, SFENCE e MFENCE so interessantes: Elas so chamadas de instrues
de barreira (uma cerca, fence em ingls, uma barreira que colocamos em torno de uma casa)
porque esperam que o processador termine de executar todas as instrues pendentes que fazem
carga (Load), armazenamento (Storage) ou ambas as coisas (M, de MFENCE significa Memory).
Para garantir que os efeitos de execuo fora de ordem no interfiram em nossas medidas,
precisamos serializar o processador. A rotina de leitura do TSC, em forma de macro, ficaria assim:

43 Processadores baseados em arquitetutas mais recentes Haswell, por exemplo tm o potencial de executarem at
8 instrues simultaneamente!

89
/* A varivel 'x', passada para esse macro, deve ser
do tipo 'unsigned long long'. */
#define TSC_READ(x) \
{ \
register unsigned int lo, hi; \
\
__asm__ __volatile__ ( \
"mfence;" \
"rdtsc;" \
: "=a" (lo), "=d" (hi) ); \
\
(x) = ((unsigned long)hi << 32) | \
(unsigned long)lo; \
}

No macro acima no h perigo de misturar bits entre os valores das variveis lo e hi, j que os bits
superiores estaro zerados (de novo: RDTSC s atualiza EAX e EDX, mas zera a poro superior
de RAX e RDX). S que esse clculo adicional pode muito bem ser colocado dentro do bloco
assembly e, se usarmos uma varivel long como retorno, garantido que RAX vai ser colocado
nela:
#define TSC_READ(x) \
{ \
register unsigned long r; \
\
__asm__ __volatile__ ( \
"mfence;" \
"rdtsc;" \
"shll $32,%%edx;" \
"orl %%edx,%%eax
: "=a" (r) : : "%rdx" ); \
}

Como RDTSC altera, inclusive RDX, necessrio coloc-lo na lista dos registradores preservados
para dar uma chance ao GCC de salv-lo, se necessrio.
Essas instrues extras, SHL e OR, sero reordenadas depois do MFENCE, claro, mas elas
provavelmente sero executadas em paralelo com a instruo CALL da chamada a ser testada.
Mesmo que a chamada seja colocada inline em seu cdigo, essas instrues adicionais sero
emparelhadas com outras, tendo muito pouca influncia no valor final. E, mesmo que tenha, elas
gastaro apenas 1 ciclo de clock. No se trata de grandes perdas...
MFENCE uma instruo do SSE2. Se seu processador for um antigo Pentium III ou se no
suportar SSE2, voc poder querer troc-la por CPUID, mas necessrio fazer algumas
modificaes, j que CPUID altera EAX, EBX, ECX e EDX, precisamos colocar RBX e RCX na
lista de preservao (RDX tambm, mas por causa de RDTSC):
#define TSC_READ(x) \
{ \
register unsigned long r; \
\
__asm__ __volatile__ ( \
"xor %%eax, %%eax;" \
"cpuid;" \
"rdtsc;" \
"shll $32,%%edx;" \
"orl %%edx,%%eax
: "=a" (r) : : "%rbx", "%rcx", "%rdx" ); \
}

Existe um paper da Intel mostrando que esse simples uso de leitura do TSC pode ser problemtico
quando se mede a performance de uma funo... O problema que a segunda chamada gastar
tempo executando MFENCE e os MOVs finais podero estar fora de ordem. A Intel recomenda
formas de leitura diferentes. Uma para o incio da medio e outra para o fim:
unsigned long r0, r1;

90
__asm__ __volatile__ (
mfence;
"rdtsc;"
"shll $32,%%rdx;"
"orl %%rdx,%%rax
: "=a" (r0) : : "%rdx"
);

/* Funo a ser medida... */


f();

__asm__ __volatile__ (
"rdtscp;"
"shll $32,%%rdx;"
"orl %%rdx,%%rax"
: "=a" (r1) : : "%rdx", "%rcx"
);

/* tsc conter a diferena dos timestamps. */


tsc = r1 r0;

O segundo bloco em assembly usa a instruo RDTSCP que serializa o processador sem o overhead
da chamada de MFENCE.

Mas, o gcc possui funes intrnsecas para executar CPUID e RDTSC!


Sim, possui... Elas esto localizadas nos headers ia32intrin.h e cpuid,h, mas como so chamadas
individuais possvel que o compilador gere mais cdigo do que necessrio ou menor do que
queremos. Um exemplo de uso, onde obteremos valores mais ou menos iguais aos que
conseguiramos usando a funo em assembly, este:
#include <x86intrin.h>


unsigned long c, c0;
int dummy;

_mm_mfence();
c0 = __rdtsc();
f();
/* __rdtscp() usa um ponteiro para obter o valor do registrador IA32_TSC_AUX_MSR. */
c = __rdtscp(&dummy);
c -= c0;

-----%<----- corte aqui -----%<-----
; cdigo parcial gerado. Compilado com -O2

mfence
rdtsc
mov rbx,rax
sal rdx,32
or rbx,rdx

call f

rdtscp
sal rdx,32
or rax,rdx
sub rax,rbx

; Neste ponto, RAX o contedo da varivel c.

Que um cdigo bem decente...


S tome cuidado com as otimizaes do compilador. Se for usar o mximo de otimizaes, voc
pode topar com o rearranjo de cdigo que no medir coisa alguma (as leituras do TSC podem ser
rearranjadas). Para evitar isso, recomendo que compile o cdigo a ser testado em um mdulo

91
separado e use a opo de otimizao '-O3'. J o cdigo que contm as chamadas s funes
intrnsecas _mm_fence, __rdtsc e __rdtscp, num mdulo com a otimizao '-O0'.

Melhorando a medio de performance


Quando voc brincar um bocado com a medio de ciclos de clock perceber que, para uma mesma
funo sob teste, a contagem de ciclos diferente em cada medida. Isso perfeitamente explicvel
graas ao trabalho feito pelo processador ao gerenciar tarefas, pginas, cache, interrupes etc. Ou
seja, o valor que voc est medindo nunca ser exato44.
Como precisamos obter um significado a partir dos valores lidos e no temos exatido, nada mais
justo que usar o recurso da estatstica. E o meio mais simples de obter significado de um conjunto
de valores olhar para a mdia. Se tenho 50 valores ligeiramente diferentes, posso dizer que um
valor nico representando a mdia desses valores aquilo que procuro.
Para funes pequenas, uma maneira de obter uma boa mdia medir a execuo de diversas
chamadas mesma funo. A medio anterior poderia ser feita assim:

unsigned long c0, c1;
int dummy;

_mm_mfence();
c0 = __rdtsc();
/* Funo f() executada 50 vezes! */
f(); f(); f(); f(); f(); f(); f(); f(); f(); f();
f(); f(); f(); f(); f(); f(); f(); f(); f(); f();
f(); f(); f(); f(); f(); f(); f(); f(); f(); f();
f(); f(); f(); f(); f(); f(); f(); f(); f(); f();
f(); f(); f(); f(); f(); f(); f(); f(); f(); f();
c2 = __rdtscp(&dummy);

/* Imprime a mdia simples de 50 medidas! */


printf("Ciclos de clock: %lu\n", ((c2 c1) / 50));

Substituir as 50 chamadas explcitas acima por um loop no faz o que parece:


for (i = 50; i > 0; --i)
f();

O loop no ser desenrolado. Pelo menos no totalmente! mais provvel que voc acabe com
um cdigo assim:
mov ebx,50
.L1:
call f
dec ebx
jg .L1

Ao invs de 50 chamadas voc acabar adicionando uma inicializao (de EBX), 50 decrementos e
49 saltos condicionais, no melhor dos casos... Assim, sua medio no ser somente da funo, mas
do cdigo de controle do loop tambm. E, assim, voc d adeus preciso que queria na medio!

O clculo do ganho de performance


Para permanecermos na mesma sintonia, o clculo do ganho de performance sempre feito em
relao a alguma rotina original que, supostamente, mais lenta que a rotina otimizada. A relao
esta:

44 No apndice B mostro um jeito de aumentar a preciso das medidas.

92
Onde G o ganho percentual da rotina otimizada em relao rotina original.
Suponha que a funo original gaste 1000 ciclos de clock e que a rotina otimizada gaste 100.
Temos:

Ou seja, temos um ganho de performance na funo otimizada de 900% em relao funo


original (ou, vendo de outra forma, Foriginal 10 vezes mais lenta!).
E se tivssemos o contrrio? Se a rotina otimizada gastasse mais ciclos que a rotina original?
Suponha que Foriginal gastasse 100 ciclos e Fotimizada, 125. O valor de G ser de -25%, que
exatamente a perda de performance da rotina otimizada (note o valor negativo!):

claro que, para uma medida estatstica mais precisa, teramos que realizar diversas medies,
obter o mdia dos valores e calcular o erro relativo. Por exemplo. Se obtenho 1300 e 1280 ciclos de
duas medies ento ela provavelmenet gasta 12900.78% ciclos:

Acontece que essa variao (0,78%) to pequena que no vale o esforo. Erros menores que uns
2% podem ser desconsiderados... Mas, suponha que tenhamos 3 medidas: 1300, 200 e 800. O valor
mdio ser de cerca de 767 e o erro relativo ser de 69,5%. Um erro demasiadamente grande.
Neste caso vale a pena usar a mdia45...
Toda vez que formos comparar performance essa a regra do jogo: Medimos a quantidade de ciclos
de clock de duas rotinas, a original e a otimizada, e aplicamos a formula acima para obter o ganho
ou a perda. Fazmos isso com vrios medidas e usamos a mdia para nos orientarmos, considerando
a faixa de erro das medies...

Quando um ganho vale pena?


Voc ver, algumas vezes, que descarto valores pequenos de ganho como desprezveis. Por
exemplo, se uma rotina original gasta 120 ciclos e uma otimizada gasta 115, o ganho de
performance de, aproximadamente, 4.3%. O valor percentual pequeno, mas no somente ele o
avaliado. O detalhe que houve um ganho de apenas 5 ciclos de clock!
Se o ganho relativo for pequeno e o ganho absoluto tambm for, ento descarto esse suposto ganho,
considerando-o desprezvel. Isso diferente, por exemplo, se tivssemos Foriginal=11000 e
Fotimizada=10300. Isso nos d um ganho relativo de 6.8% que pequeno mas, em valores
absolutos, ganhamos 700 ciclos de clock. Neste ltimo caso a performance relativa no impressiona

45 Pode ser interessante empregar uma anlise estatstica mais refinada, usando desvio padro para determinar o
quanto os valores desviam-se da mdia aritmtica simples.

93
mesmo, mas o ganho absoluto pode ser interessante, no caso dessa rotina otimizada estar sendo
usada dentro de um loop.
Os dois valores tm que ser pesados e, tambm, as circunstncias... No vale pena usar uma
verso otimizada de uma rotina que ser executada apenas uma vez quando o ganho relativo
pequeno, mas vale pena us-la quando o ganho absoluto grande e essa rotina usada dentro de
um loop ou chamada diversas vezes.

Usando perf para medir performance


Linux disponibiliza uma ferramenta, por linha de comando, que permite usar diversos performance
counters. Alm dos ciclos gastos por uma rotina, voc pode medir cache misses, page misses, erros
na predio dos branches etc. O nosso cdigo de teste ficaria simplesmente assim:
/* test.c */
extern void f(void); /* rotina sob teste. */

int main(int argc, char *argv[])


{
f();

return 0;
}

Ao usar perf voc no mede a performance de uma rotina especfica, mas do seu programa como
um todo. Ao compilar e linkar (sem usar o tsc.asm), obtemos o programa 'test'. Da podemos usar:
$ perf stat -r 20 ./test

Performance counter stats for './test' (20 runs):

0,143854 task-clock (msec) # 0,599 CPUs utilized ( +- 0,72% )


0 context-switches # 0,348 K/sec ( +-100,00% )
0 cpu-migrations # 0,000 K/sec
117 page-faults # 0,813 M/sec
489.242 cycles # 3,401 GHz ( +- 0,80% )
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
407.411 instructions # 0,83 insns per cycle ( +- 0,30% )
77.121 branches # 536,103 M/sec ( +- 0,23% )
2.401 branch-misses # 3,11% of all branches ( +- 4,01% )

0,000240241 seconds time elapsed ( +- 1,05% )

O programa 'test', executado 20 vezes (opo '-r 20'), gastou em mdia 482242 ciclos de clock,
executou 407411 instrues, fez 77121 saltos. Nesse percurso, que durou apenas 240 s, ocorreram
117 page faults e 2401 branch misses. Mas, note, foi o programa como um todo, incluindo as
rotinas de inicializao e finalizao da libc.
claro que voc pode tentar descontar os valores obtidos por um programa que no faz nada. Mas,
um programa que no faz nada no tem muitas inicializaes e finalizaes para fazer, tem?
O utilitrio perf muito bom para dar uma idia do que est acontecendo. Estamos interessados na
performance de rotinas, no do programa geral. Isso significa que o mtodo usando TSC_READ
mais til, neste contexto. Mesmo sendo mais impreciso...

94
Captulo 8: Otimizaes
automticas
Compiladores C e C++ modernos conseguem melhorar muito o cdigo criado pelo programador
usando uma srie de algoritmos de otimizao, reorganizando o cdigo final. Aqui eu vou te
mostrar algumas das otimizaes automticas realizadas por esses compiladores.

Nveis de otimizao
Existem dzias de otimizaes que podem ser habilitadas no compilador. Entender e consultar toda
as possibilidades uma tarefa cansativa. Para evitar o cansao o compilador disponibiliza 6
conjuntos de otimizaes, habilitadas pelas opes '-O0', '-O1', '-Os', '-O2', '-O3', '-Ofast'. As opes
esto listadas aqui na ordem em que geram cdigo do menos otimizado ao mais otimizado.
A opo '-O0' criar o cdigo mais puro possvel. Ao contrrio do que parece, esse nvel no
significa que nenhuma otimizao ser feita. '-O0' realiza otimizaes mnimas no seu cdigo. Isso
tende a gerar funes maiores e menos performticas e, ao mesmo tempo, faz com que o
compilador traduza o seu cdigo-fonte quase que ao p da letra. Ele til quando for debugar
cdigo. Tambm til quando voc construir cdigos de teste para medir a velocidade, via
TSC_READ. Essa opo evita que o compilador faa com que parte do seu cdigo desaparea,
devido s otimizaes.
As opes '-O1', '-O2' e '-O3' habilitam otimizaes adicionais, progressivamente mais agressivas.
Todas elas habilitam, por exemplo, otimizaes de dead code elimination (dce), data store
elimination (dse), guess branch probability (onde o compilador tenta adivinhar se um salto
condicional ser feito ou no til para otimizar 'if's e loops). Mas a opo '-O2' habilita
otimizaes de alocao de registradores, alinhamento de loops, alinhamento de saltos e global
common subexpression elimination. A opo '-O3' habilita as otimizaes mais agressivas, incluindo
vetorizao e funes inline.
A opo '-Os' a mesma coisa que a opo '-O2', mas ela diz ao compilador que temos preferncia
por gerar cdigos pequenos ('s' de small). Essa opo no gera os menores cdigos possveis, ela s
uma dica para que o compilador escolha instrues ou conjuntos de instrues mais simples e
menores que as escolhidas pela otimizao '-O2'.
A opo '-Ofast' a opo '-O3' com algumas adies ainda mais agressivas.
A opo '-O', sem o valor numrico, sinnima de '-O1'.

Common Subexpression Elimination (CSE)


Essa uma das otimizaes mais bsicas e mais interessantes. O nome dessa tcnica bem
evidente. Trata-se de eliminar expresses comuns. Suponha que seu programa tenha algo assim:
x = 2 * a + 3 * b + c;
y = 2 * a + 3 * b + d;

Repare que parte das duas expresses tm, em comum, a sub-expresso 2 * a + 3 * b. Ao habilitar
CSE o compilador criar cdigo equivalente a isto:
_tmp = 2 * a + 3 * b;
x = _tmp + c;
y = _tmp + d;

95
O ganho de performance bvio: Ao invs de quatro multiplicaes e quatro adies, teremos duas
multiplicaes e trs adies. O ganho de performance pode chegar a perto de 100%! Outro efeito
colateral a potencial diminuio do cdigo.
CSE agressivo pode ser um problema ao usar nossa rotina de obteno do TSC. Em alguns casos o
compilador pode entender que a chamada TSC_READ e o clculo envolvendo a mesma varivel
uma CSE e simplesmente eliminar uma das chamadas. Por isso uso a opo -O1 ou -O2, ou at '-
O0', na compilao do cdigo testador.

Desenrolamento de loops
Outra otimizao simples. Se voc precisa chamar uma funo quatro vezes, para facilitar a
codificao voc poderia fazer algo assim:
for (i = 0; i < 4; i++)
DoSomething();

Ao ver algo assim o compilador poder desenrolar esse loop, eliminando-o completamente, e gerar
quatro chamadas para DoSomething(). Este o cenrio mais bvio. No entanto, o compilador pode
ser mais esperto com loops com mais iteraes. Suponha que ao invs de 4 tenhamos 400. O
compilador poder escolher fazer um desenrolamento parcial. Algo mais ou menos assim:
/* Suponha que esse seja o loop original. */
for (i = 0; i < 400; i++)
DoSomething();

/* O compilador poderia substituir por isso: */


for (i = 0; i < 50; i++)
{
DoSomething();
DoSomething();
DoSomething();
DoSomething();
DoSomething();
DoSomething();
DoSomething();
DoSomething();
}

Aqui o compilador escolheu continuar com um loop, para poupar o cache L1, mas desenrol-lo para
que tenhamos menos saltos condicionais. Desenrolamento de loops pode tornar seu cdigo maior e
no melhorar tanto assim a performance, por isso o compilador tem um limite para o
desnrolamento. O limite definido de acordo com a arquitetura e parmetros internos do
compilador (mas pode ser ajustado)...
Todas as opes de otimizao, exceto pelo nvel 0, habilitam alguma forma de loop unrolling.

Movendo cdigo invariante de dentro de loops (Loop Invariant Code Motion)


Alguns cdigos podem ser rearranjados drasticamente para evitar processamentos desnecessrios
Considere o seguinte:
for (i = 0; i < 100; i++)
{
x = a + b;
DoSomething(x);
}

A varivel 'x' calculada 100 vezes considerando os mesmos valores contidos nas variveis 'a' e 'b'.
Mas 'a' e 'b' no so atualizadas dentro do loop... Poderamos mover a linha onde 'x' calculado para
fora do loop sem nenhum problema. E isso que o compilador far:

96
x = a + b;
for (i = 0; i < 100; i++)
DoSomething(x);

Eliminao de cdigo morto (Dead Code Elimination)


s vezes criamos rotinas que simplesmente jamais sero executadas. Ou, quando o so, no fazem
coisa alguma. O compilador tentar eliminar esses cdigos. Eis alguns exemplos:
for (i = 0; i < 10000; i++); /* loop provavelmente ser eliminado. /

if (k && !k) /* DoSomething() jamais ser chamado. */


DoSomething();

while (1)
DoSomething();
DoSomethingElse(); /* DoSomethingElse() jamais ser chamado. */

Outro exemplo que voc pode encontrar o uso de funes que no retornam valor algum, no
alteram variveis globais. O compilador pode eliminar essas funes em alguns casos. Por exemplo:
#include <stdio.h>

int f(int x, int y) { return x*y; }

void main(void) { f(10, 20); puts(ok); }

Quando usamos uma opo de otimizao difernte de -O0 o compilador vai ser livrar da chamada
para f sem pestanejar.
Ainda outro exemplo de cdigo morto o assinalamento de uma varivel para si mesma. um
macete til para evitar avisos de parmetros no usados em suas rotinas. Por exemplo, o cdigo
abaixo talvez te d esse tipo de aviso:
int f(int x, int y) { return x+x; }

Se voc realmente quer que a funo acima recebe dois parmetros, mas no use um deles, evite o
aviso fazendo assim:
int f(int x, int y) { y=y; return x+x; }

A atribuio de y para si mesmo ser eliminada pelo compilador e ele no pode reclamar que voc
no usou y...

Eliminao de armazenamento morto (Dead Store Elimination)


Do mesmo jeito que cdigo morto descartado na otimizao, o no uso de variveis declaradas
tambm faz com que elas sejam descartadas, quando possvel. Mas, o compilador pode te avisar
disso...

Previso de saltos (Branch Prediction)


Desde o 486 os processadores Intel tm um recurso chamado branch prediction. Graas natureza
superescalar do processador (execuo de diversas instrues simultaneamente, na mesma pipeline),
quando o ele encontra um salto condicional, possvel que o estado atual dos flags no reflita o
estado em que estaro quando a instruo for executada. Quer dizer: Antes dos 486 toda instruo
de salto serializava o processador, colocando uma unidade de execuo em estado idle (de espera)
at que todas as instrues antes do salto fossem executadas. S ento poderia haver certeza do

97
estado dos flags.
Na arquitetura superescalar o processador usa um recurso estatstico para tentar prever se um
salto condicional ser feito ou no.
O compilador tenta dar uma mozinha tentando prever se um salto vai ser sempre feito ou no.
Quando escrevemos algo assim:
if (condicao)
{

}

O compilador tende a gerar um cdigo mais ou menos assim:


cmp [condicao],0
je .L1

.L1:

Note que o salto feito se a comparao for falsa! Mas, para aproveitar o branch prediction do
processador, em loops, o compilador tende a modifica a posio onde a condio de parada de um
loop feita. Por exemplo:

while (condicao)
{

}
-----%<----- corte aqui ----%<-----
; cdigo que voc espera que o compilador crie:
cmp [condicao],0
je .L1
.L2:

jmp .L2
.L1:
-----%<----- corte aqui -----%<-----
; cdigo gerado pelo compilador:
jmp .L1
.L2

.L1:
cmp [condicao],0
jne .L2

Neste caso esperado que a comparao seja verdadeira para cada iterao do loop. Assim, se o
compilador no tentasse adivinhar se saltos sero ou no tomados, misturar loops e 'if's causaria
uma confuso dos diabos nos algoritmos de branch prediction do processador, tornando o cdigo
mais lento.
Essa adivinhao no perfeita. Podemos dar uma mozinha ao compilador usando uma funo
embutida chamada __builtin_expect. Ela usada para melhorar a adivinhao do compilador
consideravelmente. Como exemplo, suponha que esperemos que uma condio de um if seja falsa
na maioria das vezes. Poderamos escrever algo assim:
if (__builtin_expect(x <= 0, 0))
{

}

Se soubermos que 'x' ser positivo ou zero na maioria das vezes, o cdigo acima ajudar o
compilador a organizar os saltos condicionais para aproveitar, ao mximo, o branch prediction.
O kernel usa duas macros que, por sua vez, usam a funo __builtin_expect. Trata-se de likely e
unlikely:

98
#define likely(c) __builtin_expect(!!(c), 1)
#define unlikely(c) __builtin_expect(!!(c), 0)

Esses nomes so mais intuitivos... likely pode ser traduzido para possivelmente...

Simplificaes lgicas
O que voc espera que o compilador faa com expresses como esta?
y = a | (a & b);

No evidente, mas a expresso acima simplificada para, simplesmente, y = a;. O GCC (e


tambm outros bons compiladores) realizam facilmente as seguintes simplificaes lgicas:

Expresso completa Expresso simplificada


a&0 0
a & ~0 a
a|0 a
a | ~0 ~0
a&a a
a|a a
a | (a & b) a
(a & b) | (a & ~b) a
(a & b) | (a & c) a & (b | c)
(a & b) | (~a & c) | (b & c) a?b:c
(a & ~b) | (~a & b) a^b
Tabela 8: Simplificaes lgicas feitas pelo compilador.

Essas e outras simplificaes so esperadas do ponto de vista do compilador. Elas no so vlidas


apenas para expresses de atribuio, mas tambm acontecem com os operadores lgicos booleanos
(|| e &&). Por exemplo, se tivermos:
if (((a < 0) && (b < 0) && (c < 0)) ||
((a < 0) && (b >= 0) && (c < 0)) ||
((a >= 0) && (b < 0) && (c < 0)) ||
((a >= 0) && (b >= 0) && (c < 0)))
DoSomething();

Esse monte de comparaes ser simplificado, pelo compilador, para:


if (c < 0)
DoSomething();

Este exemplo parece bvio e esse recurso bem interessante. Mas ele pode ser problemtico se
voc estiver lidando com dispositivos externos. Por exemplo: Uma das rotinas do meu antigo curso
de assembly tinha, numa das listagens, algo assim:
*bitplane |= 0;

O objetivo era ler o contedo da memria de vdeo, fazer um OR com todos os bits zerados e gravar
o contedo na memria de vdeo novamente. Isso pode ser feito numa nica instruo:

99
or byte [bitplane],0

Isso era um passo necessrio para atualizar os latches dos bitplanes da memria de vdeo. Acontece
que com as otimizaes ligadas, o compilador vai ignorar a linha acima... Afinal, fazer um OR com
zero a mesma coisa que no fazer OR algum!
Casos como esse e ainda bem que so rarssimos so srios candidatos ao desenvolvimento em
assembly, no em C!

Simplificao de funes recursivas


O GCC tende a eliminar as chamadas recursivas automaticamente. O cdigo abaixo famoso.
Trata-se do clculo de fatorial:
unsigned long fatorial(ungigned long x)
{
if (x <= 0) return 1;
return x*fatorial(x-1);
}

Sem otimizaes o compilador criar cdigo como o mostrado abaixo:


fatorial:
sub rsp,8

mov [rsp],rdi ; usa a pilha para armazenamento temporrio.


cmp qword [rsp],0
jg .L2
mov rax,1
jmp .L3

.L2:
mov rax,[rsp]
sub rax,1
mov rdi,rax
call factorial ; Eis a chamada recursiva.
imul rax,[rsp]

.L3:
add rsp,8
ret

O problema com a recursividade que ela coloca presso sobre a pilha. Cada chamada fatorial,
acima, usa 16 bytes da pilha: Oito bytes para o parmetro, e oito para o endereo de retorno,
colocado l pela instruo CALL. Se passarmos um parmetro com valor gigantesco, logo teremos
uma exceo de Stack Overflow nas mos, por causa dos muitos empilhamentos.
O cdigo gerado com a mxima otimizao no tem quaisquer chamadas recursivas. O compilador
transforma o cdigo original em algo assim:
long fatorial(long x)
{
long r = 1;

while (x > 1)
r *= x--;

return r;
}

claro que certas recursividades no podem facilmente ser simplificadas. De fato, algumas tm
simplificaes matematicamente impossveis. Nesses casos o compilador no tem o que fazer a no
ser incluir a chamada recursiva. Vale a pena dar uma olhada no cdigo gerado, em assembly gerado
pelo GCC...

100
Auto vetorizao (SSE)
No modo x86-64, SIMD (SSE) est sempre disponvel. Este o motivo dos registradores de XMM0
at XMM7 serem usados como parmetros de entrada de valores em ponto flutuante, na conveno
de chamada. Mas, o compilador tende a usar apenas o primeiro componente desses registradores.
SSE suporta agrupar at 4 floats num mesmo registrador.
Realizar 4 operaes com floats (ou 'ints') ao mesmo tempo o que chamamos de vetorizao.
Em certos casos o compilador decide usar vetorizao para tornar o cdigo mais eficiente. Eis um
exemplo:
extern int a[256], b[256], c[256];

void f(void)
{
int i;

for (i = 0; i < 256; i++)


a[i] = b[i] + c[i];
}

J que temos 256 inteiros em cada um dos arrays, o cdigo gerado tender a usar vetorizao com
valores inteiros (32 bits):
f:
xor eax,eax
align 4
.L2:
movdqa xmm0,[b+rax]
paddd xmm0,[c+rax]
movdqa [a+rax],xmm0
add rax,16
cmp rax,1024
jne .L2
rep ret

Isso equivale, mais ou menos, ao cdigo, em C46:


extern int a[256], b[256], c[256];

void f(void)
{
__m128i *pa = (__m128i *)a,
*pb = (__m128i *)b,
*pc = (__m128i *)c;

for (i = 0; i < 64; i++)


*pa++ = _mm_add_epi32(*pb++, *pc++);
}

Mas, ateno! O compilador no tentar vetorizar referncias a arrays se o tamanho do loop no for
conhecido de antemo. A funo abaixo no tende a ser vetorizada:
void f2(int *out, int *a, int *b, size_t count)
{
while (count--)
out[count] = a[count] + b[count];
}

O compilador no tem como saber qual o valor de 'count' de antemo!

Otimizaes atravs de profiling


Todas as otimizaes do compilador so feitas de maneira esttica, sem levar em conta a execuo

46 Ver captulo Instrues Extendidas.

101
de facto do cdigo gerado. O compilador tenta usar esse conjunto de regras para gerar o cdigo
mais performtico possvel, de acordo com o nvel de otimizao selecionado, mas nem sempre
consegue!
O GCC possui o recurso de gravar informaes sobre a execuo do cdigo gerado para que
possamos compilar o cdigo uma segunda vez levando em conta essas informaes. uma
compilao de dois passos: Primeiro, compilamos o cdigo com as otimizaes selecionadas e
geramos um executvel. Depois, executamos o cdigo e um arquivo com extenso .gcda criado,
contendo informaes sobre saltos e outros detalhes. Ao compilar uma segunda vez, usando este
arquivo, o compilador ter uma base melhor para decidir reorganizar o cdigo.
Usar o recurso de branch profiling simples assim:
$ gcc -O3 -march=native -fprofile-generate -o test test.c
$ ./test

$ ls -l
-rwxr-xr-x 1 user user 20200 Jan 6 11:37 test
-rw-r--r-- 1 user user 113 Jan 6 11:37 test.c
-rw-r--r-- 1 user user 140 Jan 6 11:42 test.gcda
$ gcc -O3 -march=native -fprofile-use -o test test.c
$ ls -l
-rwxr-xr-x 1 user user 6810 Jan 6 11:44 test
-rw-r--r-- 1 user user 113 Jan 6 11:37 test.c
-rw-r--r-- 1 user user 140 Jan 6 11:42 test.gcda

A opo '-fprofile-generate' injeta cdigo para colher informaes do seu programa e armazen-lo
no arquivo '.gcda' e, por isso, o executvel ficar bem mais que deveria. Ao compilar pela segunda
vez, usando a opo '-fprofile-use' o compilador usa essas informaes e otimiza melhor o cdigo
gerado. Ambas as opes aceitam parmetros informando o nome do arquivo '.gcda' que, por
default, tem o mesmo nome do arquivo '.c'.

102
Captulo 9: Caches
Este captulo contm informaes conceituais sobre o funcionamento do cache que no condizem
totalmente realidade. Deixei de fora o conceito de set associativity porque creio que ele
suprfluo para o desenvolvedor de software. Trata-se de detalhe interno do processador que, ao meu
ver, interessa apenas ao fabricante. Deixando esse conceito de fora, o que resta serve perfeitamente
para os propsitos do entendimento das vantagens e limitaes impostas pelos caches.
Cache uma estrutura usada para conter, dentro do processador, um pedao de um subconjunto de
dados originalmente contidos na memria RAM. O exagero do pedao do subconjunto
proposital. Quero dizer que apenas um pedao pequeno da memria copiado para os caches.
A existncia dos caches deve-se ao fato de que a memria RAM lenta. E tem que ser, j que
memrias rpidas so muito caras. Os caches, ento, so artifcios usados para aumentar a
velocidade de acesso aos dados que o processador manipula. Para fazer isso, esse acesso nunca
feito diretamente na memria RAM. Tem que, obrigatoriamente, passar pelos caches.

Figura 10: Acesso memria sempre feita pelos caches.

Processadores modernos implementam vrios nveis de cache. O cache L1 (Level 1) aquele mais
prximo aos circuitos de manipulao de dados do processador. Ele dividido em dois tipos: L1d e
L1i, com 32 KiB cada. Os sufixos 'd' e 'i' especificam, respectivamente, data e instruction. Isso
significa que o cache L1, na verdade, so dois: Um dedicado para dados e outro para instrues.
Existe um segundo nvel (L2) de maior capacidade, mas ele mistura tudo o que est na memria:
cdigo e dados. No h diviso. O motivo da existncia do segundo nvel que o primeiro
pequeno e, por isso, precisa ser recarregado frequentemente. O cache L2 o cache do cache, ele foi
criado para evitar a recarga do cache L1 diretamente a partir da memria RAM, mais lenta. Nveis
maiores podem ser encontrados em processadores mais modernos. No incomum encontrar um
cache L3 e arquiteturas como Haswell suportam at um cache L4...
Do ponto de vista da performance, precisamos melhorar ao mximo o acesso feito ao cache L1,
tanto o L1d, quanto o L1i. No h muita necessidade de nos preocuparmos com o L2 porque ele
grande: tem uns 256 KiB. Por isso o restante do captulo tratar o cache como se tivesse apenas um
nvel.

A estrutura dos caches


Diferente da memria do sistema, que organizada de maneira linear com um byte atrs do outro, a
memria do cache organizada em linhas. Cada linha tem a estrutura que contm bits de
controle, usados internamente pelo processador. Dentre outras coisas eles determinam a validade da
linha... Contm tambm uma etiqueta ou tag, seguida de 32 a 64 bytes de dados, dependendo da
arquitetura do processador47.

47 Vou lidar com linhas de tamanho de 64 bytes daqui pra frente. Processadores como i5 e i7 tm caches com linhas
desse tamanho, normalmente.

103
Figura 11: Estrutura de uma linha do cache L1.

Essa tag nada mais do que os bits superiores do endereo fisico48 associado a uma linha. O
processador usa essa tag para determinar se um bloco de memria est contido no cache ou no.

Figura 12: Estrutura de um endereo linear no contexto do cache L1.

A figura anterior mostra como um endereo fisico interpretado pelo cache. claro que o offset
tem apenas 6 bits de tamanho (26=64). O campo linha especifica uma das 512 linhas do cache. E
o campo tag a identificao que ser comparada com a respectiva linha no cache.
Se o endereo nos d o nmero da linha no cache, ao tentar acessar memria, a primeira coisa que o
processador far determinar se a linha vlida (ela pode ainda no ter sido usada!) e contm a
mesma tag do endereo. Caso a tag exista no cache, se a linha for invlida, ela ser carregada do
cache L2 (a mesma coisa acontece do cache L2 para o L3, se esse existir, e do L3 para a memria
do sistema).
Se a linha for vlida, mas a tag no for a mesma, ento a linha precisar ser trocada. O
contedo atual ser escrito no cache de nvel superior e depois os 64 bytes correspondentes nova
tag sero lidos. De novo, o processo repetido nos caches de nvel superior...
Cada leitura e escrita de linhas do cache L1 gasta de 1 a 4 ciclos de clock, dependendo do
processador, j para os caches de nvel superior a quantidade de ciclos gastos aumenta (uns 12
ciclos na arquitetura Haswell). E se esse processo de validao de linhas ocorre sempre que o
processador tentar ler/escrever num endereo de memria, fica evidente que se tivermos algum
meio de manter linhas de cache vlidas e carregadas, menos tempo o processador ter que gastar
fazendo trocas de linhas.
Cada processador lgico nos ncleos tem seu prprio par de caches, L1i e L1d. J os caches L2 e
L3 (se existir um) so compartilhados por todos.

Determinando o tamanho de uma linha


Pode ser til o seu programa saber o tamanho de uma linha do cache L1. Para obtermos essa
informao basta usa a instruo CPUID:
#include <stdio.h>
#include <cpuid.h>

int main(int argc, char argv[])


{
unsigned int a, b, c, d;

/* A operao 0x01 com CPUID nos d informaes sobre


o cache L1. */
__cpuid(1, a, b, c, d);

48 Veja sobre endereo linear no captulo sobre memria virtual.

104
/* bits 15~8 de EBX, multiplicados por 8
do o tamanho da linha de cache L1. */
printf("L1 cache line size = %u bytes).\n",
(b & 0xff00) >> 5);

return 0;
}

O dilema do alinhamento
Tentar conter dados e cdigo em uma nica linha de cache , obviamente, um fator decisivo para
garantir performance. Se seus dados ltrapassam o limite de uma linha, corre o risco das cpias
entre caches e a memria fsica acontecerem. A mesma coisa vale para as instrues em linguagem
de mquina.
Uma maneira de ter certeza de que uma linha est devidamente preenchida contarmos a
quantidade de bytes do cdigo ou dos dados a partir do incio de uma linha. S que isso
impraticvel! Concorda comigo que ficar contando os bytes para cada instruo em linguagem de
mquina tarefa que deixaria qualquer um meio maluco? Alm do mais, se tivermos que lidar com
nossos programas ao nvel de linguagem de mquina (micro-cdigos), ento qual a utilidade de
uma linguagem de programao? E, mesmo que voc tenha essa pacincia toda, o desperdcio de
memria pode ser grande na tentativa de evitar o overlap entre linhas.
Overlap?! Imagine que a ltima instruo numa linha de cache tenha 5 bytes de tamanho, mas os
primeiros 60 bytes j estejam em uso na linha. Ao colocar essa instruo de 5 bytes, 4 deles ficariam
na linha vlida do cache e o byte excedente ter que ficar numa outra linha que pode ou no estar
carregada no cache...
Voc poderia preencher os 4 bytes finais dessa linha com NOPs e colocar a prxima instruo na
linha seguinte, mas isso causar o mesmo problema e criar outro: Teremos instrues inteis que
gastam tempo!
Com dados ocorre o mesmo, mas mais controlvel. Suponha que seu cdigo use uma estrutura
assim:
struct mystruct_s {
long a[7]; /* 56 bytes: do offset 0 at 55. */
char b[5]; /* 5 bytes: do offset 56 at 60. */
long d; /* 8 bytes: do offset 61 at 68. (ops! Cruza duas linhas!) */
};

Ao declarar uma varivel desse tipo, se ela estiver alinhada com o incio de uma linha, o membro de
dados 'd' ainda assim cruzaria a fronteira entre duas linhas. Neste caso a soluo simples. Basta
alterar a ordem das variveis na estrutura:
struct mystruct_s {
long a[7]; /* 56 bytes: do offset 0 at 55. (linha 1) */
long d; /* 8 bytes: do offset 56 at 63. (linha 1) */
char b[5]; /* 5 bytes: do offset 58 at 62. (linha 2) */
};

Vamos usar duas linhas de qualquer jeito, mas ao no colocarmos uma varivel parcialmente em
cada uma, quando uma delas for trocada, a outra poder continuar vlida.
Ento, o dilema este: Sempre que for possvel, precisamos colocar instrues inteiras e dados
inteiros dentro de linhas de cache, evitando que eles cruzem a fronteira entre duas delas. Um jeito
de conseguir isso usando alinhamento. Se alinharmos o incio de nossas funes de 64 em 64
bytes garantiramos que, pelo menos, o incio da funo est no incio de uma linha do cache L1i.
Essa forma de alinhamento tambm imprticvel porque causar mais trocas de linhas dos caches

105
ao longo prazo. S temos 512 linhas disponveis no L1 e, hoje em dia, cdigo e dados podem ser
muito grandes...
Uma soluo de bom senso poderia ser o alinhamento por QWORD. Ele no desperdia tanto o
espao da linha e til, na arquitetura x86-64, onde ler um QWORD desalinhado, da memria para
um registrador de uso geral, causa uma penalidade de 1 ciclo de clock... Mas isso no to til no
que se refere s instrues. Uma nica instruo em linguagem de mquina pode ocupar at 15
bytes (esse o limite imposto pela Intel e pela AMD). Assim, no caso dos cdigos, mais til o
alinhamento de 16 bytes.
Isso no significa que voc no possa usar alinhamentos menores, como uma espcie de ajuste
fino... O compilador C tenta alinhar loops e incios de funes tanto quanto possvel, uma vez que
ele sabe onde os pontos crticos estaro. No caso de assembly, podemos alinhar nossas funes e at
mesmo loops usando a diretiva align do NASM:
align 16 ; entrada da funo alinhada!
f:
xor rax,rax
mov rcx,1000
jmp .L1

; J que o loop ficar retornando a esse ponto, alinhamos por QWORD!


; Se o salto condicional estiver muito longe (digamos, uns 128 bytes alm
; deste ponto) melhor alinharmos por 16 bytes tambm!
align 8
.L2:

.L1:
dec rcx
jnz .L2

No exemplo acima usamos dois alinhamentos diferentes. O incio da funo estar alinhada com o
incio de uma linha do cache L1i, mas a posio de retorno do loop pode no precisar disso.
Podemos alinh-lo por QWORD se o loop inteiro couber dentro de algumas poucas linhas do cache.
Com os dados o compilador tende a alinhar o incio de uma varivel por QWORD, mas nem sempre
consegue (como o caso das estruturas), se bem que isso caracterstica dependente de
implementao, de acordo com a especificao da linguagem C. Existem excees: Ao usar SIMD
os tipos __m64, __m128, __m256 e __m512 so, automaticamente, alinhados por QWORD
(__m64), a cada 16 bytes (__m128), 32 (__m256) e 64 bytes (__m512). Esse ltimo tipo, claro,
cabe numa linha inteira! Esses tipos so forosamente alinhados para garantir a boa performance de
instrues de carga a partir da memria, caso contrrio o processador gastar mais ciclos de clock,
independente do tipo ter cruzado a fronteira de uma linha ou no. Se, alm do desalinhamento, o
processador tiver que cruzar a fronteira entre linhas de cache, ento muito mais ciclos sero
adicionados execuo da instruo e, em alguns casos, uma exceo do tipo segmentatil fault pode
ocorrer.

Dica para usar melhor os caches...


Mantenha seus cdigos e dados pequenos! Se puder encurtar seus loops para que caibam em uma
linha de cache, tanto melhor. Seno, procure no consumir muitas linhas. Lembre-se que, alm do
seu programa, existe o kernel, device drivers, scripts etc, sendo executados!
Um cdigo que tem um loop com tamanho de mais de 4 KiB 49 pode causar srios problemas de
performance. Neste caso, 64 linhas de cache tero que estar vlidas para todo o loop. Mas, dentro
do loop podemos ter chamadas para outras funes, o que pode invalidar parte desses linhas,
causando a perda de centenas ou milhares de ciclos de clock.

49 O motivo dos 4 KiB ficaro claros no captulo sobre memria virtual.

106
Esteja ciente para o fato de que essa dica impraticvel na maioria das vezes, mas importante que
seja seguida sempre que possvel. Na prtica voc queimar a mufa em rotinas que eleger como
crticas e resolver desenvolv-las em assembly.

Audaciosamente indo onde um byte jamais esteve...


Por causa das frequentes trocas de linhas entre os caches e a memria, outro conceito importante
quando lidamos com os caches o de temporalidade. Voc pode encarar o funcionamento dos
caches sob o aspecto espacial (o tamanho de uma linha) e temporal (o tempo em que os dados
ficaro sob os cuidados do cache). Quando a AMD e a Intel incorporaram SIMD nos seus
processadores, comearam tambm a preocuparem-se em controlar a temporalidade de blocos de
dados contidos nos caches.
Ao aumentar o tamanho dos registradores que precisam acessar variveis alinhadas, a presso nos
caches aumenta consideravelmente. Pode ser til dizer ao processador que uma linha deve ser
mantida vlida por mais tempo que o necessrio, evitando a recarga do cache cada vez que um
bloco de 16 bytes (SSE) seja lido ou gravado. Outra maneira de encarar a temporalidade o quo
fcil o processador pode esquecer uma linha de cache.
O ajuste fino da temporalidade feito em todos os nveis de cache ao mesmo tempo. As
instrues PREFETCHxx so usadas para dar uma dica ao processador (que pode respeit-la ou
no) sobre a temporalidade de uma linha e, de lambuja, carregar (prefetch) a linha no cache L1.
Existem 3 instrues desse tipo:
Instruo Significado
PREFETCHTx Onde x um valor entre 0 e 3.

Quanto menor o valor de x, menos esquecvel


a linha. O mnemnico pode ser entendido
como prefetch t#. Onde Tx o nvel de
temporalidade.
PREFETCHNTA A parte NTA significa Non-Temporal
Access. Esse PREFETCH pe a dica de que a
linha no pode ser facilmente esquecida.
PREFETCHW O W diz que a linha ser cacheada com a dica
de que o dado est sempre pronto para ser
escrito. Esse modo invalida os caches de alta
ordem para a linha, fazendo com que ela precise
ser escrita nesses nveis quando a linha for
esquecida.

necessrio tomar cuidado com a instruo PREFETCH... Embora o ajuste fino seja til em certas
circunstncias, evitando gastos de ciclos de clock na troca de linhas, isso pode colocar grande
presso no cache L1.

Funes inline e a saturao do cache


Quando algum aprende sobre funes inline nas linguagens C e C++ tende a us-las para ganhar
performance. Afinal, chamadas e retorno de funes gastam uns 20 ciclos de closk. O problema de
abusar das funes inline justamente a saturao do cache L1i.

107
Sempre que uma funo inline usada, o cdigo da chamada, que era um simples CALL,
substitudo pela incorporao do cdigo dessa funo inline na funo chamadora. Isso tem o
potencial de tornar seu cdigo muito grande e cdigos grandes violam o princpio mantenha seus
cdigos pequenos.
Vale a pena perder algumas dezenas de ciclos com o par de instrues CALL/RET do que usar
funes inline que podem consumir milhares de ciclos adicionais por causa do cache saturado!
Repare que, em C, existem duas maneiras de tornar uma funo inline:
1. Atravs do uso do atributo inline na assinatura da funo que apenas uma dica ao
compilador que pode ignor-la;
2. Colocando chamadas a funes declaradas no mesmo mdulo.
Para evitar o segundo caso basta declarar o prottipo da funo no mdulo chamador e declarar a
definio da funo em outro:
/* func.c */

/* A funo f DEFINIDA neste mdulo. */


int f(int x) { return x + x; }
-----%<----- corte aqui -----%<-----
/* main.c */

/* A funo f somente DECLARADA neste mdulo.


Nunca ser inline, mesmo se usarmos otimizao -O3! */
extern int f(int);

int g(int x)
{ return f(x) + 2; }

108
Captulo 10: Memria Virtual
H anos vi uma boa analogia a respeito de memria virtual: Um palhao tenta equilibrar trs ou
mais bolinhas coloridas, enquanto as joga no ar. Duas delas estaro o tempo todo nas mos dele,
mas as outras sempre estaro suspensas. Eventualmente uma das bolas que estava nas mos do
palhao ir voar e uma que estava voando cair em uma das mos. O palhao no capaz de
trabalhar com todas as bolinhas ao mesmo tempo, ento uma delas ter que ser jogada e
recuperada depois.
Fica parecendo que o palhao est equilibrando trs bolinhas nesse ato de malabarismo quando,
na realidade, ele est lidando sempre com duas. mais ou menos assim que funciona o mecanismo
de page swapping. Enquanto o processador lida com uma poro da memria, outra pode no estar
fisicamente presente.

Virtualizao de memria
Memria endereada como se fosse um grande array. Um endereo um ndice para esse array.
Quando dizemos que a memria virtual, dizemos que um endereo no aponta mais para a
memria fsica, mas aponta para um modelo conceitual. Ou seja, um endereo pode apontar para
memria que no existe.
Neste modelo conceitual a memria parece linear, composta de blocos adjacentes (um bloco depois
do outro), do mesmo jeito que a analogia do array, mas o endereo que usamos para acessar dados
nesse modelo conceitual, chamado de endereo linear, precisa ser traduzido, atravs de um
mapeamento, para um endereo usado pela memria fsica (que no conceitual, mas existe de
fato!). Esse modelo conceitual chamado de virtual address space.

Espaos de endereamento
Um espao de endereamento um modelo conceitual de como um endereo interpretado.
Basicamente, existem 3 deles, nos modos i386 e x86-64:
Physical Address Space: O endereo o mesmo que aparece no barramento de endereos
do processador. o que usado para ler o contedo de um pente de memria.
Logical Address Space: O endereo obtido atravs do endereo base, vindo de um
descritor de segmento atravs de um registrador seletor de segmento, que adicionado a um
offset. A no ser que estejamos lidando com memria virtual, este espao , essencialmente,
o mesmo que o espao fsico.
Linear Address Space: Neste espao um endereo interpretado como um valor inteiro
que, decomposto, contm ndices para tabelas de traduo de pgina e um offset. Esse
endereo conhecido como endereo linear.
J que estamos lidando com o espao de endereamento virtual aqui, daqui para frente falarei
apenas de endereos lineares e fsicos.

Paginao
Voc no l um livro de uma vez s, l? Fazer isso um desafio at pra aquelas pessoas com
domnio absoluto em leitura dinmica. Por isso um livro dividido em captulos, sesses, tpicos,
pginas, pargrafos, sentenas, palavras e letras (e nmeros, e smbolos, ...). Essa diviso estrutural

109
faz com que voc possa passar de um item a outro sem que tenha que visualiz-los na totalidade.
A analogia boa com aquilo que seu processador faz com o acesso memria... O virtual address
space a totalidade terica da memria que pode ser usada (o livro, na analogia) e essa memria
dividida em pginas.
Considere uma pgina como sendo um bloco com tamanho de 4 KiB. Quero dizer, o processador, se
fizer uso do recurso da paginao, ele dividir o virtual address space nesses bloquinhos. Cada
pgina mapeada num conjunto de tabelas, e a manipulao dessas tabelas s possvel no ring 0,
portanto, apenas o kernel tem condies de manipul-las.

Figura 13: Endereo linear (32 bits)

Quando estamos lidando com o modo paginado do processador, o registrador de controle CR3
contm o endereo fsico, na memria, de uma tabela de 4 KiB chamada Page Directory Table
(PDT). Cada entrada dessa tabela contm o endereo fsico de outras tabelas de 4 KiB chamadas
Page Teble (PT). Essas ltimas contm entradas com os endereos fsicos de uma pgina, na
memria fisica.
Repare na estruturara de um endereo linear: Os primeiros dois campos (diretrio e tabela) so,
na verdade, ndices para entradas nas tabelas PDT e PT. O ndice para a PDT nos fornece uma
entrada para o diretrio de pgina (Page Directory Entry, ou PDE). Da mesma forma, o ndice
para a PT nos fornece um PTE (Page Table Entry).
De posse do endereo base da pgina, obtida da PTE, adicionando o offset contido nos 12 bits
inferiores do endereo linear, o processador obtm o endereo fsico desejado.
Este esquema maluco, que lembra um pouco a estrutura de subdiretrios em file systems, nos d
algumas vantagens... As entradas das tabelas contm, alm do endereo fsico, alguns bits de
controle: Alm do privilgio da pgina (supervisor correspondente ao ring 0 ou user
correspondente ao ring 3), temos tambm um bit indicando se a pgina est presente na memria
fsica ou no. Se a pgina no estiver presente na memria fsica, de acordo com esse bit, ento uma
exceo de page fault gerada, dando a chance ao processador de fazer algo a respeito (page
swapping).

Paginao e swapping
Paginao uma coisa que existe desde a poca dos famigerados mainframes. A ideia , justamente,
a de que memria fsica um recurso caro e com pouca capacidade 50. Nos antigos mainframes,
50 No caso dos antigos mainframes, a capacidade de memria era de apenas alguns quilobytes (KiB!).

110
algumas dessas pginas eram armazenadas em fita e outras eram mapeadas na memria fsica.
Sempre que quisssemos usar uma pgina que no estava presente na memria, uma pgina fsica
era gravada na fita e lamos outra pgina da fita que substitu a pgina previamente mapeada na
memria fsica. Mais ou menos como a analogia das bolinhas coloridas do palhao.
At hoje mais ou menos assim que a memria gerenciada pelo seu sistema operacional funciona.
S que ao invs de fita, as pginas podem ser armazenadas em disco: num arquivo ou numa
partio. Essa troca do espao fsico da memria com o contedo do disco conhecido como
page swapping (troca de pginas).
No vou descrever como a partio de swap (ou o arquivo de paginao, no caso do Windows)
estruturado aqui. Esse detalhe diz respeito somente ao sistema operacional e no tem qualquer
relevncia no que concerne performance de nossas aplicaes. O que importa que page
swappings, quando ocorrem, causam um tremendo atraso no processamento. O processador tem
que, literalmente, parar tudo o que est fazendo, acessar o disco, modificar alguma coisa na
estrutura das tabelas de pginas e s ento retornar ao processamento normal.

Tabelas de paginao no modo x86-64


No modo x86-64 essa estrutura de subdiretrios foi estendida: Cada tabela continua tendo, no
mximo, 4 KiB, mas as entradas agora tem 64 bits de tamanho. Por causa disso cada tabela passa a
ter 512 entradas e os ndices no endereo linear tm 9 bits de tamanho. E, graas necessidade de
um maior espao de endereamento, ao invs de apenas duas tabelas passamos a ter quatro. A
primeira chamada PML4T (Page Map Level 4 Table). A segunda, PDPT (Page Directory Pointer
Table), a terceira e a quarta so nossas velhas conhecidas PDT e PT.

Figura 14: Endereo linear tpico (64 bits).

Para facilitar a compreenso sobre essas tabelas, daqui por diante falarei apenas sobre a PT, que
mapeia uma pgina diretamente. Toda a discusso aplica-se s outras tabelas, s que elas apontam
para pginas que contm tabelas e no a pgina enderevel pelo componente offset do endereo
linear (isso ficou claro, at aqui, creio).

111
As entradas da Page Table
Cada entrada da PT (PTE) tem a seguinte estrutura:
struct table_entry_s __attribute__((packed))
{
unsigned long p:1; /* A pgina est presente? */
unsigned long rw:1; /* read-write ou read-only? */
unsigned long us:1; /* Privilgio: User ou Supervisor? */
unsigned long pwt:1; /* page write-through */
unsigned long pcd:1; /* page cache disable */
unsigned long a:1; /* acessada? (pode ser usada por software) */
unsigned long d:1; /* suja? (dirty?) (pode ser usada por software) */
unsigned long ps:1; /* PSE: Sempre 0 na PTE. */
unsigned long g:1; /* pgina global? */
unsigned long unused1:3; /* precisa ser zero! */
unsigned long addr:40; /* bits superiores do endereo fsico. */
unsigned long unused2:11; /* precisa ser zero! */
unsigned long xd:1; /* NX bit: Se setado no permite cdigo executvel. */
}

Apenas alguns desses bits nos interessam: O bit P, se zerado, indica que qualquer tentativa de acesso
a uma pgina para essa entrada causar um page fault. Neste caso, os demais bits da entrada no so
considerados para coisa alguma. Os bits RW e US indicam, respectivamente, se a pgina pode ser
lida e escrita (read/write) ou apenas lida (read only) e o privilgio necessrio para acess-la (user ou
supervisor). As pginas relacionadas ao kernel so marcadas como supervisor.
O bit G indica que essa pgina precisa ser mantida no cache global de pginas (chamado de TLB) a
todo custo, evitando que a traduo precise ser refeita a cada acesso pgina. Isso poupa tempo.
Falarei mais sobre TLBs adiante.
O bit XD indica se a pgina pode conter cdigo executvel ou no. No userspace, quando voc
aloca memria com malloc, o kernel cria entradas em tabelas de pginas para o seu processo com
o bit XD setado (XD sigla de eXecution Disable). Por isso no basta alocar memria, colocar um
monte de bytes contendo um cdigo em linguagem de mquina l e saltar para alguma posio
desse bloco alocado. Essa tcnica de code injection no funciona e mostrarei como faz-la
corretamente mais adiante.

As extenses PAE e PSE


Deu pra perceber que o esquema de paginao no aumenta a quantidade de memria diretamente
acessvel pela CPU? O endereo linear de 48 bits continua sendo traduzido para um endereo fsico
de 48 bits. Tudo o que a paginao faz quebrar a memria em blocos de 4 KiB.
A partir dos processadores Pentium Pro, lanados em 1995, a Intel resolveu criar uma extenso que
aumenta o tamanho de um endereo fsico contido nas tabelas de pginas. No modo i386 o endereo
fsico, usado como base para uma pgina, passa a ter 36 bits de tamanho (tornando possvel usar 64
GiB do virtual address space) e, no modo x86-64, passa a ter 52 bits (4 PB do virtual address
space). Essa extenso, a Physical Address Extension, ou PAE, ativada atravs do bit 5 do
registrador de controle CR4.
O endereo linear ainda tem o mesmo tamanho. O que PAE permite mapear uma pgina num
espao de endereamento virtual maior, mas as pginas continuam com o mesmo tamanho (4 KiB)
e, portanto, a faixa completa de todos os endereos lineares possveis (de 0 at 0x0000ffffffffffff, no
modo x86-64) continua sendo de, no mximo, 256 TB (ou 4 GiB do modo i386).
Um detalhe interessante que a extenso PAE tm que estar obrigatriamente habilitada no modo
x86-64.
Outra extenso a PSE (Page Size Extension), introduzida pela Intel no Pentium III. O nome j diz:

112
Com essa extenso podemos usar pginas maiores que 4 KiB. O kernel do Linux usa PSE, se
disponvel. Com essa extenso podemos ter pginas de tamanho varivel entre 4 KiB, 2 MiB e 1
GiB de tamanho, no modo x86-64.
PSE funciona assim: Um dos bits da estrutura de uma tabela de diretrios (excetp a PT!) usado
para habilitar esse tamanho maior que 4 KiB. O bit nomeado de PS (Page Size). Ele serve para
suprimir a tabela seguinte (PT, por exemplo), fazendo com que o offset no endereo linear aumente
de tamanho.
No caso do x86-64, se o bit PS estiver ativo numa PDE (veja figura anterior) ento a PDT torna-se a
nova PT e o offset passa a ter 21 bits de tamanho (ou seja, uma pgina passa a ter 2 MiB de
tamanho). Por outro lado, se PS estiver setado numa PDPTE, ento a PDPT torna-se a nova PT e o
offset ter 30 bits de tamanho (ou seja, teremos uma pgina de 1 GiB de tamanho).
Alm dos bits PS dessas tabelas de diretrio, o bit 4 do registrador de controle CR4 tambm precisa
estar setado para que a extenso tenha efeito.

Translation Lookaside Buffers


Se cada vez que o processador usar um endereo linear ele precisar traduzi-lo, ento o
processamento ficaria muito lento. Como existem os caches para poupar o processador o acesso
memria, diretamente, tambm existem caches para traduo de endereos lineares chamados
Translation Lookaside Buffers (TLBs). Cada vez que acesso memria feito, uma comparao
feita com o contedo dos TLBs. Se a traduo j estiver disponvel, nenhuma traduo nova
necessria (porque j foi feita!).
O processador tem diversos TLBs mantidos internamente. A quantidade e a especializao (TLBs
para dados, cdigo ou compartilhados) depende da arquitetura. Em meu processador, um Core i7,
da arquitetura Haswell, existem 64 TLBs para dados (dTLBs) e 128 TLBs para cdigo (iTLBs).
Onde cada processador lgico tm 16 iTLBs. No entanto, saber disso no l muito til, j que no
possvel ter acesso s TLBs diretamente. O processador as mantm para seu uso particular.

A importncia de conhecer o esquema de paginao


Page Faults, swapping, TLBs... tudo isso tem impactos na performance...
Sabendo que o processador divide a memria fsica em unidades atmicas de tamanho mnimo de
4 KiB (pginas), podemos evitar page misses (e, portando, page swappings) alocando memria em
blocos de tamanho mltiplo de 4 KiB e alinhados com o incio de uma pgina (os 12 bits inferiores
do endereo linear zerados, ou seja offset=0). Isso evita, por exemplo, que nossos dados cruzem a
fronteira entre pginas.
Se o offset do endereo linear base for zero e o bloco tem 4 KiB de tamanho, ento estaremos
necessariamente dentro de uma pgina. Se nossos dados puderem ser condidos de acordo com essa
restrio, no h motivos para o processador gerar excees do tipo page fault e realizar page
swappings, assim, ganhamos tempo precioso em nossas rotinas!
A mesma lgica pode ser feita para um conjunto de pginas. Determinar quantas pginas ainda
esto disponveis na memria fsica, mas ainda no mapeadas, e com base nessa informao
alocarmos somente o tamanho necessrio evitar termos pginas flutuando no ar como se fossem
as bolinhas coloridas na mo do proverbial palhao...
Algumas dessas coisas so levadas em conta nas rotinas da libc.

113
Tabelas de pginas usadas pelo userspace
Ao que parece, cada processo no userspace tem suas prprias tabelas de pginas, copiadas a partir
de um processo pai, sendo init o patriarca. Todo processo novo surge a partir do fork de um
processo pai, onde as tabelas de pginas do processo original so copiadas para o filho. Por isso
ambos os processos compartilham todos os dados, incluindo descritores de arquivos...
No entanto, ao realizar o fork, o kernel ajusta o status das pginas de dados de ambos os processos
como read-only e, quanto h uma tentativa de escrever numa varivel, uma page fault ocorre,
modificando o mapeamento da pgina do processo para uma outra pgina, com a respectiva cpia
da pgina original... Esse procedimento conhecido como copy-on-write (copia quando escreve),
ou COW.
Ao que parece, ambos os processos compartilham do mesmo endereo linear, como pode ser
demonstrado no cdigo abaixo:
/* test.c */
#include <unistd.h>
#include <stdio.h>

int x = 0;

void main(void)
{
pid_t pid;

printf("Antes do fork - Processo pai: &x = 0x%016lx\n", &x);

pid = fork();
if (pid == -1)
{
puts("ERRO no fork().");
return;
}
else if (pid == 0)
{
/* Processo filho */
printf("Depois do fork - Processo filho: &x = 0x%016lx\n", &x);
x = 1;
printf("Depois do fork (escrita: x = %d) - Processo filho: &x = 0x%016lx\n", x, &x);
}
else
{
/* Processo pai */
printf("Depois do fork - Processo pai: &x = 0x%016lx\n", &x);
x = 2;
printf("Depois do fork (escrita, x = %d) - Processo pai: &x = 0x%016lx\n", x, &x);
}
}
-----%<----- corte aqui -----%<-----
$ gcc -o test test.c
$ ./test
Antes do fork - Processo pai: &x = 0x0000000000600b4c
Depois do fork - Processo pai: &x = 0x0000000000600b4c
Depois do fork (escrita, x = 2) - Processo pai: &x = 0x0000000000600b4c
Depois do fork - Processo filho: &x = 0x0000000000600b4c
Depois do fork (escrita: x = 1) - Processo filho: &x = 0x0000000000600b4c

Como voc pode observar, tanto no processo pai quanto no processo filho o endereo linear da
varivel 'x' 0x600b4c. Para processos diferentes isso s possvel se ambos os processos usarem
tabelas de diretrios de pginas diferentes, j que em ambos os casos PDPTE=0, PDPE=3 e PTE=0,
no endereo linear.
Isso significa que, embora os processos compartilhem o mesmo endereo linear, ao usar tabelas
diferentes, eles no compartilham o mesmo endereo fsico. Cada processo tem seu prprio
conjunto de tabelas de pginas e, portanto, sempre que feito um chaveamento de tarefas
envolvendo processos diferentes, CR3 ser carregado de acordo, pelo kernel.

114
De fato, no modo i386, o contedo de CR3 para uma tarefa mantido na estrutura do TSS (Task
State Segment), nos dizendo que CR3 modificado, de fato, entre chaveamentos de contextos.
No modo x86-64 isso no feito com assistncia direta do processador, j que o TSS bem
diferente nesse modo.
Atualizar CR3 constantemente tem o danoso efeito colateral de invalidar as TLBs, exceto aquelas
cujas entradas em tabelas de pgina estejam marcadas como globais.

Alocando memria: malloc e mmap


A implementao da funo malloc (e suas derivadas: calloc e realloc), na libc, interessante: Ao
alocar menos que 128 KiB de memria (32 pginas), malloc toma conta do espao reservado no
heap da aplicao. Provavelmente o loader do sistema operacional ou a prpria libc pr alocam
pginas suficientes para no ter que requerer remapeamento.
Para blocos menores que 128 KiB, malloc usa a system call sbrk, que modifica o tamanho da
memria j alocada para a imagem binria alocada ao carregar a aplicao. Essa funo aumenta a
quantidade de pginas alocadas, tomando como base as que j esto l. J para blocos maiores que
128 KiB, malloc usa a system call mmap, que aloca pginas privadas (ao userspace).
Um algoritmo otimista assumido para malloc, isto , a libc supe que a memria requisitada est
disponvel para alocao. No h garantias que as pginas recm alocadas estejam presentes na
memria fsica (possibilitando o swapping) e, se o processo requisitar memria que o kernel
informa que no pode disponibilizar, malloc retornar um ponteiro nulo (NULL), deixando que seu
cdigo decida o que fazer.
Por causa do virtual address space, o retorno do ponteiro nulo um evento raro, na maioria das
vezes. Nem por isso voc dever ser leviano com o ponteiro retornado por malloc. essencial
sempre verific-lo:
void *ptr;

if ((ptr = malloc(size)) == NULL)


{
/* Oops! Um erro de alocao aqui! */

}

Quando o sistema operacional aloca memria, ele o faz sempre com blocos de tamanho mltiplos
de uma pgina. No h como alocar menos que 4 KiB por vez. Se um tamanho de bloco cuja
granularidade no seja do tamanho de uma pgina for requisitado, seu cdigo desperdiar
memria, uma vez que uma pgina inteira ser alocada de qualquer maneira. O que malloc e outras
rotinas de alocao da libc fazem reaproveitar pginas j alocadas sempre que possvel.
Claro que ainda h o problema dos buffers de traduo (TLBs)... possvel que quando o cdigo
tente acessar uma pgina diferente, no exista ainda um TLB vlido contendo o cache da traduo
do endereo linear. Ou, pior, pode ser que todos os TLBs estejam em uso e que algum tenha que ser
invalidado. Neste caso o processador ter que fazer uma paradinha para traduzir o endereo e
atualizar uma TLB. Ao atualizar uma TLB o processador pode ter que escrever o contedo do
cache associado a ele, piorando a situao...
No caso da arquitetura Haswell, os dTLBs ('d' de 'data') podero realizar cache de traduo de 64
pginas ao mesmo tempo, o que nos d 256 KiB para pginas de 4 KiB. Mais do que isso e teremos
dTLBs invlidos que precisaro ser validados, tornando o acesso memria mais lento. Ao manter
a menor quantidade possvel de pginas alocadas, voc contribui para o aumento de performance da
sua aplicao.

115
Adicione a limitao de espao no cache L1d e voc ver quanto problema de performance pode
conseguir encarando memria como um recurso sem limites, como ensinam nos cursos de anlise
de sistemas...
Ao invs de usarmos malloc, podemos usar a syscall mmap, que alocar uma ou mais pginas,
alinhadas, para ns. Uma desvantagem de usar mmap ao invs de malloc que, para liberar o
bloco alocado, necessrio usar munmap passado o endereo e tambm o tamanho do bloco
previamente alocado (diferente de free, j que as estruturas de malloc mantm essa informao!).
Eis um exemplo simples de uso de mmap:
#include <stdio.h>
#include <sys/mman.h>


void *ptr;
size_t blksize = 4096*10;

/* Aloca 10 pginas, deixa que o kernel escolha o endereo. */


if ((ptr = mmap(NULL,
blksize,
PROT_READ | PROT_WRITE,
MAP_ANNONYMOUS,
-1, 0)) == NULL)
{
/* erro! */

}

/* Usa o ponteiro 'ptr' aqui... */


/* libera o bloco. */
munmap(ptr, blksize);

Um exemplo de injeo de cdigo, usando pginas


A turma que gosta de exploits vai adorar essa. Usar a funo mmap para alocar pginas nos d
alguns poderes que malloc no tem. Por exemplo, podemos alocar uma pgina, injetar um cdigo
em linguagem de mquina nela, desabilitar o bit XD e saltar para o cdigo.
Esse o princpio do compilador JIT (Just In Time). Um cdigo, numa linguagem qualquer,
compilado e o cdigo e colocado em pginas, em runtime! Claro que no vou montar um
compilador aqui, mas ai vai um exemplo interessante de code injection:
/* exploit.c */
#include <stdio.h>
#include <memory.h>
#include <sys/mman.h>

/* Tamanho de uma pgina. */


#define PAGE_SIZE 4096

/* Cdigo que vai ser injetado. */


const unsigned char code[] = {
0x48, 0x89, 0xf8, /* mov rax,rdi; */
0x48, 0x01, 0xf8, /* add rax,rdi; */
0xc3 /* ret; */
};

int main(int argc, char *argv)


{
long (*fp)(long);
int x = 3, value;

/* Aloca uma nica pgina.


Poderamos alocar o tamanho suficiente para caber o cdigo, mas

116
mmap vai alocar uma pgina de qualquer jeito! */
fp = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (fp != NULL)
{
/* Copia o cdigo para a pgina */
memcpy(fp, code, sizeof(code));

/* Desabilita a escrita na pgina e habilita a execuo. */


mprotect(fp, PAGE_SIZE, PROT_READ | PROT_EXEC);

/* Executa o cdigo injetado via ponteiro. */


value = fp(x);

/* Dealoca a pgina! */
munmap(fp, PAGE_SIZE);

printf("O dobro de %d %d.\n", x, value);


}
else
puts("No consegui alocar uma pgina!");

return 0;
}

O cdigo injetado, acima, bem simples. Ele no contm referncias memria. Se tivesse,
teramos que ajust-lo em runtime, tomando como base o endereo base da pgina e endereos de
variveis ou funes, do programa em C. Esses ajustes tm o nome tcnico de fix-ups.
Considere esse pequeno cdigo:
bits 64
section .text

func:
call f
ret
f:
mov rax,rdi
add rax,rax
ret

O NASM criar um arquivo objeto assim:


00000000 E8 01 00 00 00 call 6
00000005 C3 ret
00000006 48 89 F8 mov rax,rdi
00000009 48 01 C0 add rax,rax
0000000C C3 ret

O que segue o valor 0xE8 o endereo relativo para onde o CALL saltar em relao a prxima
instruo. o valor que ser acrescentado ao RIP.
Esse cdigo no precisa de fix-ups, mas se fossemos implement-los para essa rotina teramos que
manter um ponteiro para a posio depois de 0xE8 e adicionar 5 a esse unsigned int, completando
o ajuste. Existem casos onde uma verso do CALL usa um endereo absoluto (um endereo linear)
e, neste caso, ele ter 8 bytes de tamanho. Neste caso os fix-ups so obrigatrios. o caso de saltos
indiretos.
Criar um code injection, desse jeito trabalhoso... Se voc quiser um JIT compiler real no seu
cdigo, recomendo o uso da libJIT51.
Aviso: Usar a sesso .data ou .bss para conter um cdigo executvel costuma falhar porque o
sistema operacional aloca pginas para essas sesses com atributos que impedem a execuo de
cdigo (bit XD setado). Se voc tentar fazer algo assim:
char buffer[1] = { '\xc3' }; /* C3 = RET */

51 Download em https://www.gnu.org/software/libjit/

117
/* Declara um ponteiro para uma funo e coloca o endereo do buffer nele. */
void (*fptr)(void) = (void (*)(void))buffer;

/* Chama a funo via ponteiro. */


fptr();

A chamada 'fptr()' vai causar um segmentation fault. E a mesma coisa vai acontecer se voc
alocar um buffer com malloc. O que fiz com mmap foi criar uma entrada de pgina com o atributo
PROT_EXEC, ou seja, o bit XD zerado!

Quanta memria fsica est disponvel?


importante que sua aplicao tente no usar mais memria do que a fisicamente disponvel. Isso
causar menos page faults e evitar swapping, mantendo a performance de sua aplicao previsvel.
Para tanto, voc ter que manter um registro de quanta memria est usando e quanta memria est
disponvel.
No Linux, voc poder trabalhar com duas funes: sysinfo e getrusage. Com a primeira voc
obtm informaes globais, que concernem ao sistema operacional. Com a segunda voc obtm
informaes relacionadas com o seu processo.
A funo sysinfo uma syscall e retorna a seguinte estrutura:
struct sysinfo {
long uptime; /* Seconds since boot */
unsigned long loads[3]; /* 1, 5, and 15 minute load averages */
unsigned long totalram; /* Total usable main memory size */
unsigned long freeram; /* Available memory size */
unsigned long sharedram; /* Amount of shared memory */
unsigned long bufferram; /* Memory used by buffers */
unsigned long totalswap; /* Total swap space size */
unsigned long freeswap; /* swap space still available */
unsigned short procs; /* Number of current processes */
unsigned long totalhigh; /* Total high memory size */
unsigned long freehigh; /* Available high memory size */
unsigned int mem_unit; /* Memory unit size in bytes */
char _f[20-2*sizeof(long)-sizeof(int)]; /* Padding to 64 bytes */
};

No coincidncia que essa funo retorne os mesmos valores que voc obtm usando o comando
free:
$ free -lh
total used free shared buffers cached
Mem: 7.7G 1.3G 6.4G 0B 50M 836M
Low: 7.7G 1.3G 6.4G
High: 0B 0B 0B
-/+ buffers/cache: 468M 7.2G
Swap: 7.9G 0B 7.9G

Acima temos os valores pessimista (em vermelho) e otimista (verde) da memria fsica livre. Para
obt-los, via sysinfo, basta usar os macros:
#define MIN_FREE_RAM(si) ((si).freeram)
#define MAX_FREE_RAM(si) ((si).freeram + (si).sharedram + (si).bufferram)

Linux tentar liberar a memria usada por buffers e caches quando as aplicaes demandarem
recursos. Na maioria das vezes seguro obter a memria fsica livre com o mtodo otimista, mas
nem sempre...
Quanto ao uso de recursos pela sua aplicao, Linux usa um conceito chamado Resident Set Size
(RSS). Literalmente tamanho do conjunto residente. a memria fsica usada pelo processo atual
ou, se voc quiser, incluindo os processos filhos. Assim como sysinfo, a funo getrusage retorna
uma estrutura contendo diversas estatsticas sobre o seu processo, incluindo a quantidade de page

118
faults, operaes de swap executadas pelo processo e at mesmo chaveamentos de contexto de
tarefas. Diferente de sysinfo, os valores obtidos so retornados em KiB, no em bytes.
No caso do Windows, voc pode usar a funo GetProcessMemoryInfo para obter os valores de
RSS (que, na nomenclatura da Microsoft, chamado de Working Set Size). Essa funo parte da
PSAPI (pode ser necessrio importar a psapi.dll, dependendo da verso do seu Windows). Para
obter a memria fsica livre do sistema, use GlobalMemoryStatus.

119
120
Captulo 11: Threads!
Voc provavelmente tem um processador que possui mais de um ncleo e deve estar se perguntando
se threads no so a soluo definitiva para ganho de performance... Infelizmente, no!
Threads existem para dividir o trabalho, permitindo a execuo de uma mesma rotina em vrias
frentes, teoricamente, em paralelo. Existe um potencial ganho de performance geral, j que dois ou
mais processos trabalhando em partes diferentes podem terminar o trabalho que uma rotina discreta
levaria o dobro do tempo... Mas no sempre assim. E necessrio entender como as threads
funcionam para tirar boa vantagem delas...
Para entender o que , de fato, uma thread preciso entender os conceitos de contexto de tarefa e a
diferena entre tarefa e thread. Ambas so unidades de execuo e ao falar de ambas, falamos de
paralelismo.
Tarefa est associada a um recurso de infraestrutura presente no processador, desde o 286, que se
refere ao chaveamento de contexto de tarefa, que no implementado no modo x86-64. Pelo
menos, no da mesma forma que o no modo i386.
Contexto de tarefa o conjunto de valores de todos os registradores que estavam sendo usados por
um processo antes dele ser interrompido. TODOS os registradores, incluindo:
Os de uso geral: De RAX at R15, RSP, RIP, RBP, RSI e RDI;
Os seletores de segmento CS, FS e GS (os demais no so usados no modo x86-64);
Os registradores SIMD (XMM0 at XMM15, e/ou YMM0 at YMM15);
A pilha do coprocessador matemtico;
RFLAGS
Quando o kernel muda de uma tarefa para outra, ele salva o contexto de tarefa inteiro em algum
lugar, carrega o contexto da outra tarefa de outro lugar e salta para o CS:RIP da nova tarefa. S
assim h a garantia de que a nova tarefa continuar exatamente de onde parou. Essa troca de tarefas
chamada de chaveamento de tarefa ou task switching. Notou que esse chaveamento implica,
necessariamente na interrupo da execuo de uma tarefa e o reincio de outra?
Thread, por outro lado, um conceito mais abrangente. Em processadores com vrios ncleos, ou
sistemas com vrios processadores, podemos executar tarefas de forma verdadeiramente paralela
(chama-se Simetrical Multi Processing, ou SMP). Mas uma thread pode tambm ser executada em
fatias de tempo (Time slicing Multi Processing) e, neste caso, haver task switching.
No que concerne o chaveamento de tarefas, isso s pode ser feito mediante uma a entrega do
controle do seu programa, que executado no userspace, para o kernel (kernelspace). A forma
como isso feito depende do sistema operacional, mas a maioria dos sistemas decentes o fazem de
forma preemptiva...

Multitarefa preemptiva e cooperativa


Nos idos do incio dos anos 90 a multitarefa cooperativa era o state-of-the-art dos sistemas
operacionais de baixo custo. Reinava o Windows 3.1. Por cooperao entende-se que a aplicao
entregava o controle ao kernel chamando funes da API e, dentre outras coisas, este aproveitava o
ensejo para executar o cdigo de um scheduler.
Scheduler, numa traduo literal, um agendador. uma rotina que decide quanto e quais tarefas

121
sero chaveadas, dando a impresso de multiprocessamento. No esquema de multitarefa cooperativa
a aplicao cooperava com o sistema operacional, por assim dizer, entregando o controle de
tempos em tempos para o kernel. O problema que, se na aplicao surgisse um loop infinito ou
nenhuma funo da API fosse chamada, a iluso de multiprocessamento era quebrada.
Um dos modos como o Windows fazia isso era atravs de uma chamada explcita para uma funo
de gerenciamento da fila de mensagens contida na funo WinMain da aplicao:

while (GetMessage(&msg))
{
TranslateMessage(&msg)
DispatchMessage(&msg);
}

A funo GetMessage executava o scheduler, alm de obter a ltima mensagem da fila...


J os sistemas UNIX sempre foram baseados em multiprocessamento preemptivo.
Preemptividade a capacidade do kernel de interromper uma tarefa sem a cooperao do cdigo
no userspace. Ao contrrio da crena popular, preemptividade no tem nada haver com
processamento simultneo. O significado da palavra evidente: do dicionrio, um dos sinnimos
antecipado. Aqui isso quer dizer que o kernel controla o chaveamento, no o seu programa!

Mltiplos processadores
Todo processador executa cdigo apontado pelo par de registradores CS:RIP. Para fazer isso o
processador colocado num modo especfico (x86-64, no nosso caso) e uma srie de inicializaes
so feitas para aproveitar todo o recurso de hardware disposio da CPU (disco, memria, USB
etc). Num ambiente multiprocessado no muito diferente...
Seja num sistema com mltiplos processadores ou num processador com mltiplos ncleos, um
deles eleito para ser o bootstrap processor (BSP) e este controla o funcionamento de todos os
demais, chamados de application processors (AP). O BSP inicializado no modo protegido e
paginado pelo sistema operacional e os APs devem ser colocados no mesmo modo, j que todos eles
so inicializados, logo depois do power up, no modo real. Depois de inicializados, os APs so
colocados para dormir, atravs da instruo HLT (Halt) e sero acordados pelo BSP quando o
sistema operacional precisar executar uma thread de forma simtrica.
Para isso, todos os processadores do seu sistema executam parte do kernel. S que o BSP no pra
nunca, nunca colocado na cama, ningum d um beijinho de boa noite e deseja bons sonhos. Ele
no pode parar, o chefe! No caso dos APs, se no estiverem executando nada, estaro dormindo. O
BSP pode, ento, enviar uma interrupo para acord-los, enviando tambm um endereo lgico,
completo, relativo ao CS:RIP da thread que ser executada...
Repare que os APs tm seus prprios conjuntos de registradores, suas prprias unidades de
execuo, seus prprios caches, etc. Os APs podem ser processadores fsicos independentes ou
processadores lgicos, contidos nos ncleos de sua CPU. Tanto faz! Para o kernel esses
processadores so todos isolados, comunicando-se via um barramento de dados isolado chamado
ICC (Interprocessor Commuunication Channel).
J que interrupes tm um papel to importante nesse tipo de ambiente, a Intel resolveu incorporar
no chip da CPU um controlador local programvel avanado de interrupes (Local APIC ou
LAPIC) em cada um dos processadores lgicos. Antigamente o PIC (Programmable Interrupt
Controller) era um chip externo CPU, desde o Pentium o LAPIC j acompanha o chip.
Esses LAPICs contm, alm do controle de interrupes, timers e identificao do processador. E ,

122
atravs do barramento ICC, o BSP pode enviar interrupes para os APs, iniciando, suspendendo ou
interrompendo threads.
O cdigo responsvel por decidir para onde uma thread vai ser enviada, ou se ela vai ser
contextualizada para ser chaveada, tarefa de um pedao de cdigo do kernel chamado scheduler.

Como o scheduler chamado?


A maneira mais fcil atravs de interrupes ao processador. Certos dispositivos no seu
computador enviam um pedido de interrupo ao processador pedindo ateno. Por exemplo,
quando voc pressiona uma tecla em seu teclado o circuito associado a ele pede que o processador
pare tudo o que est fazendo para executar uma rotina especfica que l as portas do teclado e
armazene os cdigos relativos tecla pressionada. Quanto essa rotina termina, o processador
retorna execuo que foi interrompida.
Uma das maneiras de interromper o processador de forma previsvel usar algum tipo de timer para
enviar requisies de interrupo ao processador com frequncia conhecida. Por exemplo, podemos
programar um timer de alta resoluo para pedir interrupes a intervalos de 18 ms52. Se o scheduler
for chamado pela rotina da interrupo desse timer, ele ter chance de chavear tarefas a cada 18 ms
de intervalo.
Dessa forma, o scheduler interrompe tarefas e reinicia outras de acordo com um algoritmo
complicado que leva diversos fatores em conta: Prioridade da tarefa (quais tarefas tero maiores ou
menores fatias de tempo para si), paralelismo real versus time slicing etc. No caso do Linux, o nome
adotado para o algoritmo CFS (Completly Fair Scheduler). O que nos interessa saber que o
scheduler realizar chaveamento de tarefas, quando for necessrio, e manter registros de tarefas
paralelizadas (via SMP).

Finalmente, uma explicao de porque SS no zero no modo x86-64!


Quando falei sobre os seletores no modo x86-64 mostrei que apenas o seletor CS considerado pelo
processador. Todos os outros (DS, ES, FS, GS e SS) so ignorados. Na ocasio, mostrei um
pequeno cdigo para imprimir o contedo dos seletores e, para surpresa geral, o registrador SS
contm um valor diferente de zero e, pior, com um RPL condizente com o userspace!
Existe apenas um motivo, pelo que posso perceber: Quando feito um chaveamento de tarefa entre
o ring 0 e o ring 3, via interrupo, o processador salva o contedo de SS:RSP na pilha e zera SS.
O sistema operacional pode, ento, manter um valor vlido em SS no userspace como uma maneira
de saber, rapidamente, em que ring ele est. Mais importante: SS poder conter um ndice para um
descritor que tenha informaes importantes para o kernel, sobre o processo que foi chaveado!

Na prtica, o que uma thread?


Para simplificar, uma thread uma funo. Essa funo executada como se estivesse num
processo separado, mas compartilhando todos os recursos do processo que criou a thread. De fato,
threads tambm so conhecidas como lightweight processes, ou processos leves. Alm do nome,
no h muita diferena entre threads e processos: Ambos tm pilha e contextos prprios, por
exemplo.
Sobre recursos compartilhados com o processo, quero dizer que suas threads enxergam todas as
variveis globais do seu programa. Isso diferente de um fork, onde um novo processo criado

52 Consulte a configurao do seu kernel via sudo sysctl kernel.sched_latency_ns. No meu caso, a latncia do
scheduler de 18 milissegundos (ou 18000000 de nanossegundos).

123
com base na cpia do processo original e os dados do novo processo s so copiados se forem
modificados pelo processo filho (copy-on-write). Isso no acontece com threads.
Um detalhe sobre a pilha assinalada a uma thread: aconselhvel que ela seja pequena para no
colocar presso no sistema de paginao. Alm do possvel chaveamento de tarefas, o uso de muitas
pginas no compartilhadas (pilhas separadas) pode exaurir a capacidade do cache L1d e das TLBs,
causando grandes atrasos.
Outra coisa importante saber que no aconselhvel disparar um grande nmero de threads (ou
processos). Lembre-se que seu processador tem nmero limitado de ncleos (e, em sistemas
multiprocessados, um nmero limitado de processadores)... Se o nmero de threads simultneas
suplantar o nmero de processadores lgicos, o kernel ter, necessariamente, que realizar
chaveamento de tarefas, ao invs de usar processamento simtrico (SMP).
Num ambiente Linux, por exemplo, possvel termos at 32768 processos simultneos (veja
/proc/sys/kernel/pid_max), incluindo ai todas as threads alm daquelas associadas diretamente aos
processos. Isso um exagero, claro! Esse nmero de threads, num processador capaz de lidar com
8 threads em SMP ( o caso do i7) significa que teremos 4096 chaveamentos de contexto, que
deixaro cada uma das tarefas bem lenta... Considere o caso em que um chaveamento de tarefa pode
ser feito a cada 18 ms e que 32768 tarefas tenham o mesmo nvel de privilgio. Com 4096
chaveamentos, teremos latncia de 73 segundos entre as tarefas. Ou seja, uma tarefa executa 18 ms
de processamento e colocada para dormir durante 73 segundos (4096 chaveamentos vezes 18 ms)
at que seja acordada novamente!
Nesse cenrio o seu cdigo cuidadosamente desenhado para atingir a melhor performance possvel
ser to lento que te dar vontade de bater com a cabea na parede, com fora!

Criando sua prpria thread usando pthreads


Pthreads ou Posix Threads a biblioteca padro, em ambientes POSIX (obviamente), para lidar
com threads (mais obvio ainda!). Trata-se de um conjunto de funes para criar e manusear threads.
E bem fcil de ser usado.
Resumidamente, quando voc cria sua thread est criando uma ramificao paralela do fluxo de
execuo, desassociado do fluxo da thread principal (a thread do processo). Assim, teremos dois
fluxos de execuo. Em algum momento teremos que juntar (join) o fluxo novo com o fluxo da
thread principal. Essa juno coloca a thread principal para dormir, enquanto ela espera pelo
trmino da thread secundria.
Usar threads com pthreads essencialmente algo assim (omiti todos os tratamentos de erro para
facilitar a leitura):
pthread_t tid; /* Identificador da nova thread. */
pthread_attr_t tattr; /* Atributos da nova thread. */
void *mythread(void *); /* Prottipo da funo que ser executada na nova thread. */
void *param_ptr; /* Ponteiro para os parmetros que a
thread receber. */
int retval; /* Valor retornado pela thread, se algum.
Pode ser de qualquer tipo. Uso 'int' como exemplo apenas. */

/* Inicializa os atributos default da thread, alterando


apenas o tamanho da pilha para o menor possvel. */
pthread_attr_init(&tattr);
psthread_attr_setstacksize(&tattr, PTHREAD_STACK_MIN);

/* Cria e pe a nova thread em execuo. */


pthread_create(&tid, &tattr, mythread, param_ptr);

124
/* Continua a fazer algo na thread principal enquanto a thread secundria 'roda'. */

/* Espera pela hora de 'juntar' a thread criada com a principal.


Coloca a thread principal para dormir enquanto a thread identificada por 'tid' no
retorna. */
pthread_join(tid, &retval);

/* Neste ponto a thread secundria j no existe mais. */

Note que antes de criarmos a thread temos que informar seus atributos. A funo pthread_attr_init
inicializa esses atributos com valores default. Eis alguns deles:
A thread ser criada como joinable;
O nvel de prioridade 0 (default);
A pilha da nova thread tem tamanho default.
Pode ser necessrio usar uma a funo do tipo pthread_attr_setXXX (onde XXX o atributo a ser
modificado) para fazer ajustes finos. No exemplo acima, um dos atributos que modifiquei foi o
tamanho da pilha da thread. Por default, pthread criar pilhas do mesmo tamanho informado por
'ulimit':
$ ulimit -s
8192

Como podem ver, no meu sistema o tamanho de pilha default de 8 MiB 53. A funo
pthread_attr_setstack precisa receber um valor igual ou superior constante
PTHREAD_STACK_MIN, caso contrrio, a thread no ser criada (pthread_create retornar um
valor diferente de zero, indicando erro).
Quanto ao atributo joinable, perfeitamente possvel alter-lo para criar threads desassociadas da
thread principal. Assim, no precisaremos 'junt-las'. Isso exige que voc tenha o controle da vida
da thread, isto , se ela ainda est em execuo ou no.
Voc tambm pode alterar o nvel de prioridade da thread. Nveis maiores que zero tendem a dar
mais time slices para a thread do que para aquelas com privilgio menores. A funo
pthread_attr_setschedparam pode ser usada para isso.
Outro atributo que voc pode achar interessante a afinidade da thread. Voc pode dizer em qual
processador lgico quer que a thread seja executada usando a funo pthread_attr_setaffinity_np.
Mas recomendo que esse recurso seja deixado a cargo da inicializao default e, em ltima anlise,
do prprio sistema operacional... O sufixo 'np' no nome da funo significa non portable. uma
dica para que voc evite usar funes assim...

Criando threads no Windows


Usar a API do Windows para criar threads tambm bem fcil, mas o controle das threads no to
facilitado assim. No h a facilidade de juntar as threads, por exemplo. Diferente de pthreads as
threads do Windows so desassociadas da thread principal. Mas existe uma maneira de emular um
join, como pode ser visto mais adiante...
Para criar uma thread s precisamos usar a funo CreateThread, que toma seis parmetros:

53 No que a thread ou o processo criem uma pilha de 8 MiB. O sistema aloca uma pgina presente (4 KiB) e as
demais pginas como no presentes. Assim, pelo processo de page fault pode-se fazer a pilha crescer at 8 MiB,
se necessrio.

125
HANDLE WINAPI CreateThread(
LPSECURITY_ATTRIBUTES lpThreadSecurityAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreatingFlags,
LPDWORD lpThreadId
);

Na API do Windows, geralmente os valores default so assumidos ao se passar um ponteiro NULL


(ou zero). o caso dos dois primeiros parmetros e dos flags de criao (atributos?). Eis um
exemplo de criao de uma thread:
HANDLE hthread;

/* Prottipo da nossa funo que ser paralelizada. */


DWORD WINAPI MyThreadFunc(LPVOID);

/* cria thread e coloca para executar. */


if ((hthread = CreateThread(NULL,
128*1024, /* Pilha de 128 KiB. */
MyThreadFunc,
NULL,
0,
NULL)) == NULL)
{
trata erro aqui
}

faz alguma coisa

/* join */
WaitForSingleObject(hthread, INFINITE);

Se o parmetro do tamanho da pilha for 0 ento CreateThread usar uma pilha de 1 MiB de
tamanho (esse o tamanho de pilha default do Windows).
O parmetro do tipo LPTHREAD_START_ROUTINE um ponteiro para uma funo executada
pela thread secundria, do mesmo tipo usada na chamada pthread_create. H tambm o ponteiro
genrico para o parmetro que ser passado para essa funo, os atributos da thread
(dwCreateFlags) e o ponteiro para o identificador da thread.
Diferente de pthread_create, podemos criar uma thread suspensa setando o bit
CREATE_SUSPENDED nos flags de criao. Criar uma thread suspensa, em pthreads, no
possvel. Mas, h meios de simular esse comportamento. Uma dessas maneiras trabalhar com
sinais. Outra, usando artefatos de sincronizao como mutexes.
Outra diferena entre Windows API e pthreads o primeiro parmetro de CreateThread. J que a
infraestrutura de segurana do Windows meio esquisita, necessrio informar se o identificador
da nova thread pode ser ou no compartilhado por processos filhos. Normalmente passamos NULL
nesse parmetro, deixando o Windows escolher a melhor caracterstica de segurana para a nova
thread.
Assim como em pthreads, se sua thread no foi criada suspensa ela colocada em execuo
imediatamente. Caso contrrio voc ter que chamar ResumeThread, passando o identificador. Se
sua thread est em execuo e, atravs de uma outra thread, voc queira coloc-la em estado
suspenso, basta executar SuspendThread, passando o identificador.
J disse que a metfora de juntar threads no existe na API do Windows. Voc pode esperar pelo
trmino de uma thread via chamada funo WaitForSingleObject. Essa funo, em essncia,
funciona do mesmo jeito que pthread_join, exceto que voc no obtm o cdigo de retorno da
thread atravs dela. Para obter esse valore retorno ter que usar a funo GetExitCodeThread.
Existe outra funo de espera: WaitForMultipleObjects, que tem uma vantagem sobre as mltiplas

126
chamadas a pthread_join necessrias para juntar mltiplas threads secundrias... Se tivermos que
esperar por 3 threads, por exemplo, podemos fazer:
HANDLE tids[3];
int trys;

/* Coloca os handles das threads no array. */


tids[0] = tid1;
tids[1] = tid2;
tids[2] = tid3;

/* Tenta esperar pelo fim, de todas as threads, 10 vezes.


Espera 3 segundos (3000 milissegundos) a cada vez. */
trys = 10;
while (--trys &&
WaitForMultipleObjects(3, tids, TRUE, 3000) == WAIT_TIMEOUT)
{
/* Sinaliza, de alguma maneira, para as threads que elas precisam terminar! */
}

if (trys <= 0)
{
trata erro aqui
}

Parar uma thread na marra quase sempre no uma boa ideia


Em ambos os casos, tanto no Windows quanto no POSIX, possvel parar uma thread na marra.
No caso do Windows temos a funo TerminateThread, no caso do UNIX, pthread_kill. Elas no
fazem a mesma coisa: TerminateThread fora o trmino de uma thread de forma que ela no tem
opo de continuar o processamento at algum ponto de controle. Isso pode causar srias
instabilidades no seu cdigo, j que voc poder estar esperando que algum estado esteja presente,
durante a execuo de seus programas que no ser atualizado devido ao trmino inesperado da
thread.
No caso das pthreads, a funo pthread_kill envia um sinal especificado thread. Esta pode ter a
chance de trat-lo (a no ser que o sinal seja SIGKILL ou outro sinal no tratvel, de acordo com a
especificao POSIX).
Pthreads tambm implementa a funo pthread_cancel, que um apelido para pthread_kill
enviando um sinal SIGUSR1 ou SIGUSR2 para a thread a ser cancelada. Se for matar uma thread
na marra, prefira usar pthread_cancel.
De qualquer maneira, parar uma thread de forma no graciosa sempre uma m ideia.
imprescindvel que seus cdigos criem e destruam threads de uma maneira estvel. Evite sempre
matar uma thread sem mais nem menos.

Nem todas as threads podem ser mortas


Para que pthread_cancel funcione, se voc realmente precisar mat-la na marra, preciso que
existam pontos de cancelamento. Como regra geral, toda funo que use recursos do sistema
operacional so pontos de cancelamento. Uma thread que est em loop infinito vazio, por exemplo:
for (;;);

Pode permanecer em loop infinito eternamente, mesmo que um pthread_cancel seja usado.
A tcnica mais usada, nesse caso, usar uma syscall, por exemplo, a funo sleep, pedindo que o
processo durma por zero segundos:
for (;;) sleep(0);

127
Toda syscall um ponto de cancelamento. Isso d a chance de podermos cancelar a thread. Tem
ainda o efeito benfico de fazer com que o scheduler no morra de fome. Se no h chamadas para
o kernel, via syscall, ento o scheduler s chamado via interrupo, causando um pico de
consumo de processamento.
claro que uma chamada a sleep adicionar ciclos ao seu processo. O que mais um argumento
contra o aumento de performance automtico assumido para processos multithreaded.
Esse exemplo do loop infinito meio pobre. Existem meios de colocarmos uma thread para dormir
sem recorrermos a uma syscall. Por exemplo, podemos usar pthread_sigwait para esperar que um
sinal seja enviado para a thread.
Em assembly existe a instruo PAUSE, que age como se fosse um NOP, exceto que oferece uma
dica ao processador de que a thread atual encontra-se em loop. PAUSE no substitui uma chamada
via syscall, mas til nas rotinas de sincronismo, como spin locks. O motivo da existncia de
PAUSE nada tem haver com o scheduler, claro. O processador tem l seus problemas com muti
processamento tambm!

Threads, ncleos e caches


Um dos grandes problemas com as threads, e ambientes com mltiplos processadores ou ncleos,
que os estados dos caches sero compartilhados em algum momento (j que threads criadas no seu
programa compartilham o mesmo espao de endereamento). bom lembrar que o cache L1 existe
separadamente para cada processador lgico, ou seja, para cada ncleo de seu processador, mas
podemos estar usando algum sistema com mais de um processador tambm. Assim, duas threads
podem ser atrasadas pelo protocolo de consistncia de caches dos processadores (ou ncleos).
Considere duas threads (A e B) executando de maneira simtrica. Se a thread A escreve numa
varivel global 'x' e a thread B l a a mesma varivel, temos duas threads acessando a mesma regio
de memria e, como consequncia disso, alguma coisa tem que ser feita para manter a consistncia
dos caches. Consistncia, aqui, significa que todos os processadores vero a mesma coisa nos
caches sempre que possvel. No interessante que a cpia do cache L1 de um processador seja
feita para os demais. Isso s possvel graas a um protocolo de consistncia chamado MOESI
(Modified, Owned, Exclusive, Shared, Invalid), cuja descrio est alm do escopo deste texto.
O motivo de eu te falar deste protocolo deixar claro que threads tem o potencial de causar
problemas com caches, especialmente caches L1d, o que pode adicionar muitos ciclos de mquina
de perda de performance nos seus programas. De novo, threads no so o recurso barato para
alcanar performance elevada...

Trabalhar com threads no to simples quanto parece


Como os dados e cdigo de threads so compartilhados, o que acontece que as vrias threads que
podem estar associadas a um nico processo pai podem acessar a mesma varivel. Isso tem o
potencial de gerar alguns absurdos...
Pode parecer estranho, j que uma instruo normalmente completada antes de uma interrupo,
mas considere o caso do incremento de uma varivel do tipo 'long', no modo i386: Neste modo no
podemos usar os registradores estendidos ou atualizar valores na memria que tenham tamanho
maior que 32 bits (a no ser que usemos MMX ou SSE). O incremento de uma varivel 'x', do tipo
'long', seria mais ou menos assim:
add dword [x],1
adc dword [x+4],0

128
Se houver um chaveamento de tarefa entre as instrues ADD e ADC, o incremento de x ser
feito pela metade! E esse no o nico caso esquisito. Num cenrio semelhante, suponha que uma
thread incremente uma varivel inteira e outra thread escreva '0' na mesma varivel. O que
acontece?
O algoritmo da primeira thread est esperando um valor incrementado (maior que zero?), mas
quando a tarefa for chaveada de volta para ela, a varivel conter zero, colocado l pela outra
thread!
Sempre que uma das threads quer modificar uma varivel usada por outra thread pode acontecer
esse monte de problemas que so chamados de race conditions.

Evitando race conditions


Para evitar esses problemas usa-se o artifcio de sincronizar as threads. Trata-se de uma espcie
de sinal de trnsito, um semforo, usado para temporariamente interromper o processamento de uma
ou mais threads enquanto uma delas detm o controle sobre o recurso compartilhado. como se
esse semforo ficasse verde para uma thread e vermelho para as outras. Quando uma thread obtm
um sinal vermelho ela entra num estado de espera. colocada para dormir at o sinal ficar
verde54.
Quem diz para o semforo ficar vermelho so as prprias threads. Quem acionar o boto que pede
ateno, primeiro, passa a ser a dona do semforo. Ou, melhor, quem obtm a trava (lock)
primeiro, no permite que as outras threads o travem at que o semforo seja destravado (unlock).
Por exemplo, se quisermos alterar uma varivel global em nossas threads, devemos fazer algo
assim:
pthread_mutex_lock(&mtx);
if (++x < MAX_X)
dosomething();
pthread_mutex_unlock(&mtx);

Mutexes55 so um tipo de objeto de sincronismo de threads. Eles so travveis e destravveis pela


thread que os controla... Se j estiver travado, ento a rotina entra em loop e coloca a thread para
dormir. O cdigo entre o lock e o unlock chamado de critical section. No toa: Essa a parte
crtica da thread que precisa ser sincronizada...
A sincronizao deve ser usada com cautela. Se voc travar toda a sua thread ento no h sentido
em criar threads! O sincronismo deve ser feito em pedaos chave do cdigo da thread (as sesses
crticas) para evitar os tipos de conflito causados por race conditions, mas s... Trata-se de ter o
poder de parar outras threads semelhantes, temporria mente, para que um pedao do
processamento sempre seja feito de forma correta, sem a interferncia da concorrncia.
Existem tipos de objetos de sincronizao diferentes que funcionam como trava. Alm dos
mutexes temos spinlocks e semaphores. Essencialmente, eles so a mesma coisa, mas cada um tm
l seus conjuntos de vantagens e desvantagens. A explicao sobre cada um deles est alm do
escopo deste livro.
Embora o sincronismo resolva, em parte, as race conditions, pode causar outro: dead locks.
Acontece quando duas threads obtm travas (locks) de objetos de sincronismo diferentes de forma
que nenhuma das duas threads seja capaz de destrav-los individualmente, deixando ambas em
estado de espera eterno. Em certas circunstncias dead locks podem ocorrer porque tanto no

54 Semforos tm um sentido especfico com relao aos esquemas de sincronizao. Estou usando aqui a analogia,
no o objeto semaphore.
55 Mutex uma abreviao de MUTually Exclusive.

129
processamento simtrico quanto no chaveamento de tarefas no temos ideia da posio em que
nossas threads se encontram, do ponto de vista da execuo... O sincronismo pode ser feito numa
sequncia que voc no considerava possvel, deixando uma ou as duas travas eternamente
travadas.
Um cenrio alegrico: Voc e um amigo tm chaves para abrir duas travas de uma porta de um
quarto... Vocs entram no quarto e fecham as travas, mas seu amigo morre e leva a chave dele
consigo (caiu num poo no meio do quarto, por exemplo)... Voc jamais sair do quarto porque a
fechadura que s abre com a chave do seu amigo est morta pra voc (dead locked).

Threads e bibliotecas
Nem toda funo pode ser usada impunemente numa thread. Para que possa, ela tem que ser
reentrante, ou seja, no usar recursos externos funo (como variveis globais, por exemplo). No
o caso de muitas funes da libc, por exemplo.
Para o Linux, a libc bem documentada pela Free Software Foundation. A documentao completa
pode ser obtida no link http://www.gnu.org/software/libc/manual/. E, por l, voc encontrar, para
qualquer funo, algumas dicas sobre o uso com ambientes multithreaded... Toda funo tem uma
dica, depois do prottipo:
size_t strlen (const char *s)
[Function]
Preliminary: | MT-Safe | AS-Safe | AC-Safe
The strlen function returns the length of the null-terminated string s in bytes. (In other words, it returns the offset of the terminating null
character within the array.) ...

Este MT-Safe nos diz que podemos usar strlen em cdigos multithreaded sem medo. Mas,
ateno! O fato de uma funo ser thread safe no significa que ela seja atmica, quer dizer, que ela
seja executada totalmente antes que um chaveamento de contexto seja feito. Tambm no significa
que no possamos ter problemas com race conditions. Considere o caso da funo strcpy, que
tpicamente implementada como:
char *strcpy(char *dest, const char *src)
{
char *p = dest;

while (*dest++ = *src++);

return p;
}

Se duas threads usarem o mesmo ponteiro de destino para as chamadas a strcpy, provavelmente
teremos uma race condition, mesmo que strcpy no use nenhuma varivel global ou esttica
local.
Assim, os problemas que listei antes continuam valendo, mesmo para as funes que so ok para
serem usadas em threads.
As dicas AS-Safe e AC-Safe referem-se a sinais. No caso de AS-Safe, a funo pode ser usada
em rotinas que respondem as interrupes causadas por sinais. No caso de AC-Safe a funo
um ponto de cancelamento de thread.
Um exemplo de funo da libc que thread unsafe strtok. Ela explicitamente marcada como
MT-Unsafe. Ela mantm um estado global para que a segunda chamada a ela funcione como
deve. Existe uma verso thread safe: strtok_r, que toma um parmetro adicional onde o
programador deve armazenar o estado da funo.

130
Threads e Bloqueios
Alm de thread safety existe a questo de bloqueios de I/O. Algumas funes precisam ser
completadas antes que possam ser chamadas novamente. Funes como write so syscalls que
precisam ser completadas. No caso desta funo ser chamada por duas threads, a segunda thread a
cham-la parecer estar dormindo, enquanto a primeira est executando a funo.
Algumas syscalls podem ser configuradas para serem non blocking, mas isso s significa que a
funo retornar imediatamente com algum cdigo de erro, enquanto executa a requisio em
background. o caso de funes de sockets. O fato de voc configurar o descritor de arquivo criado
pela funo socket como non blocking, no significa que seus pacotes sero dispachados em
paralelo! Significa que eles sero serializados pelo kernel e colocados numa fila e a funo que o
fez ira te dizer isso (e voc precisa tratar essa informao!).
Lembre-se que o comportamento padro para a maioria das systcalls blocking.

O que significa isso tudo?


claro que multiprocessamento, quando implementado com cuidado, tem o potencial de acelerar
alguns processos, mas tenha em mente que a quantidade de cuidados grande e exige estudo
intenso do que a thread deve fazer. Por causa da complexidade, o potencial para atrapalhar a boa
performance de suas rotinas muito grande.
O que temos at agora? Task Switching grava e recupera o contexto de tarefas. Esses contextos so
chaveados atravs de software contido num scheduler, que executado, provavelmente, pela rotina
de resposta de interrupes de timers atendidas pelo processador. Cada tarefa pode ser executada
em regime de time slicing, onde uma fatia de tempo dada, pelo scheduler, para cada tarefa ou de
forma simtrica, distribudas entre os processadores lgicos. Temos ainda a necessidade de
sincronizao de threads por causa de recursos compartilhados e a necessidade de se precaver
contra dead locks...
Se isso no fosse o suficiente, muitos problemas com paginao e caches podem surgir, atrasando
ainda mais a CPU. No que o multiprocessamento til, afinal de contas?
Em grande parte, os sistemas operacionais modernos usam multiprocessamento para lidar com a
percepo do usurio. Se duas tarefas parecem estar sendo feitas ao mesmo tempo, o usurio fica
feliz. Mas, no contexto de performance, provavelmente as tarefas esto sendo feitas mais
lentamente do que se fossem feitas isoladamente.
Mesmo com essa m notcia, o multiprocessamento pode ser muito til, em certos casos. Por
exemplo, para quebrar as iteraes de um loop em vrias pequenas iteraes individualizadas em
threads. Esse tipode diviso de tarefas, se for feito via SMP, tem mesmo o potencial de acelerar a
rotina. o que biliotecas como OpenMP tentam fazer...

Tentando evitar o chaveamento de contextos de tarefas


Num cenrio ideal teramos apenas uma thread sendo executada em cada ncleo o processador
lgico, evitando o task switching. Em teoria, isso faria com que nossos processos usem o total poder
de processamento do processador. No entanto, outros processos tambm esto em execuo, o que
estraga a coisa toda.
Tudo o que podemos fazer tentar minimizar a quantidade de threads que colocaremos em
execuo. A primeira coisa que precisamos saber para chegarmos a esse objetivo : Quantos
processadores eu tenho?. Eis uma rotina que nos devolve essa informao para vrios sistemas
operacionais:

131
/* threads.c */
#if defined(__linux__)
#include <unistd.h>
#elif defined(__WINNT__)
#include <windows.h>
#elif defined(__FreeBSD__)
#include <sys/param.h>
#include <sys/sysctl.h>
#endif

static int get_number_of_cores(void);

int get_number_of_processors(void)
{
#if defined(__linux__)
return sysconf(_SC_NPROCESSORS_ONLN);
#elif defined(__WINNT__)
/* FIXME: Essa funo s existe no Win7 e Win2008-R2 ou superiores. */
return GetMaximumProcessorCount(ALL_PROCESSOR_GROUPS);
#elif defined(__FreeBSD__)
int num_processors, r[2];
size_t size;

/* Pega `sysctl hw.ncpu` */


r[0] = CTL_HW;
r[1] = HW_NCPU;
if (sysctl(r, 2, &num_processors, &size, NULL) == -1)
return get_number_of_cores();
return num_processors;
#else
return get_number_of_cores();
#endif
}

static int get_number_of_cores(void)


{
int cores;

__asm__ __volatile__ (
movl $1,%%eax\n
cpuid\n
: =b (cores)
);

return (cores >> 16) & 0xff


}

Usando o OpenMP
Algum percebeu que algumas rotinas podem aproveitar o paralelismo, disponvel nos
processadores modernos automaticamente. Um padro foi divisado para permitir que isso fosse
feito sem que o desenvolvedor altere muito os cdigos pr-existentes. Trata-se da biblioteca
OpenMP (MP, de MultiProcess).
Suponha que voc queira preencher um array de 400 inteiros com zeros. A funo bvia para faz-
lo seria algo assim:
for (i = 0; i < 400; i++)
array[i] = 0;

Mas, e se pudessemos dizer ao compilador que esse loop pode ser dividido em loops menores
distribudos entre os ncleos do processador? Isso possvel graas ao OpenMP e diretiva do
compilador pragma:
#pragma omp parallel for
for (i = 0; i < 400; i++)
array[i] = 0;

Usar essa diretiva diz ao compilador para injetar cdigo que quebrar o loop em n loops individuais,

132
onde n o nmero de processadores (que o OpenMP sabe, sozinho, qual !). como se tivssesmos
um fork para n processos e depois um join, ao final do loop.
Crie uma funo simples, coloque o fragmento de cdigo acima (incluindo o header omp.h) e crie a
listagem em assembly... Ao dar uma olhada na listagem voc ver um cdigo enorme, que faz uso
de funes da libgomp (GNU OpenMP).
Essencialmente, usar OpenMP uma questo de usar um pragma que aplicvel para um bloco:
#pragma omp parallel [tipo] [clusulas]
{
cdigo que ser 'paralelizado' aqui
}

Os modificadores 'tipo' e 'clusulas' so opcionais e servem para otimizao e para evitar alguns
problemas. No nosso exemplo usamos o tipo 'for' para otimizar o paralelismo para loops. Mas, no
usamos quaisquer clusulas. O exemplo abaixo mostra a necessidade de uma clusula:
#pragma omp parallel for private(j)
for (i = 0; i < 400; i++)
for (j = 0; j < 100; j++)
matrix[i][j] = 0;

Sem a clusula private, acima, provavelmente o compilador tentaria paralelizar os dois loops. Ao
dizer que a varivel 'j' privada ao bloco, dizemos que o loop interno no deve ser paralelizado. Isto
, s o loop externo ser dividido em threads.
Podemos ser mais explcitos e adicionar a clusula 'shared', com a lista de variveis compartilhadas
pelas threads:
#pragma omp parallel for shared(i) private(j)

Existem outras diretivas alm de 'parallel' e outras clusulas alm de 'shared' e 'private' na
especificao.

OpenMP no mgico
S um lembrete: J que estamos falando de paralelismo, race conditions tambm podem aparecer
no uso de OpenMP. A biblioteca tambm fornece recursos de sincronizao para tentar minimizar o
problema, o que tambm pode acarretar em dead locks... Em resumo, os mesmos problemas que
voc teria com threads manualmente construdas em seu cdigo, pode obter com OpenMP.

Compilando e usando OpenMP


Alm de usar pragmas, temos que dizer ao compilador que estamos usando OpenMP e, quando
formos linkar, usar a bibioteca libgomp (no caso do GCC). Se precisarmos usar funes da
especificao do OpenMP em nossos cdigos, temos que incluir o header omp.h tambm. Uma
compilao com OpenMP ficaria assim:
$ gcc -O3 -march=native -fopenmp -c -o test.o test.c
$ gcc -o test test.o -lgomp

Sem usarmos '-fopenmp' e linkarmos com a libgomp, todos os pragmas sero inquos.
Ateno: OpenMP no substitui pthreads. E tambm no uma panacia! Ele existe como tentativa
de aproveitar o paralelismo simtrico de algumas arquiteturas. De fato, se voc chamar a funo
omp_get_max_threads, da libgomp, ver que ele devolve apenas o nmero de processadores lgicos
contidos no seu sistema. A diferena que via pthreads voc pode criar tantas threads quanto o

133
sistema operacional permitir, mas com OpenMP voc s pode criar um nmero limitado delas...
claro, no h garantias de que as threads criadas pela libgomp sero simtricas, mas essa a ideia...

OpenCL e nVidia CUDA


O CL em OpenCL significa Concurrency Library e CUDA Compute Unified Device
Arquitecture. A primeira (OpenCL) uma biblioteca genrica, usada para abstrair as unidades de
processamento em seu sistema, a segunda proprietria da nVidia e serve para usar as unidades de
processamento da sua placa de vdeo (GPU).
No caso do OpenCL o programador aloca dispositivos de processamento, sem necessariamente
saber quais so eles. Da, faz o upload de pequenos programas chamados kernels (no
confundir com o kernel do sistema operacional) que so executados em paralelo nesses dispositivos.
Numa mquina standalone (seu computador caseiro) o OpenCL usar os seus processadores lgicos
e os diversos cores da GPU contida na sua placa de vdeo. Tome como exemplo a placa de vdeo
que possuo em minha estao de trabalho: Uma nVidia GT-635. Segundo o site da nVidia essa placa
tem 384 CUDA cores. Meu processador um i7, com mais 8. Com OpenCL posso, em teoria,
criar programas que executem 392 threads concorrentes.
Num ambiente mais profissional, podemos mapear processadores remotos para juntarem-se ao
time... Imagine um data center com 100 mquinas iguais a minha. Podemos ter o valor terico de
39200 therads concorrentes executando, mesmo em mquinas diferentes.
Na prtica, usar tantas threads causam muitos problemas, especialmente com relao
sincronizao. A linguagem usada pelo OpenCL (e pelo CUDA) ajuda um bocado, j que parecida
com C, mas tem algumas caractersticas prprias, dedicadas a esse tipo de ambiente.
No vou mostrar um exemplo aqui. Se quiser saciar sua curiosidade procure documentao sobre
OpenCL e saiba que os drivers de sua placa de vdeo disponibilizam bibliotecas dinmicas para que
voc possa usar esse recurso.

134
Captulo 12: Ponto flutuante
Neste captulo quero mostrar que lidar com ponto flutuante no uma coisa to simples quanto
parece. Existe muita confuso sobre o assunto porque estamos acostumados a lidar com esses tipos
de valores, com componentes fracionrias em base decimal, e pensar neles como exatos. No o
que um computador faz. Ele sempre lida com valores em formato binrio e inteiros. Ponto flutuante
um artifcio, incorporado nos processadores atuais, que permite representar valores fracionrios.
Essa representao no perfeita. aproximada. Entender os problemas causados por essas
imperfeies te faro evitar ou minimizar erros de impreciso ao usar tipos como float e double.

Preciso versus Exatido


No nosso dia a dia, quando se fala em preciso estamos falando de o quo prximo da realidade um
valor calculado pode estar de um valor exato. Isso implica em algum tipo de arredondamento. Eis
um exemplo simples: O valor 2 exato, mas no . A frao composta de dois valores exatos,
mas seu resultado s pode ser expresso com alguma aproximao.
O termo preciso precisa ser melhor definido: No contexto da aritmtica com ponto flutuante ele
significa a quantidade de digitos ou algarismos usados na representao do valor. Ele no significa
a quantidade de digitos ou algarismos depois da vrgula. uma distino importante e deve ser
mantida em mente para o melhor entendimento do restante deste captulo.

O que ponto flutuante?


Todos os tipos de dados, em linguagens de programao como C, tm tamanho fixo. Assim como
char, int e long, os tipos float, double e long double no so excees. O tipo float tem exatamente
32 bits de tamanho, o double tem 64 e o long double, 80. Para facilitar a discusso sobre ponto
flutuante lidarei apenas com o tipo float daqui por diante. Pode-se extrapolar a explicao para
double e long double aumentando o tamanho de seus componentes, como veremos seguir.
Quanto maior o tipo, maior a quantidade que pode ser armazenada em seu interior. Veremos mais
adiante que float pode representar valores com preciso de, pelo menos, 7 algarismos decimais.
Para conseguir usar mais algarismos temos que recorrer a tipos maiores como double e long
double.
Se podemos usar apenas 7 algarismos num tipo float, valores como 3,1415926 no podem ser
representados de forma exata. Graas limitao da preciso esse valor transforma-se em
3,141593. Note que ele tem que ser arredondado para que a preciso de 7 algarismos seja atendida.
Usando a preciso de 7 algarismos, se multiplicarmos 3,141593 por 10 a vrgula flutuar para a
direita, obtendo 31,41593. Continuamos com a preciso de 7 algarismos, mesmo que tenhamos uma
quantidade inferior de casas depois da vrgula. Essa flutuao da vrgula (ou do ponto) o que
d nome a esse tipo de aritmtica.

Estrutura de um float
Como todo tipo usado em linguagens de programao, a atribuio da faixa de valores vlidos, em
base decimal, uma iluso conveniente. Os tipos lidam somente com valores binrios. A frmula
abaixo nos diz como um valor do tipo float armazenado56:

56 Para o tipo double basta considerar 'e' com 11 bits e a mantissa 'm' com 53. O valor 127 torna-se 1023.

135
Os valores s, m57 e e (iniciais de sinal, mantissa e expoente) so obtidos da estrutura
binria do tipo:

Figura 15: Representao de um float.

A mantissa corresponde a parte fracionria do valor em ponto flutuante. Embora a palavra


mantissa no seja uma boa definio para a parte fracionria de um nmero real, j que
definida, matematicamente, como parte fracionria de um logaritmo. Na falta de um termo
melhor, e por motivos histricos, usarei esse termo mesmo.
Quando digo que a mantissa somente a parte fracionria, quero dizer que a parte inteira do valor
armazenado na estrutura implcito e sempre igual a 1, em binrio. Abaixo, falo sobre a analogia
com a notao cientfica, que explica melhor o porqu do uso dessa tcnica. No caso de um float
a mantissa tem 23 bits de tamanho.
Isso significa que a quantidade de bits no campo m tem, realmente 24 bits de tamanho. Embora a
mantissa seja a parte fracionria o que temos um valor inteiro, positivo, de 24 bits que, quando
multiplicado por representar um nmero que contm uma parte fracionria.
Num float o expoente e tem 8 bits e varia de 0 a 255, fazendo com que a potncia de dois, usada
para escalonar o valor binrio (ou deslocar a vrgula), na frmula acima, possa variar entre -127 e
128. No restante do captulo falarei de e como se ele tivesse sinal e usarei um E, maisculo, para
obter o expoente real. Tenha em mente que o sinal obtido a partir da subtrao de e com o mximo
nmero positivo de 8 bits. Ou seja, o expoente de base 2 sempre calculado como:

Da estrutura sobra apenas o bit s. Se esse bit estiver setado, significa que o valor representado na
mantissa negativo, caso contrrio, positivo.
O campo m a parte interessante. Ele inteiro, mas representa valores fracionrios, j que seus bits
so ordenados a partir da posio -1 (bit mais significativo) at a posio -23 (bit menos
significativo). Desconsiderando o expoente e (fazendo igual a 127 e, portanto ), pode parecer
que o menor valor positivo que pode ser armazenado na mantissa 1 (o bit implcito) e o prximo
valor possvel seria . Veremos mais adiante que esse degrau chamado de (letra grega
epsilon) e importante para podermos fazer comparaes entre valores... Eu disse pode parecer
porque h outra maneira de codificar um valor na estrutura de um float.

Analogia com notao cientfica


O motivo de adicionarmos 1 antes do m pode ser entendido, por analogia, ao conceito de notao
cientfica. Em fsica, costumeiro expressar valores muito grandes ou muito pequenos usando
potncias de 10 (ou seja, ) e limitando a parte inteira do valor a apenas um algarismo diferente
de zero. Para escrever o valor 10 bilhes mais fcil escrever do que 10000000000.
Gasta menos espao.
Num valor em ponto flutuante acontece a mesma coisa. O nico valor diferente de zero que a
57 A mantissa m representada em binrio nessa notao. E o expoente e em decimal. Isso pode confundir, mas a
maneira que tenho para compreender o que acontece... Pense em e como o deslocamento do ponto binrio, ao
invs de uma potncia de base 2...

136
poro inteira pode assumir 1 porque, em notao cientfica na base 2, o nico algarismo diferente
de zero que temos 1. Ele implcito para poupar espao, ou melhor, para acrescentar um bit na
exatido do valor, e no precisa estar codificado na estrutura do float. Assim, num tipo float, temos
sempre 24 bits de preciso: O bit 1, implcito, e os 23 bits da mantissa.
Note que qualquer nmero dentro da faixa de preciso de um float pode ser expresso dessa maneira.
Por exemplo, 0.5 pode ser escrito, em binrio, como . Isso
ser codificado num float como s=0, e=126 (para E=-1) e m=0. O valor inteiro contido numa
varivel float ser 0x3F000000, como mostro no exemplo abaixo:
/* test.c */
#include <stdio.h>

void main(void)
{
float x = 0.5f;

printf("%#08X", *(unsigned int *)&x);


}
-----%<----- corte aqui -----%<-----
$ gcc -o test test
$ ./test
0x3F000000

Valores especiais na estrutura de um float.


Existem quatro excees importantes ao esquema previamente apresentado: O valor 0.0 um dos
casos especiais. Se, na estrutura do float, o e for zero e a mantissa tambm, ento teremos um valor
zero.
A segunda exceo acontece quando e for zero e a mantissa no for. Neste caso temos um valor em
ponto flutuante denormalizado ou subnormalizado. Essa categoria de valores recebe esse nome
porque no segue norma (que assume um valor inteiro 1.0 implicito). Esses valores especiais so
usados para representar valores bem prximos de zero e seguem a frmula:

As outras duas excees acontecem quando e for igual a 255 (0xff). Neste caso, se m for zero temos
a representao de infinito, mas se m for diferente de zero temos a representao de algo que no
um nmero (NaN ou Not A Number). Valor NaN ocorre quando, por exemplo, tentamos extrair a
raiz quadrada de nmeros negativos ou quanto tentamos calcular 0/0.
Existem dois tipos de NaNs: qNaNs e sNaNs. O q usado como prefixo na sigla significa quieto.
Em casos como a extrao de raiz quadrada de valores negativos, o valor obtido um qNaN. A
diferena entra um qNaN e um sNaN, sendo esse ltimo um NaN sinalizado, que este ltimo
representa um erro... No que um qNaN tambm no seja, mas consumir um sNaN pode ser usado
para disparar uma rotina de tratamento de erros enquanto um qNaN facilmente ignorado.
Eis algumas operaes que resultam em NaN:

137
Operao Motivo
0/0 Valor indeterminado.
Valor indeterminado.
, Valores inderminados.
, Tangente desses arcos no existem.
, , Logartmos de valores negativos no existem.
Arco-seno e arco-cosseno de valores fora da
arcsen, arccos
faixa entre so invlidos.
Qualquer operao envolvendo NaNs. NaN no um valor!
Tabela 9: Casos onde ocorrem NaNs

Operaes como e so definidas como tendo resultado igual a 1,0 e, portanto, no resultam
em NaNs, mas para valores diferentes de 0 e a potncia provavelmente resultat em NaN.

Pra que os valores denormalizados existem?


Pode parecer que , que o menor nmero normalizado que pode ser representado num float,
seja pequeno o suficiente para qualquer possvel valor que queiramos usar, mas acontece que em
operaes aritmticas podem ocorrer underflows.
Underflow, claro, o contrrio de overflow. Um overflow acontece quando extrapolamos o
mximo valor possvel e underflow, o mnimo... Os valores denormalizados so a maneira que o
padro IEEE 754 achou de criar underflows progressivos. Ou seja, se o resultado de uma operao
for denormalizado ele ainda vlido, mas o desenvolvedor deve tomar cuidado...
Abaixo de , o menor valor que pode ser armazenado num nmero denormalizado .
claro que a faixa aumenta se estivermos falando de tipos como double e os underflows ficaro ainda
menores.

Intervalos entre valores


Por causa do nmero limitado de bits no h como representar todos os valores no domnio dos
nmeros reais. No caso de float, o menor incremento entre valores que pode ser obtido
(desconsiderando o escalonamento) da ordem de , ou seja, se o LSB da mantissa estiver
setado. Se a mantissa tem 23 bits de tamanho e o bit mais significativo corresponde ao valor ,
ento o bit menos significativo dela corresponde ao valor .
Esse valor ( ), onde desconsideramos o escalonamento (E=0), chamado de epsilon e
representado pela letra grega e pela constante FLT_EPSILON, no header float.h. Ele definido
como o menor incremento possvel para o valor 1,0. Se o valor representado no float for maior que
1,0, ento precisar ser escalonado de acordo, para obtermos o menor degrau de variao possvel
Com os valores denormalizados o bit mais direita que representaria estar escalonado por um
fator de , ou seja, o menor degrau de um valor denormalizado de . Mas a faixa dos
valores denormalizados pequena em relao a toda a faixa dos valores normalizados. Como regra
geral podemos considerar que o valor de , para intervalos entre 1 e 2, ele prprio ( ), para
valores entre 2 e 4 o degrau aumenta para , j que o ltimo bit desaparecer (o ponto foi
deslocado para a direita!), entre 4 e 8 o valor aumenta para e assim por diante.
Da mesma maneira, entre 1 e o valor de ser de , entre e ele muda para e assim por

138
diante.
Conforme os intervalos vo ficando maiores devido ao incremento do expoente, maior vai ficando o
intervalo entre os valores representveis. A figura abaixo ilustra:

Figura 16: O intervalo mnimo entre valores (epsilon).

Repare que a cada vez que ganhamos um bit na parte inteira, perdemos um na parte fracionria, por
isso o intervalo dobra.
Para valores muito grandes, por exemplo, , a menor distncia entre valores adjacentes ser de
aproximadamente 1,0. Lembre-se que, num float, a preciso de, aproximadamente, 7 algarismos
decimais e, como 10 milhes tem 7 algarismos, a menor distncia ser de 1... Se o valor fosse
, a menor distncia entre valores adjacentes ser da ordem de 1000.
Esses so clculos aproximados que so melhor compreendidos em binrio, usando a estrutura do
float... O valor ser codificado como s=0, e=160, m=0x1502F9. O valor de e desloca a
virgula para a direita em 33 bits ( ) e o valor de m, adicionando o valor 1.0,
implicito, torna-se 0b1.00101010000001011111001. Deslocando a vrgula teremos 0x2540BE400
que d, exatamente, 10000000000 em decimal. Se adicionarmos 1 ao LSB da mantissa teremos
m=0x1502FA que, ao somamos 1.0 e deslocarmos a vrgula obteremos 0x2540BE800 ) que d
exatamente 10000001024, em decimal.
Isso torna os clculos de valores monetrios de grande monta um problema. O que acontece se um
sujeito ou uma empresa tem R$ 10 milhes em alguma conta e h um rendimento de R$ 500,00? O
resultado final continuar sendo R$ 10 milhes e os R$ 500,00 que foram adicionados ser perdido,
j que no pode ser armazenado num float! O intervalo entre valores adjacentes devem ser sempre
levados em considerao com base no maior valor usado na operao!

Um problema de base
Assim como na nossa batida base 10, em base 2 existem valores que no podem ser expressos
exatamente. Na base 10 as fraes , , , etc no podem ser expressas exatamente. Voc obter
uma dzima peridica. Se usssemos outra base numrica, como a base 3, por exemplo, a frao
seria exatamente igual a . Mas, o processador trabalha com a base 2...
Eis outra surpresa: Em relao aos valores fracionrios, s possvel escrever com exatido os
valores terminados em 0 e 5, na base 2! (0,5, por exemplo exatamente ). Para entender porqu,
eis um programinha que nos mostra a estrutura de floats:
#include <stdio.h>

/* 0.4, 0.6, 0.8 e 0.9 so mltiplos de 0.2 e 0.3. No os coloquei aqui para no
usar muito espao em mostrar os resultados. */
float values[] = { 0.1, 0.2, 0.3, 0.5, 0.7 };
#define ARRAY_SIZE (sizeof(values) / sizeof(values[0]))

void main(void)
{
int i;

for (i = 0; i < ARRAY_SIZE; i++)


printf("%.1f: 0x%08X\n", values[i], *((unsigned *)&values[i]));
}
-----%<----- corte aqui -----%<-----
$ gcc -o imprecise imprecise.c

139
$ ./imprecise
0.1: 0x3DCCCCCD
0.2: 0x3E4CCCCD
0.3: 0x3E99999A
0.5: 0x3F000000
0.7: 0x3F333333

D para perceber que, em todos os casos, exceto com 0.5, o valor da mantissa uma dzima
peridica? O compilador s resolveu arredondar alguns valores para cima para que esses fiquem
mais prximos do valor real.
Tomemos 0,1 como exemplo: Em decimal ele s pode ser transformado em
. Faa as contas e ver que algo semelhante ocorre com os outros valores
que citei... A dizima peridica s pode indicar uma coisa: Aproximao. Nunca exatido!
Vendo de outra forma, cada bit da mantissa equivale a uma potncia de 2 com expoente negativo.
Depois do 1,0 implcito temos 0,5; 0,25; 0,125; 0,0625; 0,03125; Os valores sempre terminam
em 5! Na verdade eles terminam em 1, em binrio... Assim, na hora de arredondar, o compilador
somar um 5 na ltima casa decimal.
como aquela regrinha de arredondamento que usvamos na escola...

O conceito de valor significativo


Quantas vezes, durante o perodo escolar, voc no teve que fazer clculos usando o valor com
duas casas decimais apenas? Ao invs de usar 3,141592653589793238462643383279502 voc no
usou 3,14? Dependendo da grandeza do que voc teve que calcular o valor 3,14 atendia muito bem.
Da a preciso de 2 casas depois da vrgula era significativa e voc ignorava todo o resto (afinal,
um valor irracional!). A mesma coisa acontecia com o valor da acelerao da gravidade g...
Na verdade voc no ignorava todo o resto, voc somava 0,005 e depois ignorava todas as cadas
decimais alm da segunda.
Outro exemplo: Quando vai calcular distncia percorrida por um trem naqueles famigerados
probleminhas de Fsica, desconsiderava grandezas inferiores a metros, por exemplo... Centmetros
ou milmetros, neste caso, no tem l grande influncia no que significa distncia, no resultado do
problema.
A biblioteca padro faz a mesma coisa com os valores em ponto-flutuante. Algumas imprecises
so simplesmente ignoradas, aps o arredondamento, quando imprimimos valores via printf(), por
exemplo. Um erro de , devidamente escalonado em relao ao expoente, jogado fora. No
tem significado no clculo!
Isso no significa que o processador no use esse pequeno erro para obter o valor final. na hora de
mostr-lo que o erro desconsiderado atravs de arredondamento. Este um detalhe muito
importante ao lidarmos com ponto flutuante: Os clculos so feitos sempre com os valores como
eles esto e na hora de mostr-los que arredondametos so feitos!
Mas, arredondamentos no so to simples assim. Existem quatro tipos diferentes:
Para cima, em direo ao +;
Para baixo, em direo ao -;
Em direo ao zero e;
O mais prximo possvel.
Normalmente o processador configurando para arredondar para o valor mais prximo possvel e
existem diversas vantagens nisso do ponto de vista matemtico. Os outros tipos de arredondamento

140
podem ser usados em casos especiais, bastando reconfigurar a unidade aritmtica.
O arredondamento resolve o problema da limitao de bits da mantissa, mas gera outros
interessantes...

Regras da matemtica elementar nem sempre so vlidas


Alm da impossibilidade de obter um valor real a partir de uma diviso por zero ou da extrao de
uma raiz quadrada de um nmero negativo, voc tambm deve ter aprendido na escola que as
operaes fundamentais tm as seguintes propriedades:
Comutativa: ou ;
Associativa: ou
Distributiva:
O que nos fora a aceitar um fato surpreendente sobre aritmtica com ponto flutuante: Das trs
regras, apenas a comutativa sempre vlida. As propriedades associativa e distributiva nem sempre
ocorrem. E no estou falando de valores de grandezas diferentes. Eis um exemplo:
double x = 0.1 + (0.2 + 0.3);
double y = (0.1 + 0.2) + 0.3;

/* Imprimir x diferente de y! */
if (x == y)
puts("x igual a y");
else
puts("x diferente de y");

O problema ocorre porque, graas ao arredondamento, existem erros diferentes para cada uma das
representaes de valores acima. O valor 0.2, por exemplo, pode ter um erro de arredondamento um
pouquinho maior do que a representao do valor 0.1 e 0.3, por exemplo. Assim, o valor 0.5,
calculado a partir dos valores aproximados de 0.2 e 0.3, no exato, arredondado.
O que quero dizer que voc no est vendo adies exatas no exemplo ai de cima. O 0,3 calculado
a partir da adio de 0,1 e 0,2 no o mesmo 0,3 que adicionado em seguida, no caso da varivel
y. Voc obter um valor um pouco fora do 0,6 esperado. A mesma coisa acontece com o clculo da
varivel x, mas com um erro um pouco diferente do 0,6 calculado para y... Assim, os dois 0,6 sero
diferentes!
Lembra-se que o valor de maior quando o valor estiver entre 0,5 e 1,0 do que quando ele est
entre 0,25 e 0,5 e vai ficando menor quando a faixa vai sendo escalonada por um fator de ? Ao
somar 0,2 e 0,3 obtemos 0,5 somando a um grande. E ao somar 0,1 e 0,2 obtemos um 0,3 com
um menor que o outro. Quero dizer:

Ao somar os valores restantes teremos a adio de erros parecidos (j que supostamente teramos os
mesmos resultados, mas observe o que obtemos:

O que obviamente far x e y serem diferentes!


Essas acumulaes de erros tambm ocorrem em multiplicaes e divises. Da a propriedade
distributiva tambm falha.

141
Veremos, mais adiante, como lidar com comparaes de valores em ponto flutuante, levando esses
pequenos erros em considerao.

Que tal trabalhar na base decimal, ao invs da binria?


Desde a especificao ISO C99 (pelo que me lembro), a linguagem C possui mais 3 tipos de ponto
flutuante alm dos float, double e long double. Trata-se de tipos de ponto flutuante decimais:
_Decimal32, _Decimal64 e _Decimal128. Esses trs novos tipos esto na especificao IEEE 754
desde 2008.
O nmero que segue o tipo diz o tamanho do mesmo, em bits. Mas, sua representao interna
equivalente decimal, no a binria. Assim, o valor armazenado , mais ou menos, codificado
como:

Onde 'd' um dgito entre 0 e 9, 'm' a mantissa (em decimal) e o resto parecido com os formatos
de ponto flutuante binrio, mas em base 10.
Isso permite a representao daqueles valores: 0,1; 0,2; ...
Mas, existem alguns problemas ao lidar com esses tipos: O primeiro que eles so pouco
performticos. H dependncia de uso de funes especiais para lidar com eles. O processador no
os suporta nativamente. E a codificao dos valores um tanto complexa.
O segundo problema que funes da libc, como printf, no tm suporte a esses tipos. Pelo menos
no a libc pr-compilada pelo mantenedor das distribuies mais famosas do Linux... Uma maneira
de verificar se a libc suporta esses tipos perguntando ao compilador:
$ gcc -v 2>&1 | grep "\-\-enable\-decimal\-float"

Provavelmente o grep no encontrar essa string na lista de flags usados na configurao do gcc58.
O fato de printf no suportar os tipos _DecimalXX no significa que eles no possam ser usados.
Para mostr-los teremos que convert-los para float ou double e usar a funo printf. A converso
feita atravpes de funes built-in do compilador. Consulte a documentao do GCC...
Outra coisa: Os valores literais tm um sufixo especial. Por exemplo:
_Decimal32 d32 = 1.2df; /* df = decimal float? */
_Decimal64 d64 = 0.4dd; /* dd = decimal double? */
_Decimal128 d128 = 0.3dl; /* dl = decimal lond double? */

Lembre-se tambm que esses tipos no resolvem o problema da preciso. Nmeros como 2/3
continuam sendo irracionais...
Minha opinio sobre os tipos ponto flutuante decimais que eles so inteis por causa do problema
da baixa performance, em relao aos tipos em ponto flutuante binrios.

A preciso decimal de um tipo float ou double


At agora falamos da preciso de um float sob o ponto de vista binario. Para calcular a preciso
decimal, grosseira, basta usar um logartmo:

Onde pbinria a quantidade de bits na mantissa mais o 1.0 implcito, no caso de valores
normalizados. E pdecimal o nmero de algarismos significativos na base 10. Para o tipo float temos

58 Os caracteres '-', no grep foram escapados para que ele no confunda a regular expression com um parmetro.

142
pdecimal=8. Para o tipo double, pdecimal=17.
A especificao de C nos diz que a preciso do tipo float de, pelo menos, 6 digitos. E do tipo
double, 15. Isso condizente com a frmula acima.
Note que, mesmo que tenhamos um valor do tipo 1,2345671033, isso no a mesma coisa que um
valor inteiro de 34 algarismos (deslocando a vrgula 33 vezes para a direita!). O nmero de
algarismos significativos (a preciso!) no pode ser maior que 7 porque tantos algarismos no
podem ser obtidos com a preciso de 24 bits da mantissa de um float!

Comparando valores em ponto flutuante


Graas aos pequenos arredondamentos nos clculos com valores em ponto flutuante precisamos de
um jeito de comparar dois valores prximos como se fossem iguais. Fazer uma comparao do
tipo abaixo, como vimos antes, pode ser problemtica:
if (x == y)

Se x e y forem semelhantes, diferindo em um erro pequeno, ento elas no sero iguais. Por
exemplo, ao comparar uma constante 0,1 com um valor calculado que d 0,1 como resultado de
uma operao aritmtica, teremos pequenos erros que tornam ambos diferentes entre si. E a
comparao acima vai falhar (x == y ser falso).
Uma maneira melhor de garantir que dois valores prximos sejam considerados como iguais
usando um fator de diferena mnima. O valor de (epsilon):

Se a diferena entre x e y for menor ou igual a , ento podemos consider-los iguais. O motivo de
termos que comparar o valor absoluto da diferena com porque tanto x quanto y podem ser
negativos. Isso nos d um macro:
#define equal(a,b) (fabs((a)-(b)) <= FLT_EPSILON)

Mas existe um problema...


Lembre-se que o valor de ser escalonado por quando . O valor de pode ser pequeno
demais para a maioria das comparaes! Uma soluo obter o valor de relativo ao maior valor
dos dois sendo comparados:

O macro para o clculo desse erro relativo ficaria assim:


inline int float_cmp(float a, float b)
{
float A = fabs(a), B = fabs(b);
float diff = A B;

return diff <= (FLT_EPSILON * max(A, B));


}

A especificao ISO C99 nos d os seguintes macros para comparar valores em ponto flutuante
(definidos em math.h): isgreater(), isgreaterequal(), isless(), islessequal() e islessgreater(). O
ltimo compara pela diferena. Estranhamente ele no fornece um isequal().
Usar para decidir como comparar valores parece ser uma boa idia, especialmente quando usamos
o erro relativo. Ao subtrair dois valores muito parecidos o resduo poder resultar num valor
denormalizado ou, pelo menos, bem prximo de zero. Isso no significa que voc no possa usar

143
seu prprio . Alis, recomendvel que o faa! Por exemplo, se minhas contas no precisam ter
mais que 4 algarismos depois da vrgula, posso definir um de 0,0001 e criar meus macros
substituindo a constante FLT_EPSILON por uma chamada FLOAT_EPISOLON, por exemplo,
contendo esse valor.

No compare tipos de ponto flutuante diferentes


Quando voc lida com constantes literais, em C, est atrelando a elas um tipo definido de tipo. Se
escreve 1.1 este tipo , por default, double. Se escreve 1.1f ou 1.1F, o tipo float.
Comparar 1.1 com 1.1f pode ser desastroso. As precises so diferntes e, portanto, os erros de
arredondamento so diferentes. No cdigo abaixo:
float x = 1.1;

if (x != 1.1)
printf("So diferentes!");

Provavelmente obter a string so diferntes! impressa. O detalhe que o compilador converter o


valor double 1.1 para o tipo float, fazendo os arredondamentos necessrios e o atribuir a x, em
tempo de compilao. Mas, no 'if' a converso ser feita em runtime e no sentido contrrio... Pe
acordo com a especificao da linguagem, C sempre converte tipos conflitantes para aqueles de
maior preciso. Antes de comparar x com 1.1 a varivel ser convertida para double (em runtime), e
usando as regras de arredondamento do processador, no do compilador!
Ou seja, voc est comparando laranjas com mas...
Regra geral: Mantenha seus tipos, em ponto flutuante, sob controle... Se tiver floats, compare-os
com floats... Se forem doubles, compare-os com doubles.
Algumas maneiras de conseguir isso:
Se quiser usar floats, use o sufixo 'f' nos valores literais. SEMPRE!
Use type casting, se no puder usar o sufixo.
O fragmento de cdigo acima poderia ser escrito assim:
float x = 1.1f

if (x != 1.1f)
...

Evite overflows!
Uma dica importante sobre overflows. Especialmente se voc quer usar ponto flutuante para
calcular potncias. O exemplo clssico o do famoso teorema de Pitgoras:

A equao parece inofensiva, mas se um dos valores de a ou b estourarem, por causa da elevao da
potncia ao quadrado, voc ter um valor invlido dentro da raiz quadrada e o resultado,
provavelmente, ser um NaN. Como evitar isso?
No caso de Pitgoras, o mtodo tradicional elevar ao quadrado apenas o menor valor, retirando o
maior valor da raiz quadrada, assim:

144
Se no causa um underflow ento a equao no criar um overflow de forma alguma... E, s
para satisfazer sua curiosidade, essa nova interpretao do teorema pode ser entendida assim:

importante lembrar que os valores denormalizados existem para garantir um underflow gradual,
mas para overflows isso no existe. Assim, possvel que seja computado com um valor
denormalizado, se a for muito maior que b.
Neste caso, tambm importante escolher o maior dos dois valores para retirar da raiz. A rotina
poderia ficar assim:
#define swapf(a,b) { float t; t = (a); (a) = (b); (b) = t; }

float hipotenuse(float a, float b)


{
if (b > a)
swap(a, b);

if (!a)
return b;

return a * sqrtf(1.0f + (b*b)/(a*a));


}

Ponto fixo
Se voc precisa lidar com valores monetrios e no quer gastar muito tempo analisando os efeitos
de acumulao erros em ponto flutuante, sempre pode usar a tcnica do ponto fixo. Diferente do
ponto flutuante, essa tcnica permite que operaes fundamentais sejam feitas em inteiros como se
contivessem valores em ponto flutuante.
Aritmtica com inteiros bem mais veloz e mais simples. No caso de valores monetrios
precisamos apenas de duas casas depois da vrgula e da parte inteira. Nenhuma das operaes
precisa de preciso maior que essa. Abaixo temos algumas rotinas para demonstrar a tcnica:
#include <math.h>

#define MUL100(x) ((x) * 100)


#define DIV100(x) ((x) / 100)

/* Rotinas de converso */
long float_to_money(float x) { return (long)floorf(x * 100.0f); }
float money_to_float(long x) { return (float)x / 100.0f; }

/* Operaes elementares */
// long money_add(long x, long y) { return x + y; }
// long money_sub(long x, long y) { return x y; }
long money_mul(long x, long y) { return DIV100(x * y); }
long money_div(long x, long y) { return MUL100(x) / y; }

Com essa tcnica todo valor em ponto flutuante multiplicado por 100. Um valor 1,5 torna-se 150.
Um valor 3.1415 torna-se 314. Se voc quiser um arredondamento para cima basta substituir a
funo de converso float_to_money por:
long float_to_money2(float x) { return (long)ceilf(x * 100.0f); }

Adies e subtraes podem ser feitas diretamente ou, se preferir, via funes money_add e
money_sub (comentadas). O detalhe todo est nas multiplicao e diviso: J que multiplicamos o
valor original, digamos 'x', por 100, o que temos . Ao multiplicar x por y, temos
ou ento . Fica claro que temos uma multiplicao por 100

145
adicional nessa operao, da a necessidade de dividir o valor final por 100.
Com a diviso o problema semelhante... Ao dividir dois valores escalonados por 100, acabamos
sem escalonamento algum. Afinal nos d . Da a necessidade de multiplicar o dividendo por
100 antes de realizar a diviso.
Para usar as funes basta fazer algo assim:
long x, y, r;

x = float_to_money(10.0f);
y = float_to_money(33.2f);
r = money_mul(x, y);
printf("10.00 * 33.20 = %.2f\n",
money_to_float(r));

As operaes sero feitas com inteiros, que so exatos e as nicas converses ocorrem para
obteno dos valores. E j que estamos lidando com valores exatos, comparaes tambm no
causam problemas...
Outra ventagem: O tipo long suporta armazenar valores at 91018. Multiplicando por 100
limitamos esse valor para . Isso um 9 seguido de 16 zeros, ou 90 quintilhes com duas
casas decimais. Suficiente para qualquer aplicao que lida com dinheiro, no?

Modo x86-64 e ponto flutuante


Quando falei sobre misturar C com assembly, falei sobre a conveno de chamada e mostrei que os
valores em ponto flutuante so passados atravs dos registradores XMM0 at XMM7. Isso quer
dizer que a arquitetura x86-64 usa SSE. Acontece que ponto flutuante bviamente existia antes
dessa arquitetura e um co-processador matemtico (incorporado na arquitetura Intel desde os 486)
era necessrio.
A forma como o co-processador matemtico funciona um pouco diferente do SSE. Ele possui uma
pilha de 8 nveis onde os valores so inseridos e depois das operaes feitas. Da mesma forma que
feito numa calculadora HP... Isso chama-se notao polonsa reversa e um esquema mais
complicado e menos flexvel do que o do SSE.
O nico motivo que voc tem para aprender sobre o co-processador matemtico que ele permite
precises ainda maiores que o SSE... Enquanto float tem 32 bits de tamanho e double, 64. O co-
processador suporta valores com 80 bits de tamanho!

O tipo long double


O padro IEEE 754, que dita as regras para os nmeros em ponto flutuante binrios, especifica
quatro tipos, em sua ltima reviso (2008): single precision, double precision, extended precision e
half precision.
A preciso estendida conhecida por long double e foi adotada pela IEEE depois que a Intel
incorporou em seu co-processador matemtico 8087 o armazenamento de 80 bits. Esse no um
tipo l muito amigvel, mas a mantissa tem 64 bits (contra os 54 do tipo double e dos 24 do tipo
float) e o expoente maior tambm (15 bits). O algarismo 1, implcito nos tipos float e double
explcito nesse formato e o formato preferido do 8087.
O grande problema do tipo long double que s podemos us-lo atravs da pilha do 8087. Isso gera
muitas instrues e cria um problema ainda maior: Ao mudar a preciso entre tipos, ocorre o mesmo
problema que citei nas operaes aritmticas fundamentais: adio de erros de arredondamento!
Como o 8087 realiza todas as operaes com preciso mxima, ao adicionarmos dois floats, por

146
exemplo, eles tero que ser, necessariamente, convertidos para long double pelo processador e, para
isso, precisaro ser arredondados antes da operao ser feita. Da temos triplo arredondamento: O
arredondamento de cada operando transformado para long double, seguido do arredondamento da
operao e, por fim, o arredondamento do resultado de volta ao tipo original.
Isso no ocorre com SSE. Bem... ocorrer se voc tentar transformar um float num double ou vice-
versa, mas a carga de floats e doubles direta, se voc usa SSE ou SSE2.
Opereraes usando SSE, mesmo no modo i386, tendem a ser mais precisas do que quando usamos
o 8087 (o default, no modo i386)... Felizmente, no modo x86-64, SSE e SSE2 so os defaults.

147
148
Captulo 13: Instrues
Estendidas
Em 1996 a Intel anunciou uma extenso para a sua linha de processadores chamada MMX. A coisa
causou um alvoroo porque MMX uma sigla proprietria que significa MultiMedia eXtension. A
ideia era que os novos processadores lidariam com multimedia (ou seja, vdeos!) por hardware.
Nada poderia ser mais falso!
No que essa extenso no acelere o processamento de vdeos... De fato, o faz. Mas, MMX, SSE
(Streaming SIMD Extension) e AVX (Advanced Vector eXtension a verso mais recente) nada
tm haver com multimedia. E a coisa toda sequer uma inveno da Intel: Nos anos 70 a Texas
Instruments criou o conceito de SIMD (Single Instruction, Multiple Data). O nome diz tudo: Uma
nica instruo pode lidar com mais de um dado, ao contrrio do que a maioria dos processadores
fazem, normalemente.
Hoje em dia o MMX foi relegado a uma mera curiosidade. SSE e AVX, bem como outras extenses,
so mais avanadas e possuem menos problemas. Para citar um, o MMX usa a pilha do ponto
flutuante como se fossem registradores individuais, de forma que voc no pode usar as instrues
de ponto flutuante tradicionais e MMX sem que alguns cuidados sejam observados. Isso no
acontece com SSE. Por causa disso, vou ignorar o MMX completamente e partir logo para o SSE. O
caso do AVX outro: Nem toda arquitetura x86 o suporta. Se bem que todas os processadores
modenos (i3, i5 e i7) tendem a suport-lo. Mostrarei as diferenas mais adiante.
Alm das extenses MMV, SSE e AVX, outras foram incorporadas aos processadores: BMI (Bit
Manipulation Instructions), FMA (Floating point Multyply and Add) e F16C (Floating point 16 bit
Conversion).

SSE
Para processar vrios dados ao mesmo tempo, SIMD usa registradores especiais que contm
conjuntos de dados. No caso da plataforma x86-64, temos 16 registradores que podem conter at 16
bytes (ou 8 'shorts', ou 4 'ints' ou 'floats', ou 2 'doubles' e at um grande nmero inteiro de 128 bits
de tamanho). So os registradores XMM0 at XMM15:

Figura 17: Capacidade dos registradores XMM (SSE).

SSE vem em diversos sabores. SSE, SSE2, SSSE3 (sim, so trs 'S'), SSE4.1 e SSE4.2. Cada um
adiciona ao anterior um conjunto de instrues especializadas.

149
Funes intrinsecas para SSE.
A maneira de acessar esses registradores especiais, em C, atravs de tipos especiais de classes
de armazenamento. O tipo __m128 (com dois 'underscores') equivale a um registrador SSE ou a 16
bytes, dependendo de onde o compilador escolher armazen-lo. Este tipo divide um registrador
XMM em 4 floats. Temos ainda o tipo __m128i e __m128d. O primeiro usado para lidar com os
registradores XMM como se contivessem valores inteiros (inclusive de 128 bits) e o outro usado
para lidar dividir os registradores XMM em 2 doubles.
Outra vantagem de usar o tipo que ele automaticamente alinhado em 16 bytes. Esse alinhamento
importante, j que a carga de um registrador SSE atravs de um conjunto de dados desalinhados,
na memria, consome, pelo menos, 1 ciclo de mquina adicional. O alinhamento tambm
importante com relao aos caches. Num cache com bloco de 64 bytes, podemos ter a carga de at 4
registradores SSE atravs de um nico bloco de cache. Para exemplificar, a multiplicao de
matrizes de 4 linhas e 4 colunas, onde cada elemento um 'float', cabe completamente em dois
blocos de cache (que esto na mesma linha!). Se voc obter o endereo de uma varivel do tipo
__m128 vai sempre ter os 4 bits menos significativos do endereo linar zerados:

__m128 x;


printf("0x%016lX\n", (unsigned long)&x);

O fragmento de cdigo, acima, imprimir algo como 0x0000000000601050. No importa onde o


tipo __m128 esteja, esse '0', nos bits menos significativos do endereo, sempre estaro zerados.
O problema com o tipo __m128 que operaes simples no devem ser feitas diretamente. No GCC
temos externses que possibilitam usar operaes bsicas com o tipo _v4si, cujo apelido __m128.
Mas isso no vlido para outros compiladores. Voc pode fazer algo assim, por exemplo:
__m128 x, y, z;

/* carrga y e z de alguma forma... */

/* Funciona no GCC, mas o jeito errado! */


x = y + z;

Pode at mesmo adicionar ou subtrair (ou multiplicar, ou dividir) constantes:


__m128 y, x = { 1, 2, 3, 4 };


y = x + 1; /* Faz: y = x + {1,1,1,1} */

Da maneira tradicional, para realizar operaes, precisamos fazer uso de funes intrinsecas. Uma
funo intrinseca, no caso do SSE, aquela que traduzida diretamente para uma instruo em
assembly. A operao de adio, acima, ficaria assim:
x = _mm_add_ps(y, z); /* Mesma coisa que x = y + z; */

Se voc supor que o compilador escolha 'x' sendo xmm0, 'y' sendo xmm1 e 'z' sendo xmm2, a linha
acima ser traduzida diretamente para:
movaps xmm0, xmm1
addps xmm0, xmm2

O motivo para essa complicao est justamente na falta de infomaes que o compilador tem com
relao a como os registradores XMM sero divididos, ao usar o tipo. Essa informao codificada
no prprio nome da funo intrnseca (ou na instruo em assembly). No caso acima, 'ps' significa

150
packed singles, o que diz ao processador que dividiremos o registrador XMM em quatro floats.
Se usssemos o sufixo 'pd' estaramos usando o tipo dividido em dois doubles (de packed
doubles). O sufixo comeado com 's' significa scalar, que quer dizer que apenas o primeiro
componente do registrador ser usado. 'ss' scalar single, ou seja, um simples float.
Para usar funes instrnsecas de SSE no GCC basta fazer duas coisas: Incluir o header x86intrin.h
nos seus cdigos e dizer ao compilador qual a verso do SSE que deseja usar atravs da opo -
msse. SSE usado por padro na arquitetura x86-64, mas voc pode escolher usar SSE2, SSE4.1
ou SSE4.2, se seu processador suportar. Para usar SSE4.2, por exemplo, basta usar a opo -
msse4.259.

Exemplo do produto escalar


Eis um exemplo simples: Um produto escalar a multiplicao, cordenada por coordenada, de um
vetor. Numa aplicao matemtica ou num graphics engine tridimensional poderamos ter vetores
de coordenadas (x, y, z). O produto vetorial ento definido como:
a
b=( a xb x , a yb y , azb z)
Isso pode ser traduzido numa funo de maneira bem simples:
struct vector_s {
float x, y, z;
};

float dot(struct vector_s *a, struct vector_s *b)


{
return (a->x * b->x) + (a->y * b->y) + (a->z * b->z);
}

Compilando e verificando o cdigo assembly gerao, com a mxima otimizao, temos:


dot:
movss xmm0, dword ptr [rdi]
movss xmm1, dword ptr [rdi+4]
mulss xmm0, dword ptr [rsi]
mulss xmm1, dword ptr [rsi+4]
addss xmm0, xmm1
movss xmm1, dword ptr [rdi+8]
mulss xmm1, dword ptr [rsi+8]
addss xmm0, xmm1
ret

A primeira implementao, usando SSE, que poderamos fazer para acelerar um pouquinho as
coisas esta:
#include <x86intrin.h>

union xmm_u {
struct {
float x, y, z; /* Estou ignorando o 4 elemento aqui! */
} s;

__m128 x;
};

float dot_sse(__m128 a, __m128 b)


{
union xmm_u u;

u.x = _mm_mul_ps(a, b);


return u.s.x + u.s.y + u.s.z;
}

59 Uma excelente referncia para as funes intrnsecas pode ser encontrada online em
https://software.intel.com/sites/landingpage/IntrinsicsGuide/

151
O que melhora um pouco as coisas:
dot_sse:
mulps xmm0, xmm1
movaps xmmword ptr [rsp-24], xmm0
movss xmm0, dword ptr [rsp-24]
addss xmm0, dword ptr [rsp-20]
addss xmm0, dword ptr [rsp-16]
ret

Existem alguns ajustes finos que podemos fazer para melhorar isso, mas o interessante mesmo
saber que existe uma instruo SSE que faz justamente um produto vetorial, se seu processador
tiver suporte ao SSE 4.1:
float dot_sse41(__m128 a, __m128 b)
{
return _mm_cvtss_f32(_mm_dp_ps(a, b, 0x71));
}

A funo intrinseca _mm_dp_ps multiplica cada parte de registradores SSE relacionados com a
mscara dos 4 bits superiores do terceiro parmetro e os soma. Depois, armazena o resultado nas
posies dos 4 bits inferiores da mscara. Por isso o valor 0x71 (7 a mscara para os 3 floats
inferiores de 'a' e 'b'; e 1 a mscara para colcar o resultado na posio 0 do registrador resultante).

A funo instrinseca _mm_cvtss_f32 pega o float na posio 0 do tipo __m128 e retorna um 'float'.
Isso eliminado no cdigo final, como pode ser visto abaixo, mas necessrio se quisermos lidar
com o resultado como um tipo 'float' simples, em C:
dot_sse41:
dpps xmm0, xmm1, 113
ret

Nada mal, huh? De 8 instrues camos para 5 e, finalmente, melhoramos para uma s. Isso poupa
um bocado de espao, mas essa otimizao final , essencialmente, o mesmo que a funo anterior
talvez poupando um nico ciclo de mquina... As duas ltimas funes so bem melhores que a
primeira. Enquanto a primeira toma entre 25 e 30 ciclos, as duas ltimas tomam cerca de 14, de
acordo com a documentao da Intel. Ou seja, as duas ltimas funes tiveram um aumento de
performance de cerca de 100% em relao primeira.

Uma otimizao que falhou o produto vetorial


Num esquema normal de processamento, para calcular o produto vetorial de dois vetores
tridimensionais (x,y,z) teramos uma rotina deste tipo:
struct vector_s {
float x, y, z;
};

void cross(struct vector_s *vout, const struct vector_s *v1, const struct vector_s *v2)
{
vout->x = (v1->y * v2->z) - (v1->z * v2->y);
vout->y = (v1->z * v2->x) - (v1->x * v2->z);
vout->z = (v1->x * v2->y) - (v1->y * v2->x);
}

O cdigo gerado, mesmo com otimizao, mais ou menos este:

152
cross:
movss xmm3, dword ptr [rdx+8]
movss xmm1, dword ptr [rsi+8]
movss xmm0, dword ptr [rsi+4]
movss xmm2, dword ptr [rdx+4]
mulss xmm0, xmm3
mulss xmm2, xmm1
subss xmm0, xmm2
movss dword ptr [rdi],xmm0
movss xmm2, dword ptr [rdx]
movss xmm0, dword ptr [rsi]
mulss xmm1, xmm2
mulss xmm3, xmm0
subss xmm1, xmm3
movss DWORD PTR [rdi+4],xmm1
mulss xmm0, dword ptr [rdx+4]
mulss xmm2, dword ptr [rsi+4]
subss xmm0, xmm2
movss dword ptr [rdi+8],xmm0
ret

E aqui usaremos a mgica do SSE. Repare que cada componente multiplado com outro diferente.
Temos:
( y 1, z 1, x 1)(z 2, x 2, y 2 ) (z 1, x 1, y1 )( y 2, z 2, x 2)
Ento, tudo o que temos que fazer embaralhar as coordenadas, multiplic-las e depois subtra-
las. A rotina fica, assim:
#include <x86intrin.h>

__m128 cross_sse(__m128 v1, __m128 v2)


{
return _mm_sub_ps(
_mm_mul_ps(
_mm_shuffle_ps(v1, v1, _MM_SHUFFLE(3,1,2,0)),
_mm_shuffle_ps(v2, v2, _MM_SHUFFLE(3,2,0,1))
),
_mm_mul_ps(
_mm_shuffle_ps(v1, v1, _MM_SHUFFLE(3,2,0,1)),
_mm_shuffle_ps(v2, v2, _MM_SHUFFLE(3,1,2,0))
)
);
}

Compilcado, no? Mas ela parece ser mais rpida que a funo cross:
cross_sse:
movaps xmm3, xmm1
movaps xmm2, xmm0
shufps xmm3, xmm1, 216
shufps xmm2, xmm0, 225
shufps xmm1, xmm1, 225
shufps xmm0, xmm0, 216
mulps xmm2, xmm3
mulps xmm0, xmm1
subps xmm0, xmm2
ret

Podemos, ainda, melhorar a rotina um pouco mais, eliminando um shuffle:

153
__m128 cross2_sse(__m128 v1, __m128 v2)
{
__m128 r;

r = _mm_sub_ps(
_mm_mul_ps(v1,
_mm_shuffle_ps(v2, v2, _MM_SHUFFLE(3,0,2,1))),
_mm_mul_ps(v2,
_mm_shuffle_ps(v1, v1, _MM_SHUFFLE(3,0,2,1)))
);

return _mm_shuffle_ps(r, r, _MM_SHUFFLE(3,0,2,1));


}

E o resultado parece ainda mais promissor:


cross2_sse:
movaps xmm2,xmm0
shufps xmm2,xmm0,201
mulps xmm2,xmm1
shufps xmm1,xmm1,201
mulps xmm1,xmm0
subps xmm1,xmm2
shufps xmm1,xmm1,201
movaps xmm0,xmm1
ret

Eis a pergunta de 1 milho de dlares: Ser que a funo cross2_sse mais rpida que cross_sse? E
quanto a primeira rotina?
Por incrvel que parea as trs rotinas so muito parecidas. A primeira gasta uns 127 ciclos e a
ltima uns 121. No l um ganho que valha pena (uns 2.5%!), dada a complexidade aparente das
funes que usam SSE! Eu manteria a ltima somente pelo fato dela ser menor.

Uma otimizao bem sucedida: Multiplicao de matrizes


Dadas duas matrizes de 4 linhas e 4 colunas (column major, ao estilo OpenGL), a multiplicao
delas produzir uma terceira matriz. Se informarmos essas matrizes em forma de vetores de 16
floats, a rotina clssica para a multiplicao a descrita na funo Matrix4x4Multiply, e a verso
SSE, otimizada est listada em Matrix4x4MultiplySSE:
/* Ao invs de usar arrays bidimensionais, uso arrays
de 16 floats... */
void Matrix4x4Multiply(float *out, const float *a, const float *b)
{
int row, col, k;

for (row = 0; row < 4; row++)


for (col = 0; col < 4; col++)
{
out[4*row+col] = 0.0f;

for (k = 0; k < 4; k++)


out[4*row+col] += a[4*k+col] + b[4*row+k]
}
}

154
/* Usando um pouco de criatividade para lidar com os registradores
de 128 bits do SSE, essa funo faz a mesma coisa que a anterior! */
void Matrix4x4MultiplySSE(float *out, const float *a, const float *b)
{
int i, j;
__m128 a_column, b_column, r_column;

for (i = 0; i < 16; i += 4)


{
b_column = _mm_set1_ps(b[i]);
a_column = _mm_loadu_ps(a);
r_column = _mm_mul_ps(a_column, b_column);

for (j = 1, j < 4; j++)


{
b_column = _mm_set1_ps(b[i+j]);
a_column = _mm_loadu_ps(&a[j*4]);
r_column = _mm_add_ps(
_mm_mul_ps(a_column, b_column),
r_column
);
}

_mm_storeu_ps(&out[i], r_column);
}
}

A diferena de performance das duas funes chega a ser de quase 1000 ciclos de clock, onde a
segunda mais rpida. Em minhas medies a primeira gasta cerca de 2800 ciclos, enquanto a
segunda, 1800. Ou seja, um aumento de performance de 55%!

E quando ao AVX?
AVX (Advanced Vector eXtension) , em essncia, a mesma coisa que SSE, com registradores
maiores... No AVX os registradores XMM so estendidos de 128 bits de tamanho para 256 bits e
so renomeados para YMM.
Enquanto escrevo a Intel pretende lanar nova arquitetura de processadores que possuem suporte ao
AVX-512... A mesma coisa que SSE e AVX, mas com registradores de 512 bits, chamados agora de
ZMM.
Para AVX e AVX2 existem os tipos intrnsecos __m256, __m256i e __m256d. Eles funcionam da
mesma maneira que os tipos __m128 e seus derivados, no caso do SSE, s que suportam o dobro de
componentes e, claro, tm o dobro de tamanho. No AVX-512 temos o tipo __m512 e os mesmos
derivados.

Outras extenses teis: BMI e FMA


Tanto a Intel quanto a AMD esto, de tempos em tempos, adicionando novas instrues em seus
processadores e fazendo disso um padro da indstria. A extenso BMI lida com manipulao de
bits (Bit Manipulation Instructions) e a FMA faz um mistureba de operaes em ponto flutuante
(Fused Multiply and Add Instructions).
A primeira extenso, BMI, for criada para aglutinar, em uma nica instruo, coisas que podem ser
feitas com conjuntos de instrues como AND, OR e SHL (ou SHR). Por exemplo, se quisermos
zerar todos os bits a partir da posio 31 de RDI, sem alterar seu valor e colocando o resultado em
RAX podemos fazer:
mov rax,rdi
and rax,0x3fffffff ; usando AND com uma mscara

A mscara confusa, j que temos que contar as posies dos bits a partir de zero. A instruo

155
BZHI torna isso mais fcil, mas no mais performtico:
mov rdx,30
bzhi rax,rdx,rdx ; RDI ter seus bits a partir de RDX zerados e tudo copiado para RAX.

Fizemos, essencialmente, a mesma coisa que o AND faz, mas BZHI mais lenta, j que ela usa o
um prefixo.
Isso no quer dizer que BMI seja intil. Considere essa outra necesssidade: Suponha que voc
queira contar quantos bits estejam setados num registrador. Com instrues tradicionais poderamos
fazer algo assim:
; Prottipo: unsigned int popcnt(unsigned long x);
popcnt:
xor eax,eax
mov edx,64
.loop:
lea ecx,[rax+1]
test dli,1
cmovne eax,ecx
shr rdi,1
dec rdx
jnz .loop
ret

A extenso BMI vem a calhar, neste caso, porque podemos fazer isso com apenas uma instruo e
sem alterarmos o contedo de RCX,RDX e RDI:
popcnt rax,rdi
ret

BMI tambm possui instrues interessantes para extrao de bits e manipulaes lgicas
fundidas (como ANDN, onde feito um AND seguido de um NOT)... Para habilitar o uso de BMI
no GCC, se seu processador suportar, use as opes -mbmi ou -mbmi2.
A extenso FMA adiciona extenses ao SSE e ao AVX para lidar com equaes lineares, do tipo:
y=ax+ b
Ao invs de usarmos duas instrues, uma para multiplicar e outra para somar, as duas esto
fundidas (fused) numa mesma instruo. Se voc habilitar FMA no gcc, via opes -mfma ou
-mfma4, sempre que compilador topar com uma equao linear em ponto flutuante, ele tentar usar
a extenso, gerando cdigos menores e, possivelmente, mais rpidos. Se tivermos uma funo do
tipo:
float ma(float a, float x, float b) { return a*x + b; }

A diferena das duas verses (com e sem FMA) seria assim:


; verso 1
ma:
mulss xmm0,xmm1
addss xmm0,xmm2
ret
-----%<----- corte aqui -----%<-----
; verso 2 (fma)
ma:
vfmaddss xmm0, xmm0, xmm1, xmm2
ret

Suspeito que a diferena real seja apenas uma presso menor nos caches, j que a segunda verso
gera cdigo menor:

156
$ objdump -d -M intel-mnmonic test1.o
test1.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <ma>:
0: f3 0f 59 c1 mulss xmm0,xmm1
4: f3 0f 58 c2 addss xmm0,xmm2
8: c3 ret

$ objdump -d -M intel-mnmonic test2.o


test1.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <ma>:
0: c4 e3 f9 6a c2 10 vfmaddss xmm0,xmm0,xmm1,xmm2
6: c3 ret

Claro que para funes mais complicadas, tendo uma funo especializada para calcular equaes
lineares, evita muitas movimentaes de registradores XMM para temporrios. Potencialmente
aumentando a performance do cdigo final.

157
158
Captulo 14: Dicas e macetes
Este captulo apenas uma coletnea de dicas e macetes que ofereo a voc, a respeito de aspectos
que podem ser importantes para que certos erros no sejam cometidos, ou at mesmo, coisas
interessantes, das quais voc pode no ter pensado...

Valores booleanos: Algumas dicas interessantes em C


Numa comparao, na linguagem C, um valor zero sempre considerado falso. J um valor
diferente de zero sempre verdadeiro. E, ao mesmo tempo, o tipo int o tipo padro para a
avaliao de expresses booleanas, no entanto, a regra do zero e do no-zero vale para qualquer
tipo, incluindo ponteiros. O smbolo NULL, por exemplo, definido como sendo um ponteiro void
que aponta para 0.
Acontece que, ao avaliar uma expresso booleana, o compilador atribui apenas um de dois valores:
0 para falso e 1 para verdadeiro. Isso parte da especificao da linguagem e um recurso
garantido. Pode ser demonstrado com a chamada abaixo:
printf("true = %d\nfalse = %d", (10 == 10), (0 == 1));

Isso imprimir os valores 1 e 0, nesta ordem.


O compilador pode ignorar a avaliao e se ater apenas primeira regra e comparar apenas contra o
valor zero, se isso criar cdigo mais performtico, mas se voc quiser usar o resultado de uma
expresso booleana os valores 1 e 0 so, como j disse, garantidos. Por exemplo, as instrues
abaixo so equivalentes:
if (x < 2) y++; /* adiciona 1 a y se x < 2. */

y += (x < 2); /* adiciona 1 a y se x < 2! */

Outra dica legal o uso do operador booleano ! duas vezes. O operador ! serve para inverter o valor
boleano que o segue, se a expresso direita da exclamao for 0 o resultado ser 1 e vice-versa.
Ao usar !! manteremos o valor booleano original (mas, se tivermos originalmente um valor
diferente de zero, mas que no seja 1, ento o resultado bvio ser 1)... Assim, para evitar que o
compilador emita avisos em construes como:
/* Equivale a 'if ((f = fopen(...)) != NULL) ' */
if (f = fopen("file.dat", "r"))

Neste caso o compilador avisa que uma atribuio (vlida!) foi colocada no critrio do if... Esse
um erro comum, ento o gcc avisa... mas, se for isso mesmo que voc quer fazer, basta colocar
dois !!, assim:
if (!!(f = fopen("file.dat", "r")))

Se f for NULL o operador !! avaliar a expresso para 0 (falso), seno para 1 (verdadeiro). E
nenhum aviso dado. A mesma coisa pode ser feita com o exemplo anterior (aquele, ai em cima,
onde incrementamos y), mas se quisermos comparar x com true, por exemplo:
if (x) y++; /* Incrementa y se x for 'verdadeiro'. */
y += !!x; /* Faz a mesma coisa! */

159
Quanto mais as coisas mudam...
No fique surpreso em constatar que as duas instrues abaixo so executadas em um nmero
diferente de ciclos de mquina:
add rax,[var]
add [var],rax ; Essa duas vezes mais lenta que a anterior.

A segunda instruo efetua uma leitura do endereo da varivel 'var', faz a adio e grava o
resultado de volta na posio de 'var'. A primeira faz apenas uma leitura. O comportamento do
primeiro caso conhecido como leitura-modificao-escrita. Isso sempre adiciona ciclos de
mquina ao processamento da instruo e um comportamento que deve ser evitado.
Outra coisa interessante que, das sequncias:
cmp qword [var],0
-----%<----- corte aqui -----%<-----
xor rax,rax
cmp [var],rax

A sequncia XOR/CMP mais rpida. O motivo que a instruo de comparao com um valor
imediato do tamanho de um QWORD muito grande. Quanto maior a instruo, mais lenta ela . O
par de instrues XOR/CMP, onde CMP no est usando um valor imediato, ocupa menos espao.

INC e DEC so lerdas!


O que supreende que algumas instrues tm comportamento de leitura-modificao-escrita
mesmo que seu operando no serja uma referncia memria. o caso de INC e DEC. Essas
instrues fazem alteraes parciais no registrador EFLAGS (elas no afetam o flag de Carry) e isso
causa uma leitura de EFLAGS, modificao e escrita, adicionando ciclos. por esse motivo que um
bom compilador C prefere usar as instrues ADD e SUB, que escrevem sobre os flags
diretamente60.
Existe ou outro problema com DEC (e, provavelmente, INC). No modo x86-64 no possvel us-
la com registradores de 16 ou 32 bits. Instrues como:
dec ecx

No existem nesse modo! Isso acontece porque o micro-cdigo usado para a instruo usado para
outra coisa! Voc pode usar DEC com registradores como CL e RCX, mas no as outras variantes.

No faa isso!!!
A Intel recomenda que toda instruo RET seja emparelhada com uma funo CALL
correspondente. O que ela quer dizer com isso que, para uma chamada feita com CALL, dever
existir uma instruo RET que retorne da chamada. Isso acontece naturalmente quando criamos
nossas funes em C, mas em certos casos a coisa pode no parecer to bvia:
int zeromem(void *dest, size_t size) { return memset(dest, 0, size); }

Seu compilador provavelmente criar o seguinte cdigo:


zeromem:
mov rdx,rsi
xor esi,esi
jmp memset

60 Estou falando apenas dos flags ligados a operaes aritmticas. Flags D e I, por exemplo, so mantidos num outro
cirtuito, mesmo que sejam mapeados em EFLAGS.

160
A instruo RET, que est emparelhada com o CALL que chamou essa rotina, est na funo
memset. Isso est ok... Mas, se voc fizer uma coisa esquisita dessas, para chamar uma funo:
mov rax,zeromem
push rax
ret ; Retornar para a funo zeromem!

Voc j no ter um RET emparelhado com o um CALL, mas sim dois RETs (o que est listado
acima e o que est na funo zeromem). Essa tcnica funciona, mas causar vrios ciclos de
mquina de penalidade em grande parte do seu cdigo, j que deixar o branch prediction confuso.
Alm do que, a instruo RET mais lenta que a instruo CALL...
Outra tcnica comum, quando se quer obter o valor de RIP esta:
call here
here:
pop rax ; Pega o RIP armazenado na pilha!

Como sumimos com o RET que era esperado, j que o eliminamos para obter o contedo de RIP
que estava na pilha, o branch prediction sofrer com isso. Se voc realmente precisar fazer algo
desse tipo, eis um cdigo melhor:
call here
; Neste ponto RAX conter o valor de RIP.

here:
mov rax,[rsp]
ret ; RET, emparelhado com o CALL.

Aritmtica inteira de multipla preciso


Uma vez um grande amigo, entusiasmado com a linguagem LISP, me provocou com o seguinte
cdigo, me desafiando a fazer o mesmo em C:
;;; Imprime o valor de 42424242.
(expt 4242 4242)

Esse cdigo simples em C impossvel, j que os tipos integrais tm, no mximo, 64 bits de
tamanho. O resultado da operao acima te mostrar um grande nmero inteiro de 15390
algarismos e, com 64 bits, o valor mximo que podemos obter, sem overflow, algo como 1,81019.
Ou seja, cerca de 20 algarismos.
Acontece que, em assembly, o flag CF e algumas instrues aritmticas, nos permitem lidar com
aritmtica de multipla preciso. Por exemplo: Se tivssemos dois valores de 128 bits e
quisssemos som-los, poderamos fazer algo assim:
mov rax,[valor1]
mov rdx,[valor1+8] ; coloca em RDX:RAX os 128 bits de valor1.

add rax,[valor2]
adc rdx,[valor2+8] ; Note que RDX adicionado com a parte superior de valor2 e
; com o carry, vindo da adio anterior.

Usando o flag CF para obter os overflows das adies parciais, inferiores, podemos realizar adies
com a quantidade de bits que quisermos. O mesmo acontece com subtraes: Existe uma instruo
SBB (Subtract with Borrow), onde o flag CF usado como emprstimo. E, com um pouquinho de
trabalho podemos extrapolar algo semelhante para multiplicaes e divises.
No entanto, esse artifcio s pode ser alcanado em assembly. Em C no h como, diretamente,
acessar os flags. A linguagem esconde isso. Para tanto, existem bibliotecas especializadas para lidar

161
com mltipla preciso... Uma delas a libgmp (GMP acrnimo de GNU Multiple Precision). A
rotina em LISP poderia ser escrita, em C, assim:
/* Calcula exp(4242,4242), como em lisp. */
#include <stdio.h>
#include <gmp.h>

int main(void)
{
mpz_t r, s;

mpz_init(r);
mpz_init(s);

mpz_set_ui(s, 4242); /* s = 4242; */


mpz_pow_ui(r, s, 4242); /* r = pow(s, 4242); */

mpz_clear(s); /* no precisamos mais do 's' */

/* printf() no suporta o tipo mpz_t.


Por isso usamos um printf especializado. */
gmp_printf("%Zd\n",r);

mpz_clear(r); /* no precisamos mais do 'r' */

return 0;
}

Mas, ateno! A biblioteca libgmp provavelmente no usa aritmtica inteira em seu interior. LISP,
provvel, tambm no! Esse tipo de biblioteca costuma usar strings como containers para os valores
que sero manipulados. Dessa forma, as variveis do tipo mpz_t so limitadas apenas pela
quantidade de memria disponvel para a aplicao.
LibGMP capaz de calcular o valor de , pelo mtodo de Chudnovsky, com 1 milho de casas
decimais em apenas 0,38 segundos, 10 milhes em 7 segundos, 100 milhes em 100 segundos e 1
bilho em meia-hora. Mas, no se entusiasme tanto assim com essa biblioteca... Ela consome muita
memria e extremamente mais lenta que a aritmtica inteira (e tambm em ponto flutuante) nativa
do processador. Alm de ser mais complicada de usar... A no ser que voc tenha um bom motivo
como, por exemplo, querer calcular o ngulo de reentrada de uma sonda que voc mandou para
Marte recomendvel que no use aritmtica de multipla preciso.

Voc j no est cansado disso?


Provavelmente voc j viu um cdigo parecido com o abaixo:
FILE *f;
void *ptr;

if ((f = fopen(filename, "rt")) != NULL)


{
if ((ptr = malloc(size)) == NULL)
{
fclose(f);
return 1;
}

if (erro)
{
free(ptr);
fclose(f);
return 2;
}

162

free(ptr);
fclose(f);
}

return 0;

No acha que existem muitas chamadas para fclose? claro que podemos reduzir isso com um goto
bem posicionado:
FILE *f;
void *ptr;
int retcode = 0;

if ((f = fopen(filename, "rt")) != NULL)


{
if ((ptr = malloc(size)) == NULL)
{
recode = 1;
goto endOfFunction;
}

if (erro)
{
free(ptr);
retcode = 2;
goto endOfFunction;
}

free(ptr);
endOfFunction:
fclose(f);
}

return retcode;

Mas se voc um daqueles chatos que consideram goto a pior coisa desde a inveno da ax music,
ento eis uma outra maneira. Podemos colocar um tratador de erros no comeo de nossas funes e
simplesmente saltar para ela, mas sem usar goto:
FILE *f;
void *ptr;
jmp_buf jb;
int retcode;

/* Ponto de salto, em caso de erro. */


if (retcode = setjmp(jb))
{
if (retcode == 2)
free(ptr);
fclose(f);
return retcode;
}

if ((f = fopen(filename, "rt")) != NULL)


{
if ((ptr = malloc(size)) == NULL)
longjmp(jb, 1);

if (erro)
longjmp(jb, 2);

163
free(ptr);
fclose(f);
}

return 0;

Ok, longjmp um goto disfarado, mas o cdigo fica mais elegante, no? como se registrssemos
uma rotina de erros e depois coloquemos o cdigo para funcionar (e no foi isso que fiz?!).

Otimizao de preenchimento de arrays


s vezes algumas rotinas clssicas apresentam problemas interessantes. Um desses problemas :
Como preencher um array da maneira mais rpida possvel?. Para responder isso vamos construir
uma rotina que preenche um array com zeros. No nosso exemplo esse array tem tamanho varivel e
do tipo char:
char array[n];

S que queremos fazer nossa rotina o mais flexvel possvel, portanto o tipo do array deve ser
irrelevante. Isso significa que usaremos ponteiros void. Ainda, 'n' pode ser qualquer tamanho. Pode
ser 1, mas pode ser 32748235 tambm...
A rotina bvia essa:
void zerofill(void *ptr, size_t size)
{
char *pc;
int i;

pc = ptr;
for (i = 0; i < size; i++)
*pc++ = 0;
}
-----%<----- corte aqui -----%<-----
; Cdigo gerado pelo compilador.
zerofill:
lea rax,[rdi+rsi] ; RAX contm agora o ponteiro alm do final do array.
test rsi,rsi ; Se 'size' == 0, sai da funo.
je .L1
.L2:
add rdi,1 ; ptr++;
mov byte ptr [rdi-1],0 ; *(ptr 1) = 0;
cmp rdi,rax ; Chegamos ao final do array?
jne .L2 ; se no chegamos, volta ao loop.
.L1:
ret

Esse cdigo ai em cima gerado com a opo -O2 do compilador. Ao compilarmos com mxima
otimizao temos uma surpresa:
zerofill:
test rsi,rsi
je .L1
mov rdx,rsi
xor esi,esi
jmp memset
.L1:
ret

Descobrimos que memset uma daquelas funes intrinsecas do compilador. Ao perceber o


preenchimento de um array ele simplesmente substituiu todo o nosso cdigo por uma chamada do
tipo:
if (size != 0)
memset(ptr, 0, size);

164
Vamos ignorar essa violao de nossa vontade e nos concentrar na rotina original... O problema com
ela que preenche um nico byte de cada vez. Uma instruo MOV executada em um cclo de
mquina independente do tamanho do operando. Assim, poderamos preencher o array com uma
QWORD por vez e o restante do array (que pode ter nenhum at 7 bytes adicionais) com tipos
menores. A rotina fica mais complicada e maior, mas em muitos casos mais performtica:
void zerofill2(void *ptr, size_t size)
{
size_t qsize;
unsigned long *qptr;
int I;

qsize = size / sizeof(unsigned long);


size = size % sizeof(unsigned long);
qptr = ptr;

while (qsize--)
*qptr++ = 0;
ptr = qptr;

if (size >= sizeof(unsigned int))


{
*(unsigned int *)ptr = 0;
ptr += sizeof(unsigned int);
size -= sizeof(unsigned int);
}

if (size >= sizeof(unsigned short))


{
*(unsigned short *)ptr = 0;
ptr += sizeof(unsigned short);
size -= sizeof(unsigned short);
}

if (size > 0)
*(unsigned char *)ptr = 0;
}

O cdigo acima muito bom para arrays com tamanhos maiores que 8. Mas, eis um cdigo melhor:
; zfill.asm
bits 64
section .text

;
; RDI = ptr, RSI = size
;
global zerofill3:function
align 16
zerofill3:
xor eax,eax
mov rcx,rsi
rep stosb ; preenche byte por byte?!?!
ret

E esse um exemplo de uma coisa que era boa, passou a ser ruim por muito tempo e voltou a ser
boa, de novo! A instruo STOSB foi feita para armazenar o valor do registrador AL no endereo
apontado por RDI. Ao acrescentar o prefixo REP a instruo armazena AL no bloco de memria
cujo endereo base dado por RDI e tem RCX bytes de tamanho. Com uma pegadinha: Nas
arquiteturas modernas (Nehalem e superiores) o processador procurar usar o macete de
armazenar o maior tamanho possvel primeiro.
A mesma coisa funciona para REP MOVSB. As funes REP CMPSB e REP SCASB, parecem,
continuam sendo excees a essa regra... elas ainda so ruins, em termos de performance. Mas, no
confie em mim, bom medir:

165
Funo Ciclos Ganho
zerofill 231523 -
zerofill2 20910 1007%
zerofill3 5219 4336%
Tabela 10: Performances das verses de zerofill.

S modificando o jeito de preencher um array (de bloco em bloco), melhoramos a performance em


mais de 11 vezes. Usando um recurso do processador, melhoramos em 44!
Aviso: Uma maneira de verificar se seu processador suporta LODSB/STOSB/MOVSB rpidos
com essa pequena rotina:
; Prottipo:
; int test_fast_block_operations(void);
; Funo equivalente, em C:
; int test_fast_block_operations(void)
; {
; int a,b,c,d;
; _cpuid(7, a, b, c, d);
; return (b & 0x200) != 0;
; }
test_fast_block_operations:
push rbx
mov eax,7
cpuid
xor eax,eax
test ebx,0b10_0000_0000 ; bit 9 de EBX indica essa feature.
setz al
pop rbx
ret

Tentando otimizar o RFC 1071 check sum. E falhando...


Em um projeto envolvendo sockets, precisei usar a rotina de clculo de check sum estabelecida pela
RFC 1071. O que menos complicado do que calcular check sums?, voc deve estar pensando.
Bem, eis a funo, como implementada pela RFC:
unsigned short cksum(void *addr, size_t count)
{
unsigned int sum;
unsigned short *p;

/* Condio em que a rotina funciona bem. */


assert(count <= 131072);

sum = 0;
p = addr;
while ( count > 1 )
{
sum += *p++;
count -= sizeof(unsigned short);
}

if ( count > 0 )
sum += *(unsigned char *)p;

while (sum>>16)
sum = (sum & 0xffff) + (sum >> 16);

return ~sum;
}

A rotina usa uma varivel de 32 bits para, no primeiro loop, acumular poroes de 16 bits de
tamanho, obtidas do buffer. Se sobrar um byte que ainda no foi acumulado, ento a rotina o faz. E,
como passo final, os transportes (a parte dos 16 bits superiores do acumulador) so acrescentados a

166
ele para que obtenhamos apenas os 16 bits inferiores.
Tudo funciona muito bem se obtivermos at 65535 transportes (todos os 16 bits superiores do
acumulador setados). Qualquer nmero de transportes adicionais e a rotina devolver o check sum
errado. Dessa forma, a rotina aceita lidar com buffers de at 128 KiB de tamanho (65535
transportes vezes 2 bytes). A linha com a assertiva est l para, durante o debugging, verificarmos
se essa condio foi violada.
Uma maneira de acelerar as coisas e resolver a limitao do tamanho do buffer, em teoria, usar os
recursos que temos em mos. No caso, registradores de 64 bits. A rotina abaixo s tem um nico
loop que l unsigned long's de cada vez, levando em conta os transportes parciais. Ela usa um
artifcio perigoso... Depois do loop, se houverem bytes adicionais, ela l um ltimo unsigned long
do buffer, mas zera os bits que no esto presentes. perigoso porque estamos lendo alm do
tamanho total do buffer, mas ignorando o que no deveramos ter lido.
No final da rotina acumulamos os 32 e 16 bits parciais do acumulador, levando o transporte em
considerao:
unsigned short cksum2(void *data, size_t count)
{
unsigned long sum, tmp, *p;
unsigned int u1, u2;
unsigned short s1, s2;
size_t ulcount;
int shift;

sum = 0;
p = data;
ulcount = count >> 3;
count -= ulcount << 3;

while (ulcount--)
{
sum += *p;
if (sum < *p) sum++;
p++;
}

if (count)
{
shift = (8 - count) * 8;
tmp = *p & (0xffffffffffffffffUL >> shift); /* perigoso, mas funciona! */
sum += tmp;
if (sum < tmp) sum++;
}

u1 = sum;
u2 = sum >> 32;
u1 += u2;
if (u1 < u2) u1++;

s1 = u1;
s2 = u1 >> 16;
s1 += s2;
if (s1 < s2) s1++;

return ~s1;
}

Era de se esperar que cksum2 fosse, pelo menos, duas vezes mais rpida que a rotina original, j que
estamos lendo duas mais rpido, no loop principal. Mas no isso o que acontece. Eis a medio
dos ciclos gastos, usando um buffer de 32775 bytes61:

61 Como condio de testes o buffer deve ter 7 bytes a mais do que o mltiplo de 8. Assim, teremos que ler (n+1)
unsigned longs. Onde o ltimo contm os 7 bytes restantes...

167
Funo Ciclos
cksum 6696
cksum2 8648 (perda de 29%!)
Tabela 11: Consumo de ciclos de cksum e cksum2.

Essa uma daquelas funes onde criar um cdigo assembly no adianta muita coisa... Tentei criar
uma rotina usando alguns macetes (como acumular 32 bytes de cada vez, na iterao do loop
inicial) e o que consegui foi uma performance ligeiramente superior cksum2 (somente cerca de
1% de ganho!).
Na arquitetura x86-64, o bom e velho cksum da RFC 1071 ainda muito bom!

Previso de saltos e um array de valores aleatrios


Esse problema interessante me foi indicado por um amigo: A rotina abaixo preenche um array de
32768 ints com valores aleatrios entre 0 e 255. Depois todos os valores do array so somados. A
soma dos itens do array feita 100 mil vezes para facilitar a medio do tempo. O cdigo original
usa a funo clock para medir a performance, em segundos:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX_ITERATIONS 100000


#define MAX_BUFFER_SIZE 32768

int buffer[MAX_BUFFER_SIZE];

/* Preenche array com valores aleatrios entre 0 e 255. */


void random_fill_buffer(void)
{
int i;

/* No alimento o 'seed' aqui para obter a mesma sequncia aleatria toda vez! */
for (i = 0; i < MAX_BUFFER_SIZE; i++)
buffer[i] = rand() % 256;
}

/* Usada por qsort. */


int compare(const void *x, const void *y) { return *(int *)x < *(int *)y; }

int main(void)
{
int i, j;
long sum;
clock_t clkstart;
double clkelapsed;

random_fill_buffer();

// Retire isso daqui para testar com o array fora de ordem.


qsort(buffer, MAX_BUFFER_SIZE, sizeof(int), compare);

clkstart = clock();
for (i = 0; i < MAX_ITERATIONS; i++)
for (j = 0; j < MAX_BUFFER_SIZE; j++)
if (buffer[j] >= 128)
sum += buffer[j];

clkelapsed = (double)(clock() - clkstart) / CLOCKS_PER_SEC;

printf("sum = %ld.\n"
"Tempo decorrido: %.3f segundos.\n", sum, clkelapsed);

return 0;
}

168
Eis a tabela com o tempo de execuo medido com a ordenao do array e sem ordenao do array,
bem como com nveis diferentes de otimizao:
-O0 -O1 -O2 -O3
Array ordenado 6.49 segundos 1.76 segundos 1.74 segundos 2.62 segundos
Array no ordenado 17.34 segundos 10.61 segundos 10.26 segundos 2.62 segundos
Tabela 12: Tempos de execuo na varredura em SortedArrayTest.

Os nveis de otimizao 1 e 2 obtiveram um tempo muito bom com o array ordenado, mas a rotina
uma porcaria quando os itens so aleatrios. O nvel 3 manteve a varredura do array em tempo
constante e, claro, o sem otimizaes tivemos os piores resultados.
O que acontece nos nveis 2 e 3, por exemplo?
O problema da rotina o 'if' dentro do loop que acumula o contedo dos itens do array na varivel
'sum':
if (data[i] >= 128)
sum += data[i];

Um 'if' sempre executa um salto condicional. No exemplo, o conjunto de instrues que obtm o
valor de 'data[i]' e somam com o contedo de 'sum', colocando de volta em 'sum', omitido se
'data[i]' for menor que 128. A verso do cdigo deste 'if', gerado pela otimizao de nvel 2, mais
ou menos assim:
; Considere: RDX = &data[i]
; EBX = sum

movsx rcx,dword [rdx]
cmp rcx,127
jle 1f
add ebx,ecx
1:

Sempre que 'data[i]' for menor ou igual a 127 um salto feito, evitando a acumulao.
Acontece que desde os 486s os processadores da arquitetura 80x86 decidificam as instrues bem
antes delas serem executadas. No caso de saltos condicionais o processador tem que adivinhar se
o salto ser tomado ou no antes de saber o estado dos flags. Isso feito por uma rotina interna
do processador chamada branch prediction. O processador mantm uma estatstica sobre os saltos
adivinhados que aconteceram ou no, na tentativa de melhorar a previso.
Na primeira vez que o processo passa pelo 'if' e se 'data[i]' for, de fato, maior ou igual a 128, o salto
no ser feito. O processador assume que da prxima vez que encontrar esse salto condicional ele
tambm no saltar. Mas, se isso no acontecer, houve um erro na adivinhao. O processador ter
que voltar atrs e decodificar as instrues que deixou de faz-lo, j que o salto aconteceu de fato.
Alm de atualizar as estatsticas do branch prediction! Isso toma ciclos adicionais...
Numa sequncia ordenada, 99% dos saltos sero previstos corretamente. Numa sequncia fora de
ordem a previso pode estar errada em 99% dos casos!
Mas, o que o nvel 3 de otimizao faz que consegue obter tempo constante em ambos os casos? Ele
se livra do salto condicional!

169
; Considere: RDX = &data[i]
; EBX = sum
; R8 = tmp_sum;

mov ecx,[rdx]
movsx r8,ecx
add r8,rbx
cmp ecx,128
cmovge rbx,r8

O compilador fez um trabalho interessante aqui. R8 s ser movido para RBX se, e somente se,
ECX for maior ou igual a 128. A instruo CMOVcc gasta sempre 1 ciclo de mquina, mesmo
quando a condio no satisfeita. E note que o salto condicional sumiu!
A dica aqui : Elimine os saltos condicionais tanto quanto possvel. E, ainda, verifique se seu
compilador fez isso!

Usar raiz quadrada, via SSE, parece estranho...


Especialmente no modo x86-64, o compilador C usar SSE para lidar com aritmtica em ponto
flutuante. Isso j foi discutido anteriormnte. Voc topar com um cdigo bem estranho no caso do
uso de raiz quadrada:
/* Simples exemplo de cdigo em C */
#include <math.h>

float get_sqrt(float x)
{
return sqrtf(x);
}
----- cortar aqui -----
; Cdigo equivalente, em assembly:
get_sqrt:
sqrtss xmm1,xmm0
ucomiss xmm1,xmm1
jp .L1
movaps xmm0,xmm1
ret
.L1:
push rax
call sqrtf
pop rdx
ret

A primeira estranheza por que o compilador incluiu essa comparao e uma chamada funo
sqrtf da libc? O problema est em alguns valores usados nos operadores da instruo SQRTSS.
As regras para lidarmos com resultados NaNs e infinitos no processador so diferentes das regras
tradicionais da funo sqrtf e, por isso, o compilador coloca esse cdigo adicional 62. Ele testa pelo
resultado de SQRTSS e, se o flag de paridade PF estiver setado, significa que houve um problema.
Da o cdigo ir usar a funo sqrtf para obter a raiz quadrada.
O que me causa outra estranheza a necessidade do compilador salvar rax e recuper-lo em rdx, j
que amos os registradores no precisam ser preservados na conveno de chamada x86-64. Em
revises fututas deste livro, se eu achar uma explicao para esse fato, comentarei sobre o assunto.
Por enquanto, essa necessidade, para mim, ainda um mistrio.
Poderamos usar as funes intrnsecas do compilador para SSE e obter um cdigo menor:

62 O processador usa tiops diferentes de NaNs: SNaNs e QNaNs. Onde o primeiro mais drstico que o segundo.

170
#include <x86intrin.h>

float get_sqrt(float x)
{
return _mm_cvtss_f32(_mm_sqrt_ss(_mm_set_ss(x)));
}
-----%<----- corte aqui -----%<-----
; Cdigo equivalente em assembly
get_sqrt:
movss [rsp-12],xmm0 ; WTF?!
movss xmm0,[rsp-12] ;

sqrtss xmm0,xmm0
ret

Embora a rotina final fique menor e potencialmente mais rpida (mas nem tanto!), essa necessidade
de garantir que os floats superiores estejam zerados me parece preciosismo do compilador. E, em C,
no h muito o que fazer para evitar isso.

Gerando nmeros aleatrios


s vezes precisamos obter valores aleatrios em nossas aplicaes. A maneira mais simples de faz-
lo usando uma funo da libc: rand(). Ela declarada em stdlib.h como:
int rand(void);

E a documentao nos diz que a funo retorna um valor entre 0 e RAND_MAX, que tpicamente
definida como INT_MAX, ou seja, 0x7fffffff. Mas essa funo tem dois problemas.
O primeiro problema a maneira como o valor aleatrio gerado... A maioria das funes de
gerao de valores aleatrios que existem por ai usam o que se chama de Gerador de Congruncia
Linear. um nome complicado para uma frmula:

Onde os valores de 'a', 'b' e 'm' so fixos, pr-calculados. O valor aleatrio Xn+1 depende
exclusivamente do valor inicial Xn. Esse valor inicial, X0, conhecido como semente aleatria
(random seed). Eis a implementao, simplificada, de rand(), no cdigo da glibc que tenho em
mos:
return (seed = (seed * 1103515245 + 12345) % 0x7fffffff);

O valor de 'seed' inicial tambm escolhido dedo para parecer que rand() seja aleatrio, se uma
semente no for fornecida, mas sempre o mesmo. A funo srand(), que fornece a semente para
o gerador de congruncia linear somente atualiza essa varivel esttica.
Assim, a funo rand() pseudo-aleatria. Felizmente a arquitetura Haswell implementa um
gerador aleatrio de verdade, usando um fenmeno quntico como fonte de entropia... Isso tudo
dentro do seu processador! Atravs de uma nica instruo podemos obter o nmero aleatrio que
quisermos que garantido ser aleatrio. Trata-se da funo RDRAND:
; random.asm
bits 64
section .text

; Prottipo:
; unsigned long getrandom(void);
global getrandom:function
getrandom:
rdrand rax
jnc getrandom
ret

A instruo RDRAND, quando falha, retorna com o flag Carry zerado. Tudo o que temos que fazer

171
executar RDRAND novamente. A desvantagem que RDRAND pode ser lenta... A documentao
da Intel nos diz que ela pode falhar se leituras forem feitas em intervalos menores que 10
milissegundos, o que nos colocaria num loop por cerca de uns 5 milhes de ciclos de mquina, mas
a coisa no to crtica assim... E, mesmo que seja, perder um tempinho (10 ms, no mximo) para
garantir a aleatoriedade dos valores um preo pequeno a pagar.
O outro problema do gerador de congruncia de linear que no nem um pouco bvio (problema,
alis, que RDRAND no tem!) que os bits inferiores do valor obtido tendem a ser menos
aleatrios que o restante dos bits63... Suponha que estamos querendo obter um valor entre 1 e 6 para
simular o lanamento de um dado de seis faces. O programador pode escolher fazer algo assim:
value = (rand() % 6)+1;

Ao obter o resto da diviso por 6 estamos aproveitando apenas os 3 bits inferiores do resultado de
rand(). Podemos obter mais faces voltadas para o 3, no dado, do que as demais faces! Uma possvel
soluo, usando rand(), obtermos os 4 bits superiores do resultado (j que o bit 31 sempre estar
zerado!):
value = ((rand() >> 28) % 6) + 1;

A soluo bvia, se seu processador possuir a instruo RDRAND, usar nossa funo
getrandom() ao invs de rand(). Mas, fique atendo que ao obter o resto da diviso por 6, o valor
aleatrio obtido ainda um pouco sofrvel... O ideal escolher um divisor mltiplo de 2n. Isso
equivale a isolar bits inferiores do valor atravs do uso de uma mscara com uma simples instruo
AND:
value = getrandom() & 0x7f; /* Obtem um valor aleatrio entre 0 e 127 */

A instruo RDRAND vem em 3 sabores: 16, 32 ou 64 bits, dependendo do registrador usado como
operando. Poderamos escrever o seguinte macro em assembly inline64:
#define RANDOM(x) \
__asm__ __volatile__ ( \
"1:\n" \
"rdrand %0\n" \
"jnc 1b" : =g ((x)) \
)

O compilador escolher a variao de RDRAND apropriada dependendo do tamanho da varivel


passada para o macro...

63 Tenho que agradecer a Nelson Brito por me indicar esse detalhe... Num projeto, elaborado pelo Nelson, ele usava
um macro com valores em ponto flutuante para lidar com esse problema. No momento em que o entendi pude
otimizar algumas rotinas para uso exclusivamente de valores inteiros.
64 Uma explicao sobre o salto condicional no assembly inline: Labels numricos devem ser referenciados com um
sufixo 'b' ou 'f' (de back e forward). Ao usar 'b' o salto feito para o label anterior (para trs).

172
Captulo 15: Misturando Java e C
Misturar Java e C uma coisa muito simples de ser feita. O Java Development Kit (JDK) possui
bibliotecas e headers para que o desenvolvedor possa usar rotinas externas JVM. O padro
chama-se JNI (Java Native Interface).
Eu disse simples? Java tem l suas idiossincrasias... Embora usar a JNI seja simples, preciso
entender como java funciona para us-la corretamente. O uso incorreto acarreta srios problemas
para a aplicao, j que a JVM (Java Virtual Machine) responsvel por gerenciar todo o ambiente
do seu prprio jeito, que incompatvel com a maneira como a libc lida com os seus recursos.
Meu objetivo em adicionar Java na discusso num livro sobre C e Assembly mostrar o quo
esquisita essa linguagem (java!) e te dar um vislumbre de todo o trabalho que a JVM faz por
debaixo dos panos. Isso esclarece porque no sou l muito chegado em linguagens em ambientes
gerenciados como Java e C#.

Porque no uma boa ideia misturar C com ambientes gerenciados


Por ambientes gerenciados quero dizer linguagens como Java e C# e qualquer outra que use o
conceito de Garbage Collection. Nesses ambientes aplicaes so executadas sob o que chamamos
mquinas virtuais. Tm esse nome porque elas emulam uma mquina fictcia, com regras de
gerenciamento de memria e linguagem de mquina prprias. Ao usar cdigos feitos em C nesses
ambientes voc poder usar funes da libc para generenciar memria, como malloc, mas a
alocao de memria dinmica no estar sob controle do Garbage Collector.
Tudo no Java (e no C#), exceto pelos tipos primitivos (byte, char, short, int, long, float e double) so
objetos. E todo objeto usado atravs de um ponteiro disfarado. Ponteiros so chamados de
referncias em Java. Mas um pouco mais complicado do que isso...
Em C++, na definio de uma classe, podemos dizer ao compilador para usar uma tabela de
ponteiros para as funes-membro. Essas funes so marcadas como virtuais. Mas C++ tambm
permite a declarao de funes-membro no-virtuais. Para ilustrar a diferena, os cdigos abaixo
so mais ou menos equivalentes:
/* MyClass.cc */
class MyClass
{
public:
int x;

virtual int getX(void);


};

int MyClass::getX(void) { return this->x; }


-----%<----- corte aqui -----%<-----
/* MyClass.c */
struct MyClass;

struct MyClass_vtbl
{
int (*getX)(struct MyClass *this);
};

static MyClass_vtbl myclass_vtbl;

173
struct MyClass
{
int x;

struct MyClass_vtbl *vtbl;


};

int MyClass_getX(struct MyClass *this) { return this->x; }

void MyClass_ctor(struct MyClass *p)


{
p->vtbl = &myclass_vtbl;
p->vtbl->getX = MyClass_getX;
}

Em C++ toda chamada a uma funo virtual feita atravs de ponteiros contidos na tabela virtual,
mais ou menos como descrito no cdigo em C, acima... Um objeto tem, atrelado a si, um ponteiro
para uma estrutura contendo os ponteiros para as funes virtuais da classe. Ao chamar a funo
esse ponteiro automaticamente usado (em C++). Mais ou menos assim:
/* teste.cc */
MyClass obj;

int y = obj.getX();
-----%<----- corte aqui -----%<-----
/* teste.c */
struct MyClass obj;

MyClass_ctor(&obj);

int y = obj.vtbl->getX(&obj);

fcil perceber que toda chamada a funes virtuais so feitas de forma indireta, pela tabela virtual.
Isso existe para que, durante a herana, o polimorfismo possa ser facilmente implementado. Vou
deixar esse detalhe de fora por aqui...
No java, por default, todo mtodo (outro nome que java d s funes-membro) virtual. E j que
usar objetos sempre feito por referncia, usando ponteiros, toda chamada de mtodo feita por
uma indireo dupla... Temos o ponteiro disfarado sob o nome do objeto e temos o ponteiro
escondido da tabela virtual. Uma comparao superficial de chamadas usando cdigos em Java e
C, seria assim:
// Em java:
obj.f();
-----%<----- corte aqui ----%<-----
/* Em C: */
obj->vtbl->f(); /* obj um ponteiro! */

necessrio entender isso se voc for usar a JNI...

Garbage Collection, no Java


Outra coisa que preciso entender como Java usa a memria. A JVM pr-aloca e divide o heap
em, basicamente, 3 blocos. O primeiro bloco chamado Eden ou Young Generation, que contm
objetos pequenos e de vida curta. O segundo chamada Elder Generation. Ele contm objetos
maiores e que conseguiram sobreviver ao Garbage Collector, vindos da Young Generation. J o
terceiro bloco chamado Permanent Generation.
Objetos contidos nas regies Eden e Elder so movidas o tempo todo pelo coletor de lixo. Aqueles
objetos que no esto mais em uso precisam ser liberados e os que ainda esto sendo usado
precisam ser movidos para evitar a fragmentao dessas regies. Como esses objetos so movidos,
seus ponteiros so alterados com frequncia. para manter um registro da nova posio do objeto
que existe a regio Permanent. Ponteiros para os objetos nas geraes sob a influncia do Garbage

174
Collector so mantidos na regio Permanent e quando o coletor move algum bloco tudo o que ele
tem que fazer alterar o ponteiro da referncia no lado permanente. Assim, o cdigo em Java sabe
onde o objeto est o tempo todo:

Figura 18: Garbage Collector movimentando um


nico objeto.

Juntando o conceito das geraes com as funes virtuais, java mantm o ponteiro para as estruturas
do objeto na regio permanente e esse ponteiro aponta para a tabela virtual numa das regies sob
influncia do Garbage Collector e a tabela virtual contm um ponteiro para uma outra gerao
escondida que contm o bytecode da funo...

Misturando com um exemplo simples...


A primeira coisa a ser feita quando for misturar seus cdigos em C com o ambiente Java criar uma
classe que carregue uma shared library, definida por voc, que contenha as suas funes:
// HelloWorld.java
//
// Essa classe um stub. Ela contm as declaraes das funes nativas.
//
public class HelloWorld {
// Inicializador carrega a shared library libHello.so.
static { System.loadLibrary("Hello); }

// A funo nativa em C: void showMessage(void);


public static native void showMessage();
}

O objeto da classe HelloWorld, atravs do inicializador, vai carregar o shared object libHello.so e
ela tambm declara a funo showMessage, que se localiza nessa biblioteca... Se o cdigo for
executar no Windows, provavelmente a JDK vai procurar por hello.dll nos diretrios indicados pela
varivel de ambiente PATH.
Depois de compilar a classe voc poder usar o utilitrio 'javah' para obter o header com as
declaraes das funes da classe (listada abaixo sob o nome HelloWorld.h):
$ javac HelloWorld.java
$ javah -o HelloWorld.h HelloWorld
$ ls -l
-rw-r--r-- 1 user user 152 Jan 21 16:07 libHello.c
-rw-r--r-- 1 user user 130 Jan 21 16:16 Hello.java
-rw-r--r-- 1 user user 388 Jan 21 16:29 HelloWorld.h
-rw-r--r-- 1 user user 120 Jan 21 16:04 HelloWorld.java
-rw-r--r-- 1 user user 448 Jan 21 16:58 Makefile
-----%<----- corte aqui -----%<-----

175
/* HelloWorld.h (criado por javah) */
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>

/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld

#ifdef __cplusplus
extern "C" {
#endif

/*
* Class: HelloWorld
* Method: showMessage
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloWorld_showMessage
(JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif

#endif

Eis a rotina em C que compor a shared library. Ela deve incluir o header gerado acima:
/* libHello.c */
#include <stdio.h>
#include "HelloWorld.h."

JNIEXPORT void JNICALL Java_HelloWorld_showMessage(JNIEnv *env, jclass obj)


{
puts("Hello, world!");
}
-----%<----- corte aqui -----%<-----
$ gcc -O3 -I /usr/lib/jvm/default-java/include -march=native -fPIC -o libHello.o libHello.c
$ gcc -o libHello.so -shared libHello.o

Importante reparar que o nome da nossa funo obedece o nome contido no header. O java usa um
esquema de nomenclatura especial (o exemplo acima s um caso simples existem nomes mais
confusos).
Aqui surge o primeiro problema. necessrio dizer ao GCC onde que podemos encontrar o
header jni.h, que acompanha o JDK. A concluso bvia que segue desse fato que sua shared
object dependente da verso do JDK usado.
Tudo o que precisamos fazer agora criar uma classe java que vai instanciar nossa classe
HelloWorld, que contm as declaraes nativas e executar o cdigo:
// HelloWorld.java
public class Hello {
public static void main(String[] args) {
HelloWorld hw = new HelloWorld();

hw.showMessage();
}
}
-----%<----- corte aqui -----%<-----
$ javac -classpath . Hello.java
$ LD_LIBRARY_PATH=. java Hello
Hello, world!

Repare que 'javac' precisa saber onde a classe HelloWorld est, por isso defini o classpath
apontando para o diretrio corrente. Ainda, 'java' precisa saber onde o nosso libHello.so est. Por
isso, forcei a barra e defini LD_LIBRARY_PATH apontando para o diretrio corrente. Na prtica,
voc poder colocar sua shared library em /usr/lib, por exemplo. De novo, no Windows isso no

176
necessrio: DLLs so procuradas, pelo sistema operacional, nos diretrios contidos na varivel de
ambiente PATH e no diretrio corrente.
Eis um makefile para tudo isso:
# Makefile
#
# Use: make (para compilar)
# make run (para executar Hello.class)
# make clean (para apagar arquivos deixando apenas os fontes)

JDK_LIBPATH=/usr/lib/jvm/default-java/include

.PHONY: all run clean

all: Hello.class HelloWorld.class libHello.so

libHello.so: libHello.o
gcc -o $@ -shared $^

libHello.o: libHello.c HelloWorld.h


gcc -I $(JDK_LIBPATH) -c -fPIC -O3 -march=native $@ libHello.c

# javah precisa receber apenas o nome da classe.


HelloWorld.h: HelloWorld.class
classfile="$<"; \
javah -o $@ $${classfile%.class}

%.class: %.java
javac -classpath . $<

run:
LD_LIBRARY_PATH=.:$$LD_LIBRARY_PATH java Hello

clean:
rm *.o *.so *.class *.h

Voltando ao cdigo em C, os tipos usados pelo Java so diferentes. So nomeados com um 'j' na
frente:
Tipo javans Tipo C
jboolean unsigned char
jbyte char
jchar unsigned short
jshort short
jint int
jlong long
jfloat float
jdouble double
jclass ?
jstring ?
jobject ?
j<T>Array <T>[]
void void
Tabela 13: Tipos usados no JNI

Na tabela acima, o <T> um tipo primitivo ou object do java e o tipo equivalente do C. Um array

177
do tipo jintArray mapeado para 'int []', em C, por exemplo.
Os tipos jclass, jstring e jobject so tipos especiais. Todos os tipos no primitivos so objetos, em
Java. Para acess-los temos que usar algumas funes disponibilizadas pela JVM acessveis atravs
do ponteiro do tipo JEnv, passado para a nossa funo.
Aqui entra o conceito de funes virtuais que mostrei anteriormente. Esse parmetro do tipo JEnv
um ponteiro para uma tabela que contm ponteiroa para 230 funes disponibilizadas pela JVM. Ou
seja, um ponteiro para uma tabela virtual.

Usando strings
Alm dos tipos primitivos (int, char, float...) todos os outros so objetos, em Java, e so passados
para a nossa funo como tal. Sendo objetos, esses parmetros podem ser movidos num dos heaps
gerenciados pelo java. Da, temos que tomar precaues especiais com relao a eles.
Tenho boas notcias e ms notcias com relao s strings no Java. Qual voc quer ler primeiro?
Bem... A boa notcia que todas as strings, em java, so codificadas no formato UTF-8. Mesmo que
voc edite o cdigo fonte usando um charset mais simples, como WINDOWS-1252 ou ISO-8859-1,
o compilador converter suas strings para UTF-8.
A m notcia que java usa um formato proprietrio para o charset UTF-8. Segundo a
documentao da Oracle, por exemplo, o caracter nulo ('\0') no corresponde ao byte zero, mas aos
bytes \xc0\x80'. Isso um grande problema com relao ao esquema de codificao de strings
usado pelo C, que espera um caracter nulo no final. Felizmente, ao que parece, java mantm essa
regra, reservando o caracter especial para '\0' explcito.
Tenha em mente que alguns caracteres especiais podero aparecer errados, do ponto de vista de C,
graas s idiossincrasias do java.
Para lidar com as strings, ou voc as obtm nesse formato e lida com elas assim mesmo, ou as
converte para unicode. Unicode, no contexto de C, o tipo wchar_t. Cada caracter ter tamanho
fixo de 16 bits. A vantagem de usar UTF-8 que ele tem mapeamento direto ao ASCII. A
desvantagem que este um multibyte charset. Isso quer dizer que alguns caracteres podem ter
mais que um byte de tamanho. No Linux isso no problema, j que o charset default o UTF-8.
No Windows, isso pode ser meio chato... Windows usa, internamente, o formato unicode single
charset, onde cada caracter tem 2 bytes de tamanho. Em C, se for declarar uma string literal usando
unicode, deve-se usar um 'L' na frente da string:
wchar_t mystring[] = L"Teste de string em unicode.";

Neste caso, para evitar avisos do compilador, voc dever fazer coverses de tipos no seu cdigo,
ao chamar uma das funes da JVM:
jstring s = (*env)->NewString(env, (const jchar *)mystring, wcslen(mystring));

Ou fazer o type casting para const wchar_t *. Claro que esse casting pode no ser necessrio. O
tipo jchar definido como short e, portanto, tem 16 bits de tamanho.
Para obter acesso ao contedo do objeto String temos que obter uma referncia, trav-la para que o
Garbage Collector no bagunce tudo movendo os dados de um lugar para outro e, depois de usar os
dados do buffer apontado pela referncia, liberar a trava. Para tanto, existem pares de rotinas que
devem sempre ser usadas para lidar com strings: GetString* e ReleaseString*. A seguir, eis um
exemplo de uso:
Suponha que a nossa funo showMessage, no exemplo anterior em java, seja assim:

178
public static native void showMessage(String str);

Ao executar javah em HelloWorld.class, obtemos um arquivo HelloWorld.h com um prottipo e


mapeamos esse prottipo para a funo abaixo:
JNIEXPORT void JNICALL Java_HelloWorld_showMessage(JEnv *env, jclass this, jstring str)
{
const char *s;

/* Obtm o ponteiro da string em UTF-8. */


if ((s = (*env)->GetStringUTFChars(env, str, NULL)) != NULL)
{
puts(s);

/* Liberamos o ponteiro da string em UTF-8. */


(*env)->ReleaseStringUTFChars(env, str, s);
}
}

Usamos 'env' para chamar as funes da JVM. Note que o ponteiro 'env' contm um ponteiro que
aponta para uma tabela virtual. Por isso, em C, temos que us-lo como '(*env)'. Se estivssemos
usando C++ e o tipo JEnv fosse declarado como uma classe contendo funes virtuais (existe uma
em jni.h), ento poderamos fazer a chamada assim:
env->GetStringUTFChars(str, NULL);

O ponteiro 'this' e a indireo da tabela virtual assumida por default, em C++, quando h
declarao de chamadas de funes de classes, que sejam virtuais... Mas, estamos lidando com C
aqui...
No exemplo, GetStringUTFChars obtm um ponteiro para o buffer contendo a string passada como
parmetro. O terceiro parmetro, NULL, passado para GetStringUTFChars, diz JVM que ela no
tem que fazer uma cpia da string original. Este parmetro um ponteiro para o tipo jboolean que,
se apontar para um valor JNI_TRUE, indica para a JVM que vamos trabalhar com uma cpia, no
com o buffer original:
jboolean copy = JNI_TRUE;
s = (*env)->GetStringUTFChars(env, str, &copy);

Cpias podem ser alteradas o quanto quisermos. Trabalhar com o buffer original no l uma boa
ideia, j que ele deveria ser gerenciado apenas pela JVM. Nada impede que o meu prprio cdigo
crie uma cpia da string original. Por isso costumo passar NULL ou zero todas as vezes:
const char *s;
char *s2;

s = (*env)->GetStringUTFChars(env, str, NULL);


s2 = strdup(s);
...
free(s2);
(*env)->ReleaseStringUTFChars(env, str, s);

bom verificar o resultado da chamada a GetString*. Ela pode retornar um ponteiro NULL, nos
dizendo que houve um erro na JVM (OutOfMemory?!).
A chamada a ReleaseStringUTFChars necessria para fazer o destravamento do objeto.
Agora, suponha que a funo, em java, devolva uma string:

179
...
public static native String getMessage();
...
-----%<----- corte aqui -----%<-----
static const char myString[] = "Teste";

JNIEXPORT jstring JNICALL Java_HelloWorld_getMessage(JEnv *env, jclass this)


{
return (*env)->NewStringUTF(env, myString);
}

A funo NewStringUTF criar um objeto String em algum lugar do heap da JVM (possivelmente
na gerao Young), obter a string do buffer apontado por myString (criando a sua prpria cpia) e
devolver a referncia dada pelo tipo jstring para a JVM. Mas, garantido que essa referncia seja
uma referncia local, do ponto de vista do java... Isso significa que, se nenhuma funo fora da
nossa for usar a string, responsabilidade da nossa funo indicar JVM que essa referncia poder
ser coletada e colocada no lixo...
No exemplo acima, retornamos a referncia (o tipo jstring um ponteiro!) para o mtodo chamador
e deixamos que ele lide com a referncia. Mais adiante mostrarei um caso onde teremos que lidar
com a referncia por ns mesmos...

Usando arrays unidimensionais


Arrays tambm so objetos e comportam-se de forma muito parecida com strings. Pelo menos para
leitura.
O tamanho de uma string, tanto em C quanto em Java, pode ser conhecido pelo caracter '\0', no final
do buffer. No caso de arrays, o tamanho ou conhecido de antemo, ou pode ser obtido atravs de
uma chamada funo GetArrayLength da JVM.
A leitura de um array pode ser feita usando Get<T>ArrayElements e Release<T>ArrayElements,
onde <T> corresponde a um tipo primitivo ou Object:
int *parray;

/* Pega o array... */
parray = (*env)->GetIntArrayElements(env, javaArrayVar, NULL);
if (parray)
{
/* Manipula o array, em C, aqui...
...

/* Finalmente, libera o array */


(*env)->ReleaseIntArrayElements(env, javaArray, parray, 0);
}

Get<T>ArrayElements funciona igualzinho a GetString. J Release<T>ArrayElements tem um


parmetro adicional que especifica o que deve ser feito do buffer. Ele aceita 3 valores:
Modo Significado
0 Copia de volta para o array java e libera o buffer.
JNI_COMMIT Copia de volta e no libera o buffer.
JNI_ABORT No copia de volta.
Tabela 14: Modos aceitos por Release<T>ArrayElements.

Para criar novos arrays existem as funes New<T>Array que tomam o nmero de elementos como
parmetro. Essa funo somente aloca espao na JVM para um array. Para preench-lo precisamos
usar a funo Set<T>ArrayRegion, que toma o objeto da classe do array devolvido por

180
New<T>Array, o ndice inicial, a quantidade de elementos e o ponteiro para o nosso array, em C.
Isso copiar nosso array para o espao previamente alocado pela JVM:
int myArray[3] = { 1, 2, 3 };
jintArray javaArray;

if ((javaArray = (*env)->NewIntArray(env, 3)) != NULL)


{
(*env)->SetIntArrayRegion(env, javaArray, 0, 3, myArray);

}

A coisa comea a complicar se tivermos arrays com mais de uma dimenso. Para compreender essa
complicao precisamos, primeiro, entender como lidar com o tipo object...

Usando objetos
O tipo object uma espcie de coringa no baralho dos tipos do java. Com esse tipo podemos usar
qualquer tipo de referncias a outras classes de armazenamento, incluindo arrays, classes, objetos,
tipos primitivos, funes e interfaces. como se object fosse um ponteiro void, em C.
Voc deve ter reparado que nas funes nativas o segundo parmetro do tipo jclass65. Isso
acontece porque podemos querer saber de qual objeto a funo foi chamada. Ento nossa funo
nativa recebe a referncia this por esse parmetro.
Tome cuidado que, embora esse parmetro seja declarado como jclass, ele no recebe uma
referncia para uma classe java. Ele recebe a referncia para a instncia do objeto a qual a funo
native percence.
Para mim isso sempre foi meio estranho: Em java, classes e objetos so, ambos, objetos!
Java tem objetos dos mais variadas. At mesmo arrays so objetos. E para lidar com objetos
precisamos saber qual a sua classe. Por exemplo, se quisssemos alocar espao para um objeto da
classe MyClass, vazio, teramos que fazer algo assim:
jclass oclass = (*env)->FindClass(env, "LMyClass;");
jobject obj = (*env)->AllocObject(env, oclass);

A string passada para a funo FindClass uma assinatura que descreve a classe de
armazenamento. uma forma abreviada de descrever o objeto e a tabela abaixo mostra os
descritores usados.

65 Pode ser substitudo, sem medo, pelo tipo jobject, que faria mais sentido. Mas esse o prottipo que javah gera...

181
Classe Assinatura Significado
byte B
char C
double D
float F
int I
long J
short S
void V
boolean Z
T[] [T Array to tipo T
Classe de objeto Lclasspath; A classe String, por exemplo,
descrita como
Ljava/lang/String; (os pontos
so subtitudos por '/').
Mtodo (args)T 'args' uma lista delimitada por
';' e T o tipo de retorno.
Tabela 15: Lista de assinaturas que descrevem tipos, em Java.

Nossa classe Hello, por exemplo, possui as seguintes assinaturas:


$ javap -s Hello
public class Hello {
public Hello();
Signature: ()V

public static void main(java.lang.String[]);


Signature: ([Ljava/lang/String;)V
}

O utilirio javap, que acompanha o JDK, um pr-processador e disassembler. Ele te permite ver o
bytecode gerado pelo compilador javac, bem como outras informaes... A opo '-s' lista apenas as
assinaturas (signatures) da classe.
No caso de main a assinatura ([Ljava/lang.String;)V nos diz que este objeto uma funo que
retorna void (os parnteses e o 'V' no final) e possui um nico argumento que um array para uma
string, [Ljava/lang/String;.
Suponha que tenhamos uma classe com um membro de dados e um mtodo assim:
private String[] slist;
public int[][] getArray(int size, String str);

As assinaturas sero, respectivamente: [Ljava.lang.String; e (I;Ljava.lang.String;)[[I.


Mesmo funes so objetos no Java, por isso possuem assinaturas descritivas.

Chamando mtodos do prprio objeto


Se nossa funo nativa precisar chamar um mtodo do prprio objeto onde foi declarada, podemos
fazer algo assim:

182
// HelloWorld.java
public class HelloWorld {
static { System.loadLibrary("Hello"); }

public static native void call();

private void callback() { System.out.println("chamada por C."); }


}
-----%<----- corte aqui -----%<-----
...
/* definida como public static void call(); na classe HelloWorld. */
JNIEXPORT void JNICALL Java_HelloWorld_call(JEnv *env, jclass this)
{
jclass cls;
jmethodID mid;

/* Obtm a classe da referncia ao objeto 'this'. */


cls = (*env)->GetObjectClass(env, this);

/* Obtm o ndice da tabela virtual da classe.


Se retornar NULL significa que no achou o mtodo.
*/
if ((mid = (*env)->GetMethodID(env, cls, "callback", "()V")) != NULL)
{
/* Executa o mtodo da entrada n 'mid' da tabela virtual do objeto. */
(*env)->CallVoidMethod(env, this, mid);
}
}

O fato de que GetMethodID precisa usar o nome da funo para obter sua posio na tabela virtual
nos diz que o cdigo compilado via JIT (Just In Time compiler), que supostamente gera o cdigo
em assembly equivalente ao bytecode, vai usar, necessariamente RTTI (RunTime Type Information).
Isso quer dizer que o cdigo final nunca ser to performtico quanto um cdigo em C ou assembl,
j que haver muita comparao com strings!
No caso da funo a ser chamada precisar de parmetros, precisamos prepar-los. E a funo
Call<T>Method aceita mltiplos parmetros, do mesmo jeito que funes como printf o fazem:
/ HelloWorld.java
public class HelloWorld {
static { System.loadLibrary("Hello"); }

// membro de dados... usado mais adiante.


public int myField;

public static native void call();

private void callback(String str) { System.out.println(str); }


}
-----%<----- corte aqui -----%<-----

JNIEXPORT void JNICALL Java_HelloWorld_callback(JEnv *env, jclass this)
{
static char s[] = "Chamada pelo cdigo nativo!";
jstring str;
jclass cls;
jmethodID mid;

if ((str = (*env)->NewStringUTF(env, s)) != NULL)


{
/* Obtm a classe da referncia do objeto 'this'. */
cls = (*env)->GetObjectClass(env, this);

/* Obtm o ndice da tabela virtual da classe.


Se retornar NULL significa que no achou o mtodo.
*/
if ((mid = (*env)->GetMethodID(env, cls, "callback", "()V")) != NULL)
{
/* Executa o mtodo da entrada n 'mid', do objeto passando um parmetro adicional. */
(*env)->CallVoidMethod(env, this, mid, str);
}

183
/* Livra-se da referncia. */
(*env)->DeleteLocalRef(env, str);
}
}

A string passada para o callback precisa ser criada e sua referncia sempre local. J que no
vamos devolv-la para alguma funo chamadora, temos que marc-la como descartvel, usando
a funo DeleteLocalRef. Sem isso teramos um memory leak.

Acessando membros de dados do prprio objeto


Acessar um membro de dados feito do mesmo jeito que fizemos para conhecer o ID do mtodo.
S que o ID obtido do membro de dados, chamado no java de field:
jclass cls = (*env)->GetObjectClass(env, this);
jfieldID fieldID = (*env)->GetFieldID(env, cls, "myfield", "I");
int myIntField;

if (fieldID != NULL)
{
myIntField = (*env)->GetIntField(env, this, fieldID);
/* faz algo com 'myField' aqui. */
}

No h necessidade de nos livrarmos da referncia, j que ela pertence ao prprio objeto.


Note que, ao obter o FieldID, temos que passar, alm do nome do campo, a assinatura do mesmo.
De novo, java parece ser fortemente ligado ao RTTI.
GetFieldID pode retornar NULL, indicando que o membro de dados no pde ser encontrado no
objeto. Omiti a verificao de erros por pura preguia...

De volta aos arrays: Usando mais de uma dimenso


Arrays com mais de uma dimenso so, na verdade, arrays de arrays. O primeiro nvel sempre um
array de objects. Somente a ltima dimenso que um array de um tipo primitivo. Ao declarar
uma funo nativa:
// Em Java:
public static native int[][] getBidimensionalArray();

O que a funo, em C, de fato retorna uma referncia a um array do tipo jobjectArray.


Para criar esse array no seu cdigo voc usar NewObjectArray, obter os ponteiros com
GetObjectArrayElements, para cada elemento do array usar NewIntArray (como descrito
anteriormente) e usar SetIntArrayRegion para preench-los.
J que o que estamos retornando ao chamador uma referncia a jobjectArray, voc dever
informar JVM, depois de liberar o array de inteiros com ReleaseIntArrayElements, que esses
devem ser marcados como descatveis, usando DeleteLocalRef.
O mtodo chamador lidar com a referncia ao array de objects. Mas os arrays mais internos
devero estar marcados como descartveis para no causar um memory leak, j que o mtodo
chamador no foi o responsvel pela criao destes!
O acesso a arrays de mltiplas dimenses feito de maneira semelhante:
// Em Java:
public static native int doSomething(int[][] array);

A nossa funo nativa recebe um array de objects e voc deve obter os arrays mais internos a partir

184
dos elementos do array mais externo (de objects).
Lembre-se que arrays so objetos em si. Eles tm funes membro atreladas. Uma delas
GetArrayLength, que devolve o nmero de elementos do array. Essa funo pode ser usada para
conhecer o tamanho das dimenses:

/* Pega elementos do primeiro nvel. */
int obj_elements = (*env)->GetArrayLength(env, array);
int i;

for (i = 0; i < obj_elements; i++)


{
/* Pega elementos do segundo nvel. */
jobject o = (*env)->GetObjectArrayElement(env, array, i);
int *iarr = (*env)->GetIntArrayElements(env, o, 0);
int iarr_elements = (*env)->GetArrayLength(env, o);
int j;

for (j = 0; j < iarr_elements; i++)


{
/* faz algo com os elementos dos array inteiro (ponteiro iarr). */
}

(*env)->ReleaseIntArrayElements(env, o, iarr, 0);


}

E, j que recebemos o array do chamador, no somos responsveis por liberar quaisquer referncias
locais.

185
186
Captulo 16: Usando Python como
script engine
Tambm no gosto muito de linguagens interpretadas, as chamadas script languages. Elas tem
que ser interpretadas (parsed), quando o parser busca por erros na gramtica e depois
executadas passo por passo. claro que a performance fica seriamente comprometida no
processo. Mas, elas so teis...
Imagine um cenrio onde voc possa criar algumas rotinas, em modo texto, que possam ser
chamadas pelo seu cdigo, em C, mais performtico. Nesse caso, os scripts podem ser
modificados quando necessrio sem afetar muito o seu cdigo original.
Dentre as linguagens script mais badaladas, o Python tem uma vantagem: Ele pode ser pr-
compilado. Alm de ter uma srie de recursos interessantes, em sua linguagem.

Contagem de referncias
Assim como Java, o interpretador Python tambm tem um garbage collector. Ele funciona de uma
maneira muito simples: Todo objeto tem um contador interno que comea em 1 quando
instanciado e esse contador incrementado a cada vez que criada uma nova referncia ao objeto,
por exemplo, se ele passado como parmetro para uma funo. Sempre que essa referncia sai do
escopo, o contador decrementado. Quando o contador chega a zero, o garbage collector livra-se
do objeto.
O interpretador faz isso automaticamente, mas nosso cdigo, ao usar objetos do python, no tem
esse luxo. Temos que usar os macros Py_INCREF e Py_DECREF. Raramente precisaremos usar
Py_INCREF, mas devemos usar Py_DECREF se pretendermos marcar o objeto como
descartvel, assim como fazemos no java, com a funo DeleteLocalRef.
Py_DECREF tem um problema. Se a referncia chega a zero o objeto liberado e tambm setado
para NULL. E usar ponteiros nulos causa segmentation faults. Isso pode ser resolvido com o uso de
Py_XDECREF, que faz a mesma coisa que o macro anterior, mas ignora referncias nulas... O
problema que, ao usar Py_XDECREF, no saberemos se nosso programa tem um bug ou no.
bom restringir seu uso para os casos onde voc sabe que a referncia pode ser NULL.

Instanciando o python
Abaixo temos um esqueleto de programa que usa o interpretador python. Nosso nosso programa
hospedeiro (host):
#include <Python.h>

int main(int argc, char *argv[])


{
Py_Initialize();
if (!Py_IsInitialized())
{
fprintf(stderr, "No foi possvel carregar o Python.\n");
return 1;
}

/* Usar as funes da libpythonX.X aqui. */

Py_Finalize();
return 0;
}

187
bem simples, huh?

Carregando um mdulo
Assim como na linguagem C, um arquivo com extenso '.py', que contm cdigo python,
chamado de mdulo. Precisamos saber como carregar um mdulo contendo nossas funes e
classes. bem simples, mas tem um detalhe.
Python carrega seus mdulos a partir de um conjunto de diretrios pr-definidos. Podemos saber
quais so com esse cdigo:
#!/usr/bin/python
# showsyspath.py
import sys

print sys.path
-----%<----- corte aqui -----%<-----
$ ./showsyspath.py
['/home/user/work/book-srcs/python', '/usr/lib/python2.7',
'/usr/lib/python2.7/plat-x86_64-linux-gnu', '/usr/lib/python2.7/lib-tk',
'/usr/lib/python2.7/lib-old', '/usr/lib/python2.7/lib-dynload',
'/usr/local/lib/python2.7/dist-packages', '/usr/lib/python2.7/dist-packages',
'/usr/lib/python2.7/dist-packages/PILcompat', '/usr/lib/python2.7/dist-packages/gtk-2.0',
'/usr/lib/pymodules/python2.7', '/usr/lib/python2.7/dist-packages/ubuntu-sso-client']

O primeiro diretrio de busca dos mdulos o diretrio atual. Mas isso no funciona bem com
mdulos carregados a partir de nossos programas em C. Ao inicializar o interpretador com
Py_Initialize, a lista dos diretrios estar em sys.path exceto o diretrio atual. Por isso, temos que
incluir o diretrio onde o mdulo se encontra, se no for um dos diretrios default.
A listagem seguinte mostra como o mdulo mymodule.py carregado a partir do diretrio onde
nosso programa ser chamado:
#include <Python.h>

#define MODULE_NAME "mymodule"

void executeModule(Py_Object *);

int main(int argc, char *argv[])


{
Py_Object *sys, *path;
Py_Module *module;

Py_Initialize();
if (!Py_IsInitialized())
{
fprintf(stderr, "No foi possvel carregar o Python.\n");
return 1;
}

/* Importa o mdulo 'sys' */


if ((sys = PyImport_ImportModule("sys")) != NULL)
{
/* 'sys' um objeto que contm o atributo 'path' */
if ((path = PyObject_GetAttrString(sys, "path")) == NULL)
{
fprintf(stderr, "Erro ao obter o atributo 'sys.path'.\n");
goto endOfProgram;
}

/* Adiciona o diretrio corrente em 'sys.path' */


/* PyList_Append() captura a referncia do objeto! */
PyList_Append(path, PyString_FromString("."));

188
module = PyImport_ImportModule(MODULE_NAME);
if (module != NULL)
{
/* Usar as funes da libpythonX.X aqui. */
executeModule(module);

/* No precisamos mais do mdulo. */


Py_DECREF(module);
}

endOfProgram:
Py_DECREF(sys);
}
else
{
fprintf(stderr, "Erro ao carregar o mdulo 'sys'.\n");
Py_Finalize();
return 1;
}

Py_Finalize();

return 0;
}

bem verdade que ao chamarmos Py_Finalize, todas as referncias sero liberadas, j que o
interpretador tambm ser descarregado. O cdigo acima literal com relao ao decrementar das
referncias e, para isso, fao uso de um goto e de um label... Considerados como crias do inferno
por alguns puristas...
A nica coisa estranha que no precisamos liberar o objeto 'path'. O motivo que objetos
containers, do tipo listas, tuplas, pilha, filas, capturam a referncia de um objeto para si. Quando o
container some devido ao decremento de referncias, leva consigo seus itens.

Executando um mdulo simples


Separei a execuo do mdulo na funo executeModule, acima, para facilitar a leitura do cdigo e
no ser repetitivo. Para executar uma funo simples, em python, temos apenas que chamar a
funo PyObject_CallObject, passando o objeto a ser chamado (a funo) e seus parmetros.
Suponha que tenhamos o seguinte mdulo em python:
# mymodule.py
def multiply(a,b):
print Multiplicando,a,"por",b
return a*b

Nossa funo executeModule ficaria:


void executeModule(PyObject *pModule)
{
PyObject *pFunc, *pArgs, *pValue;

/* Obtem o objeto da funo e verifica se o objeto , de fato,


uma funo. */
pFunc = PyObject_GetAttrString(pModule, "multiply");
if (pFunc != NULL && PyCallable_Check(pFunc))
{
/* Prepara 2 parmetros para serem passados para a funo. */
if ((pArgs = PyTuple_New(2)) != NULL)
{
/* Os parametros 3 e 2 so colocados na posio 0 e 1 de uma 'tupla'. */
PTuple_SetItem(pArgs, 0, PyObject_FromLong(3));
Ptuple_SetItem(pArgs, 1, PyObject_FromLong(2));

/* Finalmente, chama a funo. */


pValue = PyObject_CallObject(pFunc, pArgs);

189
/* Deixa o nosso cdigo apresentar o resultado. */
printf("Valor retornado do python: %ld.\n", PyInt_AsLong(pValue));

/* Livra-se das referencias */


Py_DECREF(pValue);
Py_DECREF(pArgs);
}

Py_DECREF(pFunc);
}
}

190
Apndice A: System calls
A tabela de system calls, abaixo, usada pela instruo SYSCALL do processador, nos sistemas
Linux para a arquitetura x86-64 (amd64). Repare que todas as system calls esto disponveis em
funes da libc.
Ao usar SYSCALL voc deve passar os parmetros em registradores, ao invs da pilha. A
documentao do ponto de entrada de uma syscall, no cdigo fonte do kernel, nos diz:
Registrador Significado
RAX Nmero da funo da syscall, de acordo com a tabela abaixo...
R11 RFLAGS guardado em R11 antes (se necessrio) e no
retorno da instruo SYSCALL.
RDI, RSI, RDX, R10, R8, R9 Do primeiro ao sexto parmetro da funo.
Tabela 16: Uso dos registradores numa syscall.

Note que, a no ser pelo registrador RAX, R10 e R11, a instuo SYSCALL segue a conveno de
chamada da linguagem C para o modo x86-64. Assim, executar a funo exit fica assim:
mov eax,60 ; funo exit.
mov edi,EXIT_CODE
syscall ; chama exit(EXIT_CODE);

Outro exemplo. Para escrever uma string na tela, em C, usando a funo write, precisamos passar o
file descriptor o FILENO_STDOUT, o ponteiro para a string e o tamanho dela:
char *msg = "Hello, world!\n";
write(FILENO_STDOUT, msg, strlen(msg));

Este descritor tem o valor 1. Assim, a chamada em assembly, usando SYSCALL fica:
bits 64
section .data
msg: db 'Hello, world',0x0a
LENGTH equ $ - msg
FILENO_STDOUT equ 1

section .text

; write tem o prottipo:
; ssize_t write(int fd, void *buffer, size_t count);
mov eax,1 ; syscall: write
mov edi,FILENO_STDOUT
mov rsi,msg
mov edx,LENGTH
syscall ; write(FILENO_STDOUT, msg, LENGTH);

Consideraes sobre o uso da instruo SYSCALL


Lembre-se que SYSCALL usada como interface entre o userspace e o kernelspace. Todos os
ponteiros passados atravs da conveno de chamada e os ponteiros recebidos no retorno da funo
(via registrador RAX) so endereos lineares vlidos para uso no userspace. o caso, por exemplo,
de mmap:

191
; Equivalente a fazer:
; if ((p = mmap(NULL, 8192, PROT_READ | PROT_WRITE, MAP_ANONYMOUS, -1, 0)) == NULL)
; {
; error
; }
;
mov eax,9 ; mmap syscall
xor edi,edi ; addr = NULL
mov esi,8192 ; length = 8192
mov edx,3 ; prot = PROT_READ | PROT_WRITE
mov r10d,32 ; flags = MAP_ANONYMOUS
mov r8d,-1 ; fd = -1
xor r9,r9 ; offset = 0
syscall

mov [p],rax ; RAX contm o endereo linear do ponteiro de retorno de mmap.


or rax,rax ; RAX NULL?
jnz .L1 ; se no for, salta a rotina de erro.
error
.L1:

Outra considerao importante a de que seu processador pode no suportar as instrues


SYSCALL/SYSRET e as verses anteriores, SYSENTER/SYSEXIT (no use essas!). Para verificar
se seu processador suporta este modo basta verifiacar, via CPUID:
mov eax,1
cpuid
test edx,0x800 ; Testa o bit 11 de EDX.
; Neste ponto, se ZF=0, SYSCALL suportada.

Quando o processador no suporta SYSCALL, temos que usar um mtodo que emula as syscalls, do
mesmo jeito que o modo i386 faz. Neste caso o Linux x86-64 suporta syscalls via interrupo 0x80.
Os registradores que so usados como parmetros sero EAX, RBX, RCX, RDX, RSI, RDI e RBP,
nessa ordem. Chamamos int 0x80 e o valor de retorno colocado em RAX.
Esse acesso s syscalls uma emulao do que feito no modo i386. A diferena que ponteiros
tm que ser informados nos registradores estendidos de 64 bits. Outros tipos de parmetros seguem
as mesmas regras de tipos para o uso de registradores.
Deve-se levar em conta que os nmeros dos servios, colocados em EAX, so diferentes dos
usados com a instruo SYSCALL. Por exemplo, a rotina que imprime uma string ficaria assim:
bits 64
section .data
msg: db 'Hello, world',0x0a
LENGTH equ $ - msg
FILENO_STDOUT equ 1

section .text

; write tem o prottipo:
; ssize_t write(int fd, void *buffer, size_t count);
mov eax,4 ; syscall: write (modo emulado)
mov edi,FILENO_STDOUT
mov rsi,msg
mov edx,LENGTH
int 0x80 ; write(FILENO_STDOUT, msg, LENGTH);

Repare que o nmero da funo syscall_write 4. Quando usamos SYSCALL ele 1. A mesma
coisa acontece com syscall_exit. Usando SYSCALL a funo tem nmero 60, com a int 0x80 o
valor 1.

Onde obter a lista de syscalls do Linux?


No cdigo fonte do kernel do Linux voc pode obter uma lista de syscalls em

192
arch/x86/entry/syscalls. Existem dois arquivos: syscalls_32.tbl e syscalls_64.tbl, para chamadas via
int 0x80 e SYSCALL, respectivamente. Mais informaes sobre uma syscall em particular pode
ser obtida na manpage syscalls ou nas manpages da funo da libc correspondente.
Novamente, a conveno de chamada para a instruo SYSCALL a mesma do POSIX x86-64
ABI, exceto pelo registrador R10 e R11 (RFLAGS retornado em R11!).

Usar syscalls no Windows no uma boa idia!


Assim como no Linux, o Windows tambm tem funes disponibilizadas pelo kernel para uso no
userspace. Na Win64 API a instruo SYSCALL tambm pode ser usada, mas o conjunto de
registradores de entrada e de sada so diferentes. Da mesma forma, o Windows oferece uma
interrupo de software para o caso da instruo SYSCALL no estar disponvel, a interrupo
0x2E (pelo menos as documentadas!).
O grande problema que as funes no so padronizadas sequer entre verses diferentes do
Windows. O Windows NT tem funes numeradas de um jeito, o 2000 de outro, o XP diferente
dos outros dois e assim para o Vista, 8 e a atual verso 10...
Outro problema que as syscalls do Windows so complicadas de usar. Ao que parece, a Microsoft
segue o mesmo padro usado em rotinas do MS-DOS e BIOS: Ou seja, cada servio tem suas regras
particulares.
Desafio ao leitor a achar algum material bom sobre syscalls do Windows na Internet...

193
Apndice B: Desenvolvendo para
Windows usando GCC
Uma boa ferramenta para desenvolvimento em C para Windows o projeto MinGW (Minimalist
GNU for Windows). Com ele voc pode usar o gcc e o g++ e algumas ferramentas como objdump,
strip, ar e gas. A outra vantagem em usar MinGW que voc pode desenvolver para Windows sem
sair do Linux...

Usando o MinGW no Linux


Existem dois sabores do projeto: MinGW32 e MinGW64.O procedimento no pode ser mais
simples: Instale os pacotes mingw32 e mingw64:
$ sudo apt-get install mingw32 mingw64

Com isso os compiladores e utilitsios (linker, assembler, archiver etc) sero instalados e nomeados
com os prefixos i586-mingw32msvc- e x86_64-w64-mingw32-. Eis um simples exemplo de
cdigo compilado com o MinGW64:
/* hello.c */
#include <windows.h>

int CALLBACK WinMain(HINSTANCE hInstance,


HINSTANCE hPrevInstance,
LPSTR lpszCmdLine,
int nCmdShow)
{
MessageBox(NULL, Hello, world!, Hello, MB_ICONINFORMATION | MB_OK);
return 0;
}
-----%<----- corte aqui -----%<-----
$ x86_64-w64-mingw32-gcc -O3 -march=native -s -o test.exe test.c -Wl,--subsystem,windows

A opo '--subsystem,windows' tem que ser repassada para o linker, seno voc acaba com uma
aplicao para DOS.

Usando o MinGW no Windows


Baixe o MinGW de sua preferncia (32 ou 64) de um desses sites:
MinGW32: http://www.mingw.org/
MinGW64: http://mingw-w64.sourceforge.net/
Para instalar qualquer um dos dois o procedimento , tambm, bem simples. Basta baixar o
instalador e execut-lo. O programa criar uma rvore de diretrios, por exemplo, em C:\MinGW e
tudo o que voc ter que fazer colocar o subdiretrio C:\MinGW\bin na varivel de ambiente
PATH.
A partir dai poder usar o gcc e outros utilitrios.
Atualmente o MinGW32 tem um instalador ao estilo apt-get, mas grfico. bem til na seleo de
pacotes do MinGW...

195
As bibliotecas mingwm10.dll e msvcrt.dll
A Microsoft disponibiliza, no diretrio C:\windows\system32, uma biblioteca dinmica chmada
msvcrt.dll (Microsoft Visual C++ RunTime) que contm ganchos para chamadas API do
Windows. Essa biblioteca funciona como se fosse a libc.
MinGW precisa de outra biblioteca chamada mingwm10.dll, que uma surrogate library para a
msvcrt.dll. Sem ela seu executvel, compilado com o gcc, provavelmente no funcionar. A DLL
mingwm10.dll distribuda junto com o gcc e encontra-se no subdiretrio bin, no caso do Windows,
ou em /usr/share/doc/mingw32-runtime/, no caso do Linux. Este ltimo disponibiliza a DLL em
formato compactado com o gzip, portanto, necessrio que voc o descompacte antes de copi-lo
para o Windows:
$ gunzip /usr/share/doc/mingw32-runtime/mingwm10.dll.gz
$ cp test.exe mingwm10.dll ~/My_Windows_Virtual_Machine_Shared_Directory/

Aparentemente isso no necessrio se voc est lidando com o MinGW64...

Limitaes do MinGW
Como era de se esperar, o MinGW no distribui a maioria das shared libraries disponveis no Linux.
Se voc pretende us-lo como um cross compiler, na tentativa de criar um cdigo nico para os dois
ambientes (Linux e Windows), esteja avisado: Isso no ser fcil!
O MinGW do Linux disponibliza cerca de 150 bibliotecas estticas relacionadas API do Windows.
Essas bibliotecas so estticas porque, na verdade, so surrogates para msvcrt.dll e outras DLLs da
API do Windows. No entanto, no site, voc pode encontrar algumas bibliotecas adicionais como
zlib, pthreads, iconv e GMP.
A libc usada no MinGW tambm no completa. Por exemplo, ela no contm a funo asprintf.
E, finalmente, lembre-se que Windows usa uma conveno diferente, na arquitetura x86-64, no que
concerne o tamannho de tipos inteiros longos. Windows usa o padro IL32P64. Ou seja, 'int' e 'long'
tm o mesmo tamanho. Se quiser usar inteiros de 64 bits ter que, obrigatriamente, usar o tipo
'long long' ou um dos typedefs definidos em stdint.h.

Assembly com o MinGW e NASM


Lembre-se que a conveno de chamada usada no Win64 diferente da usada no padro POSIX!
Alm de se lembrar disso, pouca coisa muda com relao ao NASM. Apenas o formato do arquivo
objeto ter que ser informado como win64 ao invs de elf64 e, no caso da declarao de
exportao de smbolos (via diretiva GLOBAL), os atributos devem ser omitidos.

Windows usa codificao de caracteres de 16 bits, internamente


Diferente do Linux, que usa UTF-8, por default, o Windows usa um charset prprio onde cada
caracter tem 2 bytes de tamanho. Em C isso no problema, j que podemos escrever uma string
desse tipo assim:
wchar_t str[] = L"minha string";

Dessa forma, cada caracter da string ocupa 2 bytes ao invs de um s:


00: 6d 00 69 00 6e 00 68 00 61 00 20 00 73 00 74 00 |m.i.n.h.a. .s.t.|
16: 72 00 69 00 6e 00 67 00 00 00 |r.i.g... |

196
Em teoria, isso permite o uso de 65535 caracteres, no conjunto.
Para usar as funes do Windows da maneira mais performtica possvel voc ter que lidar com
essas strings estendidas... As funes da API que lidam com string tm um sufixo W no nome
para indicar o tipo Wide Char, em posio ao Ansi Char (onde as funes tm um sufixo A). Por
exemplo: A funo CreateWindow tem as variaes CreateWindowW e CreateWindowA.
No header windows.h existem macros que assumem, por default, os nomes de funes com sufixo
A e, assim, voc pode usar caracteres com 8 bits de tamanho nas strings. Mas essas funes
convertero a string para UNICODE, internamente.
Outra coisa importante que no mesmo header temos alguns tipos pr-definidos como LPWSTR e
LPSTR. Eles so definidos como:
typedef wchar_t *LPWSTR; /* Long Pointer to Wide-Char String */
typedef char *LPSTR; /* Long Pointer to [Ansi] String */

Digo isso porque, provavelmente, voc no ver o tipo padro wchar_t sendo usado em cdigos do
Windows... Mas a mesma coisa que LPWSTR. ainda mais provvel que voc encontre um tipo
chamao LPTSTR onde o T significa que o tipo ser escolhido com base em smbolos definidos no
ato da compilao (ou em alguma opo do compilador). Neste caso as strings literais podero ser
codificadas com o macro _T:
LPTSTR str = _T("minha string");

Outros tipos que voc pode encontrar so LPCSTR e LPCWSTR (e, acredito, o LPCTSTR), onde
C signfica const. Esses tipos so definidos como:
typedef char * const LPCSTR;
typedef wchar_t * const LPCWSTR;

Essas definies tm a pretenso de manter os cdigos fonte, escritos em C, compatveis entre as


diversas verses do Windows. O termo long (o L no tipo) tem motivo histrico. Ele vem da
poca do Windows 3.1, onde ponteiros near e far eram comuns. Todo ponteiro, no Windows
long.

Vale a pena desenvolver aplicaes inteiras em assembly para Windows?


Existem duas maneiras de usarmos funes definidas em DLLs, inclusive as definidas na Win64
API. A primeira fazer uma importao esttica, deixando o Windows carregar a DLL para ns.
A outra carreg-la dinamicamente, atravs das funes LoadLibraryW, GetProcAddressW e
FreeLibraryW.
No primeiro caso, o MinGW64 disponibiliza um utilitrio chamado dlltool, que cria uma biblioteca
esttica (que contm um arquivo objeto), contendo os smbolos para as funes definidas na DLL.
Uma funo como MessageBoxW, por exemplo, associada a um smbolo nomeado
__imp_MessageBoxW, que um ponteiro para a funo. Isso fcil de observar, o pequeno cdigo
abaixo mostra:
/* msgbox.c */
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,


LPSTR lpszCmdLine, int nCmdShow)
{
MessageBoxW(NULL, L"Minha String", L"Aviso", MB_OK | MB_ICONEXCLAMATION);
return 0;
}
-----%<----- corte aqui -----%<-----
$ x86_64-w64-mingw32-gcc -O3 -S -masm=intel msgbox.c

197
-----%<----- corte aqui -----%<-----
; Cdigo equivalente, em asm:
bits 64
section .rodata

MinhaString dw __utf16le__("Minha String"), 0


Aviso dw __utf16le__("Aviso"), 0

section .text

extern __imp_MessageBoxW

global WinMain
align 16
_start:
xor ecx,ecx
lea edx,[MinhaString]
lea r8,[Aviso]
mov r9d,0x40 ; MB_OK | MB_ICONINFORMATION
call [__imp_MessageBoxW] ; Essas chamadas so sempre indiretas!
xor eax,eax
ret

A chamada para MessageBox feita sempre de maneira indireta. O programa tem que saber o
endero da rotina, que no est localizada no seu programa, mas em USER32.DLL (mesmo no
modo x86-64 o arquivo tem esse nome!).
A diferena do cdigo em assembly e o cdigo em C que o GCC entende, graas ao header
windows.h, que o ponto de entrada do programa WinMain(). No caso do cdigo em assembly o
ponto de entrada, quando linkado usando o GNU Linker, continua sendo _start. Fora esse fato, todo
o resto segue exatamente o mesmo mtodo usado por programas em C: necessrio linkar o seu
objeto contra a biblioteca importada!
J que no h diferenas e syscalls so inviveis, por causa da falta de padronizao, no vale l
muito a pena codificar programas inteiros em assembly para Windows.
Outra coisa: Num programa em C a pseudo funo WinMain aceita 4 parmetros (onde o segundo
existe por motivos histricos e no usado na Win64 API). O compilador insere cdigo para
inicializar esses parmetros atravs de chamadas discretas a funes da API. Isso quer dizer que o
compilador C prepara o terreno para voc, coisa que no acontece em assembly.

Importando DLLs no MinGW-w64


O MinGW64 tem um utilitrio importante para a importao de funes contidas em DLLs. Trata-
se do dlltool. Ele cria, entre outras coisas, uma biblioteca esttica contendo as referncias para a
DLL desejada:
$ x86_64-w64-mingw32-dlltool -D mydll.dll -z mydll.dll.def -l mydll.dll.a

Dlltool cria dois arquivos: Um com extenso .DEF, contendo a listagem dos smbolos exportados
pela DLL e uma biblioteca esttica (extenso .a, no padro do GNU). O arquivo .DEF textual e
possui mais ou menos o formato:
LIBRARY nome-da-dll
EXPORTS
smbolo1
smbolo2

Ele til para que voc saiba quais so os smbolos contidos na DLL desejada, mas no
necessrio para linkarmos a biblioteca esttica ao nosso projeto.
No exemplo do uso de MessageBoxW, poderamos importar toda a USER32.DLL, criando o
arquivo user32.dll.a (ou user32.a, voc escolhe!) e usar a opo -l do linker para linka-lo ao

198
nosso programa.
Note que, no exemplo em C anterior, o smbolo __imp_MessageBoxW tambm criado para
permitir uma chamada indireta seja mais direta. Por mais direta quero dizer que normalmente a
biblioteca esttica cria funes wraper que tratam de fazer a chamada correta. O smbolo
__imp_MessageBoxW provavelmente foi criado a partir de uma extenso do compilador, um
atributo para funes chamado __declspec(dllimport). No caso das funes wraper a chamada
pode ser feita diretamente, mas o que voc estar chamando a funo contida na biblioteca
esttica, no a funo da API.
Usar bibliotecas estticas para acessar funes de DLLs um mtodo conhecido como early
binding, ou juntar mais cedo, numa traduo literal. Podemos juntar mais tarde, late binding,
usando as funes da Win64 API LoadLibraryW, GetProcAddressW e FreeLibrary. Neste caso no
precisamos nos preocupar obter uma biblioteca esttica externa:
#include <windows.h>

int __attribute__((stdcall)) (*myfunc)(int);


/* Tenta carregar mylib.dll */
if ((hModule = LoadLibraryW(L"mylib.dll")) == NULL)
return FALSE;

/* Tenta pegar o endereo de myfunc() */


if ((myfunc = GetProcAddressW(hModule, L"myfunc")) == NULL)
{
FreeLibrary(hModule);
return FALSE;
}

/* Finalmente, chama myfunc. */


x = myfunc(10);

FreeLibrary(hModule);

199
Apndice C: Built-ins do GCC
O GCC possui diversas funes j prontinhas para uso que permitem acesso a recursos do
processador. Muitas delas so especficas para a arquitetura do processador alvo. Temos funes
especficas para ARM, MIPS e outros. Aqui vou mostrar apenas algumas funes built-in que
podem ser usadas para evitar a necessidade da criao de cdigo em assembly.
Saiba que existem outras: Todas as instrues MMX, SSE, AVX, AVX2, FMA, BMI e um monte de
outras extenses esto disponveis via funes built-in, mas conveniente que voc continue
usando as funes intrnsecas, disponibilizadas no header x86intrin.h.
Funo Descrio
__builtin_cpu_is Retorna um booleano se a sua CPU a
informada na string:

if (!__builtin_cpu_is("intel"))
{
puts("CPU no Intel!");
exit(1);
}

Dentre as strings vlidas, esto: intel, amd,


atom, core2, corei7, nehalem,
sandybridge, dentre algumas outras.
__builtin_cpu_supports Retorna um booleano se sua CPU suporta a
feature informada na string. O uso similar
funo __builtin_cpu_is, exceto que a string
pode ser: cmov, mmx, popcnt, sse,
sse2, sse3, ssse3, sse4.1, sse4.2,
avx ou avx2.
__builtin_expect Usada para dar uma dica ao brach prediction do
processador. O valor 0 ou 1 no segundo
parmetro indica se o valor esperado pela
expresso verdadeiro ou falso:

/* Indica que, na maioria das vezes,


a expresso ser verdadeira. */
if (__buiitin_expect(x == 0, 1))

Deve ser usada com cautela, apenas nos casos


mais crticos.
__builtin_clear_cache Usada para liberar linhas do cache que
contenham os endereos de incio (primeiro
parmetro) at o fim (segundo parmetro). A
rotina provavelmente usa a instruo CLFLUSH
para isso.

/* p1 e p2 so ponteiros onde p2 > p1. */


__builtin_clear_cache(p1, p2);

201
Deve ser evitada para no bagunar muito o
algortmo de caching.
__builtin_prefetch Tenta fazer o prefetching no cache. A funo
aceita de 1 a 3 parmetros. O primeiro o
endereo do dado desejado. O segundo um
booleano (0 ou 1), onde 1 segnifica prefetching
para escrita e 0 (o default), bviamente,
prefetching para leitura.

O terceiro parmetro, se informado, indica a


localidade ou temporalidade. O valor 0
indica no temporalidade, ou seja, o dado ser
usado muitas vezes no cache. Valores entre 1 e 3
indicam o nvel de temporalidade. 3 (o default)
significa alto nvel de temporalidade e 1, baixo,
onde um baixo nvel significa que o dado poder
ser esquecido com mais facilidade.
/* x colocado no cache para leitura
e temporalidade 3 */
__builtin_prefetch(&x);

/* y colocado no cache para escrita


e no-temporal (durvel). */
__builtin_prefetch(&y, 1, 0);

De novo, evitar usar para no bagunar o


algortmo de caching do processador.
__builtin_bswap64 Torna uma represetao little-endian em big-
endian ou vice-versa, mas com 64 bits. Funes
como ntohl e htonl faz o mesmo, mas com 32
bits (e tambm existe __builtin_bswap32):

/* Onde x e y so do tipo long


(ou long long). */
y = __builtin

No caso das funes __builtin_cpu_is e __builtin_cpu_support necessrio passar uma string para
testar um recurso. Embora esse seja um mtodo fcil, as strings tero que ser alocadas no segmento
de dados (.rodata ou .data). Isso algo que eu no aprecio... Acho prefervel usar CPUID para obter
o mesmo efeito. Abaixo, temos funes equivalentes, em C, para uso do GCC no modo x86-64:

202
/* cpu.c */
#include <memory.h>
#include <cpuid.h>

__attribute__((noinline)) static const char *get_cpu_id(void)


{
static char cpuAux[12];
int *p, a, b, c, d;

__cpuid(0, a, b, c, d);
p = (int *)cpuAux;
*p++ = b;
*p++ = d;
*p = c;

return cpuAux;
}

int cpu_is_intel(void) { return memcmp("GenuineIntel", get_cpu_id(), 12) == 0; }


int cpu_is_amd(void) { return memcmp("AuthenticAMD", get_cpu_id(), 12) == 0; }

/* Uso:
* if (get_cpu_features() & bit_SSE2)
*/
int get_simd_features(void)
{
int a, b, c, d, _tmp;

__cpuid(1,a,b,c,d);

/* Os bits no se sobrepem, ento seguro fazer um OR com os valores mascarados. */


c &= bit_SSE3 | bit_SSSE3 | bit_FMA | bit_SSE4_1 | bit_SSE4_2 | bit_AVX | bit_F16C;
d &= bit_SSE | bit_SSE2 | bit_MMX;
_tmp = c | d;

__cpuid_count(7,0,a,b,c,d);

/* Os bits ainda no se sobrepem e continua seguro fazer um OR. */


b &= bit_BMI | bit_AVX2 | bit_BMI2;

return _tmp | b;
}
-----%<----- corte aqui -----%<-----
/* test.c */
#include <cpuid.h>
#include <stdio.h>

/* definidas em cpu.c */
extern int cpu_is_intel(void);
extern int get_simd_features(void);

void main(void)
{
printf("CPU ");
if (!cpu_is_intel())
printf(" no");
printf(" Intel.\n"
"CPU ");
if (!(get_simd_features() & bit_SSE2))
printf(" no");
puts(" suporta SSE2.");
}
-----%<----- corte aqui -----%<-----
# Makefile
test: test.o cpu.o
gcc -O3 -o $@ $^

%.o: %.c
gcc -O3 -march=native -c -o $@ $<

203
Apndice D: Mdulos do Kernel
Na maioria das vezes estamos interessados em desenvolver programas que executam no userspace.
Mas, alguns recursos s esto disponveis no kernelspace.
De tempos em tempos topo com pessoas que querem desenvolver pequenos programas que
interagem com a porta serial, ou outro dispositivo, de alguma maneira no padronizada. Querem,
por exemplo, ter o poder de usar instrues IN e OUT, que normalmente no podem ser usadas no
userspace. Para essas pessoas a melhor alternativa desenvolverem um mdulo para o kernel que
funcionar como interface entre sua aplicao no userspace e o dispositivo.

Anatomia de um mdulo simples


Eis o cdigo fonte de um mdulo bem simples. As macros module_init e module_exit registram as
funes de inicializao e trmido do mdulo. Essas funes devem ser marcadas com os
modificadores __init e __exit, respectivamente. As outras macros (MOD_LICENSE,
MOD_AUTHOR, MOD_DESCRIPTION) so usadas apenas para documentao:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init init_hello(void)


{
printk(KERN_INFO "Ol, kernelspace!\n");
return 0;
}

static void __exit exit_hello(void)


{
printk(KERN_INFO "Adeus, kernelspace!\n");
}

module_init(init_hello);
module_exit(exit_hello);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Frederico Lamberti Pissarra");
MODULE_DESCRIPTION("Hello module");

Alguns detalhes importantes: Um mdulo do kernel pode usar a libc, mas no deve faz-lo...
preciso ter muito cuidado em usar funes reentrantes (marcadas com MT-Safe na documentao da
libc) e tomar cuidado com condies que possam causar problemas com threads. Note, por
exemplo, que a funo printk foi usada ao invs de printf.

205

Você também pode gostar