C ++删除函数

时间:2017-11-22 05:48:40

标签: c++ memory

如果我有这样的功能

void check(char *buffer)
{
    //some stuff that only needs to be run once
}

我有什么办法可以删除'我打电话一次后从记忆中得到这个功能吗?

我可以轻松地在纯ASM中执行此操作。但我想知道是否有更标准的方法。

2 个答案:

答案 0 :(得分:8)

C ++标准(甚至是C标准)认为程序代码的总体是不可变的。所以"删除"是没有意义的。一个功能。

但是,让我们务实。大多数情况下,您的C ++程序被编译成在某些操作系统中运行的某些executable(我假设操作系统是Linux,但您可以根据其他操作系统调整我的答案)。有关操作系统的一般概述,阅读Operating Systems: Three Easy Pieces (可免费下载)

然后,当你的程序开始时(可能是你的shell在某些fork(2)中执行execve(2)然后process),会创建一个新的virtual address space(并进行管理)由内核配置MMU并通过page faults处理paging适当的数据,例如来自磁盘)。

通常,程序的ELF可执行文件包含code segment,它在虚拟地址空间的某些只读和可执行段中进行内存映射。由于该部分是只读的,因此您无法删除"或"写"它。使用shared libraries,虚拟地址空间更复杂,并且包含几个代码段(每个库可能有一个或多个)。

  

我可以轻松地在纯ASM中执行此操作。

某些(现代)操作系统上运行的程序

错误。由于代码段是只读的,因此您需要更改虚拟地址空间(仅 方式)通过适当的系统调用,例如mmapmunmapmprotect;您可以在C ++,C或甚至ASM中调用它们。某些功能已在汇编程序中编码的事实不会改变这一点。甚至汇编程序代码也不能覆盖代码段(在这种情况下你仍会得到一些segmentation fault),而不会改变你的虚拟地址空间。

你可以玩低级技巧,例如使用munmap(2)系统调用 - 来自某些C ++代码或C代码或汇编代码 - 来改变你的虚拟地址空间并从中删除一些页面。我强烈建议不这样做。但是,如果你走这条路,你需要了解ELF可执行文件和共享对象以及虚拟地址空间的所有细节。请注意ASLRelf(5)ld-linux(8)vdso(7)。使用objdump(1)& readelf(1)&请参阅ld(1)以了解ELF可执行文件的详细信息。

您可以将(check和其他几个函数的代码)放入某些plugin中,但在您的情况下,您可能不应该这样做。在这种情况下,你可以dynamically load插件然后卸载它。您将使用dlopen(3)加载插件,并dlclose将其卸载。插件的dlopen会执行几个 mmap(2)(因此增加虚拟地址空间)并处理relocations,其dlclose执行多个/proc/ {3}}(缩小虚拟地址空间)。

在Linux上使用cat /proc/self/maps(请参阅munmap(2))。在终端中尝试cat /proc/$$/mapscat /proc/$$/status以及/proc/self/maps。并从您的C ++程序中读取wc /proc/4735/maps(例如,请参阅proc(5))。

例如,this是用C ++编码的shell。它现在在我的桌面上运行pid 4735。它的虚拟地址空间有69个段,与556dad571000-556dad697000 r-xp 00000000 08:01 11272485 /usr/bin/fish 一样。它的代码段被映射(只读和可执行):

7fa96bcb3000-7fa96be4c000 r-xp 00000000 08:01 1704209  /lib/x86_64-linux-gnu/libc-2.25.so

我的C标准库的代码段可能在

ValueTuple

我不了解您的安全问题(仅在评论中提及,这应该包含在问题中),因为您的代码段是只读。实际上,恶意函数可以改变某些代码段的保护(使用fish)然后改变它。但是你需要解释代码注入(恶意函数)是如何发生的(你可以编写一个依赖于mprotect(2)的程序并用process isolation进行验证以避免这种情况发生,假设你的Linux内核是值得信赖的。)

也许您有隐私问题(但这会产生不同的问题)。显然,您不希望在代码中将密码保留为明文。

可能(但不太可能)您正在编写独立程序(没有任何操作系统),例如对于嵌入式系统及其微控制器(例如formal methods)。在这种情况下,您的代码在闪存中。如何更改或删除它将变为特定于硬件。顺便说一下,多次覆盖闪存会损害硬件。

答案 1 :(得分:1)

你可以这样做,但会有轻微的性能影响(如果你想只调用一次该功能则无关紧要):

在.c文件中:

static void check_private(char *buffer)
{
    //some stuff that only needs to be run once
    check = NULL; // disable calling this ever again
}

// declare check as pointer to function (pointer to char) returning void
// and intialize to check_private
void (*check)(char*) = check_private;

在相应的.h文件中:

extern void (*check)(char*);

只要您删除调试符号,并且不将其存储在程序的其他位置,check指针将成为存储check_private地址的唯一位置,因此一旦覆盖它消失了。

如果你知道你在具有内存保护的普通操作系统上运行,那么

NULL是一个不错的禁用值,因为它将生成的UB将是段错误,并且很容易检查。

如果您不想要段错误,可以将其设置为指向其他功能,例如:

static void check_disabled(char *buffer)
{
    (void)buffer; // disable compilation warning about unused parameter
    abort(); // maybe this
    exit(EXIT_FAILURE); // or this
    return; // or this for no-op
    for(;;); // or even this on some tiny embedded system
}

如果这是一个库,并且你想要某种基本的硬保护来防止无效使用,那么你可以使用这个导出函数代替指针:

void check(char *buffer)
{
    static int called = 0;
    if (!called) {
        called = 1;
        check_private(buffer);
    } else {
        // called 2nd time, do something, see above for suggestions
    }
}

同样,只要剥离调试符号,只有存储called变量地址的位置在此函数的代码中,因此无法访问它。好吧,除了通过调用UB以某种方式猜测它的地址并使用指针来修改它,但特别是对于现代优化编译器,这实际上是UB而不仅仅是hack,因为不能保证汇编和数据编译器实际生成什么这个C代码。