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.

r

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.

f

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.

Last updated