越过交叉编译的重重阻碍,我在将 ROS2 及其生态迁移到原生 OpenHarmony 平台上的过程中遇到了一个比较大的问题:ROS2 似乎无法加载插件形态的动态链接库!就是说,launch 一个 ROS2 应用(需要动态链接库)本身是可以的,但是这个 ROS2 应用如果使用到了动态链接库插件(一般通过 ROS2 的 class_loader 组件间接加载)就出问题了。

TL; DR:(太长不看版)最终解决方案请参见 “Final Solution” 一节;如果想看问题根源请参见 “问题根源” 一节。

下文将详细叙述调试经过,供读者思考、相互学习。举个实际迁移调试过程中的例子。下面是我编写的问题描述:


我在 OpenHarmony 上运行迁移的 Navigation2 框架时遇到一些问题,加载的 navigation2 相关的动态链接库出现加载失败的问题。

运行的应用环境是任意一个使用 navigation2 的 ROS 应用,例如 B 站鱼香肉丝 UP 开发的 fishbot 机器人的 navigation2.launch.py(删除 rviz2 结点)。

问题的图片信息如下:


并且不仅仅是一个 DifferentialMotionModellibmotions_lib.so),其他所有插件,如 StaticLayer(liblayers.so)全部报错。。

一、猜测:Symbols Not Found?

首先我考虑,会不会是符号缺失问题?在 OpenHarmony 上交叉编译可能有些链接参数和 runpath 设置有问题?

于是使用 readelf -dnm -D -Cldd 工具(从 glibc 上移植过来的),一通查找发现,并不缺少动态链接库,也不缺少符号。

那么排除符号缺失问题,只能是加载问题。为什么相同的动态链接库代码,在 Ubuntu 上正常运行,但在 OpenHarmony 上运行失败呢?它们最大的区别是 musl libc 和 glibc。

二、临时的解决方案?新问题出现

这样只能从 ROS2 加载所有插件的源码入手。通过源码层层查找可知,ROS2 主动加载动态链接库(尤其是插件)的逻辑位于 rcutils 包,以及 class_loader 包。

查找方法是先找加载 Nav2 插件(其实不仅仅是 Nav2 插件)的函数,按调用链溯源到 rcutils/src/shared_library.cclass_loader/include/class_loader/class_loader_core.hpp

我们发现,class_loader 中,创建插件中对象实例的函数中 dynamic_cast 总是返回空指针,提示转换失败。

我再次确认,Ubuntu 上并不存在这个问题,所以指针指向的数据区域理应是对的(因为代码逻辑没问题)?

为了快速确认是否是这里的问题,我临时将 dynamic_cast 改为 static_cast 来规避运行时检查,强制按照目标类型来使用目标内存,结果成功了?看起来没有,因为出现新的问题了:

注意:中间我们还修复了另一个问题,bad_weak_ptr,这个问题是 Humble 官方问题,和 OH 无关,解决方案 Github 都是有的,这里是 对应 issue对应 PR,我们照着修改 ros_navigation2/nav2_util/include/nav2_util/lifecycle_node.hppros_navigation2/nav2_util/src/lifecycle_node.cpp 就行,这里和讨论无关,不再赘述。

由于新问题 “Exception when loading BT: [Any:convert]: no known safe conversion between…” 在网络上甚至找不到相关求助帖,我们只能继续手动调试。

这个问题似乎是和 Behavior Tree 解析 XML(.../navigate_through_poses_w_replanning_and_recovery.xml)和实例化有关,并且看起来和前一个问题没关系(暂时的)。

因此我们继续补充日志,使用 execinfo.h 打印问题堆栈,在递归构造 Behavior Tree 的时候打印节点详细信息等等,一步步溯源。添加的第一版日志内容:

以及 XML 文件信息:

结合源码(behavior_tree_cpp_v3/src 中的 xml_parsing.cppbt_factory.cpp)看出,程序解析到 RemovePassedGoals 后,调用 XMLParser::Pimpl::createNodeFromXML 创建 Behavior Tree 节点时抛出上面的异常。

于是根据这个线索阅读源码、定向加强日志:

问题出在 BehaviorTreeFactory::instantiateTreeNode 执行加载 Behavior Tree 的插件 RemovePassedGoals 上!

阅读 bt_factory.hnav2_behavior_tree/plugins/action/remove_passed_goals_action.cpp 源码可知,插件加载过程中,会从 BehaviorTree::Blackboard(注释描述是在插件间存放 Behavior Tree 有类型数据的数据结构)上读取类型为 std::shared_ptr<tf2_ros::Buffer> 的数据。

这个数据在插件注册(configure)时被用 Any 的方法写到 Blackboard 中,而插件实例化(activate)时从 Any 中以 safe_any::cast<T> 的方法读取出来。

重点来了:但两种类型相同为什么会出错呢?我猛然想到上一个 dynamic_cast 失败的问题。。

三、问题的共性:C++ Template RTTI (Run-Time Type Information)

新问题中,std::shared_ptr<tf2_ros::Buffer> 恰好是一个模板实例化出来的类型,而旧问题中,dynamic_cast 映射的 impl::AbstractMetaObject<Base> 也是一个模板实例化的类型。

这两个问题的共性是,看起来它们的类型名称是一样的,但实际上运行时 libc 认为它们是不同的类型?这个验证起来简单,直接打印类型对应的 type info 和 typeid

以旧问题为例,使用打印调试法,检查 factoryMap 中究竟存放了什么,为什么类型不匹配?在 factory 赋值语句后面添加调试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// --- DEBUGGING BLOCK START ---
impl::AbstractMetaObjectBase* base_ptr = factoryMap[derived_class_name];
if (!factory && base_ptr) {
const std::type_info& src_type = typeid(*base_ptr);
const std::type_info& dst_type = typeid(impl::AbstractMetaObject<Base>);

CONSOLE_BRIDGE_logError("class_loader.impl: DYNAMIC CAST FAILURE DEBUGGING:");

// 1. Check raw pointer
CONSOLE_BRIDGE_logError(" Raw Pointer Address: %p", (void*)base_ptr);

// 2. Compare Type Names
CONSOLE_BRIDGE_logError(" Source Type Name: %s", src_type.name());
CONSOLE_BRIDGE_logError(" Target Type Name: %s", dst_type.name());

// 3. Compare Type Info Addresses
CONSOLE_BRIDGE_logError(" Source TypeInfo Address: %p", (void*)&src_type);
CONSOLE_BRIDGE_logError(" Target TypeInfo Address: %p", (void*)&dst_type);

// 4. Check Base Class TypeInfo
const std::type_info& base_param_type = typeid(Base);
CONSOLE_BRIDGE_logError(" Base Template Param TypeInfo Addr: %p", (void*)&base_param_type);
}
// --- DEBUGGING BLOCK END ---

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[component_container_isolated-1] Error:   class_loader.impl: DYNAMIC CAST FAILURE DEBUGGING:
[component_container_isolated-1] at line 295 in .../class_loader_core.hpp
[component_container_isolated-1] Error: Raw Pointer Address: 0x7f8ac2f430
[component_container_isolated-1] at line 298 in .../class_loader_core.hpp
[component_container_isolated-1] Error: Source Type Name: N12class_loader4impl10MetaObjectIN9nav2_amcl23DifferentialMotionModelENS2_11MotionModelEEE
[component_container_isolated-1] at line 301 in .../class_loader_core.hpp
[component_container_isolated-1] Error: Target Type Name: N12class_loader4impl18AbstractMetaObjectIN9nav2_amcl11MotionModelEEE
[component_container_isolated-1] at line 302 in .../class_loader_core.hpp
[component_container_isolated-1] Error: Source TypeInfo Address: 0x7ef19881a8
[component_container_isolated-1] at line 306 in .../class_loader_core.hpp
[component_container_isolated-1] Error: Target TypeInfo Address: 0x7efa22ad28
[component_container_isolated-1] at line 307 in .../class_loader_core.hpp
[component_container_isolated-1] Error: Base Template Param TypeInfo Addr: 0x7efa224840
[component_container_isolated-1] at line 311 in .../class_loader_core.hpp

关键来了:

1
2
[component_container_isolated-1] Error:      Source TypeInfo Address: 0x7ef19881a8
[component_container_isolated-1] Error: Target TypeInfo Address: 0x7efa22ad28

这两个类型的类型对象(type info object)的地址不同,这意味着 libc 将插件中的 AbstractMetaObject(0x7ef…)与 class_loader 中的 AbstractMetaObject(0x7efa…)视为完全不同的类型,尽管它们名称相同!

这正是典型的 Split RTTI(Run-Time Type Information)问题!

四、问题根源

为了验证我的想法,我先后执行:

1
2
nm -C -D libclass_loader.so | grep AbstractMetaObject
nm -C -D libmotions_lib.so | grep AbstracMetaObject

观察到第一条指令输出:

1
2
3
4
... (omit)
0000000000042078 V typeinfo for class_loader::impl::AbstractMetaObjectBase
0000000000016a6c V typeinfo name for class_loader::impl::AbstractMetaObjectBase
0000000000042060 V vtable for class_loader::impl::AbstractMetaObjectBase

第二条输出:

1
2
3
4
5
 ... (omit)
0000000000008110 V typeinfo for class_loader::impl::AbstractMetaObject<nav2_amcl::MotionModel>
0000000000008100 V typeinfo for class_loader::impl::AbstractMetaObjectBase
00000000000043f9 V typeinfo name for class_loader::impl::AbstractMetaObject<nav2_amcl::MotionModel>
000000000000443e V typeinfo name for class_loader::impl::AbstractMetaObjectBase

这证明了一件事,在两个动态链接库中,AbstractMetaObjectBase RTTI symbols 全标记为 Weak(V)。

询问了 GPT 后发现,这个加载行为对于 musl libc 和 glibc 是不同的:

Area glibc musl
Typeinfo merging forgiving strict
Symbol interposition permissive limited
RTLD_GLOBAL side effects common reduced
Weak RTTI symbols often merged not merged

于是我判断问题的根源:

  1. 两个库中都将符号 typeinfo for ... AbstractMetaObjectBase 标记为 V(弱引用)。
  2. musl libc 的动态链接器(ld-musl-xxx.so)对符号作用域边界要求更严格。当 class_loader 使用 dlopen 加载插件时,它不会将 RTTI symbols 加入全局符号表
  3. 再由于 RTTI symbols 不是全局可见(或插件处于隔离状态)的,插件无法 “看到” 主进程中的现有 TypeInfo;此时 musl libc 会 fallback 到使用 RTTI symbol local copy 模式:对每个没见过的 RTTI symbol,都在自己的内存空间中实例化创建一个新的 type info object

正因如此,跨动态链接库(DSOs)的 TypeInfo 对象的指针地址才会不一致,进而导致了两个相同的类型信息错误地存在两个不同的内存地址,进而导致 dynamic_cast<T> 以及 safe_any::cast<T> 这样的逻辑没法正确判断横跨插件传递的类型是否一致

五、问题缘由?

所以为什么会出现这个问题?

一开始我怀疑 musl libc 将不同动态链接库间共享全局 RTTI symbols 的机制改坏了,导致 RTTI 符号没办法跨动态链接库共享(或者是 HUAWEI 官方为了“安全”有意为之。。)

因为我调试时发现添加 LD_PRELOAD 提前将 libclass_loaderliblayers 加载到所有进程的内存中可以避免 RTTI symbols 重复创建,进而解决问题

之后和深开鸿那边的技术人员交流和调试后发现,目前交叉编译工具链中的链接器 flags 是这样设置的:

1
2
# last 4 is for moveit_core
export SSRVODKA_APPEND_COMMON_LINK_FLAGS="-Wl,--no-as-needed -lpthread -ldl -lm -lsframe -fopenmp -lc -lrt -lBulletSoftBody -lBulletDynamics -lBulletCollision -lLinearMath"

因为 -Wl,--no-as-needed 是一个状态开关,会影响其后所有引入库的处理方式,这导致编译链接时由 CMake 添加的其他库会被强制加入被编译库的依赖列表中。

一般程序可能只是加载时性能差一点,但是它还会改变库的加载方式和符号可见性,让我们误以为 RTTI symbols 已经共享(表现出之前 readelf / ldd 看到不缺少符号),但是实际上仍然有 split RTTI symbols 的问题。甚至会干扰正常的加载次序(也就是说滥用 -Wl,--no-as-needed 相当于乱填写 LD_PRELOAD);

为什么这么判断呢?因为发现加入 -Wl,--as-needed 就不需要 LD_PRELOAD 了,但仍然会出现 split RTTI symbols;

总之,目前是未知原因(判断可能是 OH musl libc 的行为)导致的 split RTTI symbols + -Wl,--no-as-needed 误导,合起来造成了这个错误。

五、解决方案

有了清晰的思路,问题就很好解决了。为了避免 RTTI symbols 在各个插件间不可见的问题,

  1. 首先是要确保 RTTI Symbols 能够正确导出,这个需要对所有可执行文件的链接过程(CMAKE_EXE_LINKER_FLAGS)加上 -rdynamic(或 -Wl,--export-dynamic);

  2. 然后需要让所有插件需要的 RTTI Symbols 显式加载到 global symbol table 中,让插件加载前这些 RTTI symbols 就已经位于内存中,这样再加载插件时,musl libc 能从 global symbol table 中找到,就不会错误创建额外的 type info object 了!

到这里,问题就能解决了吗?很可惜,还差一点。我们发现,仅仅是这样还是会出现 type info 不一致的问题。不应该啊?我们确定了问题根源,也找到了非常正确解决方案。。

最后这一个坑我怀疑是 OpenHarmony 留给我们的。因为我发现执行完上述方案后在 OH 上仍然存在 Split RTTI Symbols 的问题,这个对于正常的 musl / glibc 的 dynamic linker 都是没有的行为。最后还是需要强制将 Behavior Tree 相关的 RTTI symbols 强制加载进去。

Final Solution

  1. 确保 RTTI Symbols 能够正确导出,这个需要对所有可执行文件的链接过程(CMAKE_EXE_LINKER_FLAGS)加上 -rdynamic(或 -Wl,--export-dynamic);
  2. 确保交叉编译工具链的链接器 flags 不含有多余的 -Wl,--no-as-needed(如非必要,不应该将此选项保持开启,应该用完就关),可以尽可能避免插件错误的依赖和加载次序;
  3. (OpenHarmony 上的权宜之计)ROS2 源码将 src/ros/class_loader/include/class_loader/class_loader_core.hpp 中的 dynamic_cast 改为 static_cast
  4. (OpenHarmony 上的权宜之计)启动前添加 LD_PRELOAD 强制加载一些库(假设安装位置是 /data/install):export LD_PRELOAD=/data/install/lib/libc++_shared.so:/data/install/lib/libbehaviortree_cpp_v3.so:/data/install/lib/libbehavior_server_core.so:/data/install/lib/libnav2_behavior_tree.so:/data/install/lib/libbt_navigator_core.so

如果不是 OpenHarmony musl libc 而是普通 musl libc,则不需要第三、四步就行!

更多 ROS2 + OpenHarmony 技术解决方案,欢迎关注 OpenHarmony Robot PMC 一同交流!