#Linux动态大小裁剪以及包大小变大排查思路

news2025/4/19 15:58:08

1 动态库裁剪

  库分为动态库和静态库,动态库是在程序运行时才加载,静态库是在编译时就加载到程序中。动态库的大小通常比静态库小,因为动态库只包含了程序需要的函数和数据,而静态库则包含了所有的函数和数据。静态库可以理解为引入源码编译,链接器在链接过程中会自动分析需要可不需要的代码进行删除裁剪。因此静态库不存在包大小问题(除了特定平台生成静态库过大导致无法生成库文件的问题)。

  动态库裁剪的思路很简单:

  1. 通过工具或者编译选项删除不必要的数据和代码;
  2. 只导出需要的函数和数据;
  3. 关闭不必要的语言特性,如C++的异常处理等;
  4. 优化代码,比如能用constexpr实现的尽量用constexpr实现;

1.1 代码层面

  首先代码层面,需要尽可能确保不同模块之间的耦合度低,避免出现循环依赖的情况。其次,需要尽可能减少代码的重复,避免出现冗余代码的情况。最后,需要尽可能减少代码的复杂度,避免出现复杂的算法和数据结构的情况。对于一些能够用constexpr实现的功能,尽量用constexpr实现,这样可以减少动态库的大小。

  C++中容易导致C++膨胀的代码:

  1. 模板函数和模板类。模板函数和模板类在实例化时都会有一个对应版本的实例,如果任何函数都通过编译器的默认推导来实例化很容易导致膨胀。因此模板函数和模板类应该尽量避免使用默认推导,尽可能显示推导能减少实例化版本。因此可以使用类型擦除和显示实例化来解决模板膨胀的问题。
  2. 内联函数。内联函数在编译时会被展开,因此内联函数的代码会被复制到调用处,这样会导致代码膨胀。因此内联函数应该尽量避免使用,除非函数的代码量很小。但是这一条对于现代C++ inline的含义已经发生了变化,inline优化基本完全由C++编译器自动优化。
  3. 宏。宏在编译时会被替换,因此宏的代码会被复制到调用处,这样会导致代码膨胀。因此宏应该尽量避免使用,除非宏的代码量很小。
  4. 异常处理。异常处理会导致代码膨胀,因为异常处理需要在运行时进行,因此异常处理会导致代码膨胀。因此异常处理应该尽量避免使用,除非异常处理的代码量很小。异常处理通常需要存储异常栈回溯相关的信息,因此容易导致代码膨胀。
  5. RTTI。RTTI 允许在运行时获取对象的类型信息。 RTTI 需要在代码中插入额外的类型信息,这会增加二进制文件的大小。
  6. 虚函数表。虚函数表是一个指针数组,它包含了虚函数的地址。虚函数表需要在运行时进行查找,这会增加二进制文件的大小。但是一般情况下,虚函数表的大小是固定的,因此虚函数表的大小并不是二进制膨胀的主要原因。

1.2 编译选项

  通过编译选项可以控制编译器的行为,从而控制编译过程中的优化和裁剪。编译选项通常是通过编译器的命令行参数来设置的。常用的降低二进制大小的编译选项有:

  1. 优化等级,在编译动态库时,使用 -O2 或 -O3 优化级别。 这些优化级别可以使编译器生成更紧凑的代码,从而减小动态库的大小。或者使用-Os之类平衡性能和大小的选项。
  2. 代码裁剪。
    1. -function-sections:将每个函数放入单独的代码段。
    2. -gc-sections:在链接时删除未使用的代码段。
    3. -Wl,--gc-sections:在链接时删除未使用的代码段。
  3. LTO。使用链接时优化(Link-Time Optimization, LTO)可以进一步减小动态库的大小。 LTO 允许编译器在链接时进行全局优化,从而消除冗余代码和数据。
    1. -flto:启用 LTO 优化。
    2. -fwhole-program:启用 LTO 优化。

1.3 导出符号

  导出符号是指动态库中可以被其他模块(例如可执行文件或其他动态库)访问的函数和变量。 换句话说,它们是库的公共接口。默认情况下,在 Linux 系统中,使用 GCC 或 Clang 编译动态库时,所有非 static 的函数和全局变量都会被导出。 这通常会导致导出过多的符号,增加库的大小。导出符号越多,库的大小越大。 通过只导出必要的符号,可以显著减小库的大小。

  控制导出符号不同编译器提供的方式不同,但是一般来说,有以下几种方式:

  1. 通过导出文件指定导出的符号列表;
  2. 代码中通过标记来标记需要导出的函数。
#ifndef MY_LIBRARY_EXPORT_H
#define MY_LIBRARY_EXPORT_H

#ifdef _WIN32
  #ifdef MY_LIBRARY_BUILD
    #define MY_EXPORT __declspec(dllexport)
  #else
    #define MY_EXPORT __declspec(dllimport)
  #endif
#elif defined(__GNUC__)
  #define MY_EXPORT __attribute__((visibility("default")))
#else
  #define MY_EXPORT
#endif

#endif // MY_LIBRARY_EXPORT_H

1.4 strip

  通常情况下,二进制产物会包含一些调试信息,比如符号表、调试符号等。这些信息对于调试和分析二进制文件非常有用,但是它们通常不会被用于发布版本。因此,在发布版本中,通常会使用strip工具来去除这些调试信息,从而减小二进制文件的大小。

  • 不可逆操作: strip命令会直接修改文件,并且无法恢复。 因此,在运行 strip命令之前,请务必备份文件。
  • 影响调试: 移除符号表和调试信息会使调试变得更加困难。 如果需要调试程序,请不要运行 strip命令。
  • 发布版本: strip命令通常用于发布最终版本的程序,以减小文件大小并提高安全性。
  • 调试信息分离: 可以使用 --only-keep-debug--add-gnu-debuglink选项将调试信息分离到单独的文件中。 这样可以在不影响程序运行的情况下进行调试。

2 实验

2.1 测试代码和环境

  我们的测试环境是:

Linux DESKTOP-JLHBOB4 4.4.0-19041-Microsoft #4355-Microsoft Thu Apr 12 17:37:00 PST 2024 x86_64 x86_64 x86_64 GNU/Linux
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

  测试代码如下,分别是一个头文件和一个源文件编译成so库:

// my_lib.h
#ifndef MY_LARGE_LIBRARY_H
#define MY_LARGE_LIBRARY_H

#include <iostream>
#include <vector>

// 用于控制导出符号,可以参考之前的通用 EXPORT 宏
#ifdef _WIN32
  #ifdef MY_LARGE_LIBRARY_BUILD
    #define MY_LARGE_LIBRARY_API __declspec(dllexport)
  #else
    #define MY_LARGE_LIBRARY_API __declspec(dllimport)
  #endif
#elif defined(__GNUC__)
  #define MY_LARGE_LIBRARY_API __attribute__((visibility("default")))
#else
  #define MY_LARGE_LIBRARY_API
#endif


// 模板类
template <typename T>
class MY_LARGE_LIBRARY_API MyTemplateClass {
public:
    MyTemplateClass(T value);
    T getValue() const;
private:
    T m_value;
};

// 内联函数
inline int MY_LARGE_LIBRARY_API inlineFunction(int x) {
    return x * x * x; // 复杂的计算,增加内联的代价
}

// 虚基类
class MY_LARGE_LIBRARY_API BaseClass {
public:
    BaseClass(int id);
    virtual ~BaseClass();
    virtual int calculate() const;
    int getId() const;
protected:
    int m_id;
};

// 派生类
class MY_LARGE_LIBRARY_API DerivedClass : public BaseClass {
public:
    DerivedClass(int id, double factor);
    ~DerivedClass() override;
    int calculate() const override;
private:
    double m_factor;
};

// 一个导出函数,使用了上述的类和函数
MY_LARGE_LIBRARY_API int processData(const std::vector<int>& data);

#endif // MY_LARGE_LIBRARY_H
// my_lib.cpp
#include "Mylib.hpp"
#include <numeric> // std::accumulate

// 模板类的实现
template <typename T>
MyTemplateClass<T>::MyTemplateClass(T value) : m_value(value) {}

template <typename T>
T MyTemplateClass<T>::getValue() const {
    return m_value;
}

// 显式实例化一些常用的模板类型,减少编译单元间的重复实例化
template class MY_LARGE_LIBRARY_API MyTemplateClass<int>;
template class MY_LARGE_LIBRARY_API MyTemplateClass<double>;

// 基类的实现
BaseClass::BaseClass(int id) : m_id(id) {}

BaseClass::~BaseClass() {}

int BaseClass::calculate() const {
    return m_id * 2;
}

int BaseClass::getId() const {
    return m_id;
}

// 派生类的实现
DerivedClass::DerivedClass(int id, double factor) : BaseClass(id), m_factor(factor) {}

DerivedClass::~DerivedClass() {}

int DerivedClass::calculate() const {
    return static_cast<int>(m_id * m_factor * 3);
}

// processData 函数的实现
int processData(const std::vector<int>& data) {
    int sum = std::accumulate(data.begin(), data.end(), 0);
    int inlinedResult = inlineFunction(sum);
    MyTemplateClass<int> templateObject(inlinedResult);
    BaseClass* baseObject = new DerivedClass(sum, 2.5);
    int finalResult = templateObject.getValue() + baseObject->calculate();
    delete baseObject;
    return finalResult;
}

2.1.2 不同操作对二进制大小的影响

默认-O1-O2-O3-Os符号sectionltowholertti异常debugstrip包大小(Byte)
57400
53752
53560
54784
53464
53480
53936
23120
10408
10016
10016
9640
6008

  下面是不同配置的详细说明:

  • 默认配置:使用默认的编译选项和编译方式,不进行任何裁剪和优化。
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylib.so
  • 使用不同优化选项对比,具体-O0-O1-O2-O3
  • 隐藏符号:使用-fvisibility=hidden选项隐藏所有符号。
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_hidden.so -fvisibility=hidden -Os
  • 独立section裁剪:使用-ffunction-sections-fdata-sections选项将每个函数和数据放入单独的代码段和数据段。
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_sections.so -ffunction-sections -fdata-sections -Os
  • lto
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_sections_lto.so -ffunction-sections -fdata-sections -Os -Wl,--gc-sections -flto
  • 更激进的优化:-fwhole-program
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_sections_lto_whole.so -ffunction-sections -fdata-sections -Os -Wl,--gc-sections -flto -fwhole-program
  • 禁用RTTI:-fno-rtti
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_sections_lto_whole_nortti.so -ffunction-sections -fdata-sections -Os -Wl,--gc-sections -flto -fwhole-program -fno-rtti
  • 禁用异常-fno-exceptions
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_sections_lto_whole_nortti_noex.so -ffunction-sections -fdata-sections -Os -Wl,--gc-sections -flto -fwhole-program -fno-rtti -fno-exceptions
  • 分离调试信息:-gsplit-dwarf
    • g++ -fPIC -shared Mylib.cpp -g -DMY_LARGE_LIBRARY_BUILD -o mylibos_sections_lto_whole_nortti_noex_debuginfo.so -ffunction-sections -fdata-sections -Os -Wl,--gc-sections -flto -fwhole-program -fno-rtti -fno-exceptions -gsplit-dwarf
  • 删除无用的信息:strip
    • strip -g -x -s mylib.so

  从上面的结果来看我们上面大部分操作都可以减少二进制,而且效果明显,我们的库从最开始的57400Byte减少到了6008Byte。能够看到成效是非常明显的。但是本来预期能够降低包大小的操作没有降低包大小的同时,反而增加了包大小这是为什么。

  实际工程中往往限制导出符号比较能够降低包大小,上面的实验没有降低包大小的原因是因为我们的测试代码非常简单函数太少,因此包大小的优化效果不是很明显。以及一些其他参数没有降低包大小的原因也是因为我们的测试代码比较简单。

2.1 包大小排查思路

  下面我们就简单排查下。
  根据上面的数据我们能够看到有两个选项导致了包大小变大,分别是-O3gc-sections,前者是因为该选项更倾向于优化性能而牺牲存储空间,因此已经有明确的结论不需要我们去排查。但是我们期望gc-sections等选项带来的是包大小优化,但是事实却不是如此。
  首先,对于一个二进制动态库,其有不同的section组成,为了确认包大小变大的原因我们首先要做的是确认是哪个section变大了。因此我们使用objdump -h工具拆分二进制包来确认哪个部分增大了。下面是拆分得到的结果:

27 .debug_aranges     00000080 0000000000000000 DEBUG
30 .debug_line        000005f1 0000000000000000 DEBUG
31 .debug_str         00003bbe 0000000000000000 DEBUG
33 .debug_ranges      00000180 0000000000000000 DEBUG

27 .debug_aranges     00000110 0000000000000000 DEBUG
30 .debug_line        0000055f 0000000000000000 DEBUG
31 .debug_str         00003bae 0000000000000000 DEBUG
33 .debug_ranges      000000f0 0000000000000000 DEBUG

  从上面的拆包能够看到增加的主要是调试信息。而这部分调试信息在后续的strip中已经被删除了,因此影响我们最终产物大小的额外因素已经被排除了。如果希望知道具体增大了什么可以通过相关的提取对应section的信息来确认哪一部分增大了。

  上面的排查路径其实不是很典型,因为一般情况下包大小都是因为代码引起的

  下面简单描述下如何排查包大小问题:

  1. 首先,对比的产物一定是相同编译参数下的最终产物,使用两个带调试信息的不同编译参数的包对比没有意义(因此排查的前提是代码相同编译参数不同或者编译参数相同代码更改);
  2. 准备好后,使用objdump -h分析不同section的大小,来确认方向:
    • 不同section对应不同的数据,一般情况下比较容易出现增大的是data和text段
    • .text: 代码段,包含可执行指令。 如果包大小增加主要是 .text section 变大,则需要关注代码优化。
    • .rodata: 只读数据段,包含字符串常量、只读变量等。 大量的字符串常量或嵌入式资源会增加此 section 的大小。
    • .data: 已初始化数据段,包含已初始化的全局变量和静态变量。 大的静态数组或全局变量会增加此 section 的大小。
  3. 明确具体包大小变化比较大的section后,可以尝试对比代码变动来初步确定变大的根本原因,如果无法确定则继续;
  4. 使用命令nm -CS <your_binary> | sort -rnk1 对代码段和数据段进行排序,然后对比不同版本之间的差异。
  5. 找到差异的具体部分之后再使用objdump -d反汇编并对比源码来确认最终原因。

emsp; 需要注意的是,有些博客会推荐使用bloaty,个人建议如果能够通过该工具排查发现数据异常,推荐直接使用linux native的工具链。(在实际项目中发现bloaty似乎统计的不是很准确。)

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

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

相关文章

基于微信小程序的中医小妙招系统的设计与实现

hello hello~ &#xff0c;这里是 code袁~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的在校大学生…

css button 点击效果

<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><title>button点击效果</title><style>#container {display: flex;align-items: center;justify-content: center;}.pushable {position: relat…

Foundation Agent:深度赋能AI4DATA

2025年5月17日&#xff0c;第76期DataFunSummit&#xff1a;AI Agent技术与应用峰会将在DataFun线上社区举办。Manus的爆火并非偶然&#xff0c;随着基础模型效果不断的提升&#xff0c;Agent作为大模型的超级应用备受全世界的关注。为了推动其技术和应用&#xff0c;本次峰会计…

Docker--Docker镜像原理

docker 是操作系统层的虚拟化&#xff0c;所以 docker 镜像的本质是在模拟操作系统。 联合文件系统&#xff08;UnionFS&#xff09; 联合文件系统&#xff08;UnionFS&#xff09; 是Docker镜像实现分层存储的核心技术&#xff0c;它通过将多个只读层&#xff08;Image Laye…

SpringAI+DeepSeek大模型应用开发——2 大模型应用开发架构

目录 2.大模型开发 2.1 模型部署 2.1.1 云服务-开放大模型API 2.1.2 本地部署 搜索模型 运行大模型 2.2 调用大模型 接口说明 提示词角色 ​编辑 会话记忆问题 2.3 大模型应用开发架构 2.3.1 技术架构 纯Prompt模式 FunctionCalling RAG检索增强 Fine-tuning …

2.2/Q2,Charls最新文章解读

文章题目&#xff1a;Association of uric acid to high-density lipoprotein cholesterol ratio with the presence or absence of hypertensive kidney function: results from the China Health and Retirement Longitudinal Study (CHARLS) DOI&#xff1a;10.1186/s12882-…

李飞飞团队新作WorldScore:“世界生成”能力迎来统一评测,3D/4D/视频模型同台PK

从古老神话中对世界起源的幻想&#xff0c;到如今科学家们在实验室里对虚拟世界的构建&#xff0c;人类探索世界生成奥秘的脚步从未停歇。如今&#xff0c;随着人工智能和计算机图形学的深度融合&#xff0c;我们已站在一个全新的起点&#xff0c;能够以前所未有的精度和效率去…

如何在米尔-STM32MP257开发板上部署环境监测系统

本文将介绍基于米尔电子MYD-LD25X开发板&#xff08;米尔基于STM35MP257开发板&#xff09;的环境监测系统方案测试。 摘自优秀创作者-lugl4313820 一、前言 环境监测是当前很多场景需要的项目&#xff0c;刚好我正在论坛参与的一个项目&#xff1a;Thingy:91X 蜂窝物联网原型…

MySQL之SQL优化

目录 1.插入数据 2.大批量插入数据 3.order by优化 4.group by优化 5.limit优化 6.count优化 count用法 7.update优化 1.插入数据 如果我们需要一次性往数据库表中插入多条记录&#xff0c;可以从以下三个方面进行优化 第一个:批量插入数据 Insert into tb_test va…

python_level1.2

目录 一、变量 例如&#xff1a;小正方形——>大正方形 【1】第一次使用这个变量&#xff0c;所以说&#xff1a;定义一个变量length&#xff1b; 【2】&#xff1a;是赋值符号&#xff0c;不是等于符号。&#xff08;只有赋值&#xff0c;该变量才会被创建&#xff09;…

Linux、Kylin OS挂载磁盘,开机自动加载

0.实验环境&#xff1a; 1.确定挂载目录&#xff0c;如果没有使用mkdir 进行创建&#xff1a; mkdir /data 2.查看磁盘 lsblk #列出所有可用的块设备df -T #查看磁盘文件系统类型 3.格式化成xfs文件系统 (这里以xfs为例&#xff0c;ext4类似) mkfs.xfs /dev/vdb 4.挂载到…

FPGA-VGA

目录 前言 一、VGA是什么&#xff1f; 二、物理接口 三、VGA显示原理 四、VGA时序标准 五、VGA显示参数 六、模块设计 七、波形图设计 八、彩条波形数据 前言 VGA的FPGA驱动 一、VGA是什么&#xff1f; VGA&#xff08;Video Graphics Array&#xff09;是IBM于1987年推出的…

【嵌入式】【阿里云服务器】【树莓派】学习守护进程编程、gdb调试原理和内网穿透信息

目录 一. 守护进程的含义及编程实现的主要过程 1.1守护进程 1.2编程实现的主要过程 二、在树莓派中通过三种方式创建守护进程 2.1nohup命令创建 2.2fork()函数创建 2.3daemon()函数创建 三、在阿里云中通过三种方式创建守护进程 3.1nohup命令创建 3.2fork()函数创建 …

前沿篇|CAN XL 与 TSN 深度解读

引言 1. CAN XL 标准演进与设计目标 2. CAN XL 物理层与帧格式详解 3. 时间敏感网络 (TSN) 关键技术解析 4. CAN XL + TSN 在自动驾驶领域的典型应用

AI大模型科普:从零开始理解AI的“超级大脑“,以及如何用好提示词?

大家好&#xff0c;小机又来分享AI了。 今天分享一些新奇的东西&#xff0c; 你有没有试过和ChatGPT聊天时&#xff0c;心里偷偷犯嘀咕&#xff1a;"这AI怎么跟真人一样对答如流&#xff1f;它真的会思考吗&#xff1f;" 或者刷到技术文章里满屏的"Token"…

STM32单片机入门学习——第40节: [11-5] 硬件SPI读写W25Q64

写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难&#xff0c;但我还是想去做&#xff01; 本文写于&#xff1a;2025.04.18 STM32开发板学习——第一节&#xff1a; [1-1]课程简介第40节: [11-5] 硬件SPI读…

如何将 .txt 文件转换成 .md 文件

一、因为有些软件上传文件的时候需要 .md 文件&#xff0c;首先在文件所在的目录中&#xff0c;点击“查看”&#xff0c;然后勾选上“文件扩展名”&#xff0c;这个时候该目录下的所有文件都会显示其文件类型了。 二、这时直接对目标的 .txt 文件进行重命名&#xff0c;把后缀…

Qt 创建QWidget的界面库(DLL)

【1】新建一个qt库项目 【2】在项目目录图标上右击&#xff0c;选择Add New... 【3】选择模版&#xff1a;Qt->Qt设计师界面类&#xff0c;选择Widget&#xff0c;填写界面类的名称、.h .cpp .ui名称 【4】创建C调用接口&#xff08;默认是创建C调用接口&#xff09; #ifnd…

Spring 数据库编程

Spring JDBC 传统的JDBC在操作数据库时&#xff0c;需要先打开数据库连接&#xff0c;执行SQL语句&#xff0c;然后封装结果&#xff0c;最后关闭数据库连接等资源。频繁的数据库操作会产生大量的重复代码&#xff0c;造成代码冗余&#xff0c;Spring的JDBC模块负责数据库资源…

进阶篇|CAN FD 与性能优化

引言 1. CAN vs. CAN FD 对比 2. CAN FD 帧结构详解