Questão:
Como posso reverter a divisão / módulo de inteiro otimizado por operações constantes?
Dougall
2013-03-30 12:14:20 UTC
view on stackexchange narkive permalink

Ao compilar uma divisão ou módulo por uma constante, meu compilador (LLVM GCC) gera uma série de instruções que não entendo.

Quando eu compilo os seguintes exemplos mínimos:

  int mod7 (int x) {return x% 7;} int div7 (int x) {return x / 7;}  

O seguinte código é gerado:

  _mod7: push rbp mov rbp, rsp mov ecx, 0x92492493 mov eax, edi imul ecx add edx, edi mov eax, edx shr eax, 0x1f sar edx, 0x2 add edx, eax imul ecx, edx, 0x7 mov eax, edi sub eax, ecx pop rbp ret _div7: push rbp mov rbp, rsp mov ecx, 0x92492493 mov eax, edi imul ecx add edx, edi mov ecx, edx shr ecx, 0x1f sar edx, 0x2 mov eax , edx add eax, ecx pop rbp ret  
  • Como isso é matematicamente equivalente e de onde vêm as constantes?
  • Qual é a maneira mais fácil de virar a montagem de volta para C (para um constantes arbitrárias no lado direito)?
  • Como uma ferramenta, como um descompilador ou desmontador de análise, pode automatizar esse processo?
Isso às vezes é chamado de * multiplicação recíproca *. Aqui está uma [breve explicação] (http://www.nynaeve.net/?p=115) com links para recursos mais detalhados. Eu vi Hex-Rays digerir isso sem problemas.
Trzy respostas:
Peter Andersson
2013-03-31 14:26:13 UTC
view on stackexchange narkive permalink

Primeiro

Infelizmente, não parece que o MathJax está ativado nesta troca de pilha, então as partes matemáticas abaixo estão horrivelmente formatadas. Também estou longe de ser um matemático, então a notação pode estar errada em alguns lugares.

Compreendendo o número mágico e o código

O objetivo do código acima é reescrever uma divisão em uma multiplicação porque a divisão leva mais ciclos de clock do que uma multiplicação. Está na área de cerca de duas vezes mais ciclos, dependendo muito da CPU. Portanto, precisamos encontrar uma boa maneira sem ramificações de fazer isso. Se ramificarmos, provavelmente perderemos para simplesmente fazer a divisão.

Uma maneira é simplesmente perceber que a divisão é o mesmo que a multiplicação com o inverso do número, ou seja, . O problema é que é um número muito pobre para armazenar como um inteiro. Portanto, precisamos multiplicar o divisor e o dividendo por algum número. Como operamos com números de 32 bits e obtemos resultados de multiplicação em números de 64 bits, obtemos a melhor precisão com e também evitamos problemas de estouro. Então basicamente obtemos . Essa parte fracionária é o que nos causa problemas, porque causará erros de arredondamento.

Então, vamos tentar formalizar isso:

Onde é nosso multiplicando, por exemplo, , ou realmente qualquer número , mas funciona muito bem com nossos tamanhos de registro, pois podemos simplesmente descartar o registro inferior de 32 bits. é o número que você deve adicionar para tornar igualmente divisível por . é o número que desejamos dividir.

Podemos reescrever a equação acima, como

O que ilustra o que temos nosso dividendo dividido por nosso divisor e então um fator de erro de .

Estudando nossa equação original de , fica claro que podemos afetar muito pouco. precisa ser uma potência de 2, não pode ser muito grande ou corremos o risco de transbordar e não pode ser muito pequeno, pois tem um efeito negativo direto em nosso fator de erro . depende diretamente de e .

Então, vamos tentar que fornece uma fração máxima de erro de com o valor máximo de sendo , então , infelizmente isso não é menor que para que possamos obter erros de arredondamento.

Vamos aumentar o expoente de para , o que dá , fração máxima de erro que é menor que . Isso significa que nosso multiplicando é , que não é menor ou igual ao valor máximo com sinal que podemos armazenar em um registrador de 32 bits (). Então, em vez disso, fazemos o multiplicando . Como uma observação lateral, graças à magia do complemento de dois quando subtraímos , o número é que é quando interpretado como um número sem sinal. Mas estamos fazendo aritmética assinada aqui. Portanto, precisamos corrigir a expressão final adicionando . Isso também só resolve o problema de , para números negativos teremos um erro de 1, então precisamos adicionar 1 se tivermos um número negativo.

Essa é a explicação para a constante na multiplicação e como chegar a ele. Agora, vamos examinar o código:

 ; Carregar -1840700269mov ecx, 0x92492493; Carregar nmov eax, edi; n * -1840700269imul ecx; adicione n para compensar 2 ^ 32 subtraçãoadd edx, edi; verifique o bit de sinal de nosso resultmov ecx, edxshr ecx, 0x1f; divida por 2 ^ 2 para nos compensar usando y = 2 ^ 34 em vez de 2 ^ 32sar edx, 0x2mov eax, edx; adicione o valor do bit de sinal ao resultado finaladd eax, ecx  

Calculando o divisor do número mágico e do código

Eu não provei isso matematicamente, no entanto, se você quiser para recuperar o divisor de um dump de montagem como o que você mostrou, podemos fazer alguns exercícios mentais simples. Primeiro, precisamos perceber que o seguinte é válido

Onde é o ajuste que fizemos para colocar o valor na faixa de um valor de 32 bits. A partir do código, podemos conceber o seguinte, o deslocamento à direita por dois significa que temos , , , é desconhecido. Isso significa que estamos perdendo uma variável para realizar uma solução perfeita. No entanto, o efeito de é desprezível, pois seu propósito é trazer o divisor o mais próximo possível de seu valor inteiro. Isso significa que a solução pode ser encontrada resolvendo

Outro exemplo com o divisor 31337 que tem o multiplicando número mágico 140346763 e desloca para a direita 10 bits.

Finalmente

Para uma análise matemática completa de como isso funciona, incluindo todas as provas e algoritmos apropriados para calcular o números mágicos, deslocamentos e acréscimos, consulte Hacker's Delight, capítulo 10-3.

A questão não era apenas como calcular as constantes mágicas, mas também como recuperar o divisor.
Tentei responder. Não tive tempo de formular uma prova, então não tenho 100% de certeza se ela está correta.
Sob os pressupostos da engenharia reversa (se a divisão const / módulo por multiplicação é misturada com outras operações), pode-se converter a constante de multiplicação inteira em uma fração binária, cujo recíproco está relacionado ao operando da constante divisão / módulo até um desconhecido potência de 2 fatores multiplicativos. Deduzir a potência desconhecida de 2 fator às vezes é impossível devido à combinação e otimização com outras operações.
Para sua informação: a resposta parece boa com o aplicativo de troca de pilha, já que tem mathjax ativado para todos os sites
John Källén
2016-03-30 23:32:39 UTC
view on stackexchange narkive permalink

Aqui está uma resposta tardia. O descompilador Reko recupera os divisores inteiros realizando uma pesquisa de divisão e conquista usando mediantes.

Reko começa reconhecendo o padrão onde a palavra alta de um Produto de 64 bits ( r * c ) é usado. O multiplicador constante c é dividido por 2 ^ 32 para produzir um número de ponto flutuante de precisão dupla entre 0,0 e 1,0. Começando com os números racionais 0/0 e 1/1, Reko calcula uma sequência de mediantes que coloca entre colchetes o número de ponto flutuante. A partir dessa sequência de mediantes, ele escolhe o número racional que mais se aproxima do número de ponto flutuante e o retorna.

O código ainda não foi totalmente testado - não tive a chance de trabalhar com números negativos ainda, por um lado, mas parece dar resultados razoáveis. O código está aqui se você estiver curioso: https://github.com/uxmal/reko/blob/master/src/Decompiler/Analysis/ConstDivisionImplementedByMultiplication.cs

Sebastian Graf
2017-02-21 16:39:42 UTC
view on stackexchange narkive permalink

Este artigo pode ser de interesse: Divisão por multiplicação invariante.

Esbarre nisso aqui.



Estas perguntas e respostas foram traduzidas automaticamente do idioma inglês.O conteúdo original está disponível em stackexchange, que agradecemos pela licença cc by-sa 3.0 sob a qual é distribuído.
Loading...