在C语言 程序员内功心法之程序环境和预处理 博文中,我们就学习到 – 一个程序要被运行起来需要经历四个阶段:预处理 (预编译)、编译、汇编、链接,下面我们来简单回顾一下这四个阶段会进行的操作。
预处理也叫预编译,程序在预处理阶段会完成如下操作:
在Linux下我们可以通过如下命令来得到预处理之后的代码:
gcc -E test.c -o test.i
# gcc:表示用 gcc 编译器来编译此代码
# -E:表示让代码在完成预处理后停下来,不再继续往后编译
# test.c:我们要编译的代码
# test.i 预处理产生的文件一般以.i为后缀
# -o test.i:用于指明临时文件的名称(test.i),它会将预处理之后的代码保存到指明的临时文件中,而不是直接打印到终端上
//测试案例 -- test.c
#include <stdio.h>
#define M 1024
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("%d\n", M);
//测试条件编译
#ifdef SHOW
printf("hello SHOW\n");
#else
printf("hello DEFAULT\n");
#endif
return 0;
}
可以看到,预处理后 stdio.h 里面的内容会被拷贝到 test.i 里面,所以 test.i 一共有800多行;同时,预处理阶段也完成了我们上述提到的其他工作。
程序在编译阶段会完成如下操作:
在Linux下我们可以通过如下命令来得到编译之后的代码:
gcc -S test.i -o test.s
# -S:表示让代码在完成编译后停下来,不再继续往后编译
# 编译产生的文件一般以.s为后缀
可以看到,编译阶段会将高级语言转换为汇编语言。
汇编阶段是把编译阶段生成汇编代码转成计算机可以识别的二进制目标代码,其中生成的 .o 文件被称为可重定向二进制目标文件。
在Linux下我们可以通过如下命令来得到编译之后的代码:
gcc -c test.s -o test.o
# -c:表示让代码在完成编译后停下来,不再继续往后编译
# 汇编产生的文件一般以.o为后缀
如上,汇编得到的二进制目标文件使用一般的文本编辑器打开时是一堆我们看不懂的符号 (与符号的编码有关 – utf-8),我们可以使用 od 指令以指定格式来打开它 (默认是以八进制打开)。
程序在链接阶段会完成如下操作:
在Linux中,链接我们直接使用 gcc 即可,没有额外选项,因为链接是程序的最后一个阶段;同时,链接的结果默认存放在 a.out 中。
gcc test.o -o test.out
链接得到的文件被称为可执行程序,它里面存放的也是计算机能够识别的二进制指令。
注:gcc 预处理编译链接三个阶段对应的选项和文件后缀有一个记忆技巧 – ESc 与 iso,其中 ESc 分别代表 -E -S -c,iso 分别代表 .i,.s,.o;ESc 可以对比电脑上的 Esc 键,将其中的 s 改为大写即可;iso 可以对比苹果 ios,将 o 和 s 的顺序调换即可。
同时,我们此处将 gcc 编译代码分为预处理、编译、汇编、链接四个阶段是为了让大家更深层次的理解一个程序的运行过程;日常编译代码的时候直接使用 “gcc test.c -o test.out” 或 “gcc test.c” 编译源文件得到可执行程序即可。
我们在编写代码的时候,除了自己实现函数之外,我们还会去调用函数库中的代码,比如 scanf/printf/malloc/fopen;但是我们要明白,库中的代码是别人给我们写好供我们直接使用的,即我们只有该函数的调用,而没有函数的实现;
同时,程序在预处理、编译和汇编阶段处理的都是我们自己编写的代码,只有在链接的时候,库函数的实现才会和我们的代码关联起来 (符号表的重定位);所以,链接的本质是我们在调用库函数时如何与标准库相关联的问题。
程序一共有两种链接方式:动态链接与静态链接;
动态链接是指执行代码时,如果遇到库函数调用就跳转到动态库中对应函数的定义处,然后执行该函数,执行完毕后再跳转回原程序并继续往下执行;它的优点是形成的可执行程序小,缺点是受到动态库变动 (删除、升级等) 的影响。 静态链接则是直接将本程序内部要使用的库函数从对应的静态库中拷贝一份过来;它的优点是不与静态库产生关联,即不受静态库变动 (删除、升级等) 的影响;缺点是形成的可执行程序非常大。
函数库是一些事先写好的,用于给别人复用的函数的集合,函数库一般分为静态库和动态库两种:
静态库是指编译链接时,把需要的库文件代码全部拷贝到可执行文件中,因此生成的文件非常大,但在运行时也就不再需要库文件了,在Linux下其后缀名为 “.a”,在Windows下其后缀名为 “.lib”; 动态库也被称为共享库,它与静态库相反,在编译链接时并没有把相应的库文件代码加入到可执行文件中,而是在程序执行时由运行时链接文件来加载库,这样可以节省系统的开销,在Linux下其后缀名为 “.so”,在Windows下其后缀名为 “.dll”; 注:动态链接必须使用动态库,静态链接必须使用静态库;即进行动态链接时只能跳转到动态库中对应函数的实现处,进行静态链接时只能拷贝静态库中的函数。
Linux中默认使用动态库进行动态链接,原因如下:
Linux 一般都会自动安装C语言动态库,因为Linux下的大多数指令以及我们默认使用 gcc 编译得到的可执行程序都是进行动态链接,依赖C动态库的;但是C静态库、C++静态库可能就需要我们自己安装了。
我们可以使用如下命令来安装C和C++静态库:
sudo yum install -y glibc-static
sudo yum install -y libstdc++-static
我们也可以使用 “-static” 选项来指定程序使用静态方式来进行链接:
可以看到,以静态链接方式形成的可执行程序比动态链接形成的要大100~200倍,即一个动态链接只有100M的文件,静态链接就会变成十几个G,二者之间相差非常大。
其实上面在学习预处理、编译、汇编、链接时我们就已经在使用 gcc/g++ 了,只是比较零散,下面我们来系统的学习一下 gcc/g++。
gcc/g++ 的安装
在Linux下,我们可以使用如下指令来安装 gcc 与 g++:
sudo yum install -y gcc
yum install -y gcc-c++ libstdc++-devel
gcc/g++ 的使用
gcc 和 g++ 的使用方法非常类似,因为他们的选项基本都是一样的:
在Windows中使用VS的时候我们知道:程序的发布方式一共有两种 – debug 模式和 release 模式;其中 debug 模式是给程序员用的,其中包含调试信息,程序员可以根据这些调试信息对程序进行修改与完善;而 release 模式则是给用户用的,它不包含调试信息,因为用户不负责也不关心如何对程序进行调试。
Linux 中使用 gcc/g++ 编译链接得到的程序默认是 release 模式的,如果我们要使用 gdb 进行调试,必须在源代码生成二进制程序的时候添加 -g 选项;
//测试代码
#include <stdio.h>
int Add(int begin, int end)
{
int ret = 0;
int i = 0;
for(i=begin; i<=end; ++i)
{
ret += i;
}
return ret;
}
int Fac(int x)
{
int ret = 1;
int i = 0;
for(i=1; i<=x; ++i)
{
ret *= i;
}
return ret;
}
int main()
{
int a = 0;
int b = 100;
int add = Add(a, b);
printf("ret = %d\n", add);
printf("ret = %d\n", add);
printf("ret = %d\n", add);
int fac = Fac(10);
printf("fac = %d\n", fac);
printf("fac = %d\n", fac);
printf("fac = %d\n", fac);
return 0;
}
可以看到,以 debug 和 release 模式发布的程序无论是在程序大小、程序内部包含的有关调试的二进制信息,还是 gdb 模式下是否具有调试样例都是有明显区别的。
当我们指定 -g 得到以 debug 模式发布的可执行程序后,我们就可以使用 gdb 对其进行调试了;
gdb 的安装
在Linux下,我们可以使用如下命令来安装 gdb:
sudo yum install -y gdb
gdb 调试的常见选项如下:
指令演示
l 行号显示源代码:
l 函数显示该函数的源代码:
r 运行程序:
b 行号打断点,info b 查看断点,d 断点编号 删除断点:
r 调试运行:
n 逐过程调试,s 逐语句调试:
c 运行至下一个断点处停下:
bt 查看调用堆栈:
p 变量 查看变量值,display 跟踪查看变量,undisplay 取消跟踪:
finish 把当前函数运行完,q 退出 gdb: