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上按模块进行的,因此偏移量不能在模块内更改。