天天看點

android中裁剪壓縮圖檔_軟體釋出前的庫優化與裁剪:初識

android中裁剪壓縮圖檔_軟體釋出前的庫優化與裁剪:初識

在軟體釋出時,都會對其裁剪。在此過程中,會先查明編譯的依賴關系,并将重型的依賴庫進行替換或者移除。本文将會介紹在軟體釋出時,庫裁剪指令

strip

的使用,目錄結構如下:

  1. CMake GraphViz查明編譯依賴
  2. 關掉CMake中的Debug選項
  3. 使用strip指令對動态庫裁剪
    1. strip基本介紹
      1. 符号表(Symbol table)
      2. 重定向(Relocation)
    2. strip使用說明
    3. strip裁剪靜态庫的問題
    4. 不同架構下的strip
    5. gcc和CMake都有內建strip
  4. 釋出準備:用zip進一步壓縮打包

1. CMake GraphViz查明編譯依賴

梳理依賴關系的方法,通常是在

cmake

指令中追加參數graphviz,如

cmake .. --graphviz=../target_deps_graphviz

,用來生成每個目标的依賴

dot

檔案,再結合

dot

指令,如

dot -Tpng -o target.png ./target.dot

生成類似下面的PNG圖或者PDF檔案,以梳理依賴關系,為裁剪庫做準備和參考。

android中裁剪壓縮圖檔_軟體釋出前的庫優化與裁剪:初識

最近因為架構編譯出動态和靜态庫都很大,假設上圖的可執行檔案編譯出來有200MB+,除了可以用更輕量級别的log替代上面的glog,或者不使用gflags外。有兩個地方在壓縮時還需要關注:

  1. 關掉CMake中的Debug選項:對靜态和動态庫的壓縮都有效
  2. 使用strip指令對動态庫裁剪:通常隻對動态庫的壓縮有效(靜态庫隻能剪裁debug資訊,

    -s

    全部裁剪會導緻不可用,見後文)

2. 關掉CMake中的Debug選項

Android NDK提供的

toolchain.cmake

中帶有debug用的

-g

。發現這個還是因為在某次用gdb去Debug一個可執行檔案時,在

bt

指令執行後發現竟然可以定位出段錯誤的代碼具體行。那麼我想

CMAKE_C_FLAGS

CMAKE_CXX_FLAGS中

必然開啟了

-g

此外,我還對比了我們自己的庫與NCNN的靜态庫和全連接配接層的靜态庫的大小(NCNN沒有提供動态庫)。 為此,我下載下傳了20190611的release代碼,其中:

  • 源碼編譯。android-armv8的

    libncnn.a

    是15MB;
  • prebuilt包。下載下傳release的android-armv8的lib即

    lbncnn.a

    卻是2MB。

後來經過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左右。

參考

  • cmake 打包 android sdk · Tencent/ncnn Wiki

3. 使用strip指令對動态庫裁剪

3.1 strip基本介紹

在類Unix和Unix的作業系統中,

strip

程式可對可執行二進制程式和對象檔案中,删除不必要的資訊,進而帶來更好的性能和減少磁盤空間的使用。“不必要的資訊”指的是正常執行功能過程中,不需要的二進制資訊,比方調試和符号資訊。但該指令裁剪的程度,取決于開發者對這部分代碼的具體實作。

此外,使用

strip

可提高二進制檔案在逆向工程中的安全性。如果沒有二進制檔案的資訊和對象的名稱,分析它将更加困難。

strip

的效果可由連接配接器(linker)直接實作(見後文與在gcc或者cmake中的使用)。例如,在GNU編譯器集合中,這個選項是“-s”。

GNU項目作為

GNU binutils

包的一部分提供了

strip

的實作。該指令也移植到了其他作業系統,包括Microsoft Windows。

作為補充知識,引用維基百科關于計算機在編譯過程中,符号表(Symbol table)與重定向(Relocation)的介紹。

#### 3.1.1 符号表(Symbol table)

在計算機科學中,符号表(Symbol table)是語言翻譯程式(如編譯器或解釋器)所使用的資料結構,其中程式源代碼中的每個辨別符(即符号)都與源代碼中的聲明或外觀相關的資訊相關聯。換句話說,符号表的條目存儲與條目對應符号相關的資訊。

翻譯器使用的符号表中包含的最小資訊包括:

  • 符号的名稱;
  • 其可重定位屬性(絕對、可重定位等)(relocatability attributes (absolute, relocatable, etc.);
  • 其位置或位址。

對于可重定位符号,必須存儲一些重定位資訊。進階程式設計語言的符号表存儲符号的類型:字元串、整數、浮點等、大小、尺寸和界限。

并非所有這些資訊都包含在輸出檔案中,但可以提供用于調試。在許多情況下,符号的交叉引用資訊與符号表一起存儲或連結。大多數編譯器會在符号表和交叉引用清單中列印部分或全部這些資訊,并在翻譯結束時列印這些資訊。

比方下面的代碼以及其對應的符号表:

// 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編譯器解析這段代碼将至少包含下列符号表條目:

android中裁剪壓縮圖檔_軟體釋出前的庫優化與裁剪:初識

此外,符号表也包含條目為中間表達式(IR)編譯器生成的值,如循環變量

i

的表達式,被轉換成

double

類型,并傳回調用

bar

函數的值、聲明标簽等等。

動态連結庫是ELF(Executable and Linkable Format)檔案的一種,一般有兩個符号表類型:

android中裁剪壓縮圖檔_軟體釋出前的庫優化與裁剪:初識

.dynsym

.symtab

的子集,指令

strip

會去掉ELF檔案中

.symtab

,但不會去掉

.dynsym

正常情況下編譯出的共享庫包含了所有的符号資訊與調試資訊,對于開發和調試會非常友善。但是對于正常的Release版本我們并不需要這些資訊,同時這些資訊會占用比較大的磁盤空間。是以裁剪時,這部分資訊可以移除。

3.1.2 重定向(Relocation)

重定位(或重定向,relocation)是為位置相關的代碼和程式資料配置設定加載位址,并調整代碼和資料,以反映配置設定的位址的過程。

在多核系統出現之前,以及目前的許多嵌入式系統中,對象的位址絕對是從已知位置開始的,通常為零。

由于多處理系統在程式之間動态連結和切換,是以必須能夠使用與位置無關的代碼重定位對象。

連結器通常與符号解析一起執行重定位,在運作程式之前,連結器會搜尋檔案和庫以将庫的符号引用或名稱替換為記憶體中的實際可用位址的過程。

重定位通常由連結器在連結時完成,但也可以在加載時由重定位加載程式完成,或者在運作時由正在運作的程式本身完成。

有些架構通過将位址配置設定延遲到運作時來完全避免重定位;這稱為零位址算術。

參考:

  • 動态連結庫優化---清除符号表資訊 - 簡書
  • elf檔案格式與動态連結庫(非常之好)-----不可不看 - helloworlddm的部落格 - CSDN部落格

3.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的符号資訊
           

3.3 strip裁剪靜态庫的問題

Stackoverflow的How to strip executables thoroughly問題,有回答表示:用strip裁剪後,雖然體積變小但是導緻裁剪後的庫不能link任何其它庫,不可用。或許可以試試

strip --strip--unneeded

參數,雖然可能不如

-s

移除所有符号資訊的裁剪率高,但是裁剪後的庫仍然可用(連結其它動态庫)。

部落格園上也有一篇講到:靜态庫不要strip 太厲害,作者用

strip -s

同時對相同實作的動靜态庫分别裁剪,完成後發現裁剪後的靜态庫無法使用,原本實作的某些函數在靜态庫中找不到了(裁剪前的靜态庫是可以使用的)。

  • 那有辦法裁剪靜态庫麼?
  • 保證使用strip指令後也能正常使用該靜态庫檔案?
  • 為什麼對so檔案使用了strip指令後不會影響它和測試程式的連結?
  • 而且對靜态庫檔案就會連結不成功?

對應

*.o、*.a

檔案裁剪時,不給下面這兩個參數的話大機率會出問題(如連結不上)。

  • -g -S -d --strip-debug

    : Remove debugging symbols only;
  • --strip-unneeded

    : Remove all symbols that are not needed for relocation processing。

因為

*.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
           

3.4 不同架構下的strip

正如交叉編譯時,因編譯Target的硬體架構不同,使用的gcc指令來自NDK,不是來自本地x86機器的gcc:

  • x86_64架構的交叉編譯機的

    gcc -v

    Target

    x86_64-linux-gnu

  • 交叉編譯所用的NDK中的

    arm-linux-androideabi-g++ -v

    Target

    arm-linux-androideabi

    即armv7,當然還有armv8的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
           

3.5 gcc和CMake都有內建strip

CSDN上一篇部落格講到使用cmake/gcc:strip縮減程式體積,而且StackoverFlow上也有一個類似問題:How to config cmake for strip file。

3.5.1 gcc

gcc

自帶了一個

-s

選項,可以做到與

strip

指令同樣的功能:移除掉可執行程式中的符号表和重定位資訊。這是來自gcc(1): GNU project C/C++ compiler | Linux man page的

gcc

指令使用說明中,對

-s

參數的描述,注意是小寫的

s

,如

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -s")

而大寫的

-S

表示生成彙編代碼,如生成

test.S

的指令是

gcc -S test.c

3.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()
           

4. 釋出準備:用zip進一步壓縮打包

看到ncnn的釋出腳本package.sh中,有使用

zip

指令進一步壓縮包:

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

其中有兩個參數值得注意:

  • zip -9:數字的1到9,表示壓縮級别,預設壓縮級别是6,壓縮級别0表示所有檔案不壓縮隻是打包到一起,最高的數字9表示使用最優的方法來壓縮所有檔案,如

    zip -9 -r archivename.zip directory_name

    ,壓縮級别的數字越高,将會占用更多CPU資源和時間來進行壓縮計算;
  • zip -y:-y表示保留符号連結。對于UNIX和VMS(V8.3及更高版本),将符号連結存儲在zip存檔中,而不是壓縮和存儲連結引用的檔案。zip(1): package/compress files - Linux man page的對于該參數的解釋是:這可以避免在壓縮檔案中包含多個檔案副本,因為zip會重複使用目錄樹并直接通過連結通路檔案。

參考:

  • How to Zip Files and Directories in Linux | Linuxize
  • zip(1): package/compress files - Linux man page

5. 擴充閱讀

  • A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux:很好的教程,以裁剪ELF為例教你如何裁剪
  • Executable compression - Wikipedia
  • Relocation (computing) - Wikipedia
  • Executable and Linkable Format - Wikipedia
  • Symbol table - Wikipedia

繼續閱讀