精简动态符号表
- 删除一个符号表项后 so 减小的体积为:符号名字符串长度+ 1 + Elfxx_Sym + Elfxx_Half + Elfxx_Word
- 如果一个 so 的某些符号,被其他 so 通过 dlsym 方式使用,那么这些符号也应该保留在该 so 的导出符号中(否则会导致运行时异常)。
- 使用 visibility 和 attribute 控制符号可见性
- 可以通过给编译器传递 -fvisibility=VALUE 控制全局的符号可见性。
- visibility 是全局符号可见性开关,attribute 方式是针对单个符号的可见性开关。
- 只能控制变量或函数是否存在于动态符号表中(即是否删除其动态符号表项),而不会删除其实现体。
- default:除非对变量或函数特别指定符号可见性,所有符号都在动态符号表中,这也是不使用 -fvisibility 时的默认值。
- hidden:除非对变量或函数特别指定符号可见性,所有符号在动态符号表中都不可见。
- CMake 项目的配置
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden")
- ndk-build 项目的配置 LOCAL_CFLAGS += -fvisibility=hidden
- 针对单个变量或函数
- attribute((visibility("hidden")))
- int hiddenInt=3;
- 使用 static 关键字控制符号可见性
- 当使用 static 表示"该函数或变量仅在本文件可见"时,那么这个函数或变量就不会出现在动态符号表中,但只会删除其动态符号表项,而不会删除其实现体。
- 使用 exclude libs 移除静态库中的符号
- 它是传递给链接器的参数,可以使依赖的静态库的符号在动态符号表中不存在。同样,也是只能删除符号表项,实现体仍然会存在于产生的 so 文件中。
- CMake 项目的配置
- set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,ALL")#使所有静态库中的符号都不被导出
- set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,libabc.a")#使 libabc.a 的符号都不被导出
- ndk-build 项目的配置
- LOCAL_LDFLAGS += -Wl,--exclude-libs,ALL #使所有静态库中的符号都不被导出
- LOCAL_LDFLAGS += -Wl,--exclude-libs,libabc.a #使 libabc.a 的符号都不被导出
- 使用 version script 控制符号可见性
- version script 是传递给链接器的参数,用来指定动态库导出哪些符号以及符号的版本。
- version script 支持通配符,* 代表0个或者多个字符,? 代表单个字符。比如 my*; 就代表所有以 my 开头的符号。有了通配符的支持,配置 version script 会更加方便。
- version script 方式可以删除 __bss_start 这样的一些符号(这是链接器默认加上的符号)
- version script 方式优于 visibility 结合 attribute 的方式。同时,使用了 version script 方式,就不需要使用 exclude libs 方式控制依赖的静态库中的符号是否导出了。
- 开启 version script 需要先编写一个文本文件,用来指定动态库导出哪些符号。然后将文件的路径传递给链接器即可
- 查看未优化 so 的导出符号表,找到目标函数被修饰后的符号,然后填写到 version_script.txt 中
- 在 version_script.txt 中使用 extern 语法
- CMake 项目的配置 set(CMAKE_SHARED_LINKER_FLAGS "CMAKESHAREDLINKERFLAGS−Wl,−−version−script={CMAKE_SHARED_LINKER_FLAGS} -Wl,--version-script=CMAKESHAREDLINKERFLAGS−Wl,−−version−script={CMAKE_CURRENT_SOURCE_DIR}/version_script.txt") #version_script.txt 与当前 CMakeLists.txt 同目录
- ndk-build 项目的配置 LOCAL_LDFLAGS += -Wl,--version-script=${LOCAL_PATH}/version_script.txt #version_script.txt 与当前 Android.mk 同目录
移除无用代码
- 开启 LTO(Link Time Optimization 链接期优化)
- LTO 能够在链接目标文件时检测出 DeadCode 并删除它们
- GCC 和 Clang 均支持 LTO。LTO 方式编译的目标文件中存储的不再是具体机器的指令,而是机器无关的中间表示(GCC 采用的是 GIMPLE 字节码,Clang 采用的是 LLVM IR 比特码)。
- 如果使用 Clang,编译参数和链接参数中都要开启 LTO,否则会出现无法识别文件格式的问题(NDK22 之前存在此问题)。使用 GCC 的话,只需要编译参数中开启 LTO 即可。
- 如果项目工程依赖了静态库,可以使用 LTO 方式重新编译该静态库,那么编译动态库时,就能移除静态库中的 DeadCode,从而减小最终 so 的体积。
- 经过测试,如果使用 Clang,链接器需要开启非 0 级别的优化,LTO 才能真正生效。经过实际测试(NDK 为 r16b),O1 优化效果较差,O2、O3 优化效果比较接近。
- CMake 项目的配置
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto")
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto")
- set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -O3 -flto")
- ndk-build 项目的配置
- LOCAL_CFLAGS += -flto
- LOCAL_LDFLAGS += -O3 -flto
- 开启 GC sections
- 这是传递给链接器的参数,对无用的 section 进行回收。注意,这里的 section 不是指最终 so 中的 section,而是作为链接器的输入的目标文件中的 section。
- GC sections 参数通知链接器,仅保留动态符号(及 .init_array 等)直接或者间接引用到的 section,移除其他无用 section。
- 链接器会把所有输入的目标文件的同类型的 section 进行合并,组装出最终的 so 文件。
- 编译器默认会把所有函数放到同一个 section 中,把所有相同特点的数据放到同一个 section 中
- CMake 项目的配置
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fdata-sections -ffunction-sections")
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdata-sections -ffunction-sections")
- set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--gc-sections")
- ndk-build 项目的配置
- LOCAL_CFLAGS += -fdata-sections -ffunction-sections
- LOCAL_LDFLAGS += -Wl,--gc-sections
优化指令长度
-
使用 Oz/Os 优化级别
- O0 表示不开启优化(这种情况主要是为了便于调试以及更快的编译速度),从 O1 到 O3,优化程度越来越强。Clang 和 GCC 均提供了 Os 的优化级别,其与 O2 比较接近,但是优化了生成产物的体积。而 Clang 还提供了 Oz 优化级别,在 Os 的基础上能进一步优化产物体积。
- CMake 项目的配置
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Oz")
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Oz")
- ndk-build 项目的配置 LOCAL_CFLAGS += -Oz
-
如果项目中没有使用 C++ 的异常机制(例如 try...catch 等),可以通过禁用 C++ 的异常机制,来减小 so 的体积。
- CMake 项目的配置 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
- ndk-build 默认会禁用 C++ 的异常机制,因此无需特意禁用。如果现有项目开启了 C++ 的异常机制,说明确有需要,需仔细确认后才能禁用。
-
如果项目中没有使用 C++ 的 RTTI 机制(例如 typeid 和 dynamic_cast 等),可以通过禁用 C++ 的 RTTI ,来减小 so 的体积。
- CMake 项目的配置 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
- ndk-build 默认会禁用 C++ 的 RTTI 机制,因此无需特意禁用。如果现有项目开启了 C++ 的 RTTI 机制,说明确有需要,需仔细确认后才能禁用。
-
so 仅被另外一个 so 动态依赖时,可以将这些 so 合并为一个 so。可以在不修改项目源码的情况下,在编译层面实现 so 的合并。
- 可以删除部分导入导出动态符号表项
- 可以删除部分 PLT 表项和 GOT 表项
- 减少了依赖关系,可以减轻优化的工作量
- 由于链接器对导出符号拥有了更全的上下文信息,LTO 优化也能取得更好的效果。
-
当多个 so 以静态方式依赖了某个相同的库时,可以考虑将此库提取成一个单独的 so,原来的几个 so 改为动态依赖该 so。提取多 so 共同依赖库,可以对不同 so 内的相同代码进行合并,从而减小总的 so 体积。
-
收益
- 删除了大量的非必要导出符号从而提升了 so 的安全性。
- 因为 .data .bss .text 等运行时占用内存的 section 减小了,所以也能减小应用运行时的内存占用。
- 如果优化过程中减少了 so 对外依赖的符号,还可以加快 so 的加载速度。