Existem casos inteligentes de modificação do código de tempo de execução?

119

Você consegue pensar em algum uso legítimo (inteligente) para modificação de código de tempo de execução (programa modificando seu próprio código em tempo de execução)?

Os sistemas operacionais modernos parecem desaprovam programas que fazem isso, pois essa técnica foi usada por vírus para evitar a detecção.

Tudo o que consigo pensar é em algum tipo de otimização de tempo de execução que removeria ou acrescentaria algum código sabendo algo em tempo de execução que não pode ser conhecido em tempo de compilação.

deo
fonte
8
Nas arquiteturas modernas, isso interfere muito no cache e no pipeline de instruções: o código auto-modificável acabaria não modificando o cache, portanto você precisaria de barreiras e isso provavelmente tornaria seu código lento. E você não pode modificar o código que já está no pipeline de instruções. Portanto, qualquer otimização baseada no código de modificação automática deve ser realizada muito antes do código ser executado para ter um impacto de desempenho superior a, por exemplo, uma verificação de tempo de execução.
Alexandre C.
7
@ Alexandre: é comum o código auto-modificável fazer modificações raramente (por exemplo, uma vez, duas vezes), apesar de ter sido executado um número arbitrário de vezes, para que o custo único possa ser insignificante.
Tony Delroy
7
Não sei por que isso está marcado como C ou C ++, pois nenhum deles possui nenhum mecanismo para isso.
MSalters
4
@ Alexandre: O Microsoft Office é conhecido por fazer exatamente isso. Como conseqüência (?), Todos os processadores x86 têm excelente suporte para código de modificação automática. Em outros processadores, é necessária uma sincronização dispendiosa, o que torna a coisa toda menos atraente.
Mackie Messer
3
@Cawas: Normalmente, o software de atualização automática baixa novos assemblies e / ou executáveis ​​e sobrescreve os existentes. Em seguida, reiniciará o software. Isto é o que o Firefox, o Adobe, etc. A modificação automática normalmente significa que, durante o tempo de execução, o código é reescrito na memória pelo aplicativo devido a alguns parâmetros e não necessariamente persistente no disco. Por exemplo, ele pode otimizar caminhos de código inteiros se puder detectar de maneira inteligente esses caminhos não seriam exercidos durante essa execução específica para acelerar a execução.
NotMe

Respostas:

117

Existem muitos casos válidos para modificação de código. A geração de código em tempo de execução pode ser útil para:

  • Algumas máquinas virtuais usam a compilação JIT para melhorar o desempenho.
  • Gerar funções especializadas em tempo real é comum em gráficos de computador. Veja, por exemplo, as compensações de Rob Pike e Bart Locanthi e John Reiser Hardware Software para Bitmap Graphics on the Blit (1984) ou esta publicação (2006) de Chris Lattner sobre o uso de LLVM pela Apple para especialização de código de tempo de execução em sua pilha OpenGL.
  • Em alguns casos, o software recorre a uma técnica conhecida como trampolim, que envolve a criação dinâmica de código na pilha (ou em outro local). Exemplos são as funções aninhadas do GCC e o mecanismo de sinal de alguns Unices.

Às vezes, o código é traduzido em código em tempo de execução (isso é chamado de tradução binária dinâmica ):

  • Emuladores como o Rosetta da Apple usam essa técnica para acelerar a emulação. Outro exemplo é o software de transformação de código da Transmeta .
  • Depuradores e criadores de perfil sofisticados, como Valgrind ou Pin, usam-no para instrumentar seu código enquanto ele está sendo executado.
  • Antes que as extensões fossem feitas no conjunto de instruções x86, softwares de virtualização como o VMWare não podiam executar diretamente o código x86 privilegiado nas máquinas virtuais. Em vez disso, teve que traduzir quaisquer instruções problemáticas em tempo real para um código personalizado mais apropriado.

A modificação do código pode ser usada para solucionar as limitações do conjunto de instruções:

  • Houve um tempo (há muito tempo, eu sei), em que os computadores não tinham instruções para retornar de uma sub-rotina ou endereçar indiretamente a memória. O código de modificação automática era a única maneira de implementar sub-rotinas, ponteiros e matrizes .

Mais casos de modificação de código:

  • Muitos depuradores substituem instruções para implementar pontos de interrupção .
  • Alguns vinculadores dinâmicos modificam o código em tempo de execução. Este artigo fornece algumas informações sobre a realocação em tempo de execução das DLLs do Windows, que é efetivamente uma forma de modificação de código.
Mackie Messer
fonte
10
Essa lista parece misturar exemplos de código que se modifica e código que modifica outro código, como vinculadores.
precisa saber é o seguinte
6
@ AShelly: Bem, se você considerar o vinculador / carregador dinâmico como parte do código, ele se modificará. Eles moram no mesmo espaço de endereço, então acho que esse é um ponto de vista válido.
Mackie Messer
1
Ok, a lista agora faz distinção entre programas e software do sistema. Espero que isto faça sentido. No final, qualquer classificação é discutível. Tudo se resume ao que exatamente você inclui na definição de programa (ou código).
Mackie Messer
35

Isso foi feito em computação gráfica, especificamente em renderizadores de software para fins de otimização. No tempo de execução, o estado de muitos parâmetros é examinado e uma versão otimizada do código do rasterizador é gerada (potencialmente eliminando muitos condicionais), o que permite renderizar primitivos gráficos, por exemplo, triângulos muito mais rapidamente.

trenki
fonte
5
Uma leitura interessante são os artigos Pixomatic em três partes de Michael Abrash sobre DDJ: drdobbs.com/architecture-and-design/184405765 , drdobbs.com/184405807 , drdobbs.com/184405848 . O segundo link (Parte 2) fala sobre o soldador de código Pixomatic para o pipeline de pixels.
Typo.pl
1
Um artigo muito bom sobre o assunto. Desde 1984, mas ainda assim uma boa leitura: Rob Pike, Bart Locanthi e John Reiser. Compensações de software de hardware para gráficos de bitmap na memória .
Mackie Messer
5
Charles Petzold, explica um exemplo deste tipo em um livro intitulado "Beautiful Code": amazon.com/Beautiful-Code-Leading-Programmers-Practice/dp/...
Nawaz
3
Esta resposta fala sobre geração de código, mas a questão é perguntando sobre a modificação do código ...
Timwi
3
@ Timwi - modificou o código. Em vez de manipular uma grande cadeia de ifs, ele analisou a forma uma vez e reescreveu o renderizador, para que fosse configurado para o tipo correto de forma sem precisar verificar todas as vezes. Curiosamente este é agora comum com código OpenCL - uma vez que é compilado em tempo real você pode reescrevê-lo para o caso específico em tempo de execução
Martin Beckett
23

Uma razão válida é porque o conjunto de instruções asm carece de algumas instruções necessárias, que você mesmo pode construir . Exemplo: No x86, não há como criar uma interrupção para uma variável em um registro (por exemplo, interromper com o número da interrupção no machado). Somente números const codificados no opcode foram permitidos. Com o código auto-modificador, pode-se imitar esse comportamento.

flolo
fonte
Justo. Existe algum uso dessa técnica? Parece perigoso.
Alexandre C.
4
@ Alexandre C .: Se bem me lembro, muitas bibliotecas de tempo de execução (C, Pascal, ...) precisavam do DOS vezes uma função para executar chamadas de interrupção. Como essa função obtém o número de interrupção como parâmetro, você tinha que fornecê-la (é claro que se o número fosse constante, você poderia ter gerado o código correto, mas isso não era garantido). E todas as bibliotecas o implementaram com código auto-modificador.
Fl4
Você pode usar uma caixa de opção para fazer isso sem modificação do código. A reduzir é que o código de saída será maior
phuclv
17

Alguns compiladores costumavam usá-lo para inicialização de variável estática, evitando o custo de uma condicional para acessos subseqüentes. Em outras palavras, eles implementam "execute este código apenas uma vez" substituindo esse código por no-ops na primeira vez em que é executado.

JoeG
fonte
1
Muito bom, especialmente se estiver evitando bloqueios / desbloqueios de mutex.
Tony Delroy
2
Realmente? Como isso ocorre para o código baseado em ROM ou para o código executado no segmento de código protegido contra gravação?
Ira Baxter
1
@Ira Baxter: qualquer compilador que emita código relocável sabe que o segmento de código é gravável, pelo menos durante a inicialização. Portanto, a declaração "alguns compiladores usaram" ainda é possível.
MSalters
17

Existem muitos casos:

  • Os vírus costumavam usar código auto-modificável para "desobstruir" seu código antes da execução, mas essa técnica também pode ser útil para frustrar a engenharia reversa, a quebra e a invasão indesejada
  • Em alguns casos, pode haver um ponto específico durante o tempo de execução (por exemplo, imediatamente após a leitura do arquivo de configuração) quando se sabe que - pelo resto da vida útil do processo - uma ramificação específica será sempre ou nunca tomada: em vez de desnecessariamente verificando alguma variável para determinar qual caminho ramificar, a própria instrução de ramificação pode ser modificada de acordo
    • Por exemplo, pode-se saber que apenas um dos tipos derivados possíveis será tratado, de forma que o despacho virtual possa ser substituído por uma chamada específica
    • Depois de detectar qual hardware está disponível, o uso de um código correspondente pode ser codificado
  • O código desnecessário pode ser substituído por instruções no-op ou um pulo sobre ele, ou ter o próximo trecho de código deslocado diretamente no lugar (mais fácil se usar opcodes independentes da posição)
  • O código escrito para facilitar sua própria depuração pode injetar uma instrução de interceptação / sinal / interrupção esperada pelo depurador em um local estratégico.
  • Algumas expressões predicadas baseadas na entrada do usuário podem ser compiladas no código nativo por uma biblioteca
  • Incluindo algumas operações simples que não são visíveis até o tempo de execução (por exemplo, da biblioteca carregada dinamicamente) ...
  • Adição condicional de etapas de auto-instrumentação / criação de perfil
  • As rachaduras podem ser implementadas como bibliotecas que modificam o código que as carrega (não exatamente "auto", mas precisando das mesmas técnicas e permissões).
  • ...

Alguns modelos de segurança de sistemas operacionais significam que o código auto-modificável não pode ser executado sem privilégios de administrador / root, tornando-o impraticável para uso geral.

Da Wikipedia:

O software aplicativo em execução em um sistema operacional com segurança W ^ X estrita não pode executar instruções nas páginas nas quais é permitido gravar - apenas o próprio sistema operacional pode escrever instruções na memória e posteriormente executá-las.

Nesses sistemas operacionais, mesmo programas como a Java VM precisam de privilégios root / admin para executar seu código JIT. (Veja http://en.wikipedia.org/wiki/W%5EX para mais detalhes)

Tony Delroy
fonte
2
Você não precisa de privilégios de root para modificar o código. Nem a Java VM.
Mackie Messer
Eu não sabia que alguns sistemas operacionais eram tão rigorosos. Mas certamente faz sentido em algumas aplicações. I fazer maravilha no entanto, se a execução de Java com privilégios de root se realmente aumentar a segurança ...
Mackie Messer
@ Mackie: Eu acho que deve diminuí-lo, mas talvez ele possa definir algumas permissões de memória e depois alterar o uid efetivo de volta para alguma conta de usuário ...?
Tony Delroy
Sim, eu esperaria que eles tivessem um mecanismo refinado para conceder permissões para acompanhar o modelo de segurança estrito.
Mackie Messer #
15

O SO Synthesis basicamente avaliou parcialmente seu programa com relação às chamadas de API e substituiu o código do SO pelos resultados. O principal benefício é que muitas verificações de erros desapareceram (porque se o seu programa não solicitar ao sistema operacional que faça algo estúpido, não será necessário verificar).

Sim, esse é um exemplo de otimização de tempo de execução.

Ira Baxter
fonte
Não consigo entender o ponto. Se digamos que uma chamada do sistema será proibida pelo sistema operacional, você provavelmente receberá um erro de volta, que precisará verificar o código, não é? Parece-me que modificar o executável em vez de retornar um código de erro é uma espécie de superengenharia.
Alexandre C.
@Alexandre C.: você pode eliminar verificações de ponteiro nulo dessa maneira. Muitas vezes, é trivialmente óbvio para o chamador que um argumento é válido.
MSalters
@ Alexandre: Você pode ler a pesquisa no link. Acho que eles tem speedups bastante impressionantes, e que seria o ponto: -}
Ira Baxter
2
Para syscalls relativamente triviais e não vinculados a E / S, a economia é significativa. Por exemplo, se você está escrevendo um deamon para o Unix, há um monte de syscalls que você faz para desconectar o stdio, configurar vários manipuladores de sinal etc. Se você sabe que os parâmetros de uma chamada são constantes e que o parâmetro os resultados sempre serão os mesmos (fechando stdin, por exemplo), grande parte do código que você executa no caso geral é desnecessário.
precisa
1
Se você ler a tese, o capítulo 8 contém alguns números realmente impressionantes sobre E / S em tempo real não trivial para aquisição de dados. Lembrando que esta é uma tese de meados dos anos 80, e a máquina em que ele estava operando era 10? Mhz 68000, ele conseguiu capturar dados de áudio com qualidade de CD (44.000 amostras por segundo) em software simples. Ele afirmou que as estações de trabalho da Sun (clássico Unix) só podiam atingir cerca de 1/5 dessa taxa. Eu sou um codificador de linguagem assembly antigo daqueles dias, e isso é bastante espetacular.
Ira Baxter
9

Muitos anos atrás, passei uma manhã tentando depurar algum código auto-modificável; uma instrução alterou o endereço de destino da instrução a seguir, ou seja, eu estava computando um endereço de filial. Foi escrito em linguagem assembly e funcionou perfeitamente quando eu percorri o programa, uma instrução de cada vez. Mas quando eu executei o programa falhou. Eventualmente, eu percebi que a máquina estava buscando 2 instruções da memória e (como as instruções foram dispostas na memória) a instrução que eu estava modificando já havia sido buscada e, portanto, a máquina estava executando a versão não modificada (incorreta) da instrução. Obviamente, quando eu estava depurando, ele fazia apenas uma instrução por vez.

Meu ponto de vista, o código auto-modificável pode ser extremamente desagradável para testar / depurar e muitas vezes tem suposições ocultas quanto ao comportamento da máquina (seja hardware ou virtual). Além disso, o sistema nunca pôde compartilhar páginas de código entre os vários threads / processos em execução nas (agora) máquinas com vários núcleos. Isso anula muitos dos benefícios da memória virtual, etc. Também invalidaria as otimizações de ramificações feitas no nível do hardware.

(Nota - eu não incluí o JIT na categoria de código auto-modificável. O JIT está traduzindo de uma representação do código para uma representação alternativa, não está modificando o código)

No geral, é apenas uma péssima idéia - muito legal, muito obscura, mas muito ruim.

é claro - se tudo o que você tem são 8080 e ~ 512 bytes de memória, talvez seja necessário recorrer a essas práticas.

Jay
fonte
1
Não sei, boas e más não parecem ser as categorias certas para pensar sobre isso. Claro que você realmente deve saber o que está fazendo e também por que está fazendo. Mas o programador que escreveu esse código provavelmente não queria que você visse o que o programa estava fazendo. É claro que é desagradável se você precisar depurar códigos como esse. Mas esse código provavelmente era para ser assim.
Mackie Messer
As CPUs modernas do x86 têm uma detecção SMC mais forte do que a exigida no papel: Observando instruções obsoletas sendo buscadas no x86 com código de modificação automática . E na maioria das CPUs não-x86 (como ARM), o cache de instruções não é coerente com os caches de dados, portanto, a liberação / sincronização manual é necessária antes que os bytes recém-armazenados possam ser executados como instruções de maneira confiável. community.arm.com/processors/b/blog/posts/… . De qualquer forma, o desempenho do SMC é terrível nas CPUs modernas, a menos que você modifique uma vez e execute várias vezes.
Peter Cordes
7

Do ponto de vista de um kernel do sistema operacional, todo o Just In Time Compiler e Linker Runtime executam a auto-modificação do texto do programa. Um exemplo importante seria o V8 ECMA Script Interpreter do Google.

datenwolf
fonte
5

Outro motivo do código de modificação automática (na verdade, um código de "geração automática") é implementar um mecanismo de compilação Just-in-time para desempenho. Por exemplo, um programa que lê uma expressão algébrica e a calcula em vários parâmetros de entrada pode converter a expressão em código de máquina antes de declarar o cálculo.

Giuseppe Guerrini
fonte
5

Você sabe a velha opinião que não há diferença lógica entre hardware e software ... também se pode dizer que não há diferença lógica entre código e dados.

O que é código de modificação automática? Código que coloca valores no fluxo de execução para que possa ser interpretado não como dados, mas como um comando. Certamente, existe o ponto de vista teórico nas linguagens funcionais de que realmente não há diferença. Estou dizendo que podemos fazer isso de maneira direta em linguagens imperativas e compiladores / intérpretes sem a presunção de status igual.

Estou me referindo no sentido prático de que os dados podem alterar os caminhos de execução do programa (em algum sentido, isso é extremamente óbvio). Estou pensando em algo como um compilador-compilador que cria uma tabela (uma matriz de dados) que se percorre na análise, passando de estado para estado (e também modificando outras variáveis), assim como um programa se move de comando para comando , modificando variáveis ​​no processo.

Portanto, mesmo na instância usual de onde um compilador cria espaço para código e se refere a um espaço para dados totalmente separado (a pilha), ainda é possível modificar os dados para alterar explicitamente o caminho da execução.

Mitch
fonte
4
Nenhuma diferença lógica, é verdade. No entanto, não vi muitos circuitos integrados auto-modificáveis.
perfil completo de Ira Baxter
@Mitch, IMO alterar o caminho exec não tem nada a ver com (auto) modificação de código. Além disso, você confunde dados com informações. Não posso responder o seu comentário à minha resposta no LSE porque eu sou banido de lá, desde fevereiro, por 3 anos (1.000 dias) por expressar na meta-LSE meu pov que americanos e britânicos não possuem inglês.
Gennady Vanin Геннадий Ванин
4

Eu implementei um programa usando evolução para criar o melhor algoritmo. Ele usou código auto-modificador para modificar o modelo de DNA.

David
fonte
2

Um caso de uso é o arquivo de teste EICAR, que é um arquivo COM executável legítimo do DOS para testar programas antivírus.

X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

Ele precisa usar a modificação de código próprio, porque o arquivo executável deve conter apenas caracteres ASCII imprimíveis / tipáveis ​​no intervalo [21h-60h, 7Bh-7Dh], o que limita significativamente o número de instruções codificáveis.

Os detalhes são explicados aqui


Também é usado para o envio de operações de ponto flutuante no DOS

Alguns compiladores serão emitidos CD xxcom xx variando de 0x34-0x3B em locais de instruções de ponto flutuante x87. Como CDé o opcode para intinstruções, ele pulará para a interrupção 34h-3Bh e emulará essa instrução no software se o coprocessador x87 não estiver disponível. Caso contrário, o manipulador de interrupção substituirá esses 2 bytes por, 9B Dxpara que as execuções posteriores sejam tratadas diretamente por x87 sem emulação.

Qual é o protocolo para emulação de ponto flutuante x87 no MS-DOS?

phuclv
fonte
1

O kernel do Linux possui módulos carregáveis ​​do kernel que fazem exatamente isso.

O Emacs também tem essa capacidade e eu o uso o tempo todo.

Qualquer coisa que suporte uma arquitetura de plug-in dinâmico está essencialmente modificando seu código em tempo de execução.

dietbuddha
fonte
4
dificilmente. ter uma biblioteca carregável dinamicamente que nem sempre é residente tem muito pouco a ver com o código auto-modificável.
Dov
1

Eu executo análises estatísticas em um banco de dados atualizado continuamente. Meu modelo estatístico é gravado e reescrito toda vez que o código é executado para acomodar novos dados que se tornam disponíveis.

David LeBauer
fonte
0

O cenário em que isso pode ser usado é um programa de aprendizado. Em resposta à entrada do usuário, o programa aprende um novo algoritmo:

  1. procura na base de código existente um algoritmo semelhante
  2. se nenhum algoritmo semelhante estiver na base de código, o programa apenas adiciona um novo algoritmo
  3. se existir um algoritmo semelhante, o programa (talvez com alguma ajuda do usuário) modifica o algoritmo existente para poder atender tanto ao objetivo antigo quanto ao novo objetivo

Há uma pergunta sobre como fazer isso em Java: Quais são as possibilidades de auto-modificação do código Java?

Serge Rogatch
fonte
-1

A melhor versão disso pode ser as macros Lisp. Diferentemente das macros C, que são apenas um pré-processador, o Lisp permite que você tenha acesso a toda a linguagem de programação o tempo todo. Este é o recurso mais poderoso do lisp e não existe em nenhum outro idioma.

Eu não sou um especialista, mas chame um dos cegos falando sobre isso! Há uma razão para eles dizerem que o Lisp é a linguagem mais poderosa e as pessoas inteligentes, não que provavelmente estejam certas.

Zachary K
fonte
2
Isso realmente cria código auto-modificador ou é apenas um pré-processador mais poderoso (que gera funções)?
Brendan Long
@Brendan: de fato, mas é o caminho certo para fazer o pré-processamento. Não há modificação no código de tempo de execução aqui.
Alexandre C.