为堆内存设置可执行权限后执行函数触发段错误问题排查
为什么复制函数字节码到堆执行会触发段错误?
我来帮你拆解这个问题的核心原因,以及对应的解决方案:
问题复现回顾
你编写的代码逻辑是:复制函数a的字节码到对齐的堆内存,修改内存权限为可执行后尝试调用,但触发段错误。你的代码如下:
#include <stdio.h> #include <string.h> #include <sys/mman.h> #include <stdlib.h> #include <malloc.h> #include <unistd.h> void a() { printf("a"); } int main() { char * heap_pointer; char * func_pointer; int ret; int pagesize; int c; func_pointer = (char *) a; pagesize = sysconf(_SC_PAGE_SIZE); heap_pointer = aligned_alloc(pagesize, pagesize); //alligned memory allocation c = 0; while(func_pointer[c] != (char)0xc3) { //copying the instructions from one pointer to the other heap_pointer[c] = func_pointer[c]; c++; } heap_pointer[c] = (char)0xc3; mprotect(heap_pointer, pagesize, PROT_WRITE | PROT_READ | PROT_EXEC); //allowing execution (*(void (*)())(func_pointer))(); (*(void (*)())(heap_pointer))(); mprotect(heap_pointer, pagesize, PROT_READ); return 0; }
从objdump输出可以看到函数a的指令序列:
00000000000011a9 <a>: 11a9: f3 0f 1e fa endbr64 11ad: 55 push %rbp 11ae: 48 89 e5 mov %rsp,%rbp 11b1: bf 61 00 00 00 mov $0x61,%edi 11b6: e8 c5 fe ff ff call 1080 <putchar@plt> 11bb: 90 nop 11bc: 5d pop %rbp 11bd: c3 ret
核心原因:相对调用指令的地址偏移失效
段错误的直接原因是这条call指令:
11b6: e8 c5 fe ff ff call 1080 <putchar@plt>
x86-64架构中,e8是相对调用指令,它的操作数是「相对于当前指令下一条地址的偏移量」。在原.text段中,这个偏移计算后刚好指向putchar@plt的地址,但当你把这段指令复制到堆内存后:
- 堆内存的地址和原
.text段的地址完全不同(通常堆在高地址,.text在低地址) - 执行这条
call时,CPU会用堆中当前指令的地址加上偏移量计算目标地址,结果会指向一个完全无效的内存区域,直接触发段错误。
除此之外,还有一个潜在的次要问题:函数开头的endbr64指令是Intel CET(控制流强制技术)的一部分,如果你的Ubuntu系统开启了CET,堆内存即使被设为可执行,也可能没有被标记为合法的CET目标区域,这也会导致执行异常,但核心问题还是相对跳转。
解决方案
针对这个问题,有两种可靠的解决思路:
方案一:编译生成位置无关代码(PIC)
修改编译命令,强制生成位置无关的可执行文件,这样函数中的调用会使用基于全局偏移表(GOT)的间接跳转,而不是相对偏移:
gcc -fPIC -pie your_code.c -o your_program
这种方式下,复制到堆的字节码中的call指令会变成通过GOT表查找putchar的地址,只要程序的GOT表有效,堆中的代码就能正确调用函数。
方案二:手动修正堆中的相对调用偏移
如果你不想改变编译方式,可以在复制字节码后,手动计算并修正call指令的偏移量:
- 定位
call指令在函数中的偏移:从objdump可以看到,call指令位于函数a的第0xd字节处(从起始地址11a9到11b6的距离)。 - 获取
putchar的实际地址:可以用dlsym动态获取,避免硬编码地址(因为ASLR会随机化地址)。 - 计算新的相对偏移:根据相对调用的公式
偏移量 = 目标地址 - (当前call指令地址 + 5)(call指令长度为5字节,下一条指令地址是当前地址+5)。 - 写入新的偏移量到堆内存中对应的位置。
示例修正代码(插入在复制完字节码后):
#include <dlfcn.h> // 需要包含这个头文件 // 获取putchar的实际地址 void* putchar_addr = dlsym(RTLD_NEXT, "putchar"); if (!putchar_addr) { perror("dlsym failed"); exit(1); } // 定位call指令的偏移位置(e8后面的4字节是偏移量) uint32_t* call_offset_ptr = (uint32_t*)(heap_pointer + 0xd + 1); // 计算堆中call指令的地址 uint64_t call_heap_addr = (uint64_t)(heap_pointer + 0xd); // 计算新的相对偏移 uint32_t new_offset = (uint32_t)((uint64_t)putchar_addr - (call_heap_addr + 5)); // 写入新的偏移量 *call_offset_ptr = new_offset;
编译时需要链接动态库:
gcc your_code.c -o your_program -ldl
额外提示:不要用0xc3判断函数结尾
虽然当前案例中可行,但这种方式非常脆弱——复杂函数中可能包含多个0xc3字节(比如内联函数、异常处理代码)。更好的方式是:
- 使用编译器扩展
__builtin_function_size(a)获取函数的准确长度 - 或者解析ELF文件的符号表获取函数的起始和结束地址
内容的提问来源于stack exchange,提问作者Canbach
相关产品推荐
相关产品推荐

