寫給安卓程序員的C/C++編譯入門(交叉編譯妄壶,Makefile)

最近一直在和Linux C開發(fā)打交道,開發(fā)過程中會用到交叉編譯和Makefile相關(guān)知識寄狼,但是對這塊真的是沒有了解,所以在網(wǎng)上搜索氨淌,找到一篇不錯(cuò)的博客泊愧。本文大部分摘抄自該博客寫給安卓程序員的C/C++編譯入門(作者:嘉偉咯)。如有侵權(quán)請聯(lián)系刪除盛正。

為什么要學(xué)C/C++編譯

很多的安卓程序員可能都會用Android Studio寫一些簡單的C/C++代碼,然后通過jni去調(diào)用,但是對C/C++是如何編譯的其實(shí)并沒有什么概念.有人可能會問,為什么安卓程序員會需要了解C/C++是如何編譯的呢?

我一直都認(rèn)為,要成為一個(gè)真正的高級安卓應(yīng)用開發(fā)工程師,安卓源碼和C/C++是兩座繞不過的大山.安卓源碼自然不必多說,而C/C++流行了幾十年,存在著許多優(yōu)秀的開源項(xiàng)目,我們在處理一些特定的需求的時(shí)候,可能會需要使用到它們.如腳本語言Lua,計(jì)算機(jī)視覺庫OpenCV,音視頻編解碼庫ffmpeg,谷歌的gRPC,國產(chǎn)游戲引擎Cocos2dx...有些庫提供了完整的安卓接口,有些提供了部分安卓接口,有些則沒有.在做一些高級功能時(shí),我們常常需要使用源碼,通過裁剪和交叉編譯,才能編譯出可以在安卓上使用的so庫.總之,安卓做深做精總避不開C/C++交叉編譯删咱。

C/C++編譯器

類似java編譯器javac可以將java代碼編譯成class文件,C/C++也有g(shù)cc、g++豪筝、clang等多種編譯器可以用于編譯C/C++代碼.這里我們用gcc來舉例痰滋。

gcc原名為GNU C 語言編譯器(GNU C Compiler),因?yàn)樗局荒芴幚鞢語言.但GCC很快地?cái)U(kuò)展,變得可處理C++。后來又?jǐn)U展能夠支持更多編程語言,如Fortran续崖、Pascal敲街、Objective-C、Java严望、Ada多艇、Go以及各類處理器架構(gòu)上的匯編語言等,所以改名GNU編譯器套件(GNU Compiler Collection)。

使用gcc其實(shí)只需要一個(gè)命令就能將一個(gè)c文件編譯成可運(yùn)行程序了:

gcc test.c -o test

通過上面這條命令可以將test.c編譯成可運(yùn)行程序test.但是其實(shí)C/C++的編譯是經(jīng)過了好幾個(gè)步驟的,我這邊先給大家大概的講一講像吻。

C/C++的編譯流程

C/C++的編譯可以分為下面幾個(gè)步驟:

預(yù)處理

相信學(xué)過C/C++的同學(xué)都知道"宏"這個(gè)東西,它在編譯的時(shí)候會被展開替換成實(shí)際的代碼,這個(gè)展開的步驟就是在預(yù)處理的時(shí)候進(jìn)行的.當(dāng)然,預(yù)處理并不僅僅只是做宏的展開,它還做了類似頭文件插入峻黍、刪除注釋等操作.

預(yù)處理之后的產(chǎn)品依然還是C/C++代碼,它在代碼的邏輯上和輸入的C/C++源代碼是完全一樣的.

我們來舉一個(gè)簡單的例子,寫一個(gè)test.h文件和一個(gè)test.c文件:

//test.h
#ifndef TEST_H            
#define TEST_H

#define A 1     
#define B 2        

/**
 * add 方法的聲明
 */               
int add(int a, int b);

#endif
//test.c
#include "test.h"

/**
 * add 方法定義
 */

int add(int a, int b) {
    return a + b;
}

int main(int argc,char* argv[]) {
    add(A, B);
    return 0;                 
}

然后可以通過下面這個(gè)gcc命令預(yù)處理test.c文件,并且把預(yù)處理結(jié)果寫到test.i:

gcc -E test.c -o test.i

然后就能看到預(yù)處理之后的test.c到底長什么樣子了:


可以看到這里它把test.h的內(nèi)容(add方法的聲明)插入到了test.c的代碼中,然后將A、B兩個(gè)宏展開成了1和2,將注釋去掉了,還在頭部加上了一些信息.但是光看代碼邏輯,和之前我們寫的代碼是完全一樣的.

匯編代碼

可能大家都聽過匯編語言這個(gè)東西,但是年輕一點(diǎn)的同學(xué)不一定真正見過.簡單來說匯編語言是將機(jī)器語言符號化了的語言,是機(jī)器不能直接識別的低級語言.我們可以通過下面的命令,將預(yù)處理后的代碼編譯成匯編語言:

gcc -S test.i -o test.s

然后就能看到生成的test.s文件了,里面就是我們寫的c語言代碼翻譯而成的匯編代碼:

.file   "test.c"
        .text
        .globl  add
        .type   add, @function
add:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    %edi, -4(%rbp)
        movl    %esi, -8(%rbp)
        movl    -4(%rbp), %edx
        movl    -8(%rbp), %eax
        addl    %edx, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   add, .-add
        .globl  main
        .type   main, @function
main:
.LFB1:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $16, %rsp
        movl    %edi, -4(%rbp)
        movq    %rsi, -16(%rbp)
        movl    $2, %esi
        movl    $1, %edi
        call    add
        movl    $0, %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE1:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609"
        .section        .note.GNU-stack,"",@progbits

匯編

匯編這一步是將匯編代碼編譯成機(jī)器語言:

gcc -c test.s -o test.o

生成的test.o文件里面就是機(jī)器代碼了,我們可以通過nm命令來列出test.o里面的符號:

nm test.o

得到的結(jié)果如下:

0000000000000000 T add
0000000000000014 T main

鏈接

由于我們的例子代碼比較簡單只有一個(gè)test.h和test.h,所以只生成了一個(gè).o文件,其實(shí)一般的程序都是由多個(gè)模塊組合成的.鏈接這一步就是將多個(gè)模塊的代碼組合成一個(gè)可執(zhí)行程序.我們可以用gcc命令將多個(gè).o文件或者靜態(tài)庫拨匆、動態(tài)庫鏈接成一個(gè)可執(zhí)行文件:

gcc test.o -o test

得到的就是可執(zhí)行文件test了,可以直接用下面命令運(yùn)行

./test

當(dāng)然是沒有任何輸出的,因?yàn)槲覀兙蜎]有做任何的打印

編譯so庫

在安卓中我們一般不會直接使用C/C++編譯出來的可運(yùn)行文件.用的更多的應(yīng)該是so庫.那要如何編譯so庫呢?

首先我們需要將test.c中的main函數(shù)去掉,因?yàn)閟o庫中是不會帶有main函數(shù)的:

#include "test.h"

/**
 * add 方法定義
 */
int add(int a, int b){
        return a + b;
}

然后可以使用下面命令將test.c編譯成test.so:

gcc -shared test.c -o test.so

其實(shí)也就是多了個(gè)-shared參數(shù),指定編譯的結(jié)果為動態(tài)鏈接庫.

這里是直接將.c文件編譯成so,當(dāng)然也能像之前的例子一樣先編譯出.o文件再通過鏈接生成so文件.

當(dāng)然一般編譯動態(tài)鏈接庫,我們還會帶上-fPIC參數(shù).

fPIC (Position-Independent Code)告訴編譯器產(chǎn)生與位置無關(guān)代碼,即產(chǎn)生的代碼中沒有絕對地址,全部使用相對地址.故而代碼可以被加載器加載到內(nèi)存的任意位置,都可以正確的執(zhí)行.不加fPIC編譯出來的so,是要再加載時(shí)根據(jù)加載到的位置再次重定位的.因?yàn)樗锩娴拇a并不是位置無關(guān)代碼.如果被多個(gè)應(yīng)用程序共同使用,那么它們必須每個(gè)程序維護(hù)一份.so的代碼副本了.因?yàn)?so被每個(gè)程序加載的位置都不同,顯然這些重定位后的代碼也不同,當(dāng)然不能共享.

交叉編譯

通過上面的例子,我們知道了一個(gè)C/C++程序是怎么從源代碼一步步編譯成可運(yùn)行程序或者so庫的.但是我們編譯出來的程序或者so庫只能在相同系統(tǒng)的電腦上使用.

例如我使用的電腦是Linux系統(tǒng)的,那它編譯出來的程序也就只能在Linux上運(yùn)行,不能在安卓或者Windows上運(yùn)行.

當(dāng)然正常情況下不會有人專門去到android系統(tǒng)下編譯出程序來給安卓去用.一般我們都是在PC上編譯出安卓可用的程序,在給到安卓去跑的.這種是在一個(gè)平臺上生成另一個(gè)平臺上的可執(zhí)行代碼的編譯方式就叫做交叉編譯.

交叉編譯有是三個(gè)比較重要的概念要先說明一下:

  • build : 當(dāng)前你使用的計(jì)算機(jī)
  • host : 你的目的是編譯出來的程序可以在host上運(yùn)行
  • target : 普通程序沒有這個(gè)概念姆涩。對于想編譯出編譯器的人來說此屬性決定了新編譯器編譯出的程序可以運(yùn)行在哪

如果我們想要交叉編譯出安卓可運(yùn)行的程序或者庫的話就不能直接使用gcc去編譯了.而需要使用Android NDK提供了的一套交叉編譯工具鏈.

我們首先要下載Android NDK,然后配置好環(huán)境變量NDK_ROOT指向NDK的根目錄.

然后可以通過下面命令安裝交叉編譯工具鏈:

$NDK_ROOT/build/tools/make-standalone-toolchain.sh \
    --platform=android-19 \
    --install-dir=$HOME/Android/standalone-toolchains/android-toolchain-arm \
    --toolchain=arm-linux-androideabi-4.9 \
    --stl=gnustl

然后我們就能在HOME/Android/目錄下看到安裝好的工具鏈了.進(jìn)到HOME/Android/standalone-toolchains/android-toolchain-arm/bin/目錄下我們可以看到有arm-linux-androideabi-gcc這個(gè)程序.

它就是gcc的安卓交叉編譯版本.我們將之前使用gcc去編譯的例子全部換成使用它去編譯就能編譯出運(yùn)行在安卓上的程序了:

如下面命令生成的so庫就能在安卓上通過jni調(diào)用了:

$HOME/Android/standalone-toolchains/android-toolchain-arm/bin/arm-linux-androideabi-gcc -shared -fPIC test.c -o test.so

不同CPU架構(gòu)的編譯方式

當(dāng)然安卓也有很多不同的CPU架構(gòu),不同CPU架構(gòu)的程序也是不一定兼容的,相信大家之前在使用Android Studio去編譯so的時(shí)候也有看到編譯出來的庫有很多個(gè)版本像armeabi、armeabi-v7a惭每、mips骨饿、x86等.

那這些不同CPU架構(gòu)的程序又要如何編譯了.

我們可以在$NDK_ROOT/toolchains目錄下看到者幾個(gè)目錄:

arm-linux-androideabi-4.9
aarch64-linux-android-4.9
mipsel-linux-android-4.9
mips64el-linux-android-4.9
x86-4.9
x86_64-4.9

這就是不同CPU架構(gòu)的交叉編譯工具鏈了.還記得我們安裝工具鏈的命令嗎?

$NDK_ROOT/build/tools/make-standalone-toolchain.sh \
    --platform=android-19 \
    --install-dir=$HOME/Android/standalone-toolchains/android-toolchain-arm \
    --toolchain=arm-linux-androideabi-4.9 \
    --stl=gnust

toolchain參數(shù)就能指定使用哪個(gè)工具鏈,然后就能使用該工具鏈去編譯該架構(gòu)版本的程序了.

但是,我們看到這下面并沒有armeabi-v7a的工具鏈,那armeabi-v7a的程序要如何編譯呢?

其實(shí)armeabi-v7a的程序也是用arm-linux-androideabi-4.9去編譯的,只不過在編譯的時(shí)候可以帶上-march=armv7-a:

arm-linux-androideabi-gcc -march=armv7-a -shared -fPIC test.c -o test.so

Makefile

我們前面的例子都是直接用gcc或著各個(gè)交叉編譯的版本的gcc去編譯C/C++代碼的.在代碼量不多的時(shí)候這么做還是可行的,但是如果軟件一旦復(fù)雜一些,代碼量一多,那么編譯的命令就會十分的復(fù)雜,而且還需要考慮到多個(gè)模塊之間的依賴關(guān)系.

Makefile就是一個(gè)幫助我們解決這些問題的工具.它的基本原理十分簡單,先讓我們看看它最最基本的用法:

target ... : prerequisites ...
    command
    ...
    ...

target可以是一個(gè)object file(目標(biāo)文件),也可以是一個(gè)執(zhí)行文件,還可以是一個(gè)標(biāo)簽(label)样刷。

prerequisites就是仑扑,要生成那個(gè)target所需要的文件或是目標(biāo)。

command也就是make需要執(zhí)行的命令置鼻。(任意的shell命令)

這是一個(gè)文件的依賴關(guān)系镇饮,也就是說,target這一個(gè)或多個(gè)的目標(biāo)文件依賴于prerequisites中的文件箕母,其生成規(guī)則定義在 command中储藐。說白一點(diǎn)就是說,prerequisites中如果有一個(gè)以上的文件比target文件要新的話嘶是,command所定義的命令就會被執(zhí)行钙勃。這就是makefile的規(guī)則。也就是makefile中最核心的內(nèi)容聂喇。

還是舉我們的例子代碼,首先創(chuàng)建一個(gè)文件,名字叫Makefile,然后寫上:

test.so : test.c test.h                                                          
    arm-linux-androideabi-gcc -march=armv7-a -shared -fPIC test.c -o test.so
clean :
    rm test.so

然后就可以用make命令去編譯了.make命令會找到當(dāng)前目錄下的Makefile,然后比較目標(biāo)文件文件和依賴文件的修改時(shí)間,如果依賴文件的修改時(shí)間比較晚,或者干脆就還沒有目標(biāo)文件.就會執(zhí)行命令.

clean不是一個(gè)文件辖源,它只不過是一個(gè)動作名字,有點(diǎn)像c語言中的lable一樣希太,其冒號后什么也沒有克饶,那么,make就不會自動去找它的依賴性誊辉,也就不會自動執(zhí)行其后所定義的命令矾湃。要執(zhí)行其后的命令(不僅用于clean,其他lable同樣適用)堕澄,就要在make命令后明顯得指出這個(gè)lable的名字邀跃。這樣的方法非常有用,我們可以在一個(gè)makefile中定義不用的編譯或是和編譯無關(guān)的命令蛙紫,比如程序的打包拍屑,程序的備份,等等惊来。

這只是比較簡單的用法丽涩,具體的Makefile知識請查看跟我一起寫Makefile.

CMake

CMake是一種跨平臺編譯工具,比make更為高級裁蚁,使用起來要方便得多矢渊。CMake主要是編寫CMakeLists.txt文件,然后用cmake命令將CMakeLists.txt文件轉(zhuǎn)化為make所需要的makefile文件枉证,最后用make命令編譯源碼生成可執(zhí)行程序或共享庫(so(shared object))矮男。

#1.cmake verson,指定cmake版本 
cmake_minimum_required(VERSION 3.2)

#2.project name室谚,指定項(xiàng)目的名稱毡鉴,一般和項(xiàng)目的文件夾名稱對應(yīng)
project(myPro)

#3.head file path崔泵,頭文件目錄
include_directories(include)

#4.添加需要鏈接的庫文件目錄
link_directories(include)

#5.source directory,源文件目錄
aux_source_directory(src DIR_SRCS)

#6.set environment variable猪瞬,設(shè)置環(huán)境變量憎瘸,編譯用到的源文件全部都要放到這里,否則編譯能夠通過陈瘦,但是執(zhí)行的時(shí)候會出現(xiàn)各種問題幌甘,比如"symbol lookup error xxxxx , undefined symbol"
set(TEST_MATH ${DIR_SRCS})

#7.add executable file,添加要編譯的可執(zhí)行文件
add_executable(${PROJECT_NAME} ${TEST_MATH})

#8.add link library痊项,添加可執(zhí)行文件所需要的庫锅风,比如我們用到了libm.so(命名規(guī)則:lib+name+.so),就添加該庫的名稱
target_link_libraries(${PROJECT_NAME} m)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鞍泉,一起剝皮案震驚了整個(gè)濱河市皱埠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌咖驮,老刑警劉巖边器,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異游沿,居然都是意外死亡饰抒,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門诀黍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人仗处,你說我怎么就攤上這事眯勾。” “怎么了婆誓?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵吃环,是天一觀的道長。 經(jīng)常有香客問我洋幻,道長郁轻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任文留,我火速辦了婚禮好唯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘燥翅。我一直安慰自己骑篙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布森书。 她就那樣靜靜地躺著靶端,像睡著了一般谎势。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上杨名,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天脏榆,我揣著相機(jī)與錄音,去河邊找鬼台谍。 笑死须喂,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的典唇。 我是一名探鬼主播镊折,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼介衔!你這毒婦竟也來了恨胚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤炎咖,失蹤者是張志新(化名)和其女友劉穎赃泡,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乘盼,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡升熊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绸栅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片级野。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖粹胯,靈堂內(nèi)的尸體忽然破棺而出蓖柔,到底是詐尸還是另有隱情,我是刑警寧澤风纠,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布况鸣,位于F島的核電站,受9級特大地震影響竹观,放射性物質(zhì)發(fā)生泄漏镐捧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一臭增、第九天 我趴在偏房一處隱蔽的房頂上張望懂酱。 院中可真熱鬧,春花似錦速址、人聲如沸玩焰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昔园。三九已至蔓榄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間默刚,已是汗流浹背甥郑。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留荤西,地道東北人澜搅。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像邪锌,于是被迫代替她去往敵國和親勉躺。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344