源码编译入门-Android程序员C/C++编译入门(交叉编译、Makefile)

最近还在处理LinuxC开发。 开发过程中会用到交叉编译和Makefile相关知识,对此我确实不太了解,于是在网上搜索了一下,发现了一篇不错的博客。 本文大部分内容摘自博客《Android程序员C/C++编译入门》(作者:嘉伟略)。 如有侵权,请联系删除。

为什么要学习C/C++编译

很多Android程序员可能还使用AndroidStudio写了一些简单的C/C++代码,然后通过jni调用,并不知道C/C++是如何编译的。 有人可能会问为什么Android程序员需要了解C/C++是如何编译的?

我总觉得要成为一名真正的中级Android应用开发工程师,Android源码和C/C++是绕不过去的两座大山。 当然Android源码就不用多说了,但是C/C++已经流行好几年了,也有很多优秀的开源项目,我们在处理一些特定需求的时候可能需要用到它们。 比如脚本语言Lua、计算机视觉库OpenCV、音视频编解码库ffmpeg、微软的gRPC、国产游戏引擎Cocos2dx……。有的库提供了完整的Android Socket,有的提供了部分Android Socket,有的则没有。 在做一些中间功能的时候,我们经常需要用到源码。 通过裁剪和交叉编译,我们可以编译出一个可以在Android上使用的库。 所以图书馆。 事实上,Android如果再深、再精,也无法避免C/C++交叉编译。

C/C++编译器

与java编译器javac可以将java代码编译成class文件类似,C/C++也有gcc、g++、clang等编译器可以用来编译C/C++代码。 这里我们以gcc为例。

Gcc最初被称为GNUC语言编译器(GNUC Compiler),因为它只能处理C语言。 但GCC扩展得很快源码编译入门,它似乎能够处理C++。 后来,它被扩展为支持更多的编程语言,例如 Fortran、Pascal、Objective-C、Java、Ada、Go 以及各种处理器架构上的汇编语言,因此更名为 GNU Compiler Collection。

使用gcc,虽然只需要一条命令就可以将ac文件编译成可执行程序:

gcc test.c -o test

通过前面的命令,可以将test.c编译成可运行的程序test。 而C/C++的编译虽然经历了几个步骤,但我还是先给大家概述一下。

C/C++编译过程

C/C++的编译可以分为以下步骤:

预处理

相信学过C/C++的朋友都知道“宏”,它在编译时会被扩展并替换为实际代码。 此扩展步骤是在预处理期间执行的。 事实上,预处理不仅做宏扩展,还进行插入头文件、删除注释等操作。

预处理后的产物仍然是C/C++代码,其代码逻辑与输入的C/C++源代码完全相同。

举个简单的例子,编写一个test.h文件和一个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;                 
}

之后,可以通过以下gcc命令对test.c文件进行预处理,并将预处理结果报告给test.i:

gcc -E test.c -o test.i

然后你可以看到预处理后的 test.c 是什么样子的:


这里可以看到,它把test.h的内容(添加模式的声明)插入到test.c的代码中,然后将A和B这两个宏展开为1和2,去掉注释,添加了一些信息。 而且光看代码逻辑,和我们之前写的代码是一模一样的。

汇编代码

也许大家都听说过汇编语言,年轻的朋友可能还没有见过。 简单地说,汇编语言是一种象征机器语言的语言,是机器无法直接识别的低级语言。 我们可以通过以下命令将预处理后的代码编译为汇编语言:

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

汇编

汇编步骤是将汇编代码编译成机器语言:

gcc -c test.s -o test.o

生成的test.o文件上面是机器码,我们可以使用nm命令列出test.o上面的符号:

nm test.o

得到的结果如下:

0000000000000000 T add
0000000000000014 T main

关联

因为我们的示例代码比较简单,只有一个test.h和test.h,所以只生成了一个.o文件,虽然通常的程序是由多个模块组成的。 链接步骤是将多个模块的代码组合成一个可执行程序。 我们可以使用gcc命令将多个.o文件或者静态库和动态库链接成一个可执行文件:

gcc test.o -o test

得到的是可执行文件test,可以直接使用以下命令运行

./test

事实上,没有输出,因为我们没有进行任何复制

编译so库

在Android中,我们通常不会直接使用C/C++编译出来的可执行文件。 so库应该多用。 如何编译so库?

首先我们需要去掉test.c中的main函数,因为so库中没有main函数:

#include "test.h"
/**
 * add 方法定义
 */
int add(int a, int b){
        return a + b;
}

然后可以使用以下命令将test.c编译成test.so:

gcc -shared test.c -o test.so

虽然多了一个-shared参数,但指定编译的结果是一个动态链接库。

这里是直接将.c文件编译成so。 其实也可以像前面的例子一样,先编译.o文件,然后通过链接生成so文件。

事实上,通常在编译动态链接库时,我们都会带上-fPIC参数。

fPIC(Position-IndependentCode)告诉编译器形成位置无关代码,即形成的代码中没有绝对地址,全部使用相对地址。 因此,代码可以被加载器加载到显存的任意位置并能正确执行。 否 fPIC 编译出来的内容是在重载时根据加载的位置进行重定位。 因为上面的代码不是位置无关的代码。 如果它被多个应用程序使用,则它们必须为每个程序维护一个副本。 复制.so的代码。 由于每个程序加载.so的位置不同,所以这次重定位后的代码实际上是不同的,并且无法共享。

交叉编译

通过前面的例子,我们知道了一个C/C++程序是如何从源代码一步步编译成可执行程序或者so库的。 并且编译出来的程序或者so库只能在同系统的笔记本上使用。

例如,我使用的笔记本电脑是Linux系统,因此编译出来的程序只能在Linux上运行,而不能在Android或Windows上运行。

其实一般情况下,没有人会去android系统去编译程序供Android使用。 通常,我们在PC上编译适用于Android的程序,然后在Android上运行它们。 这些在一个平台上在另一个平台上产生可执行代码的编译方法称为交叉编译。

交叉编译还有三个比较重要的概念需要先解释一下:

如果我们要交叉编译一个可以在Android上运行的程序或库,我们不能直接使用gcc来编译。 相反,我们需要使用AndroidNDK提供的一套交叉编译工具链。

我们首先需要下载AndroidNDK,然后配置环境变量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/目录中看到安装的工具链。 进入HOME/Android/standalone-toolchains/android-toolchain-arm/bin/目录,我们可以看到arm-linux-androideabi-gcc这个程序。

它是gcc的Android交叉编译版本。 我们把之前所有使用gcc编译的例子都替换成它来编译在Android上运行的程序:

Android上可以通过jni调用以下命令生成的so库:

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

不同CPU架构的编译形式

其实Android也有很多不同的CPU架构,不同CPU架构的程序不一定兼容。 相信大家在使用AndroidStudio编译so的时候也看到了编译出来的库有很多版本,比如armeabi、armeabi-v7a、mips、x86等。

如何编译不同CPU架构的程序?

我们可以看到$NDK_ROOT/toolchains目录下有几个目录:

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架构的交叉编译工具链。 还记得我们安装工具链的命令吗?

$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参数可以指定使用哪个工具链,然后可以使用该工具链来编译该框架版本的程序。

但是,我们看到这里并没有armeabi-v7a工具链,那么如何编译armeabi-v7a程序呢?

虽然armeabi-v7a程序也是用arm-linux-androideabi-4.9编译的,但是编译时可以带上-march=armv7-a:

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

生成文件

我们上面的反例都是直接使用gcc或者gcc的各种交叉编译版本来编译C/C++代码。 在代码量不多的情况下这样做还是可行的,如果软件比较复杂,代码量就会增加。 很多,这样编译出来的命令会非常复杂,而且还必须考虑多个模块之间的依赖关系。

Makefile就是帮助我们解决这个问题的一个工具。 它的基本原理很简单,我们来看看它最基本的用法:

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

target可以是objectfile(目标文件),也可以是执行文件,也可以是标签。

先决条件是生成该目标所需的文件或目标。

command是make需要执行的命令。 (任何 shell 命令)

这是一种文件依赖,即目标的一个或多个目标文件依赖于先决条件中的文件,其生成规则在命令中定义。 说白了,如果先决条件中有多个文件比目标文件新,那么command定义的命令就会被执行。 这是makefile的规则。 这就是makefile中的核心内容。

让我们以我们的示例代码为例,首先创建一个名为 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命令会在当前目录下查找Makefile,然后比较目标文件和依赖文件的更改时间。 如果依赖文件的更改时间比较晚,或者根本没有目标文件。 将执行该命令。

clean不是一个文件,它只是一个动作名源码编译入门,有点像c语言中的lable,逗号后面没有任何内容,所以make不会手动查找它的依赖项,也不会手动执行其后定义的命令。 要执行后续命令(除clean外,其他标签也适用),需要在make命令后强调该标签的名称。 这个方法非常有用。 我们可以在makefile中定义未使用的编译或与编译无关的命令,例如程序打包、程序备份等。

这只是一个比较简单的用法。 具体Makefile知识请参考跟我一起写Makefile。

CMake

CMake是一个跨平台的编译工具,比make更中间,使用起来方便很多。 CMake主要编译CMakeLists.txt文件,然后使用cmake命令将CMakeLists.txt文件转换为make需要的makefile文件,最后使用make命令编译源代码生成可执行程序或共享库(所以(共享对象))。

#1.cmake verson,指定cmake版本 
cmake_minimum_required(VERSION 3.2)
#2.project name,指定项目的名称,一般和项目的文件夹名称对应
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,设置环境变量,编译用到的源文件全部都要放到这里,否则编译能够通过,但是执行的时候会出现各种问题,比如"symbol lookup error xxxxx , undefined symbol"
set(TEST_MATH ${DIR_SRCS})
#7.add executable file,添加要编译的可执行文件
add_executable(${PROJECT_NAME} ${TEST_MATH})
#8.add link library,添加可执行文件所需要的库,比如我们用到了libm.so(命名规则:lib+name+.so),就添加该库的名称
target_link_libraries(${PROJECT_NAME} m)

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 源码编译 源码编译入门-Android程序员C/C++编译入门(交叉编译、Makefile) https://www.wkzy.net/game/153354.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务