先來看一下,如果要在PC上運行一個二進制程序(以源碼的方式進行編譯,不要以包管理工具的方式來安裝),需要怎樣做?
首先,要有這個二進制程序的源代碼(有可能是直接下載的,也有可能是自己編寫的代碼),然后在PC上進行編譯鏈接生成可執(zhí)行文件,最后在Terminal下面去執(zhí)行該可執(zhí)行文件。
上述流程中包含了幾個角色,首先是要有源代碼,然后是要知道最終運行該二進制程序的機器是哪一個(其實就是本機器),當然,其中最重要的就是編譯器和鏈接器了,對于C或者C++程序來講,就是使用gcc和g++,而該編譯器是需要預先安裝在機器上的。分析了這么多角色,總結成一句話就是:使用本機器的編譯器,將源代碼編譯鏈接成為一個可以在本機器上運行的程序。這就是正常的編譯過程,也稱為Native Compilation,中文譯作本機編譯。
了解了本機編譯之后,再來看一下何為交叉編譯。所謂交叉編譯,就是在一個平臺(如PC)上生成另外一個平臺(Android、iOS或者其他嵌入式設備)的可執(zhí)行代碼。相較于正常編譯,下面來看一下交叉編譯的相應角色。首先,最終程序運行的設備就是Android或者iOS設備,源代碼就是從第三方開源網(wǎng)站上下載的源代碼,編譯機器就是我們的PC,而編譯器也必須要安裝到該PC上。但是這里對編譯器是有特殊需求的,最終程序運行的系統(tǒng)必須要提供可運行在PC上的編譯器,而該編譯器就是大家常說的交叉工具編譯鏈。
了解了交叉編譯之后,大家應該能夠理解交叉編譯存在的必要性了。在一般的嵌入式系統(tǒng)開發(fā)中,運行程序的目標平臺其存儲空間和運算能力都是有限的,盡管現(xiàn)在的iOS和Android設備擁有越來越強勁的計算能力,但是在這種嵌入式設備中進行本地編譯是不太可能的,一則是因為計算能力的問題,還有一個重要的原因就是編譯工具以及整個編譯過程異常繁瑣,所以在這種情況下,直接在ARM平臺下進行本機編譯幾乎是不可能的。而具有更加強勁的計算能力與更大存儲空間的PC才是理想的選擇,所以大部分的嵌入式開發(fā)平臺都提供了本身平臺交叉編譯所需要的交叉工具編譯鏈,通過該交叉工具編譯鏈,開發(fā)者就能在PC上編譯出可以運行在ARM平臺下的程序了。
無論是自行安裝PC上的編譯器,還是下載其他平臺(Android或者iOS)的交叉工具編譯鏈,它們都會提供以下幾個工具:CC、AS、AR、LD、NM、GDB。那么,這幾個工具到底是做什么用的呢?下面就來逐一解釋一下。
- CC:編譯器,對C源文件進行編譯處理,生成匯編文件。
- AS:將匯編文件生成目標文件(匯編文件使用的是指令助記符,AS將它翻譯成機器碼)。
- AR:打包器,用于庫操作,可以通過該工具從一個庫中刪除或者增加目標代碼模塊。
- LD:鏈接器,為前面生成的目標代碼分配地址空間,將多個目標文件鏈接成一個庫或者是可執(zhí)行文件。
- GDB:調試工具,可以對運行過程中的程序進行代碼調試工作。
- STRIP:以最終生成的可執(zhí)行文件或者庫文件作為輸入,然后消除掉其中的源碼。
- NM:查看靜態(tài)庫文件中的符號表。
- Objdump:查看靜態(tài)庫或者動態(tài)庫的方法簽名。
在這個過程中,gcc、ar、g++是我們用到的三個編譯工具,在這里沒有用到的ranlib、gdb、nm、strip等都會包含在PC的編譯器中,同樣其他平臺提供的交叉工具編譯鏈中也會包含這些命令行工具,比如Android提供的NDK,其交叉工具編譯鏈中的prebuilt/darwin-x86_64/bin中,就包含了對應的gcc、ar、g++、gdb、strip、nm、ranlib等工具。
Android原生開發(fā)包(NDK)可用于Android平臺上的C++開發(fā),NDK不僅僅是一個單一功能的工具,還是一個包含了API、交叉編譯器、鏈接程序、調試器、構建工具等的綜合工具集。
下面大致列舉了一下經(jīng)常會用到的組件。
- ARM、x86的交叉編譯器
- 構建系統(tǒng)
- Java原生接口頭文件
- C庫
- Math庫
- 最小的C++庫
- ZLib壓縮庫
- POSIX線程
- Android日志庫
- Android原生應用API
- OpenGL ES(包括EGL)庫
- OpenSL ES庫
下面來看一下Android所提供的NDK根目錄下的結構。
- ndk-build:該Shell腳本是Android NDK構建系統(tǒng)的起始點,一般在項目中僅僅執(zhí)行這一個命令就可以編譯出對應的動態(tài)鏈接庫了,后面會有詳細的介紹。
- ndk-gdb:該Shell腳本允許用GUN調試器調試Native代碼,并且可以配置到Eclipse的IDE中,可以做到像調試Java代碼一樣調試Native的代碼。
- ndk-stack:該Shell腳本可以幫助分析Native代碼崩潰時的堆棧信息,后續(xù)會針對Native代碼的崩潰進行詳細的分析。
- build:該目錄包含NDK構建系統(tǒng)的所有模塊。
- platforms:該目錄包含支持不同Android目標版本的頭文件和庫文件,NDK構建系統(tǒng)會根據(jù)具體的配置來引用指定平臺下的頭文件和庫文件。
- toolchains:該目錄包含目前NDK所支持的不同平臺下的交叉編譯器——ARM、x86、MIPS,其中比較常用的是ARM和x86。構建系統(tǒng)會根據(jù)具體的配置選擇不同的交叉編譯器。
系統(tǒng)到底會使用哪些編譯器以及打包器和鏈接器來編譯我們的程序呢?
會使用$NDK_ROOT/toolchains/arm-linux-androideabi4.8/prebuilt/darwin-x86_64/bin/目錄(以Mac平臺為例)下面的gcc、g++、ar、ld等工具。同樣在該目錄下的strip工具將會用于清除so包里面的源碼,nm工具可以供開發(fā)者查看靜態(tài)庫下的符號表。
那么進行gcc編譯的時候,頭文件將放在哪里呢?
$NDK_ROOT/platforms/android-18/arch-arm/usr/include/目錄下會存放編譯過程所依賴的頭文件。
那么在鏈接過程中,經(jīng)常使用的log或者OpenSL ES以及OpenGL ES等庫又將放在哪里呢?
答案其實已在前文中提到過,$NDK_ROOT/platforms/android18/arch-arm/usr/lib/目錄下會存放鏈接過程中所依賴的庫文件。
LAME的交叉編譯
在Android的編譯中,一般情況下會使用一個Shell腳本文件,指定好編譯器里面的各個工具,然后把對應的Configure的命令與選項開關配置好,最后執(zhí)行該Shell腳本:
#!/bin/bash
NDK_ROOT=/Users/apple/soft/android/android-ndk-r9b
PREBUILT=$NDK_ROOT/toolchains/arm-linux-androideabi-4.6/prebuilt/darwin-x86_64
PLATFORM=$NDK_ROOT/platforms/android-9/arch-arm
export PATH=$PATH:$PREBUILT/bin:$PLATFORM/usr/include:
export LDFLAGS="-L$PLATFORM/usr/lib -L$PREBUILT/arm-linux-androideabi/lib
-march=armv7-a"
export CFLAGS="-I$PLATFORM/usr/include -march=armv7-a -mfloat-abi=softfp -mfpu=vfp
-ffast-math -O2"
export CPPFLAGS="$CFLAGS"
export CFLAGS="$CFLAGS"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="$LDFLAGS"
export AS=$PREBUILT/bin/arm-linux-androideabi-as
export LD=$PREBUILT/bin/arm-linux-androideabi-ld
export CXX="$PREBUILT/bin/arm-linux-androideabi-g++ --sysroot=${PLATFORM}"
export CC="$PREBUILT/bin/arm-linux-androideabi-gcc --sysroot=${PLATFORM}
-march=armv7-a "
export NM=$PREBUILT/bin/arm-linux-androideabi-nm
export STRIP=$PREBUILT/bin/arm-linux-androideabi-strip
export RANLIB=$PREBUILT/bin/arm-linux-androideabi-ranlib
export AR=$PREBUILT/bin/arm-linux-androideabi-ar
./configure --host=arm-linux \
--disable-shared \
--disable-frontend \
--enable-static \
--prefix=./armv7a
make clean
make -j8
make install
第一部分是設置NDK_ROOT,并且聲明platform和prebuilt,最終配
置可在環(huán)境變量中查看。
第二部分主要是聲明CFLAGS與LDFLAGS,其目的是在編譯和鏈接階段找到正確的頭文件與鏈接到正確的庫文件。這里需要特別注意的是,在這兩個設置的后邊都加上了-march=armv7-a,這相當于是讓編譯器知道要編譯的目標平臺是armv7-a。
第三部分是聲明CC、AS、AR、LD、NM、STRIP等工具,具體每一個工具是做什么用的,前面都已經(jīng)介紹過了,如果要編譯armv5、x86或者arm64-v8a,那么在代碼倉庫中會提供全量編譯的Shell腳本文件。
第四部分就是使用LAME本身的Configure進行編譯裁剪。
第五部分就是使用標準的編譯鏈接和安裝。
最終執(zhí)行腳本成功之后,可以看到在指定的Prefix目錄下面,包含了lib和include目錄,里面分別是靜態(tài)庫文件和頭文件,這兩個目錄的作用在前面已經(jīng)說過很多遍了,在此不再贅述。