Inline assembly: quando o C não é suficiente
1. Introdução ao Inline Assembly
Inline assembly é um recurso da linguagem C que permite inserir instruções em assembly diretamente no código-fonte C, sem a necessidade de criar arquivos separados com extensão .s. Essa funcionalidade existe porque, embora C ofereça alto nível de abstração e portabilidade, há situações em que o programador precisa de controle absoluto sobre o hardware — seja para otimizações críticas de desempenho, acesso a instruções especiais da CPU (como RDTSC, CPUID, ou operações atômicas) ou manipulação direta de registradores.
Diferentemente do assembly puro, onde todo o código é escrito em arquivos separados e montado com um assembler externo, o inline assembly é embutido no meio do código C, permitindo que variáveis e expressões C sejam usadas como operandos. O compilador é responsável por alocar registradores e gerenciar a interface entre o código C e o assembly, o que reduz parte da complexidade, mas exige cuidado redobrado com as restrições e efeitos colaterais.
2. Sintaxe Básica no GCC/Clang (asm)
No GCC e Clang, a sintaxe básica do inline assembly segue o formato:
asm [volatile] ( "instruções assembly"
: operandos_de_saída
: operandos_de_entrada
: clobbers
);
Os componentes são:
- Assembly string: sequência de instruções assembly, geralmente entre aspas duplas.
- Operandos de saída: variáveis C que receberão valores do assembly.
- Operandos de entrada: variáveis C cujos valores serão usados no assembly.
- Clobbers: registradores ou áreas de memória que o assembly modifica sem declarar como operando.
Exemplo simples: mover um valor entre registradores.
#include <stdio.h>
int main() {
int valor = 42;
int resultado;
asm ("movl %1, %0"
: "=r" (resultado) // saída: registrador r
: "r" (valor) // entrada: registrador r
);
printf("Resultado: %d\n", resultado); // 42
return 0;
}
3. Operandos e Restrições (Constraints)
Os operandos são vinculados a variáveis C por meio de restrições (constraints). As restrições comuns incluem:
r: qualquer registrador de propósito geral.m: endereço de memória.i: constante imediata (conhecida em tempo de compilação).g: qualquer registrador, memória ou imediato.
Operandos de saída usam prefixo =, indicando que são apenas para escrita. Para operandos de leitura e escrita, usa-se +r. O earlyclobber (=&r) informa ao compilador que o operando é escrito antes que todos os operandos de entrada sejam lidos, evitando que o compilador reuse o mesmo registrador para entrada e saída.
int a = 10, b = 20, soma;
asm ("addl %2, %0"
: "=r" (soma) // saída
: "0" (a), "r" (b) // entrada: "0" significa mesmo registrador que o operando 0
);
4. Clobbers e Volatile
Clobbers são essenciais para evitar que o compilador assuma que registradores ou flags da CPU permanecem inalterados após o bloco assembly. Dois clobbers comuns:
"memory": informa que o assembly pode ler ou escrever em qualquer posição de memória."cc": indica que o assembly modifica as flags de condição da CPU (registrador EFLAGS no x86).
O modificador volatile impede que o compilador otimize ou reordene o bloco assembly. Use-o quando o assembly tiver efeitos colaterais que devem ocorrer exatamente onde escrito, como em operações de hardware ou loops de espera ativa.
asm volatile ("": : : "memory"); // barreira de memória
5. Exemplos Práticos: Otimização e Acesso a Hardware
Operação atômica: atomic_xchg com xchg
int atomic_exchange(int *ptr, int novo) {
int antigo;
asm volatile (
"xchgl %0, %1"
: "=r" (antigo), "+m" (*ptr)
: "0" (novo)
: "memory"
);
return antigo;
}
Leitura do contador de ciclos da CPU (RDTSC)
unsigned long long read_tsc() {
unsigned int lo, hi;
asm volatile ("rdtsc" : "=a" (lo), "=d" (hi));
return ((unsigned long long)hi << 32) | lo;
}
Manipulação de bits: contar zeros à direita (bsf)
int count_trailing_zeros(unsigned int x) {
int resultado;
asm ("bsfl %1, %0" : "=r" (resultado) : "rm" (x));
return resultado;
}
6. Armadilhas Comuns e Undefined Behavior
Inline assembly é um dos recursos mais propensos a erros em C. As principais armadilhas incluem:
- Violação de restrições: usar uma restrição
r(registrador) com uma variável que o compilador não pode colocar em registrador, como um campo de bitfield. - Clobbers ausentes: esquecer de declarar que o assembly modifica um registrador pode fazer com que o compilador reutilize aquele registrador para outra variável, corrompendo dados.
- Efeitos colaterais não declarados: modificar memória sem usar clobber
"memory"pode levar a otimizações incorretas. - Não portabilidade: código inline assembly para x86 não funciona em ARM. Sempre documente a arquitetura alvo.
// ERRADO: clobber ausente
asm ("movl $0, %%eax" : : : ); // modifica eax sem declarar
// CORRETO:
asm ("movl $0, %%eax" : : : "%eax");
7. Alternativas ao Inline Assembly
Antes de recorrer ao inline assembly, considere alternativas mais seguras e portáveis:
- Intrínsecos do compilador: funções embutidas que mapeiam diretamente para instruções assembly, como
__builtin_popcount(contagem de bits),__sync_fetch_and_add(operações atômicas) e_mm_pause(pausa em spinlocks). - Bibliotecas de abstração:
libatomicpara operações atômicas,libccom funções comomemcpyotimizadas, ou bibliotecas de hardware comox86intrin.h. - Assembly puro: quando o inline assembly fica muito complexo, vale mais a pena escrever uma função em um arquivo
.sseparado, que é mais fácil de depurar e manter.
// Equivalente ao RDTSC com intrínseco (MSVC)
#include <intrin.h>
unsigned long long read_tsc_intrinsic() {
return __rdtsc();
}
8. Boas Práticas e Portabilidade
Para usar inline assembly de forma segura e eficiente:
- Isole em macros ou funções inline: facilita a manutenção e permite substituir por alternativas portáveis.
- Documente restrições de arquitetura e versão do compilador: indique claramente se o código funciona apenas em x86, x86_64, ARM, etc.
- Teste com diferentes níveis de otimização:
-O0,-O2,-Os— o compilador pode rearranjar o código de maneiras inesperadas. - Sempre declare todos os clobbers: especialmente
"memory"e"cc"quando aplicável. - Prefira intrínsecos sempre que possível: eles são mantidos pelos desenvolvedores do compilador e são mais portáveis.
#ifdef __x86_64__
#define HAVE_RDTSC
static inline unsigned long long rdtsc() {
unsigned int lo, hi;
asm volatile ("rdtsc" : "=a"(lo), "=d"(hi));
return ((unsigned long long)hi << 32) | lo;
}
#else
// fallback portável (menos preciso)
static inline unsigned long long rdtsc() { return 0; }
#endif
Referências
- GCC Inline Assembly HOWTO — Tutorial clássico e completo sobre sintaxe e restrições do inline assembly no GCC.
- GCC Documentation: Extended Asm — Documentação oficial do GCC sobre a sintaxe estendida de assembly embutido.
- Clang Language Extensions: Inline Assembly — Documentação oficial do Clang sobre suporte a inline assembly.
- x86 Instruction Set Reference (Intel) — Manual de referência das instruções x86, essencial para escrever assembly correto.
- OSDev Wiki: Inline Assembly — Guia prático voltado para desenvolvimento de sistemas operacionais, com exemplos para x86 e ARM.
- CppCon 2017: "Assembly in C++" by Matt Godbolt — Palestra sobre quando e como usar assembly inline em C/C++, com demonstrações práticas.