Inline Assembly no GCC
Aprendendo a usar o inline Assembly do compilador GCC.
Inline Assembly é uma extensão do compilador que permite inserir código Assembly diretamente no código de saída do compilador. Dessa forma é possível misturar C e Assembly sem a necessidade de usar um módulo separado só para o código em Assembly, além de permitir alguns recursos interessantes que não são possíveis sem o inline Assembly.
O compilador Clang cont√©m uma sintaxe de inline Assembly compat√≠vel com a do GCC, logo o conte√ļdo ensinado aqui tamb√©m √© v√°lido para o Clang.

Inline Assembly b√°sico

A sintaxe do uso b√°sico √©: asm [qualificadores] ( instru√ß√Ķes-asm ).
Onde qualificadores é uma (ou mais) das seguintes palavra-chaves:
  • volatile: Isso desabilita as otimiza√ß√Ķes de c√≥digo no inline Assembly, mas esse j√° √© o padr√£o quando se usa o inline ASM b√°sico.
  • inline: Isso √© uma "dica" para o compilador considerar que o tamanho do c√≥digo Assembly √© o menor poss√≠vel. Serve meramente para o compilador decidir se vai ou n√£o expandir uma fun√ß√£o como inline, e usando esse qualificador voc√™ sugere que o c√≥digo √© pequeno o suficiente para isso.
As instru√ß√Ķes Assembly ficam dentro dos par√™nteses como uma string literal e s√£o despejadas no c√≥digo de sa√≠da sem qualquer altera√ß√£o por parte do compilador. Geralmente se usa \n\t para separar cada instru√ß√£o pois isso vai ser refletido literalmente na sa√≠da de c√≥digo. O \n √© para iniciar uma nova linha e o \t (TAB) √© para manter a indenta√ß√£o do c√≥digo de maneira id√™ntica ao c√≥digo gerado pelo compilador.
Exemplo:
#include <stdio.h>
‚Äč
int main(void)
{
asm(
"mov $5, %eax\n\t"
"add $3, %eax\n\t"
);
‚Äč
return 0;
}
Isso produz a seguinte saída ao visualizar o código de saída:
main:
endbr64
pushq %rbp
movq %rsp, %rbp
#APP
# 5 "main.c" 1
mov $5, %eax
add $3, %eax
# 0 "" 2
#NO_APP
movl $0, %eax
popq %rbp
ret
Entre as diretivas #APP e #NO_APP fica o c√≥digo despejado do inline Assembly. A diretiva # 5 "main.c" 1 √© apenas um atalho para a diretiva #line onde ela serve para avisar para o assembler de qual linha (5) e arquivo ("main.c") veio aquele c√≥digo. Assim se ocorrer algum erro, na mensagem de erro do assembler ser√° exibido essas informa√ß√Ķes.
Repare que o inline Assembly apenas despeja literalmente o conte√ļdo da string literal. Logo voc√™ pode adicionar o que quiser a√≠ incluindo diretivas, coment√°rios ou at√© mesmo instru√ß√Ķes inv√°lidas que o compilador n√£o ir√° reclamar.
Também é possível usar inline Assembly básico fora de uma função, como em:
#include <stdio.h>
‚Äč
int add(int, int);
‚Äč
int main(void)
{
printf("%d\n", add(2, 3));
return 0;
}
‚Äč
asm (
"add:\n\t"
" lea (%edi, %esi), %eax\n\t"
" ret"
);
Porém não é possível fazer o mesmo com inline Assembly estendido.

Inline Assembly estendido

A versão estendida do inline Assembly funciona de maneira semelhante ao inline Assembly básico, porém com a diferença de que é possível acessar variáveis em C e fazer saltos para rótulos no código fonte em C.
A sintaxe da vers√£o estendida segue o seguinte formato:
asm [qualificadores] (
"instru√ß√Ķes-asm"
: operandos-de-saída
: operandos-de-entrada
: clobbers
: rótulos-goto
)
Os qualificadores s√£o os mesmos da vers√£o b√°sica por√©m com mais um chamado goto. O qualificador goto indica que o c√≥digo Assembly pode efetuar um salto para um dos r√≥tulos listados no √ļltimo operando. Esse qualificador √© necess√°rio para se usar os r√≥tulos no c√≥digo ASM. Enquanto o qualificador volatile desabilita a otimiza√ß√£o de c√≥digo, que √© habilitada por padr√£o no inline Assembly estendido.
Dentre esses operandos somente os de saída são "obrigatórios", os demais podem ser omitidos. E todos eles podem conter uma lista vazia exceto o de rótulos.
Existe um limite máximo de 30 operandos com a soma dos operandos de saída, entrada e rótulos.

operandos-de-saída

Cada operando de saída é separado por vírgula e contém a seguinte sintaxe:
[nome] "restri√ß√Ķes" (vari√°vel)
Onde nome é um símbolo opcional que você pode criar para se referir ao operando no código Assembly. Também é possível se referir ao operando usando %n, onde n seria o índice do operando (contando a partir de zero). E usar %[nome] caso defina um nome.
Como o % é usado para se referir à operandos, no inline Assembly estendido se usa dois % para se referir à um registrador. Já que %% é um escape para escrever o próprio % na saída, da mesma forma que se faz na função printf.
‚ÄčAs restri√ß√Ķes √© uma string literal contendo letras e s√≠mbolos indicando como esse operando deve ser armazenado (r para registrador e m para mem√≥ria, por exemplo). No caso dos operandos de sa√≠da o primeiro caractere na string deve ser um = ou +. Onde o = indica que a vari√°vel ter√° seu valor modificado, enquanto + indica que ter√° seu valor modificado e lido.
Operandos de saída com + são contabilizados como dois, tendo em vista que o + é basicamente um atalho para repetir o mesmo operando também como uma entrada.
Essas informa√ß√Ķes s√£o necess√°rias para que o compilador consiga otimizar o c√≥digo corretamente. Por exemplo caso voc√™ indique que a vari√°vel ser√° somente escrita com = mas leia o valor da vari√°vel no Assembly, o compilador pode assumir que o valor da vari√°vel nunca foi lido e portanto descartar a inicializa√ß√£o dela durante a otimiza√ß√£o de c√≥digo. Isso criaria um comportamento estranho no inline Assembly onde se obteria lixo como valor da vari√°vel.
Um exemplo deste erro:
#include <stdio.h>
‚Äč
int main(void)
{
int x = 5;
‚Äč
asm("addl $3, %0"
: "=rm"(x));
‚Äč
printf("%d\n", x);
return 0;
}
A otimização de código pode remover a inicialização x = 5 já que não informamos que o valor dessa variável é lido dentro no inline Assembly. O correto seria usar + nesse caso.
Um exemplo (dessa vez correto) usando um nome definido para o operando:
#include <stdio.h>
‚Äč
int main(void)
{
int x;
‚Äč
asm("movl $5, %[myvar]"
: [myvar] "=rm"(x));
‚Äč
printf("%d\n", x);
return 0;
}
Caso utilize um operando que você não tem certeza que será armazenado em um registrador, lembre-se de usar o sufixo na instrução para especificar o tamanho do operando. Para evitar erros é ideal que sempre use os sufixos.

operandos-de-entrada

Os operandos de entrada seguem a mesma sintaxe dos operandos de sa√≠da por√©m sem o = ou + nas restri√ß√Ķes. N√£o se deve tentar modificar operandos de entrada (embora tecnicamente seja poss√≠vel) para evitar erros, lembre-se que o compilador ir√° otimizar o c√≥digo assumindo que aquele operando n√£o ser√° modificado.
Tamb√©m √© poss√≠vel passar express√Ķes literais como operando de entrada ao inv√©s de somente nomes de vari√°veis. A express√£o ser√° avaliada e seu valor passado como operando sendo armazenado de acordo com as restri√ß√Ķes.

clobbers

Clobbers (que eu não sei como traduzir) é basicamente uma lista, separada por vírgula, de efeitos colaterais do código Assembly. Nele você deve listar o que o seu código ASM modifica além dos operandos de saída. Cada valor de clobber é uma string literal contendo o nome de um registrador que é modificado pelo seu código. Também há dois nomes especiais de clobbers:
Clobber
Descrição
cc
Indica que o código ASM modifica as flags do processador (registrador EFLAGS).
memory
Indica que o código ASM faz leitura ou escrita da/na memória em outro lugar que não seja um dos operandos de entrada ou saída. Por exemplo em uma memória apontada por um ponteiro de um operando.
‚Äč
Esse clobber evita que o compilador assuma que os valores das variáveis na memória permanecem os mesmos após a execução do código ASM. E também garante que o compilador escreva o valor de todas as variáveis na memória antes de executar o inline ASM.
rax
Indica que o registrador RAX ser√° modificado.
rbx
Indica que o registrador RBX ser√° modificado.
etc.
...
Qualquer nome de registrador √© v√°lido para ser usado como clobber exceto o Stack Pointer (RSP). √Č esperado que no final da execu√ß√£o do inline ASM o valor de RSP seja o mesmo de antes da execu√ß√£o do c√≥digo. Se n√£o for o c√≥digo muito provavelmente ir√° ter problemas no restante da execu√ß√£o.
Quando você adiciona um registrador a lista de clobbers ele não será utilizado para armazenar operandos de entrada ou saída, assim garantindo que o registrador pode ser utilizado livremente no inline ASM sem causar qualquer erro. Isso também garante que o compilador não irá assumir que o valor do registrador permanece o mesmo após a execução do inline ASM.
Exemplo:
int add(int a, int b)
{
int result;
‚Äč
asm("movl %[a], %%eax\n\t"
"addl %[b], %%eax\n\t"
"movl %%eax, %[result]"
: [result] "=rm"(result)
: [a] "r"(a),
[b] "r"(b)
: "cc",
"eax");
‚Äč
return result;
}

rótulos-goto

Ao usar asm goto pode-se referir à um rótulo usando o prefixo %l seguido do índice do operando de rótulo. Onde a contagem inicia em zero e é contabilizado também os operandos de entrada e saída.
Exemplo:
#include <stdio.h>
#include <stdbool.h>
‚Äč
int do_anything(bool value)
{
int result = 3;
‚Äč
asm goto("test %[value], %[value]\n\t"
"jz %l1"
:
: [value] "r"(value)
: "cc"
: my_label);
‚Äč
result += 2;
‚Äč
my_label:
return result;
}
‚Äč
int main(void)
{
printf("%d, %d\n", do_anything(true), do_anything(false));
return 0;
}
Mas felizmente também é possível usar o nome do rótulo no inline Assembly, bastando usar a notação %l[nome]. O exemplo acima poderia ter a instrução de salto reescrita para jz %l[my_label].

Restri√ß√Ķes

As restri√ß√Ķes (constraints) s√£o uma lista de caracteres que determinam onde um operando deve ser armazenado. √Č poss√≠vel indicar m√ļltiplas alternativas para o compilador simplesmente adicionando mais de uma letra indicando tipos de armazenamento diferentes.
Abaixo a lista de algumas restri√ß√Ķes dispon√≠veis no GCC.

Restri√ß√Ķes comuns

Restrição
Descrição
m
Operando na memória.
i
Um valor inteiro imediato.
F
Um valor floating-point imediato.
g
Um operando na memória, registrador de propósito geral ou inteiro imediato. Mesmo efeito que usar "rim" como restrição.
p
Um operando que é um endereço de memória válido.
X
Qualquer operando é permitido. Basicamente deixa a decisão nas mãos do compilador.

Restri√ß√Ķes para fam√≠lia x86

Restrição
Descrição
R
Registradores legado. Qualquer um dos oito registradores de propósito geral disponíveis em IA-32.
q
Qualquer registrador que seja possível ler o byte menos significativo. Como RAX (AL) ou R8 (R8B) por exemplo.
Q
Qualquer registrador que seja possível ler o segundo byte menos significativo, como RAX (AH) por exemplo.
a
O registrador "A" (RAX, EAX, AX ou AL).
b
O registrador "B" (RBX, EBX, BX ou BL).
c
O registrador "C" (RCX, ECX, CX ou CL).
d
O registrador "D" (RDX, EDX, DX ou DL).
S
RSI, ESI, SI ou SIL.
D
RDI, EDI, DI ou DIL.
A
O conjunto AX:DX.
t
ST0
u
ST1
y
Qualquer registrador MMX.
x
Qualquer registrador SSE.
Yz
XMM0
I
Um inteiro constante entre 0 e 31, usado para shift com valores de 32-bit.
J
Um inteiro constante entre 0 e 63, usado para shift com valores de 64-bit.
K
Inteiro sinalizado de 8-bit.
N
Inteiro n√£o-sinalizado de 8-bit.

Dicas

Rótulos locais no inline Assembly

Se voc√™ simplesmente declarar r√≥tulos dentro do inline Assembly pode acabar se deparando com uma redeclara√ß√£o de s√≠mbolo por n√£o ter uma garantia de que ele seja √ļnico. Mas uma dica √© usar o escape especial %= que expande para um n√ļmero √ļnico para cada uso de asm, assim sendo poss√≠vel dar um nome √ļnico para os r√≥tulos.
Exemplo:
#include <stdio.h>
#include <stdbool.h>
‚Äč
int do_anything(bool value)
{
int result = 3;
‚Äč
asm("test %[value], %[value]\n\t"
"jz .my_label%=\n\t"
"addl $2, %[result]\n\t"
".my_label%=:"
: [result] "+a"(result)
: [value] "r"(value)
: "cc");
‚Äč
return result;
}
‚Äč
int main(void)
{
printf("%d, %d\n", do_anything(true), do_anything(false));
return 0;
}

Usando sintaxe Intel

Caso prefira usar sintaxe Intel √© poss√≠vel fazer isso meramente compilando o c√≥digo com -masm=intel. Isso porque o inline Assembly simplesmente despeja as instru√ß√Ķes no arquivo de sa√≠da, portanto o c√≥digo ir√° usar a sintaxe que o assembler utilizar.
Outra dica é usar a diretiva .intel_syntax noprefix no início, e depois .att_syntax no final para religar a sintaxe AT&T para o restante do código. Exemplo:
int add(int a, int b)
{
int result;
‚Äč
asm(".intel_syntax noprefix\n\t"
"lea %[result], [ %[a] + %[b] ]\n\t"
".att_syntax"
: [result] "=a"(result)
: [a] "r"(a),
[b] "r"(b));
‚Äč
return result;
}

Escolhendo o registrador/símbolo para uma variável

Ao usar o storage-class register é possível escolher em qual registrador a variável será armazenada usando a seguinte sintaxe:
register int x asm("r12") = 5;
Nesse exemplo a vari√°vel x obrigatoriamente seria alocada no registrador R12.
Também é possível escolher o nome do símbolo para variáveis locais com storage-class static ou para variáveis globais. Como em:
static int x asm("my_var") = 5;
A variável no código fonte é referida como x mas o símbolo gerado para a variável seria definido como my_var.
Export as PDF
Copy link
On this page
Inline Assembly b√°sico
Inline Assembly estendido
operandos-de-saída
operandos-de-entrada
clobbers
rótulos-goto
Restri√ß√Ķes
Restri√ß√Ķes comuns
Restri√ß√Ķes para fam√≠lia x86
Dicas
Rótulos locais no inline Assembly
Usando sintaxe Intel
Escolhendo o registrador/símbolo para uma variável