函数体在堆上

use*_*639 4 c memory heap-memory

程序由三部分组成:文本、数据和堆栈。函数体位于文本部分。我们可以让函数体存在于堆上吗?因为我们可以更自由地操作堆上的内存,所以我们可能会获得更多的自由来操作函数。

在下面的 C 代码中,我将 hello 函数的文本复制到堆上,然后将函数指针指向它。该程序可以通过 gcc 编译良好,但在运行时会出现“分段错误”。

你能告诉我为什么吗?如果我的程序无法修复,您能否提供一种让函数驻留在堆上的方法?谢谢!

图灵机器人

#include "stdio.h"
#include "stdlib.h"
#include "string.h"

void
hello()
{
    printf( "Hello World!\n");
}

int main(void)
{
    void (*fp)();

    int size = 10000;     //  large enough to contain hello()
    char* buffer;
    buffer = (char*) malloc ( size );
    memcpy( buffer,(char*)hello,size );
    fp = buffer;
    fp();
    free (buffer);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

Vad*_*Vad 8

我下面的示例适用于x86_64带有 的Linux gcc,但类似的注意事项也适用于其他系统。

我们可以让函数体存在于堆上吗?

是的,我们绝对可以。但通常这称为 JIT(即时)编译。请参阅了解基本想法。

因为我们可以更自由地操作堆上的内存,所以我们可能会获得更多的自由来操作函数。

确切地说,这就是为什么像 JavaScript 这样的高级语言有 JIT 编译器。

在下面的 C 代码中,我将 hello 函数的文本复制到堆上,然后将函数指针指向它。该程序可以通过 gcc 编译良好,但在运行时会出现“分段错误”。

实际上,该代码中有多个"Segmentation fault"s 。

第一个来自这一行:

 int size = 10000;     //  large enough to contain hello()
Run Code Online (Sandbox Code Playgroud)

如果你看到你的 函数x86_64生成的机器代码,它编译后只有17 个字节gcchello

0000000000400626 <hello>:
  400626:   55                      push   %rbp
  400627:   48 89 e5                mov    %rsp,%rbp
  40062a:   bf 98 07 40 00          mov    $0x400798,%edi
  40062f:   e8 9c fe ff ff          call  4004d0 <puts@plt>
  400634:   90                      nop
  400635:   5d                      pop    %rbp
  400636:   c3                      retq   
Run Code Online (Sandbox Code Playgroud)

因此,当您尝试复制10,000 个字节时,您会遇到不存在的内存并得到"Segmentation fault".

其次,您使用 分配内存malloc,这会为您提供一块受 CPU 保护以防止在 Linux 上执行的内存片x86_64,因此这将为您提供另一片内存"Segmentation fault"

在幕后使用诸如、、 和 之malloc类的系统调用来分配内存。您需要做的是使用带保护的系统调用来分配可执行内存。brksbrkmmapmmapPROT_EXEC

第三,当gcc编译hello函数时,您并不真正知道它将使用哪些优化以及生成的机器代码是什么样子。

例如,如果您看到编译hello函数的第 4 行

40062f: e8 9c fe ff ff          call  4004d0 <puts@plt>
Run Code Online (Sandbox Code Playgroud)

gcc优化它以使用putsfunction 而不是printf,但这甚至不是主要问题。

x86架构上,您通常使用call汇编助记符来调用函数,但是,它不是单个指令,实际上有许多不同的机器指令可以call编译成,请参阅英特尔手册页第 1 卷。2A 3-123,供参考。

在您的情况下,编译器选择对汇编指令使用相对call寻址。

您可以看到这一点,因为您的call指令有e8操作码:

E8 - Call near, relative, displacement relative to next instruction. 32-bit displacement sign extended to 64-bits in 64-bit mode.
Run Code Online (Sandbox Code Playgroud)

这基本上意味着指令指针将从当前指令指针跳转相对字节数。

现在,当您将代码重新定位memcpy到堆时,您只需复制该相对值call,它现在会将指令指针相对值从您将代码复制到堆中,并且该内存很可能不存在,并且您将获得另一个"Segmentation fault".

如果我的程序无法修复,您能否提供一种让函数驻留在堆上的方法?谢谢!

下面是一个工作代码,这是我所做的:

  1. 执行printf一次以确保gcc将其包含在我们的二进制文件中。
  2. 将正确大小的字节复制到堆中,以免访问不存在的内存。
  3. 使用和选项分配可执行内存。mmapPROT_EXEC
  4. printf函数作为参数传递给我们heap_function,以确保gcc使用绝对跳转进行call指令。

这是一个工作代码:

#include "stdio.h"
#include "string.h"
#include <stdint.h>
#include <sys/mman.h>


typedef int (*printf_t)(char* format, char* string);
typedef int (*heap_function_t)(printf_t myprintf, char* str, int a, int b);


int heap_function(printf_t myprintf, char* str, int a, int b) {
    myprintf("%s", str);
    return a + b;
}

int heap_function_end() {
    return 0;
}


int main(void) {
    // By printing something here, `gcc` will include `printf`
    // function at some address (`0x4004d0` in my case) in our binary,
    // with `printf_t` two argument signature.
    printf("%s", "Just including printf in binary\n");

    // Allocate the correct size of
    // executable `PROT_EXEC` memory.
    size_t size = (size_t) ((intptr_t) heap_function_end - (intptr_t) heap_function);
    char* buffer = (char*) mmap(0, (size_t) size,
         PROT_EXEC | PROT_READ | PROT_WRITE,
         MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    memcpy(buffer, (char*)heap_function, size);

    // Call our function
    heap_function_t fp = (heap_function_t) buffer;
    int res = fp((void*) printf, "Hello world, from heap!\n", 1, 2);
    printf("a + b = %i\n", res);
}
Run Code Online (Sandbox Code Playgroud)

保存main.c并运行:

gcc -o main main.c && ./main
Run Code Online (Sandbox Code Playgroud)