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)
我下面的示例适用于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".
如果我的程序无法修复,您能否提供一种让函数驻留在堆上的方法?谢谢!
下面是一个工作代码,这是我所做的:
printf一次以确保gcc将其包含在我们的二进制文件中。mmapPROT_EXECprintf函数作为参数传递给我们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)