文章目录
- 一、Linux编译器-gcc/g++
- 1.程序运行的四个阶段
- 1.1 预处理
- 1.2 编译
- 1.3 汇编
- 1.4 链接
- 2.链接方式与函数库
- 2.1 动态链接与静态链接
- 2.2 动态库与静态库
- 3.gcc/g++的使用
- 二、Linux调试器--gdb
- 1.debug与release
- 2.gdb 的使用
一、Linux编译器-gcc/g++
1.程序运行的四个阶段
我们知道,一个程序被运行起来之后需要经历四个阶段然后才变成一个可执行的程序,他们分别为:预处理,编译,汇编和链接。下面我们将介绍这四个阶段的操作,测试代码如下:
#include <stdio.h>
// 宏定义
#define N 10
int main()
{
// 测试注释
// printf("hello 1\n");
// printf("hello 2\n");
// printf("hello 3\n");
// printf("hello 4\n");
printf("hello 5\n");
printf("hello 6\n");
printf("hello 7\n");
printf("hello 8\n");
printf("hello 9\n");
printf("hello 10\n");
// 测试宏
printf("%d\n", N);
// 测试条件编译
#ifdef SHOW
printf("hello SHOW\n");
#else
printf("hello DEFAULT\n");
#endif
return 0;
}
1.1 预处理
预处理也叫预编译,程序在预处理阶段会完成如下操作:
展开头文件,将头文件中的代码拷贝到当前代码中
删除注释
执行条件编译
完成#define定义的符号,宏的替换以及删除
在Linux下我们可以通过如下指令就可以得到经过预处理之后的代码
gcc -E test.c -o test.i
gcc : 表示使用gcc编译器来编译该代码
-E :表示让代码完成预处理之后就停下来
test.c : 我们要编译的代码
test.i:预处理产生的文件一般以.i为后缀
-o test.i :用于指明临时文件的名称(test.i),经过预处理之后它会形成一个名称为test.i的临时文件,而不是输出到终端上
我们可以看到,经过预处理之后,头文件stdio.h中的内容全部会被拷贝到test.i中,所以test.i一个有800多行,我们注释的内容也全部被删除,宏定义被替换,条件编译被执行
1.2 编译
程序在编译阶段会完成如下操作:
语法分析
词法分析
语义分析
符号汇总
我们通过如下指令来获取程序编译之后的代码:
gcc -S test.i -o test.s
-s:表示让代码在完成编译之后停下来,不再继续之后后面的过程
编译产生的文件一般以.s为后缀
我们可以看到,编译阶段会将高级语言转换成汇编语言
1.3 汇编
汇编阶段会将编译阶段生成的汇编代码转换成计算机可以识别的二进制目标代码,其中生成的.o文件被称为可重定向的二进制目标文件
我们通过如下指令来获取程序汇编之后的代码:
gcc -c test.o -o test.s
-c:表示让代码在完成汇编之后停下来,不再继续往后执行
汇编产生的文件一般以.s为后缀
这样我们就得到了二进制目标文件,但我们使用一般的文本编辑器打开发现是一堆我们看不见的符号,我们可以通过如下指令以指定的格式打开它(默认是八进制):
od test.o
1.4 链接
程序在链接的过程中会完成如下操作:
1.合并段表,编译器会把汇编阶段生成的多个文件中相同格式的数据合并到一起,最终形成一个.exe文件
2.符号表的合并和重定位,符号表的合并是编译器会把在汇编阶段生成的多个符号表合成一个符号表,重定位是当同一个符号出现在两个符号表中时,编译器会选取其中一个有效的地址相关的一个,舍弃另外一个
在Linux中,链接我们直接使用gcc即可,如果我们没有指定将生成的临时文件的话,默认会将结果存放在a.out中
gcc test.o //默认
gcc test.c -o test.out // 保存到test.out文件中
我们最后通过链接得到的文件称为可执行程序,它里面保存的也是计算机能够识别的二进制指令
我们两个文件都可以输出最终的结果:
虽然我们将gcc编译代码分为预处理,编译,汇编和链接,但是我们在日常生活中运行代码时,直接使用"gcc test.c -o test.out"或者"gcc test.c"即可,这里分为四个阶段是为了让我们更好的理解程序的运行过程
2.链接方式与函数库
2.1 动态链接与静态链接
我们的C程序中,并没有定义“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现“printf”函数的呢?
最后的答案是:系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径“/usr/lib”下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用
这是因为我们在编写代码的时候,除了自己实现的函数外,比如printf函数我们会去调用函数库中的代码,但是stdio.h里只有函数的声明,没有函数的实现。此外,程序在预处理,编译和汇编阶段处理的都是我们自己的代码,只有在链接的时候,库函数的实现才会和我们的代码关联起来(符号表的重定位),所以,链接的本质是我们在调用库函数的时候如何与标准库相关联的问题,程序一共有两种链接方式:动态链接和静态链接
动态链接
动态链接是指执行代码的时候,如果遇到库函数调用就跳转到动态库中对应的函数的定义的地方,然后执行该函数,执行完毕后再跳转到原程序处然后继续往后执行,它的优点就是形成的可执行程序小,缺点是受动态库变动(删除,升级等)的影响
静态链接
静态链接是直接将程序需要使用的库函数都从对应的静态库中拷贝一份到原程序中,它的优点是不受静态库(删除,升级等)的影响,缺点是形成的可执行程序大
2.2 动态库与静态库
函数库是一些事先写好的,用于给别人使用的函数的集合,函数库分为静态库和动态库
静态库
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。在Linux在其后缀名一般为“.a“,在windows在的后缀名为”.lib“
动态库
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。在Linux下动态库一般后缀名为“.so”,在windows下其后缀名为“.dll”
【注意】
1.动态链接必须使用动态库,静态链接必须使用静态库,即进行动态链接时只能跳转到动态库中对应的函数实现处,进行静态链接时只能拷贝静态库中的函数实现
2.gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文件,gcc默认生成的二进制程序,是动态链接的.原因如下:
- 如果使用静态链接,那么程序形成的可执行程序大,此时,不仅仅占用磁盘的空间大,其被运行时加载到内存所占用的空间也会很大,而我们的内容有限,运行过大的程序可能会导致卡死无法运行
- 虽然动态链接受库函数变动的影响,但是库函数一般是很少变动的,即使变动也会兼容以前的版本,所以对程序的影响不大
- 在Linux下,我们可以使用"file"指令来辨别文件的类型,使用"ldd"来打印或查看程序运行所 需要的共享库
Linux一般会自动安装C语言动态库,因为Linux下的大多数指令以及我们默认使用的gcc编译得到的可执行程序都是动态链接的,依赖于C动态库,但是C静态库,C++静态库可能就需要我们自己进行安装。我们可以使用如下命令来安装C和C++的静态库:
sudo yum install -y glibc-static
sudo yum install -y libstdc++-static
我们可以使用"-static"选项来指定程序使用静态方式进行链接:
我们可以看到,使用静态链接方式形成的可执行程序比动态链接方式形成的可执行程序的要大100倍左右,即一个动态链接只有1M的文件,使用静态链接方式形成的可执行程序就有几百M,两者之间相差非常大,所以Linux默认使用静态链接的方式
3.gcc/g++的使用
在Linux在,我们可以使用如下指令来安装gcc和g++:
sudo yum install -y gcc
sudo yum install -y gcc-g++ libstdc++-devel
gcc与g++的使用如下:
-E 只激活预处理,在完成预处理后停下来,不再往后继续执行,生成以.i为后缀的文件
-S 在完成编译后停下来,不再继续往后执行,生成的文件以.o为后缀 编译到汇编语言不进行汇编和链接
-c 在完成汇编之后停下来,不再继续往后执行,生成以.o为后缀的文件 编译到目标代码
-o 将输出到终端的内容保存到指定的文件中
-static 此选项对生成的文件采用静态链接
-g 以debug的方式发布软件,生成调试信息。GNU 调试器可利用该信息。
-shared 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库.
-O0-O1-O2-O3 编译器的优化选项的4个级别
-O0表示没有优化,这是默认的编译选项
-O1为缺省值,编译器会在不花费太多编译时间的同时试图生成更快更小的代码。这些优化是非常基础的,但一般这些任务肯定能顺利完成
-O2 -O2会比-O1启用多一些标记。设置了-O2后,编译器会试图提高代码性能而不会增大体积和大量占用的编译时间
-O3优化级别最高 在O2的基础上进行更多的优化,用这个选项会延长编译代码的时间,并且在使用gcc4.x的系统里不应全局启用
-w 不生成任何警告信息。
-Wall 生成所有警告信息。
二、Linux调试器–gdb
1.debug与release
在windows中使用VS的时候我们知道,程序的发布方式一共有两种-debug模式和release模式,其中debug模式是给程序员使用的,其中包含调试的各种信息,程序员可以根据这些调试信息对程序进行修改和完善,而release模式则是给用户使用的,它不包含调试信息,因为用户不负责也不关心对程序进行调试。
Linux gcc/g++出来的二进制程序,默认是release模式,如果我们要使用gdb调试,必须在源代码生成二进制程序的时候, 加上 -g 选项,标识以debug方式发布
#include <stdio.h>
#include <time.h>
int AddToVal(int from,int to)
{
int sum=0;
int i=0;
for(i=from;i<=to;++i) {
sum+=i;
}
return sum;
}
void Print(int sum)
{
long long timestamp=time(NULL);
printf("result=%d,timestamp:%lld\n",sum,timestamp);
}
int main()
{
int sum=AddToVal(0,100);
Print(sum);
return 0;
}
我们可以发现,以debug方式发布的和以release方式发布的程序无论是在大小,程序内部的包含的相关的调试信息,还是debug模式下是否具有调试样例都是由明显区别的
2.gdb 的使用
当我们使用-g选项的时候就可以得到以debug模式发布的可执行程序,然后我们就可以使用gdb对其进行调试了:
gdb的安装
在Linux在,我们可以使用如下指令来安装gdb:
sudo yum install -y gdb
gdb调试的常见选项如下:
list/l 行号:显示binFile源代码,接着上次的位置往下列,每次列10行。我们第一次使用l显示源代码后,我们下一次再使用l或者下一次使用enter时,它会接着上一次的位置接着向下显示
list/l 函数名:列出某个函数的源代码。
r或run:运行程序。调试运行,如果程序中有断点,则在断点处停下来,如果没有,则直接 将程序执行完毕,相当于VS中的F5
n 或 next:单条执行。逐过程调试,相当于VS的F10
s或step:进入函数调用 逐语句调试,相当于VS的F11
break(b) 行号:在某一行设置断点,相当于VS的F9
break 函数名:在某个函数开头设置断点
info break :查看断点信息。
finish:执行到当前函数返回,然后停下来等待命令 把当前函数执行完毕
print§:打印表达式的值,通过表达式可以修改变量的值或者调用函数
p 变量:打印变量值。
set var:修改变量的值
continue(或c):从当前位置开始连续而非单步执行程序 运行至下一个断点处停下来,如果断点所在行不是一条语句,比如"{" “}” 或者空行,那么它会继续往后继续执行到有效行处
run(或r):从开始连续而非单步执行程序
delete breakpoints:删除所有断点
delete breakpoints n:删除序号为n的断点 (d 断点编号),每个断点都有自己的编号,我们删除断点时需要指明对应的断点编号
disable breakpoints:禁用断点
enable breakpoints:启用断点
info(或i) breakpoints:参看当前设置了哪些断点
display 变量名:跟踪查看一个变量,每次停下来都显示它的值
undisplay:取消对先前设置的那些变量的跟踪
until X行号:跳至X行
breaktrace(或bt):查看各级函数调用及参数
info(i) locals:查看当前栈帧局部变量的值
quit:退出gdb
我们重点掌握下面的即可:
指令演示
l行号显示源代码
l 函数显示该函数的源代码
r 运行程序
b 行号打断点,info b查看断点,d 断点编号 删除断点
r 调试运行
n 逐过程调试,s逐语句调试
c 运行至下一个断点处停下
bt 查看调用堆栈
p 变量,查看变量值,display 跟踪查看变量 undisplay取消跟踪
finish 把当前函数运行完,q 退出gdb1