调用从先前执行中保存的函数指针如何失败?
How can calling a function pointer saved from a previous execution fail?
我很好奇函数指针是否可以存储在一个文件中并在程序退出并重新启动时的某个未来时间点使用。例如,我的第一个测试程序 看起来 类似于这样的伪代码:
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();
}
}
这就是我想做的事情的逻辑。我将使用输入 1
启动它,让它退出,然后使用输入 2
再次 运行 它。不要认为那是我的真实代码,因为我删除了原始测试因为...
只有在我不更改可执行文件所在的目录时才有效!
向目录添加一个新文件(大概也删除一个文件)并将可执行文件移动到新的地方都会导致 fp();
崩溃。新函数地址将是一个不同的值。
所以我做了一个新的测试来计算旧函数指针和当前函数地址之间的差异。将该偏移量应用于旧函数指针并调用它会产生正确的函数调用,无论我对目录做了什么。
我相信这是UB。但是,就像取消引用空指针会导致段错误一样,UB 非常一致。
除了用垃圾重写数据,假设函数没有加载到DLL中,这种方法成功的可能性有多大?在什么情况下它仍然无法工作?
函数指针只有在程序每次加载到相同地址时才会起作用。现代 OS 有 "address space randomization",这会导致代码、数据和堆栈的实际地址随机移动 - 以避免修改 return 地址的堆栈溢出攻击 - 因为它是如果它是随机选择的,则不可能知道 "return to" 的地址。
有禁用随机更改的设置。
显然,如果在被调用函数所在的代码段开始之间的代码发生更改,它也将不起作用。
指针转换为 void *
,这应该是可能的 - 显然,文件内容不会在另一个 OS 或处理器架构上工作,但我没有看到特定的这不起作用的原因。
但是,一种更便携的方法是存储您正在使用的操作的序列号,而不是函数指针。然后做这样的事情:
for(;;)
switch(sequence)
{
case 1:
f();
sequence++;
break;
case 2:
g();
sequence++;
break;
}
...
}
失败时,存储 sequence
(或 sequence - 1
)。
以上假定 f
和 g
函数正在抛出异常或使用 longjmp
退出 [或者 ...
正在检查错误]。
除此之外,我看不出
的技术原因
如其他人所述,此问题是由 "Address Space Layout Randomization" (ASLR) 引起的。这种随机化是针对每个模块(即每个可执行映像)完成的。这意味着,如果您的所有函数都包含在您的 .exe 中,则可以保证它们始终与模块的基址具有相同的偏移量。如果某些函数在 DLL 中,同样适用,但来自 DLL 模块的基础。重要的是相关模块地址保持不变,否则将无法定位入口点和 DLL 函数。
在 Windows 环境中:
在 Visual Studio(和 MSVC)中,ASLR 默认是打开的,但是你可以在 "Linker > Advanced > Randomized Base Address" 选项中禁用它(命令行中的 /DYNAMICBASE:NO)。通过禁用此选项,函数将始终位于同一地址。
您还可以在运行时确定偏移量。模块基地址可以用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();
我很好奇函数指针是否可以存储在一个文件中并在程序退出并重新启动时的某个未来时间点使用。例如,我的第一个测试程序 看起来 类似于这样的伪代码:
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();
}
}
这就是我想做的事情的逻辑。我将使用输入 1
启动它,让它退出,然后使用输入 2
再次 运行 它。不要认为那是我的真实代码,因为我删除了原始测试因为...
只有在我不更改可执行文件所在的目录时才有效!
向目录添加一个新文件(大概也删除一个文件)并将可执行文件移动到新的地方都会导致 fp();
崩溃。新函数地址将是一个不同的值。
所以我做了一个新的测试来计算旧函数指针和当前函数地址之间的差异。将该偏移量应用于旧函数指针并调用它会产生正确的函数调用,无论我对目录做了什么。
我相信这是UB。但是,就像取消引用空指针会导致段错误一样,UB 非常一致。
除了用垃圾重写数据,假设函数没有加载到DLL中,这种方法成功的可能性有多大?在什么情况下它仍然无法工作?
函数指针只有在程序每次加载到相同地址时才会起作用。现代 OS 有 "address space randomization",这会导致代码、数据和堆栈的实际地址随机移动 - 以避免修改 return 地址的堆栈溢出攻击 - 因为它是如果它是随机选择的,则不可能知道 "return to" 的地址。
有禁用随机更改的设置。
显然,如果在被调用函数所在的代码段开始之间的代码发生更改,它也将不起作用。
指针转换为 void *
,这应该是可能的 - 显然,文件内容不会在另一个 OS 或处理器架构上工作,但我没有看到特定的这不起作用的原因。
但是,一种更便携的方法是存储您正在使用的操作的序列号,而不是函数指针。然后做这样的事情:
for(;;)
switch(sequence)
{
case 1:
f();
sequence++;
break;
case 2:
g();
sequence++;
break;
}
...
}
失败时,存储 sequence
(或 sequence - 1
)。
以上假定 f
和 g
函数正在抛出异常或使用 longjmp
退出 [或者 ...
正在检查错误]。
除此之外,我看不出
的技术原因如其他人所述,此问题是由 "Address Space Layout Randomization" (ASLR) 引起的。这种随机化是针对每个模块(即每个可执行映像)完成的。这意味着,如果您的所有函数都包含在您的 .exe 中,则可以保证它们始终与模块的基址具有相同的偏移量。如果某些函数在 DLL 中,同样适用,但来自 DLL 模块的基础。重要的是相关模块地址保持不变,否则将无法定位入口点和 DLL 函数。
在 Windows 环境中:
在 Visual Studio(和 MSVC)中,ASLR 默认是打开的,但是你可以在 "Linker > Advanced > Randomized Base Address" 选项中禁用它(命令行中的 /DYNAMICBASE:NO)。通过禁用此选项,函数将始终位于同一地址。
您还可以在运行时确定偏移量。模块基地址可以用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();