诡异的bug之dlopen

本文给大家分享一个比较诡异的bug,是关于dlopen的,我大致罗列了我项目中使用代码方式及结构,更好的复现这个问题,也帮助大家进一步理解dlopen.

问题复现

以下是项目代码的文件结构:

# tree
.
├── file1
│   ├── file1.cpp
│   └── file1_sub
│       ├── file1_sub.cpp
│       └── file1_sub.h
├── file2
│   ├── file2.cpp
│   └── file2_sub
│       ├── file2_sub.cpp
│       └── file2_sub.h
├── include
│   ├── factory.h
│   └── factory_register.h
└── main.cpp

首先来说该项目会产生一个可执行程序和4个库:

main.cpp  -> main(可执行程序)
file1_sub.cpp -> libfile1_sub.so
file1.cpp -> libfile1.so(依赖libfile1_sub.so)
file2_sub.cpp -> libfile2_sub.so
file2.cpp -> libfile2.so(依赖libfile2_sub.so)

代码比较简单,仅仅是main函数中打开libfile1.so和libfile12.so两个so库,并调用相应的函数runFile1和runFile2,我保证这里是最复杂的代码了:)

// main.cpptypedef void (*Func)();int main() {void *handler1 = dlopen("./libfile1.so", RTLD_LAZY | RTLD_GLOBAL);if (handler1 == NULL) {printf("ERROR:%s :dlopen1\n", dlerror());return -1;}Func file1Func = (Func) dlsym(handler1, "_Z8runFile1v");if (file1Func == NULL) {printf("ERROR:%s :dlsym1\n", dlerror());return -1;}void *handler2 = dlopen("./libfile2.so", RTLD_LAZY | RTLD_GLOBAL);if (handler2 == NULL) {printf("ERROR:%s :dlopen2\n", dlerror());return -1;}Func file2Func = (Func) dlsym(handler2, "_Z8runFile2v");if (file2Func == NULL) {printf("ERROR:%s :dlsym2\n", dlerror());return -1;}file1Func();file2Func();for (;;) {}return 0;
}

然后再继续看file1和file2中分别做了什么, 因为file1和file2都会用到factory这个,那就先来看下factory.h

// factory.htemplate<typename T>
struct Factory {static Factory& instance() {static Factory f;return f;}T t{};
};

一个很简单模板类,就一个T的成员。比较关键的是,这个提供了单例对象。而后我们都会使用这个单例对象。

// file1.cppvoid runFile1() {File1Sub sub;sub.run();std::cout << "addr:" << &(Factory<int>::instance().t) << ", value:" << Factory<int>::instance().t << std::endl;
}

file1中首先会去调用File1Sub的run函数,然后打印Factory的成员的值和地址。
其实file2中也是做类似的事情:

// file2.cppvoid runFile2() {File2Sub sub;sub.run();std::cout << "addr:" << &(Factory<int>::instance().t) << ", value:" << Factory<int>::instance().t << std::endl;
}

然后我们再来看下file1_sub和file2_sub的run做了什么事情,在这之前还扔需要看下factory_register文件,因为这两个类会用到:

// factory_register.hstruct FactoryRegister
{FactoryRegister(int val) {Factory<int>::instance().t = val;}
};

FactoryRegister仅仅就是在构造函数中调用一下Factory并给其成员赋值。

继续看下file1_sub和file2_sub

// file1_sub.cpp
void File1Sub::run() {FactoryRegister r(12);
}// file2_sub.cpp
void File2Sub::run() {FactoryRegister r(22);
}

最简单的语言来说就是,file_sub来设定单例的值,file来获取单例的值。

这里看到file_sub1,file_sub2,file1,file2使用的是同一个单例对象。不过稍微绕一点的是使用factory_register来赋值,这在实际项目中也是会遇到的,假如你想在main函数之前就将factory注册成功呢,就需要一个static或者全局变量来操作factory,这里就是提供了factory_register这个实现。

我们使用如下指令来编译:

# 编译main,dlopen需要用到dl库
g++ main.cpp -ldl -o main# 编译file_sub库
g++ file1/file1_sub/file1_sub.cpp -fPIC -shared -o libfile1_sub.so
g++ file2/file2_sub/file2_sub.cpp -fPIC -shared -o libfile2_sub.so# 编译file库(需要依赖file_sub库)
g++ file1/file1.cpp -fPIC -shared -L. -lfile1_sub -o libfile1.so
g++ file2/file2.cpp -fPIC -shared -L. -lfile2_sub -o libfile2.so

然后我们运行main试试:

# ./main
addr:0x7f04b67cf06c, value:12
addr:0x7f04b67cf06c, value:22

一切完美,都是相同的变量地址,值也设定成功了。

不过我的问题也不是出现在这里,项目中使用qnx,我们编译完运行的结果却是这样的:

# ./main
addr:111cf37048, value:12
addr:111cf5d048, value:0

是不是很意外,地址不一样也就算了,关键的值还没有赋值成功,太诡异了。

问题分析

我们先在linux上分析一波,我们猜想问题肯定出现在factory和factory_register这两个文件,我们在各个库上看下这两个符号:

# nm -C libfile1.so | grep "Factory"
000000000020106c u Factory<int>::instance()::f# nm -C libfile2.so | grep "Factory"
000000000020106c u Factory<int>::instance()::f# nm -C libfile1_sub.so | grep "Factory"
0000000000000914 W FactoryRegister::FactoryRegister(int)
000000000020104c u Factory<int>::instance()::f# nm -C libfile2_sub.so | grep "Factory"
0000000000000914 W FactoryRegister::FactoryRegister(int)
000000000020104c u Factory<int>::instance()::f

我只罗列了关键的信息,可以看到factory的单例对象在四个库中都有,FactoryRegister::FactoryRegister构造函数就只有在file_sub库中有。
然后我观测qnx编译的库也是类似的。

那我们也先不要FactoryRegister,赋值的地方直接调用Factory的instance来设定,看下运行结果:

# qnx
addr:111cf37048, value:12
addr:111cf5d048, value:22

虽然地址不一样,但是值确实是赋值成功了,也可以达到预期。

这里其实到了一个相对盲区的地方,一般来说我们其实是动态库之间不应该出现相同符号的。

到这里我们其实也应该知道了大致的原因了,就是因为qnx和linux上使用dlopen时针对同名符号解析是不同的。

通过在qnx上符号的地址查看,可以得出下图:

libfile1.so对factory的引用都是在自己所在的so中,libfile1_sub.so对factory的引用是在libfile1.so,但是libfile2_sub.so对FactoryRegister引用需要到libfile1_sub.so中,进一步到libfile1.so中对factory设定。

所以在libfile.so中获取的factory的地址是不一样的。而libfile2.so对factory成员值的获取是0。

关于dlopen

我们使用的dlopen的mode是RTLD_LAZY | RTLD_GLOBAL

  • RTLD_LAZY表示该库函数符号会延迟到使用调用时采取解析重定位等,与之相反的是RTLD_NOW。
  • RTLD_GLOBAL表示该库中的符号会加入到全局符号表中,以便于后边使用dlopen的库使用。与之相反的是RTLD_LOCAL表示该库的符号仅给该组中库使用。(这里的组表示该库及随之一起加载的依赖的库)

由上边的排查,我们知道实际上是由于dlopen函数对相同符号解析位置的设定导致这个问题的出现,我们打开qnx的官方文档对于dlopen符号查找位置顺序解释:

  1. 加载的动态库
  2. LD_PRELOAD环境变量指定ELF文件(这里我们没有用到)
  3. 全局列表
  4. 加载的动态库所依赖的动态库

那我们再回来看下各个符号的查找细节:

  • file1中factory的符号在本库是有的,对factory引用就直接到本库中找就行的。
  • file1_sub中FactoryRegister放到全局列表中,file1_sub中FactoryRegister对factory的引用就会优先到file1中查找。
  • file2的factory也是定位到本库的
  • file2_sub对FactoryRegister引用就会先到file2中查找,但是file2中是没有的,然后回去全局列表中查找,就找到了file1_sub中。

所以对file2就不会拿到预期的值,去掉FactoryRegister的引用就可以到预期了。

总结

本文我们从例子中看出来dlopen的解析符号的位置和顺序会影响程序的正确性。
我们大致总结三点:

  1. dlopen等函数不仅仅是依赖于运行时库还依赖操作系统,不同操作系统上表现可能不一样
  2. 尽量不要多个不同的ELF文件含有相同的符号,比如这个例子中我们就可以让单独一个so库对factory及factory_register进行封装,大家都使用这个库保证符号的单一性。
  3. 排查问题时可以使用readelf,nm,pmap等指令查看elf文件中的符号,以及运行时符号所在的位置等

ref

http://www.qnx.com/developers/docs/qnxcar2/index.jsp?topic=%2Fcom.qnx.doc.neutrino.sys_arch%2Ftopic%2Fdll_SYMBOLNAME.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/142790.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023-11-15 LeetCode每日一题(K 个元素的最大和)

2023-11-15每日一题 一、题目编号 2656. K 个元素的最大和二、题目链接 点击跳转到题目位置 三、题目描述 给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。你需要执行以下操作 恰好 k 次&#xff0c;最大化你的得分&#xff1a; 从 nums 中选择一个元素 m 。将选中…

C语言从入门到精通之【概述】

#include指令和头文件 例如#include <stdio.h>&#xff0c;我们经常看到C文件最上面会有类似这样的语句&#xff0c;它的作用相当于把stdio.h文件中的所有内容都输入该行所在的位置。实际上&#xff0c;这是一种“拷贝-粘贴”的操作。 #include这行代码是一条C预处理器…

STM32中使用看门狗实现系统自动复位

STM32中的看门狗(Watchdog)是一种用于监控系统运行状态并在系统故障或死锁时执行自动复位的硬件功能。在本文中&#xff0c;我将介绍如何在STM32微控制器中使用看门狗来实现系统的自动复位。下面是详细的解释&#xff1a; 一、看门狗原理简介 看门狗是一种独立的硬件计时器&am…

DNA甲基化的相关知识

目录 1. DNA甲基化简介 2. 原理 3. 酶分类 4. DNA甲基化类型 5.机制 6. 十大DNA甲基化研究核心问题 6.1 植物中的甲基化 6.2 植物中DNA甲基化的主要功能 6.3 DNA甲基化作为生物标志物的潜力 6.4 DNA甲基化检测方法 1. DNA甲基化简介 DNA甲基化&#xff08;DNA methy…

MySQL MVCC机制详解

MySQL MVCC机制详解 MVCC, 是Multi Version Concurrency Control的缩写&#xff0c;其含义是多版本并发控制。这一概念的提出是为了使得MySQL可以实现RC隔离级别和RR隔离级别。 这里回顾一下MySQL的事务&#xff0c; MySQL的隔离级别和各种隔离级别所存在的问题。 事务是由 …

【大语言模型】Docker部署清华大学ChatGLM3教程

官方地址&#xff1a;https://github.com/THUDM/ChatGLM3 1 将代码保存至本地 方法1&#xff1a; git clone https://github.com/THUDM/ChatGLM3 方法2&#xff1a; https://github.com/THUDM/ChatGLM3/archive/refs/heads/main.zip 2 创建Docker文件 注&#xff1a;请先…

人工智能与新能源电动车的融合——技术创新引领未来交通革命

人工智能与新能源电动车的融合——技术创新引领未来交通革命 摘要&#xff1a;本文探讨了人工智能与新能源电动车领域的技术融合&#xff0c;分析了其在智能驾驶、电池技术、充电设施等方面的应用与创新。文章指出&#xff0c;这两大技术的结合将重塑交通产业&#xff0c;为我…

Unity之NetCode多人网络游戏联机对战教程(8)--玩家位置同步

文章目录 前言添加相机玩家添加对应组件服务端权威&#xff08;server authoritative&#xff09;客户端权威&#xff08;client authoritative&#xff09;服务端同步位置阅读与理解PlayerTransformSync.csNetworkVariableUploadTransformSyncTransform 后话 前言 承接上篇&a…

【MybatisPlus】条件构造器、自定义SQL、Service接口

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaEE 操作系统 Redis 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 MybatisPlus 一、条件构造器1.1 基于QueryW…

【设计原则篇】聊聊开闭原则

开闭原则 其实就是对修改关闭&#xff0c;对拓展开放。 是什么 OCP&#xff08;Open/Closed Principle&#xff09;- 开闭原则。关于开发封闭原则&#xff0c;其核心的思想是&#xff1a;模块是可扩展的&#xff0c;而不可修改的。也就是说&#xff0c;对扩展是开放的&#xf…

学开发语言 求职互联网行业的未来发展

我喜欢回答各种各样的问题&#xff0c;自然也喜欢记录下自己的一些观点和看法。希望给朋友们多一点参考&#xff0c;也欢迎交流探讨。 提问&#xff1a; 自考本科&#xff0c;学的开发语言&#xff0c;问互联网行业求职和发展&#xff01; 作为一个资深码农&#xff0c;对这样…

php 插入排序算法实现

插入排序是一种简单直观的排序算法&#xff0c;它的基本思想是将一个数据序列分为有序区和无序区&#xff0c;每次从无序区选择一个元素插入到有序区的合适位置&#xff0c;直到整个序列有序为止 5, 3, 8, 2, 0, 1 HP中可以使用以下代码实现插入排序算法&#xff1a; functi…

Word 插入的 Visio 图片显示为{EMBED Visio.Drawing.11} 解决方案

World中&#xff0c;如果我们插入了Visio图还用了Endnote&#xff0c; 就可能出现&#xff1a;{EMBED Visio.Drawing.11}问题 解决方案&#xff1a; 1.在相应的文字上右击&#xff0c;在出现的快捷菜单中单击“切换域代码”&#xff0c;一个一个的修复。 2.在菜单工具–>…

数据结构 | 图

最小生成树算法 Prime算法 算法思路&#xff1a;从已选顶点所关联的未选边中找出权重最小的边&#xff0c;并且生成树不存在环。 其中&#xff0c;已选顶点是构成最小生成树的结点&#xff0c;未选边是不属于生成树中的边。 例子&#xff1a; 第一步&#xff1a; 假设我们从顶…

从0到0.01入门 Webpack| 002.精选 Webpack面试题

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

使用VSCode进行Python模块调试

使用VSCode进行Python模块调试 创建测试文件 创建文件test/a/b.py&#xff0c;且当前工作路径为test/ b.py文件内容&#xff1a; def cal(numa, numb):print(int(numa) int(numb))if __name__ "__main__":import sys# 判断系统参数长度是否为4且判断第2个参数是…

Sentinel底层原理(下)

1、概述 Sentinel的核心原理&#xff0c;也就是前面提到暗流涌动的SphU.entry(…)这行代码背后的逻辑。 Sentinel会为每个资源创建一个处理链条&#xff0c;就是一个责任链&#xff0c;第一次访问这个资源的时候创建&#xff0c;之后就一直复用&#xff0c;所以这个处理链条每…

打造全身视角的医院可视化能源监测管理平台,实现医院能源可视化管理

医院是大型公共建筑的一种&#xff0c;随着医院规模的不断扩大&#xff0c;医院能源消耗剧增&#xff0c;能源消耗居高不下。医院对于能源监管的需求也越来越高。医院建立一套能耗监测管理平台&#xff0c;对于降低医院能耗有着非常重要的作用。 医院能耗存在的问题 1、医院能…

科研学习|科研软件——有序多分类Logistic回归的SPSS教程!

一、问题与数据 研究者想调查人们对“本国税收过高”的赞同程度&#xff1a;Strongly Disagree——非常不同意&#xff0c;用“0”表示&#xff1b;Disagree——不同意&#xff0c;用“1”表示&#xff1b;Agree--同意&#xff0c;用“2”表示&#xff1b;Strongly Agree--非常…

快速掌握华为VRP系统的CLI管理技巧,让你轻松玩转命令行!

华为VRP基础 基本概述 VRP(通用路由平台) 系统软件&#xff1a;.cc 配置文件&#xff1a;.cfg,.zip,.dat 补丁文件&#xff1a;.pat paf文件&#xff1a;.bin 设备初始化&#xff1a; 设备管理方式&#xff1a; WEB网管&#xff1a;配置与设备同网段IP地址&#xff0c;使用浏览…