O autor se concentra no campo da segurança do Android. Bem-vindo ao prestar atenção à minha conta pública pessoal do WeChat " Engenharia de segurança do Android " (clique para digitalizar o código a seguir). A conta pública pessoal do WeChat se concentra principalmente na proteção de segurança e análise reversa de aplicativos Android, compartilhando vários métodos de ataque e defesa de segurança, tecnologia Hook, compilação ARM e outros conhecimentos relacionados ao Android
Sugestão: Este artigo tem muito conteúdo, é recomendável salvá-lo e usá-lo como um manual de referência quando necessário posteriormente. Geralmente, as instruções IR precisam apenas saber que existe uma determinada instrução e não há necessidade de gastar tempo memorizando-a.
visão geral
A instrução IR é uma representação intermediária em LLVM, que é usada para representar o fluxo de controle, fluxo de dados, acesso à memória, etc. do programa.É uma forma estática de atribuição única baseada na forma SSA (Static Single Assignment). No LLVM, cada instrução IR possui um único opcode (opcode), que é utilizado para identificar o tipo da instrução, e cada opcode corresponde a um conjunto de possíveis operandos (operandos), que podem ser constantes, registradores ou o resultado de outros instruções.
No IR do LLVM, todos os tipos de dados são definidos com base no sistema de tipo LLVM. Esses tipos de dados incluem números inteiros, números de ponto flutuante, ponteiros, matrizes, estruturas etc., e cada tipo de dados tem seus próprios atributos, como largura de bits. , alinhamento, e assim por diante. Em IR, cada valor possui um tipo, que pode ser especificado explicitamente ou deduzido dos operandos da instrução.
As instruções IR do LLVM são muito ricas, incluindo aritmética, lógica, comparação, conversão, fluxo de controle, etc.
Existem muitos tipos de instruções IR, a seguir estão alguns tipos de instrução comuns:
- Instruções de adição, subtração, multiplicação e divisão: add, sub, mul, sdiv, udiv, fadd, fsub, fmul, fdiv, etc.
- Instruções de operação de bit: and, or, xor, shl, lshr, ashr, etc.
- Instruções de conversão: trunc, zext, sext, fptrunc, fpext, fptoui, fptosi, uitofp, sitofp, ptrtoint, inttoptr, bitcast, etc.
- Instruções de memória: alloca, load, store, getelementptr, etc.
- Instruções de fluxo de controle: br, switch, ret, indirectbr, invocar, retomar, inacessível, etc.
- Outros comandos: phi, select, call, va_arg, landingpad, etc.
Instruções de adição, subtração, multiplicação e divisão
1. Instrução de adição (adicionar)
A instrução de adição é usada para somar dois valores. No LLVM, a sintaxe da instrução de adição é a seguinte:
%result = add <type> <value1>, <value2>
Dentre eles, <type>
representam o tipo de dado do valor a ser somado, que pode ser um número inteiro, um número de ponto flutuante, etc.; <value1>
e <value2>
representam os dois números a serem somados, que podem ser constantes, registradores ou resultados de outras instruções.
No LLVM, os parâmetros add
da instrução <type>
especificam o tipo de <value1>
e <value2>
, bem como <result>
o tipo de . Os tipos suportados incluem:
- Tipo inteiro:
i1
,i8
,i16
,i32
,i64
,i128
etc.; - Tipos de ponto flutuante:
half
,float
,double
,fp128
etc.; - Tipo de vetor:
<n x i8>
,<n x i16>
,<n x i32>
etc.; - Tipo de ponteiro:
i8*
,i32*
,float*
etc.; - TagType:
metadata
;
Por exemplo, se quisermos somar dois números inteiros e obter um resultado inteiro, podemos usar a seguinte instrução:
%result = add i32 1, 2
Aqui, <type>
especificado como i32
, <value1>
é um valor inteiro 1
, <value2>
é um valor inteiro 2
, <result>
é um tipo inteiro i32
. Os vários tipos de tamanhos de espaço de memória (em bits) são os seguintes:
- Tipo inteiro:
i1
1 bit,i8
8 bits,i16
16 bits,i32
32 bits,i64
64 bits,i128
128 bits; - Tipo de ponto flutuante:
half
16 bits,float
32 bits,double
64 bits,fp128
128 bits; - Tipo de vetor:
<n x i8>
espaço reservadon * 8
,<n x i16>
espaço reservadon * 16
,<n x i32>
espaço reservadon * 32
, etc.; - Tipo de ponteiro: o tamanho do tipo de ponteiro depende do sistema operacional e da arquitetura em tempo de execução. Por exemplo, em um sistema operacional de 32 bits, o tipo de ponteiro geralmente ocupa 4 bytes (32 bits) e em um sistema operacional de 64 bits , o tipo ponteiro geralmente ocupa 8 bytes bytes (64 bits);
- Tipo de tag:
metadata
o tipo geralmente ocupa o mesmo espaço que o tipo de ponteiro;
Deve-se observar que aqui está apenas o tamanho padrão de vários tipos no LLVM. Na verdade, ao usar o LLVM IR, os desenvolvedores podem especificar explicitamente o tamanho do tipo adicionando um número após o tipo. Por exemplo, o tipo pode ser i16
Pass i16 123
para representar um valor inteiro de 16 bits 123
.
A seguir está um exemplo de código para uma instrução de adição, que adiciona dois inteiros:
%x = add i32 2, 3
Esta instrução soma as constantes 2
e 3
, e armazena o resultado no registrador %x
.
Além das constantes, também é possível utilizar registradores ou resultados de outras instruções como operandos para instruções de adição, por exemplo:
%x = add i32 %a, %b
%z = add i32 %x, %y
A primeira linha de código adiciona os valores nos registradores %a
e %b
, e armazena o resultado no registrador %x
; a segunda linha de código adiciona os valores nos registradores %x
e %y
, e armazena o resultado no registrador %z
.
No LLVM, instruções de adição com carry ( add with carry
) e instruções de adição com estouro ( add with overflow
) também são suportadas, então não as repetirei aqui.
2. Instrução de subtração (sub)
A instrução de subtração é usada para subtrair dois valores, a sintaxe é:
%result = sub <type> <value1>, <value2>
Dentre eles, <type>
representam o tipo de dado do valor a ser subtraído, que pode ser um número inteiro, um número de ponto flutuante, etc.; <value1>
e <value2>
representam os dois números a serem subtraídos, que podem ser constantes, registradores ou resultados de outras instruções.
A seguir está um exemplo de código de uma instrução de subtração que subtrai dois inteiros:
%diff = sub i32 %x, %y
Esta instrução %x
subtrai o valor no registro %y
do valor em e armazena o resultado no registro %diff
.
Há também uma forma de instrução de subtração que pode ser usada para calcular a diferença entre dois números de ponto flutuante. A sintaxe é:
%result = fsub <type> <value1>, <value2>
Dentre eles, <type>
o tipo de dado que representa o valor a ser subtraído deve ser um tipo numérico de ponto flutuante; <value1>
e <value2>
representar os dois números a serem subtraídos, que podem ser constantes, registradores ou resultados de outras instruções.
A seguir está um exemplo de código para uma instrução de subtração de ponto flutuante que subtrai dois números de ponto flutuante de precisão simples:
%diff = fsub float %x, %y
Esta instrução %x
subtrai o número de ponto flutuante de precisão simples no registrador %y
do número de ponto flutuante de precisão simples no registrador e armazena o resultado no registrador %diff
.
3. Instrução de multiplicação (mul)
A instrução de multiplicação é usada para multiplicar dois valores, a sintaxe é:
%result = mul <type> <value1>, <value2>
Dentre eles, <type>
representam o tipo de dado do valor a ser multiplicado, que pode ser um número inteiro, um número de ponto flutuante, etc.; <value1>
e <value2>
representam os dois números a serem multiplicados, que podem ser constantes, registradores ou resultados de outras instruções.
O seguinte é um exemplo de código de uma instrução de multiplicação que multiplica dois números inteiros:
%prod = mul i32 %x, %y
Esta instrução multiplica os valores nos registradores %x
e , e armazena o resultado no registrador . Também podemos realizar operações de multiplicação em números de ponto flutuante da seguinte forma:%y
%prod
%result = mul double %value1, %value2
Esta instrução multiplica os valores nos registradores %value1
e , e armazena o resultado em . Deve-se observar que, para a operação de multiplicação de números de ponto flutuante, você precisa usar tipos de ponto flutuante como ou .%value2
%result
double
float
Além disso, o LLVM também fornece alguns outros tipos de instruções de multiplicação, como instruções de multiplicação de vetores, instruções de multiplicação de números inteiros sem sinal e assim por diante. Consulte a documentação oficial do LLVM para métodos específicos de uso de instruções.
4. Instrução de divisão (div)
O comando de divisão é usado para dividir dois valores, a sintaxe é:
%result = <s/u>div <type> <value1>, <value2>
Em que 表示要执行有符号(`sdiv`)还是无符号(`udiv`)的除法运算;
o tipo de dado que representa o valor a ser dividido pode ser um número inteiro, um número de ponto flutuante, etc.; 和
representam respectivamente dois números a serem divididos, que podem ser uma constante, um registrador ou o resultado de outras instruções.
Aqui está um exemplo de código de uma instrução de divisão, que divide dois números inteiros:
%quot = sdiv i32 %x, %y
Esta instrução %x
divide o valor no registrador %y
pelo valor em e armazena o resultado no registrador %quot
. Como a instrução é usada sdiv
, uma operação de divisão com sinal é executada.
Se você quiser fazer divisão sem sinal, você pode usar udiv
a instrução:
%quot = udiv i32 %x, %y
Esta instrução %x
divide o valor no registrador %y
pelo valor em e armazena o resultado no registrador %quot
. Por causa udiv
da instrução, uma operação de divisão sem sinal é executada.
instruções de operação de bits
IR tem uma variedade de instruções de operação de bit, incluindo bit e (e), bit ou (ou), bit exclusivo ou (xor), inversão de bit (não), etc. Essas instruções executam operações bit a bit em tipos inteiros e armazenam o resultado em um novo registrador. A seguir estão as instruções de operação de bits comuns e suas funções em IR:
- E bit a bit (e): executa uma operação AND bit a bit nas representações binárias de dois inteiros.
- Bitwise or (or): executa uma operação bitwise OR nas representações binárias de dois inteiros.
- XOR bit a bit (xor): executa uma operação XOR bit a bit nas representações binárias de dois inteiros.
- Inversão de bit (não): executa uma operação de inversão bit a bit na representação binária de um número inteiro.
Essas instruções podem ser utilizadas com sintaxe semelhante, onde <type>
representa o tipo de dado do inteiro a ser submetido à operação de bit, que pode ser i1, i8, i16, i32, i64, etc <value1>
.; <value2>
Por exemplo:
%result = and i32 %x, %y
%result = or i32 %x, %y
%result = xor i32 %x, %y
%result = xor i32 %x, -1
A primeira instrução executa a operação AND bit a bit de e armazena o resultado em %x
; a segunda instrução executa a operação OR bit a bit de e armazena o resultado em ; a terceira instrução executa a operação bit a bit exclusivo ou de e , e salva o resultado em ; a última instrução executa uma operação XOR bit a bit com um número que é 1 em binário, ou seja, inverte cada bit de e salva o resultado em .%y
%result
%x
%y
%result
%x
%y
%result
%x
%x
%result
instrução de conversão
trunc
: trunca um número inteiro ou de ponto flutuante para um número menor de dígitos, ou seja, remove alguns bits binários de alta ordem.zext
: Aumente o número de bits de um valor inteiro ou booleano e preencha os bits altos do novo número de bits com zeros, ou seja, extensão de zero.sext
: Aumente o número de dígitos de um inteiro e os bits de ordem superior do novo número de dígitos são preenchidos com os dígitos mais altos originais, ou seja, a extensão do sinal é executada.fptrunc
: trunca um número de ponto flutuante para um número menor de dígitos, ou seja, remove alguns dígitos binários de ordem superior. Esta é uma operação de arredondamento e alguma precisão pode ser perdida.fpext
: Aumente o número de bits de um número de ponto flutuante e preencha os bits superiores do novo número com zeros, ou seja, execute a extensão zero de ponto flutuante.fptoui
: converte um número de ponto flutuante em um inteiro sem sinal. Se o float for negativo, o resultado é zero.fptosi
: converte um número de ponto flutuante em um inteiro com sinal. Se o float for negativo, o resultado é o menor inteiro que é negativo.uitofp
: converte um inteiro sem sinal em um número de ponto flutuante.sitofp
: converte um inteiro com sinal em um número de ponto flutuante.ptrtoint
: converte um tipo de ponteiro em um tipo inteiro. Esta instrução é normalmente usada para converter ponteiros em números inteiros para cálculos.inttoptr
: converte um tipo de número inteiro em um tipo de ponteiro. Essa instrução geralmente é usada para converter um inteiro em um ponteiro para cálculo de endereço de memória.bitcast
: Converte um valor de um tipo para outro, mas os tipos devem ter o mesmo número de bits. Esta instrução pode ser usada para implementar operações de memória de baixo nível, como converter números de ponto flutuante em inteiros para operações de bit.
A seguir estão as instruções de uso detalhadas e exemplos da instrução de conversão IR:
1. tronco
trunc
A instrução trunca um número inteiro ou de ponto flutuante para um número menor de bits do que o original, ou seja, remove alguns bits de ordem superior. trunc
O formato de uso do comando é o seguinte:
%result = trunc <source type> <value> to <destination type>
Dentre eles, <source type>
e <destination type>
representam o tipo de origem e o tipo de destino respectivamente, <value>
representando o valor a ser convertido. Por exemplo, o código a seguir trunca um inteiro de 64 bits para um inteiro de 32 bits:
%long = add i64 1, 2
%short = trunc i64 %long to i32
Neste exemplo, %long
é um número inteiro de 64 bits cujo valor é 3 (1+2). %short
é um inteiro de 32 bits cujo valor é 3. Como %long
é truncado para um número inteiro de 32 bits, apenas os 32 bits mais baixos do valor permanecem.
2. pressão
zext
A instrução aumenta o número de bits de um valor inteiro ou booleano, e os bits de ordem superior do novo número de bits são preenchidos com zeros, ou seja, estendidos para zero. zext
O formato de uso do comando é o seguinte:
%result = zext <source type> <value> to <destination type>
Por exemplo, o código a seguir estende um inteiro de 8 bits para um inteiro de 16 bits:
%short = add i8 1, 2
%long = zext i8 %short to i16
Neste exemplo, %short
é um inteiro de 8 bits cujo valor é 3 (1+2). %long
é um inteiro de 16 bits cujo valor é 3. Uma vez que %short
é estendido para um inteiro de 16 bits, os 8 bits superiores são preenchidos com zeros.
3.sexta
sext
A instrução aumenta o número de bits de um inteiro, e os bits mais altos do novo número de bits são preenchidos com os bits mais altos originais, ou seja, a extensão do sinal é executada. sext
As diretivas são usadas em um formato zext
semelhante às diretivas:
%result = sext <source type> <value> to <destination type>
Por exemplo, o código a seguir estende um inteiro de 8 bits para um inteiro de 16 bits:
%short = add i8 -1, 2
%long = sext i8 %short to i16
Neste exemplo, %short
é um inteiro de 8 bits cujo valor é 1-2=-1. %long
é um inteiro de 16 bits cujo valor é 0xffff. Uma vez que %short
é expandido para um inteiro de 16 bits, os 8 bits superiores são todos preenchidos com 1s.
4.fptrunc
fptrunc
A instrução trunca um número de ponto flutuante para um número menor de bits do que o original, ou seja, remove alguns bits binários de alta ordem. fptrunc
O formato de uso do comando é o seguinte:
%result = fptrunc <source type> <value> to <destination type>
Por exemplo, o código a seguir trunca um número de ponto flutuante de precisão dupla para um número de ponto flutuante de precisão simples:
%double = fadd double 1.0, 2.0
%float = fptrunc double %double to float
Neste exemplo, %double
é um número de ponto flutuante de precisão dupla cujo valor é 3,0 (1,0+2,0). %float
é um número de ponto flutuante de precisão simples cujo valor é 3,0. Como %double
é truncado para um número de ponto flutuante de precisão simples, o valor de ordem superior é truncado e apenas o valor de ordem inferior permanece.
5.fpext
fpext
A instrução expande um número de ponto flutuante para um número maior de dígitos e os bits de ordem superior do novo número de dígitos são preenchidos com zeros. fpext
As diretivas são usadas em um formato fptrunc
semelhante às diretivas:
%result = fpext <source type> <value> to <destination type>
Por exemplo, o código a seguir expande um número de ponto flutuante de precisão simples para um número de ponto flutuante de precisão dupla:
%float = fadd float 1.0, 2.0
%double = fpext float %float to double
Neste exemplo, %float
é um número de ponto flutuante de precisão simples cujo valor é 3,0 (1,0+2,0). %double
é um número de ponto flutuante de precisão dupla cujo valor é 3,0. Como resultado %float
de ser estendido para um número de ponto flutuante de precisão dupla, os novos bits de ordem superior são todos preenchidos com zero.
6.fptoui
fptoui
A instrução converte um número de ponto flutuante em um inteiro sem sinal. Ao converter, se o valor do float for negativo, o resultado será 0. fptoui
O formato de uso do comando é o seguinte:
%result = fptoui <source type> <value> to <destination type>
Por exemplo, o código a seguir converte um número de ponto flutuante de precisão dupla em um inteiro sem sinal de 32 bits:
%double = fadd double 1.0, 2.0
%uint = fptoui double %double to i32
Neste exemplo, %double
é um número de ponto flutuante de precisão dupla cujo valor é 3,0 (1,0+2,0). %uint
é um inteiro sem sinal de 32 bits cujo valor é 3. Como %double
o valor de é positivo, ele pode ser convertido em um inteiro sem sinal de 32 bits.
7. ptose
fptosi
A instrução converte um número de ponto flutuante em um inteiro com sinal. Ao converter, se o valor do número de ponto flutuante estiver fora do intervalo representável do tipo de destino, o resultado será o valor mínimo ou máximo desse tipo. fptosi
O formato de uso do comando é o seguinte:
%result = fptosi <source type> <value> to <destination type>
Por exemplo, o código a seguir converte um número de ponto flutuante de precisão dupla em um inteiro com sinal de 32 bits:
%double = fadd double 1.0, -2.0
%i32 = fptosi double %double to i32
Neste exemplo, %double
é um número de ponto flutuante de precisão dupla cujo valor é -1,0 (1,0-2,0). %i32
é um inteiro com sinal de 32 bits cujo valor é -1. Como %double
o valor de é negativo, ele pode ser convertido em um inteiro com sinal de 32 bits.
8.offofp
uitofp
A instrução converte um inteiro sem sinal em um número de ponto flutuante. uitofp
O formato de uso do comando é o seguinte:
%result = uitofp <source type> <value> to <destination type>
Por exemplo, o código a seguir converte um inteiro sem sinal de 32 bits em um número de ponto flutuante de precisão simples:
%uint = add i32 1, 2
%float = uitofp i32 %uint to float
Neste exemplo, %uint
é um inteiro sem sinal de 32 bits cujo valor é 3. %float
é um número de ponto flutuante de precisão simples cujo valor é 3,0. Como %uint
o valor de é positivo, ele pode ser convertido em um número de ponto flutuante de precisão simples.
9.sitofp
sitofp
A instrução converte um inteiro com sinal em um número de ponto flutuante. sitofp
O formato de uso do comando é o seguinte:
%result = sitofp <source type> <value> to <destination type>
Por exemplo, o código a seguir converte um inteiro com sinal de 32 bits em um número de ponto flutuante de precisão simples:
%i32 = add i32 1, -2
%float = sitofp i32 %i32 to float
Neste exemplo, %i32
é um inteiro com sinal de 32 bits cujo valor é -1. %float
é um número de ponto flutuante de precisão simples cujo valor é -1,0. Como %i32
o valor de é negativo, ele pode ser convertido em um número de ponto flutuante de precisão simples.
10.ptrtoint
ptrtoint
A instrução converte um tipo de ponteiro em um tipo inteiro. ptrtoint
O formato de uso do comando é o seguinte:
%result = ptrtoint <source type> <value> to <destination type>
Por exemplo, o código a seguir converte um tipo de ponteiro em um tipo inteiro de 64 bits:
%ptr = alloca i32
%i64 = ptrtoint i32* %ptr to i64
Neste exemplo, %ptr
um ponteiro para um tipo inteiro de 32 bits. %i64
é um tipo inteiro de 64 bits cujo valor é %ptr
o endereço de um ponteiro. Como a largura de bits do tipo ponteiro e do tipo inteiro são diferentes, é necessário usar ptrtoint
instruções para conversão de tipo.
11.inttoptr
inttoptr
A instrução converte um tipo de número inteiro em um tipo de ponteiro. inttoptr
O formato de uso do comando é o seguinte:
%result = inttoptr <source type> <value> to <destination type>
Por exemplo, o código a seguir converte um tipo inteiro de 64 bits em um ponteiro para um tipo inteiro de 32 bits:
%i64 = add i64 1, 2
%ptr = inttoptr i64 %i64 to i32*
Neste exemplo, %i64
é um tipo inteiro de 64 bits cujo valor é 3. %ptr
é um ponteiro para um tipo inteiro de 32 bits cujo valor é 3. Como as larguras de bit dos tipos inteiros e dos tipos de ponteiro são diferentes, são necessárias instruções inttoptr
para a conversão de tipo.
12.bitcast
bitcast
Uma instrução converte a representação de bit de um valor em outro tipo, mas não altera o valor em si. bitcast
O formato de uso do comando é o seguinte:
%result = bitcast <source type> <value> to <destination type>
Por exemplo, o código a seguir converte um número de ponto flutuante de precisão dupla de 64 bits em um tipo inteiro de 64 bits:
%double = fadd double 1.0, -2.0
%i64 = bitcast double %double to i64
Neste exemplo, %double
é um número de ponto flutuante de precisão dupla de 64 bits cujo valor é -1,0 (1,0-2,0). %i64
é um tipo inteiro de 64 bits e seu valor é 0xbff8000000000000 (-4616189618054758400). Como os números de ponto flutuante de precisão dupla e os tipos inteiros de 64 bits têm a mesma largura de bits, bitcast
as instruções podem ser usadas para conversão de tipo.
instrução de memória
LLVM IR fornece algumas instruções de memória comuns , incluindo alloca
, load
, store
, getelementptr
, , , e assim por diante. Essas instruções podem ser usadas para alocação de memória, inicialização e operações de cópia. Cada uma dessas instruções é descrita a seguir, juntamente com os exemplos de código correspondentes.malloc
free
memset
memcpy
memmove
1.alloca
alloca
instrução para alocar memória na pilha e retornar um ponteiro para a memória recém-alocada. alloca
O formato de uso do comando é o seguinte:
%ptr = alloca <type>
onde <type>
é o tipo de bloco de memória a ser alocado. Por exemplo, o código a seguir aloca uma matriz de 5 inteiros:
%array = alloca [5 x i32]
2.carregar
load
As instruções são usadas para ler dados da memória e carregá-los nos registradores. load
O formato de uso do comando é o seguinte:
%val = load <type>* <ptr>
onde <type>
é o tipo de dados a serem lidos e <ptr>
é um ponteiro para o bloco de memória do qual ler os dados. Por exemplo, o código a seguir carrega o primeiro elemento de um array inteiro em um registrador:
%array = alloca [5 x i32]
%ptr = getelementptr [5 x i32], [5 x i32]* %array, i32 0, i32 0
%val = load i32, i32* %ptr
Neste exemplo, %array
é uma matriz de inteiros, %ptr
é um ponteiro para o primeiro elemento da matriz e load
a instrução %ptr
carrega os dados no bloco de memória apontado para %val
o registrador.
3.loja
store
As instruções são usadas para gravar dados dos registradores na memória. store
O formato de uso do comando é o seguinte:
store <type> <val>, <type>* <ptr>
Onde, <type>
é o tipo de dados a serem gravados, <val>
é o valor dos dados a serem gravados e <ptr>
é um ponteiro para o bloco de memória no qual os dados devem ser gravados. Por exemplo, o código a seguir armazena um inteiro no primeiro elemento de uma matriz de inteiros:
%array = alloca [5 x i32]
%ptr = getelementptr [5 x i32], [5 x i32]* %array, i32 0, i32 0
store i32 42, i32* %ptr
Neste exemplo, %array
é uma matriz de inteiros, %ptr
é um ponteiro para o primeiro elemento da matriz e store
a instrução armazena o valor inteiro 42 no %ptr
bloco de memória apontado.
4.getelementptr
getelementptr
As instruções são usadas para calcular deslocamentos de ponteiros para acessar dados na memória. getelementptr
O formato de uso do comando é o seguinte:
%ptr = getelementptr <type>, <type>* <ptr>, <index type> <idx>, ...
Entre eles, <type>
está o tipo de dado apontado pelo ponteiro, <ptr>
é o ponteiro para o dado, <index type>
é o tipo do índice <idx>
e é o valor do índice. getelementptr
As diretivas podem aceitar vários índices, cada um dos quais pode ser de qualquer tipo. O tipo de índice deve ser um tipo inteiro, que é usado para calcular o deslocamento. Por exemplo, o código a seguir calcula um ponteiro para um elemento em uma matriz bidimensional:
%array = alloca [3 x [4 x i32]]
%ptr = getelementptr [3 x [4 x i32]], [3 x [4 x i32]]* %array, i32 1, i32 2
Neste exemplo, %array
é uma matriz bidimensional, %ptr
que é um ponteiro para o elemento na segunda linha e na terceira coluna.
5.malloc
malloc
instrução para alocar memória no heap e retornar um ponteiro para a memória recém-alocada. malloc
O formato de uso do comando é o seguinte:
%ptr = call i8* @malloc(i64 <size>)
onde <size>
é o tamanho do bloco de memória a ser alocado. Por exemplo, o código a seguir aloca uma matriz de 10 inteiros:
%size = mul i64 10, i64 4
%ptr = call i8* @malloc(i64 %size)
%array = bitcast i8* %ptr to i32*
Neste exemplo, %size
é o número de bytes ocupados por 10 inteiros, call
a instrução chama malloc
a função para alocar memória, %ptr
é um ponteiro para o bloco de memória recém-alocado e bitcast
a instrução %ptr
converte o ponteiro em um tipo de ponteiro inteiro.
6. grátis
free
As diretivas são usadas para liberar memória previamente malloc
alocada por diretivas. free
O formato de uso do comando é o seguinte:
call void @free(i8* <ptr>)
onde <ptr>
é um ponteiro para o bloco de memória a ser liberado. Por exemplo, o código a seguir libera um array inteiro alocado anteriormente:
%ptr = bitcast i32* %array to i8*
call void @free(i8* %ptr)
Neste exemplo, é um ponteiro para um array de inteiros %array
previamente alocados pela instrução, que converte o ponteiro em um ponteiro do tipo, e a instrução chama uma função para liberar a memória.malloc
bitcast
%array
i8*
call
free
7.memset
memset
A instrução é usada para definir o conteúdo de uma área de memória para um valor especificado. Sua sintaxe básica é a seguinte:
call void @llvm.memset.p0i8.i64(i8* %dst, i8 %val, i64 %size, i1 0)
Dentre eles, o primeiro parâmetro %dst
é o endereço inicial da área de memória a ser configurada, que deve ser do tipo ponteiro. O segundo parâmetro %val
é o valor a definir, deve ser um número inteiro. O terceiro parâmetro %size
é o tamanho da área de memória, que deve ser um inteiro de 64 bits. O último parâmetro é um booleano indicando o alinhamento. Se for 1, significa alinhado de acordo com o tipo de ponteiro; se for 0, significa não alinhado de acordo com o tipo de ponteiro.
Aqui está um exemplo de uso simples que define todos os elementos em uma matriz inteira como 0:
define void @set_to_zero(i32* %array, i32 %size) {
entry:
%zero = alloca i32, align 4
store i32 0, i32* %zero, align 4
%array_end = getelementptr i32, i32* %array, i32 %size
call void @llvm.memset.p0i8.i64(i8* %array, i8 0, i64 sub(i32* %array_end, %array), i1 false)
ret void
}
8.memcpy
memcpy
As instruções são usadas para copiar o conteúdo de uma região de memória para outra região de memória. Sua sintaxe básica é a seguinte:
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %dst, i8* %src, i64 %size, i1 0)
Dentre eles, o primeiro parâmetro %dst
é o endereço inicial da área de memória alvo, que deve ser do tipo ponteiro. O segundo parâmetro %src
é o endereço inicial da área de memória de origem, que deve ser do tipo ponteiro. O terceiro parâmetro %size
é o tamanho da área de memória, que deve ser um inteiro de 64 bits. O último parâmetro é um booleano indicando o alinhamento. Se for 1, significa alinhado de acordo com o tipo de ponteiro; se for 0, significa não alinhado de acordo com o tipo de ponteiro.
Aqui está um exemplo de uso simples para copiar um array de inteiros para outro array:
define void @copy_array(i32* %src, i32* %dst, i32 %size) {
entry:
%src_end = getelementptr i32, i32* %src, i32 %size
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %dst, i8* %src, i64 sub(i32* %src_end, %src), i1 false)
ret void
}
9.memover
A instrução memmove é utilizada para mover os dados do bloco de memória apontado pelo endereço de origem para o bloco de memória apontado pelo endereço de destino, e sua definição é a seguinte:
declare void @llvm.memmove.p0i8.p0i8.i32(i8* nocapture, i8* nocapture, i32, i32, i1)
A instrução aceita cinco parâmetros, que são endereço de destino, endereço de origem, número de bytes a serem copiados, alinhamento e sinalizador para verificação de sobreposição. Entre eles, o parâmetro de alinhamento indica o alinhamento do bloco de memória e é definido como 1 se nenhum alinhamento for necessário. O sinalizador precisa ser definido como verdadeiro se a verificação de sobreposição for feita, caso contrário, como falso.
O seguinte é um exemplo de uso da instrução memmove para mover os dados no bloco de memória apontado pelo endereço de origem para o bloco de memória apontado pelo endereço de destino:
%src = alloca [10 x i32], align 4
%dst = alloca [10 x i32], align 4
%size = getelementptr [10 x i32], [10 x i32]* %src, i32 0, i32 10
%sizeVal = ptrtoint i32* %size to i32
call void @llvm.memmove.p0i8.p0i8.i32(i8* bitcast ([10 x i32]* %dst to i8*), i8* bitcast ([10 x i32]* %src to i8*), i32 %sizeVal, i32 4, i1 false)
Neste exemplo, dois blocos de memória do tipo [10 x i32] são primeiro alocados na pilha e o tamanho do bloco de memória é obtido por meio da instrução getelementptr. Em seguida, a instrução memmove é chamada para mover os dados no bloco de memória apontado pelo endereço de origem para o bloco de memória apontado pelo endereço de destino. Deve-se notar que a instrução bitcast precisa ser usada para converter os endereços de origem e destino em ponteiros do tipo i8*.
instruções de fluxo de controle
As instruções de fluxo de controle incluem as seguintes instruções:
-
br
: Instrução de ramificação condicional, salta para o bloco básico especificado de acordo com a condição. -
switch
: Instrução de ramificação multidirecional, salta para diferentes blocos básicos de acordo com o valor de entrada. -
ret
: A função retorna a instrução, retornando ao local onde a função foi chamada. -
indirectbr
: Instrução de ramificação indireta, salta para o bloco básico armazenado no endereço especificado. -
invoke
: chama a instrução, chama uma função com manipulação de exceção e passa o controle quando ocorre uma exceção. -
resume
: Instrução de recuperação de exceção, que restaura a exceção que ocorreu ao chamar a instrução de chamada. -
unreachable
: Instrução inacessível, indicando que o programa não deve ser executado até este ponto, e se for executado até este ponto, levará a um comportamento indefinido.
Essas instruções são usadas no LLVM IR para controlar o fluxo de programas, permitindo técnicas avançadas de otimização, como análise de fluxo de controle e renomeação de variáveis com base no formulário SSA. As instruções de fluxo de controle do LLVM incluem instruções de ramificação condicional br
, instruções de ramificação multidirecional switch
, instruções de retorno de função , ret
instruções de ramificação indireta , indirectbr
instruções de chamada invoke
, instruções de recuperação de exceção resume
e instruções inacessíveis unreachable
. O seguinte explicará essas instruções uma a uma e fornecerá exemplos de código correspondentes.
1. Instrução de desvio condicional (br)
br
As instruções são usadas para executar desvios condicionais, saltando para diferentes blocos básicos com base nas condições. Sua sintaxe é a seguinte:
br i1 <cond>, label <iftrue>, label <iffalse>
onde <cond>
é o valor da condição, se seu valor for verdadeiro, salta para o <iftrue>
bloco básico marcado com; caso contrário, salta para o <iffalse>
bloco básico marcado com . Aqui está um exemplo simples:
define i32 @test(i32 %a, i32 %b) {
%cmp = icmp eq i32 %a, %b
br i1 %cmp, label %equal, label %notequal
equal:
ret i32 1
notequal:
ret i32 0
}
Neste exemplo, definimos uma função test
que recebe dois argumentos inteiros %a
e %b
. Primeiro, icmp
comparamos os dois valores de igualdade usando a instrução e salvamos o resultado em %cmp
. Em seguida, usamos br
a instrução %cmp
para pular para um bloco básico diferente com base no valor de e retornar se forem iguais 1
; caso contrário 0
.
2. Instrução de ramificação multidirecional (interruptor)
switch
As instruções são usadas para executar ramificações multidirecionais, saltando para diferentes blocos básicos, dependendo do valor de entrada. Sua sintaxe é a seguinte:
switch <intty> <value>, label <defaultdest> [ <intty> <val>, label <dest> ... ]
Entre eles, <intty>
está o tipo inteiro, <value>
é o valor de entrada e <defaultdest>
é o bloco básico do salto padrão. Cada par subseqüente <val>, <dest>
representa uma opção e, se for <value>
igual <val>
, salta para <dest>
o bloco básico marcado. Aqui está um exemplo:
define i32 @test(i32 %a) {
switch i32 %a, label %default [
i32 0, label %zero
i32 1, label %one
]
zero:
ret i32 0
one:
ret i32 1
default:
ret i32 -1
}
Neste exemplo, definimos uma função test
que recebe um parâmetro inteiro %a
. Em seguida, usamos switch
a instrução %a
para pular para um bloco básico diferente com base no valor de , e retornar se for %a
igual ; retornar se for igual ; caso contrário, retornar .0
0
%a
1
1
-1
3. Instrução de retorno de função (ret)
ret
Diretivas são usadas para retornar um valor de uma função. Sua sintaxe é a seguinte:
ret <type> <value>
Entre eles, <type>
está o tipo do valor de retorno e <value>
é o valor retornado. Se a função não retornar um valor, ela <type>
deveria void
. Aqui está um exemplo:
define i32 @test(i32 %a, i32 %b) {
%sum = add i32 %a, %b
ret i32 %sum
}
Neste exemplo, definimos uma função test
que recebe dois argumentos inteiros %a
e %b
. Primeiro, nós add
os adicionamos usando a instrução e salvamos o resultado em formato %sum
. Então, usamos o valor ret
retornado pela instrução %sum
.
4. Instrução de ramificação indireta (indirectbr)
indirectbr
As instruções são usadas para pular para diferentes blocos básicos com base em endereços indiretos. Sua sintaxe é a seguinte:
indirectbr <type> <address>, [ label <dest1>, label <dest2>, ... ]
Entre eles, <type>
está o tipo de destino do salto <address>
e um ponteiro para o endereço do destino do salto. Cada um dos <dest>
tokens a seguir representa um bloco básico de alvo de salto. Aqui está um exemplo:
define i32 @test(i32* %ptr) {
%dest1 = label %one
%dest2 = label %two
indirectbr i8* %ptr, [ label %default, label %dest1, label %dest2 ]
one:
ret i32 1
two:
ret i32 2
default:
ret i32 -1
}
Neste exemplo, definimos uma função test
que leva um argumento de ponteiro para o endereço de um número inteiro %ptr
. Em seguida, definimos três tags, rotuladas como %one
, %two
e %default
. Em seguida, usamos indirectbr
a instrução %ptr
para pular para um bloco básico diferente com base no valor de , 0
e retornar se for igual 1
; se for igual 1
, retornar 2
; caso contrário, retornar -1
.
5. Instrução de chamada (invocação)
invoke
As instruções são usadas para chamar uma função e passar o controle quando ocorre uma exceção. Sua sintaxe call
é semelhante à instrução, mas possui uma ramificação de tratamento de exceção. Aqui está um exemplo:
define void @test() {
%catch = catchswitch within none [label %catch] unwind label %cleanup
invoke void @foo()
to label %normal
unwind label %catch
normal:
catchret from %catch to label %end
end:
ret void
catch:
%excp = catchpad within %catch [i8* null]
call void @handle()
catchret from %excp to label
Dentre elas, definimos uma função test
, que não aceita nenhum parâmetro. Primeiro, usamos catchswitch
a diretiva para criar um bloco de tratamento de exceção %catch
e, em seguida, usamos invoke
a diretiva para chamar a função foo
. Se a chamada da função for bem-sucedida, pule para a tag %normal
; caso contrário, pule para o bloco de tratamento de exceção %catch
. No marcador %normal
, usamos catchret
a diretiva para retornar o controle ao bloco de tratamento de exceção %catch
. No bloco de tratamento de exceção %catch
, usamos catchpad
a diretiva para criar um bloco de tratamento de exceção %excp
e chamar handle
a função para lidar com a exceção. Por fim, usamos catchret
a diretiva para retornar o controle à tag invoke
da diretiva .unwind
%cleanup
6. Comando Resume (resume)
resume
instrução para retomar a execução de um bloco de tratamento de exceção. Sua sintaxe é a seguinte:
resume <type> <value>
onde <type>
é o tipo de exceção a ser recuperado e <value>
é o valor atípico. Aqui está um exemplo:
define void @test() {
%catch = catchswitch within none [label %catch] unwind label %cleanup
invoke void @foo()
to label %normal
unwind label %catch
normal:
catchret from %catch to label %end
end:
ret void
catch:
%excp = catchpad within %catch [i8* null]
%is_error = icmp eq i32 %excp, 1
br i1 %is_error, label %handle, label %next
handle:
call void @handle()
resume void null
next:
catchret from %excp to label %end
}
Neste exemplo, usamos resume
a instrução para retomar a execução do bloco de tratamento de exceção. Na marcação %catch
, catchpad
criamos um bloco de tratamento de exceção com uma diretiva %excp
. Em seguida, usamos a instrução para comparar icmp
o outlier com , e saltamos para o sinalizador ou sinalizadores, dependendo da comparação . No marcador , chamamos a função para tratar a exceção e usamos a instrução para retomar a execução do bloco de tratamento de exceção. No marcador , usamos a diretiva para retornar o controle ao marcador da diretiva .1
%handle
%next
%handle
handle
resume
%next
catchret
invoke
unwind
%cleanup
7. Comando inacessível (inacessível)
unreachable
As instruções são usadas para indicar que o programa não será executado aqui. Sua sintaxe é a seguinte:
unreachable
Aqui está um exemplo:
define i32 @test(i32 %a, i32 %b) {
%is_zero = icmp eq i32 %b, 0
br i1 %is_zero, label %error, label %compute
compute:
%result = sdiv i32 %a, %b
ret i32 %result
error:
unreachable
}
Neste exemplo, definimos uma função test
cuja função é calcular a / b
um valor. Na marcação %compute
, usamos o valor sdiv
calculado pela diretiva a / b
e ret
retornamos o resultado usando a diretiva. No marcador %error
, usamos unreachable
a instrução para indicar que o programa não será executado aqui porque b
o valor de 0
. Nesse caso, não precisamos retornar nada, pois o programa já travou.
outras instruções
Além das sete instruções de fluxo de controle apresentadas acima, o llvm possui outras instruções comumente usadas. Neste artigo, apresentaremos cinco instruções, phi, select, call, va_arg e landingpad.
1.phi
A instrução phi é usada para passar valores entre blocos básicos. Sua sintaxe é a seguinte:
%result = phi <type> [ <value1>, <label1> ], [ <value2>, <label2> ], ...
onde <type>
é o tipo do valor a ser passado, <value1>
é o primeiro valor a ser passado <label1>
e é o bloco básico do qual passar o primeiro valor. As outras <value>
somas <label>
são semelhantes. Aqui está um exemplo:
define i32 @test(i32 %a, i32 %b) {
%cmp = icmp slt i32 %a, %b
br i1 %cmp, label %if_true, label %if_false
if_true:
%result1 = add i32 %a, 1
br label %merge
if_false:
%result2 = add i32 %b, 1
br label %merge
merge:
%result = phi i32 [ %result1, %if_true ], [ %result2, %if_false ]
ret i32 %result
}
Neste exemplo, definimos uma função test
que compara a
valores de soma b
e retorna um resultado. Na marca %if_true
, usamos o valor add
calculado pela instrução a+1
, na marca %if_false
, usamos o valor add
calculado pela instrução b+1
. Então, no marcador %merge
, usamos phi
a diretiva para selecionar um valor. Especificamente, se %cmp
o valor de for true
, escolhemos %result1
o valor de (ie a+1
); caso contrário, escolhemos %result2
o valor de (ie b+1
).
2.selecione
A diretiva select é usada para selecionar um dos dois valores com base em uma condição. Sua sintaxe é a seguinte:
%result = select i1 <cond>, <type> <iftrue>, <type> <iffalse>
onde <cond>
é a condição a ser testada, <iftrue>
é o valor a ser retornado se a condição for verdadeira e <iffalse>
é o valor a ser retornado se a condição for falsa. Aqui está um exemplo:
define i32 @test(i32 %a, i32 %b) {
%cmp = icmp slt i32 %a, %b
%result = select i1 %cmp, i32 %a, i32 %b
ret i32 %result
}
Neste exemplo, definimos uma função test
que compara a
valores de soma b
e retorna um resultado. Usamos icmp
a instrução para comparar a
e armazenar o resultado da comparação em . Em seguida, usamos diretivas baseadas em `b
%cmp
select
3. ligar
A instrução call é usada para chamar uma função. Sua sintaxe é a seguinte:
%result = call <type> <function>(<argument list>)
onde <type>
é o tipo do valor de retorno da função, <function>
é o nome da função a ser chamada e <argument list>
é a lista de parâmetros da função. Aqui está um exemplo:
declare i32 @printf(i8*, ...)
define i32 @test(i32 %a, i32 %b) {
%sum = add i32 %a, %b
%format_str = getelementptr inbounds [4 x i8], [4 x i8]* @.str, i64 0, i64 0
call i32 (i8*, ...) @printf(i8* %format_str, i32 %sum)
ret i32 %sum
}
Neste exemplo, primeiro usamos o valor add
calculado pela diretiva e, em seguida, usamos a diretiva para obter um ponteiro para a string global que contém a string formatada . Finalmente, chamamos a função usando a diretiva , passando o e como parâmetros para ela.a+b
getelementptr
@.str
%d\n
call
printf
%format_str
%sum
4.va_arg
A diretiva va_arg é usada para obter o próximo argumento em uma função variádica. Sua sintaxe é a seguinte:
%result = va_arg <type*> <ap>
onde <type*>
é um ponteiro para o tipo de parâmetro e <ap>
é um ponteiro que contém a lista de parâmetros. Aqui está um exemplo:
declare i32 @printf(i8*, ...)
define i32 @test(i32 %a, i32 %b, ...) {
%ap = alloca i8*, i32 0
store i8* %0, i8** %ap
%sum = add i32 %a, %b
%format_str = getelementptr inbounds [4 x i8], [4 x i8]* @.str, i64 0, i64 0
%next_arg = va_arg i32*, i8** %ap
%value = load i32, i32* %next_arg
%product = mul i32 %sum, %value
call i32 (i8*, ...) @printf(i8* %format_str, i32 %product)
ret i32 %sum
}
Neste exemplo, definimos uma função variádica test
que recebe dois argumentos inteiros a
e b
um número variável de outros argumentos. Primeiro usamos alloca
a instrução para alocar um ponteiro na pilha para armazenar o endereço do próximo parâmetro. Em seguida, usamos store
a instrução para armazenar o endereço do primeiro argumento nesse ponteiro. Em seguida, usamos va_arg
a instrução para obter o endereço do próximo parâmetro e, em seguida, usamos load
a instrução para carregar o valor desse parâmetro em %value
. Por fim, usamos o valor mul
calculado pela instrução (%a + %b) * %value
e printf
o enviamos para a saída padrão usando a função.
5.landing pad
A diretiva landingpad é usada para implementar o tratamento de exceções. Sua sintaxe é a seguinte:
%result = landingpad <type>
Entre eles, <type>
está o tipo do valor de retorno da função de tratamento de exceção. Aqui está um exemplo:
declare void @llvm.landingpad(i8*, i32)
define void @test() personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*) {
%exn = landingpad i8*
%exn_val = extractvalue {
i8*, i32 } %exn, 0
%exn_selector = extractvalue {
i8*, i32 } %exn, 1
call void @printf(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str, i64 0, i64 0), i8* %exn_val, i32 %exn_selector)
resume {
i8*, i32 } %exn
}
Neste exemplo, definimos uma função test
que implementa o tratamento de exceções. Primeiro, usamos personality
a palavra-chave para especificar a função de tratamento de exceção __gxx_personality_v0
, que faz parte da biblioteca de tratamento de exceção GCC C++. Em seguida, usamos landingpad
a diretiva para obter o objeto de exceção e o número do tipo de exceção. Usamos extractvalue
diretivas para dividi-lo em objeto de exceção e número do tipo de exceção e passá-los para printf
a função. Por fim, usamos resume
a diretiva para lançar novamente a exceção.
O mecanismo de tratamento de exceções aqui é relativamente complicado e requer um entendimento mais aprofundado, então não vou repeti-lo aqui.