诡异的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 。将选中…

【已解决】ModuleNotFoundError: No module named ‘timm‘

问题描述 Traceback (most recent call last): File "main.py", line 48, in <module> import utils File "/media/visionx/monica/project/stable_signature/hidden/utils.py", line 26, in <module> import timm ModuleNotFound…

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…

React Native 源码分析(四)—— TurboModules JSI通信机制

本文会详细分析React Native 基于JSI的通信方式,除不会涉及Hemers引擎部分,其余代码都会详细分析,但比较简单的,不会很啰嗦,可以说是网上最完整详细的分析文章,代码通过断点截图,可以更方便查看运行的过程 1、React Native 源码分析(一)—— 启动流程 2、React Nativ…

Promise原理、以及Promise.race、Promise.all、Promise.resolve、Promise.reject实现;

为了向那道光亮奔过去&#xff0c;他敢往深渊里跳&#xff1b; 于是今天朝着Promise的实现前进吧&#xff0c;写了四个小时&#xff0c;终于完结撒花&#xff1b; 我知道大家没有耐心&#xff0c;当然我也坐的腰疼&#xff0c;直接上代码&#xff0c;跟着我的注释一行行看过去…

python语法之数据类型

在python编程中&#xff0c;数据类型是一个重要的概念。 变量可以存储不同类型的数据&#xff0c;不同的类型可以做不同的事情。 Python在这些类别中默认内置了以下数据类型: 文本类型&#xff1a;str数值类型&#xff1a;int, float, complex序列类型&#xff1a;list, tup…

MySQL MVCC机制详解

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

立哥先进研发-API安全方案

项目背景&#xff1a;随着技术进步&#xff0c;很多优秀技术也被用在黑灰产之中&#xff0c;例如爬虫系统在票务系统中的滥用&#xff0c;尤其机票系统。机票爬虫们威力之大&#xff0c;让人叹为观止&#xff1a;多数订票网站&#xff0c;真实用户产生的不到10%&#xff0c;其浏…

【大语言模型】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…

Visual Studio Code 配置 C/C++ 开发环境的最佳实践(VSCode + Clangd + CMake)

Visual Studio Code 配置 C/C 开发环境的最佳实践(VSCode Clangd CMake) 知乎原文参考地址1 与 VSCode 官方文档配置 相比拥有的优势 Clangd 具有更优秀的性能&#xff0c;微软官方 Cpptools 的代码提示功能有明显延迟Clangd 提供更精准的「转到定义」、「重命名符号」、「…

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

开闭原则 其实就是对修改关闭&#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…

import gradio时出现SyntaxError: future feature annotations is not defined解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

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

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