
一、Linux编译器 - gcc/g 使用1.1 引言在C语言《程序员内功心法之程序环境和预处理》博文中我们就学习到 – 一个程序要被运行起来需要经历四个阶段预处理 (预编译)、编译、汇编、链接下面我们来简单回顾一下这四个阶段会进行的操作。1.2 预处理预处理也叫预编译程序在预处理阶段会完成如下操作注释的删除。#define 定义的符号、宏的替换以及删除。条件编译的执行。头文件的包含将头文件中的代码拷贝到当前代码中来。在Linux下我们可以通过如下命令来得到预处理之后的代码gcc-Etest.c-otest.i# gcc表示用 gcc 编译器来编译此代码# -E表示让代码在完成预处理后停下来不再继续往后编译# test.c我们要编译的代码# test.i 预处理产生的文件一般以.i为后缀# -o test.i用于指明临时文件的名称(test.i)它会将预处理之后的代码保存到指明的临时文件中而不是直接打印到终端上具体我们后面系统学可以先看//测试案例 -- test.c#includestdio.h#defineM1024intmain(){//测试注释//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);//测试条件编译#ifdefSHOWprintf(hello SHOW\n);#elseprintf(hello DEFAULT\n);#endifreturn0;}可以看到预处理后 stdio.h 里面的内容会被拷贝到 test.i 里面所以 test.i 一共有800多行同时预处理阶段也完成了我们上述提到的其他工作。1.3 编译程序在编译阶段会完成如下操作语法分析。词法分析。语义分析。符号汇总。在Linux下我们可以通过如下命令来得到编译之后的代码gcc-Stest.i-otest.s# -S表示让代码在完成编译后停下来不再继续往后编译# 编译产生的文件一般以.s为后缀可以看到编译阶段会将高级语言转换为汇编语言(有点像vs的反汇编)。1.4 汇编汇编阶段是把编译阶段生成的汇编代码转成计算机可以识别的二进制目标代码其中生成的 .o 文件被称为可重定向二进制目标文件。在Linux下我们可以通过如下命令来得到汇编之后的代码gcc-ctest.s-otest.o# -c表示让代码在完成汇编后停下来不再继续往后编译# 汇编产生的文件一般以.o为后缀如上汇编得到的二进制目标文件使用一般的文本编辑器打开时是一堆我们看不懂的符号 (与符号的编码有关 – utf-8)我们可以使用 od 指令以指定格式来打开它 (默认是以八进制打开)。1.5 链接程序在链接阶段会完成如下操作合并段表编译器会把在汇编阶段生成的多个目标文件中相同格式的数据合并在一起最终形成一个 .exe 文件。符号表的合并和重定位符号表的合并是指编译器会把在汇编阶段生成的多个符号表合并为一个符号表重定位则是指当同一个符号出现在两个符号表中时编译器会选取其中和有效地址相关的那一个舍弃另外一个。在Linux中链接我们直接使用 gcc 即可没有额外选项因为链接是程序的最后一个阶段同时链接的结果默认存放在 a.out 中。gcc test.o-otest.out链接得到的文件被称为可执行程序它里面存放的也是计算机能够识别的二进制指令。注gcc 预处理编译链接三个阶段对应的选项和文件后缀有一个记忆技巧 – ESc 与 iso其中 ESc 分别代表 -E -S -ciso 分别代表 .i.s.oESc 可以对比电脑上的 [Esc] 键将其中的 c 改为小写即可iso 就是系统镜像同时我们此处将 gcc 编译代码分为预处理、编译、汇编、链接四个阶段是为了让大家更深层次的理解一个程序的运行过程日常编译代码的时候直接使用gcc test.c -o test.out或gcc test.c编译源文件得到可执行程序即可。二、链接方式与函数库2.1 动态链接与静态链接我们在编写代码的时候除了自己实现函数之外我们还会去调用函数库中的代码比如scanf/printf/malloc/fopen但是我们要明白库中的代码是别人给我们写好供我们直接使用的即我们只有该函数的调用而没有函数的实现同时程序在预处理、编译和汇编阶段处理的都是我们自己编写的代码只有在链接的时候库函数的实现才会和我们的代码关联起来 (符号表的重定位)所以链接的本质是我们在调用库函数时如何与标准库相关联的问题。程序一共有两种链接方式动态链接与静态链接动态链接是指执行代码时如果遇到库函数调用就跳转到动态库中对应函数的定义处然后执行该函数执行完毕后再跳转回原程序并继续往下执行它的优点是形成的可执行程序小缺点是受到动态库变动 (删除、升级等) 的影响。静态链接则是直接将本程序内部要使用的库函数从对应的静态库中拷贝一份过来它的优点是不与静态库产生关联即不受静态库变动 (删除、升级等) 的影响缺点是形成的可执行程序非常大。2.2 动态库与静态库函数库是一些事先写好的用于给别人复用的函数的集合函数库一般分为静态库和动态库两种静态库是指编译链接时把需要的库文件代码全部拷贝到可执行文件中因此生成的文件非常大但在运行时也就不再需要库文件了在Linux下其后缀名为 “.a”在Windows下其后缀名为 “.lib”动态库也被称为共享库它与静态库相反在编译链接时并没有把相应的库文件代码加入到可执行文件中而是在程序执行时由运行时链接文件来加载库这样可以节省系统的开销在Linux下其后缀名为 “.so”在Windows下其后缀名为 “.dll”库分为动态库和静态库库有自己的命名风格libname.so.xxx或libname.a.xxx这中间对应文件的name区域那么即为库的名其中xxx为库的不同版本例如c语言中的库name对应就为c那么就为c标准库注动态链接必须使用动态库静态链接必须使用静态库即进行动态链接时只能跳转到动态库中对应函数的实现处进行静态链接时只能拷贝静态库中的函数。Linux中默认使用动态库进行动态链接原因如下程序形成的可执行程序大不仅仅是占用的磁盘空间大其被运行时加载到内存所占用的内存空间也会非常大而目前我们使用的机器的内存基本上都是8/16GB的所以运行不了过大的可执行程序虽然动态链接受函数库变动的影响但是函数库一般很少会变动即使变动也必须兼容以前的版本所以影响不大在Linux下我们可以使用file指令来辨识文件类型使用ldd来打印或者查看程序运行所需的共享库。Linux 一般都会自动安装C语言动态库因为Linux下的大多数指令以及我们默认使用 gcc 编译得到的可执行程序都是进行动态链接依赖C动态库的但是C静态库、C静态库可能就需要我们自己安装了。我们可以使用如下命令来安装C和C静态库(因为涉及在系统位置安装要加sudo)我们也可以在 gcc 中使用-static选项来指定程序使用静态方式来进行链接gcc test.c-otest.exe-static可以看到以静态链接方式形成的可执行程序比动态链接形成的要大100~200倍即一个动态链接只有100M的文件静态链接就会变成十几个G二者之间相差非常大。三、gcc/g 的使用3.1 gcc/g 的安装其实上面在学习预处理、编译、汇编、链接时我们就已经在使用 gcc/g 了只是比较零散下面我们来系统的学习一下 gcc/g。在Linux下我们可以使用如下指令来安装 gcc 与 gsudoyuminstall-ygcc yuminstall-ygcc-c libstdc-devel3.2 gcc/g 的使用gcc 和 g 的使用方法非常类似因为它们的选项基本都是一样的-E在完成预处理后停下来不再继续往后编译生成的文件以 .i 为后缀-S在完成编译后停下来不再继续往后编译生成的文件以 .s 为后缀-c在完成汇编后停下来不再继续往后编译生成的文件以 .o 为后缀-o将输出到终端的内容保存到指定文件中-static对生成的文件采用静态链接-g以debug方式发布软件即生成调试信息GNU 调试器可利用该信息-shared尽量使用动态库需要系统存在动态库四、Linux调试器 - gdb 使用4.1 debug 和 release在Windows中使用VS的时候我们知道程序的发布方式一共有两种 – debug 模式和 release 模式其中 debug 模式是给程序员用的其中包含调试信息程序员可以根据这些调试信息对程序进行修改与完善而 release 模式则是给用户用的它不包含调试信息因为用户不负责也不关心如何对程序进行调试。Linux 中使用 gcc/g 编译链接得到的程序默认是 release 模式的如果我们要使用 gdb 进行调试必须在源代码生成二进制程序的时候添加 -g 选项//测试代码#includestdio.hintAdd(intbegin,intend){intret0;inti0;for(ibegin;iend;i){reti;}returnret;}intFac(intx){intret1;inti0;for(i1;ix;i){ret*i;}returnret;}intmain(){inta0;intb100;intaddAdd(a,b);printf(ret %d\n,add);printf(ret %d\n,add);printf(ret %d\n,add);intfacFac(10);printf(fac %d\n,fac);printf(fac %d\n,fac);printf(fac %d\n,fac);return0;}这个指令可以查看debug信息readelf-Stest.exe|grepdebug可以看到以 debug 和 release 模式发布的程序无论是在程序大小、程序内部包含的有关调试的二进制信息还是 gdb 模式下是否具有调试样例都是有明显区别的。4.2 gdb 的安装在Linux下我们可以使用如下命令来安装 gdbsudoyuminstall-ygdb4.3 gdb 的使用gdb 调试的常见选项如下l 行号从指定的行号开始往下显示源代码每次显示10行 (l – list)(注gdb 有自动记忆命令的功能即当我们第一次使用 l 显示源代码后我们下一次再使用 l 或者下一次按下 enter 键时它会接着上次的位置往下显示)l 函数列出某个函数的源代码 (l – list)r运行程序 (r – run)b 行号在某一行打一个断点相当于VS中的F9 (b – breakpoint)info b查看断点d 断点编号删除断点 (d – delete)(注每个断点都有自己的编号我们删除断点时需要指明对应的断点编号)n逐过程调试相当于VS中的F10 (n – next)s逐语句调试相当于VS中的F11 (s – step)c运行至下一个断点处停下 (c – continue)(注如果断点所在行不是一条语句比如 “{” “}” 或者空行那么它会继续往下到有效行处停下)bt查看调用堆栈 (backtrace)p 变量查看变量值 (p – print)display/undisplay 变量跟踪查看一个变量每次停下来都显示它的值undisplay 取消对先前设置的那些变量的跟踪finish把当前函数运行完disable breakpoints禁用断点enable breakpoints启用断点quit退出 gdb启动 gdbgdb 可执行文件名观察到如下场景即为我们的gdb调试器启动成功那么接下来我们将在gdb命令行中输入调试命令进行我们程序的调试退出q退出查看行号对应10行代码l nn代表你所要查看行号即可显示该行号对应代码如果输入的行号为1那么会从第1行开始显示共计10行代码如果该行号为一个中间的行号那么会居中显示该行号并且显示该行号上5行下5行共计10行代码gdb会默认记住命令如果你输入命令后下一次输入默认继续执行上一次的命令并且如果你查看的是行号会继续从上一次查看的最后一行的位置继续向下查看查看函数代码l 函数名查看函数运行程序r可以运行程序如果没有设置断点那么默认执行到程序的最后如果设置了断点那么默认运行到第一个断点的位置设置断点b nn代表行号在某一行设置断点b 函数名在函数的开头设置断点b 源文件: n在源文件行号为n的位置处设置断点查看断点info b可以查看断点的信息并且根据断点设置的时间先后标序号删除断点d n这个n代表断点的序号例如下图中的Num那一行代表的序号d breakpoints可以删除所有断点禁用断点和启用断点disable n这个n代表断点的序号禁用断点enable n这个n代表断点的序号启用断点逐过程调试和逐语句调试这里的调试过程一定是r先运行起来才能进行逐过程调试或逐语句调试nnext逐过程调试类似于vs中的F10不会进入函数s(step)逐语句调试类似于vs中的F11会进入函数打印变量值p 变量调试运行起来可以进行打印变量对应的值这里的作用仅仅是打印无其它作用跟踪变量让变量每次停下来都显示display 变量名跟踪查看一个变量每次停下来都显示它的值类似于vs中的监视窗口监视变量的作用取消追踪变量undisplay可以取消对所有变量的追踪这里不可以单独取消对某个变量的追踪要取消只能统一取消对全部变量的追踪查看当前局部变量的值info locals可以查看当前局部变量的值设置局部变量的值set var 变量名你要给变量设置的值比如循环前50次没有问题你想快速跳转至第51次有没有问题可以使用修改循环变量的值达到快速跳转进行调试执行到函数返回finish可以执行到当前函数的返回并且停在函数调用的那个位置跳到下个断点c跳转到下一个断点跳转到指定行until nn代表行号跳转到指定行n查看函数调用堆栈bt查看函数调用堆栈即各级函数调用及其参数