飞道的博客

Linux中gcc的编译、静态库和动态库的制作

314人阅读  评论(0)
欢迎大家关注笔者,你的关注是我持续更博的最大动力


gcc是文本编译器,就是编译代码的工具,下面介绍gcc编译C语言(.c文件)的流程。

1 gcc的编译过程

1.1 gcc的编译过程

gcc的编译分为以下四个阶段:

  • gcc预处理器:把.c文件编译成预处理.i文件
  • gcc编译器:把预处理.i文件编译成.s的汇编文件
  • gcc汇编器:把.s汇编文件编译成.o二进制文件
  • gcc链接器:把.o二进制文件链接成一个可执行文件

四个阶段的编译命令:

  • 预处理:gcc -E hello.c -o hello.i
  • 编译:gcc -S hello.i -o hello.s
  • 汇编:gcc -c hello.s -o hello.o
  • 链接:gcc hello.o -o hello

上面的四个过程也可以用一个命令执行,直接生成可执行的文件:

gcc hello.c -o hello
# 或
gcc hello.c    # 没有指定输出问价名,默认是生成一个a.out可执行文件。

注意:

1、记忆参数可以用ESc-o参数是指定输出文件的名字
2、在windows下,如果gcc hello.c,默认生成的可执行文件为a.exe;如果gcc hello.c -o myapp,会直接生成可执行文件myapp.exe,自动添加后缀。
3、在第二阶段把预处理.i文件编译成.s汇编文件浪费时间
4、即使是直接生成可执行文件,但是也是经过了预处理编译汇编链接这些过程,只是没有生成中间的这些文件。


四个阶段的具体功能:

  • 预处理:1)把.c文件中的头文件展开添加到.i预处理文件的开头;2)然后把.c文件代码添加到.i的头文件内容之后;3)把宏定义的量值替换为具体的值,去掉原代码中的注释

  • 编译:把c文件翻译汇编文件,就是两种程序语法的转化。

  • 汇编:把汇编文件编程二进制文件,此时的文件已经看不出具体的内容。

  • 链接:将函数库中相应的代码组合到目标文件中。

1.2 gcc的常用参数

下面具体实例:

一、执行文件和头文件同级目录

1、创建一个sum.c文件,内容如下:

#include <stdio.h>
// 双引号导入的头文件是自己写的
#include "head.h"
#define DEBUG

// main是入口函数
int main(void)
{
    int a = NUM1;
    int aa;
    int b = NUM2;
    int sum = a + b;
    // 这是一个加法运算
#ifdef DEBUG
    printf("The sum value is : %d + %d = %d\n", a, b, sum);
#endif
    return 0;   
}

2、在sum.c的同级创建head.h头文件,内容如下:

#ifndef __HEAD_H_
#define __HEAD_H_

#define NUM1 10
#define NUM2 20
#endif

两个文件的层级结构,同级目录:

├── head.h
├── sum.c

3、预处理:gcc -E sum.c -o sum.i

执行完之后用vi sum.i查看预处理之后sum.i内容,如下:

从文件中可以看到,文件内容很长,之前的导入的头文件,被替换为具体的头文件代码内容,代码中的宏定义量被替换为具体的值,代码中的注释去掉。(相当于做菜食材的准备阶段)

4、编译:gcc -S sum.i -o sum.s

编译就是把预处理的.i文件编译成.s的汇编语言,编译之后的sum.s内容,如下:

从文件中可以看出,这个文件显示的已经不是C语言编写的代码,已经被转换为汇编语言的代码,如果你对单片机了解,你可能也对汇编语言的语法有所了解。(编译:就是把C语言翻译成汇编语言

5、汇编:gcc -c sum.s -o sum.o

汇编就是把汇编文件变成二进制文件,汇编之后的sum.o内容,如下:

从文件中可以看出,汇编成二进制文件之后,里面的内容已经看不出来了。

6、链接:gcc sum.o -o sum

使用gcc链接器二进制文件链接成一个可执行文件,将函数库中相应的代码组合到目标文件中。通过./sum即可执行该可执行文件,执行结果如下:

如果你打开可执行文件sum,显示的内容和sum.o差不多。

二、执行文件和头文件同级目录

目录层级结构:

├── include
│   └── head.h
├── sum.c

如果直接编译(gcc sum.c -o sum),会提示找不到头文件,如下:

找不到头文件有两种解决方法:

  • 直接在程序编写的时候指定头文件的位置
  • 在编译的时候用-I参数,指定头文件所在的文件夹位置

gcc sum.c -I ./include -o sum

三、gcc的其他参数使用

1、参数-D:指定一个宏定义

上面的程序中有printf()打印程序调试的log信息,但是程序发布的时候,我们是不需要这些log信息的,当然我们可以通过加调试的#define DEBUG宏的声明,但是,程序中需要调试输出的log信息比较多的时候,这种方法显然不合适。

现在我们把DEBUG的宏定义注释掉

#include <stdio.h>
// 双引号导入的头文件是自己写的
#include "head.h"
//#define DEBUG

// main是入口函数
int main(void)
{
    int a = NUM1;
    int aa;
    int b = NUM2;
    int sum = a + b;
    // 这是一个加法运算

// 程序有 DEBUG宏定义,程序才会执行prinf()
#ifdef DEBUG
    printf("The sum value is : %d + %d = %d\n", a, b, sum);
#endif
    return 0;   
}

然后再执行:

>>>gcc sum.c -o sum
>>>./sum

结果:

并不会输出print打印的信息了,如果再次打印出信息呢,此时可以通过参数-D,在执行命令的时候给程序指定一个宏,如下:

>>>gcc sum.c -o sum -D DEBUG
>>>./sum

此时就可以打印出printf()信息了。

总结:

-D参数的作用:不在程序中定义宏,在程序编译的时候定义。不指定,在程序预处理的时候,printf()就会被删掉了。

2、-O参数:程序预处理的时候对代码优化

在程序预处理的时候对代码进行优化,把冗余的代码去掉,有三个优化等级:

  • -O1:优化等级低
  • -O2:优化等级中
  • -O3:优化等级高

举个例子:

int a = 10
int b = a
int c = b
int d = c

# 优化完之后就是
int d = 10  // 就是对d的一个赋值操作

3、-Wall参数:输出程序中的警告信息

例如我们在程序中定义一个变量int aa;,但是没有使用,此时就会输出警告信息。

4、-g参数:在程序中添加一些调试信息

gcc sum.c -o sum -g

  • -g参数之后,输出的可执行文件会比不加的大(因为包含调试信息)
  • 程序发布是不需要加-g参数
  • 调试需要加-g参数,否则没有调试信息不可以调试。(gdb调试的时候必须加此参数

总结:

参数:-E-S,不是很重要,-c比较重要,后面我们在制作静态库和动态库的时候需要用到生成的.o二进制值文件

2 gcc 静态库的制作

比如你和别人做项目合作,你不可能直接把源代码给被人,那样被人就可以自己开发,因为源代码就是你的核心技术。你不应该卖给他源代码,而是应该是程序,这样你就可以根据他有什么需求进行改或添加什么功能模块等,就可以改一次就可以收费一次,这样就可以有一个长期合作。

那应该给到客户的是什么呢?

  • 生成的库
  • 头文件

这样把生成的库头文件给客户也能够使用,只是他不知道里面具体怎么实现的。这样二者才能维持一个长期的合作

头文件对应的.c文件都被打包到了静态库动态库里面了。

2.1 静态库的制作流程

一、静态库的制作

1、命名规则

  • 1)lib + 库的名字 + .a
  • 2)例如:libmytest.a

2、制作步骤:

  • 1)生成对应的.o二进制文件 .c --> .o eg:gcc sum.c -c sum.o
  • 2)将生成的.o文件打包,使用ar rcs + 静态库的名字(libMytest.a) + 生成的所有的.o
  • 3)发布和使用静态库:
    • 发布静态库
    • 头文件

说明:

  • .c文件,也就是源代码转化成.o二进制文件之后,客户就不知道到你的核心技术具体是怎么实现的了。
  • ar是对.o的二进制文件进行打包rcs是打包参数,把所有.o二进制文件打包成一个.a文件,即:静态库。因此:静态库是一个打包了二进制文件的集合
  • 接口API是在头文件中体现出来的。

实例:

目录结构:

Calc
├── include
│   └── head.h
├── lib
├── main.c
└── src
    ├── add.c
    ├── div.c
    ├── mul.c
    └── sub.c

说明:

  • include文件夹:存放头文件,提供给用户调用的接口API
  • lib文件夹:存放库文件,即:生成的静态库、动态库
  • src文件夹:存放源文件
  • main.c程序:是用户调用head.h头文件里面的接口,然后在调用静态库里面我们实现的算法(只不过已经不是源码,而是被编译成二进制文件)

下面开始吧:

源代码 src/add.c实现的是加法运算:

#include "head.h"

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

头文件 include/head.h实现是对源代码调用的接口API

#ifndef __HEAD_H_
#define __HEAD_H_
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
#endif

main.c是对头文件调用,然后调用静态文件,对算法的使用,但是并不知道算法的具体实现源代码

#include <stdio.h>
#include "head.h"

int main(void)
{
    int sum = add(2, 24);
    printf("sum = %d\n", sum);
    return 0;
}

用户在main.c中引入头文件#include "head.h",即在./include/head.h,就可以使用./include/head.h中定义的接口int add(int a, int b);,当main.c程序执行到add(int a, int b);接口时,就会到./src文件夹下找静态文件(打包的二进制文件——即:加法算法的具体实现)


下面是具体的制作流程:

shliang@shliang-vm:~/shliang/gcc_learn/Calc$ tree
.
├── include
│   └── head.h
├── lib
├── main.c
└── src
    ├── add.c
    ├── div.c
    ├── mul.c
    └── sub.c

3 directories, 6 files
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ cd src
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls
add.c  div.c  mul.c  sub.c

1、源代码生成二进制文件(.o文件)
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ gcc *.c -c -I ../include
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls
add.c  add.o  div.c  div.o  mul.c  mul.o  sub.c  sub.o

2、对生成的二进制文件(.o文件),打包成静态文件(.a文件),并移动到lib目录下
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ar rcs libMyCalc.a *.o
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls
add.c  add.o  div.c  div.o  libMyCalc.a  mul.c  mul.o  sub.c  sub.o
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ mv libMyCalc.a ../lib
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ cd ..
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ls

3、调用include目录下的头文件(即:封装的API接口)
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ gcc main.c lib/libMyCalc.a -I ./include -o sum
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ls
include  lib  main.c  src  sum
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ./sum
sum = 26
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ 

主要:

  • 制作好的静态文件要放到lib目录下
  • 调用头文件中的接口API,然后用gcc编译的自己调用的main.c文件,需要加上静态文件(.a文件)
  • 程序发布的时候只需要给用户的文件:
    • 1)include目录下的头文件(head.h):封装的是具体算法实现的接口API
    • 2)lib目录下的静态文件(.a文件):是源代码编译的之后的二进制文件(.o文件),然后被打包成静态文件(.a文件)

用于另外一种调用静态库的方法为:

gcc main.c -Iinclude -L lib -l MyCalc -o myapp

参数说明:

  • -I参数:指定头文所在的文件夹名,文件夹名可以和参数贴着写在一起
  • -L参数:指定静态库的文件夹名
  • -l参数:指定静态库的名字,但名字要掐头去尾,eg:原静态库名字为libMyCalc.a,在指定-l参数值的时候为:-l MyCalc
  • -o参数:输出编译之后可执行文件的名字

注意:

之所以用-l指定静态库的名字,是因为lib目录下可能有多个静态库文件,但是我们只需要使用其中的某一个,此时可以用这种方法指定相应的静态库文件。

二、静态库相关文件查看

1、nm命令查看静态库

可以使用nm命令查看静态库文件中具体打包了哪些二进制文件.o文件

2、nm命令查看生成的可执行文件

T:代表的含义是把add代码会被放到代码区

2.2 静态库的优缺点

1、通过静态库生成可执行文件

  • 静态库中封装了多个.o文件
  • main.c 中调用静态库中相应可执行文件(二进制文件)中的函数
  • 图中只调用了add.o和sub.o中的函数,因此main.c在生成可执行文件的时候只会把静态文件中的add.osub.o两个文件打包到可执行文件中,静态文件中的其他没有用到的.o文件不会被打包进可执行文件中。
  • 在生成可执行文件的时候也是以.o可执行文件单位打包的,并不会把整个静态文件.a都打包到可执行文件中。

静态库的优点:

  • 1)发布程序的时候,不需要提供对应的库了,因为库已经被打包到了可执行文件中去了。
  • 2)库的加载速度比较快,因为库已经被打包到可执行文件中去了。

静态库的缺点:

  • 1) 库被打包到应用程序(最后生成的可执行文件)中,如果库很多的话就会导致应用程序的体积很大。
  • 2)库发生了改变,需要重新编译程序,如果源代码比较多,可能编译一遍一天就过去了。

3 gcc 动态库 / 共享库 的制作

动态库也叫共享库,在windows中对用.dll文件

3.1 动态库 / 共享库的制作流程

一、动态库相关说明

1、命名规则:

  • 1)lib + 名字 + .so
  • 2)例如:libMyCalc.so

2、制作步骤:

  • 1)生成与位置无关的代码 (生成与位置无关的.o)
  • 2)将.o打包成共享库(动态库)
  • 3)发布和使用共享库:

注意:

  • 静态库生成的.o文件是和位置有关的
  • gcc生成和位置无关的.o文件,需要使用参数-fPIC(常用) 或 -fpic

二、动态库制作相关实例

在了解什么叫生成和位置无关的.o文件,我们来先了解一下虚拟地址空间

linux上打开一个运行的程序进程),操作系统就会为其分配一个(针对32位操作系统)0-4G的地址空间虚拟地址空间),虚拟地址空间不是在内存中,而是来自硬盘的存储空间。

从下到上:

0-3G:是用户区

  • .text 代码段:存放的是代码
  • .data :存放的是已初始化的变量
  • .bss:存放的是未初始化的变量
  • 堆空间:
  • 共享库:动态库的空间,每次程序运行的时候把动态库加载到这个空间
  • 栈空间:我们定义的局部变量都是在栈空间分配的内存
  • 命令行参数
  • 环境变量

在往上 3-4G是内核区

  • 静态库生成与位置有关二进制文件(.o文件)

虚拟地址空间是从0开始的,生成的二进制文件(.o文件)会被放到代码段,即.text代码区。生成的.o代码每次都被放到同一个位置,是因为使用的是绝对地址

  • 动态库生成与位置无关二进制文件(.o文件)

动态库 / 共享库 在程序打包的时候并不会把.o文件打包到可执行文件中,只是做了一个记录,当程序运行之后才去把动态库加载到程序中,也就是加载到上图中的共享库空间,但是每次加载到共享库空间的位置可能不同
还是和上面静态库制作同样的目录结构:


动态库制作实例

Calc
├── include
│   └── head.h
├── lib
├── main.c
└── src
    ├── add.c
    ├── div.c
    ├── mul.c
    └── sub.c

shliang@shliang-vm:~/shliang/gcc_learn/Calc$ cd src/
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls
add.c  div.c  mul.c  sub.c

1、把源码生成和位置无关的二进制文件
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ gcc -fPIC -c *c -I ../include
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls
add.c  add.o  div.c  div.o  mul.c  mul.o  sub.c  sub.o

2、使用gcc把生成的二进制文件(.o文件),打包成动态库(.so文件)
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ gcc -shared -o libMyCalc.so *o -Iinclude
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls
add.c  add.o  div.c  div.o  libMyCalc.so  mul.c  mul.o  sub.c  sub.o

3、把生成的动态库文件移动到lib目录下
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$  mv libMyCalc.so ../lib
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ cd ..
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ls
include  lib  main.c  src
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ 

参数说明:

  • -PIC:生成和位置无关的.o文件
  • -shared:共享,就是把.o文件,打包成动态库 / 共享库

上面就已经完成动态库的制作,然后把下面的两个文件发布用户即可调用

  • include/head.h: 头文件,定义接口API
  • lib/libMyCalc.so:动态库,封装了编译之后的源代码二进制文件

用户使用动态库

用户使用动态库和静态库一样有两种方法:

  • 用户使用动态库方法一:

gcc main.c lib/libMyCalc.so -o app -Iinclude

  • 用户使用动态库方法二:

gcc main.c -Iinclude -L lib -l MyCalc -o myapp

3.2 动态库查找不到解决方法

我们可以看到,第二种方法,至执行可执行程序的时候,提示找不到动态库,这并不一定是动态库文件不存在,可能是由于链接不到

是不是真的链接不到,我们可以通过一个命令ldd:查看可执行文件执行的时候,依赖的所有共享库/动态库(.so文件)

ldd命令使用:

ldd 可执行文件名

shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ldd myapp 
	linux-vdso.so.1 =>  (0x00007fff59d26000)  # 后面的数字是库的地址
	# 提示我们自己的动态库 / 共享库 libMyCalc.so没有找到
	libMyCalc.so => not found
	# libc.so.6 是linux下的标准C库 (写C程序都会调用标准C库里的一些函数)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1e27462000)
	# 动态链接器,动态链接器的本质就是一个动态库
	/lib64/ld-linux-x86-64.so.2 (0x00007f1e2782c000)
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ 

如上图:可执行程序./a.out在执行的时候,调用需要调用动态库libmytest.so,但是实际上这个调用是通过动态链接器的来调用的。动态库就是通过动态链接器--/lib64/ld-linux-x86-64.so.2加载到我们的可执行程序(应用程序)中的。

那么动态链接器是-- /lib64/ld-linux-x86-64.so.2是通过什么规则查找可执行文件在执行时,需要的动态文件的呢?

其实就是通过环境变量

在linux下查看环境变量:

echo $PATH

当然PATH下并不是存放动态库的路径,这里只是做一个演示,如何查看环境变量。

一、动态库查找不到解决方法一(不推荐——不允许使用

把自己制作的动态库放到根目录下的系统动态库中,即/lib目录下

sudo cp ./lib/libMyCalc.so /lib


从上面的结果可以看到,把自己制作的动态库拷贝到系统动态库中之后,动态链接器根据环境变量就可以找到这个动态库,然后正确加载到可执行程序中。

注意:

这种方法一般不会使用的,因为如果你的动态库的名字和系统中某个动态库的名字一样,就可能会导致系统奔溃的!!!这里只是做一个演示,证明动态链接器是根据环境变量去查找要加载的动态库。

二、动态库查找不到解决方法二(临时测试设置

通过把动态库添加到动态库环境变量中,即:LD_LIBRARY_PATH

使用export添加环境变量,把当前动态库所在的位置(文件夹位置)添加到LD_LIBRARY_PATH变量中,可执行程序在执行的时候会在默认的动态库之前从LD_LIBARAY_PATH变量中查找有没有所需动态库。

# export
export LD_LIBARAY_PATH=./lib

注意:

但是,这种方法只是临时的,当我们关闭终端,下次再执行程序又会提示找不到动态库。因此,这钟方法一般是再开发动态库的过程中,用于临时的测试

三、动态库查找不到解决方法三(永久设置——不常用

当前用户家目录home)下的.bashrc文件中配置LD_LIBRARY_PAHT环境变量。

cd ~
vi .bashrc

# 然后再最后一行添加一个环境变量,如果没有就创建(Shift+G跳到最后一行)
# 然后把动态库的绝对路径赋值给该变量
export LD_LIBARAY_PATH=/home/shliang/shliang/gcc_learn/Calc/lib

# 保存退出,用source激活配置,如果不激活需要重启终端,因为终端每次重启都会从.bashrc中加载一次配置
source .bashrc

上面添加完环境变量之后就可以找到动态库了。

四、动态库查找不到解决方法四(永久设置

这种方法,相对与前三种复杂一些,一定要掌握,可能以后用作中用到的就是这种。做法如下:

1、需要找到动态连接器配置文件/etc/ld.so.conf
2、把我们自己制作的动态库目录的绝对路径写到配置文件中
3、更新配置文件:sudo ldconfig -v

  • ld:dynamic library 动态库的缩写
  • -v :是更细配置文件的时候输出更新信息。

修改配置文件的路径位置:/etc/ld.so.conf

/home/shliang/shliang/gcc_learn/Calc/lib添加到/etc/ld.so.conf配置文件中

之后就可以找到动态库了,如下:

3.3 动态库的优缺点

1、动态库的优点

  • 执行程序的体积小:程序在执行的时候采取加载动态库,并没有和可执行程序打包在一起
  • 动态库更新了,不需要重新编译程序(不是绝对的,前提是函数的接口不变,内容便里没事)

2、动态库的缺点

  • 程序发布的时候,需要把动态库提供给用户
  • 动态库没有被打包到应用程序中,加载速度相对较慢




♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠


转载:https://blog.csdn.net/weixin_41010198/article/details/105434085
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场