C++ 调用前一次执行保存的函数指针怎么会失败?
我很好奇函数指针是否可以存储在文件中,并在程序退出并重新启动后的某个时间点使用。例如,我的第一个测试程序类似于以下伪代码:C++ 调用前一次执行保存的函数指针怎么会失败?,c++,undefined-behavior,C++,Undefined Behavior,我很好奇函数指针是否可以存储在文件中,并在程序退出并重新启动后的某个时间点使用。例如,我的第一个测试程序类似于以下伪代码: void f(){} typedef void(*Fptr)(); int main() { int i; cin >> i; if (i == 1) { std::ofstream out(/**/); out << &f; } else { std
void f(){}
typedef void(*Fptr)();
int main() {
int i;
cin >> i;
if (i == 1) {
std::ofstream out(/**/);
out << &f;
}
else {
std::ifstream in(/**/);
Fptr fp;
in >> fp;
fp();
}
}
void f(){}
类型定义无效(*Fptr)();
int main(){
int i;
cin>>i;
如果(i==1){
std::流出(/**/);
out>fp;
fp();
}
}
这正是我想做的事情的逻辑。我会用输入1
启动它,让它退出,然后用输入2
再次运行它。不要认为这是我真正的代码,因为我删除了原来的测试,因为…
只有在我不更改可执行文件所在的目录的情况下,这才有效强>
向目录中添加一个新文件(可能也会删除一个文件)并将可执行文件移动到新的位置都会导致fp()代码>崩溃。新函数地址将是一个不同的值
所以我做了一个新的测试,计算旧函数指针和当前函数地址之间的差异。将该偏移量应用于旧函数指针并调用它会生成正确的函数调用,而不管我对目录做了什么
我相信这是UB。然而,就像取消引用空指针会导致segfault一样,UB是相当一致的
除了使用垃圾重写数据,并假设函数未加载到DLL中之外,此方法成功的可能性有多大?它在哪些方面仍然无法工作?只有在每次以相同的地址加载程序时,函数指针才会工作。现代操作系统具有“地址空间随机化”,这会导致代码、数据和堆栈的实际地址随机移动,以避免修改返回地址的堆栈溢出攻击,因为如果随机选取地址,则不可能知道要“返回”的地址
有一些设置可以禁用随机更改
显然,如果在被调用者函数所在的代码段的开头之间更改代码,它也将不起作用
指针被转换为一个void*
,这应该是可能的-显然,文件内容在另一个操作系统或处理器体系结构上不起作用,但我看不出这不起作用的具体原因
然而,一种更便携的方法是存储您正在使用的操作的序列号,而不是函数指针。然后像这样做:
for(;;)
switch(sequence)
{
case 1:
f();
sequence++;
break;
case 2:
g();
sequence++;
break;
}
...
}
出现故障时,存储序列
(或序列-1
)
以上假设f
和g
函数正在抛出异常或使用longjmp
退出[或..
正在检查错误]
除此之外,我看不出一个技术原因,为什么像其他人提到的那样,这个问题是由“地址空间布局随机化”(ASLR)引起的。对每个模块(即,每个可执行图像)进行随机化。这意味着,如果所有函数都包含在.exe中,则保证它们与模块底部的偏移量始终相同。如果某个函数在DLL中,同样适用,但从DLL模块的底部开始。重要的是,相对模块地址保持不变,否则将无法定位入口点和DLL函数
在Windows环境中:
在Visual Studio(和MSVC)中,默认情况下ASLR处于启用状态,但您可以在“链接器>高级>随机基地址”选项中禁用它(/DYNAMICBASE:NO In命令行)。通过禁用此选项,功能将始终位于同一地址
您还可以在运行时确定偏移量。可以使用GetModuleHandle()
获取模块基址(模块句柄实际上就是基址)。有了它,您可以使用指针的相对地址
uintptr_t base_address = (uintptr_t)GetModuleHandle(NULL);
uintptr_t offset = (uintptr_t)&f - base_address;
out << offset;
in >> offset;
fp = (Fptr)(offset + base_address);
fp();
uintpttr\u t base\u address=(uintpttr\u t)GetModuleHandle(NULL);
uintpttr_t offset=(uintpttr_t)&f-基址;
输出>偏移量;
fp=(Fptr)(偏移量+基址);
fp();
VisualC++将默认生成可执行的加载地址的可执行文件。相对地址只要在一个模块中就可以正常工作。正如你所说的,这是一种未定义的行为。如果你想写安全的、可移植的代码,你永远不能依赖它。它可能会工作一年,在一次无辜的操作系统更新后失败。谁知道呢。如果你喜欢冒险,那就去吧。+1表示你的好奇心。如前所述,在大多数情况下,使用相对地址是可行的,因为这正是在调用函数时在较低级别所做的。(至少对于位置独立的代码)但是,如果&function1-&function2
总是产生相同的偏移量,那么将保存的指针\u地址-&function1
添加到任何旧的函数指针值将产生当前函数的地址,对吗?但是,我的建议即使重新编译代码也会起作用,添加一些调试代码或类似的东西…从理论上讲,模块的各个部分是否可以加载到不同的非连续内存中?如果函数与基函数的偏移量与正常值不同,那将是我的黑客崩溃和烧毁的最快方式。或者,如果函数指针是某种查找表的键,因此值与函数的位置无关。@JamesRoot函数可以放在不同的部分,可以以不同的偏移量加载,但如果我没有弄错的话,ASLR至少是在Windows上按模块进行的,因此偏移量不能在模块内更改。