C 堆上的函数体
程序有三个部分:文本、数据和堆栈。函数体位于文本部分。我们能让函数体存在于堆上吗?因为我们可以更自由地操作堆上的内存,所以我们可以获得更多操作函数的自由 在下面的C代码中,我将hello函数的文本复制到堆上,然后将函数指针指向它。该程序可以通过gcc进行良好编译,但在运行时会出现“分段错误” 你能告诉我为什么吗? 如果我的程序无法修复,您能提供一种方法让函数在堆上生存吗? 谢谢 图灵机器人C 堆上的函数体,c,memory,heap,C,Memory,Heap,程序有三个部分:文本、数据和堆栈。函数体位于文本部分。我们能让函数体存在于堆上吗?因为我们可以更自由地操作堆上的内存,所以我们可以获得更多操作函数的自由 在下面的C代码中,我将hello函数的文本复制到堆上,然后将函数指针指向它。该程序可以通过gcc进行良好编译,但在运行时会出现“分段错误” 你能告诉我为什么吗? 如果我的程序无法修复,您能提供一种方法让函数在堆上生存吗? 谢谢 图灵机器人 #include "stdio.h" #include "stdlib.h" #include "stri
#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;
}
hello
不是要复制到缓冲区的源。您正在欺骗编译器,而它正在运行时进行报复。通过将hello
键入char*
,程序使编译器相信这一点,但事实并非如此永远不要超出编译器的智能。我可以想象,这可能适用于一个非常简单的体系结构,或者使用一个设计简单的编译器
这项工作的许多要求中有几个:
- 所有内存引用都必须是绝对的。。。没有pc相对地址,除了强>
- 某些控制传输需要是pc相关的(因此您复制的函数的本地分支可以工作),但如果其他控制传输恰好是绝对的,则最好是模块的外部控制传输,如
,可以工作printf()
memcpy()
搞砸了
你可以通过一步一步地追踪它并看着它自己射中头部来学习一些东西。如果memcpy黑客是问题所在,或许可以尝试以下方法:
f() {
...
}
g() {
...
}
memcpy(dst, f, (intptr_t)g - (intptr_t)f)
在malloc之后,您应该检查指针是否不是null
buffer=(char*)malloc(size);
memcpy(缓冲区,(字符*)hello,size)代码>这可能是您的问题,因为您试图在内存中分配一个大区域。你能检查一下吗?原则上,这是可行的。然而。。。您正在从“hello”复制,它基本上包含可能调用、引用或跳转到其他地址的汇编指令。当应用程序加载时,其中一些地址会得到修复。只要复制并调用它就会崩溃。此外,一些系统(如windows)具有数据执行保护,作为一种安全措施,可以防止执行数据形式的代码。还有,“你好”有多大?试图复制到它的末尾也可能会崩溃。您还依赖于编译器如何实现“hallo”。不用说,这将是非常依赖于编译器和平台的,如果它工作的话。你的程序是分段错误的,因为你的记忆不仅仅是“你好”;这个函数的长度不是10000字节,所以一旦你通过hello本身,你就会出错,因为你正在访问不属于你的内存
您可能还需要在某些时候使用mmap(),以确保尝试调用的内存位置实际上是可执行的
有许多系统可以做您想要做的事情(例如,Java的JIT编译器在堆中创建本机代码并执行它),但是您的示例要比这复杂得多,因为在运行时不容易知道函数的大小(在编译时,编译器还没有决定要应用什么优化,这就更加困难了)。您可能可以像objdump那样,在运行时读取可执行文件以找到正确的“大小”,但我不认为这是您在这里真正想要实现的。我下面的示例是针对Linuxx86_64
和gcc
,但类似的考虑应该适用于其他系统
我们能让函数体存在于堆上吗
是的,我们当然可以。但通常这叫做JIT(准时制)编译。基本思想请参阅
因为我们可以更自由地操作堆上的内存,所以我们可以获得更多操作函数的自由
确切地说,这就是为什么像JavaScript这样的高级语言有JIT编译器
在下面的C代码中,我将hello函数的文本复制到堆上,然后将一个函数指针指向它
实际上,您在该代码中有多个“分段错误”
s
第一个来自这一行:
int size = 10000; // large enough to contain hello()
如果您看到您的
hello
函数,它的编译长度仅为17个字节:
gcc
对其进行了优化,以使用put
函数而不是printf
,但这是
甚至不是主要问题
在x86
体系结构上,通常使用汇编调用函数
然而,助记符并不是一条指令,实际上有许多不同的机器指令可以编译成调用
,参见第2A卷3-123页,以供参考
在您的情况下,编译器选择对调用
汇编指令使用相对寻址
您可以看到这一点,因为您的调用
指令具有e8
操作码:
E8 - Call near, relative, displacement relative to next instruction. 32-bit displacement sign extended to 64-bits in 64-bit mode.
这基本上意味着指令指针将从当前指令指针跳转相对字节数
现在,当您使用memcpy
将代码重新定位到堆中时,您只需复制相对call
,它现在将相对于您复制y的位置跳转指令指针
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
40062f: e8 9c fe ff ff call 4004d0 <puts@plt>
E8 - Call near, relative, displacement relative to next instruction. 32-bit displacement sign extended to 64-bits in 64-bit mode.
#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);
}
gcc -o main main.c && ./main