例如,我有一个函数func()
:
int func (int a, int b) {return a + b;}
现在我想把它写到一个文件中,这样我就可以使用系统调用mmap用PROT_EXEC加载它,我可以从另一个程序调用它。我该怎么做呢?
答案 0 :(得分:5)
如果您在编译时知道所需的签名和静态库或共享库的位置,您可能只想在输出库中包含标题和链接。如果要动态调用函数,可能需要dlopen / dlsym(UNIX)或LoadLibrary / GetProcAddress(Windows)来动态加载库并检索地址按名称命名的功能。
请注意,您实际需要动态加载库(至少是显式)的情况非常罕见。这通常用于模块化体系结构(例如“插件”或“扩展”),其中应用程序的各个部分是分开分布的(使用IPC而不是动态加载可以更安全地实现...参见下面的注释)。或者,如果您的应用程序不允许静态包含依赖项,并且需要根据其正在执行的环境中某些库依赖项的存在而有条件地提供行为。但是,在大多数情况下,您只需要包含一个标头,声明您需要的符号并为每个目标平台进行编译(如果符号在OS或OS版本中有所不同,可能使用#if...#else
宏。)< / p>
从稳定性,安全性和代码复杂性的角度来看,我个人建议您避免动态库加载。对于核心系统功能,链接到动态库是合理的,但是你想要以动态加载的负担完全在你的工具链上的方式进行(即你不需要显式地调用dlopen或LoadLibrary) 。对于其他功能,静态链接几乎总是更好(假设您在依赖项有安全修复程序时分发更新),因为这样可以避免因不兼容的版本更新而中断,还可以防止用户遇到依赖性地狱(需要版本A,但其他一些应用程序需要版本B);模块化体系结构通常通过进程间通信(IPC)实现更好(更安全),因为动态加载的库存在于加载它们的程序的过程中(从而使它们能够访问整个进程的虚拟内存空间),而进程间通信,每个组件将是一个单独的进程,并且各个组件只能访问由调用进程明确提供给它的信息,这将使恶意组件更难以从调用者或其他方式窃取数据组件或产生不稳定性。
答案 1 :(得分:2)
如果您希望在现实世界中实际使用它,那么可能只是在每个平台上将源代码编译为程序的一部分,就像常规函数一样。
接下来最好的可能是一个与你交谈而不是合并的独立过程。
半理智(但仍然不是一个很好的选择,请参阅我们在另一个答案中的讨论)将制作共享库,就像Michael Aaron Safyan所说。
但是如果你想知道它是如何工作的,只是因为 - 比如说,你想编写自己的动态链接器,或者像JIT编译器那样进行某种运行时代码生成,或者如果你只是想知道 - 你可以制作原始代码文件。
要使用它,我们必须做的事情与链接器的作用类似 - 将代码加载到一个特定的地址,然后运行它并运行它。有位置无关的代码也可以在任何地址运行。
让我们首先编译和链接我们的函数,然后输出到某个地址的原始图像。假设文件func
中的函数为func.c
,我们在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加载程序,从标准文件中读取该元数据。当然,一旦你完成了大部分工作,你将完全按照操作系统提供dlopen
和LoadLibrary
....所以除非目标只是了解胆量,只需调用这些函数并完成调用即可!