一 编译过程
(1) 问题引入
问题: C语言程序从'源代码'到'二进制程序'都'经历了'那些过程?
说明: 本文以'Centos7.7'下C语言的'编译过程'为例
备注: 以一个'初学者的角度'尽可能详细解读
(2) 源码准备
(3) 编译过程
① 编译前预处理
预处理'处理什么': 读取c源程序,对其中的'伪指令(以# 开头的指令)'和'特殊符号'进行处理
++++++++++++++++++'伪指令'主要包括以下'四个方面'++++++++++++++++++
1) '宏'定义指令 -->'替换'
如: '# define PRICE 12'、'# undef'等
-- 对于'前一个'伪指令,预编译所要做的是将程序中的'所有PRICE'用12'替换',但作为'字符串常量'的 Name则'不被'替换
-- 对于'后者',则将'取消'对某个宏的定义,使'以后'该串的出现不再被替换
2) '条件编译'指令 -->'过滤'
如:'#if 0'、'#ifdef'、'#ifndef'、'#else'、'#elif'、'#endif'等。
-- 作用:这些'伪指令'的引入使得程序员可以'通过定义不同的宏'来决定'编译程序'对'哪些代码进行处理'
-- 效果:预编译程序将根据有关的文件,将那些'不必要'的代码'过滤'掉
3) '头文件'包含指令 -->'展开到ceshi.c中'
如: '#include "FileName"' 、'#include < FileName>' 等
备注: 在头文件中一般用伪指令'#define'定义了'大量的宏(最常见的是字符常量)',同时包含有各种'外部符号'的声明
-- 目的: 采用'头文件的目的'主要是为了使'某些定义'可以供'多个不同'的C源程序使用
-- 使用: 在需要'用到这些定义'的C源程序中,只需加上一条'# include语句'即可,而'不必'再在此文件中将这些定义'重复'一遍
-- 预编译程序将把'头文件中的定义'统统都'加入'到它所产生的'输出文件'中,以供编译程序对之进行处理
补充: 包含到c源程序中的'头文件'可以是'系统提供'的,这些头文件一般被'放在/usr/include'目录下
细节: 在程序中'# include'它们要使用'<尖括号>'。另外开发人员也可以'定义自己的头文件',这些文件一般与c源程序'放在同一目录下',此时在# include中要用"双引号"
4) '特殊符号',预编译程序可以'识别'一些特殊的符号
-- 在源程序中'出现的LINE标识'将被解释为'当前行号(十进制数)'
-- FILE则被解释为当前被编译的'C源程序的名称',预编译程序对于在源程序中出现的这些串将用'合适的值'进行'替换'
5) '小结'
-- 作用: 预编译程序所完成的'基本上'是对源程序的"替代"工作
-- 效果: 经过此种替代,生成一个'没有'宏定义、没有条件编译指令、没有特殊符号的输出文件
-- 补充: 这个文件的含义'同没有经过预处理'的源文件是相同的,但'内容有所不同'
1)进行预处理
gcc -E ceshi.c -o ceshi.i
备注: -E 实质是调用'cpp'命令来完成'预处理'
'等价': cpp ceshi.c -o test.i
2)对比文件大小和类型
1)预处理之后'文件体积变大'
2)预处理之后的程序'还是文本',可以用'文本编辑器'打开
3)查看预处理后的文件
'#include' 头文件处理:直接把'stdio.h'从操作系统中找出来,把文件的内容'复制'一份,然后粘贴到'ceshi.h'中
4)对比系统的stdio.h文件
② 编译
1)进行编译
1)这里的编译'不是'指程序从源文件到二进制程序的全部过程,'而是'指将经过'预处理之后的程序'转换成'特定汇编代码'(assembly code)的过程
编译的'指令'如下: gcc -S ceshi.i -o ceshi.s
强调: 上述命令中'-S'让编译器在'编译之后停止','不进行'后续过程
效果: 编译过程'完成后',将生成程序的'汇编代码ceshi.s',也是'文本'文件
实质: 编译是将'预处理过的c语言文件'编译为'汇编语言',等待'汇编时'转换为'机器码(二进制)'
2) -S参数
-S参数: '只'编译、'不'汇编'、不'链接
2)汇编作用
+++++++++++'汇编阶段的作用'+++++++++++
1)检查语法,语法'有错误',则退出
2)语法正确,则生成对应的'汇编文件'
3)查看汇编文件
③ 汇编
1)-c参数
效果: 进行'编译'和'汇编',不进行'链接' --> 翻译成为'机器指令'
备注: 如果已经'编译'过则不会再'编译'
2)进行汇编
汇编过程将上一步的'汇编代码(assemble code)'转换成'机器码(machine code)',这一步产生的文件叫做'目标文件',是'二进制'格式
gcc -c ceshi.s -o ceshi.o
备注: gcc汇编过程底层'通过as'命令完成:
as ceshi.s -o ceshi.o
说明: 每一个'源文件'产生一个'目标'文件
3).o文件
目标文件:就是源代码'经过汇编后',但'未进行链接'的那些'中间'文件
说明: Linux下的'.o文件'就是'目标'文件
注意: 目标文件和可执行文件内容和格式'几乎'都一样,所以我们可以'广义地'将目标文件和可执行文化看成'一类型'文件,他们都是'按照ELF文件格式'存储的
4)查看.o文件内容
++++++++++++++++++'行'++++++++++++++++++
.text: '代码'段(存放函数的二进制机器指令)
.data: '数据'段(存已初始化的局部/全局静态变量、未初始化的全局静态变量)
.bss: 'bss'段(声明未初始化变量所占大小)
.rodata: '只读'数据段(存放" "引住的只读字符串)
.comment: '注释'信息段
.node.GUN-stack: '堆栈'提示段
++++++++++++++++++'列'++++++++++++++++++
Size:段的'长度'
File Off:段的'所在位置'(即距离文件头的'偏移'位置)
段的属性:
-- CONTENTS:表示该段'在文件中存在'
-- ALLOC:表示'只分配了大小',但没有存内容
5)补充
1)以'二进制格式'打开
vim -b ceshi.o
2)然后'用xxd'把文件转换成'十六进制'格式-->'覆盖原文件'
:%!xxd
'等价': xxd ceshi.o
++++++++++++++++深入'挖掘 .o文件'++++++++++++++++
objdump -h xxxx.o 打印'主要段'的信息
objdump -x xxxx.o 打印更多的'详细'信息
objdump -s xxx.o 将所有段的内容'以16进制方式'打印出来
objdump -d xxx.o 或者-S将所有包含指令的段反汇编
objdump -t xxx.o 查看'所有的符号'以及他们'所在段'
readelf -h xxx.o 查看.o文件的'文件头'详细信息
readelf -S xxx.o 显示.o文件中的'所有段',即查看'段表'
size xxx.o 查看.o文件中'各个段'所占大小
nm xxx.o 查看.o文件中'所有的符号'
④ 链接
链接过程将'多个目标文件'以及'所需的库文件(.so等)'链接成最终的'可执行文件'(executable file)
1)尝试直接运行
2)分析原因
3)动态链接
备注: g默认是'动态'链接
效果: 将'.o系列的目标文件'和'系统库'和'自定义库'进行'链接'
实质: 将'ceshi.c'中所用到的'函数'等'位置(position)'信息链接进'ceshi.o'文件中形成'ceshi-dynamic';'运行时'根据'提示信息'从相应的'库'中查找,然后'执行'
补充: 这里没有指定'库',说明使用系统默认'库' --> ' /usr/lib64/'、'/usr/local/lib64'
4)静态链接
5)二者对比
'静态'链接后,'对应的二进制文件',既有'源文件'自身编译的二进制文件,'还有'对应'库'的代码(复制一份),所以'体积'比较大
优点: 不需要'依赖'其它环境,'Windows'一般采用该种方式
++++++++++++++'分割线'++++++++++++++
'动态'链接后,对应的二进制文件',既有'源文件'自身编译的二进制文件,还有'对应'库的'位置(position)信息-->体积较小',运行时才去'加载'
缺点: 对环境有依赖,与'操作系统'相关的类库'强耦合'
⑤ 相关参考
Linux下有哪些'ELF类型'的文件?
'.o文件'、'可执行'文件、核心'转储文件'(core dump)、.so文件('动态'链链接库)