Android是一种基于Linux的开放源代码的操作系统,在之前写的OpenCV人脸校验,人脸识别文中https://www.jianshu.com/p/b833d0d8af80。文中提到Liunx平台下怎么编译so库问题。接下啦我们就来具体看看单个C文件的编译过程。
1.gcc 编译步骤
以hello.c文件为例
#include<stdio.h>
#define TAG "yang"
void main()
{
printf("hello world %s",TAG);
}
gcc -o hello hello.c
通过gcc -o hello hello.c命令 经过一系列的操作将hello.c文件编译打包成一个hello的可执行项目。.c文件-->可执行文件经历了哪些步骤呢?接下來把gcc编译打包成执行文件过程拆开。
1.预处理阶段:
gcc -E -o hello.i hello.c
通过gcc -E的命令将.c文件生成.i文件。用vim查看.i文件的内容,发现.i文件的内容非常的多,从中复制一段
省略代码多行.........
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 868 "/usr/include/stdio.h" 3 4
# 2 "hello.c" 2
# 5 "hello.c"
void main()
{
printf("hello world %s","yang");
}
上面内容看到 stdio.h 的内容都插到文件里去了,包括printf()函数里面的TAG 已近替换上了具体的字符串“yang”。可以得知gcc编译的第一阶段预处理阶段是将源文件中引入的头文件进行展开,define的清除,注释的删除,包括宏定义的替换等。
2.编译阶段:
gcc -S -o hello.s hello.i
执行gcc -S命令将.i文件编成.s文件,查看.s文件的内容如下
.file "hello.c"
.section .rodata
.LC0:
.string "yang"
.LC1:
.string "hello world %s"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.2.0-8ubuntu3.2) 7.2.0"
.section .note.GNU-stack,"",@progbits
相比.i文件.s文件内容又少了,但是似乎我们看的并不是太懂。gcc -S这个过程将c代码翻译成汇编代码包括词法分析、语法分析、语义分析等。
3.汇编阶段
gcc -c hello.o -o hello.s
这个时候我们在去查看文件发现是乱码。这个过程将第二阶段生产的汇编代码编译成机器可执行的指令。
4.链接阶段(链接静态库,动态库)
gcc -o hello hello.o
由于源文件会用到其他库的函数,在这个过程会去计算逻辑地址,合并数据段等。
2.静态库与动态库
2.1静态库的制作
以mathUtils,c为例编译成.a静态库。
#include<stdio.h>
int add(int a,int b) {
return a+b;
}
2.1.1生成目标.o文件
gcc -c mathUtils.c -o mathUtils.o 此过程生成.o文件但是不会去链接库。
2.1.2将目标文件进行打包
ar cvr myMathUtils.a mathUtils.o
2.1.3这样静态库就生成了。接着以来看看怎么使用链接这个静态库。以statictest.c为例
#include<stdio.h>
#include "mathUtils.c"
void main(){
int a = 10;
int b = 10;
int result = add(a,b);
printf("result = %d",result);
}
将statictest.c编译成.o文件不去链接
gcc -c statictest.c -o statictest.o
接下来我们就去链接myMathUtils.a库
gcc -o statictest statictest.o libmathUtils.a
这个过程链接libmathUtils.a库并生成可执行文件statictest 。
statictest 运行效果
2.1.4链接静态库的原理:
objdump 反汇编
objdump -S statictest > test.txt
可以看到我标记的红色的重要地方,这些就是链接过程中计算逻辑地址(相对的地址),main()函数调用add()函数 是 callq 64a <add>固定地址值,指向 000000000000064a <add> 地址,也就是add函数的相对地址。
通过分析可以得出,一个项目引入静态库中的函数,就相当于拷贝了静态库中的函数。所以编译过的项目,即使没有静态库,项目也是可以运行的。
静态库的缺点:
(1)同一个模块被多个模块链接时,那么这个模块在磁盘和内存中都有多个副本,导致很大一部分空间被浪费了。
(2)当程序的任意一个模块发生更新时,整个程序都要重新链接。
2.2动态库的制作:
2.2.1 将 .c文件生成与位置无关的目标.o文件
gcc -c mathUtils.c -o mathUtils.o -fPIC
2.2.2使用 gcc -shared 制作动态库
gcc -shared mathUtils.o -o libmathUtils.so
2.2.3 编译sharedtest.c 不去链接.so库
gcc -c sharedtest.c -o sharedtest.o
2.2.4链接动态库并生成可执行文件
gcc -o sharedtest sharedtest.o libmathUtils.so
2.2.5动态库链接原理:
我们依旧使用上图(反汇编图)来分析动态库的链接原理,上面我们在main()函数中用到了printf()函数,调用时是callq 520 < printf@plt> 。
Plt(延迟绑定),程序引用了动态库,在没有运行程序时链接函数的地址是不确定的,要运行程序就必须先加载动态库与动态链接器到进程地址空间,系统运行可执行文件之前,会将控制权交给动态链接器,由它完成所有的动态链接工作以后再把控制权交给可执行文件。这样就链接到函数地址。
动态库的缺点:
(1)链接的过程是在程序运行之后开始的,并经过链接器一系列的寻址,才能链接到函数,会导致程序运行效率变慢。
小结:
上面介绍单个c文件的编译过程,对于整个项目包含许多c文件,编译项目采用Makefile文件。Makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译。Makefile的编写基于单个文件编译过程。通过Makefile可以实现编译的自动化。