yuenshome / yuenshome.github.io

https://yuenshome.github.io
MIT License
81 stars 15 forks source link

软件发布前的库优化与裁剪:初识 #62

Open ysh329 opened 5 years ago

ysh329 commented 5 years ago

在软件发布时,都会对其裁剪。在此过程中,会先查明编译的依赖关系,并将重型的依赖库进行替换或者移除。本文将会介绍在软件发布时,库裁剪命令strip的使用,目录结构如下:

  1. CMake GraphViz查明编译依赖
  2. 关掉CMake中的Debug选项
  3. 使用strip命令对动态库裁剪 2.1 strip基本介绍 2.1.1 符号表(Symbol table) 2.1.2 重定向(Relocation) 2.2 strip使用说明 2.3 strip裁剪静态库的问题 2.4 不同架构下的strip 2.5 gcc和CMake都有集成strip
  4. 发布准备:用zip进一步压缩打包

0. CMake GraphViz查明编译依赖

梳理依赖关系的方法,通常是在cmake命令中追加参数graphviz,如cmake .. --graphviz=../target_deps_graphviz,用来生成每个目标的依赖dot文件,再结合dot命令,如dot -Tpng -o target.png ./target.dot生成类似下面的PNG图或者PDF文件,以梳理依赖关系,为裁剪库做准备和参考。

image

最近因为框架编译出动态和静态库都很大,假设上图的可执行文件编译出来有200MB+,除了可以用更轻量级别的log替代上面的glog,或者不使用gflags外。有两个地方在压缩时还需要关注:

  1. 关掉CMake中的Debug选项:对静态和动态库的压缩都有效
  2. 使用strip命令对动态库裁剪:通常只对动态库的压缩有效(静态库只能剪裁debug信息,-s全部裁剪会导致不可用,见后文)

1. 关掉CMake中的Debug选项

Android NDK提供的toolchain.cmake中带有debug用的-g。发现这个还是因为在某次用gdb去Debug一个可执行文件时,在bt命令执行后发现竟然可以定位出段错误的代码具体行。那么我想CMAKE_C_FLAGSCMAKE_CXX_FLAGS中必然开启了-g

此外,我还对比了我们自己的库与NCNN的静态库和全连接层的静态库的大小(NCNN没有提供动态库)。 为此,我下载了20190611的release代码,其中:

后来经过NCNN的群管理@无事闲来 的点拨去掉NDK(位于$ANDROID_NDK/build/cmake/android.toolchain.cmake)中的-g,编译NCNN时,cmake命令可能如下:

$ cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
      -DANDROID_ABI="armeabi-v7a" -DANDROID_ARM_NEON=ON \
      -DANDROID_PLATFORM=android-14 ..

这其中的CMAKE_TOOLCHAIN_FILE中就带有-g,编辑器打开$ANDROID_NDK/build/cmake/android.toolchain.cmake,删掉带有-g的这行:

list(APPEND ANDROID_COMPILER_FLAGS
  -g
  -DANDROID

编译完成后,发现静态库从15MB直降到1.9MB(具体观察了innerproduct层,编译出的.o文件也从100KB左右降到20KB左右),但我自己源码编译的静态库比release的静态库要小100KB左右,估计是NDK版本不同导致的。

不过我们自己的库中,并未使用NDK提供的TOOLCHAIN文件,不过我也在CMakeLists.txt中发现了set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g")这句,-g去掉后编译,静态库的大小从200MB+降到了19MB左右。

参考

2. 使用strip命令对动态库裁剪

2.1 strip基本介绍

在类Unix和Unix的操作系统中,strip程序可对可执行二进制程序和对象文件中,删除不必要的信息,从而带来更好的性能和减少磁盘空间的使用。“不必要的信息”指的是正常执行功能过程中,不需要的二进制信息,比方调试和符号信息。但该命令裁剪的程度,取决于开发者对这部分代码的具体实现。

此外,使用strip可提高二进制文件在逆向工程中的安全性。如果没有二进制文件的信息和对象的名称,分析它将更加困难。

strip的效果可由连接器(linker)直接实现(见后文与在gcc或者cmake中的使用)。例如,在GNU编译器集合中,这个选项是“-s”。

GNU项目作为GNU binutils包的一部分提供了strip的实现。该命令也移植到了其他操作系统,包括Microsoft Windows。

作为补充知识,引用维基百科关于计算机在编译过程中,符号表(Symbol table)与重定向(Relocation)的介绍。

2.1.1 符号表(Symbol table)

在计算机科学中,符号表(Symbol table)是语言翻译程序(如编译器或解释器)所使用的数据结构,其中程序源代码中的每个标识符(即符号)都与源代码中的声明或外观相关的信息相关联。换句话说,符号表的条目存储与条目对应符号相关的信息。

翻译器使用的符号表中包含的最小信息包括:

对于可重定位符号,必须存储一些重定位信息。高级编程语言的符号表存储符号的类型:字符串、整数、浮点等、大小、尺寸和界限。

并非所有这些信息都包含在输出文件中,但可以提供用于调试。在许多情况下,符号的交叉引用信息与符号表一起存储或链接。大多数编译器会在符号表和交叉引用列表中打印部分或全部这些信息,并在翻译结束时打印这些信息。

比方下面的代码以及其对应的符号表:

// Declare an external function
extern double bar(double x);

// Define a public function
double foo(int count) {
    double  sum = 0.0;
    // Sum all the values bar(1) to bar(count)
    for (int i = 1;  i <= count;  i++)
        sum += bar((double) i);
    return sum;
}

一个C编译器解析这段代码将至少包含下列符号表条目:

Symbol name Type Scope
bar function, double extern
x double function parameter
foo function, double global
count int function parameter
sum double block local
i int for-loop statement

此外,符号表也包含条目为中间表达式(IR)编译器生成的值,如循环变量 i 的表达式,被转换成 double 类型,并返回调用 bar 函数的值、声明标签等等。

动态链接库是ELF(Executable and Linkable Format)文件的一种,一般有两个符号表类型:

符号表类型 说明
.symtab 包含大量的信息(包括全局符号global symbols)
.dynsym 只保留.symtab中的全局符号

.dynsym.symtab 的子集,命令 strip 会去掉ELF文件中 .symtab ,但不会去掉 .dynsym

正常情况下编译出的共享库包含了所有的符号信息与调试信息,对于开发和调试会非常方便。但是对于正常的Release版本我们并不需要这些信息,同时这些信息会占用比较大的磁盘空间。所以裁剪时,这部分信息可以移除。

2.1.2 重定向(Relocation)

重定位(或重定向,relocation)是为位置相关的代码和程序数据分配加载地址,并调整代码和数据,以反映分配的地址的过程。

在多核系统出现之前,以及当前的许多嵌入式系统中,对象的地址绝对是从已知位置开始的,通常为零。

由于多处理系统在程序之间动态链接和切换,因此必须能够使用与位置无关的代码重定位对象。

链接器通常与符号解析一起执行重定位,在运行程序之前,链接器会搜索文件和库以将库的符号引用或名称替换为内存中的实际可用地址的过程。

重定位通常由链接器在链接时完成,但也可以在加载时由重定位加载程序完成,或者在运行时由正在运行的程序本身完成。

有些架构通过将地址分配延迟到运行时来完全避免重定位;这称为零地址算术。

参考:

2.2 strip使用说明

如果不指明strip命令的输出文件,也就是默认不带-o这一指定输出文件参数的情况下,会在待裁剪的库上直接裁剪,不会产生临时拷贝或副本。

该命令使用方式很简单,详细参数可以参考Linux strip command help and examples,这里举几个常见使用例子(常用参数):

# 下面这个例子常用来剪裁动态库
# -s, --strip-all,这两个参数都是移除所有的。作用相当于--strip-debug和--strip-symbol两个
# -o,后接剪裁后的结果(库)
$ strip -s a.out -o opt_a.out

# 剪裁静态库
# -g, -S, -d, --strip-debug | 只移除debugging的符号信息

2.3 strip裁剪静态库的问题

Stackoverflow的How to strip executables thoroughly问题,有回答表示:用strip裁剪后,虽然体积变小但是导致裁剪后的库不能link任何其它库,不可用。或许可以试试strip --strip--unneeded参数,虽然可能不如-s移除所有符号信息的裁剪率高,但是裁剪后的库仍然可用(链接其它动态库)。

博客园上也有一篇讲到:静态库不要strip 太厉害,作者用strip -s同时对相同实现的动静态库分别裁剪,完成后发现裁剪后的静态库无法使用,原本实现的某些函数在静态库中找不到了(裁剪前的静态库是可以使用的)。

对应*.o、*.a文件裁剪时,不给下面这两个参数的话大概率会出问题(如链接不上)。

因为*.o是relocatable ELF文件。 *.a算是*.o的集合。所以最多是使用--strip-unneeded参数,符号不能删除的太彻底。

ELF文件(Executable and Linkable Format, ELF, formerly named Extensible Linking Format):在计算机领域中,可执行和可链接格式(elf,以前称为可扩展链接格式)是可执行文件、对象代码、共享库和核心转储的通用标准文件格式。

此外,可以对*.o、*.so、*.a或可执行程序,使用file命令,查看其状态,动态连接、架构等信息。如使用file ./libcxx_api_lite.a ./test_model_bin命令的结果如下:

libcxx_api_lite.a: current ar archive
./test_model_bin:  ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /system/, not stripped

2.4 不同架构下的strip

正如交叉编译时,因编译Target的硬件架构不同,使用的gcc命令来自NDK,不是来自本地x86机器的gcc:

因架构不同导致使用错了,就会报错:strip: Unable to recognise the format of the input file

一般对于编译链接命令出现这样的错误,都是因为目标文件和命令的编译环境不一样导致的。我当时出现这个问题是因为我的strip命令是x86_64架构下的,而要裁剪的库是armv7或者aarch64的。

默认的gcc/strip都应该是使用的系统安装是/usr/bin中指定的命令,但我们使用来自NDK里的命令必然不在/usr/bin下,为了进一步确认也可使用type命令查看gcc和strip使用的路径:

# 用-v后缀查看编译的Target
$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.11)

# 用type命令查看所用命令的安装位置路径
$ type strip
strip is hashed (/usr/bin/strip)

因为是在手机ARMv8上跑,NDK同样提供了aarch64和armv7的版本,如android-ndk-r17c中的strip命令位于:

# 假设我们的NDK位于/opt目录下
# armv7的strip位于
/opt/android-ndk-r17c/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-strip

# armv8的strip位于
/opt/android-ndk-r17c/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip
ysh329 commented 5 years ago

2.5 gcc和CMake都有集成strip

CSDN上一篇博客讲到使用cmake/gcc:strip缩减程序体积,而且StackoverFlow上也有一个类似问题:How to config cmake for strip file

2.5.1 gcc

gcc自带了一个-s选项,可以做到与strip命令同样的功能:移除掉可执行程序中的符号表和重定位信息。这是来自gcc(1): GNU project C/C++ compiler | Linux man pagegcc命令使用说明中,对-s参数的描述,注意是小写的s,如set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -s")

而大写的-S表示生成汇编代码,如生成test.S的命令是gcc -S test.c

2.5.2 CMake

CMake生成的Makefile中,有一个target名为intall/strip可以将install的可执行程序执行strip,执行make help可看到:

$ make help
The following are some of the valid targets for this Makefile:
... all (the default if no target is provided)
... clean
... depend
... install
... list_install_components
... install/strip
... install/local
... rebuild_cache
... edit_cache

执行make install/strip安装程序时就会自动执行strip。深究细节,可以查看Makefile代码中install/strip是这样写的:

install/strip: preinstall
    @$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --cyan "Installing the project stripped..."
    /opt/toolchains/mips-gcc520-glibc222/bin/cmake -DCMAKE_INSTALL_DO_STRIP=1 -P cmake_install.cmake
.PHONY : install/strip

安装动作实际是由cmake_install.cmake来实现的,上面install/strip执行cmake时调用的脚本cmake_install.cmake中会根据CMAKE_INSTALL_DO_STRIP的值决定是否执行strip命令,如下是cmake_install.cmake脚本中的代码片段:

    if(EXISTS "${file}" AND
       NOT IS_SYMLINK "${file}")
      if(CMAKE_INSTALL_DO_STRIP)
        execute_process(COMMAND "/opt/toolchains/mips-gcc520-glibc222/bin/mips-linux-gnu-strip" "${file}")
      endif()
    endif()
ysh329 commented 5 years ago

3. 发布准备:用zip进一步压缩打包

看到ncnn的发布脚本package.sh中,有使用zip命令进一步压缩包:

$ zip -9 -y -r $IOSPKGNAME.zip $IOSPKGNAME

其中有两个参数值得注意:

参考:

4. 扩展阅读