我正在考虑为我最近一直在做的一个业余虚拟机写一个JIT编译器。我知道一点汇编,(我主要是C程序员。我可以阅读大多数汇编,并参考我不理解的操作码,并编写一些简单的程序。)但我很难理解我在网上找到的几个自修改代码的例子。
这是一个这样的例子:http://asm.sourceforge.net/articles/smc.html
所提供的示例程序在运行时会进行大约四种不同的修改,其中没有一种是明确解释的。Linux内核中断被多次使用,并且没有解释或详细说明。(作者在调用中断之前将数据移入几个寄存器。我假设他是在传递参数,但这些参数根本没有解释,让读者去猜测。)
我在寻找的是一个最简单,最直接的例子,在代码中的自我修改程序。我可以查看并使用它来理解x86汇编中的自修改代码是如何编写的,以及它是如何工作的。有没有什么资源你可以给我,或者任何例子,你可以给予,将充分证明这一点?
我使用NASM作为我的汇编程序。
编辑:我也在Linux上运行这段代码。
8条答案
按热度按时间6g8kf2rb1#
哇,这比我想象的要痛苦得多。100%的痛苦是linux保护程序不被覆盖和/或执行数据。
下面显示两种解决方案。而且涉及到很多谷歌搜索,所以有点简单的把一些指令字节和执行他们是我的,mprotect和调整页面大小是从谷歌搜索中挑选出来的,我必须为这个例子学习的东西。
自我修改代码是直接向前的,如果你把程序或至少只是两个简单的函数,编译,然后反汇编你会得到这些指令的操作码。或者使用NASM来编译汇编程序块等。从这里我决定了将一个立即数加载到eax然后返回的操作码。
理想情况下,您只需将这些字节放在某个ram中并执行该ram。要让linux做到这一点,你必须改变保护,这意味着你必须向它发送一个在mmap页面上对齐的指针。因此,分配比您需要的更多的内存,在该分配中找到位于页面边界上的对齐地址,并从该地址中进行mprotect,然后使用该内存来放置操作码,然后执行。
第二个例子采用了一个编译到程序中的现有函数,同样由于保护机制,你不能简单地指向它并更改字节,你必须取消对它的写保护。因此,您必须使用该地址和足够的字节来覆盖要修改的代码,以备份到前一个页边界调用mprotect。然后你可以用任何你想要的方式改变这个函数的字节/操作码(只要你不想继续使用它)并执行它。在这个例子中,你可以看到
fun()
工作了,然后我把它改为简单地返回一个值,再次调用它,现在它已经被修改了。x759pob22#
因为你在写一个JIT编译器,你可能不想要自我修改的代码,你想在运行时生成可执行的代码。这是两码事。自修改代码是指 * 在已经开始运行 * 之后被修改的代码。自修改代码在现代处理器上有很大的性能损失,因此对于JIT编译器来说是不可取的。
在运行时生成可执行代码应该是一个简单的问题,mmap()使用PROT_EXEC和PROT_WRITE权限访问一些内存。你也可以在你自己分配的一些内存上调用mprotect(),就像上面的dwelch所做的那样。
q35jwt9p3#
一个简单的例子,基于上面的例子。感谢davich帮助了很多。
amrnrhlw4#
这是用AT&T汇编编写的。正如您在程序执行过程中所看到的,由于自我修改代码,输出发生了变化。
编译:gcc -m32 modify.s modify.c
使用-m32选项是因为该示例适用于32位计算机
埃森:
C测试程序:
输出量:
ogq8wdun5#
我正在开发一个自我修改的游戏来教x86汇编,并且必须解决这个确切的问题。我使用了以下三个库:
AsmJit + AsmTk用于组装:https://github.com/asmjit/asmjit + https://github.com/asmjit/asmtk UDIS 86用于拆卸:https://github.com/vmt/udis86
使用Udis 86读取指令,用户可以将其编辑为字符串,然后使用AsmJit/AsmTk组装新的字节。这些可以写回内存,正如其他用户所指出的,写回需要使用Windows上的VirtualProtect或Unix上的mprotect来修复内存页面权限。
代码示例对于StackOverflow来说有点长,所以我会推荐你参考我写的一篇代码示例文章:
https://medium.com/squallygame/how-we-wrote-a-self-hacking-game-in-c-d8b9f97bfa99
这里有一个功能正常的repo(非常轻量级):
https://github.com/Squalr/SelfHackingApp
pvcm50d16#
你也可以看看像GNU lightning这样的项目。你给予它一个简化的RISC类型的机器的代码,它会动态地生成正确的机器。
你应该考虑的一个非常真实的问题是与外国图书馆的接口。您可能需要至少支持一些系统级调用/操作,以使您的VM有用。Kitsune的建议是一个很好的开始,可以让你考虑系统级调用。您可能会使用mprotect来确保您修改的内存可以合法地执行。(@KitsuneYMG)
一些允许调用用C编写的动态库的FFI应该足以隐藏许多操作系统特定的细节。所有这些问题都会对您的设计产生相当大的影响,因此最好尽早开始考虑它们。
bis0qfac7#
这个问题被标记为“assembly”和“x86”,但没有标记为“C”。虽然问这个问题的人提到他们主要使用C语言,但这个问题很可能会被寻找纯汇编解决方案的人(包括我过去)遇到。因此,这是我试图以最简单的方式演示JIT程序的尝试,它受到old_timer的答案的启发,但用纯汇编重写。
nszi6y058#
我从来没有写过自我修改的代码,尽管我对它的工作原理有一个基本的了解。基本上,你在内存中写入你想要执行的指令,然后跳转到那里。处理器解释你写的指令的字节并尝试执行它们.例如,病毒和反拷贝程序可能使用这种技术。
关于系统调用,你是对的,参数通过寄存器传递。对于Linux系统调用及其参数的参考,只需检查here。