当前位置: 首页 > article >正文

第五部分:Linux中的gcc/g++以及gdb和makefile

目录

1、编译器gcc和g++

1.1、预处理(进行宏替换)

1.2、编译(生成汇编)

1.3、汇编(生成机器可识别代码)

1.4、连接(生成可执行文件或库文件)

1.5、gcc编译器的使用

2、Linux调试器-gdb使用

2.1、debug和release

2.2、gdb的使用

3、自动化构建工具make/Makefile

3.1、背景

3.2、makefile/Makefile编写

3.3、make的使用

3.4、原理

4、一些补充


1、编译器gcc和g++

gccg++分别是C语言和C++语言的编译器,这两个编译器的使用基本上是一样的,所以在下面只讲gcc,也就是C语言的编译器。

一个写好的C语言程序,并不能立马被执行,需要经过以下几个过程,才能生成可执行程序。编译器gcc的作用就是将写好的C程序变成可执行程序。

1. 预处理(进行宏替换)

2. 编译(生成汇编)

3. 汇编(生成机器可识别代码)

4. 链接(生成可执行文件或库文件)

1.1、预处理(进行宏替换)

预处理功能主要包括宏定义,文件包含,条件编译,去注释等。

预处理指令是以#号开头的代码行,这个阶段不会对语法进行检测。

例如:

gcc -E test01.c -o test01.i

其中以.i为后缀的文件为已经过预处理的C原始程序。

我们可以使用vim打开test01.i文件,来查看经过预处理后代码发生了什么变化。

1.2、编译(生成汇编)

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

例如:

gcc -S test01.i -o test01.s

其中以.s为后缀的文件就是经过预处理和编译的C语言程序。 

我们也是可以使用vim打开test.s文件,来查看代码发生的变化。

1.3、汇编(生成机器可识别代码)

汇编阶段是把编译阶段生成的“.s”文件转成目标文件。

例如:

gcc -c test01.s -o test01.o

其中以.o为后缀的文件就是经过预处理、编译、汇编后生成的目标文件。

此时,如果我们使用vim打开test01.o文件的话就会看到一堆乱码,因为里面都是二进制,所以打开后会是乱码。我们可以使用od命令来查看二进制文件。

语法:od 文件

功能:将其内容以八进制字码呈现出来。

注意:虽然目标文件已经是二进制的文件了,但是仍然需要经过链接才能执行。

1.4、链接(生成可执行文件或库文件)

在成功编译之后,就该进入链接阶段。

例如:

gcc test01.o -o test01

 生成的test01文件就是可执行文件。可以使用:

./test01

 来执行该可执行程序。

一个重要的概念:函数库(关于细节后面的内容再详细讲)

我们的C程序中,并没有定义“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么是在哪里实“printf”函数的呢?(gcc和g++默认的头文件搜索路径就在/usr/include/)。

 答案是:头文件提供方法的声明,函数库提供方法的实现。系统把这些函数实现都放到了名为 libc.so.6的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径/usr/lib或者/usr/lib64下进行查找(/usr/lib主要包含32位库,而 /usr/lib64则包含64位库),也就是链接到 libc.so.6库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用。

函数库一般分为静态库动态库两种:

在Linux中动态库一般是以.so为后缀的,静态库是以.a为后缀的,在Windows中动态库一般以.dll为后缀,静态库一般以.lib为后缀。

静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。

静态链接的程序优点是不依赖库,程序可以独立进行运行,缺点是体积比较大,比较消耗资源。

注意:我们的云服务器上默认只会安装动态库,静态库默认是没有的。可以使用下面的命令安装静态库。

下载C语言静态库:

sudo yum install -y glibc-static

 下载C++静态库:

sudo yum install -y libstdc++-static

动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库。

动态链接的程序更加节省资源(磁盘资源、内存资源、网络资源等),缺点是动态库一旦缺失会导致各个程序都无法运行,因为会影响很多程序,包括Linux系统里面很多命令都没法运行。

如前面所述的 libc.so.6 就是动态库。gcc 在编译时默认使用动态库。

gcc默认生成的二进制程序,是动态链接的,这点可以通过之前讲的file命令验证。

例如:

file test01

 运行结果中“dynamically linked”就是指动态链接。

除了file命令外,还可以使用ldd命令来查看可执行程序所依赖的动态库,例如:

ldd test01

 从运行结果便可知道是静态链接还是动态链接。

1.5、gcc编译器的使用

语法:gcc [选项] 要编译的文件 [选项] [目标文件]

功能:编译C语言程序。

选项:

-E:只激活预处理。也就是让 gcc 在预处理结束后停止编译过程。

-S :该选项只进行编译而不进行汇编,生成汇编代码。

-c :编译到目标代码。使用选项“-c”就可看到汇编代码已转化为“.o”的二进制目标代码了。

-o :后面跟目标文件,指处理后输出到哪一个文件。

-static : 此选项对生成的可执行程序采用静态链接。

注意:如果没有动态库,只有静态库,也是可以使用gcc的,gcc是可以找到静态库的,其实默认情况下,形成的程序既不是纯的动态链接也不是纯粹的静态链接,而是混合式的。如果加上了-static选项就会变成纯的静态链接。

-g :生成调试信息。GNU 调试器可利用该信息。(该选项在下面的调试部分讲)

-w :不生成任何警告信息。

-Wall: 生成所有警告信息。

2、Linux调试器-gdb使用

2.1、debug和release

程序的发布方式有两种,debug模式和release模式

Linux gcc/g++编译出来的二进制程序,默认release模式

要使用gdb调试,必须在源代码生成二进制程序的时候, 加上-g选项。例如:

gcc test01.c -o test01 -g

也就是说在生成可执行程序时,要带上调试信息,才能使用gdb进行调试。 

扩展:可执行程序有自己的二进制格式,叫做ELF格式,我们可以使用readelf命令查看这种格式,例如:

readelf -S test01

2.2、gdb的使用

语法:gdb 可执行文件

功能:调试可执行程序。

退出: ctrl + dquitq

调试命令:

注意:gdb会自动记住历史命令,如果想要执行上一个命令,直接按enter键即可。

list 或 l +行号:显示可执行程序行号上下共10行源代码,默认每次接着上次的位置往下列,每次列10行。

list 或 l +函数名:列出某个函数的源代码。

r run:运行程序。

break b + 行号:在某一行设置断点。

break b + 函数名:在某个函数开头设置断点。

注意:还可以使用下面的方式进行打断点,表示给mycode.c文件的第19行打断点,通过这种方式就可以操作多文件的调试了,例如:

b mycode.c:19

n next:也就是逐过程执行。

s step:也就是逐语句执行。

info break info b或i b或i break:查看所有断点信息。

info break或 info b 或i b或i break+断点的编号 :查看某断点信息。

注意:断点的编号是指下图的Num下面所的序号。

finish:执行到当前函数返回,然后挺下来等待命令。当我们使用step进入某个函数后,通过观察发现这个函数没啥问题,现在想要从这个函数跳出去,而这个函数比较长,如果使用next命令单步执行到函数末尾的话就有点太慢了。 此时可以使用 finish 命令直接执行完这个函数,返回到被调用的地方。

printp +变量:打印表达式的值。

例如:

p i

 表示打印变量i的值。再比如:

p &i

打印变量i的地址。

set var + 变量名=值:修改变量的值。

例如:

set var i=10

 把 i 变量的值改为10。

注意:这个修改变量的值是在调试的过程中修改的,不会改变源代码和可执行程序。

continue c:也就是从一个断点执行到另一个断点。

delete breakpoints:删除所有断点。

delete breakpoints +断点的编号n:删除序号为n的断点。

disable breakpoints enable breakpoints:禁用或启用所有断点。

disable breakpoints enable breakpoints +断点编号:禁用或启用某个断点。

注意:禁用断点和删除断点不同,禁用只是不让断点发挥作用,但是断点还在;删除断点,断点就没了。

display 变量名:跟踪查看一个变量,每次停下来都显示它的值。

undisplay 序号:取消对先前设置的那些变量的跟踪。

注意:这个序号指的是变量名前面的序号,比如下面图片中的1。

until +行号:跳至某行,只能向后跳,不能向前跳。

bt:查看各级函数调用及参数,也就是查看函数的调用链。

info locals或i locals:查看当前栈帧局部变量的值。

3、自动化构建工具make/Makefile

3.1、背景

make是一个命令,makefile或Makefile是当前目录的一个文件,会不会写makefile或Makefile,从一个侧面说明了一个人是否具备完成大型工程的能力,一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile或Makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。

makefile带来的好处就是“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编 译,极大的提高了软件开发的效率。

make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE(集成开发环境)都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile或Makefile都成为了一种在工程方面的编译方法。

make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建。

3.2、makefile/Makefile编写

首先先创建一个makefile或Makefile文件,然后使用vim打开该文件进行编写。

例如:此处只是举个例子,实际编写完全可以写的更精简一些。

#定义变量    
cc=gcc    
option=-Wall -g -std=c99    
    
#定义目标    
target=test01    
object=test01.o    
    
#默认目标    
all:$(target)                                                                                                                                                                   
    
#依赖关系和依赖方法    
$(target):$(object)    
  $(cc) $(option) -o $@ $^     
    
%.o:%.c    
  $(cc) $(option) -c $<    
    
#清理目标    
#.PHONY:clean    
clean:    
  rm -f test01    

变量定义:

Makefile 中的变量可以在整个文件中使用,便于管理和修改编译器等等

使用=来定义变量,使用 $(NAME) 来引用变量。

例如:

CC = gcc            # 编译器  
option=-Wall -g -std=c99   # 编译选项

 再比如:

target=test01    #目标文件
object=test01.o  #依赖文件

 依赖关系和依赖方法

目标是你希望 make 命令帮助你生成的文件,如可执行文件等。

目标后面可以跟着一个冒号(:)和依赖项。

例如:

all: $(target)      # 默认目标

规则的格式为:

target: dependencies  
    command

特殊变量:

$@:目标名称。

$^:所有依赖项的名称(以空格分隔)。

$<:第一个依赖项的名称。

例如:

$(target): $(object)  
    $(CC) $(option) -o $@ $^

% 是一个通配符,表示相同的前缀。可以减少重复的代码,适用于一类文件的编译。

例如:

%.o: %.c  
    $(CC) $(CFLAGS) -c $<

clean是自定义的目标,不生成任何文件,通常用于清理构建过程生成的文件。

使用 -f 选项可避免错误提示。

例如:

clean:  
    rm -f $(TARGET) $(OBJECTS)

 包含其他文件

你可以使用include 指令包含其他 Makefile,这样可以组织更复杂的项目。

例如:

include common.mk

 伪目标

伪目标是指不生成文件的目标,通常用于执行清理操作等。

定义伪目标clean时可以用 .PHONY,这样即使该目录下有同名文件也不会出现混淆。

此外,.PHONY还可以用来修饰其他地方,表示总是可以被执行的(也就是说,不管源文件和目标文件的新旧关系如何,总是要执行的,具体内容看下面的原理部分)。

例如:

.PHONY:clean

3.3、make的使用

将makefile或Makefile编写好后,就可以使用make命令了。

make会自顶而下扫描makefile或Makefile文件,把所要形成的第一个目标文件充当为make的默认动作,也就是谁在前面,默认的动作就是谁。当然,我们也是可以使用make + 目标文件的这种方法来显示指定make的动作。

例如:

make

 表示执行第一个目标文件的动作。

 例如:

make clean

 表示执行清理动作。

3.4、原理

一般而言,一定是源文件形成可执行文件,先有源文件,再有可执行文件,所以一般而言,源文件的最近修改时间比可执行文件要老。如果我们更改了源文件,历史上曾经还有对应的可执行文件,那么源文件的最近修改时间一定比可执行程序新。make会根据源文件和目标文件的新旧,来判断是否需要重新执行对应的依赖关系和依赖方法。

简单来讲就是:如果源文件老于可执行文件,则不需要重新编译,make不会执行;如果源文件新于可执行文件,则需要重新编译,make会执行。

我们可以使用stat 文件名的方式来查看一个文件的相关时间,比如最近修改时间或最近访问时间,以及文件按最近变化的时间,例如:

stat test01.c

 注意:文件=文件内容+文件属性

在上面命令的运行结果中,其中modify所对应的时间是指文件内容的最近修改时间;change所对应的时间是指文件属性最近改变的时间;其中access对应的是最新的访问时间,但是受效率的影响,文件的访问时间并不会因为一次访问而改变,而是有着某种修改策略。

我们可以使用touch 已存在文件的方式把上面的三种时间都改为当前时间,例如:

touch test01.c

 关于touch更多的使用方式,可以自行查阅。

1. make会在当前目录下找名字叫“Makefile”或“makefile”的文件。

2. 如果找到,它会找文件中的第一个目标文件(target)。

3. 如果目标文件不存在,或是所依赖的文件的文件修改时间要比目标文件新,那么,他就会执行后面所定义的命令来生成目标文件。

4. 如果目标文件所依赖的文件不存在,那么make会在当前文件中找目标文件所依赖的文件的依赖性,如果找到则再根据那一个规则生成目标文件的依赖文件。

5. 这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。

6. 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错, 而对于所定义的命令的错误,或是编译不成功,make根本不管。

7. make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起, 我就不工作啦。

8.此外,工程是需要被清理的,像clean这种,没有与第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令——“make clean”,以此来清除所有的目标文件,以便重编译。如果我们把clean放在了makefile或Makefile的开头的话,也是可以直接使用make来执行清理工作的。

注意:make执行的时候,会回显执行的命令,如果不想要回显的话,可以在依赖方法前加上@符号,这样就不会回显所执行的方法了。

注意.PHONY修饰clean是为了避免该目录下存在同名文件而导致make clean无法执行,修饰其他地方是为了make可以重复执行,而不考虑时间问题,也就是说总是可以执行的。

4、一些补充

回车和换行不是一个概念:

回车是指光标移动到当前行的最左侧。

换行是指光标移动到下一行。

在C语言中,'\n' 其实执行的是回车+换行;'\r' 执行的是回车。

文件缓冲区概念(缓冲区的具体概念后面再讲),所谓的文件缓冲区就是内存的一块区域,从内存向磁盘输出的数据会先到缓冲区,缓冲区装满后或者刷新缓冲区再把数据送到磁盘上。(之前的C语言文章提过一点,可以去看C语言文件操作)。

例如:

#include<stdio.h>    
#include<unistd.h>    
    
int main()    
{                                                                                                                                                                               
  printf("hello world");    
  sleep(10);    
  return 0;    
}   

 编译运行后,我们会看到hello world并未立即输出到显示屏上,而是等待了10秒后,才显示出来。我们可以使用fflush()函数来刷新缓冲区,例如:

#include<stdio.h>    
#include<unistd.h>    
    
int main()    
{  
  printf("hello world");  
  fflush(stdout);                                                                                                                                                               
  sleep(10);                                                                                                                
  return 0;                                                                                                                 
}      

编译运行后,便会观察到hello world会立马输出出来。

其中sleep和fflush函数可以通过我们之前学过的man命令来查看如何使用。

注意:我们现在写的一些程序用的是linux的接口,这就导致我们写的一些程序是没法在Windows上的VS上面运行的,要注意这一点,今后有关Linux的文章基本都是如此。


http://www.kler.cn/a/518806.html

相关文章:

  • 详解磁盘IO调度算法与页高速缓存的搭配
  • 【图文详解】lnmp架构搭建Discuz论坛
  • Kafka 深入服务端 — 时间轮
  • 【电磁兼容】CE 传导骚扰
  • C++11新特性之auto与decltype(总结)
  • [java] 面向对象进阶篇1--黑马程序员
  • React第二十五章(受控组件/非受控组件)
  • AI对齐服务:从7.5亿美元市场到创新转型
  • 罗氏线圈的学习【一】
  • 多线程详解——Kotlin多线程几种实现方式
  • 2024年CSDN年度回顾:个人成长、创作历程与生活的融合与平衡
  • 在Ubuntu上使用Apache+MariaDB安装部署Nextcloud并修改默认存储路径
  • 编码器和扩散模型
  • centos搭建docker registry镜像仓库
  • Alibaba Spring Cloud 十六 Sentinel 流量控制
  • Qt Designer and Python: Build Your GUI
  • fpga系列 硬件:FPGA 最小系统参考图与图释+Zynq-7010 最小系统Zynq-7010 启动配置
  • 解锁 MySQL 数据库的无限潜能:全方位深度解析
  • 容器内判断当前的运行环境是docker还是podman
  • 从曾国藩的经历看如何打破成长中的瓶颈
  • 【算法】数论基础——唯一分解定理(算术基本定理)python
  • ES6 类语法:JavaScript 的现代化面向对象编程
  • 前端开发学习路线
  • 【信息系统项目管理师-选择真题】2017下半年综合知识答案和详解
  • 在java java.util.Date 已知逝去时间怎么求年月日
  • Spring AOP通知类型全解析:掌握方法执行前后的艺术