Operações de Deslocamento SSE/AVX em Assembly NASM
As extensões SSE e AVX fornecem instruções para deslocamento lógico e aritmético de dados.
Deslocamento de Vetores por Bytes
Existem instruções que deslocam todo o conteúdo de um registrador XMM/YMM por um número específico de bytes.
A instrução pslldq desloca os dados em um registrador XMM para a esquerda em um número de bytes especificado pelo operando imediato imm8. Os bytes de ordem inferior que são liberados são preenchidos com zeros.
pslldq xmmdest, imm8A versão AVX, vpslldq, utiliza um registrador de origem e um de destino. Ela desloca o valor do segundo operando (um registrador XMM ou YMM) para a esquerda pelo número de bytes indicado no terceiro operando. O resultado é armazenado no primeiro registrador.
vpslldq xmmdest, xmmsrc, imm8
vpslldq ymmdest, ymmsrc, imm8Na sua variante de 128 bits, a instrução também preenche os bits de 128 a 255 do registrador YMM de destino com zeros.
De forma análoga, a instrução psrldq desloca os dados em um registrador XMM para a direita, preenchendo os bytes de ordem superior liberados com zeros.
psrldq xmmdest, imm8A sua versão AVX, vpsrldq, desloca o valor do segundo operando para a direita e armazena o resultado no primeiro.
vpsrldq xmmdest, xmmsrc, imm8
vpsrldq ymmdest, ymmsrc, imm8É importante notar que essas instruções deslocam o vetor inteiro contido no registrador por bytes, ou seja, a menor unidade de deslocamento é de 8 bits.
A seguir, um exemplo de programa em Linux que demonstra o uso do deslocamento.
global _start
section .data
nums dd 3, 5, 8, 11
section .text
_start:
movaps xmm0, [nums] ; Carrega o vetor nums no registrador xmm0
psrldq xmm0, 4 ; Desloca o registrador xmm0 4 bytes para a direita
movd edi, xmm0 ; edi = 5
mov rax, 60
syscallNeste código, o registrador xmm0 recebe o vetor nums, composto por quatro números dword, onde cada um ocupa 4 bytes. A instrução psrldq xmm0, 4 desloca o conteúdo de xmm0 em 4 bytes para a direita. O vetor original [11, 8, 5, 3] se torna [0, 11, 8, 5]. Após o deslocamento, o primeiro número do vetor em xmm0 (que passa a ser 5) é movido para o registrador edi.
Com as operações de deslocamento, é possível implementar uma função para exibir o conteúdo de um registrador xmm no console. O programa abaixo demonstra essa funcionalidade em Linux.
global _start
section .data
nums dd 254, 5, 8, 11
hexstr128 db "0x123456789ABCDEFG123456789ABCDEFG", 10, 0
len equ $-hexstr128 ; Tamanho da string
hexmap db 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70
section .text
_start:
movaps xmm0, [nums] ; Carrega o vetor nums no registrador xmm0
call print_reg128
mov rax, 60
syscall
; Função para imprimir um registrador de 128 bits no console
; Parâmetros: XMM0 - valor a ser exibido
print_reg128:
mov rsi, hexmap ; Tabela de conversão
mov rbx, hexstr128 ; Início da string de destino
add rbx, 33 ; Aponta para o final do espaço do número na string
mov rcx, 16 ; 16 bytes = 32 caracteres hexadecimais para imprimir
forloop128:
movq rax, xmm0
and rax, 0xf ; Aplica uma máscara para obter o primeiro nibble (4 bits)
mov al, byte [rsi + rax] ; Encontra o caractere correspondente na tabela hexmap
mov byte [rbx], al ; Armazena o caractere ASCII
movq rax, xmm0
shr rax, 4 ; Pega o segundo nibble de XMM0
and rax, 0xf ; Aplica a máscara
mov al, byte [rsi + rax] ; Encontra o caractere correspondente
mov byte [rbx-1], al ; Armazena o segundo caractere ASCII
sub rbx, 2 ; Move o ponteiro da string 2 posições para trás
psrldq xmm0, 1 ; Desloca XMM0 1 byte para a direita para processar o próximo byte
sub rcx, 1 ; Decrementa o contador
jne forloop128 ; Repete o laço se RCX não for zero
; Imprime a string usando a chamada de sistema write
mov rax, 1 ; Número da função de sistema (write)
mov rdi, 1 ; Descritor de arquivo (saída padrão)
mov rsi, hexstr128 ; Endereço da string
mov rdx, len ; Tamanho da string
syscall ; Executa a chamada de sistema
retA função print_reg128 recebe o valor a ser impresso através do registrador xmm0. Um laço processa cada um dos 16 bytes do registrador. Em cada iteração, os dois nibbles (4 bits cada) do byte de ordem mais baixa de xmm0 são isolados, convertidos para seus caracteres hexadecimais correspondentes usando a tabela hexmap e inseridos na string hexstr128 do final para o começo. Após processar um byte, o registrador xmm0 é deslocado 1 byte para a direita com psrldq para que o próximo byte possa ser processado na iteração seguinte. Finalizado o laço, a string resultante é exibida no console através da chamada de sistema write.
Saída do programa no console:
root@Programicio:~/asm; nasm -f elf64 hello.asm -o hello.o root@Programicio:~/asm; ld hello.o -o hello root@Programicio:~/asm; ./hello 0x0000000B0000000800000005000000FE root@Programicio:~/asm;
O registrador XMM0, contendo o vetor de inteiros de 32 bits [254, 5, 8, 11], corresponde ao valor hexadecimal 0x0000000B0000000800000005000000FE.
A seguir, uma versão análoga do programa para Windows.
global _start
extern WriteFile ; Importa a função WriteFile
extern GetStdHandle ; Importa a função GetStdHandle
section .data
nums dd 254, 5, 8, 11
hexstr128 db "0x123456789ABCDEFG123456789ABCDEFG", 10, 0
len equ $-hexstr128 ; Tamanho da string
hexmap db 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70
section .text
_start:
movaps xmm0, [rel nums] ; Carrega o vetor nums no registrador xmm0
call print_reg128
ret
; Função para imprimir um registrador de 128 bits no console
; Parâmetros: XMM0 - valor a ser exibido
print_reg128:
sub rsp, 48 ; Reserva 40 bytes (5*8) para os parâmetros das funções e shadow space
mov rsi, hexmap ; Tabela de conversão
mov rbx, hexstr128 ; Início da string de destino
add rbx, 33 ; Aponta para o final do espaço do número na string
mov rcx, 16 ; 16 bytes = 32 caracteres hexadecimais para imprimir
forloop128:
movq rax, xmm0
and rax, 0xf ; Aplica uma máscara para obter o primeiro nibble
mov al, byte [rsi + rax] ; Encontra o caractere correspondente na tabela
mov byte [rbx], al ; Armazena o caractere ASCII
movq rax, xmm0
shr rax, 4 ; Pega o segundo nibble de XMM0
and rax, 0xf ; Aplica a máscara
mov al, byte [rsi + rax] ; Encontra o caractere correspondente
mov byte [rbx-1], al ; Armazena o segundo caractere ASCII
sub rbx, 2 ; Move o ponteiro da string 2 posições para trás
psrldq xmm0, 1 ; Desloca XMM0 1 byte para a direita
sub rcx, 1 ; Decrementa o contador
jne forloop128 ; Repete o laço se RCX não for zero
; Impressão da string
mov rcx, -11 ; Argumento para GetStdHandle (STD_OUTPUT_HANDLE)
call GetStdHandle ; Chama GetStdHandle
mov rcx, rax ; Primeiro parâmetro de WriteFile (handle do console)
mov rdx, hexstr128 ; Segundo parâmetro (endereço da string)
mov r8d, len ; Terceiro parâmetro (tamanho da string)
xor r9, r9 ; Quarto parâmetro (bytes escritos)
mov qword [rsp + 32], 0 ; Quinto parâmetro
call WriteFile ; Chama a função WriteFile
add rsp, 48
retExibição de Dados com a Função printf
Os dados também podem ser exibidos com a função printf da biblioteca C. O exemplo a seguir é para Linux.
global main
extern printf
section .data
nums dd 255, 5, 8, 11
format_str db "%d, %d, %d, %d", 10, 0
section .text
main:
sub rsp, 8
movaps xmm0, [nums] ; Carrega o vetor nums no registrador xmm0
movd esi, xmm0 ; Move o primeiro número para esi
psrldq xmm0, 4 ; Desloca para a direita em 4 bytes para acessar o próximo número
movd edx, xmm0 ; Move o segundo número para edx
psrldq xmm0, 4 ; Desloca novamente
movd ecx, xmm0 ; Move o terceiro número para ecx
psrldq xmm0, 4 ; Desloca novamente
movd r8d, xmm0 ; Move o quarto número para r8d
mov rdi, format_str
xor rax, rax ; Indica que nenhum registrador XMM é usado para argumentos
call printf
add rsp, 8
retPara extrair cada número do vetor em xmm0, a instrução de deslocamento para a direita é aplicada sucessivamente. A cada deslocamento, o número na posição de ordem mais baixa do vetor é movido para um dos registradores usados pela função printf para passar argumentos, conforme a convenção de chamada do Linux x86-64.
Compilação e execução do programa:
root@Programicio:~/asm; nasm -f elf64 hello.asm -o hello.o root@Programicio:~/asm; gcc -static hello.o -o hello root@Programicio:~/asm; ./hello 255, 5, 8, 11 root@Programicio:~/asm;
Exibição de Números de Ponto Flutuante
A exibição de números de ponto flutuante é um pouco mais complexa, como demonstrado no programa para Linux a seguir.
global main
extern printf
section .data
nums dd 1.2, 2.3, 3.4, 4.5
format_str db "%.1f, %.1f, %.1f, %.1f", 10, 0
section .text
main:
sub rsp, 8
movaps xmm5, [nums] ; Carrega o vetor no registrador xmm5
movss xmm0, xmm5 ; Move o primeiro número para xmm0
cvtss2sd xmm0, xmm0 ; Converte de float para double
psrldq xmm5, 4 ; Desloca o vetor 4 bytes para a direita
movss xmm1, xmm5 ; Move o segundo número para xmm1
cvtss2sd xmm1, xmm1 ; Converte de float para double
psrldq xmm5, 4 ; Desloca o vetor
movss xmm2, xmm5 ; Move o terceiro número para xmm2
cvtss2sd xmm2, xmm2 ; Converte de float para double
psrldq xmm5, 4 ; Desloca o vetor
movss xmm3, xmm5 ; Move o quarto número para xmm3
cvtss2sd xmm3, xmm3 ; Converte de float para double
mov rdi, format_str
mov rax, 4 ; Indica que 4 registradores XMM são usados para argumentos
call printf
add rsp, 8
retNeste caso, a função printf também é utilizada. Os argumentos de ponto flutuante são passados através dos registradores xmm0 a xmm7. O vetor é carregado em xmm5 para não interferir com os registradores de argumento. Cada elemento do vetor (que é um float de precisão simples) é movido para um registrador de argumento (xmm0 a xmm3) e convertido para double (precisão dupla) com a instrução cvtss2sd, pois printf espera doubles para o formato %.1f. Entre cada extração, o vetor em xmm5 é deslocado 4 bytes para a direita.
Resultado do programa:
root@Programicio:~/asm; nasm -f elf64 hello.asm -o hello.o root@Programicio:~/asm; gcc -static hello.o -o hello root@Programicio:~/asm; ./hello 1.2, 2.3, 3.4, 4.5 root@Programicio:~/asm;
Versão análoga para Windows:
global main
extern printf
section .data
align 16
nums dd 1.2, 2.3, 3.4, 4.5
format_str db "%.1f, %.1f, %.1f, %.1f", 10, 0
section .text
main:
sub rsp, 40
movaps xmm5, [rel nums] ; Carrega o vetor no registrador xmm5
movss xmm1, xmm5 ; Move o primeiro número para xmm1
cvtss2sd xmm1, xmm1 ; Converte de float para double
movq rdx, xmm1 ; Duplica em rdx para a convenção de chamada
psrldq xmm5, 4 ; Desloca o vetor 4 bytes para a direita
movss xmm2, xmm5 ; Move o segundo número para xmm2
cvtss2sd xmm2, xmm2 ; Converte de float para double
movq r8, xmm2 ; Duplica em r8
psrldq xmm5, 4 ; Desloca o vetor
movss xmm3, xmm5 ; Move o terceiro número para xmm3
cvtss2sd xmm3, xmm3 ; Converte de float para double
movq r9, xmm3 ; Duplica em r9
psrldq xmm5, 4 ; Desloca o vetor
movss xmm4, xmm5 ; Move o quarto número para xmm4
cvtss2sd xmm4, xmm4 ; Converte de float para double
movq [rsp+32], xmm4 ; Passa o quarto argumento via pilha
mov rcx, format_str
call printf
add rsp, 40
retSe o vetor contivesse números de ponto flutuante de dupla precisão (8 bytes cada), a conversão não seria necessária. O exemplo a seguir para Linux ilustra isso.
global main
extern printf
section .data
nums dq 2.3, 4.5
format_str db "%.1f, %.1f", 10, 0
section .text
main:
sub rsp, 8
movaps xmm5, [nums] ; Carrega o vetor no registrador xmm5
movsd xmm0, xmm5 ; Move o primeiro número para xmm0
psrldq xmm5, 8 ; Desloca o vetor 8 bytes para a direita
movsd xmm1, xmm5 ; Move o segundo número para xmm1
mov rdi, format_str
mov rax, 2 ; Indica que 2 registradores XMM são usados
call printf
add rsp, 8
retDeslocamento de Elementos Individuais no Vetor
Existem também instruções que deslocam bits de elementos individuais dentro de um vetor, em vez de deslocar o registrador inteiro.
(v)psllw,(v)pslld,(v)psllq: Deslocam para a esquerda, respectivamente,word(16 bits),dword(32 bits) eqword(64 bits).(v)psrlw,(v)psrld,(v)psrlq: Deslocam para a direita, respectivamente,word,dwordeqword.(v)psraw,(v)psrad,(v)psraq: Realizam um deslocamento aritmético para a direita emword,dwordeqword.
A sintaxe, exemplificada com (v)psllw, é similar para as outras instruções:
psllw xmmdest, imm8
psllw xmmdest, xmmsrc/mem128
vpsllw xmmdest, xmmsrc, imm8
vpsllw xmmdest, xmmsrc, mem128
vpsllw ymmdest, ymmsrc, imm8
vpsllw ymmdest, ymmsrc, xmm/mem128Nas versões de dois operandos, o primeiro é o valor a ser deslocado, e o segundo especifica a quantidade de bits para o deslocamento. Nas versões de três operandos, o segundo operando é o valor de origem, o terceiro é a quantidade de bits, e o primeiro é o destino do resultado.
O programa a seguir para Linux carrega um vetor de quatro números e desloca cada um deles 1 bit para a esquerda, o que equivale a uma multiplicação por 2.
global main
extern printf
section .data
nums dd 1, 2, 4, 8
format_str db "%d, %d, %d, %d", 10, 0
section .text
main:
sub rsp, 8
movaps xmm0, [nums] ; Carrega o vetor no registrador xmm0
pslld xmm0, 1 ; Desloca cada dword em xmm0 1 bit para a esquerda
movd esi, xmm0 ; Move o primeiro número para esi
psrldq xmm0, 4 ; Desloca o vetor 4 bytes para a direita
movd edx, xmm0 ; Move o segundo número para edx
psrldq xmm0, 4 ; Desloca o vetor
movd ecx, xmm0 ; Move o terceiro número para ecx
psrldq xmm0, 4 ; Desloca o vetor
movd r8d, xmm0 ; Move o quarto número para r8d
mov rdi, format_str
xor rax, rax
call printf
add rsp, 8
retCompilação e resultado:
root@Programicio:~/asm; nasm -f elf64 hello.asm -o hello.o root@Programicio:~/asm; gcc -static hello.o -o hello root@Programicio:~/asm; ./hello 2, 4, 8, 16 root@Programicio:~/asm;
Resumo
- As instruções
psrldqepslldqdeslocam todo o conteúdo de um registrador XMM/YMM por um número de bytes. - É possível usar
psrldqpara iterar sobre os elementos de um vetor, extraindo um de cada vez. - Instruções como
(v)psllde(v)psrlddeslocam os bits de cada elemento individualmente dentro do vetor. - A impressão de valores pode ser feita com chamadas de sistema ou com funções da biblioteca C como
printf. - Ao chamar
printf, é necessário respeitar a convenção de chamada da plataforma (x86-64 Linux ou Windows), especialmente para argumentos de ponto flutuante.