NDK(三):静态库和动态库

计算机的发展,离不开前人的一点点积累,让我们可以直接使用别人的轮子进行快速开发。库存在的意义,就是避免重复造轮子,对于开发好的重复可用的代码,就直接封装为库。

库一般分为两大类,一类是动态库,一类是静态库。

[TOC]

静态库、动态库的介绍以及对比

静态库

静态库,一般命名后缀为.a
会在编译阶段完全被整合进代码段中,所以生成的可执行性文件也比较大。
优点是:编译后的执行程序不再需要函数库的支持,因为所有要使用的函数己经被编译进去了。

这也是他的缺点:

  1. 生成可执行性文件体积大;
  2. 静态库如果发生了改变那么你的程序必须要重新编译。

动态库

动态库,一般命名后缀为.so
动态库在编译的时候并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。
由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。

gcc的编译流程

在介绍怎样编译出静态库和动态库之前,先介绍一下,gcc的编译过程,怎样把一个c、c++文件编译为可执行文件。

编译为可执行的文件,一般需要四个流程:

  1. 预处理(preprocessing)
  2. 编译(compilation)
  3. 汇编(assembly)
  4. 链接(linking)

gcc编译参数

1
2
3
4
5
6
7
8
9
10
# 常用选项
-E:只进行预处理,不编译
-S:只编译,不汇编,生成汇编文件
-c:只编译、汇编,不链接,把汇编文件翻译为二进制机器指令文件
-g:包含调试信息
-I:指定include包含文件的搜索目录
-o:输出成指定文件名
# 高级选项
-v:详细输出编译过程中所采用的每一个选项
-C:预处理时保留注释信息

注意:-o为输出指定文件名,这个命名不影响文件属性,如下面的预处理阶段,如果改为➜ gcc -E main.c -o main.o,也可能正常输出。但是,文件属性仍然只是进行预处理后的文件,而非可执行文件,不同后缀只是为了辨识不同阶段而已。

预处理(preprocessing)

先创建一个main.c文件

1
2
3
4
5
#include <stdio.h>
int main(){
printf("hello world\n");
return 0;
}
1
➜  gcc -E main.c -o main.i

预处理阶段主要处理include和define等。它把#include包含进来的.h 文件插入到#include所在的位置,把源程序中使用到的用#define定义的宏用实际的字符串代替

下面列出了生成的部分内容,可以看到stdio.h的内容被插入进来

1
2
3
4
5
6
7
8
9
10
11
12
13
......省略......
extern int __vsprintf_chk (char * restrict, int, size_t,
const char * restrict, va_list);


extern int __vsnprintf_chk (char * restrict, size_t, int, size_t,
const char * restrict, va_list);
# 412 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/include/stdio.h" 2 3 4
# 2 "main.c" 2
int main(){
printf("hello world\n");
return 0;
}

编译(compilation)

1
➜  gcc -S main.i -o main.s

在这个阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。

同样列出部分内容,可以看到被转化为汇编语言

1
2
3
4
5
6
7
_main:                                  ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi0:
.cfi_def_cfa_offset 16
......省略......

汇编(assembly)

1
➜  gcc -c main.s -o main.o

汇编阶段把.s汇编语言文件翻译成二进制机器指令文件.o

链接(linking)

1
➜  gcc main.s -o main

链接阶段,链接的是函数库
main.c中并没有定义printf的函数实现,且在预编译中包含进的stdio.h中也只有该函数的声明。系统把这些函数实现都放到名为libc.so的动态库,所以在这个阶段,gcc默认去系统默认路径/usr/local/lib搜索动态库并进行链接,就可以生成可执行文件。

1
2
3
# 执行可执行文件,输出结果
➜ ./main
hello world

四个阶段一并执行

上面的为分阶段介绍的过程,一般我们只是执行gcc命令,会包含以上四个阶段

1
2
3
4
➜  gcc main.c -o main2
➜ ./main2
hello world
# 可以看到与上面的效果一致

查看引用库

上面链接阶段,说到默认会去搜索本地的动态库,通过otool可以查看引用的库,linux为ldd命令

1
2
3
4
# 可以看到引用库id为libSystem.B.dylib,地址为/usr/lib
➜ otool -L ./main
./main:
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.50.4)

去到/usr/lib目录下,可以看到libSystem.B.dylib文件,再执行otool可以看到详细引用库id

1
2
3
4
➜  otool -L ./libSystem.B.dylib
./libSystem.B.dylib:
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.0.0)
......省略......

.a .so .o 文件的区别

通过上面的介绍,应该大概知道,.a.so.o文件的区别了,如果还不太理解,可以参考浅析Linux中的.a、.so、和.o文件这篇文章的介绍,通过window的文件去类比介绍

怎样编译出静态库和动态库

前面介绍不同后缀文件的区别作为铺垫,接下来,就来到这篇文章的主题

创建静态库

1
ar cr 生成的静态库名称.a   引用的可执行文件.o   [引用的可执行文件.o]

c:创建一个库。不管库是否存在,都将创建。
r:在库中插入模块(替换)

创建动态库

1
gcc -shared -fPCI 可执行文件.o [可执行文件.o] -o 生成的动态库名称.so

实例介绍

接下来,举例详细介绍怎样创建静态库和动态库,并且对比两者的不同点

创建使用到的程序

hello.h文件

1
2
3
4
5
6
#ifndef HELLO_H
#define HELLO_H

void hello(const char *name);

#endif //HELLO_H

hello.c文件

1
2
3
4
5
6
#include <stdio.h>

void hello(const char *name)
{
printf("Hello %s!\n", name);
}

main.c文件

1
2
3
4
5
6
7
#include "hello.h"

int main()
{
hello("world");
return 0;
}

把hello.c编译成.o文件

无论静态库,还是动态库,都是由.o文件创建的。因此,我们必须将源程序hello.c通过gcc先编译成.o文件

1
2
3
4
➜  gcc -c hello.c -o hello.o
# 查看当前路径下的文件,多了一个hello.o
➜ ls
hello.c hello.h hello.o main.c

创建、使用静态库

创建静态库

通过.o可执行文件,创建静态库libhello.o

1
2
3
➜  ar cr libhello.a hello.o
➜ ls
hello.c hello.h hello.o libhello.a main.c

使用静态库

使用静态库,编译main.c源文件,生成hello可执行文件

1
2
3
4
5
6
7
8
9
➜  gcc -o hello main.c -L. -lhello
➜ ls
hello hello.c hello.h libhello.a main.c
➜ ./hello
Hello world!

# 查找库文件
-LXX 指定库文件查找路径,同一个路径下,为.
-lXX 指定需要链接的库名,不需要加前缀lib和文件后缀.so

删除libhello.a文件,验证是否hello.c文件中的实现被直接链接到hello

1
2
3
4
➜  rm libhello.a
# 没有libhello.a,发现还是可以正常输出,印证了上面静态库会在编译阶段完全被整合进代码段中
➜ ./hello
Hello world!

创建、使用动态库

创建动态库

1
2
3
➜  gcc -shared -fPIC -o libhello.so hello.o
➜ ls
hello.c hello.h hello.o libhello.so main.c

使用动态库

1
2
3
4
5
➜  gcc -o hello main.c -L. -lhello
➜ ls
hello hello.c hello.h hello.o libhello.so main.c
➜ ./hello
Hello world!

把生成的libhello.so移动到其他路径,再执行

1
2
3
4
5
6
7
# 提示加载不到libhello.so库,找不到hello的实现
# 印证了上面,动态库是在程序运行时动态申请并调用其中的内容
➜ ./hello
dyld: Library not loaded: libhello.so
Referenced from: /Users/guidongyuan/code/NDKStudy/./hello
Reason: image not found
[1] 18773 abort ./hello

mac os上,如果有通过-L -l去指定路径,那么会优先在指定的路径下找,如果找不到,会去默认路径/usr/local/lib找,可以尝试把libhello.so文件移动到默认路径中,再次执行

1
2
3
4
5
6
7
8
9
➜  mv libhello.so /usr/local/lib
# 成功输出,可验证,会去默认路径寻找
➜ ./hello
Hello world!
# 同样,通过otool -L查看可执行文件引用库的id,可以发现引用了libhello.so
➜ otool -L hello
hello:
libhello.so (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.50.4)

参考资料

公众号:亦袁非猿

欢迎关注,交流学习