已关闭
【需求】HCCL仓二进制符号隐藏改造:仅开放对外接口与包间接口 #659
严正行创建于 29 天前关闭于 16 天前
29 天前 修改了issue 的描述
28 天前 添加了label:tech-debt
严正行
26 天前 评论:
26 天前 评论:
符号管控技术方案补充结论:Version Script 与属性标记的机制对比与定调建议
针对本 issue 的"符号控制技术方案"章节,补充一份基于链接器行为实测的技术对比结论,供实施时定调参考。
一、机制本质:两者作用于不同阶段,不是平行二选一
属性标记(如 __attribute__((visibility("default")))) |
Version Script(map 文件) | |
|---|---|---|
| 作用阶段 | 编译期:决定符号进不进 .dynsym 候选 |
链接期:决定最终导出集合 |
| 事实源形态 | 分散:几百个头文件声明处 | 集中:每 SO 一个白名单文件 |
| 集合视图 | 无 | 有(map 文件即接口清单) |
关键机制事实(实测 GNU ld 2.37 确证):
- 全局
-fvisibility=hidden(符号无属性宏提升)+ map 提名 = 符号导不出。hidden 符号进不了.dynsym候选,map 的global提名救不回来。这个组合是不可行的,实施时须避开。 - 属性与 map 同时存在不冲突,是上下游关系:属性决定"进不进候选",map
global提名决定"最终导出"。已有属性宏的库补挂 map 后,变成"属性是候选门票、map 是终选",两层各司其职。
二、漏符号风险的诚实对比
两种方案都会漏,且漏的瞬间都是静默的(编译链接均零告警)。真正的差异在漏了之后能否查出来:
- 纯属性方案(全局 hidden + 宏标记):漏标是静默漏导——符号无声消失,运行时 dlsym 才失败。导出意图登记在几百个头文件的几百处声明上,没有机器可读的集合,审计无法自动化。
- map 方案:漏提名同样静默,但导出意图登记在一个文件里,可以完全自动化核对:
nm -D实际导出集合 × map 白名单 × 仓内 dlsym 字符串字面量 × 头文件 extern 声明,四方机器比对,任何不一致 CI 即红。
另一个差异在直接链接消费上:map 漏提名的符号若被外部直接链接,链接期即报 undefined reference(立刻暴露);dlsym 消费链才会漏到运行时。因此 dlsym 字面量扫描必须进 CI 门禁。
三、分库定调建议
本仓(hccl)全部 SO——纯 map 模型:不加全局 -fvisibility=hidden(现状仅 opgraph_hccl 有 hidden)、不加属性宏,map 白名单单层收口。理由:
- 零源码侵入(本仓
include/hccl.h、include/hccl_mc2.h现状无任何导出宏,属性方式需给全部对外声明批量加宏,工作量大且引入两套宏体系); - map 文件即接口清单单一事实源,PR diff 直接呈现符号增删,逐条可评审;
local: *的兜底隐藏效果与 hidden 等价;- C++ 符号可用
extern "C++"demangled 语法按命名空间/类通配(见下),无需逐成员加宏。
hcomm 仓的 hccp 资源库(ra/ra_adp/net_co/rs 系,已建 hidden + HCCP_ATTRI_VISI_DEF 体系)——维持属性宏 + 补充精简 map(在姊妹 issue 中定调,此处仅说明联动关系):属性宏决定候选、map 精简终选,两者必须同时存在。本仓通过 hccl_compat dlsym 消费 hcomm 符号的链路,依赖 hcomm 侧 map 白名单的正确性,门禁脚本须覆盖跨仓 dlsym 字面量核对。
四、C++ 符号建议用 extern "C++" demangled 语法替代 mangled 名
GNU ld 2.37 与 LLD 15.0.5 均支持(实测):
{
global:
extern "C++" {
hccl::AlgTypeToStr*; /* 命名空间级函数 */
hccl::ProfilerBase::AddTag*; /* 类成员按需通配 */
/* 需要区分重载时用精确签名(引号包裹):
"hccl::Send(char const*, unsigned long)"; */
};
HcclAllReduce; /* C 符号照旧直写 */
local:
*;
};
实测结论:
- 类成员通配、重载精确签名(引号包裹 demangled 全签名)、STL 参数通配均按预期工作;
- 通配符方式跨
_GLIBCXX_USE_CXX11_ABI切换稳定(demangled 名不含__cxx11等 ABI 前缀差异,mangled 名则直接失效)——本仓 hccl_compat 存在_GLIBCXX_USE_CXX11_ABI=0条件编译场景,这点对符号表稳定性很重要; - 与
nm -D | c++filt输出同形,CI 门禁脚本可直接比对。 - 注意:类内定义(implicit inline)的成员函数不产生全局符号,不影响真实代码(类外定义形态)。
五、配套门禁建议(严格看护的必要条件)
建议实施时配套交付符号核对门禁脚本(CI 强制执行),核对四方一致性:
- map 白名单 ×
nm -D实际导出(防白名单臃肿漂移——各 SO 混入自己不提供的符号); - 仓内全部 dlsym 字符串字面量 × 所属依赖 SO 的 map 白名单(防漏提名导致运行时硬失败;grep 须
-A2抓跨行调用;本仓 hccl_compat 层*_dl.cc全部经 dlsym 消费 hcomm 符号,是重点核对对象); include/头文件 extern 声明 × 主库 map 白名单(防双源漂移)。
另建议关注测试态构建差异:map 挂载若以 if(NOT ENABLE_TEST) 守卫,测试构建不挂 map——测试绿不等于量产符号正确,符号核对门禁是唯一兜底。


16 天前 添加了label:resolved
需求背景
当前 HCCL 仓二进制(SO)的所有符号默认对外可见,存在两类问题:
include/、pkg_inc/头文件声明的接口),后续版本升级一旦调整内部实现即造成不兼容,形成对外的隐性契约负担。本需求要求:HCCL 二进制仅开放对外接口(
include/、pkg_inc/头文件声明的接口,含全局变量和常量)与包间接口(仓内 SO 相互调用的接口,仅仓内可见、不对外暴露),隐藏其余全部符号。需求列表
总体策略
include/和pkg_inc/目录)及其符号(包括全局变量和常量)。-fvisibility=hidden,并优先采用版本脚本(Version Script)白名单文件仅导出include/和pkg_inc/中声明的符号,便于对导出符号进行严格看护(白名单文件即导出接口的完整清单,增删符号在评审中一目了然)。pub_inc/目录、包间接口头文件分散在src/下各目录,此类接口可暂不迁移pub_inc/目录——白名单文件本身即可看到所有接口清单,目录归位可后续按需推进。HCCL 仓执行要点
libhccl.so及内部 SO(scatter_op.so等)。include/hccl/(hccl.h、hccl_types.h等),作为导出符号的权威来源;包间接口头文件当前分散在src/下各目录,暂不迁移,由白名单文件统一管控。符号控制技术方案
采用"全局隐藏 + 按需导出"策略:编译阶段通过
-fvisibility=hidden默认隐藏所有符号,导出控制优先采用 Version Script(版本脚本)白名单文件精确控制需要导出的符号列表,便于对导出符号进行严格看护。C 符号处理
C 符号(C 语言风格函数与全局变量)符号名不做名称修饰(mangle),处理相对简单。
编译选项:
set(CMAKE_C_VISIBILITY_PRESET hidden)导出标记两种方式:
方式一:源码属性标记(适合少量导出符号)——在头文件中使用
__attribute__((visibility("default")))标记需要导出的 C 函数:// include/hccl/hccl.h #ifdef __cplusplus extern "C" { #endif __attribute__((visibility("default"))) int HcclCommInitRootInfo(...); __attribute__((visibility("default"))) int HcclAllReduce(...); #ifdef __cplusplus } #endif方式二:Version Script 统一控制(适合大量导出符号,主要 SO 优选此方式)——创建版本脚本
exports.map:链接时传入:
Version Script 的优势:无需修改源码,集中管理导出符号列表,支持通配符匹配。
C++ 符号处理
C++ 符号经过名称修饰(mangled name,形如
_ZN4hccl...),处理更复杂。编译选项:
set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_VISIBILITY_INLINES_HIDDEN 1) # 隐藏内联函数符号导出标记方式:
// 方式一:类级别标记(导出整个类及所有非静态成员函数) class __attribute__((visibility("default"))) HcclComm { ... }; // 方式二:函数级别标记(仅导出特定成员函数) class HcclComm { public: __attribute__((visibility("default"))) int init(...); private: void internalHelper(); // 不导出,默认隐藏 }; // 方式三:统一宏简化标记 #define HCCL_EXPORT __attribute__((visibility("default"))) class HCCL_EXPORT HcclComm { ... };Version Script 处理 C++ 符号(写 mangled name 或使用通配符匹配):
C++ 符号获取方法:
# 查看当前 SO 导出的所有 C++ 符号(mangled name) nm -D libhccl.so | grep " T " # 使用 c++filt 查看对应的原始符号名 nm -D libhccl.so | c++filt特殊符号处理:
-fvisibility=hidden后 RTTI 符号默认隐藏。若外部需要dynamic_cast或typeid操作,需在 Version Script 中导出_ZTI*(typeinfo)、_ZTS*(typeinfo name),或使用链接选项-Wl,--dynamic-list-cpp-typeinfo(谨慎导出)。template class __attribute__((visibility("default"))) HcclBuffer<int>;CMake 完整配置示例
# ============ 符号可见性全局配置 ============ # 默认隐藏所有符号 set(CMAKE_C_VISIBILITY_PRESET hidden) set(CMAKE_CXX_VISIBILITY_PRESET hidden) set(CMAKE_VISIBILITY_INLINES_HIDDEN 1) # ============ 主库目标 ============ add_library(hccl SHARED ${SOURCES}) # 设置目标级可见性属性 set_property(TARGET hccl PROPERTY C_VISIBILITY_PRESET hidden) set_property(TARGET hccl PROPERTY CXX_VISIBILITY_PRESET hidden) set_property(TARGET hccl PROPERTY VISIBILITY_INLINES_HIDDEN ON) # 链接时使用 Version Script 精确控制导出符号 set_target_properties(hccl PROPERTIES LINK_FLAGS "-Wl,--version-script=${CMAKE_SOURCE_DIR}/exports.map" ) # ============ 安装对外头文件 ============ install(FILES include/hccl/hccl.h include/hccl/hccl_types.h DESTINATION include/hccl/ )包间接口符号控制(优先白名单文件管控)
仓内 SO 之间相互调用的接口(包间接口)不对外部暴露,优先通过白名单文件(Version Script)管控:
include/、pkg_inc/对外符号,包间接口不放入主库白名单的global列表。pub_inc/目录、包间接口头文件分散在src/下各目录,可暂不迁移——白名单文件本身即可看到所有接口清单,目录归位可后续按需推进。# 内部 SO(仅仓内使用) add_library(hccl_internal SHARED ${INTERNAL_SOURCES}) # 内部 SO 使用独立的 version script 白名单,仅导出被仓内调用的包间接口符号 set_target_properties(hccl_internal PROPERTIES LINK_FLAGS "-Wl,--version-script=${CMAKE_SOURCE_DIR}/internal_exports.map" )验证方法
编译完成后验证导出符号是否符合预期:
# 查看所有导出符号 readelf -s libhccl.so | grep " GLOBAL " | grep " DEFAULT " | grep -v " UND " # 或使用 nm nm -D libhccl.so | grep " T " # 统计导出符号数量(应显著减少) nm -D libhccl.so | grep " T " | wc -l实施步骤
nm -D/readelf -s获取当前所有 SO 的导出符号表。实施前探查清单
add_library目标)及对应导出符号全集nm -D/readelf -s采集include/、pkg_inc/目录实际存在及包含的头文件列表;src/下分散的包间接口头文件分布关联