Liunx链接库生成与链接原理

Android是一种基于Linux的开放源代码的操作系统,在之前写的OpenCV人脸校验,人脸识别文中https://www.jianshu.com/p/b833d0d8af80。文中提到Liunx平台下怎么编译so库问题。接下啦我们就来具体看看单个C文件的编译过程。

1.gcc 编译步骤

以hello.c文件为例

#include
#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

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
#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 运行效果


statictest.jpg

2.1.4链接静态库的原理:
objdump 反汇编

objdump -S statictest > test.txt
Liunx链接库生成与链接原理_第1张图片
反汇编.png

可以看到我标记的红色的重要地方,这些就是链接过程中计算逻辑地址(相对的地址),main()函数调用add()函数 是 callq 64a 固定地址值,指向 000000000000064a 地址,也就是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可以实现编译的自动化。

你可能感兴趣的:(Liunx链接库生成与链接原理)