纯C语言中实现函数指针边界检查的更优方法咨询
函数指针合法性校验优化方案
核心思路
要从根源上减少校验成本,优先通过架构设计避免裸函数指针直接参与存储和流转,其次再针对必须存储裸指针的场景做轻量化校验,以下是具体可落地的方案。
方案1:静态映射表+状态索引(最推荐,完全对齐原有switch逻辑)
- 核心逻辑:不直接存储可变的函数指针,保留原有状态机的整型/枚举状态值,调用前通过编译期生成的只读常量表映射得到对应函数指针,从根源上避免非法指针出现。
- 实现示例:
首先定义统一的状态枚举,替代原有魔法数字的var变量:
typedef enum { STATE_DEFAULT = 0, STATE_FOO = 1, STATE_BAR = 2, STATE_FOOBAR = 3, } State; // 编译期只读的函数指针映射表,放入程序只读段,意外篡改概率极低 static void (*const state_func_table[])(void) = { [STATE_DEFAULT] = foo, [STATE_FOO] = foo, [STATE_BAR] = bar, [STATE_FOOBAR] = foobar, };
- 校验逻辑:
State current_state = get_current_state(); // 复用原有var变量的读写逻辑 // O(1)整数范围校验,耗时和合法函数数量无关 if (current_state >= sizeof(state_func_table)/sizeof(state_func_table[0])) { current_state = STATE_DEFAULT; // 非法值直接修正,与原有switch的default逻辑完全对齐 } state_func_table[current_state]();
- 流转逻辑:
调用完对应函数后,仍按照原有switch的规则更新current_state的值即可(比如执行完foo()后把current_state设为STATE_BAR),不需要修改原有业务的状态流转规则。 - 优势:
- 校验仅需一次整数比较,性能远高于遍历地址列表
- 映射表放在只读段,本身具备较强的防篡改能力
- 完全复用原有状态机的维护逻辑,没有额外学习成本
方案2:带标签的函数指针封装(必须存储裸函数指针的场景使用)
- 核心逻辑:不单独存储裸函数指针,给每个合法指针配套一个编译期固定的校验标签,调用前以标签为唯一可信依据完成校验和指针修正,任意一项不匹配就判定为非法篡改。
- 实现示例:
// 为每个合法函数定义唯一的固定校验标签 #define FOO_TAG 0xA3F2 #define BAR_TAG 0x1B9C #define FOOBAR_TAG 0x7D04 typedef struct { void (*func)(void); uint32_t tag; // 与func绑定的校验标签 } ValidFuncHolder; // 对外仅暴露封装好的结构体,禁止单独修改func成员 ValidFuncHolder current_func; // 赋值接口必须同时写入func和对应标签 void set_default_func(void) { current_func.func = foo; current_func.tag = FOO_TAG; } void set_bar_func(void) { current_func.func = bar; current_func.tag = BAR_TAG; }
- 校验逻辑:
// 标签是唯一可信的校验依据,任意篡改都会被识别 if (current_func.tag != FOO_TAG && current_func.tag != BAR_TAG && current_func.tag != FOOBAR_TAG) { set_default_func(); // 标签非法直接重置为默认 } else { // 标签合法的前提下,直接根据标签修正指针,完全避免指针被篡改的影响 if (current_func.tag == FOO_TAG) current_func.func = foo; else if (current_func.tag == BAR_TAG) current_func.func = bar; else current_func.func = foobar; } current_func.func();
- 优势:
不需要遍历所有合法函数的地址列表,仅通过标签即可完成校验和指针修正,性能更高;只要不是同时篡改func和对应的tag字段,就能完全规避非法指针调用的风险。
方案3:直接分支校验(合法函数数量极少的场景使用)
- 核心逻辑:如果合法函数的数量只有2~3个,不需要专门定义地址列表循环遍历,直接通过多分支逻辑判断即可,编译器会自动优化为最优的跳转逻辑。
- 实现示例:
void (*fun_ptr)(void) = get_possibly_tampered_ptr(); if (fun_ptr != foo && fun_ptr != bar && fun_ptr != foobar) { fun_ptr = foo; // 非法直接修正 } fun_ptr();
- 优势:
省去了数组遍历的循环开销,也不需要额外维护合法函数地址列表,内存开销更低,性能更优。
注意:所有用户态的校验都仅能防范意外篡改(比如栈溢出覆写、野指针赋值、初始化错误),如果遇到主动的内存破解(比如直接修改只读段、绕过校验逻辑),这类用户态校验无法提供防护,需要配合系统级内存保护机制(栈保护、DEP、ASLR)实现底层防护。
内容的提问来源于stack exchange,提问作者EN20
相关产品推荐
相关产品推荐

