是否有修改运行时代码的明智案例?


119

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

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

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


8
在现代体系结构上,它严重影响了缓存和指令流水线:自修改代码最终将不修改缓存,因此您将需要障碍,这可能会使您的代码变慢。而且您无法修改指令管道中已经存在的代码。因此,任何基于自修改代码的优化都必须在代码运行之前执行,以产生比运行时检查更好的性能影响。
Alexandre C.

7
@Alexandre:自我修改的代码尽管执行了任意次,但是很少进行修改(例如,一次,两次),这是很常见的,因此一次性的成本可以忽略不计。
Tony Delroy

7
不知道为什么将其标记为C或C ++,因为这两者都没有任何机制。
MSalters 2011年

4
@Alexandre:众所周知,Microsoft Office就是这样做的。结果(?),所有x86处理器都对自修改代码提供了出色的支持。在其他处理器上,必须进行昂贵的同步,这会使整体吸引力降低。
Mackie Messer

3
@Cawas:通常,自动更新软件会下载新的程序集和/或可执行文件,并覆盖现有的程序集和/或可执行文件。然后它将重新启动软件。这就是firefox,adobe等的功能。自我修改通常意味着在运行时,由于某些参数,应用程序会将代码重写到内存中,而不必将其持久化回磁盘。例如,如果它可以智能地检测出在此特定运行期间不会执行的那些路径以加快执行速度,则它可能会优化整个代码路径。
NotMe 2011年

Answers:


117

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

  • 一些虚拟机使用JIT编译来提高性能。
  • 即时生成专业功能在计算机图形学中很常见。参见Rob Bike和Bart Locanthi以及Blit上的位图图形的 John Reiser 硬件软件折衷(1984)或Chris Lattner的本贴(2006)关于Apple使用LLVM在其OpenGL堆栈中进行运行时代码专业化的文章。
  • 在某些情况下,软件诉诸于称为蹦床的技术,该技术涉及在堆栈(或其他位置)上动态创建代码。例如GCC的嵌套函数和某些Unices 的信号机制

有时,代码会在运行时转换为代码(这称为动态二进制转换):

  • 苹果的Rosetta之类的仿真器使用此技术来加快仿真速度。另一个例子是Transmeta的代码转换软件
  • 诸如ValgrindPin之类的复杂调试器和分析器会在执行代码时使用它来检测代码。
  • 在对x86指令集进行扩展之前, 像VMWare这样的虚拟化软件无法直接在虚拟机中运行特权x86代码。相反,它必须将所有有问题的指令即时转换为更合适的自定义代码。

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

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

更多的代码修改案例:

  • 许多调试器替换指令以实现断点
  • 一些动态链接器在运行时修改代码。本文为Windows DLL的运行时重定位提供了一些背景知识,这实际上是一种代码修改形式。

10
此列表似乎将修改自身的代码示例与修改其他代码(例如链接器)的代码混合在一起。
AShelly 2011年

6
@AShelly:好吧,如果您认为动态链接器/加载器是代码的一部分,那么它确实会对其进行修改。它们生活在相同的地址空间中,因此我认为这是一种有效的观点。
Mackie Messer

1
好的,该列表现在可以区分程序和系统软件。我希望这是有道理的。最后,任何分类都是值得商bat的。一切都取决于您到底要在程序(或代码)的定义中包含什么。
Mackie Messer

35

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


5
一个有趣的阅读是迈克尔·亚伯拉什的3部分在DDJ文章的Pixomatic:drdobbs.com/architecture-and-design/184405765drdobbs.com/184405807drdobbs.com/184405848。第二个链接(第2部分)讨论了像素管线的Pixomatic代码焊接机。
typo.pl 2011年

1
关于该主题的一篇非常不错的文章。从1984年开始,但仍然读得很好:Rob Pike和Bart Locanthi和John Reiser。Blit上位图图形的硬件软件权衡
Mackie Messer

5
查尔斯Petzold的解释了这种在一本名为“美丽密码”的一个例子:amazon.com/Beautiful-Code-Leading-Programmers-Practice/dp/...
纳瓦兹

3
这个答案是关于生成代码的,但是问题是关于修改代码的……
Timwi

3
@Timwi-它确实修改了代码。它无需解析大量if链,而是解析一次形状并重写了渲染器,因此可以将其设置为正确的形状类型,而不必每次检查。有趣的是,这是现在的OpenCL代码常见-因为它的飞行编译你可以重写它在运行时的具体情况
马丁贝克特

23

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


很公平。有没有使用这种技术?好像很危险
Alexandre C.

4
@Alexandre C .:如果我没记错的话,许多运行时库(C,Pascal等)必须对DOS时代的函数进行中断调用。由于此类函数将中断号作为参数,因此您必须提供此类功能(当然,如果中断号为常数,则可以生成正确的代码,但这不能保证)。并且所有库都使用自修改代码实现了它。
flolo 2011年

您可以使用开关盒来做,而无需修改代码。缩小的尺寸是输出代码将更大
phuclv 2013年

17

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


1
非常好,尤其是在避免互斥锁/解锁的情况下。
Tony Delroy

2
真?对于基于ROM的代码或在写保护的代码段中执行的代码,该如何处理?
艾拉·巴克斯特

1
@Ira Baxter:发出可重定位代码的任何编译器都知道该代码段是可写的,至少在启动过程中是如此。因此,“某些编译器已使用它”语句仍然可行。
MSalters 2011年

17

有很多情况:

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

某些操作系统的安全模型意味着没有root / admin特权就无法运行自修改代码,这对于通用用途而言是不切实际的。

从维基百科:

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

在这样的操作系统上,甚至Java VM之类的程序也需要root / admin特权才能执行其JIT代码。(有关更多详细信息,请参见http://en.wikipedia.org/wiki/W%5EX


2
您不需要root特权即可进行自我修改的代码。Java VM也没有。
Mackie Messer

我不知道某些操作系统如此严格。但这在某些应用程序中当然很有意义。但是,我确实想知道使用root特权执行Java是否确实会提高安全性……
Mackie Messer

@Mackie:我认为它必须减少它,但是也许它可以设置一些内存权限,然后将有效的uid更改回某些用户帐户...?
Tony Delroy

是的,我希望他们有一个精细的机制来授予权限,以伴随严格的安全模型。
Mackie Messer

15

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

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


我看不出重点。如果说操作系统将禁止系统调用,您很可能会收到一个错误,您必须检入代码,不是吗?在我看来,修改可执行文件而不是返回错误代码是一种过度设计。
Alexandre C.

@Alexandre C .:您可以通过这种方式消除空指针检查。对于调用者来说,参数是有效的通常很明显。
MSalters 2011年

@Alexandre:您可以在链接上阅读研究报告。我认为他们获得了相当可观的加速,这就是重点:-}
Ira Baxter

2
对于相对琐碎且不受I / O约束的系统调用,可节省大量资金。例如,如果您正在为Unix编写守护程序,则可以执行很多样板化的系统调用,以断开stdio的连接,设置各种信号处理程序等。如果您知道调用的参数是常量,并且结果将始终是相同的(例如,关闭标准输入),通常不需要执行很多代码。
Mark Bessey

1
如果您读了这篇论文,第8章将介绍一些非常重要的数据采集实时I / O。还记得这是1980年代中期的论文,而他所使用的机器是10台吗?Mhz 68000,他能够使用普通的旧软件在软件中捕获 CD质量的音频数据(每秒44,000个样本)。他声称Su​​n工作站(经典Unix)只能达到该速度的1/5。那时我是一个古老的汇编语言程序员,这真是太壮观了。
艾拉·巴克斯特

9

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

我的观点是,自我修改的代码非常难以测试/调试,并且通常对机器(无论是硬件还是虚拟)的行为都具有隐含的假设。而且,系统永远无法在(现在)多核机器上执行的各种线程/进程之间共享代码页。这使虚拟内存等的许多优点无法实现。它还会使在硬件级别完成的分支优化无效。

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

总而言之,这只是一个坏主意-真的很整洁,很晦涩,但是真的很糟糕。

当然-如果您只有8080和〜512字节的内存,则可能必须采取这种做法。


1
我不知道,好与坏似乎并不是考虑这一点的正确类别。当然,您应该真正知道自己在做什么以及为什么要这么做。但是编写该代码的程序员可能不希望您看到程序在做什么。如果您必须像这样调试代码,那当然很讨厌。但是该代码很可能就是那样。
Mackie Messer

现代的x86 CPU具有比纸上所要求的更强大的SMC检测:观察具有自修改代码的x86上的陈旧指令提取。而且在大多数非x86 CPU(如ARM)上,指令高速缓存与数据高速缓存不一致,因此在可以可靠地将新存储的字节作为指令执行之前,需要进行手动刷新/同步。 community.arm.com/processors/b/blog/posts/…无论哪种方式,SMC的性能是可怕的现代CPU,除非你修改一次,多次运行。
彼得·科德斯

7

从操作系统内核的角度来看,每个即时编译器和链接器运行时都将执行程序文本自我修改。突出的例子是Google的V8 ECMA脚本解释器。


5

自我修改代码(实际上是“自我生成”代码)的另一个原因是实现即时编译机制以提高性能。例如,读取代数表达式并根据一定范围的输入参数进行计算的程序可能会在陈述计算之前将表达式转换成机器代码。


5

您知道古老的栗子,硬件和软件之间没有逻辑上的区别...也可以说代码和数据之间没有逻辑上的区别。

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

我指的是实际意义上的数据可以更改程序执行路径(在某种意义上,这是非常明显的)。我想到的是像编译器这样的编译器,它创建一个表(数据数组)供解析时遍历,从一个状态移动到另一个状态(并修改其他变量),就像程序从一个命令移到另一个命令一样,在此过程中修改变量。

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


4
没有逻辑上的差异,是的。不过,还没有看到太多的自修改集成电路。
艾拉·巴克斯特

@ Mitch,IMO更改exec路径与代码的(自我)修改无关。此外,您将数据与信息混淆。我无法以LSE b / c的形式回答您的评论,因为Feabruary在Meta-LSE中表达了我的观点,即美国人和英国人不拥有英语,所以我被禁止在这里进行3年(1,000天),因为我在那儿被禁止使用。
纳季·瓦宁

4

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


2

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

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

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

详细说明在这里


它也用于DOS中的浮点操作分派

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

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


1

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

Emacs也具有此功能,我一直都在使用它。

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


4
几乎不。具有不总是驻留的可动态加载的库与自修改代码几乎没有关系。
Dov

1

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


0

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

  1. 它为类似的算法查找现有的代码库
  2. 如果代码库中没有类似的算法,则程序仅添加一个新算法
  3. 如果存在类似的算法,则该程序(可能在用户的帮助下)修改了现有算法,使其既可以满足旧目的,也可以满足新目的

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


-1

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

我绝不是专家,而是让其中一个轻率的家伙谈论它!他们说Lisp是周围最强大的语言,而聪明的人则没有理由说他们可能是对的。


2
这实际上是在创建自我修改的代码,还是仅仅是功能更强大的预处理器(将生成函数的预处理器)?
布伦丹·朗

@Brendan:的确如此,但这进行预处理的正确方法。这里没有运行时代码修改。
Alexandre C.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.