120

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

现代操作系统似乎不赞成执行此操作的程序,因为病毒已使用此技术来避免检测。

我能想到的只是某种运行时优化,它可以通过在运行时知道一些在编译时无法知道的东西来删除或添加一些代码。

4

16 回答 16

118

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

有时代码在运行时被翻译成代码(这称为动态二进制翻译):

  • Apple 的Rosetta等仿真器使用这种技术来加速仿真。另一个例子是 Transmeta 的代码变形软件
  • 复杂的调试器和分析器(如ValgrindPin)在执行代码时使用它来检测您的代码。
  • 在对 x86 指令集进行扩展之前,像 VMWare 这样的虚拟化软件不能直接在虚拟机中运行特权 x86 代码。相反,它必须即时将任何有问题的指令翻译成更合适的自定义代码。

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

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

更多代码修改案例:

  • 许多调试器替换指令来实现断点
  • 一些动态链接器在运行时修改代码。本文提供了有关 Windows DLL 运行时重定位的一些背景知识,这实际上是一种代码修改形式。
于 2011-04-04T09:09:01.470 回答
35

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

于 2011-04-04T07:22:22.453 回答
23

一个有效的原因是 asm 指令集缺少一些您可以自己构建的必要指令。示例:在 x86 上,无法为寄存器中的变量创建中断(例如,使用 ax 中的中断号进行中断)。只允许编码到操作码中的 const 数字。使用自修改代码可以模拟这种行为。

于 2011-04-04T07:31:51.060 回答
17

有很多情况:

  • 病毒通常使用自我修改代码在执行前对其代码进行“反混淆”,但该技术也可用于挫败逆向工程、破解和不需要的黑客攻击
  • 在某些情况下,在运行时(例如,在读取配置文件后立即)可能存在一个特定点,当已知 - 在进程的剩余生命周期中 - 将始终或永远不会采用特定分支:而不是不必要的检查某个变量以确定分支的方式,可以相应地修改分支指令本身
    • 例如,可能会知道只有一种可能的派生类型将被处理,这样虚拟调度可以用特定的调用替换
    • 检测到哪些硬件可用后,可以对匹配代码的使用进行硬编码
  • 不必要的代码可以用无操作指令替换或跳过它,或者将下一位代码直接移动到位(如果使用与位置无关的操作码更容易)
  • 为促进其自身调试而编写的代码可能会在战略位置注入调试器预期的陷阱/信号/中断指令。
  • 一些基于用户输入的谓词表达式可能会被库编译为本机代码
  • 内联一些在运行时才可见的简单操作(例如来自动态加载的库)...
  • 有条件地添加自我检测/分析步骤
  • 裂缝可以实现为修改加载它们的代码的库(不是完全“自我”修改,但需要相同的技术和权限)。
  • ...

一些操作系统的安全模型意味着自修改代码在没有 root/admin 权限的情况下无法运行,因此对于通用用途来说是不切实际的。

来自维基百科:

在具有严格 W^X 安全性的操作系统下运行的应用程序软件无法在允许写入的页面中执行指令——只有操作系统本身才被允许将指令写入内存并稍后执行这些指令。

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

于 2011-04-04T07:29:43.977 回答
17

一些编译器过去将其用于静态变量初始化,从而避免了后续访问的条件成本。换句话说,他们通过在第一次执行时用无操作覆盖该代码来实现“仅执行此代码一次”。

于 2011-04-04T07:47:15.610 回答
16

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

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

于 2011-04-04T07:21:40.977 回答
9

很多年前,我花了一个上午尝试调试一些自修改代码,一条指令改变了下一条指令的目标地址,即我正在计算一个分支地址。它是用汇编语言编写的,当我一次执行一条指令时,它运行良好。但是当我运行程序时它失败了。最终,我意识到机器正在从内存中获取 2 条指令,并且(因为指令被放置在内存中)我正在修改的指令已经被获取,因此机器正在执行该指令的未修改(不正确)版本。当然,当我调试时,它一次只执行一条指令。

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

(注意 - 我没有将 JIT 包含在自修改代码的类别中。JIT 是从代码的一种表示形式转换为另一种表示形式,它不是在修改代码)

总而言之,这只是一个坏主意——非常整洁,非常晦涩,但非常糟糕。

当然——如果你只有一个 8080 和 ~512 字节的内存,你可能不得不求助于这种做法。

于 2011-04-04T20:56:29.080 回答
7

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

于 2011-04-04T08:27:50.600 回答
5

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

于 2011-04-04T08:43:39.357 回答
5

您知道硬件和软件之间没有逻辑差异的老栗子......也可以说代码和数据之间没有逻辑差异。

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

我指的是实际意义上的数据可以改变程序执行路径(在某种意义上这是非常明显的)。我正在考虑类似于编译器编译器的东西,它创建一个表(数据数组),一个人在解析中遍历,从一个状态移动到另一个状态(以及修改其他变量),就像一个程序如何从一个命令移动到另一个命令,修改过程中的变量。

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

于 2011-04-04T14:24:40.537 回答
4

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

于 2011-04-04T13:01:46.177 回答
2

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

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

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

详细说明here


它也用于DOS中的浮点运算调度

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

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


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

例如,在没有可变位移位的架构上(或者当它们非常慢时),那么当位移计数是通过在控制之前更改指令中包含位移计数的立即字段而提前知道时,可以仅使用恒定位移来模拟它们到达该指令并且在缓存加载运行之前

当不同(微)架构有多个版本时,它还可用于将函数调用更改为最优化的版本。例如,您使用标量、SSE2、AVX、AVX-512 编写了相同的函数......并且根据当前的 CPU,您将选择最好的一个。它可以使用代码调度程序在启动时设置的函数指针轻松完成,但是你有一个对 CPU 不利的间接级别。一些编译器支持函数多版本化它会自动编译为不同的版本,然后在加载时链接器会将函数地址修复为所需的地址。但是,如果您没有编译器和链接器支持,并且您也不想要间接呢?只需在启动时自己修改调用指令,而不是更改函数指针。现在调用都是静态的,可以被 CPU 正确预测

于 2013-11-14T02:46:50.757 回答
1

我对不断更新的数据库进行统计分析。每次执行代码以适应可用的新数据时,都会编写和重写我的统计模型。

于 2011-04-05T00:35:35.480 回答
0

Linux 内核具有可加载的内核模块,可以做到这一点。

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

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

于 2011-04-04T17:10:33.133 回答
0

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

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

有一个问题是如何在 Java 中做到这一点:Java 代码的自我修改的可能性是什么?

于 2014-10-12T10:58:09.630 回答
-1

最好的版本可能是 Lisp 宏。与只是预处理器的 C 宏不同,Lisp 让您可以随时访问整个编程语言。这是 lisp 中最强大的功能,在任何其他语言中都不存在。

我绝不是专家,但让一个 lisp 家伙谈论它!他们说 Lisp 是最强大的语言是有原因的,而聪明的人说他们可能是对的。

于 2011-04-04T14:39:50.667 回答