【问题标题】:Are there any smart cases of runtime code modification?是否有任何运行时代码修改的智能案例?
【发布时间】:2011-07-28 23:32:02
【问题描述】:

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

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

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

【问题讨论】:

  • 在现代架构上,它严重干扰了缓存和指令管道:自我修改代码最终不会修改缓存,因此您需要屏障,这可能会使您的代码变慢。并且您不能修改已经在指令管道中的代码。因此,任何基于自我修改代码的优化都必须在代码运行之前执行,以产生优于运行时检查的性能影响。
  • @Alexandre:尽管执行任意次数,但自修改代码的修改很少变化(例如一次、两次)是很常见的,因此一次性成本可能微不足道。跨度>
  • 不确定为什么这被标记为 C 或 C++,因为两者都没有任何机制。
  • @Alexandre:众所周知,Microsoft Office 就是这样做的。因此(?)所有 x86 处理器都对自修改代码提供了出色的支持。在其他处理器上,昂贵的同步是必要的,这使得整个事情的吸引力降低。
  • @Cawas:通常自动更新软件会下载新的程序集和/或可执行文件并覆盖现有的。然后它将重新启动软件。这就是firefox、adobe等所做的。自我修改通常意味着在运行时代码由于某些参数而被应用程序重写到内存中,而不必持久化回磁盘。例如,它可能会优化整个代码路径,如果它可以智能地检测到在此特定运行期间不会执行这些路径以加快执行速度。

标签: executable cpu-architecture instructions self-modifying platform-agnostic


【解决方案1】:

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

  • 一些虚拟机使用JIT 编译来提高性能。
  • 动态生成专门的函数在计算机图形学中一直很常见。参见例如Rob Pike、Bart Locanthi 和 John Reiser Hardware Software Tradeoffs for Bitmap Graphics on the Blit (1984) 或 Chris Lattner 的 posting (2006),讲述 Apple 在其 OpenGL 堆栈中使用 LLVM 实现运行时代码专业化。
  • 在某些情况下,软件采用称为 trampoline 的技术,该技术涉及在堆栈(或其他位置)上动态创建代码。例如 GCC 的 nested functions 和一些 Unices 的 signal mechanism

有时代码在运行时被翻译成代码(这称为dynamic binary translation):

  • 仿真器(如 Apple 的 Rosetta)使用这种技术来加速仿真。另一个例子是全美达的code morphing software
  • 复杂的调试器和分析器,例如ValgrindPin,在代码执行时使用它来检测您的代码。
  • 在对 x86 指令集进行扩展之前,虚拟化软件(如 VMWare)无法直接在虚拟机中运行特权 x86 代码。相反,它必须将translate any problematic instructions on the fly 转换为更合适的自定义代码。

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

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

更多代码修改案例:

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

【讨论】:

  • 这个列表似乎混合了修改自身的代码和修改其他代码(如链接器)的代码。
  • @AShelly:好吧,如果您认为动态链接器/加载器是代码的一部分,那么它会自行修改。他们生活在同一个地址空间,所以我认为这是一个有效的观点。
  • 好的,列表现在区分程序和系统软件。我希望这是有道理的。最后,任何分类都是值得商榷的。这一切都归结为你在程序(或代码)的定义中究竟包含了什么。
【解决方案2】:

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

【讨论】:

【解决方案3】:

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

【讨论】:

  • 很公平。这种技术有什么用吗?看起来很危险。
  • @Alexandre C.:如果我没记错的话,许多运行时库(C、Pascal、...)必须在 DOS 下执行一个函数来执行中断调用。由于这样的函数将中断号作为参数,您必须提供这样的函数(当然,如果数字是恒定的,您可以生成正确的代码,但这不能保证)。并且所有的库都使用自修改代码实现了它。
  • 您可以使用 switch case 来完成,而无需修改代码。缩小是输出代码会更大
【解决方案4】:

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

【讨论】:

  • 非常好,尤其是在避免互斥锁/解锁时。
  • 真的吗?这对于基于 ROM 的代码或在写保护代码段中执行的代码有何影响?
  • @Ira Baxter:任何发出可重定位代码的编译器都知道代码段是可写的,至少在启动期间是这样。所以“一些编译器使用它”的说法仍然是可能的。
【解决方案5】:

有很多情况:

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

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

来自维基百科:

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

在这样的操作系统上,即使是像 Java VM 这样的程序也需要 root/admin 权限才能执行其 JIT 代码。 (详情请见http://en.wikipedia.org/wiki/W%5EX

【讨论】:

  • 自我修改代码不需要root权限。 Java VM 也没有。
  • 我不知道有些操作系统这么严格。但这在某些应用程序中肯定是有意义的。但是,我确实想知道以 root 权限执行 Java 是否真的会增加安全性......
  • @Mackie:我认为它必须减少它,但也许它可以设置一些内存权限然后将有效的uid更改回某个用户帐户......?
  • 是的,我希望他们有一个细粒度的机制来授予权限以配合严格的安全模型。
【解决方案6】:

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

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

【讨论】:

  • 我看不懂重点。如果说操作系统将禁止系统调用,您可能会收到一个错误,您必须检查代码,不是吗?在我看来,修改可执行文件而不是返回错误代码是一种过度工程。
  • @Alexandre C. :您可以通过这种方式消除空指针检查。对于调用者来说,参数有效通常是显而易见的。
  • @Alexandre:您可以在链接中阅读研究。我认为他们获得了相当可观的加速,这就是重点:-}
  • 对于相对微不足道且不受 I/O 限制的系统调用,节省的费用非常可观。例如,如果您正在为 Unix 编写一个守护进程,那么您会执行一堆样板系统调用来断开 stdio、设置各种信号处理程序等。如果您知道调用的参数是常量,并且结果总是一样的(例如关闭标准输入),你在一般情况下执行的很多代码都是不必要的。
  • 如果您阅读了这篇论文,第 8 章包含一些关于数据采集的非平凡实时 I/O 的令人印象深刻的数字。还记得这是 1980 年代中期的论文,而他运行的机器是 10 吗? Mhz 68000,他能够在软件中使用普通的旧软件捕获 CD 质量的音频数据(每秒 44,000 个样本)。他声称 Sun 工作站(经典 Unix)只能达到这个速度的 1/5。从那时起,我就是一名老汇编语言编码员,这非常壮观。
【解决方案7】:

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

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

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

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

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

【讨论】:

  • 我不知道,好和坏似乎不是考虑这个问题的正确类别。当然,您应该真正知道自己在做什么以及为什么要这样做。但是编写该代码的程序员可能不希望您看到程序在做什么。当然,如果您必须调试这样的代码,那是很讨厌的。但那段代码很可能就是这样的。
  • 现代 x86 CPU 的 SMC 检测比纸上要求的更强:Observing stale instruction fetching on x86 with self-modifying code。在大多数非 x86 CPU(如 ARM)上,指令缓存与数据缓存不一致,因此需要手动刷新/同步才能将新存储的字节作为指令可靠地执行。 community.arm.com/processors/b/blog/posts/…不管怎样,SMC 性能在现代 CPU 上糟糕,除非您修改一次并运行多次。
【解决方案8】:

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

【讨论】:

    【解决方案9】:

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

    【讨论】:

      【解决方案10】:

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

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

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

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

      【讨论】:

      • 没有逻辑差异,是的。不过,还没有看到太多自修改集成电路。
      • @Mitch,IMO 更改执行路径与代码的(自我)修改无关。此外,您将数据与信息混淆。我无法回答你的评论 to my reply in LSE b/c 自二月以来,我在那里被禁止使用 3 年(1,000 天),因为我在 meta-LSE 中表达了我的观点,即美国人和英国人不拥有英语。跨度>
      【解决方案11】:

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

      【讨论】:

        【解决方案12】:

        一个用例是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 字符,这大大限制了可编码指令的数量

        详细解释here


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

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

        What is the protocol for x87 floating point emulation in MS-DOS?


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

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

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

        【讨论】:

          【解决方案13】:

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

          【讨论】:

            【解决方案14】:

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

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

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

            【讨论】:

            • 几乎没有。拥有一个并非总是驻留的动态可加载库与自修改代码关系不大。
            【解决方案15】:

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

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

            有一个问题是如何在 Java 中做到这一点:What are the possibilities for self-modification of Java code?

            【讨论】:

              【解决方案16】:

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

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

              【讨论】:

              • 这实际上是在创建自我修改代码,还是只是一个更强大的预处理器(会生成函数)?
              • @Brendan:确实,但它进行预处理的正确方法。这里没有运行时代码修改。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2010-09-14
              • 2021-06-30
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2020-04-27
              • 1970-01-01
              相关资源
              最近更新 更多