已关闭
【需求】HCCL仓二进制符号隐藏改造:仅开放对外接口与包间接口 #659
严正行创建于  29 天前关闭于  16 天前
严正行
严正行成员
29 天前 创建

需求背景

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

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

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

需求列表

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

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

总体策略

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

HCCL 仓执行要点

  • 涉及 SO:主库 libhccl.so 及内部 SO(scatter_op.so 等)。
  • 公开头文件:对外接口集中在 include/hccl/(hccl.h、hccl_types.h 等),作为导出符号的权威来源;包间接口头文件当前分散在 src/ 下各目录,暂不迁移,由白名单文件统一管控。
  • 构建改造:改造构建,仅导出该目录声明的符号;MC2 依赖的内部接口专项处理。

符号控制技术方案

采用"全局隐藏 + 按需导出"策略:编译阶段通过 -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:

{
global:
    HcclCommInitRootInfo;
    HcclAllReduce;
    HcclBroadcast;
    HcclReduce;
    HcclAllGather;
    HcclReduceScatter;
    /* 所有对外 C 接口逐行列出 */
local:
    *;   /* 隐藏所有其他符号 */
};

链接时传入:

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

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 或使用通配符匹配):

{
global:
    _ZN4hccl9HcclComm*;   /* 匹配 HcclComm 类的所有符号 */
    _ZN4hccl12AllReduce*; /* 匹配 AllReduce 相关符号 */
local:
    *;
};

C++ 符号获取方法:

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

# 使用 c++filt 查看对应的原始符号名
nm -D libhccl.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"))) 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)管控:

  1. 为每个内部 SO 单独维护 Version Script 白名单文件,仅导出该 SO 被仓内其他 SO 调用的符号;主库白名单仅导出 include/、pkg_inc/ 对外符号,包间接口不放入主库白名单的 global 列表。
  2. 白名单文件本身就是该 SO 的完整接口清单,符号增删在代码评审中一目了然,实现对导出符号的严格看护;HCCL 仓当前暂无 pub_inc/ 目录、包间接口头文件分散在 src/ 下各目录,可暂不迁移——白名单文件本身即可看到所有接口清单,目录归位可后续按需推进。
  3. 内部 SO 链接时,通过直接链接而非符号导出的方式调用,不额外对外暴露符号:
# 内部 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

实施步骤

  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/ 目录实际存在及包含的头文件列表;src/ 下分散的包间接口头文件分布 作为导出符号权威来源
P3 被外部组件(尤其 MC2)依赖的内部符号清单 优先推动解除依赖,无法解除的本次列为对外开放接口
P4 内部 SO 间的包间接口清单 形成各内部 SO 的白名单文件(Version Script)
P5 当前 CMake 是否已有符号控制相关配置 已确认:无
P6 是否需要符号版本化(Symbol Versioning) 已定调:不做符号版本化
P7 插件机制的符号导出策略 已定调:默认不导出

关联

likedislike
严正行严正行成员
29 天前 修改了issue 的描述
Leewis成员
28 天前 评论:

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

likedislike
LLeewis成员
28 天前 issue类型由 任务 改变为 需求
LLeewis成员
28 天前 添加了label:requirement
严正行严正行成员
28 天前 添加了label:tech-debt
严正行
严正行成员
26 天前 评论:

符号管控技术方案补充结论: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 门禁。

三、分库定调建议

本仓(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 强制执行),核对四方一致性:

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

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

likedislike
LLeewis成员
22 天前 关联了看板:HCCL
LLeewis成员
22 天前 关联了看板:HCCL
Leewis成员
16 天前 评论:

整改HCCL仓二进制保证只开放对外接口和包间接口符号 已合入上库;

likedislike
LLeewis成员
16 天前 issue状态由 待办的 改变为 已解决
LLeewis成员
16 天前 关闭了 issue
CANN-robotCANN-robot成员
16 天前 添加了label:resolved
LLeewis成员
3 天前 移除了看板:HCCL