Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Aprendendo sobre as convenções de chamada usadas no Windows (x64, cdecl e stdcall).
O Windows tem suas próprias convenções de chamadas e o objetivo desse tópico é aprender sobre as três principais que dizem respeito à linguagem C.
Essa é a convenção de chamada padrão usada em x86-64 e portanto é essencial aprendê-la caso vá programar no Windows diretamente em Assembly.
Os registradores RBX, RBP, RDI, RSI, RSP, R12 até R15 e XMM6 até XMM15 devem ser preservados pela função chamada (callee). Caso a função chamada precise alterar o valor de algum desses registradores ela tem a obrigação de preservar o valor anterior e restaurá-lo antes de retornar.
Os demais registradores são considerados voláteis, isto é, podem ter seu valor alterado quando uma chamada de função é efetuada. A função chamada pode modificar o valor dos registradores voláteis livremente.
Os primeiros quatro argumentos inteiros ou ponteiros são passados nos seguintes registradores e na mesma ordem: RCX, RDX, R8 e R9. Os demais argumentos devem ser empilhados na ordem inversa.
Os primeiros quatro argumentos float ou double são passados nos registradores XMM0 até XMM3 como valores escalares. Os demais também são empilhados na ordem inversa.
Structs e unions de 8, 16, 32 ou 64 bits são passados como se fossem inteiros do respectivo tamanho. Se forem de outro tamanho a função chamadora deve então passar um ponteiro para a struct/union que será armazenada em uma memória alocada pela própria função chamadora. Essa memória deve estar em um endereço alinhado por 16 bytes.
A função chamadora (caller) é responsável por alocar um espaço de 32 bytes na pilha chamado de shadow space. Ele é alocado com o intuito de ser usado pela função chamada (callee) para armazenar os parâmetros passados em registradores caso seja necessário, por exemplo caso a função chamada precise usar esses registradores com outro intuito. Esse espaço vem antes mesmo do primeiro parâmetro empilhado.
Exemplo de protótipo de função:
Assim que a função fosse chamada ECX, EDX, R8D e R9D armazenariam os parâmetros a
, b
, c
e d
respectivamente. O parâmetro f
seria empilhado seguido do parâmetro e
.
O 0(%rsp)
seria o endereço de retorno. O espaço entre 8(%rsp)
e 40(%rsp)
é o shadow space. 40(%rsp)
apontaria para o parâmetro e
, enquanto 48(%rsp)
apontaria para o parâmetro f
. Como na demonstração abaixo:
Valores inteiros e ponteiros são retornados em RAX.
Valores float ou double são retornados no registrador XMM0.
O retorno de structs é feito com a função chamadora alocando o espaço de memória necessário para a struct, ela então passa o ponteiro para esse espaço como primeiro argumento para a função em RCX. A função chamada (callee) deve retornar o mesmo ponteiro em RAX.
A convenção de chamada __cdecl
é a convenção padrão usada em código escrito em C na arquitetura IA-32 (x86).
Apenas os registradores EAX, ECX e EDX são considerados voláteis, ou seja, registradores que podem ser modificados livremente pela função chamada. Todos os demais registradores precisam ser preservados e restaurados antes do retorno da função.
Todos os parâmetros são passados na pilha e devem ser empilhados na ordem inversa. A função chamadora (caller) é a responsável por remover os argumentos da pilha após a função retornar.
Exemplo:
Valores inteiros ou ponteiros são retornados em EAX.
Valores float ou double são retornados em ST0.
O retorno de structs ocorre da mesma maneira que na convenção de chamada x64. Com a diferença que o primeiro argumento é, obviamente, passado na pilha.
A convenção de chamada __stdcall
é a utilizada para chamar funções da WinAPI.
Assim como na __cdecl
os registradores EAX, ECX e EDX são voláteis e os demais devem ser preservados pela função chamada.
Todos os argumentos são passados na pilha na ordem inversa. A função chamada (callee) é a responsável por remover os argumentos da pilha. Exemplo:
O retorno de valores funciona da mesma maneira que o retorno de valores da __cdecl
.
Aprendendo a mesclar Assembly e C
Se você leu o conteúdo do livro até aqui já tem uma boa base para entender como o Assembly x86 funciona e como usá-lo. Também já tem uma boa noção do que está fazendo, entende bem o que o assembler faz e o que ele está produzindo como saída, sabe como efetuar cálculos em paralelo usando SSE inclusive com valores de ponto flutuante.
Em outras palavras você já tem a base necessária para realmente entender como as coisas funcionam, não decoramos instruções aqui mas sim entendemos as coisas em seu âmago. Agora está na hora de dar um passo a frente e entender como usar Assembly de uma maneira útil no "mundo real", vamos aprender a usar C e Assembly juntos afim de escrever programas.
Já estamos fazendo isso desde o começo mas não entramos em muitos detalhes pois eu queria que inicialmente o foco fosse em entender como as coisas funcionam, essa é a parte legal .
Como já mencionado antes vamos usar o GCC para compilar nossos códigos em C. Mas diferente dos capítulos anteriores que usamos o NASM, neste aqui vamos usar o assembler GAS com sintaxe da AT&T porque assim aprendemos a ler código nessa sintaxe e a usar o GAS ao mesmo tempo.
Por convenção a gente usa a extensão .s
(ao invés de .asm
) para código ASM com sintaxe da AT&T, então é a extensão que irei usar daqui em diante para nomear os arquivos.
Assim como fizemos em aqui está um código de teste para garantir que o seu ambiente está correto:
O nome do executável do GAS é as e quando você instala o GCC ele vem junto, então você já tem ele instalado aí. Já pode tentar compilar com:
Ao usar o GCC é possível passar o parâmetro -masm=intel
para que o compilador gere código Assembly na sintaxe da Intel, onde por padrão ele gera código na sintaxe da AT&T. Você pode ver o código de saída da seguinte forma:
Onde a flag -S
faz com que o compilador apenas compile o código, sem produzir o arquivo objeto de saída e ao invés disso salvando o código em Assembly. Pode ser útil fazer isso para aprender mais sobre a sintaxe do GAS.
Você também pode habilitar as otimizações do GCC com a opção -O2
assim o código de saída será otimizado. Pode ser interessante fazer isso para aprender alguns truques de otimização.
Aprendendo a sintaxe AT&T e a usar o GAS
O GNU assembler (GAS) usa por padrão a sintaxe AT&T e neste tópico irei ensiná-la. Mais abaixo irei ensinar a diretiva usada para usar sintaxe Intel meramente como curiosidade e caso prefira usá-la.
A primeira diferença notável é que o operando destino nas instruções de sintaxe Intel é o mais à esquerda, o primeiro operando. Já na sintaxe da AT&T é o inverso, o operando mais à direita é o operando destino. Conforme exemplo:
E como já pode ser observado valores literais precisam de um prefixo $
, enquanto os nomes dos registradores precisam do prefixo %
.
Na sintaxe da Intel o tamanho dos operandos é especificado com base em palavra-chaves que são adicionadas anteriormente ao operando. Na sintaxe AT&T o tamanho do operando é especificado por um sufixo adicionado a instrução, conforme tabela abaixo:
Exemplos:
Assim como o NASM consegue identificar o tamanho do operando quando é usado um registrador e a palavra-chave se torna opcional, o mesmo acontece no GAS e o sufixo também é opcional nesses casos.
Na sintaxe Intel saltos e chamadas distantes são feitas com jmp far [etc]
e call far [etc]
respectivamente. Na sintaxe da AT&T se usa o prefixo L nessas instruções, ficando: ljmp
e lcall
.
Exemplos com o seu equivalente na sintaxe da Intel:
Como demonstrado no último exemplo o endereço relativo na sintaxe do GAS é feito explicitando RIP como base, enquanto na sintaxe do NASM isso é feito usando a palavra-chave rel
.
Na sintaxe da AT&T os saltos para endereços armazenados na memória devem ter um *
antes do rótulo para indicar que o salto deve ocorrer para o endereço que está armazenado naquele endereço de memória. Sem o *
o salto ocorre para o rótulo em si. Exemplo:
Saltos que especificam segmento e offset separam os dois valores por vírgula. Como em:
As diretivas do GAS funcionam de maneira semelhante as diretivas do NASM com a diferença que todas elas são prefixadas por um ponto.
No GAS comentários de múltiplas linhas podem ser escritos com /*
e */
assim como em C. Comentários de uma única linha podem ser escritos com #
ou //
.
Exemplos:
O GAS tem diretivas específicas para declarar algumas seções padrão. Conforme tabela:
Porém ele também tem a diretiva .section
que pode ser usada de maneira semelhante a section
do NASM. Os atributos da seção porém são passados em formato de flags em uma string como segundo argumento. As flags principais são w
para dar permissão de escrita e x
para dar permissão de execução. Exemplos:
A diretiva .align
pode ser usada para alinhamento dos dados. Você pode usá-la no início da seção para alinhar a mesma, conforme exemplo:
A diretiva .intel_syntax
pode ser usada para habilitar a sintaxe da Intel para o GAS. Opcionalmente pode-se passar um parâmetro noprefix
para desabilitar o prefixo %
dos registradores.
Uma diferença importante da sintaxe Intel do GAS em relação ao NASM é que as palavra-chaves que especificam o tamanho do operando precisam ser seguidas por ptr
, conforme exemplo abaixo:
Aprendendo sobre a convenção de chamada do C usada no Linux.
Sistemas , incluindo o Linux, seguem a padronização da System V ABI (ou SysV ABI). Onde ABI é sigla para Application Binary Interface (Interface binária de aplicação) que é basicamente uma padronização que dita como código binário deve ser escrito e executado no sistema operacional. Uma das coisas que a SysV ABI padroniza é a convenção de chamada utilizada em cada arquitetura de processador.
Neste tópico vamos aprender sobre a convenção de chamada da SysV ABI e o tamanho dos tipos de dados usados na linguagem C.
Os registradores RBP, RBX, RSP e R12 até R15 são considerados como pertencentes a função chamadora. Isto é, se a função que foi chamada precisar modificar esses registradores ela obrigatoriamente precisa preservar seus valores e antes de retornar restaurá-los para o valor anterior. Todos os outros registradores podem ser modificados livremente pela função chamada. Portanto não espere que esses outros registradores tenham seu valor preservado ao chamar uma função.
A Direction Flag (DF) no precisa obrigatoriamente estar zerada ao chamar ou retornar de uma função.
Cada função chamada pode (se precisar) reservar um pedaço para ser usada como memória local da função e pode, por exemplo, ser usada para alocar variáveis locais. Esse espaço é chamado de stack frame e o código que aloca e desaloca o stack frame é chamado de prólogo e epílogo respectivamente. Exemplo:
O espaço de 128 bytes antes do endereço apontado por RSP é uma região chamada de redzone que por convenção pode ser usada por funções folha (leaf), que são funções que não chamam outras funções. Ou então pode ser usada em qualquer função onde o valor não precise ser preservado após chamar outra função.
O endereço entre -128(%rsp)
e -1(%rsp)
pode ser usado livremente sem a necessidade de alocar um stack frame.
Caso a função precise de mais argumentos e os registradores acabem, os demais argumentos serão empilhados na ordem inversa. Por exemplo caso uma função precise de 9 argumentos inteiros eles seriam definidos na seguinte ordem pela função chamadora:
Assim que a função fosse chamada 8(%rsp)
, 16(%rsp)
e 24(%rsp)
apontariam para os argumentos 7, 8 e 9 respectivamente.
A função chamadora (caller) precisa garantir que o último valor empilhado esteja em um endereço alinhado por 16 bytes.
A função chamadora é a responsável por remover os argumentos empilhados da pilha.
No caso do retorno de estruturas (structs) a função chamadora precisa alocar o espaço necessário para a struct e passar o endereço do espaço no registrador RDI como se fosse o primeiro argumento para a função (os outros argumentos usam RSI em diante). A função então precisa retornar o mesmo endereço passado por RDI em RAX.
O retorno de valores inteiros e ponteiros é feito no registrador RAX.
Valores float ou double são retornados no registrador XMM0 na parte menos significativa.
Os registradores EBX, EBP, ESI, EDI e ESP precisam ter seus valores preservados pela função chamada. Os demais registradores de propósito geral podem ser usados livremente.
A Direction Flag (DF) no EFLAGS precisa obrigatoriamente estar zerada ao chamar ou retornar de uma função.
O stack frame em IA-32 funciona da mesma maneira que o stack frame em x86-64, com a diferença de que não existe redzone em IA-32 e toda função que precisar de memória local precisa obrigatoriamente construir um stack frame.
Vale lembrar que cada valor inserido na stack em IA-32 tem 4 bytes de tamanho, enquanto em x86-64 cada valor tem 8 bytes de tamanho.
Os argumentos da função são empilhados na ordem inversa, assim como ocorre em x86-64 quando os registradores acabam. Conforme exemplo:
Assim que a função é chamada 4(%esp)
, 8(%esp)
, 12(%esp)
e 16(%esp)
apontam para os argumentos 1, 2, 3 e 4 respectivamente.
A função chamadora precisa garantir que o último valor empilhado esteja em um endereço alinhado por 16 bytes.
A função chamadora é a responsável por remover os argumentos empilhados da pilha.
Retorno de struct é feito de maneira semelhante do x86-64. Um ponteiro para a região de memória para gravar os dados da struct é passado como primeiro argumento para a função (o último valor a ser empilhado). É obrigação da função chamada fazer o pop desse ponteiro e retorná-lo em EAX.
Valores inteiros e ponteiros são retornados em EAX.
Existe uma convenção de escrita do prólogo e do epílogo da função que se trata de preservar o antigo valor de ESP/RSP no registrador EBP/RBP, e depois subtrair ESP/RSP para alocar o stack frame. Conforme exemplo:
Também existe a instrução leave
que pode ser usada no epílogo. Ela basicamente faz a operação de mov %rbp, %rsp
e pop %rbp
em uma única instrução (também pode ser usada em 32 e 16 bits atuando com EBP/ESP e BP/SP respectivamente).
Mas como já foi demonstrado em um exemplo mais acima isso não é obrigatório e podemos apenas incrementar e subtrair ESP/RSP no prólogo e no epílogo. Código otimizado gerado pelo GCC costuma apenas fazer isso, já código com a otimização desligada costuma gerar o prólogo e epílogo "clássico".
A tabela abaixo lista os principais tipos da linguagem C e seu tamanho em bytes no IA-32 e x86-64. Como também exibe em qual registrador o tipo deve ser retornado.
*No registrador EDX é armazenado os 32 bits mais significativos e em EAX os 32 bits menos significativos.
**O tipo long double
ocupa na memória o espaço de 12 e 16 bytes por motivos de alinhamento, mas na verdade se trata de um float de 80 bits (10 bytes).
Caso tenha algum problema e precise de ajuda, pode entrar no e fazer uma pergunta.
A flag -fno-asynchronous-unwind-tables
serve para desabilitar as e melhorar a leitura do código de saída. Essas diretivas servem para gerar informação útil para um depurador mas para fins de leitura do código não precisamos delas.
Na sintaxe Intel é bem intuitivo já que ele é escrito em formato de expressão matemática. Na sintaxe AT&T é um pouco mais confuso e segue o seguinte formato:
segment:displacement(base, index, scale)
.
No NASM db
, dw
, dd
, dq
etc. que servem para despejar bytes no arquivo binário de saída. No GAS isso é feito usando as seguintes pseudo-instruções:
O exemplo abaixo é o mesmo apresentado no tópico sobre porém reescrito na sintaxe do GAS/AT&T:
Vale lembrar que empilha o endereço de retorno, portanto ao chamar uma função 0(%rsp)
aponta para o endereço de retorno da mesma.
Os parâmetros inteiros (e ponteiros) são passados em na seguinte ordem: RDI, RSI, RDX, RCX, R8 e R9. Parâmetros float ou double são passados nos registradores XMM0 até XMM7 como (na parte menos significativa do registrador).
Valores float ou double são retornados em ST0 (ver ).
Pseudo-instrução | Tipo do dado (tamanho em bits) | Equivalente no NASM |
.byte | byte (8 bits) | db |
.short .hword .word | word (16 bits) | dw |
.long .int | doubleword (32 bits) | dd |
.quad | quadword (64 bits) | dq |
.float .single | Single-precision floating-point (32 bits) | dd |
.double | Double-precision floating-point (64 bits) | dq |
.ascii .string .string8 | String (8 bits cada caractere) | db |
.asciz | Mesmo que .ascii porém com um terminador nulo no final | - |
.string16 | String (16 bits cada caractere) | - |
.string32 | String (32 bits cada caractere) | - |
.string64 | String (64 bits cada caractere) | - |
GAS | Equivalente no NASM |
.data | section .data |
.bss | section .bss |
.text | section .text |
Tipo | Tamanho IA-32 | Tamanho x86-64 | Registrador de retorno IA-32 | Registrador de retorno x86-64 |
_Bool char signed char unsigned char | 1 | 1 | AL | AL |
short signed short unsigned short | 2 | 2 | AX | AX |
int signed int unsigned int long signed long unsigned long enum | 4 | 4 | EAX | EAX |
long long signed long long unsigned long long | 8 | 8 | *EDX:EAX | RAX |
Ponteiros | 4 | 8 | EAX | RAX |
float | 4 | 4 | ST0 | XMM0 |
double | 8 | 8 | ST0 | XMM0 |
**long double | 12 | 16 | ST0 | ST0 |
Sufixo | Tamanho | Palavra-chave equivalente no NASM |
B | byte (8 bits) | byte |
W | word (16 bits) | word |
L | long/doubleword (32 bits) | dword |
Q | quadword (64 bits) | qword |
T | ten word (80 bits) | tword |
Entendendo a execução de código em C no ambiente hosted.
Na especificação da linguagem C é descrito dois ambientes de execução de código: Os ambientes hosted e freestanding. Neste tópico vamos entender alguns pontos em relação a como funciona a estrutura e a execução de um programa em C no ambiente hosted.
O ambiente hosted essencialmente é o ambiente de execução de um código em C que executa sobre um sistema operacional. Nesse ambiente é esperado que haja suporte para múltiplas threads e todos os recursos descritos na especificação da biblioteca padrão (libc). A inicialização do programa ocorre quando a função main é chamada e antes de inicializar o programa é esperado que todos os objetos com storage-class static
estejam inicializados.
A função main pode ser escrita com um dos dois protótipos abaixo:
Ou qualquer outro protótipo que seja equivalente a um desses. Como por exemplo char **argv
também seria válido por ter equivalência a char *argv[]
. Também pode-se usar qualquer nome de parâmetro, argc
e argv
são apenas sugestões.
O primeiro parâmetro passado para a função main indica o número de argumentos e o segundo é uma array de ponteiros para char
onde cada índice na array é um argumento e argv[argc]
é um ponteiro NULL.
Se o tipo de retorno da função main for int
(ou equivalente), o valor de retorno da primeira chamada para main é equivalente a chamar a função exit passando esse valor como argumento.
Os detalhes de implementação descritos aqui são baseados no código-fonte da glibc e podem ser diferentes em outras implementações da libc. Consulte as referências para ver a lista de completa de arquivos fonte consultados.
O código na glibc responsável pela inicialização do programa é chamado de C startup (CSU). Ele se encarrega de obter os argumentos de linha de comando, inicializar o TLS, executar o código na seção .init
dentre outras tarefas de inicialização do programa.
O arquivo start.S
é o que declara o símbolo _start
, ou seja, a função de entry point do programa. A última chamada nessa função é para outra função chamada __libc_start_main
que recebe o endereço da função main como primeiro argumento. Depois de algumas inicializações essa função chama a main, obtém o valor retornado em EAX e passa como argumento para a função responsável por finalizar o programa no sistema operacional (exit_group
no Linux e ExitProcess
no Windows).
Todos esses códigos estão em arquivos objetos pré-compilados no seu sistema operacional. Eles são linkados por padrão quando você invoca o GCC mas não são linkados por padrão se você chamar o linker (ld
) diretamente.
No meu Linux o arquivo objeto Scrt1.o
("crt" é sigla para "C runtime") é o que contém o entry point (código do start.S
). Os arquivos crti.o
e crtn.o
contém o prólogo e o epílogo, respectivamente, para as seções .init
e .fini
.
No meu Linux esses arquivos estão na pasta /usr/lib/x86_64-linux-gnu/
e sugiro que consulte o conteúdo dos mesmos com a ferramenta objdump, como por exemplo:
Apenas para fins de curiosidade e dar uma noção mais "palpável" de como isso ocorre, irei ensinar aqui como você pode desabilitar a linkedição do CSU e programar uma versão personalizada do mesmo no Linux. Não recomendo que isso seja feito em um programa de verdade tendo em vista que você perderá diversos recursos que o C runtime padrão da glibc provém.
Use o seguinte código de teste:
Compile com:
A opção -nostartfiles
desabilita a linkedição dos arquivos objeto de inicialização.
O que o nosso start.s
está fazendo é simplesmente chamar a syscall write
para escrever uma mensagem na tela, chama a função main passando argc
e argv
como argumentos e depois chama a syscall exit_group
passando como argumento o retorno da função main.
No Linux, logo quando o programa é iniciado no entry point, o valor contendo o número de argumentos de linha de comando (argc) está em (%rsp)
. E logo em seguida (RSP+8) está o início da array de ponteiros para os argumentos de linha de comando, terminando com um ponteiro NULL.
Experimente rodar objdump -d test
nesse executável "customizado" e depois compare compilando com o CSU comum. Verá que o programa comum contém diversas funções que foram linkadas nele.
As seções .init
e .fini
contém funções construída nos arquivos crti.o
e crtn.o
.
O propósito da função em .init
é chamar todas as funções na array de ponteiros localizada em outra seção chamada .init_array
. Essas funções são invocadas antes da chamada para a função main.
Já a função em .fini
invoca as funções da array na seção .fini_array
na finalização do programa (após main retornar ou na chamada de exit()
).
No GCC você pode adicionar funções para serem invocadas na inicialização do programa com o atributo constructor
, e para a finalização do programa com o atributo destructor
. Experimente ver o código Assembly do exemplo abaixo:
Ao ver o Assembly gerado do programa acima irá notar que os endereços das funções são despejados nas seções .init_array
e .fini_array
, como em:
Quando a função exit()
é invocada (ou main retorna), funções registradas pela função atexit()
são executadas. Onde as funções registradas devem seguir o protótipo:
As funções registradas por atexit()
são invocadas na ordem inversa a que foram registradas.
Quando a função quick_exit()
é invocada o programa é finalizado sem invocar as funções registradas por atexit()
e sem executar quaisquer handlers de sinal.
As funções registradas por at_quick_exit
são invocadas na ordem inversa em que foram registradas.
Exemplo:
Experimente executar o programa acima e depois recompilar com a chamada para quick_exit na linha 20.
A quantidade máxima de funções que podem ser registradas com atexit ou at_quick_exit depende da implementação. Mas a especificação do C11 garante que no mínimo 32 funções podem ser registradas por cada uma destas funções.
A função _Exit()
finaliza a execução do programa sem executar quaisquer funções registradas por atexit ou at_quick_exit. Também não executa nenhum handler de sinal.
Entendendo a execução de código em C no ambiente freestanding.
O ambiente de execução freestanding é normalmente usado quando o código C é compilado para executar fora de um sistema operacional. Nesse ambiente nenhum dos recursos provindos do ambiente hosted são garantidos e sua existência ou não depende da implementação.
Os únicos recursos que são oferecidos pela libc são os declarados nos seguintes header files:
<float.h>, <iso646.h>, <limits.h>, <stdalign.h>, <stdarg.h>, <stdbool.h>, <stddef.h>, <stdint.h> e <stdnoreturn.h>.
Quaisquer outros recursos são dependentes de implementação.
No GCC para compilar um código visando o ambiente freestanding é possível usar a opção -ffreestanding
. Também se pode usar a opção -fhosted
para compilar para ambiente hosted mas esse já é o padrão.
Já a opção -nostdlib
desabilita a linkedição da libc.
Entendendo como variáveis em C são representadas em Assembly.
Como já vimos no capítulo A base, variáveis nada mais são do que um espaço de memória que pode ser manipulado pelo programa. Em C existem diversas nuances em como variáveis são alocadas e mantidas pelo compilador e aqui vamos entender essas diferenças.
Na linguagem C existem palavra-chaves que são chamadas de storage-class specifiers, onde elas determinam o storage-class de uma variável. Na prática isso determina como a variável deve ser armazenada no programa. No C11 existem os seguintes storage-class specifiers:
extern
static
_Thread_local
auto (esse é o padrão)
register
As variáveis globais em C são alocadas na seção .data
ou .bss
, dependendo se ela foi inicializada ou não. Como no exemplo:
Se compilamos com gcc main.c -S -o main.s -fno-asynchronous-unwind-tables
obtemos a seguinte saída:
A variável data_var
foi alocada na seção .data
e teve seu símbolo exportado com a diretiva .globl data_var
, que é equivalente a diretiva global
do NASM.
Já a variável bss_var
foi declarada com a diretiva .comm symbol, size, aligment
que serve para declarar commom symbols (símbolos comuns). Onde ela recebe como argumento o nome do símbolo seguido de seu tamanho (em bytes) e opcionalmente um valor de alinhamento. Em arquivos objetos ELF o argumento de alinhamento é um alinhamento em bytes, nesse exemplo a variável será alocada em um endereço alinhado por 4 bytes.
Os símbolos declarados com a diretiva .comm
que não foram inicializados em qualquer arquivo objeto são alocados na seção .bss
. Logo nesse caso o uso da diretiva seria equivalente ao uso de res*
do NASM, com a diferença que no NASM precisamos usar explicitamente na seção onde o espaço será alocado.
As variáveis globais com storage-class static
funcionam da mesma maneira que as variáveis globais comum, com a diferença que seu símbolo não é exportado para que possa ser acessado em outro arquivo objeto. Como no exemplo:
Onde obtemos a saída:
Repare que dessa vez o símbolo data_var
não foi exportado com a diretiva .globl
, enquanto o bss_var
foi explicitamente declarado como local com a diretiva .local
(já que a diretiva .comm
exporta como global por padrão).
Variáveis extern
em C são basicamente variáveis que são definidas em outro módulo. O GAS tem uma diretiva .extern
que é equivalente a diretiva extern
do NASM, isto é, indica que o símbolo será definido em outro arquivo objeto. Porém qualquer símbolo não declarado já é considerado externo por padrão pelo GAS. Experimente ver o código de saída do exemplo abaixo:
Você vai reparar que na função main
o símbolo extern_var
foi lido porém ele não foi declarado.
Variáveis locais em C são comumente alocadas no stack frame da função, porém em alguns casos o compilador também pode reservar um registrador para armazenar o valor da variável.
Em C existe o storage-class register
que serve como um "pedido" para o compilador alocar aquela variável de forma que o acesso a mesma seja o mais rápido possível, que geralmente é em um registrador (daí o nome da palavra-chave). Mas isso não garante que a variável será realmente alocada em um registrador. Na prática o único efeito colateral garantido é que você não poderá obter o endereço na memória daquela variável com o operador de endereço (&
), e muitas vezes o compilador já vai alocar a variável em um registrador mesmo sem o uso da palavra-chave.
Variáveis static
local são armazenadas da mesma maneira que as variáveis static
global, a única coisa que muda é no ponto de vista do código-fonte em C onde a visibilidade da variável é limitada para o escopo onde ela foi declarada. Isso faz com o que o compilador gere um símbolo de nome único para a variável, como no exemplo abaixo:
Repare como data_var.1913
não teve seu símbolo exportado e bss_var.1914
foi declarado como local.
O storage-class _Thread_local
foi adicionado no C11. Assim como o nome sugere ele serve para alocar variáveis em uma região de memória que é local para cada thread do processo. Vamos analisar o exemplo:
No Linux, em x86-64, a região de memória local para cada thread (thread-local storage - TLS) fica no segmento apontado pelo registrador de segmento FS, por isso os valores das variáveis estão sendo lidos desse segmento.
Repare que as seções são diferentes, .tdata
(equivalente a .data
só que thread-local) e .tbss
(equivalente a .bss
) são utilizadas para armazenar as variáveis.
O sufixo @tpoff
(thread pointer offset) usado nos símbolos indica que o offset do símbolo deve ser calculado levando em consideração a TLS como endereço de origem. Por padrão o offset é calculado com o segmento de dados "normal" como origem.
Agora que já entendemos onde e como as variáveis são alocadas em C, só falta entender "o que" está sendo armazenado.
O tipo array em C é meramente uma sequência de variáveis do mesmo tipo na memória. Por exemplo podemos inicializar um int arr[4]
na sintaxe do GAS da seguinte forma:
Onde os valores 1
, 2
, 3
e 4
são despejados em sequência.
Em C não existe um tipo string porém por convenção as strings são uma array de char
, onde o último char
contém o valor zero (chamado de terminador nulo). Esse último caractere '\0'
é usado para denotar o final da string e funções da libc que lidam com strings esperam por isso. Exemplos:
As três strings acima são equivalentes na sintaxe do GAS.
Sobre a passagem de arrays (incluindo obviamente strings) como argumento para uma função, isso é feito passando um ponteiro para o primeiro elemento da array.
Ponteiros em C, na arquitetura x86/x86-64, são traduzidos meramente como o offset do objeto na memória. O segmento não é especificado como parte do valor do ponteiro.
Experimente ler o código de saída do seguinte programa:
A leitura do endereço de my_var
vai ser compilada para algo como:
Onde primeiro é obtido o endereço do início do segmento FS que depois é somado ao offset de my_var
. Assim obtendo o endereço efetivo da variável na memória.
As estruturas em C são compiladas de forma que os valores dos campos da estrutura são dispostos em sequência na memória, seguindo a mesma ordem que foram declarados na estrutura. Existe a possibilidade do GCC adicionar alguns bytes extras no final da estrutura afim de manter o alinhamento dos dados, esses bytes extras são chamados de padding. Exemplo:
Isso produziria o seguinte código para a inicialização da variável test
:
Repare a diretiva .zero 3
que foi usada para despejar 3 bytes zero no final da estrutura, afim de alinhar a mesma em 4 bytes. No total a estrutura acaba tendo 8 bytes de tamanho: 4 bytes do int
, 1 byte do char
e 3 bytes de padding.
As unions são bem simples, são alocadas com o tamanho do maior tipo declarado para a union. Por exemplo:
Essa union é alocada na memória da mesma forma que um int
, que tem 4 bytes de tamanho.
Entendendo as funções em C do ponto de vista do Assembly.
A linguagem C tem algumas variações à respeito de funções e o objetivo deste tópico é explicar, do ponto de vista do baixo-nível, como elas funcionam.
As funções na linguagem C têm protótipos que servem como uma "assinatura" indicando quais parâmetros a função recebe e qual tipo de valor ela retorna. Um exemplo:
Esse protótipo já nos dá todas as informações necessárias que saibamos como fazer a chamada da função e como obter seu valor de retorno, desde que nós conheçamos a convenção de chamada utilizada. Os parâmetros são considerados da esquerda para a direita, logo o parâmetro x
é o primeiro e o parâmetro y
é o segundo. Na convenção de chamada da SysV ABI esses argumentos estariam em EDI e ESI, respectivamente. E o retorno seria feito em EAX.
Existem alguns protótipos um pouco diferentes que vale explicar aqui para deixar claro seu entendimento. Como este:
De acordo com a especificação do C11 uma expressão do tipo void
é um tipo cujo o valor não existe e deve ser ignorado. Funções assim são compiladas retornando sem se preocupar em modificar o valor de RAX (ou qualquer outro registrador que poderia ser usado para retornar um valor) e portanto não se deve esperar que o valor nesse registrador tenha alguma informação útil.
Quando void
é usado no lugar da lista de parâmetros ele tem o significado especial de indicar que aquela função não recebe parâmetro algum ao ser chamada.
Embora possa ser facilmente confundido com o caso acima, onde se usa void
na lista de parâmetros, na verdade esse protótipo de função não diz que a função não recebe parâmetros. Na verdade esse é um protótipo que não especifica quais tipos ou quantos parâmetros a função recebe, logo o compilador aceita que a função seja chamada passando qualquer tipo e qualquer quantidade de parâmetros, inclusive sem parâmetro algum também. Veja o exemplo:
Na convenção de chamada da SysV ABI os argumentos para esse tipo de função são passados da mesma maneira que uma chamada com o protótipo "normal". A única diferença é que a função recebe um argumento extra no registrador AL indicando quantos registradores de vetor foram utilizados para passar argumentos de ponto-flutuante. Nesse exemplo apenas um argumento era um float e por isso há a instrução movl $1, %eax
indicando esse número. Experimente usar mais argumentos float ou não passar nenhum para ver se o número passado em AL como argumento irá mudar de acordo.
Funções com argumentos variáveis também seguem a mesma regra de chamada do que foi mencionado acima.
Funções static são visíveis apenas no mesmo módulo em que elas foram declaradas, ou seja, seu símbolo não é exportado. Exemplo:
Existem dois especificadores de função no C11, onde eles são:
O especificador inline
é uma sugestão para que a chamada para a função seja a mais rápida possível. Isso tem o efeito colateral no GCC de inibir a geração de código para a função em Assembly. Ao invés disso as instruções da função são geradas no local onde ela foi chamada, e portanto o símbolo da função nunca é de fato declarado.
O GCC, mesmo para uma função inline, ainda vai gerar o código para a chamada da função caso as otimizações estejam desligadas e isso vai acabar produzindo um erro de referência por parte do linker. Lembre-se de sempre ligar as otimizações de código quando estiver usando funções inline.
Funções com o especificador _Noreturn
nunca devem retornar para a função chamadora. Quando esse especificador é utilizado o compilador irá gerar código assumindo que a função nunca retorna. Como podemos ver no exemplo abaixo compilado com -O2
:
Nested functions é uma extensão do GCC que permite declarar funções aninhadas. O símbolo de uma função aninhada é gerado de maneira semelhante ao símbolo de uma variável local com storage-class static
. Exemplo:
Os atributos de função é uma extensão do GCC que permite modificar algumas propriedades relacionadas à uma função. Se define atributos para uma função usando a palavra-chave __attribute__
e entre dois parênteses uma lista de atributos separado por vírgula. Exemplo:
Alguns atributos recebem parâmetros onde estes devem ser adicionados dentro de mais um par de parênteses, se assemelhando a sintaxe de uma chamada de função. Exemplo: __attribute__((section (".another"), cdecl))
.
Abaixo alguns atributos que podem ser usados na arquitetura x86 e acho interessante citar aqui:
Esses atributos fazem com que o compilador gere o código da função usando a convenção de chamada ms_abi, sysv_abi, cdecl, stdcall, fastcall ou thiscall respectivamente. Também é útil usá-los em protótipos de funções onde a função utiliza uma convenção de chamada diferente da padrão.
Os atributos cdecl
, stdcall
, fastcall
e thiscall
são ignorados em 64-bit.
Por padrão o GCC irá adicionar o código das funções na seção .text
, porém é possível usar o atributo section
para que o compilador adicione o código da função em outra seção. Como no exemplo abaixo:
O atributo naked
é usado para desativar a geração do prólogo e epílogo para a função. Isso é útil para se escrever funções usando inline Assembly dentro das mesmas.
Esse atributo serve para personalizar a geração de código do compilador para uma função específica, permitindo selecionar quais instruções serão utilizadas ao gerar o código. Também é possível adicionar o prefixo no-
para desabilitar alguma tecnologia e impedir que o compilador gere código para ela. Por exemplo __attribute__((target ("no-sse"))
desativaria o uso de instruções ou registradores SSE na função.
Alguns dos possíveis alvos para arquitetura x86 são:
Já vimos alguns exemplos de código chamando funções da libc, essas funções porém estão em uma biblioteca dinâmica e não dentro do executável. A resolução do endereço (symbol binding) das funções na biblioteca é feito em tempo de execução onde os endereços são salvos na seção GOT (Global Offset Table).
A seção PLT (Procedure Linkage Table) simplesmente armazena saltos para os endereços armazenados na GOT. Por isso o GCC gera chamadas para funções da libc assim:
O sufixo @PLT
indica que o endereço do símbolo está na seção PLT. Onde nessa seção há uma instrução jmp
para o endereço que será resolvido em tempo de execução na GOT. Algo parecido com a ilustração abaixo:
Na sintaxe do NASM o equivalente ao uso do sufixo com @
do GAS é a palavra-chave wrt
(With Reference To), conforme exemplo:
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.
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 , 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:
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:
Porém não é possível fazer o mesmo com 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:
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.
Cada operando de saída é separado por vírgula e contém a seguinte sintaxe:
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.
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:
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:
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 (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:
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:
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:
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]
.
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.
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:
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:
Ao usar o storage-class register
é possível escolher em qual registrador a variável será armazenada usando a seguinte sintaxe:
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:
A variável no código fonte é referida como x
mas o símbolo gerado para a variável seria definido como my_var
.
Aprendendo sobre as instruções intrínsecas na arquitetura x86-64
As instruções intrínsecas é um recurso originalmente fornecido pelo compilador Intel C/C++ mas que também é implementado pelo GCC. Se tratam basicamente de tipos especiais e funções que são expandidas inline para alguma instrução do processador, ou seja, é basicamente uma alternativa mais prática e legível do que usar inline Assembly para tudo.
Usando instruções intrínsecas é possível obter o mesmo resultado de usar inline Assembly com a diferença de ter a sintaxe amigável de uma chamada de função.
Para usar instruções intrínsecas é necessário incluir o header <immintrin.h> onde ele declara as funções e os tipos.
Para entender apropriadamente as operações e tipos indicados aqui, sugiro que já tenha lido o tópico sobre .
Os tipos de dados na tabela abaixo servem para indicar como os valores usados na instrução intrínseca serão armazenados.
A maioria das instruções intrínsecas (SIMD) seguem a seguinte convenção de notação:
Onde <operação> é a operação que será executada com os dados. O <sufixo> indica o tipo de dado na operação. A primeira ou as duas primeiras letras do sufixo indicam se o dado é packed (p), extended packed (ep) ou escalar (s). Os demais caracteres do sufixo indicam o tipo de dado, como mostra a tabela abaixo:
Exemplo:
Abaixo irei listar apenas algumas instruções intrínsecas, em sua maioria relacionadas à tecnologia SSE. Para ver a lista completa sugiro que consulte a referência oficial da Intel no link abaixo:
Algumas instruções intrínsecas não são compiladas para uma só instrução mas sim uma sequência de várias delas.
As operações load carregam um valor da memória para um registrador, o conteúdo que deve estar na memória apontada pelo argumento tem que estar de acordo com o tipo da instrução identificado pelo sufixo.
Operações store leem um ou mais dados do registrador e escrevem os mesmos no endereço passado como primeiro argumento.
Já a operação extract obtém um valor de uma parte do registrador identificado pelo valor imediato passado como segundo argumento. Esse valor é o índice do campo do registrador contando da direita para a esquerda começando em zero.
Exemplos:
As instruções intrínsecas de set definem o valor de todos os campos do registrador ao mesmo tempo sem a necessidade de usar uma array para isso. Elas não são traduzidas para uma mas sim várias instruções em sequência, portanto pode haver uma penalidade de desempenho.
As operações set escalares (_mm_set_sd
e_mm_set_ss
) definem o valor da parte menos significativa do registrador e zeram os demais valores.
As duas operações abaixo definem todos os campos do registrador para o mesmo valor passado como argumento:
Exemplo:
Exemplos:
As instruções intrínsecas abaixo leem um valor aleatório gerado por hardware:
A instrução rdrand
escreve o valor aleatório obtido no ponteiro passado como argumento. Ela deve ser usada em um loop pois não há garantia de que ela irá obter de fato um valor. Se obter o valor a função retorna 1
, caso contrário retorna 0
.
Exemplo:
Você deve compilar passando a flag -mrdrnd
para o GCC para indicar que o processador suporta a tecnologia. Caso contrário você obterá um erro como este:
error: inlining failed in call to always_inline ‘_rdrand32_step’: target specific option mismatch
As instruções intrínsecas abaixo são utilizadas da mesma maneira que rdrand porém o valor aleatório não é gerado por hardware.
É necessário compilar com a flag -mrdseed
para poder usar essas instruções intrínsecas.
Já em arquivos objetos PE (do Windows) o alinhamento é um valor em potência de dois, logo para alinhar em 4 bytes deveríamos passar 2 como argumento ( ). Se a gente passar 4 como argumento então seria um alinhamento de que daria um alinhamento de 16 bytes.
Isso produz a seguinte saída ao :
é 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.
Caso utilize um operando que você não tem certeza que será armazenado em um registrador, lembre-se de usar para especificar o tamanho do operando. Para evitar erros é ideal que sempre use os sufixos.
Ativar as instruções
Desativar as instruções
3dnow
no-3dnow
3dnowa
no-3dnowa
abm
no-abm
adx
no-adx
aes
no-aes
avx
no-avx
avx2
no-avx2
avx5124fmaps
no-avx5124fmaps
avx5124vnniw
no-avx5124vnniw
avx512bitalg
no-avx512bitalg
avx512bw
no-avx512bw
avx512cd
no-avx512cd
avx512dq
no-avx512dq
avx512er
no-avx512er
avx512f
no-avx512f
avx512ifma
no-avx512ifma
avx512pf
no-avx512pf
avx512vbmi
no-avx512vbmi
avx512vbmi2
no-avx512vbmi2
avx512vl
no-avx512vl
avx512vnni
no-avx512vnni
avx512vpopcntdq
no-avx512vpopcntdq
mmx
no-mmx
sse
no-sse
sse2
no-sse2
sse3
no-sse3
sse4
no-sse4
sse4.1
no-sse4.1
sse4.2
no-sse4.2
sse4a
no-sse4a
ssse3
no-ssse3
Sufixo | Tipo |
| single-precision floating-point (float de 32-bit) |
| double-precision floating-point (double de 64-bit) |
| Inteiro sinalizado de 128-bit. |
| Inteiro sinalizado de 64-bit. |
| Inteiro não-sinalizado de 64-bit. |
| Inteiro sinalizado de 32-bit. |
| Inteiro não-sinalizado de 32-bit. |
| Inteiro sinalizado de 16-bit. |
| Inteiro não-sinalizado de 16-bit. |
| Inteiro sinalizado de 8-bit. |
| Inteiro não-sinalizado de 8-bit. |
Tecnologia | Protótipo | Instrução |
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE |
|
|
SSE |
| Sequência |
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE |
|
|
SSE4.1 |
|
|
SSE |
|
|
Tecnologia | Protótipo |
SSE2 |
|
SSE2 |
|
SSE2 |
|
SSE2 |
|
SSE2 |
|
SSE2 |
|
SSE |
|
SSE2 |
|
SSE |
|
Tecnologia | Protótipo |
SSE2 |
|
SSE |
|
Tecnologia | Protótipo | Instrução |
SSE2 |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE |
|
|
SSSE3 |
|
|
SSSE3 |
|
|
SSSE3 |
|
|
SSSE3 |
|
|
SSSE3 |
|
|
SSSE3 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE2 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE |
|
|
SSE |
|
|
SSE |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE4.1 |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE |
|
|
SSE |
|
|
SSE |
|
|
SSE2 |
|
|
SSE |
|
|
SSE2 |
|
|
SSE2 |
|
|
SSE |
|
|
SSE |
|
|
Tecnologia | Protótipo | Instrução |
RDRAND |
|
|
RDRAND |
|
|
RDRAND |
|
|
Tecnologia | Protótipo | Instrução |
RDSEED |
|
|
RDSEED |
|
|
RDSEED |
|
|
Clobber | Descrição |
cc |
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. | ... |
Restrição | Descrição |
| Operando na memória. |
|
| Um valor inteiro imediato. |
| Um valor floating-point imediato. |
| Um operando na memória, registrador de propósito geral ou inteiro imediato. Mesmo efeito que usar |
| Um operando que é um endereço de memória válido. |
| Qualquer operando é permitido. Basicamente deixa a decisão nas mãos do compilador. |
Restrição | Descrição |
| Registradores legado. Qualquer um dos oito registradores de propósito geral disponíveis em IA-32. |
| Qualquer registrador que seja possível ler o byte menos significativo. Como RAX (AL) ou R8 (R8B) por exemplo. |
| Qualquer registrador que seja possível ler o segundo byte menos significativo, como RAX (AH) por exemplo. |
| O registrador "A" (RAX, EAX, AX ou AL). |
| O registrador "B" (RBX, EBX, BX ou BL). |
| O registrador "C" (RCX, ECX, CX ou CL). |
| O registrador "D" (RDX, EDX, DX ou DL). |
| RSI, ESI, SI ou SIL. |
| RDI, EDI, DI ou DIL. |
| O conjunto AX:DX. |
|
| ST0 |
| ST1 |
| Qualquer registrador MMX. |
|
| XMM0 |
| Um inteiro constante entre 0 e 31, usado para shift com valores de 32-bit. |
| Um inteiro constante entre 0 e 63, usado para shift com valores de 64-bit. |
| Inteiro sinalizado de 8-bit. |
| Inteiro não-sinalizado de 8-bit. |
Tipo | Descrição |
| Tipo usado para representar o conteúdo de um registrador MMX. Pode armazenar 8 valores 8-bit, 4 valores de 16-bit, 2 valores de 32-bit ou 1 valor de 64-bit. |
|
| Também um registrador SSE porém armazenando 2 floating-point de 64-bit. |
| Registrador SSE que pode armazenar 16 valores inteiros de 8-bit, 8 valores inteiros de 16-bit, 4 valores inteiros de 32-bit ou 2 valores inteiros de 64-bit. |
| Representa o conteúdo de um registrador YMM usado pela tecnologia AVX. Pode armazenar 8 valores floating-point de 32-bit. |
| Registrador YMM que pode armazenar 4 floating-point de 64-bit. |
| Registrador YMM que pode armazenar 32 valores inteiros de 8-bit, 16 valores inteiros de 16-bit, 8 valores inteiros de 32-bit ou 4 valores inteiros de 64-bit. |
| Representa o conteúdo de um registrador ZMM usado pela tecnologia AVX-512. Pode armazenar 16 valores floating-point de 32-bit. |
| Registrador ZMM que pode armazenar 8 valores floating-point de 64-bit. |
| Registrador ZMM que pode armazenar 64 valores inteiros de 8-bit, 32 inteiros de 16-bit, 16 inteiros de 32-bit ou 8 inteiros de 64-bit. |
Indica que o código ASM modifica as flags do processador (registrador ).
Operando em um .
Qualquer .
Qualquer .
Representa o conteúdo de um . Pode armazenar 4 valores floating-point de 32-bit.