已关闭
【需求】HCOMM仓二进制符号隐藏改造:仅开放对外接口与包间接口 #795
严正行创建于  8月26日关闭于  28 天前
严正行
严正行成员
8月26日 创建

需求背景

当前 HCOMM 仓二进制(SO)的所有符号默认对外可见,存在两类问题:

  • 兼容性风险:用户可能直接链接并依赖内部符号(非 include/、pkg_inc/ 头文件声明的接口),后续版本升级一旦调整内部实现即造成不兼容,形成对外的隐性契约负担。
  • 架构约束:内部实现细节对外暴露后,重构、优化与模块调整的自由度被外部依赖锁死,违背"对外接口最小化"的架构演进方向。

本需求要求:HCOMM 二进制仅开放对外接口(include/、pkg_inc/ 头文件声明的接口,含全局变量和常量)与包间接口(仓内 SO 相互调用的接口,仅仓内可见、不对外暴露),隐藏其余全部符号。

需求列表

需求 工作量
HCOMM 仓符号隐藏改造 0.3k

0.5k 表示约 1 人月,0.3k 表示约 0.6 人月。

总体策略

  1. 梳理 SO 及接口:列出 HCOMM 仓所有 SO,确定各 SO 对外开放的 C/C++ 接口(主要位于 include/ 和 pkg_inc/ 目录)及其符号(包括全局变量和常量)。
  2. 编译链接控制:全局使用 -fvisibility=hidden,并优先采用版本脚本(Version Script)白名单文件仅导出 include/ 和 pkg_inc/ 中声明的符号,便于对导出符号进行严格看护(白名单文件即导出接口的完整清单,增删符号在评审中一目了然)。
  3. 内部接口处理:被外部组件依赖的内部符号,优先推动外部组件解除依赖(重点排查 MC2 组件);确实无法解除依赖的,本次即列为对外开放接口进行开放。
  4. 包间接口:仓内 SO 相互调用的接口优先通过白名单文件(Version Script)管控,默认隐藏、不对外暴露;此类接口可暂不迁移 pub_inc/ 目录——白名单文件本身即可看到所有接口清单,目录归位可后续按需推进。

HCOMM 仓执行要点

  • 涉及 SO:主库(libhcomm.so、hccl_v2.so、ccl_kernel.so、ccl_kernel_plf.so)及可能的内置算法 SO 等。
  • 公开头文件:梳理 include/ 和 pkg_inc/ 下的公开头文件,作为导出符号的权威来源。
  • 构建改造:改造 CMake 构建,增加符号可见性编译选项与版本脚本。

符号控制技术方案

采用"全局隐藏 + 按需导出"策略:编译阶段通过 -fvisibility=hidden 默认隐藏所有符号,导出控制优先采用 Version Script(版本脚本)白名单文件精确控制需要导出的符号列表,便于对导出符号进行严格看护。

C 符号处理

C 符号(C 语言风格函数与全局变量)符号名不做名称修饰(mangle),处理相对简单。

编译选项:

set(CMAKE_C_VISIBILITY_PRESET hidden)

导出标记两种方式:

方式一:源码属性标记(适合少量导出符号)——在头文件中使用 __attribute__((visibility("default"))) 标记需要导出的 C 函数:

#ifdef __cplusplus
extern "C" {
#endif

__attribute__((visibility("default")))
int HcommCommInitRootInfo(...);

__attribute__((visibility("default")))
int HcommAllReduce(...);

#ifdef __cplusplus
}
#endif

方式二:Version Script 统一控制(适合大量导出符号,主要 SO 优选此方式)——创建版本脚本 exports.map:

{
global:
    HcommCommInitRootInfo;
    HcommAllReduce;
    /* 所有对外 C 接口逐行列出 */
local:
    *;   /* 隐藏所有其他符号 */
};

链接时传入:

gcc -Wl,--version-script=exports.map -shared -o libhcomm.so *.o

Version Script 的优势:无需修改源码,集中管理导出符号列表,支持通配符匹配。

C++ 符号处理

C++ 符号经过名称修饰(mangled name,形如 _ZN5hcomm...),处理更复杂。

编译选项:

set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN 1)   # 隐藏内联函数符号

导出标记方式:

// 方式一:类级别标记(导出整个类及所有非静态成员函数)
class __attribute__((visibility("default"))) HcommComm {
    ...
};

// 方式二:函数级别标记(仅导出特定成员函数)
class HcommComm {
public:
    __attribute__((visibility("default"))) int init(...);
private:
    void internalHelper();  // 不导出,默认隐藏
};

// 方式三:统一宏简化标记
#define HCOMM_EXPORT __attribute__((visibility("default")))
class HCOMM_EXPORT HcommComm { ... };

Version Script 处理 C++ 符号(写 mangled name 或使用通配符匹配):

{
global:
    _ZN5hcomm9HcommComm*;   /* 匹配 HcommComm 类的所有符号 */
local:
    *;
};

C++ 符号获取方法:

# 查看当前 SO 导出的所有 C++ 符号(mangled name)
nm -D libhcomm.so | grep " T "

# 使用 c++filt 查看对应的原始符号名
nm -D libhcomm.so | c++filt

特殊符号处理:

  • RTTI 符号(typeinfo):-fvisibility=hidden 后 RTTI 符号默认隐藏。若外部需要 dynamic_cast 或 typeid 操作,需在 Version Script 中导出 _ZTI*(typeinfo)、_ZTS*(typeinfo name),或使用链接选项 -Wl,--dynamic-list-cpp-typeinfo(谨慎导出)。
  • 模板实例化符号:模板实例化默认受模板参数可见性约束。如需导出特定模板实例,显式实例化并标记可见性:template class __attribute__((visibility("default"))) HcommBuffer<int>;

CMake 完整配置示例

# ============ 符号可见性全局配置 ============
# 默认隐藏所有符号
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN 1)

# ============ 主库目标 ============
add_library(hcomm SHARED ${SOURCES})

# 设置目标级可见性属性
set_property(TARGET hcomm PROPERTY C_VISIBILITY_PRESET hidden)
set_property(TARGET hcomm PROPERTY CXX_VISIBILITY_PRESET hidden)
set_property(TARGET hcomm PROPERTY VISIBILITY_INLINES_HIDDEN ON)

# 链接时使用 Version Script 精确控制导出符号
set_target_properties(hcomm PROPERTIES
    LINK_FLAGS "-Wl,--version-script=${CMAKE_SOURCE_DIR}/exports.map"
)

包间接口符号控制(优先白名单文件管控)

仓内 SO 之间相互调用的接口(包间接口)不对外部暴露,优先通过白名单文件(Version Script)管控:

  1. 为每个内部 SO 单独维护 Version Script 白名单文件,仅导出该 SO 被仓内其他 SO 调用的符号;主库白名单仅导出 include/、pkg_inc/ 对外符号,包间接口不放入主库白名单的 global 列表。
  2. 白名单文件本身就是该 SO 的完整接口清单,符号增删在代码评审中一目了然,实现对导出符号的严格看护;仓内 SO 相互调用的接口可暂不迁移 pub_inc/ 目录,目录归位可后续按需推进。
  3. 内部 SO 链接时,通过直接链接而非符号导出的方式调用,不额外对外暴露符号:
# 内部 SO(仅仓内使用)
add_library(hcomm_internal SHARED ${INTERNAL_SOURCES})

# 内部 SO 使用独立的 version script 白名单,仅导出被仓内调用的包间接口符号
set_target_properties(hcomm_internal PROPERTIES
    LINK_FLAGS "-Wl,--version-script=${CMAKE_SOURCE_DIR}/internal_exports.map"
)

验证方法

编译完成后验证导出符号是否符合预期:

# 查看所有导出符号
readelf -s libhcomm.so | grep " GLOBAL " | grep " DEFAULT " | grep -v " UND "

# 或使用 nm
nm -D libhcomm.so | grep " T "

# 统计导出符号数量(应显著减少)
nm -D libhcomm.so | grep " T " | wc -l

实施步骤

  1. 使用 nm -D / readelf -s 获取当前所有 SO 的导出符号表。
  2. 对照公开头文件,区分对外符号与内部符号。
  3. 检查外部依赖(搜索 MC2 等组件引用),制定每项内部符号的处理决策(优先推动解除依赖,无法解除的本次列为对外开放接口)。
  4. 修改 CMakeLists.txt,添加符号控制编译选项和版本脚本白名单文件。
  5. 验证编译后导出符号符合预期,并回归测试功能。

实施前探查清单

编号 探查项 备注
P1 各仓 SO 清单(add_library 目标)及对应导出符号全集 nm -D / readelf -s 采集
P2 include/、pkg_inc/ 目录实际存在及包含的头文件列表 作为导出符号权威来源
P3 被外部组件(尤其 MC2)依赖的内部符号清单 优先推动解除依赖,无法解除的本次列为对外开放接口
P4 内部 SO 间的包间接口清单 形成各内部 SO 的白名单文件(Version Script)
P5 当前 CMake 是否已有符号控制相关配置 已确认:无
P6 是否需要符号版本化(Symbol Versioning) 已定调:不做符号版本化
P7 插件机制(如 NIC 插件)的符号导出策略 已定调:默认不导出

关联

likedislike
严正行严正行成员
8月26日 修改了issue 的描述
严正行严正行成员
8月26日 修改了issue 的描述
Leewis成员
8月27日 评论:

HCOMM仓二进制符号隐藏改造:仅开放对外接口与包间接口; 需求方案已明确,待确认开发计划;

likedislike
LLeewis成员
8月27日 issue类型由 任务 改变为 需求
LLeewis成员
8月27日 添加了label:requirement
严正行严正行成员
8月27日 添加了label:tech-debt
严正行
严正行成员
8月29日 评论:

符号管控技术方案补充结论:Version Script 与属性标记的机制对比与定调建议

针对本 issue 的"符号控制技术方案"章节,补充一份基于链接器行为实测的技术对比结论,供实施时定调参考。

一、机制本质:两者作用于不同阶段,不是平行二选一

属性标记(如 __attribute__((visibility("default")))) Version Script(map 文件)
作用阶段 编译期:决定符号进不进 .dynsym 候选 链接期:决定最终导出集合
事实源形态 分散:几百个头文件声明处 集中:每 SO 一个白名单文件
集合视图 无 有(map 文件即接口清单)

关键机制事实(实测 GNU ld 2.37 确证):

  1. 全局 -fvisibility=hidden(符号无属性宏提升)+ map 提名 = 符号导不出。hidden 符号进不了 .dynsym 候选,map 的 global 提名救不回来。这个组合是不可行的,实施时须避开。
  2. 属性与 map 同时存在不冲突,是上下游关系:属性决定"进不进候选",map global 提名决定"最终导出"。已有属性宏的库补挂 map 后,变成"属性是候选门票、map 是终选",两层各司其职。

二、漏符号风险的诚实对比

两种方案都会漏,且漏的瞬间都是静默的(编译链接均零告警)。真正的差异在漏了之后能否查出来:

  • 纯属性方案(全局 hidden + 宏标记):漏标是静默漏导——符号无声消失,运行时 dlsym 才失败。导出意图登记在几百个头文件的几百处声明上,没有机器可读的集合,审计无法自动化。
  • map 方案:漏提名同样静默,但导出意图登记在一个文件里,可以完全自动化核对:nm -D 实际导出集合 × map 白名单 × 仓内 dlsym 字符串字面量 × 头文件 extern 声明,四方机器比对,任何不一致 CI 即红。

另一个差异在直接链接消费上:map 漏提名的符号若被外部直接链接,链接期即报 undefined reference(立刻暴露);dlsym 消费链才会漏到运行时。因此 dlsym 字面量扫描必须进 CI 门禁。

三、分库定调建议

非 hccp 资源库(本仓主体 SO)——纯 map 模型:不加全局 -fvisibility=hidden、不加属性宏,map 白名单单层收口。理由:

  • 零源码侵入(不动几百个头文件);
  • map 文件即接口清单单一事实源,PR diff 直接呈现符号增删,逐条可评审;
  • local: * 的兜底隐藏效果与 hidden 等价;
  • C++ 符号可用 extern "C++" demangled 语法按命名空间/类通配(见下),无需逐成员加宏。

hccp 资源库(ra/ra_adp/net_co/rs 系,已建 hidden + HCCP_ATTRI_VISI_DEF 体系)——维持属性宏 + 补充精简 map:现状属性宏单独生效会把所有 VISI_DEF 标记符号全量导出(hccp.h 单头文件 97 处标记);补挂 map 后 map 从 VISI_DEF 候选中精简终选。两者必须同时存在,不是二选一;map 可以做得很小——global 段只从 VISI_DEF 接口中提名真正需要跨 SO 消费的(含 dlsym 消费链符号),其余被 local: * 收口。

四、C++ 符号建议用 extern "C++" demangled 语法替代 mangled 名

GNU ld 2.37 与 LLD 15.0.5 均支持(实测):

{
global:
    extern "C++" {
        hccl::ProfilerBase::AddTag*;      /* 类成员按需通配 */
        hccl::AlgTypeToStr*;              /* 命名空间级函数 */
        /* 需要区分重载时用精确签名(引号包裹):
           "hcomm::Send(char const*, unsigned long)"; */
    };
    HcommCommInitRootInfo;                /* C 符号照旧直写 */
local:
    *;
};

实测结论:

  • 类成员通配、重载精确签名(引号包裹 demangled 全签名)、STL 参数通配均按预期工作;
  • 通配符方式跨 _GLIBCXX_USE_CXX11_ABI 切换稳定(demangled 名不含 __cxx11 等 ABI 前缀差异,mangled 名则直接失效)——仓内存在 _GLIBCXX_USE_CXX11_ABI=0 条件编译场景,这点对符号表稳定性很重要;
  • 与 nm -D | c++filt 输出同形,CI 门禁脚本可直接比对。
  • 注意:类内定义(implicit inline)的成员函数不产生全局符号,不影响真实代码(类外定义形态)。

五、配套门禁建议(严格看护的必要条件)

建议实施时配套交付符号核对门禁脚本(CI 强制执行),核对四方一致性:

  1. map 白名单 × nm -D 实际导出(防白名单臃肿漂移——各 SO 混入自己不提供的符号);
  2. 仓内全部 dlsym 字符串字面量 × 所属依赖 SO 的 map 白名单(防漏提名导致运行时硬失败;grep 须 -A2 抓跨行调用);
  3. include/、pkg_inc/ 头文件 extern 声明 × 主库 map 白名单(防双源漂移)。

另建议关注测试态构建差异:map 挂载若以 if(NOT ENABLE_TEST) 守卫,测试构建不挂 map——测试绿不等于量产符号正确,符号核对门禁是唯一兜底。

likedislike
严正行严正行成员
8月29日 关联了pull request:[build] symbol table
LLeewis成员
9月2日 关联了看板:HCCL
LLeewis成员
9月2日 关联了看板:HCOMM
Leewis成员
28 天前 评论:

HCOMM仓二进制符号隐藏改造:仅开放对外接口与包间接口 相关需求已上库;https://gitcode.com/cann/hcomm/pull/4851

likedislike
LLeewis成员
28 天前 issue状态由 待办的 改变为 已验收
LLeewis成员
28 天前 关闭了 issue
CANN-robotCANN-robot成员
27 天前 添加了label:resolved
LLeewis成员
19 天前 移除了看板:HCOMM