我怎样才能 "dump" 一个函数到一个文件?
How can I "dump" a Function to a file?
比如我有一个函数func()
:
int func (int a, int b) {return a + b;}
现在我想把它写入一个文件,这样我就可以使用系统调用 mmap 通过 PROT_EXEC 加载它,我可以从另一个 program.What 调用它,我应该这样做吗?
如果您在编译时知道需要什么签名和静态库或共享库的位置,您可能只想在输出库中包含 header 和 link。如果你想动态调用一个函数,你可能需要 dlopen / dlsym (UNIX) or LoadLibrary / GetProcAddress (Windows) 来动态加载库并按名称检索函数的地址。
请注意,您实际需要动态(至少明确地)加载库的情况非常少见。这通常用于模块化架构(例如 "plugins" 或 "extensions"),其中应用程序的各个部分单独分发(使用 IPC 而不是动态加载可以更安全地实现...请参阅下面的注释).或者,对于不允许您的应用程序静态包含依赖项并且需要根据其恰好执行的环境中是否存在某些库依赖项来有条件地提供行为的情况。但是,在大多数情况下,您只需要包含一个 header 来声明您需要的符号并为每个目标平台编译(如果符号在 [=28 之间变化,则可能使用 #if...#else
宏=]es 或 OS 版本)。
从稳定性、安全性和代码复杂性的角度来看,我个人建议您避免动态库加载。对于核心系统功能,link 针对动态库是合理的,但您希望以动态加载的负担完全由您的工具链承担的方式进行(即您不需要调用 dlopen或显式加载库)。对于其他功能,静态 link 几乎总是更好(假设您在有针对您的依赖项的安全修复程序时分发更新),因为这将避免您被不兼容的版本更新破坏并防止您的用户体验依赖地狱(你需要版本 A 但其他一些应用程序需要版本 B);模块化架构通常通过 inter-process 通信 (IPC) 实现得更好(也更安全),因为动态加载的库存在于加载它们的程序的进程中(从而使它们能够访问整个进程的虚拟内存 space), 而对于 interprocess-communication, 每个组件都是一个单独的进程, 并且各个组件只能访问调用进程明确提供给它的信息, 这将使恶意组件更难从调用者或其他组件窃取数据或产生不稳定。
如果你想让它在现实世界中实际使用,最明智的做法可能是将源代码编译为每个平台上程序的一部分,就像常规函数一样。
下一个最好的可能是与您交谈而不是合并的单独进程。
半理智的(但仍然不是一个很好的选择,请参阅我们在另一个答案中的讨论)将创建共享库,如 Michael Aaron Safyan 所说。
但是如果你想知道它是如何工作的只是因为 - 比如说,你想编写自己的动态链接器,或者正在做某种 运行 时间代码生成,比如 JIT 编译器,或者如果你只是想知道 - 你可以制作一个原始代码文件。
要使用它,我们必须做的与链接器所做的类似 - 将代码加载到特定的地址,并 运行 它。也有位置无关的代码,可以运行在任何地址。
让我们首先编译和链接我们的函数,然后输出到某个地址的原始图像。假设函数是文件 func.c
中的 func
并且我们在 Linux 上使用 gcc。 (Windows 编译器会有类似的选项——我相信 Windows 上的 gcc 是完全一样的,但是像 Digital Mars 的 C 编译器这样的东西会有所不同,例如链接器命令是 /BINARY
)
无论如何,这就是我 运行:
gcc -c func.c # makes func.o
ld func.o --oformat=binary -e func -o func.binary
这会生成一个名为 func.binary 的文件。您可以使用 ndisasm -b 64 func.binary
(或 -b 32
如果您在 32 位模式下编译 C)最轻松地反汇编它以确认它看起来正确 - 我在那里看到一条添加指令,所以对我来说看起来不错。
如果您加载它并 mmaped 然后调用它...它应该可以工作。
问题很快就会出现:
- 如果该文件中有多个函数,它们将被压缩在一起。
- 他们试图用来互相呼叫的地址可能是完全错误的。
- 全局变量和其他静态数据会被弄乱。
还有更多。操作系统为可执行文件和库使用更复杂的文件格式是有原因的!
要进行下一步,您可以考虑编写一个 ELF 或 PE 加载程序,从标准文件中读取该元数据。当然,一旦你深入了解其中的大部分内容,你将完全按照 OS 提供的 dlopen
和 LoadLibrary
进行操作......所以除非目标只是了解胆量,只需调用这些函数即可完成!
比如我有一个函数func()
:
int func (int a, int b) {return a + b;}
现在我想把它写入一个文件,这样我就可以使用系统调用 mmap 通过 PROT_EXEC 加载它,我可以从另一个 program.What 调用它,我应该这样做吗?
如果您在编译时知道需要什么签名和静态库或共享库的位置,您可能只想在输出库中包含 header 和 link。如果你想动态调用一个函数,你可能需要 dlopen / dlsym (UNIX) or LoadLibrary / GetProcAddress (Windows) 来动态加载库并按名称检索函数的地址。
请注意,您实际需要动态(至少明确地)加载库的情况非常少见。这通常用于模块化架构(例如 "plugins" 或 "extensions"),其中应用程序的各个部分单独分发(使用 IPC 而不是动态加载可以更安全地实现...请参阅下面的注释).或者,对于不允许您的应用程序静态包含依赖项并且需要根据其恰好执行的环境中是否存在某些库依赖项来有条件地提供行为的情况。但是,在大多数情况下,您只需要包含一个 header 来声明您需要的符号并为每个目标平台编译(如果符号在 [=28 之间变化,则可能使用 #if...#else
宏=]es 或 OS 版本)。
从稳定性、安全性和代码复杂性的角度来看,我个人建议您避免动态库加载。对于核心系统功能,link 针对动态库是合理的,但您希望以动态加载的负担完全由您的工具链承担的方式进行(即您不需要调用 dlopen或显式加载库)。对于其他功能,静态 link 几乎总是更好(假设您在有针对您的依赖项的安全修复程序时分发更新),因为这将避免您被不兼容的版本更新破坏并防止您的用户体验依赖地狱(你需要版本 A 但其他一些应用程序需要版本 B);模块化架构通常通过 inter-process 通信 (IPC) 实现得更好(也更安全),因为动态加载的库存在于加载它们的程序的进程中(从而使它们能够访问整个进程的虚拟内存 space), 而对于 interprocess-communication, 每个组件都是一个单独的进程, 并且各个组件只能访问调用进程明确提供给它的信息, 这将使恶意组件更难从调用者或其他组件窃取数据或产生不稳定。
如果你想让它在现实世界中实际使用,最明智的做法可能是将源代码编译为每个平台上程序的一部分,就像常规函数一样。
下一个最好的可能是与您交谈而不是合并的单独进程。
半理智的(但仍然不是一个很好的选择,请参阅我们在另一个答案中的讨论)将创建共享库,如 Michael Aaron Safyan 所说。
但是如果你想知道它是如何工作的只是因为 - 比如说,你想编写自己的动态链接器,或者正在做某种 运行 时间代码生成,比如 JIT 编译器,或者如果你只是想知道 - 你可以制作一个原始代码文件。
要使用它,我们必须做的与链接器所做的类似 - 将代码加载到特定的地址,并 运行 它。也有位置无关的代码,可以运行在任何地址。
让我们首先编译和链接我们的函数,然后输出到某个地址的原始图像。假设函数是文件 func.c
中的 func
并且我们在 Linux 上使用 gcc。 (Windows 编译器会有类似的选项——我相信 Windows 上的 gcc 是完全一样的,但是像 Digital Mars 的 C 编译器这样的东西会有所不同,例如链接器命令是 /BINARY
)
无论如何,这就是我 运行:
gcc -c func.c # makes func.o
ld func.o --oformat=binary -e func -o func.binary
这会生成一个名为 func.binary 的文件。您可以使用 ndisasm -b 64 func.binary
(或 -b 32
如果您在 32 位模式下编译 C)最轻松地反汇编它以确认它看起来正确 - 我在那里看到一条添加指令,所以对我来说看起来不错。
如果您加载它并 mmaped 然后调用它...它应该可以工作。
问题很快就会出现:
- 如果该文件中有多个函数,它们将被压缩在一起。
- 他们试图用来互相呼叫的地址可能是完全错误的。
- 全局变量和其他静态数据会被弄乱。
还有更多。操作系统为可执行文件和库使用更复杂的文件格式是有原因的!
要进行下一步,您可以考虑编写一个 ELF 或 PE 加载程序,从标准文件中读取该元数据。当然,一旦你深入了解其中的大部分内容,你将完全按照 OS 提供的 dlopen
和 LoadLibrary
进行操作......所以除非目标只是了解胆量,只需调用这些函数即可完成!