惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
GRAHAM CLULEY
T
Tenable Blog
Know Your Adversary
Know Your Adversary
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy International News Feed
S
Security Affairs
NISL@THU
NISL@THU
O
OpenAI News
Attack and Defense Labs
Attack and Defense Labs
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hacker News: Ask HN
Hacker News: Ask HN
Webroot Blog
Webroot Blog
Schneier on Security
Schneier on Security
S
SegmentFault 最新的问题
S
Schneier on Security
G
Google Developers Blog
V
V2EX
C
Check Point Blog
U
Unit 42
Google DeepMind News
Google DeepMind News
T
Threatpost
阮一峰的网络日志
阮一峰的网络日志
T
The Exploit Database - CXSecurity.com
Recent Announcements
Recent Announcements
M
MIT News - Artificial intelligence
S
Secure Thoughts
博客园 - 司徒正美
Recorded Future
Recorded Future
P
Proofpoint News Feed
Spread Privacy
Spread Privacy
K
Kaspersky official blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
AI
AI
博客园 - 聂微东
N
News and Events Feed by Topic
SecWiki News
SecWiki News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Vulnerabilities – Threatpost
P
Palo Alto Networks Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Engineering at Meta
Engineering at Meta
Recent Commits to openclaw:main
Recent Commits to openclaw:main
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
Project Zero
Project Zero
W
WeLiveSecurity
博客园 - Franky

I'm OWenT

国产大模型(GLM 5.1、Kimi K2.6)真实场景效果和 Coding Plan 额度测试 新版本libatapp的连接管理——从etcd服务发现到拓扑驱动的自动重连 新版本libatbus的设计变更——从树形路由到拓扑驱动 Protobuf又一坑 - C++标准和ABI兼容性 AI真好用-给Blog主题统一加mermaid,chart.js,excalidraw,draw.io的多种引入方式支持 给内网部署Squid-通用HTTP下载缓存 UE使用CodeChecker和clang-tidy生成静态分析报告 找出UE的循环依赖 C++小协程栈和临时变量及作用域的栈溢出问题分析 游戏服务的可观测性能力建设(C++生态) 指标上报的多线程优化和多拉取源点优化 协程(libcopp)的Channel功能和CPU命中率优化 通用RPC代码生成器 实现strong_rc_ptr(比shared_ptr更快的引用计数智能指针) 手夯一个STL allocator和对象内存分析组件 std::condition_variable 的信号丢失问题 踩坑一处(GCC)STL std::async 实现BUG导致的crash问题 GCC 14的一个warning to error BUG 给xresloader(Excel导表工具)增强UE读表支持(包含蓝图,Blueprint) Excel转表工具(xresloader)的新验证器(验证外部Excel和文本数据,唯一性和自定义规则) protobuf v22和gRPC v1.55版本升级的依赖变化和upb适配 关于protobuf近期版本(v20/v3.20+)和 gRPC v1.54版本在某些编译环境下的一些链接和编译问题 xresloader-Excel导表工具链的近期变更汇总 打通游戏服务端框架的C++20协程改造的最后一环 Opentelemetry-cpp的Logs模块标准更新(涉及近期版本:1.8-1.9的BREAK CHANGES) 给cmake-toolset和工具链(curl等)加HTTP/2和HTTP/3支持 又开新坑之 coredns 插件: nftables和filter 关于opentelemetry-cpp社区对于C++ Head Only组件单例和符号可见性的讨论小记 填个转表工具 xresloader 去年的坑(数组尾部裁剪) 集成 upb 和 lua binding 的踩坑小记 libcopp对C++20协程的接入和接口设计 再度优化GCC、LLVM、Clang、libc++、libc++abi等套件的构建脚本 游戏服务的分布式事务优化(二)- 事务管理 游戏服务的分布式事务优化(一)- Write Ahead Log(WAL) 模块 记录一些bazel适配用编译选项 测试现代化硬件C++浮点数性能和一致性 适配Boringssl和OpenSSL 3.0 近期cmake-toolset的一些适配问题 C++20 Text Formatting/fmtlib 适配问题小记 再次重构LLVM+Clang+libcxx+libc++abi+其他相关工具的构建流程 重构基于CMake的构建工具链 新版GCC和LLVM+Clang终于Release啦 折腾一下nftables下的双拨 [C++20] Module partitions和符号交叉引用(声明和实现分离) [Rust] 实现一个线程安全且迭代器可以保存的链表 基于protobuf的代码生成 几个使用protobuf中C++接口的Arena的坑 Amazon Aurora DB存储引擎论文阅读小记 近期对libatapp的一些优化调整(增加服务发现和连接管理,支持yaml等) xresloader转表工具链增加了一些新功能(map,oneof支持,输出矩阵,基于模板引擎的加载代码生成等) 在游戏服务器中使用分布式事务 libcopp接入C++20 Coroutine和一些过渡期的设计 libatbus 的大幅优化 nftables初体验 容器配置开发环境小计 PALM Tree - 适合多核并发架构的B+树 - 论文阅读小记 跨平台协程库 - libcopp 简介 C++20 Coroutine 性能测试 (附带和libcopp/libco/libgo/goroutine/linux ucontext对比) 尝鲜Github Action 一些xresloader(转表工具)的改进 protobuf、flatbuffer、msgpack 针对小数据包的简单对比 协程框架(libcopp) 小幅优化 Excel转表工具(xresloader) 增加protobuf插件功能和集成 UnrealEngine 支持 Anna(支持任意扩展和超高性能的KV数据库系统)阅读笔记 C++20 Coroutine libcopp merge boost.context 1.69.0 Google去中心化分布式系统论文三件套(Percolator、Spanner、F1)读后感 Rust玩具-企业微信机器人通用服务 使用ELK辅助监控开发测试环境服务质量和问题定位 2018年的新通用伪随机数算法(xoshiro / xoroshiro)的C++(head only)实现 Webpack+vue+boostrap+ejs构建Web版GM工具 Rust的第二次接触-写个小服务器程序 理解和适配AEAD加密套件 atsf4g-co的进化:协程框架v2、对象路由系统和一些其他细节优化 协程框架(libcopp)v2优化、自适应栈池和同类库的Benchmark对比 可执行文件压缩 初识Rust 使用restructedtext编写xresloader文档 atframework的etcd模块化重构 C++的backtrace ECDH椭圆双曲线(比DH快10倍的密钥交换)算法简介和封装 protobuf-net的动态Message实现 pbc的proto3接入 atgateway内置协议流程优化-加密、算法协商和ECDH 整理一波软件源镜像同步工具+DevOps工具 Blog切换到Hugo libcopp v2的第一波优化完成 libcopp(v2) vs goroutine性能测试 libcopp的线程安全、栈池和merge boost.context 1.64.0 GCC 7和LLVM+Clang+libc++abi 4.0的构建脚本 libatbus的几个藏得很深的bug 用cmake交叉编译到iOS和Android 开源项目得一些小维护 atapp的c binding和c#适配 对象路由系统设计 2016年总结 近期的一个协程流程BUG 重写了llvm+clang+libc++和libc++abi的构建脚本 atsf4g完整游戏工程示例|I'm OWenT atframework基本框架已经完成|I'm OWenT
Opentelemetry社区在gRPC的几个链接问题(静态库和动态库混用,musl工具链,符号裁剪)
owent · 2023-10-29 · via I'm OWenT

blog-website

前言

opentelemetry-cpp 在标准上报协议OTLP里是支持使用 gRPC 作为传输协议的。但是,当 gRPC 被作为静态库同时链接进多个动态库时,在一些平台上会有一些问题。这是 gRPC 本身的一些实现方式导致的。 一直拖到今天才来比较完整得写这个问题的具体成因和解决方案,实际上也有一些其他库有相似实现的也会有相同的问题,所以分享出来看看有没有其他同学也可能碰到可以参考一下。

问题分析

首先是第一个静态库和动态库混用的问题,这个问题早在去年就有人提了issue(“example_otlp_grpc_log” sample not running. libopentelemetry_exporter_otlp_grpc_log.so and libopentelemetry_exporter_otlp_grpc.so library symbols are mixing/overlapping when used together. #1603)。这个问题的具体原因是因为 gRPC 初始化内部一些数据结构的时候使用了全局变量,并且通过再全部变量构造函数中完成一些全局只需要执行一次的注册类函数。这种情况如果我们把 gRPC 编译成静态库,并链接进多个动态库里,那么每个动态库里都有一份 gRPC 的全局变量和函数符号。在Windows中,由于每个dll有自己独立的符号表和堆管理,如果多个模块间没有互相访问,那么这种重复是没有关系的,因为每个模块访问的都是自己的那一份全局变量(当然如果这个全局变量想表达单例的话,那么他可能不是一个真正单例,具体可以参考我另一篇文章 《关于opentelemetry-cpp社区对于C++ Head Only组件单例和符号可见性的讨论小记》 )。而在ELF ABI(Linux)下,情况变得有点不一样了,因为在ELF ABI下整个堆和符号表是整个可执行程序共享的,ld.so 保证了当多个动态库包含相同的符号(通常是链接了相同的库)的时候,默认选中最早链接进的那一个。这种情况下不是说重复的符号不存在了,只是仅仅使用了其中一个,并且使用的是统一的一个。对于函数而言,我们认为所有同名符号的代码都是一样的(先不考虑多版本问题),所以选择任意一个都不影响结果。而对于全局变量,却是每个模块里面都存在单独的实体,每个都会去执行构造函数,并且由于默认选中的符号是相同的,所以导致执行构造函数的地址也是相同的,这就会导致问题。

我在 https://github.com/open-telemetry/opentelemetry-cpp/pull/1891 里也简单地重现了这个问题。比如有以下文件列表:

a.h

struct foo {
  int bar;
  foo();
  ~foo();

  void print(const char*);

  static foo _;
};

void print_static_global(const char*);

a.cpp

#include "a.h"

#include <iostream>
#include <thread>
#include <chrono>
#include <memory>

struct bar {
  foo* ptr = nullptr;
};

foo foo::_;
static bar s_;

foo::foo(): bar(127) {
    s_.ptr = this;
    std::cout<< "construct "<< this<< std::endl;
}

foo::~foo() {
    std::cout<< "destroy "<< this<< std::endl;
}

void foo::print(const char* prefix) {
    std::cout<< prefix<< "-foo: "<< this<< ": "<< bar<< std::endl;
}

void print_static_global(const char* prefix) {
    foo::_.print(prefix);
    std::cout<< prefix<< "-piblic API bar: "<< s_.ptr<< std::endl;
}

std::shared_ptr<std::thread> g_t(new std::thread([]() {
  std::this_thread::sleep_for(std::chrono::seconds{1});
  std::cout<< "internal API bar: "<< s_.ptr<< std::endl;
}), [](std::thread* thd) {
  thd->join();
  delete thd;
});

b.cpp

#include "a.h"

void dll_func_b() {
    print_static_global("b");
}

c.cpp

#include "a.h"

void dll_func_b();

int main() {
  print_static_global("c");
  dll_func_b();
  return 0;
}

我们执行以下编译命令,结果如下:

[owent@VM-144-59-centos test]$ clang++ a.cpp -o libtest_a.a -c -fPIC -pthread 
[owent@VM-144-59-centos test]$ clang++ b.cpp -o libtest_b.so -shared -fPIC -L$PWD -ltest_a -pthread
[owent@VM-144-59-centos test]$ clang++ c.cpp -fPIC -L$PWD -ltest_b -ltest_a '-Wl,-rpath=$ORIGIN' -pthread
[owent@VM-144-59-centos test]$ ./a.out 
[owent@VM-144-59-centos test]$ ./a.out 
construct 0x55af97755338
construct 0x55af97755338
c-foo: 0x55af97755338: 127
c-piblic API bar: 0x55af97755338
b-foo: 0x55af97755338: 127
b-piblic API bar: 0x55af97755338
internal API bar: (nil)
internal API bar: 0x55af97755338
destroy 0x55af97755338
destroy 0x55af97755338
[owent@VM-144-59-centos test]$

可以看出来,如果我们在这种情况下载构造和析构函数里有内存分配和释放会是非常危险的。在 gRPC 的场景里,由于某个内部数据接口的注册写在了全局变量的构造函数里,多次构造导致接口注册被后载入的模块覆盖了。而后续使用的时候用来比较的符号又是最早载入的接口,导致不匹配而不可用。

解决方案

这个问题如果是我们自己的代码的话解决方法很简单。就是不使用全局变量,可以使用静态函数返回static变量来代替。这么做有两个好处,第一是存在多份符号的时候因为总会调用第一次载入的符号接口,那么能保证访问到的总是同一份变量,并且还减少了不必要的构造和析构带来的内存或CPU开销。第二个好处是能控制这个变量的初始化时机。全局变量的初始化顺序是不定的,但是函数内static变量总是在第一次访问的时候初始化,这是有保障的。

然而我们没法大规模去修改 gRPC 的代码,所以我们采用另一种方式规避这个问题。新增一个编译目标 otlp_grpc_client ,并仅在这个编译目标里PRIVATE链接 gRPC。这样就能保证 gRPC 不被传递链接多次。这种方法无法解决其他库也链接 gRPC 而和 opentelemetry-cpp 冲突的问题,我们只是解决了当用户仅仅使用 opentelemetry-cpp 并编译成动态库,而 gRPC 使用静态库时的问题。本质上我们还是建议用户要么所有第三方库依赖都是用动态库,要么都使用静态库的。

这个问题的PR见: https://github.com/open-telemetry/opentelemetry-cpp/pull/1606

又引入了新问题

在修复了上面的问题以后,又引入了第二个问题。这和 gRPCgrpc::Status::OK 实现方式有关(最新版本又变更实现,可能这个问题被缓解了)。这个符号位于 gRPC 的库中,因为上层库没有直接使用这个符号(我们上面托管给了 otlp_grpc_client ),而对 grpc::Status::OK 的引用有出现在了 gRPC 的头文件中。这导致某些工具链下出现未定义的符号的链接错误。具体可参考 https://github.com/open-telemetry/opentelemetry-cpp/issues/1940https://github.com/open-telemetry/opentelemetry-cpp/issues/1998

我们的解决方法也很简单,把对 gRPC 的直接调用改为托管到 otlp_grpc_client 中,让对 grpc::Status::OK 的引用生成在 otlp_grpc_client 中。因为 otlp_grpc_client 会链接 gRPC ,所以不会出现符号找不到的问题。相关的变更可以参考 https://github.com/open-telemetry/opentelemetry-cpp/pull/2005

总结

上面的问题本质上还是动态库和静态库混合使用的问题。由于不同操作系统的ABI和行为不一样,导致很难有大一统的方法去解决这些问题。也属于C++的历史包袱和大家会觉得“难”的地方之一吧。 目前推荐的跨平台兼容性比较好的做法是对输出呃接口使用符号导出(Windows)或声明为可见(Linux/macOS等),然后把默认可见性改成 -fvisibility=hidden 。这样能尽可能保证平台一致性,减少不必要的符号导出以降低链接器负担。但是即便这样,对于head only的C++库而言,可能会导致可见性切换而导致一些其他告警(比如macOS上的STL)。一些现代化语言(比如 Rust)是在语言层面就用类似的方式去规避这个问题了,确实心智负担会小很多。

最后,欢迎有兴趣的小伙伴们互相交流。