注:前言、目录见 /qq_44220418/article/details/108428971
文章目录
一、GCC编译器1、GCC编译程序的流程2、GCC命令格式、常用参数3、GCC创建库文件(1).静态库(2).动态库(共享库)二、GDB调试器1、GDB调试器的使用2、GDB常用命令三、Make工具1、Makefile编写规则一、GCC编译器
关于gcc的一些使用,我推荐几篇博客如下:
用gcc编译c语言程序以及其编译过程
/weixin_33755847/article/details/89697445
看了这个大概知道编译的分哪些步,每一步gcc要怎么用Linux基础——gcc编译、静态库与动态库(共享库)
/daidaihema/article/details/8090
这个博客讲解了gcc编译的流程、静态库和动态库
1、GCC编译程序的流程
gcc编译C语言程序,可以分为以下几步:
预处理:
\qquad检查宏定义与预处理指令,并进行转换
\qquad删除程序中的注释和多余的空白字符
\qquad生成与源文件同名的.i
文件(C文件)编译:
\qquad进行词法和语法分析{出现错误给出错误提示、中止编译没有错误将源代码翻译成可在目标机器上执行的汇编代码\begin{cases} 出现错误 & 给出错误提示、中止编译 \\ 没有错误 & 将源代码翻译成可在目标机器上执行的汇编代码 \\ \end{cases}{出现错误没有错误给出错误提示、中止编译将源代码翻译成可在目标机器上执行的汇编代码
\qquad生成与源文件同名的.s
文件(汇编文件)汇编:
\qquad将编译产生的汇编代码汇编成目标机器指令
\qquad生成与源文件同名的.o
文件(目标文件)链接:
\qquad将在一个文件中引用的符号同在另一个文件中该符号的定义链接起来,生成可执行程序
\qquad生成可执行文件(默认为a.out
)
2、GCC命令格式、常用参数
gcc命令的常用格式如下
gcc [命令参数] 文件名称
我习惯用的格式及其理解方式是
gcc [编译参数(进行到编译的哪一步)] 要编译的文件名 -o 要生成的文件名
参数有很多
上面-E
、-S
、-c
、(啥都不加)
就分别对应了执行到预处理
、编译
、汇编
、链接
结束
# 对test.c文件进行预编译,生成test.i文件gcc -E test.c -o test.i# 对test.c文件编译到汇编结束,生成test.o文件gcc -c test.c -o test.o# 对test.c文件一步编译到可执行程序gcc test.c -o myexe# 对test.c文件一步编译到可执行程序(不加-o默认生成可执行程序a.out)gcc test.c
gcc编译,可以从.c
文件开始,也可以从.i
文件、.s
文件之类的开始
就比如,以下两条gcc命令,只要被编译的test.c
或者test.i
文件正确存在、没有编译错误,都能成功生成test.s
文件
gcc -S test.c -o test.sgcc -S test.i -o test.s
另外,参数的位置也可以调换,因此,下面两条gcc命令是等价的
gcc -S test.i -o test.sgcc -S -o test.s test.i
我个人会觉得第一种方式好理解一点
\qquad用-S
的方式(编译到编译这一步)对test.i
文件进行编译,生成test.s
文件
第二种方式也能理解,就是感觉两个文件名堆一块不顺眼
\qquad用-S
的方式(编译到编译这一步)、以生成test.s
文件为目的,对test.i
文件进行编译
下面以一个C源程序(多个.c
、.h
文件)的案例来说明如何进行编译
hello.h
文件
/*hello.h*/#ifndef HELLO_H#define HELLO_Hvoid hello() {star1();printf("hello,my friends\n");} #endif
hello.c
文件
void showhello() {hello();}
starfun.h
文件
/*****starfun.h*****/#ifndef STARFUN_H#define STARFUN_H#define NUM 4#define NUMBER 3int star1() {int i,j,k;for(k=1;k<=NUM;++k) {for(i=1;i<=(NUM-k);++i)printf(" ");for(j=1;j<=(2*k-1);++j)printf("*");printf("\n");}return 0;}int star2() {int i,j,k;for(k=NUMBER;k>=0;--k) {for(i=1;i<=(NUMBER-k+1);++i)printf(" ");for(j=1;j<=(2*k-1);++j)printf("*");printf("\n");}return 0;}#endif
star.c
文件
#include "starfun.h"#include "hello.h"#include <stdio.h>int main() {star1();star2();showhello();return 0;}
一步编译
\;
使用以下命令,一步直接将两个.c
源文件一起编译生成可执行程序myexe
gcc star.c hello.c -o myexe
Tips:.h
文件在.c
文件里被引用,不需要用gcc对.h
文件做什么处理
\;多步编译
\;
使用以下命令,将两个.c
源文件分别预处理,生成相应的.i
文件
gcc -E hello.c -o hello.igcc -E star.c -o star.i
使用以下命令,将两个.i
文件分别编译,生成相应的.s
文件
gcc -S hello.i -o hello.s -wgcc -S star.i -o star.s -w
使用以下命令,将两个.s
文件分别汇编,生成相应的.o
文件
gcc -c hello.s -o hello.ogcc -c star.s -o star.o
使用以下命令,将两个.o
文件进行链接,生成相应的可执行程序文件
gcc hello.o star.o -o myexe
Tips:.h
文件在.c
文件里被引用,不需要用gcc对.h
文件做什么处理
\;
3、GCC创建库文件
在软件开发过程中,经常会使用外部或者其他模块提供的功能
这种功能经常以库文件的形式存在,主要分为{静态库动态库(共享库)\begin{cases} 静态库 \\ 动态库(共享库) \\ \end{cases}{静态库动态库(共享库)
(1).静态库
如果编译程序在编译使用库提供的功能代码的程序时,将代码复制到该程序然后编译成可执行程序,则这种库称为静态库
静态库的文件名必须以lib
开头,以.a
作为后缀
静态库生成、使用举例
calc.h
文件
double aver(double, double);double sum(double, double);
aver.c
文件
#include "calc.h"double aver(double num1, double num2){return (num1 + num2) / 2;}
sum.c
文件
#include "calc.h"double sum(double num1, double num2){return (num1 + num2);}
main.c
文件
#include <stdio.h>#include "calc.h"int main(){double v1, v2, m, sum2;v1 = 3.2;v2 = 8.9;m = aver(v1, v2);sum2 = sum(v1, v2);printf("%3.2f和%3.2f的平均数是%3.2f\n", v1, v2, m);printf("%3.2f和%3.2f的和是%3.2f\n", v1, v2, sum2);return 0;}
生成、使用静态库的步骤如下:
① 使用gcc生成.o
目标文件
gcc -c aver.c -o aver.ogcc -c sum.c -o sum.o
② 利用ar
命令生成静态库文件
ar rc libmycalc.a aver.o sum.o
参数r
表示将目标文件插入静态库中(如果之前静态库中已经有了与之同名的文件则删除之前的)
参数c
表示创建新的静态库文件
③ 使用gcc利用静态库编译程序
gcc main.c -L . -l mycalc -o myexe
示例截图
\qquad
(2).动态库(共享库)
共享库比静态库的处理方式更加灵活,因而其所生产的可执行文件更小
使用共享库链接的可执行文件只包含了它所需要的函数的表格,并没有从目标文件中复制全部的外部函数的机器代码
在可执行文件开始执行时,操作系统将外部函数的机器代码从磁盘上的共享库文件复制到内存中,这个过程称为动态链接
它使可执行程序更加精简而且节省磁盘空间,这是因为共享库可在多个程序之间共享:操作系统允许物理内存中共享库的一个复制被所有正在运行的程序使用,因此也能节省内存。
共享库使得程序员可根据需要随时更新库文件,只要接口不变,那么使用它的源程序就不需要重新编译
正是因为这些优点,当系统中同时存在静态库与共享库时,gcc会默认使用共享库文件
例如,当使用-I name
参数指定库名称时,gcc首先搜索在路径中是否有libname.so
共享库文件,如果有,则使用该文件;如果没有,则继续查找是否有libname.a
静态库文件
共享库的文件名必须以lib
开头,以.so
作为后缀(so
代表shared object
,共享的对象)
共享库生成、使用举例
calc.h
、aver.c
、sum.c
、main.c
文件如 静态库生成、使用举例 中所示
生成、使用共享库的步骤如下:
① 使用gcc生成与位置无关的.o
目标文件
gcc -c -fPIC aver.c -o aver.ogcc -c -fPIC sum.c -o sum.o
参数-fPIC
表示生成位置无关代码
\;② 使用gcc生成共享库
gcc -shared aver.o sum.o -o libmycalc.so
参数-shared
表示生成共享库
\;③ 使用gcc利用共享库编译程序
gcc main.c -L . -l mycalc -o myexe
④ 执行程序前,配置好库文件路径的环境变量LD_LIBRARY_PATH
export LD_LIBRARY_PATH=.
Tips:这种方式只能临时生效,关闭终端后需要重新设置。如果想要知道更多配置的方式,可以参考我开头推荐的博客 Linux基础——gcc编译、静态库与动态库(共享库)
\;
示例截图
\qquad
二、GDB调试器
1、GDB调试器的使用
在使用gcc对程序进行编译时,需要加上参数-g
,生成的可执行程序才能使用GDB进行调试
步骤如下:
先使用如下命令编译程序,并生成调试信息
gcc -g 要编译的c源程序 -o 要生成的可执行文件
然后使用如下命令,使用gdb进行调试
gdb 要生成的可执行文件
2、GDB常用命令
这个,我想很多人和我一样,在一开始学C++的时候,老师就有教过怎么在一些IDE(诸如{DevC++VC++6.0VSCodeblocksCLion⋯⋯\begin{cases} \textup{Dev \;C++} \\ \textup{VC++ \; 6.0} \\ \textup{VS \; } \\ \textup{Codeblocks} \\ \textup{CLion} \\ \cdots\cdots \\ \end{cases}⎩⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎧DevC++VC++6.0VSCodeblocksCLion⋯⋯)上进行调试
那么很多像是断点
、步过(Step over)
、步入(Step into)
、退出
、恢复运行
等概念,我也就不详说了
常用的GDB命令如下:
Tips:在gdb调试环境下,依然可以使用shell中的命令,比如想要清屏就可以输入shell clear
做到
演示就先不详写了吧,之前在IDE上调试过的人稍微操作操作就很容易理解了
想看部分简单的演示,可以到 上机作业 ·002【Linux常用工具GCC、GDB、Make】 里瞅瞅
三、Make工具
Make工具主要作用是自动化源程序项目的维护
Make工具根据Makefile文件的内容构建程序,该Makefile文件列出了每一个非源程序文件及如何从其他文件构造这些文件
关于Makefile编写与使用的详细的教程,我推荐这一系列的博文
跟我一起写Makefile
https://seisman.github.io/how-to-write-makefile/introduction.html
这里我只介绍最最简单、最最基础的makefile编写
1、Makefile编写规则
Makefile的规则的一般形式如下:
目标 : 依赖文件列表要执行的命令
Make工具从Makefile文件的第一个目标开始(该第一个目标也被称为默认目标),但在执行命令之前,make必须处理该规则所依赖的其他规则
例如,有makefile文件如下:
main : main1.o main2.ogcc main1.o main2.o -o mainmain1.o : main1.cgcc -c main1.c -o main1.omain2.o : main2.cgcc -c main2.c -o main2.o
第一个目标是main
,其所依赖的是main1.o
和main2.o
那就会先处理main1.o
和main2.o
的规则,它们又分别依赖main1.c
和main2.c
但main1.c
和main2.c
没有定义规则,于是分别执行main1.o
和main2.o
这两个目标对应的命令,利用gcc生成了这两个文件
处理完main1.o
和main2.o
的规则后,回头处理main
的规则,用gcc将main1.o
和main2.o
进行链接生成可执行程序main
按我的理解,makefile可以说只会直接处理第一个目标
例如,将上面的makefile文件写成下面这样:
main :gcc main1.o main2.o -o mainmain1.o :gcc -c main1.c -o main1.omain2.o :gcc -c main2.c -o main2.o
应该是会报错的,找不到main1.o
和main2.o
文件
它并不是一条规则一条规则地顺序处理下去,而是更像是递归地处理完第一个目标所有依赖的依赖的规则,再处理自己本身
Makefile简单使用举例
revertnum.c
文件
#include <stdio.h>void ShowRevertNum(int iNum) {while (iNum > 10){printf("%d", iNum % 10);iNum = iNum / 10;}printf("%d\n", iNum);}int main(void) {int iNum;while (1) {printf("Please input a number :");scanf("%d", &iNum);if (iNum <= 0) {break; }printf("After revert : ");ShowRevertNum(iNum);}}
创建makefile
文件,编辑内容如下
revertnum : revertnum.ogcc -g revertnum.o -o revertnumrevertnum.o : revertnum.sgcc -g -c revertnum.s -o revertnum.orevertnum.s : revertnum.igcc -g -S revertnum.i -o revertnum.s -wrevertnum.i : revertnum.cgcc -g -E revertnum.c -o revertnum.i
解释:
\qquad文件的第一个目标写明了整个项目的目标——生成可执行程序revertnum
\qquad生成可执行程序revertnum
需要处理revertnum.o
文件的依赖
\qquad生成revertnum.o
文件需要处理revertnum.s
文件的依赖
\qquad生成revertnum.s
文件需要处理revertnum.i
文件的依赖
\qquad生成revertnum.i
文件需要处理revertnum.c
文件的依赖
\qquadrevertnum.c
文件没有生成规则,根据命令gcc -g -E revertnum.c -o revertnum.i
生成revertnum.i
文件
\qquadrevertnum.s
的依赖已处理,根据命令gcc -g -S revertnum.i -o revertnum.s -w
生成revertnum.s
文件
\qquadrevertnum.o
的依赖已处理,根据命令gcc -g -c revertnum.s -o revertnum.o
生成revertnum.o
文件
\qquadrevertnum
的依赖已处理,根据命令gcc -g revertnum.o -o revertnum
生成可执行程序revertnum
\;
在终端使用make
命令维护项目,生成可执行程序revertnum
\qquad