运行时代码修改是否存在智能案例?

您能想到任何合法的(智能的)用于运行时代码修改(程序在运行时修改自己的代码)吗?

现代操作系统似乎不赞成这样做的程序,因为这种技术已经被病毒用来避免检测。

我所能想到的就是某种运行时优化,它通过在运行时知道一些在编译时无法知道的内容来删除或添加一些代码。

7222 次浏览

合成操作系统基本上部分地评估了您的程序的 API 调用,并用结果替换了操作系统代码。主要的好处是,大量的错误检查消失了(因为如果您的程序不会要求操作系统做一些愚蠢的事情,它就不需要检查)。

是的,这是运行时优化的一个例子。

这已经在计算机图形学中完成了,特别是出于优化目的的软件渲染器。在运行时检查许多参数的状态,并生成光栅化程序代码的优化版本(可能会消除许多条件) ,这允许人们更快地呈现图形原语,例如三角形。

这种情况有很多:

  • 病毒常用的程序自修改是在执行之前“解模糊”他们的代码,但是这种技术也可以用于挫败逆向工程、破解和不想要的黑客行为
  • 在某些情况下,在运行时(例如在读取配置文件后立即)可能会有一个特定的时间点,当知道在进程的剩余生命周期中,某个特定的分支将始终或永远不会被采用时: 不必检查某个变量来确定分支的方式,分支指令本身可以相应地进行修改
    • e.g. It may become known that only one of the possible derived types will be handled, such that virtual dispatch can be replaced with a specific call
    • 在检测到哪些硬件可用之后,可以对匹配代码进行硬编码
  • Unnecessary code can be replaced with no-op instructions or a jump over it, or have the next bit of code shifted directly into place (easier if using position-independent opcodes)
  • 为了方便自己的调试而编写的代码可能会在战略位置注入调试器所期望的陷阱/信号/中断指令。
  • 一些基于用户输入的谓词表达式可以由库编译成本机代码
  • Inlining some simple operations that aren't visible until runtime (e.g. from dynamically loaded library)...
  • 有条件地添加自检测/分析步骤
  • 裂缝可以实现为修改加载它们的代码的库(不是“自我”修改,而是需要相同的技术和权限)。
  • ...

Some OSs' security models mean self-modifying code can't run without root/admin privileges, making it impractical for general-purpose use.

来自维基百科:

在严格 W ^ X 安全的操作系统下运行的应用软件无法在允许写入的页面中执行指令ーー只有操作系统本身可以将指令写入内存,然后执行这些指令。

在这样的操作系统上,即使是像 Java VM 这样的程序也需要 root/admin 特权来执行它们的 JIT 代码。(详情请参阅 http://en.wikipedia.org/wiki/W%5EX)

一个合理的原因是因为 asm 指令集缺乏一些必要的指令,您可以自己进行 建造。示例: 在 x86上,没有办法对寄存器中的变量创建中断(例如,在 ax 中使用中断号创建中断)。只有编码到操作码中的常数才被允许。通过自我修改代码,人们可以模仿这种行为。

一些编译器过去常常将其用于静态变量初始化,从而避免了用于后续访问的条件的开销。换句话说,他们通过在第一次执行时用 no-ops 覆盖该代码来实现“只执行该代码一次”。

从操作系统内核的角度来看,每个 Just In Time Compiler 和 Linker Runtime 都执行程序文本自我修改。突出的例子是 Google 的 V8 ECMA 脚本解释器。

程序自修改的另一个原因(实际上是一个“自生成”代码)是为了实现一个即时编译的性能机制。例如,读取代数表达式并根据一系列输入参数进行计算的程序,在说明计算之前,可以用机器码转换表达式。

有许多有效的代码修改案例。在运行时生成代码可以用于:

  • 一些虚拟机使用 JIT 编译来提高性能。
  • Generating 专门职能 on the fly has long been common in computer graphics. See e.g. Rob Pike and Bart Locanthi and John Reiser 位图图形的硬件软件折衷(1984) or this 张贴(2006) by Chris Lattner on Apple's use of LLVM for runtime code specialization in their OpenGL stack.
  • 在某些情况下,软件诉诸于一种称为 trampoline的技术,它涉及到在堆栈(或其他位置)上动态创建代码。例如,GCC 的 嵌套函数和一些 Unice 的 signal mechanism

Sometimes code is translated into code at runtime (this is called 动态二进制翻译):

代码修改可以用来解决指令集的限制:

  • 曾经有一段时间(我知道是很久以前) ,计算机没有从子程序返回或间接寻址内存的指令。自修改代码是实现 实现子程序、指针和数组的唯一途径。

更多的代码修改案例:

  • 许多调试器将指令替换为 实现断点
  • Some 动态连接器动态连接器 modify code at runtime. This article provides some background on the runtime relocation of Windows DLLs, which is effectively a form of code modification.

我已经实现了一个程序,使用进化来创建最好的算法。它用程序自修改修改了 DNA 蓝图。

你知道老生常谈,硬件和软件之间没有逻辑上的区别... ... 也可以说代码和数据之间没有逻辑上的区别。

什么是程序自修改?将值放入执行流的代码,以便不将其解释为数据而是将其解释为命令。当然,在函数式语言中有一种理论观点认为实际上没有区别。我的意思是在 e 上可以直接用命令式语言和编译器/解释器做到这一点,而不需要假定具有相同的地位。

What I'm referring to is in the practical sense that data can alter program execution paths (in some sense this is extremely obvious). I am thinking of something like a compiler-compiler that creates a table (an array of data) that one traverses through in parsing, moving from state to state (and also modifying other variables), just like how a program moves from command to command, modifying variables in the process.

因此,即使在编译器创建代码空间并引用完全独立的数据空间(堆)的通常情况下,仍然可以修改数据以显式更改执行路径。

最好的版本可能是 Lisp 宏。与 C 宏不同,Lisp 只是一个预处理程序,它允许您随时访问整个编程语言。这是 lisp 中最强大的特性,并且不存在于任何其他语言中。

I am by no means an expert but get one of the lisp guys talking about it! There is a reason 他们说 Lisp 是世界上最强大的语言,而那些聪明的人不,他们可能是对的。

The Linux 内核 has Loadable Kernel Modules which do just that.

Emacs 也有这个能力,我一直在使用它。

任何支持动态插件架构的东西实际上都是在运行时修改它的代码。

许多年前,我花了一个早上来调试一些程序自修改,一条指令改变了下面这条指令的目标地址,也就是说,我正在计算一个分支地址。它是用汇编语言编写的,当我逐步完成程序时,一条指令一条指令地完成。但当我运行这个程序时,它失败了。最后,我意识到机器正在从内存中取出2条指令,而且(当指令被放置在内存中时)我正在修改的指令已经被取出,因此机器正在执行未修改的(不正确的)指令版本。当然,在我调试的时候,它一次只执行一条指令。

我的观点是,程序自修改对于测试/调试来说可能是非常讨厌的,并且通常对机器的行为(无论是硬件还是虚拟的)有着隐藏的假设。此外,系统永远不能在(现在)多核机器上执行的各种线程/进程之间共享代码页。这使得虚拟内存等的许多好处化为乌有。它还会使在硬件级别进行的分支优化失效。

(注-我并没有把即时通讯列入程序自修改类别。JIT 正在将代码的一种表示转换为另一种表示,它没有修改代码)

总而言之,这是个坏主意——非常简洁,非常模糊,但是非常糟糕。

of course - if all you have is an 8080 and ~512 bytes of memory you might have to resort to such practices.

I run statistical analyses against a continually updated database. My statistical model is written and re-written each time the code is executed to accommodate new data that become available.

一个用例是 EICAR test file,它是一个合法的 DOS 可执行 COM 文件,用于测试防病毒程序。

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

它必须使用自我代码修改,因为可执行文件必须只包含范围[21h-60h,7Bh-7Dh ]内的可打印/可打字的 ASCII 字符,这大大限制了可编码指令的数量

详细说明 给你


它也用于 DOS 中的 浮点操作调度

一些编译器将在 x87浮点指令的位置发出 CD xx,xx 范围从0x34-0x3B。由于 CDint指令的操作码,如果 x87协处理器不可用,它将跳入中断34h-3Bh 并在软件中模拟该指令。否则,interrupt handler 将把这两个字节替换为 9B Dx,这样以后的执行将由 x87直接处理,而不需要仿真。

在 MS-DOS 中 x87浮点仿真的协议是什么?


另一种用法是 在运行时优化代码

例如,在一个没有可变位移的架构中(或者当它们非常慢的时候) ,当移位计数提前很久就知道的时候,它们可以是 只使用不变的移位来模拟,方法是在控制到达指令之前和缓存加载运行之前,改变指令中包含移位计数的直接字段

当针对不同(微)体系结构有多个版本时,它还可以用于将函数调用更改为最优化的版本。例如,你有相同的函数写在标量,SSE2,AVX,AVX-512... 并取决于当前的 CPU 你会选择最好的一个。这可以很容易地使用函数指针来完成,这些指针在启动时由代码调度程序设置,但是这样就会多出一个间接级别,这对 CPU 不利。有些编译器支持自动编译到不同版本的 函数多版本控制,然后在加载时,链接器会将函数地址固定到所需的地址。但是,如果您不支持编译器和链接器,并且您也不希望使用间接方式,那么该怎么办呢?只需在启动时自己修改调用指令,而不是更改函数指针。现在所有的调用都是静态的,并且可以被 CPU 正确地预测

可以使用这种方法的场景是一个学习程序。为了响应用户的输入,程序学习了一种新的算法:

  1. 它会查找类似算法的现有代码库
  2. 如果代码库中没有类似的算法,程序只是添加了一个新的算法
  3. 如果存在类似的算法,程序(可能在用户的帮助下)修改现有的算法,以便能够同时满足旧的用途和新的用途

在 Java 中有一个问题: 自我修改 Java 代码的可能性有哪些?