诡异的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.rhkb.cn/news/195121.html

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

相关文章

智慧环保:科技驱动下的环境保护新篇章

智慧环保&#xff1a;科技驱动下的环境保护新篇章 环境保护已经成为当今社会的重要议题&#xff0c;而科技的飞速发展为我们开启了智慧环保的新篇章。在这篇文章中&#xff0c;我们将介绍智慧环保所带来的机会和创新&#xff0c;以及科技在环境保护中的重要作用。 智慧环保的理…

Oracle OCM考试(史上最详细的介绍,需要19c OCP的证书)

Oracle 19c OCM考试和之前版本的OCM考试差不多&#xff0c;对于考生来说最大的难点是题量大&#xff0c;每场3小时&#xff0c;一共4场&#xff0c;敲键盘敲得手抽筋。姚远老师&#xff08;v:dataace&#xff09;的很多Oracle OCP学员都对19c OCM考试很有兴趣&#xff0c;这里给…

预告|万博智云CTO孙琦将出席2023 OpenInfra Days China,并发表最新容灾方案,欢迎报名参会!

2023年12月1日&#xff0c;北京海航万豪酒店将迎来一场令人瞩目的盛会——OpenInfra Days China 2023。 大会主题 本次活动是时隔两年来的首次线下盛会&#xff0c;聚集了全球社区的技术专家、行业领袖以及国内开源基础设施技术企业的代表&#xff0c;旨在交流研究OpenInfra基…

【容器化】Kubernetes(k8s)

文章目录 概述Docker 的管理痛点什么是 K8s云架构 & 云原生 架构核心组件K8s 的服务注册与发现组件调用流程部署单机版部署主从版本Operator来源拓展阅读 概述 Docker 虽好用&#xff0c;但面对强大的集群&#xff0c;成千上万的容器&#xff0c;突然感觉不香了。 这时候就…

轻松玩转华为MateX5分屏功能,乐趣层出不穷!

✅小窗交互&#xff0c;沉浸体验不打断&#xff1b; ✅分区截屏&#xff0c;花式截图&#xff0c;一招搞定&#xff1b; ✅跨屏拖拽&#xff0c;随心分享易如反掌&#xff1b; ✅悬停视频会议&#xff0c;沟通效率大不同。

对完成初始配对的std::vector<pcl::PointCloud<pcl::PointXYZRGB>::Ptr> 进行可视化

1. 数据填充 std::vector<pcl::PointCloud<pcl::PointXYZRGB>::Ptr> pure_static_landmarks_underk;std::vector<pcl::PointCloud<pcl::PointXYZRGB>::Ptr> pure_static_landmarks_k;vector<vector<int>> matched_indices;对数据pure_sta…

抠某区域地图方法

1.打开阿里云数据可视化平台DataV.GeoAtlas地理小工具系列 2. 选择要抠出来的区域&#xff0c;右侧选择要下载的json文件&#xff0c;如红框所示 3. 打开下载的文件&#xff0c;内容全部复制。 4. 打开百度地图示例Examples - Apache ECharts 5. 如下图所示&#xff0c;将下…

C生万物 | 从浅入深理解指针【最后部分】

C生万物 | 从浅入深理解指针【最后部分】 文章目录 C生万物 | 从浅入深理解指针【最后部分】前言sizeof和strlen的对比sizeofstrlen 数组和指针笔试题解析一维数组字符数组二维数组 前言 我们前面学了四个部分了&#xff0c;如果没有看前面的建议可以看一下前面的~~ C生万物 |…

使用uniapp写小程序,真机调试的时候不显示log

项目场景&#xff1a; 当小程序文件太大的情况下使用真机调试&#xff0c;但是真机调试的调试器没有任何反应 问题描述 使用uniapp写小程序&#xff0c;真机调试的时候不显示log 原因分析&#xff1a; 提示&#xff1a;因为真机调试的时候没有压缩文件&#xff0c;所以调试的…

开发知识点-前端-webpack

webpack技术笔记 一、 介绍二、 下载使用 一、 介绍 Webpack是一个现代 JavaScript 应用程序的静态模块打包器 打包&#xff1a;可以把js、css等资源按模块的方式进行处理然后再统一打包输出 静态&#xff1a;最终产出的静态资源都可以直接部署到静态资源服务器上进行使用 模…

Axure RP 9下载教程,轻松入手!

Axurerp9是产品经理必备的专业快速原型设计工具。Axurerp9可以快速高效地创建产品原型图&#xff0c;绘制APP和网页的原型图、框架图、结构图等。但Axurerp9下载在用户体验上的缺陷也很明显&#xff0c;其设置交互方式相对繁琐&#xff0c;可视化不足、条件判断、变量、中继器等…

K8S篇之实现利用Prometheus监控pod的实时数据指标

一、监控部署 1、将k8s集群中kube-state-metrics指标进行收集&#xff0c;服务进行部署 1.1 pod性能指标&#xff08;k8s集群组件自动集成&#xff09; k8s组件本身提供组件自身运行的监控指标以及容器相关的监控指标。通过cAdvisor 是一个开源的分析容器资源使用率和性能特性的…

Docker安装MinIO遇到的问题汇总——持续更新中

文章目录 Docker安装MinIO遇到的坑前言问题1&#xff1a;执行docker run报错Error response from daemon问题2&#xff1a;启动MinIO容器浏览器无法访问问题3&#xff1a;上传文件报错InvalidResponseException问题4&#xff1a;上传文件报错Connection refused最终的启动指令问…

Linux(1):开始

计算机组成概述 计算机&#xff1a;接受用户输入指令与数据&#xff0c;经由中央处理器的数学与逻辑单元处理后&#xff0c;以产生或存储有用的信息。 主要可以分为3个部分&#xff1a;输入单元、主机单元、输出单元。 中央处理器&#xff08;Central Processing Unit, CPU&a…

MyCat基础入门

1. MyCat安装 去官网下载安装包&#xff1a; 下载路径&#xff1a; 官方网站&#xff1a;http://www.mycat.org.cn/ github地址https://github.com/MyCATApache 上传到服务器上并解压&#xff1a; 它解压后是一个叫mycat的文件夹 去maycat的bin目录下&#xff0c;执行命令 ./my…

vscode删除后重装还有原来的配置问题,彻底删除vscode,删除vscode安装过的插件和缓存

VSCode卸载后进行重新安装&#xff0c;发现新安装的还有原来的一些配置&#xff0c;卸载的不彻底&#xff0c;有时候也容易出问题&#xff0c;可按照如下方法卸载干净&#xff1a; 1.进入控制面板卸载VSCode&#xff0c;也可以在VSCode的安装目录下用程序自带的卸载程序 2.这…

摄影店信息展示服务预约小程序的效果如何

无论视频还是图片在生活中的用途都非常广&#xff0c;市场中的摄影店也非常多&#xff0c;比如婚纱照、产品照、食物照、服装照等&#xff0c;除了婚纱照是刚需外&#xff0c;如今随着线上宣传渠道成为主流&#xff0c;无论企业还是自媒体个人都有一定的摄影需求&#xff0c;因…

微信小程序 限制字数文本域框组件封装

微信小程序 限制字数文本域框 介绍&#xff1a;展示类组件 导入 在app.json或index.json中引入组件 "usingComponents": {"text-field":"/pages/components/text-field/index"}代码使用 <text-field maxlength"500" bindtabsIt…

概率论和数理统计(三)数理统计基本概念

前言 “概率论”是给定一个随机变量X的分布F(x),然后求某事件A概率 P ( x ∈ A ) P(x \in A) P(x∈A)或者随机变量X的数字特征.“统计”是已知一组样本数据 { x 1 , x 2 , . . . x n } \{x_1,x_2,...x_n\} {x1​,x2​,...xn​},去求分布F(x) 统计的基本概念 在统计中&#x…

【无标题】chapter6卷积

此例以说明全连接层处理图片的时候会遇到参数过多 模型过大的问题 参数比要研究的物体总数还多 卷积&#xff0c;特殊的全联接层 平移不变形&#xff0c;局部性 原本权重为二维&#xff08;输入和输出全联接&#xff0c;想想下表组合&#xff0c;就是个二维的矩阵&#xff09;…