在mmap内存中执行代码触发段错误的问题排查
一级间接调用正常但二级间接调用触发段错误的函数复制问题
先把你提供的代码补全并整理成可复现的版本(补全了mmap的缺失参数):
#include <stdio.h> #include <string.h> #include <sys/mman.h> #include <unistd.h> #define BODY_SIZE 100 int f(void) { return 42; } int (*G(void))(void) { return f; } int (*(*H(void))(void))(void) { return G; } int (*g(void))(void) { void *r = mmap(0, BODY_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); if (r == MAP_FAILED) { perror("mmap failed"); return NULL; } // 复制f函数的汇编代码到内存区域 memcpy(r, f, BODY_SIZE); return (int (*)(void))r; } int main() { // 一级间接调用:正常运行 int (*fp1)(void) = g(); printf("一级调用结果:%d\n", fp1()); // 二级间接调用:触发段错误 int (*(*fp2)(void))(void) = g(); printf("二级调用结果:%d\n", (*fp2())()); return 0; }
问题根源分析
你遇到的段错误本质是类型不匹配导致的非法地址访问,或者是函数复制时的逻辑错位:
类型误用是直接原因:
- 一级调用时,你把
g()返回的指针当成int (*)(void)(指向返回int的函数),调用它执行的是复制后的f,自然返回42,完全正常。 - 二级调用时,你强行把同一个指针转换成
int (*(*)(void))(void)(指向返回函数指针的函数),调用fp2()时相当于执行f()并把返回的42当作函数指针去调用——42是一个极低的无效地址,访问它必然触发段错误。
- 一级调用时,你把
函数复制的目标错位:
如果你原本想复制的是G或H这类返回函数指针的函数,却错误复制了f,那类型不匹配的问题就必然出现。另外如果复制的是位置相关代码(非PIC编译的函数),函数内部的相对偏移在新内存地址会失效,也可能触发段错误。
修复方案
方案1:严格匹配类型与复制目标
如果你想实现二级间接调用的功能,需要让g()复制G这类返回函数指针的函数,同时保证返回值类型正确:
// 修改g函数,复制G而非f,返回类型匹配G的函数指针类型 int (*(*g(void))(void))(void) { void *r = mmap(0, BODY_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); if (r == MAP_FAILED) { perror("mmap failed"); return NULL; } memcpy(r, G, BODY_SIZE); return (int (*(*)(void))(void))r; } int main() { // 二级调用现在可以正常运行 int (*(*fp2)(void))(void) = g(); int (*fp1)(void) = fp2(); printf("二级调用结果:%d\n", fp1()); return 0; }
方案2:使用PIC编译避免位置相关错误
如果你的函数复制场景需要支持任意函数,编译时要开启位置无关代码(PIC)选项,确保复制后的函数在新地址仍能正确解析内部引用:
- GCC/Clang编译时添加参数:
gcc your_code.c -o your_program -fPIC
PIC模式下,函数内部的地址引用都是基于当前指令指针的相对偏移,复制到任何内存地址都能正确计算目标地址,避免因位置变化导致的无效访问。
方案3:准确计算函数大小
你定义的BODY_SIZE=100太随意,可能截断函数代码或复制无关数据。可以通过标记函数估算实际大小:
// 在目标函数后定义空函数作为结束标记 int f_end(void) { return 0; } // 计算f的实际大小 size_t f_size = (size_t)f_end - (size_t)f; // 复制时使用实际大小 memcpy(r, f, f_size);
额外注意事项
- 确保
mmap申请的内存有PROT_EXEC权限,否则执行时会触发权限错误; - 避免复制包含动态链接重定位信息的函数(比如调用动态库的函数),这类函数复制后无法正确解析重定位;
- 编译时可以添加
-fno-inline禁用内联优化,防止目标函数被编译器内联导致复制的不是实际函数代码。
内容的提问来源于stack exchange,提问作者LambdaBeta




