第8课:Linux开发工具(三):gcc

第8课:Linux开发工具(三):gcc 第8课Linux开发工具三gcc一、GCC与g的区别与使用建议基本定位GCC纯C语言编译器只能编译C语言代码gC编译器既能编译C代码也能编译C语言代码但编译C语言时会按照C的语法规则进行编译推荐使用方式编译C语言代码优先使用gcc编译C代码优先使用g【核心结论】虽然g可以编译C语言但本质是将C语言当作C来处理可能存在语法兼容性问题。使用对应编译器编译对应语言是最佳实践。历史与环境兼容性说明部分老旧Linux环境可能默认只安装了GCC未安装gC语言编写内核、操作系统等底层代码时使用GCC编译的代码质量更高、更贴近硬件二、程序翻译的四个阶段核心重点C/C代码从源文件到可执行程序必须经过预处理、编译、汇编、链接四个阶段。GCC默认会一步完成所有阶段但我们可以通过选项让它在每个阶段结束后停下来方便观察和调试。1. 预处理Preprocessing核心工作头文件展开将#include包含的头文件内容完整拷贝到源文件中宏替换将代码中所有的宏定义#define替换为实际的值去除注释删除代码中所有的单行注释//和多行注释/* */条件编译根据宏定义条件保留或裁剪对应的代码段对应GCC选项与文件后缀选项-E大写E作用从当前开始进行程序翻译完成预处理后立即停止输出文件后缀.i约定俗成非强制命令格式gcc -E 源文件.c -o 预处理文件.i代码演示与验证我们使用以下测试代码code.c#includestdio.h#defineM100#defineVERSION11intmain(){#ifdefVERSION1printf(我是version1版本的功能\n);#elseprintf(我是version2版本的功能\n);#endif//printf(hello world 1, %d\n, M);//printf(hello world 2, %d\n, M);return0;}执行预处理命令gcc-Ecode.c-ocode.i观察code.i文件可以发现宏M和VERSION1已经被完全替换所有注释已经被删除代码从原来的20多行变成了800多行这是因为stdio.h头文件被完整展开了头文件展开的本质与意义本质将头文件的内容原封不动地拷贝到源文件的对应位置为什么要把头文件和源文件分开【补充说明】主要是为了支持库的开发。当我们制作库时只需要将头文件包含函数声明交给用户而将源文件编译成二进制库文件隐藏起来这样既可以让用户知道如何使用库又能保护源代码不被泄露。条件编译的本质与应用场景本质对代码进行裁剪核心应用场景软件版本管理同一套代码维护免费版、社区版、企业版等不同版本跨平台开发根据不同操作系统编译不同的代码段调试与发布在调试版本中保留调试代码发布版本中裁剪掉【核心结论】公司内部通常只维护一套完整的代码通过条件编译选项生成不同功能的版本大大降低了维护成本。防止头文件重复包含的原理我们经常在头文件中看到这样的写法#ifndefCODE_H#defineCODE_H// 头文件内容#endif或者#pragmaonce这两种写法的本质都是条件编译。当第一次包含头文件时宏未定义会保留头文件内容并定义该宏当第二次及以后包含时宏已经定义会直接裁剪掉头文件内容从而避免重复包含导致的重定义错误。2. 编译Compilation核心工作将预处理后的干净C语言代码翻译成汇编语言代码对应GCC选项与文件后缀选项-S大写S作用从当前开始进行程序翻译完成编译后立即停止输出文件后缀.s汇编语言源文件的标准后缀命令格式gcc -S 预处理文件.i -o 汇编文件.s为什么要翻译成汇编语言而不是直接翻译成二进制这是由计算机语言的发展历史决定的最早的编程方式是二进制编程打孔纸带不需要编译器为了提高效率人们发明了汇编语言用助记符代替二进制指令同时诞生了汇编器后来为了进一步提高效率发明了C语言。C语言的设计者没有从零开始写一个直接生成二进制的编译器而是站在巨人的肩膀上先将C语言翻译成汇编语言再利用已有的汇编器生成二进制。编译器与语言的关系【核心结论】编程语言的语法本质上是编译器的翻译规则。我们学习C语言语法其实就是在学习GCC等编译器如何理解和翻译我们写的代码。编译器自举原理这是一个非常经典的鸡生蛋蛋生鸡问题第一代汇编编译器用二进制直接编写有了汇编编译器后就可以用汇编语言编写更强大的汇编编译器第一代C语言编译器用汇编语言编写有了C语言编译器后就可以用C语言编写更高级的C语言编译器现代的GCC、Clang等编译器都是用C/C编写的自己编译自己这个过程就叫做编译器自举是编程语言发展的核心动力。3. 汇编Assembly核心工作将汇编语言代码翻译成可重定位目标二进制文件对应GCC选项与文件后缀选项-c小写c作用从当前开始进行程序翻译完成汇编后立即停止输出文件后缀.o可重定位目标文件的标准后缀命令格式gcc -c 汇编文件.s -o 目标文件.o可重定位目标文件.o的特点它已经是二进制文件用文本编辑器打开会显示乱码它不能直接运行即使给它加上可执行权限也不行【易错警告】很多初学者会误以为二进制文件就能运行实际上.o文件只是半成品它里面只包含了我们自己写的代码的二进制实现但没有和系统库关联起来。4. 链接Linking核心工作将我们生成的目标文件.o与系统库文件进行关联生成最终的可执行程序头文件与库的关系头文件.h只包含函数的声明告诉编译器函数的名称、参数和返回值类型库文件包含函数的具体实现已经被编译成二进制【补充说明】开发C语言程序必须具备三个要素编译器GCC/g头文件提供函数声明库文件提供函数实现我们安装VS时下载的C开发工具包本质上就是下载了Windows平台下的C/C头文件和库文件。查看库依赖的命令ldd格式ldd 可执行文件示例ldd code输出中会显示该可执行文件依赖的所有动态库其中最重要的是libc.so.6这就是C语言标准库它包含了printf、scanf等所有标准函数的实现。为什么.o文件不能直接运行因为.o文件中只有我们自己写的代码的二进制但我们调用的printf等标准函数的实现并不在.o文件中。链接阶段的核心任务就是找到这些函数在库中的实现并将它们与我们的代码关联起来。三、动静态库详解感性认识库是预编译好的二进制代码集合分为静态库和动态库两种。1. 库的分类与命名规则系统静态库后缀动态库后缀命名格式库的真名Linux.a.solibxxx.a/libxxx.so去掉前缀lib和后缀后的部分如libc.so的真名是cWindows.lib.dllxxx.lib/xxx.dll去掉后缀后的部分【补充说明】Linux系统本身不关心文件后缀但编译器和链接器会根据后缀来识别文件类型。2. 动态库共享库形象比喻网吧。所有学生程序都去同一个网吧动态库上网调用函数。工作原理链接阶段链接器不会将库函数的代码拷贝到可执行文件中只会在可执行文件中记录库的路径和函数的地址运行阶段当程序执行到库函数时操作系统会根据记录的地址将动态库加载到内存中然后跳转到库函数中执行多个程序可以共享内存中的同一份动态库优缺点✅ 优点节省内存所有程序共享一份库可执行文件体积小库更新后不需要重新编译程序❌ 缺点运行时需要跳转速度稍慢强依赖库库缺失或版本不兼容会导致程序无法运行3. 静态库形象比喻买电脑回家。每个学生程序都把自己需要的电脑库函数买回家拷贝到自己的可执行文件中。工作原理链接阶段链接器会将程序中用到的所有库函数的代码完整地拷贝到可执行文件中优缺点✅ 优点不依赖库编译成功后可以独立运行运行时不需要跳转速度稍快❌ 缺点可执行文件体积大多个程序运行时内存中会存在多份相同的库代码浪费内存库更新后需要重新编译程序4. GCC链接方式控制默认方式动态链接。GCC默认会优先使用动态库进行链接这也是工业界的最佳实践。强制静态链接使用-static选项命令格式gcc 源文件.c -o 可执行文件 -static静态链接常见报错与解决如果你执行静态链接命令时出现以下错误/usr/bin/ld: cannot find -lc collect2: error: ld returned 1 exit status这表示系统中没有安装C语言静态库。【易错警告】Linux系统默认只安装动态库不安装静态库。解决方法以CentOS/RHEL为例sudoyuminstall-yglibc-static动静态链接生成文件的体积对比老师课堂演示结果动态链接生成的可执行文件约8KB静态链接生成的可执行文件约860KB体积相差了100多倍这直观地体现了静态链接的缺点。四、GCC常用选项总结选项作用对应阶段输出文件后缀-E完成预处理后停止预处理.i-S完成编译后停止编译.s-c完成汇编后停止汇编.o-o指定输出文件的名称所有阶段--Wall显示所有编译警告编译阶段--static强制使用静态链接链接阶段-【记忆技巧】程序翻译的三个阶段选项ESC键盘左上角的三个键注意S是大写对应生成的文件后缀ISO.i.s.o五、核心命令演示一步生成可执行文件默认动态链接gcc code.c-ocode分步编译# 1. 预处理gcc-Ecode.c-ocode.i# 2. 编译gcc-Scode.i-ocode.s# 3. 汇编gcc-ccode.s-ocode.o# 4. 链接gcc code.o-ocode强制静态链接gcc code.c-ocode-static-static查看可执行文件依赖的库ldd code查看文件类型filecodefilecode-static