将功能复制到可执行页面并调用

时间:2016-02-26 15:12:11

标签: c memory assembly

我试图将一个函数复制到一个可执行页面并从那里运行它,但我似乎遇到了一些问题。 这是我的代码:

#include <stdio.h>
#include <string.h>
#include <windows.h>

int foo()
{
    return 4;
}
int goo()
{
    return 5;
}
int main()
{
    int foosize = (int)&goo-(int)&foo;
    char* buf = VirtualAlloc(NULL, foosize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (buf == NULL)
    {
        printf("Failed\n");
        return 1;
    }

    printf("foo %x goo %x size foo %d\n", &foo, &goo, foosize);

    memcpy (buf, (void*)&foo, foosize);

    int(*f)() = &foo;
    int ret1 = f();
    printf("ret 1 %d\n", ret1);

    int(*f2)() = (int(*)())&buf;
    int ret2 = f2 (); // <-- crashes here
    printf("ret2 %d\n", ret2);

    return 0;
}

我知道一些代码在技术上是UB((int)&amp; goo-(int)&amp; foo),但在这种情况下它表现得很好。

我的问题是为什么这不按预期工作? 在我看来,我将页面映射为可执行文件并在那里复制了现有函数,我只是调用它。

我缺少什么?

使用mmap在linux上的表现会有所不同吗? 提前致谢

3 个答案:

答案 0 :(得分:2)

正如每个人在评论中已经说过的那样,这是完全未定义的行为,并且绝不应该真正期望工作。但是,我使用调试器对你的代码进行了一些操作,并意识到它不起作用的原因(至少在Cygwin gcc编译器中)你是否正在创建f2错误地指向地址指针存储分配的内存,即buf。您想指向buf指向的内存。因此,您的作业应该是

int(*f2)() = (int(*)())buf;

通过该更改,您的代码将为我执行。但是,即使它有效,只要对程序进行任何其他更改,它就可能再次中断。

答案 1 :(得分:1)

我在调试模式下尝试使用MVSC 2008的代码。编译器碰巧创建了一个带有相对偏移的jmp表,而&foo&goo只是该表中的条目。

所以,即使你已经成功创建了一个可执行缓冲区并复制了代码(远远超过了有用的......),相对跳转现在指向一个不同的位置,并且(在我的例子中)很快落入{{1}陷阱!

TL / DR:由于编译器可以随意排列其代码,并且尽可能多的跳转使用相对偏移量,因此您不能依赖于复制可执行代码。它实际上是未定义的行为:

  • 如果编译器足够聪明,只能生成如下内容:

    int 3

    它可以有效

  • 如果编译器生成了具有相对跳转的更复杂的代码,它就会中断

结论:如果您完全控制二进制机器代码,则只能复制可执行代码,例如,如果您使用汇编代码并且知道您没有重定位问题

答案 2 :(得分:1)

您需要将foogoo声明为static,否则必须停用Incremental Linking

增量链接用于缩短构建应用程序时的链接时间,正常链接和增量链接的可执行文件之间的区别在于,在逐步链接的可执行文件中,每个函数调用都会通过链接器发出的额外JMP指令。 / p>

这些JMP允许链接器在内存中移动函数,而不更新引用该函数的所有CALL指令。但正是这种JMP导致了你的问题。将函数声明为static会阻止链接器创建此额外的JMP指令。