Explicação detalhada da instrução IR do LLVM

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:

  1. Instruções de adição, subtração, multiplicação e divisão: add, sub, mul, sdiv, udiv, fadd, fsub, fmul, fdiv, etc.
  2. Instruções de operação de bit: and, or, xor, shl, lshr, ashr, etc.
  3. Instruções de conversão: trunc, zext, sext, fptrunc, fpext, fptoui, fptosi, uitofp, sitofp, ptrtoint, inttoptr, bitcast, etc.
  4. Instruções de memória: alloca, load, store, getelementptr, etc.
  5. Instruções de fluxo de controle: br, switch, ret, indirectbr, invocar, retomar, inacessível, etc.
  6. 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 addda 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, i128etc.;
  • Tipos de ponto flutuante: half, float, double, fp128etc.;
  • 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: i11 bit, i88 bits, i1616 bits, i3232 bits, i6464 bits, i128128 bits;
  • Tipo de ponto flutuante: half16 bits, float32 bits, double64 bits, fp128128 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: metadatao 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 i16Pass i16 123para 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 2e 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 %ae %b, e armazena o resultado no registrador %x; a segunda linha de código adiciona os valores nos registradores %xe %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 %xsubtrai o valor no registro %ydo 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 %xsubtrai o número de ponto flutuante de precisão simples no registrador %ydo 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 %xe , 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 %value1e , 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%resultdoublefloat

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 %xdivide o valor no registrador %ypelo 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 udiva instrução:

%quot = udiv i32 %x, %y

Esta instrução %xdivide o valor no registrador %ypelo valor em e armazena o resultado no registrador %quot. Por causa udivda 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:

  1. E bit a bit (e): executa uma operação AND bit a bit nas representações binárias de dois inteiros.
  2. Bitwise or (or): executa uma operação bitwise OR nas representações binárias de dois inteiros.
  3. XOR bit a bit (xor): executa uma operação XOR bit a bit nas representações binárias de dois inteiros.
  4. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. fptoui: converte um número de ponto flutuante em um inteiro sem sinal. Se o float for negativo, o resultado é zero.
  7. 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.
  8. uitofp: converte um inteiro sem sinal em um número de ponto flutuante.
  9. sitofp: converte um inteiro com sinal em um número de ponto flutuante.
  10. 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.
  11. 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.
  12. 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

truncA 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. truncO 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

zextA 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. zextO 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

sextA 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. sextAs diretivas são usadas em um formato zextsemelhante à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

fptruncA 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. fptruncO 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

fpextA 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. fpextAs diretivas são usadas em um formato fptruncsemelhante à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 %floatde 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

fptouiA 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. fptouiO 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 %doubleo valor de é positivo, ele pode ser convertido em um inteiro sem sinal de 32 bits.

7. ptose

fptosiA 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. fptosiO 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 %doubleo valor de é negativo, ele pode ser convertido em um inteiro com sinal de 32 bits.

8.offofp

uitofpA instrução converte um inteiro sem sinal em um número de ponto flutuante. uitofpO 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 %uinto valor de é positivo, ele pode ser convertido em um número de ponto flutuante de precisão simples.

9.sitofp

sitofpA instrução converte um inteiro com sinal em um número de ponto flutuante. sitofpO 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 %i32o valor de é negativo, ele pode ser convertido em um número de ponto flutuante de precisão simples.

10.ptrtoint

ptrtointA instrução converte um tipo de ponteiro em um tipo inteiro. ptrtointO 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, %ptrum ponteiro para um tipo inteiro de 32 bits. %i64é um tipo inteiro de 64 bits cujo valor é %ptro endereço de um ponteiro. Como a largura de bits do tipo ponteiro e do tipo inteiro são diferentes, é necessário usar ptrtointinstruções para conversão de tipo.

11.inttoptr

inttoptrA instrução converte um tipo de número inteiro em um tipo de ponteiro. inttoptrO 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 inttoptrpara a conversão de tipo.

12.bitcast

bitcastUma instrução converte a representação de bit de um valor em outro tipo, mas não altera o valor em si. bitcastO 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, bitcastas 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.mallocfreememsetmemcpymemmove

1.alloca

allocainstrução para alocar memória na pilha e retornar um ponteiro para a memória recém-alocada. allocaO 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

loadAs instruções são usadas para ler dados da memória e carregá-los nos registradores. loadO 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 loada instrução %ptrcarrega os dados no bloco de memória apontado para %valo registrador.

3.loja

storeAs instruções são usadas para gravar dados dos registradores na memória. storeO 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 storea instrução armazena o valor inteiro 42 no %ptrbloco de memória apontado.

4.getelementptr

getelementptrAs instruções são usadas para calcular deslocamentos de ponteiros para acessar dados na memória. getelementptrO 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. getelementptrAs 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, %ptrque é um ponteiro para o elemento na segunda linha e na terceira coluna.

5.malloc

mallocinstrução para alocar memória no heap e retornar um ponteiro para a memória recém-alocada. mallocO 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, calla instrução chama malloca função para alocar memória, %ptré um ponteiro para o bloco de memória recém-alocado e bitcasta instrução %ptrconverte o ponteiro em um tipo de ponteiro inteiro.

6. grátis

freeAs diretivas são usadas para liberar memória previamente mallocalocada por diretivas. freeO 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 %arraypreviamente 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.mallocbitcast%arrayi8*callfree

7.memset

memsetA 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

memcpyAs 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:

  1. br: Instrução de ramificação condicional, salta para o bloco básico especificado de acordo com a condição.

  2. switch: Instrução de ramificação multidirecional, salta para diferentes blocos básicos de acordo com o valor de entrada.

  3. ret: A função retorna a instrução, retornando ao local onde a função foi chamada.

  4. indirectbr: Instrução de ramificação indireta, salta para o bloco básico armazenado no endereço especificado.

  5. invoke: chama a instrução, chama uma função com manipulação de exceção e passa o controle quando ocorre uma exceção.

  6. resume: Instrução de recuperação de exceção, que restaura a exceção que ocorreu ao chamar a instrução de chamada.

  7. 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 , retinstruções de ramificação indireta , indirectbrinstruções de chamada invoke, instruções de recuperação de exceção resumee 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)

brAs 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 testque recebe dois argumentos inteiros %ae %b. Primeiro, icmpcomparamos os dois valores de igualdade usando a instrução e salvamos o resultado em %cmp. Em seguida, usamos bra instrução %cmppara 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)

switchAs 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 testque recebe um parâmetro inteiro %a. Em seguida, usamos switcha instrução %apara pular para um bloco básico diferente com base no valor de , e retornar se for %aigual ; retornar se for igual ; caso contrário, retornar .00%a11-1

3. Instrução de retorno de função (ret)

retDiretivas 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 testque recebe dois argumentos inteiros %ae %b. Primeiro, nós addos adicionamos usando a instrução e salvamos o resultado em formato %sum. Então, usamos o valor retretornado pela instrução %sum.

4. Instrução de ramificação indireta (indirectbr)

indirectbrAs 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 testque leva um argumento de ponteiro para o endereço de um número inteiro %ptr. Em seguida, definimos três tags, rotuladas como %one, %twoe %default. Em seguida, usamos indirectbra instrução %ptrpara pular para um bloco básico diferente com base no valor de , 0e retornar se for igual 1; se for igual 1, retornar 2; caso contrário, retornar -1.

5. Instrução de chamada (invocação)

invokeAs 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 catchswitcha diretiva para criar um bloco de tratamento de exceção %catche, em seguida, usamos invokea 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 catchreta diretiva para retornar o controle ao bloco de tratamento de exceção %catch. No bloco de tratamento de exceção %catch, usamos catchpada diretiva para criar um bloco de tratamento de exceção %excpe chamar handlea função para lidar com a exceção. Por fim, usamos catchreta diretiva para retornar o controle à tag invokeda diretiva .unwind%cleanup

6. Comando Resume (resume)

resumeinstruçã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 resumea instrução para retomar a execução do bloco de tratamento de exceção. Na marcação %catch, catchpadcriamos um bloco de tratamento de exceção com uma diretiva %excp. Em seguida, usamos a instrução para comparar icmpo 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%handlehandleresume%nextcatchretinvokeunwind%cleanup

7. Comando inacessível (inacessível)

unreachableAs 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 testcuja função é calcular a / bum valor. Na marcação %compute, usamos o valor sdivcalculado pela diretiva a / be retretornamos o resultado usando a diretiva. No marcador %error, usamos unreachablea instrução para indicar que o programa não será executado aqui porque bo 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 testque compara avalores de soma be retorna um resultado. Na marca %if_true, usamos o valor addcalculado pela instrução a+1, na marca %if_false, usamos o valor addcalculado pela instrução b+1. Então, no marcador %merge, usamos phia diretiva para selecionar um valor. Especificamente, se %cmpo valor de for true, escolhemos %result1o valor de (ie a+1); caso contrário, escolhemos %result2o 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 testque compara avalores de soma be retorna um resultado. Usamos icmpa instrução para comparar ae armazenar o resultado da comparação em . Em seguida, usamos diretivas baseadas em `b%cmpselect

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 addcalculado 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+bgetelementptr@.str%d\ncallprintf%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 testque recebe dois argumentos inteiros ae bum número variável de outros argumentos. Primeiro usamos allocaa instrução para alocar um ponteiro na pilha para armazenar o endereço do próximo parâmetro. Em seguida, usamos storea instrução para armazenar o endereço do primeiro argumento nesse ponteiro. Em seguida, usamos va_arga instrução para obter o endereço do próximo parâmetro e, em seguida, usamos loada instrução para carregar o valor desse parâmetro em %value. Por fim, usamos o valor mulcalculado pela instrução (%a + %b) * %valuee printfo 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 testque implementa o tratamento de exceções. Primeiro, usamos personalitya 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 landingpada diretiva para obter o objeto de exceção e o número do tipo de exceção. Usamos extractvaluediretivas para dividi-lo em objeto de exceção e número do tipo de exceção e passá-los para printfa função. Por fim, usamos resumea 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.

Referências

  1. Manual de referência da linguagem LLVM — documentação do LLVM 17.0.0git

Acho que você gosta

Origin blog.csdn.net/HongHua_bai/article/details/129172305
Recomendado
Clasificación