You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为堆内存设置可执行权限后执行函数触发段错误问题排查

为什么复制函数字节码到堆执行会触发段错误?

我来帮你拆解这个问题的核心原因,以及对应的解决方案:

问题复现回顾

你编写的代码逻辑是:复制函数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指令的偏移量:

  1. 定位call指令在函数中的偏移:从objdump可以看到,call指令位于函数a的第0xd字节处(从起始地址11a9到11b6的距离)。
  2. 获取putchar的实际地址:可以用dlsym动态获取,避免硬编码地址(因为ASLR会随机化地址)。
  3. 计算新的相对偏移:根据相对调用的公式偏移量 = 目标地址 - (当前call指令地址 + 5)(call指令长度为5字节,下一条指令地址是当前地址+5)。
  4. 写入新的偏移量到堆内存中对应的位置。

示例修正代码(插入在复制完字节码后):

#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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 06:38:18