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.o
和sub.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