序列化C中的函数指针并将其保存在文件中?
Serialize a function pointer in C and save it in a file?
我正在开发一个处理任意通用数据的 C 文件寄存器程序,因此用户需要提供要使用的函数,这些函数保存在寄存器结构的函数指针中并且运行良好。但是我需要能够在程序重新启动时再次 运行 这些功能,理想情况下用户不需要再次提供它们。我将有关寄存器结构的重要数据序列化并写入一个header。
我想知道如何将函数也保存在那里,编译后的 c 函数只是原始二进制数据,对吧?所以必须有一种方法可以将它存储到文件中并从文件中的内容加载函数指针,但我不确定如何做到这一点。有人能指出我正确的方向吗?
我假设 C 可以做到这一点,因为它允许您做几乎任何事情,但我可能会遗漏一些东西,我可以完全不使用系统调用来做到这一点吗?或者,如果不是,在 posix 中执行此操作的最简单方法是什么?
创建寄存器或创建新二级索引时提供的函数:
registerHandler* createAndOpenRecordFile(
int overwrite, char *filename, int keyPos, fn_keyCompare userCompare, fn_serialize userSerialize, fn_deserialize userDeserialize, int type, ...)
并保存为函数指针:
typedef void* (*fn_serialize)(void*);
typedef void* (*fn_deserialize)(void*);
typedef int (*fn_keyCompare) (const void *, const void *);
typedef struct {
...
fn_serialize encode;
fn_deserialize decode;
fn_keyCompare compare;
} registerHandler;
虽然您的逻辑在某种程度上是有道理的,但事情远比这复杂 得多。我的回答将包含此处已发表的大部分评论,仅以回答形式...
假设您有一个指向函数的指针。如果该函数中有跳转指令,则该跳转指令可以跳转到绝对地址。那就是说当你反序列化函数的时候,你必须要有办法强制把它加载到同一个地址,这样绝对跳转才能跳转到正确的地址。
这将我们带到下一点。鉴于你的问题被标记为 posix
,没有 POSIX 兼容的方法将代码加载到特定地址,有 MAP_FIXED
,但它不会工作,除非你写你自己的动态链接器。为什么这很重要?因为函数的汇编代码可能出于各种原因引用函数的起始地址,其中最突出的是函数本身是否将自己的地址作为另一个函数的参数。
这实际上将我们带到了下一点。如果序列化函数调用其他函数,您也必须将它们序列化。但那是 "easy" 部分。困难的部分是函数是否跳转到另一个函数的中间而不是调用另一个函数,这可能会发生,例如作为尾调用优化的结果。这意味着您必须序列化函数跳转到的所有内容(递归),但是如果函数跳转到 0x00000000ff173831
,您将从该地址序列化多少字节?
就此而言,您如何知道任何函数何时以可移植的方式结束?
更糟的是,你能保证函数在内存中是连续的吗?当然,所有现有的、健全的硬件 OS 内存管理器和硬件架构都使它在内存中是连续的,但它能保证从现在起 1 年后如此吗?
还有一个问题是:如果用户基于动态传递不同的函数怎么办?即如果环境变量 X
是 true
,我们需要函数 x()
,否则我们需要 y()
?
我们甚至不会考虑讨论跨硬件架构、操作系统甚至相同硬件架构版本的可移植性。
但是我们要谈谈安全问题。假设您不再需要用户向您提供指向他们代码的指针,这些代码可能有一个他们在新版本中修复的错误,您将继续使用有错误的版本,直到用户记得 "refresh"使用新代码的数据结构。
当我在上面说 "bug" 时,您应该阅读 "security vulnerability"。如果您正在序列化的易受攻击的函数启动了 shell,或者确实引用了进程之外的任何东西,它就会成为一个持久性漏洞。
简而言之,没有办法以理智和经济的方式做你想做的事。相反,您可以做的是强制用户为您打包这些功能。
最明显的方法是要求他们传递库的文件名,然后您可以使用 dlopen()
.
打开该库
另一种方法是传递类似 Lua 或 JavaScript 的字符串,并嵌入一个引擎以将这些字符串作为代码执行。
另一种方法是将路径传递给可执行文件,并在需要处理数据时执行这些文件。这是what git does。
但你可能应该做的只是要求用户始终通过这些功能。保持简单。
我正在开发一个处理任意通用数据的 C 文件寄存器程序,因此用户需要提供要使用的函数,这些函数保存在寄存器结构的函数指针中并且运行良好。但是我需要能够在程序重新启动时再次 运行 这些功能,理想情况下用户不需要再次提供它们。我将有关寄存器结构的重要数据序列化并写入一个header。
我想知道如何将函数也保存在那里,编译后的 c 函数只是原始二进制数据,对吧?所以必须有一种方法可以将它存储到文件中并从文件中的内容加载函数指针,但我不确定如何做到这一点。有人能指出我正确的方向吗?
我假设 C 可以做到这一点,因为它允许您做几乎任何事情,但我可能会遗漏一些东西,我可以完全不使用系统调用来做到这一点吗?或者,如果不是,在 posix 中执行此操作的最简单方法是什么?
创建寄存器或创建新二级索引时提供的函数:
registerHandler* createAndOpenRecordFile(
int overwrite, char *filename, int keyPos, fn_keyCompare userCompare, fn_serialize userSerialize, fn_deserialize userDeserialize, int type, ...)
并保存为函数指针:
typedef void* (*fn_serialize)(void*);
typedef void* (*fn_deserialize)(void*);
typedef int (*fn_keyCompare) (const void *, const void *);
typedef struct {
...
fn_serialize encode;
fn_deserialize decode;
fn_keyCompare compare;
} registerHandler;
虽然您的逻辑在某种程度上是有道理的,但事情远比这复杂 得多。我的回答将包含此处已发表的大部分评论,仅以回答形式...
假设您有一个指向函数的指针。如果该函数中有跳转指令,则该跳转指令可以跳转到绝对地址。那就是说当你反序列化函数的时候,你必须要有办法强制把它加载到同一个地址,这样绝对跳转才能跳转到正确的地址。
这将我们带到下一点。鉴于你的问题被标记为 posix
,没有 POSIX 兼容的方法将代码加载到特定地址,有 MAP_FIXED
,但它不会工作,除非你写你自己的动态链接器。为什么这很重要?因为函数的汇编代码可能出于各种原因引用函数的起始地址,其中最突出的是函数本身是否将自己的地址作为另一个函数的参数。
这实际上将我们带到了下一点。如果序列化函数调用其他函数,您也必须将它们序列化。但那是 "easy" 部分。困难的部分是函数是否跳转到另一个函数的中间而不是调用另一个函数,这可能会发生,例如作为尾调用优化的结果。这意味着您必须序列化函数跳转到的所有内容(递归),但是如果函数跳转到 0x00000000ff173831
,您将从该地址序列化多少字节?
就此而言,您如何知道任何函数何时以可移植的方式结束?
更糟的是,你能保证函数在内存中是连续的吗?当然,所有现有的、健全的硬件 OS 内存管理器和硬件架构都使它在内存中是连续的,但它能保证从现在起 1 年后如此吗?
还有一个问题是:如果用户基于动态传递不同的函数怎么办?即如果环境变量 X
是 true
,我们需要函数 x()
,否则我们需要 y()
?
我们甚至不会考虑讨论跨硬件架构、操作系统甚至相同硬件架构版本的可移植性。
但是我们要谈谈安全问题。假设您不再需要用户向您提供指向他们代码的指针,这些代码可能有一个他们在新版本中修复的错误,您将继续使用有错误的版本,直到用户记得 "refresh"使用新代码的数据结构。
当我在上面说 "bug" 时,您应该阅读 "security vulnerability"。如果您正在序列化的易受攻击的函数启动了 shell,或者确实引用了进程之外的任何东西,它就会成为一个持久性漏洞。
简而言之,没有办法以理智和经济的方式做你想做的事。相反,您可以做的是强制用户为您打包这些功能。
最明显的方法是要求他们传递库的文件名,然后您可以使用 dlopen()
.
另一种方法是传递类似 Lua 或 JavaScript 的字符串,并嵌入一个引擎以将这些字符串作为代码执行。
另一种方法是将路径传递给可执行文件,并在需要处理数据时执行这些文件。这是what git does。
但你可能应该做的只是要求用户始终通过这些功能。保持简单。