说明:
本文章旨在总结备份、方便以后查询,由于是个人总结,如有不对,欢迎指正;另外,内容大部分来自网络、书籍、和各类手册,如若侵权请告知,马上删帖致歉。
QQ 群 号:
内容来源:
CSDN-chablin-程序的编译、链接过程详解
CSDN-douguailove-编译和链接的过程、博客园-星星_xing-Linux中程序的编译和链接过程
下一篇:基础知识:篇2-多源文件编译过程
零、GCC编译工具链 简介
以 GCC 编译器为核心的一整套工具,用于把源代码转化成可执行应用程序,主要包含以下三部分:
(1)GCC 编译器 : 用于完成预处理和编译过程,例如把 C 代码转换成汇编代码。
(2) glibc:包含了主要的 C 语言标准函数库, C 语言中常常使用的打印函数 printf、 malloc 函数就在 glibc 库中。
(2)Binutils :除GCC编译器外的一系列小工具包括了链接器ld,汇编器as、目标文件格式查看器readelf等。
一、编程过程
情景说明:
ubuntu16.04-server下编写最简单的程序hello.c并利用gcc编译器编译后执行。
第一步:编写源文件
输入指令:vim hello.c
【使用vim编辑器编辑hello.c(若无hello.c则创建)】
#include <stdio.h>
int main (int argc ,char **argv)
{
printf("hello\n");
return 0;
}
第二步:编译源文件
输入指令:gcc hello.c
【gcc编译器更准确说是驱动程序,指挥组件依次完成相应功能】
(组件:cpp(预编译器)【预编译过程】、cc1(编译器)【编译过程】、as【汇编过程】、ld【链接过程】等)
输入指令:ls
,可看到文件a.out
PS:
默认编译结果均为:a.out,若想指定输出文件名则可使用,gcc选项:-o,即: gcc hello.c -o hello
-o选项可用于任意阶段指定输出文件名,如:-o file ,指定文件名为file。
【常见后缀名与文件类型对应关系】
文件后缀名 | 文件类型 |
---|---|
.c | 源文件 |
.h | 头文件 |
.i | 预处理文件 |
.s | 汇编文件 |
.o | 目标文件 |
a.out | 可执行文件 |
第三步:运行可执行文件
输入指令:./a.out
,可查看到输出结果:hello
.
二、详细编译过程(第二步的详细说明)
上面可知gcc是控制若干组件完成编译,那么具体是怎么样的呢?
【gcc选项】
总体选项 | 用途 |
---|---|
-v选项 | 显示制作GCC工具自身时的配置命令; 同时显示编译器驱动程序、预处理器、编译器的版本号 |
-o选项 | 指定输出文件名,任意阶段均可使用。 |
-E选项 | 执行预处理后就停下来,直接送往标准输出。 |
-S选项 | 执行编译后就停下来,输出.s文件。 |
-c选项 | 执行汇编后就停下来,输出.o文件。 |
警告选项 | 用途 |
-Wall选项 | 打开所有需要注意的警告信息 |
调试选项 | 用途 |
-g选项 | 产生符号调试工具(GNU的gdb)所必要的符号资讯 |
优化选项 | 用途 |
-O0~-O3选项 | -O0不优化,剩余优化等级随数字提高 |
PS:
-E、-S、-c选项相当于是限定了编译器执行操作结束点,而不是单独的将某一步拎出来执行(并不一定从头开始,从中间开始效果也相同)。
①预编译过程
工具:cpp工具
目的:按预处理命令将源代码进行添加、替换、删除构成一份完整的源代码。
(①删除所有的注释//和/* */。②添加行号和文件名标识。③保留所有的#progma编译器指令。④下面的预处理命令)
预处理命令:以“#”开头的命令。
包含:
①#include
包含命令(添加)
目的:将一个源文件全部内容复制到当前源文件中
②#define
宏定义命令(替换)
目的:提高程序通用性与易读性便于维护。
③#if/#ifdef等
条件编译命令(删除)
目的:按照一定条件只编译源程序部分代码。
输入指令: gcc -E hello.c -o hello.i / cpp hello.c > hello.i
输入指令:vim hello.i
。。。。。。
extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
# 912 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 942 "/usr/include/stdio.h" 3 4
# 2 "hello.c" 2
# 3 "hello.c"
int main (int argc ,char **argv)
{
printf("hello\n");
return 0;
}
该处简略截取其中一段代码,源程序只有寥寥数行,经过预编译后代码量变大。这是由于载入了stdio.h文件(位于/usr/include)中内容,不妨可打开查看一下。
输入指令:vim /usr/include/stdio.h
可发现画圈几行在hello.i
文件中可找到。
②编译过程
工具:cc1工具
目的:将上面的.i文件中的源代码“翻译”(进行一系列的词法分析、语法分析、语义分析及优化)为汇编代码。
PS:
翻译的汇编代码中包含伪指令(不参与CPU运算,只指导编译连接过程)
如:.cfi开头伪指令,辅助汇编器创建栈帧(stack frame),每次函数调用过程均会产生栈帧
(回溯(backtrace)查看函数调用信息可用到【又称栈的回卷(unwind stack)】)
输入指令:gcc -S hello.i -o hello.s
【gcc -S hello.c
】
输入指令:vim hello.s
.file "hello.c"
.section .rodata
.LC0:
.string "hello"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
movl $.LC0, %edi
call puts
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
③汇编过程
工具:as工具
目的:将上面的.s文件中的汇编代码“直译”为一定格式(可重定位)的机器代码(二进制文件)。
(Linux系统上表现为ELF的目标文件[OBJ文件,Object file])
PS:
反汇编:机器代码转换为汇编代码(调试程序时经常用)
简述汇编过程:
每一条汇编语句几乎都对应一条机器指令,故汇编过程非常简单,无复杂语法,语义,无需指令优化
直接根据汇编指令和机器指令对照表翻译即可,除此之外,还需在目标文件中创建辅助链接所需的信息。
可重定位:数据在内存中存放的起始位置不是固定的,起始位置+相对地址=绝对地址
问题:为什么.o文件不能直接执行呢?
回答:因为变量转成符号,符号表的符号地址没有分配出来,所以不能直接执行。
输入指令:gcc -c hello.s -o hello.o / as hello.s -o hello.o
【gcc -c hello.c
】
输入指令:vim hello.o
④链接过程
工具:ld工具
目的:将上面的.o文件和系统库中的.o文件与库文件链接起来形成可执行文件。
PS:
目标文件就是源代码经过编译后但未进行链接的那些中间文件(Linux下的 .o文件就是目标文件)
目标文件和可执行文件内容和格式几乎都一样,都是按照ELF文件格式存储的
简述链接过程:
将各个.o文件之间相互引用的部分正确的衔接起来。(就是把.o文件里的段整合在一起进行【符号解析】,解析正确后,再进行【符号重定位】,所有符号都拥有其正确的虚拟地址后,然后生成可执行文件,其运行的时候只需要代码段和数据段。)
符号解析:将每个符号引用刚好和一个符号定义联系起来,对符号表里的未定义的符号找到其定义的地方,代码段中,对所有指令未有其定义的符号都将其变为正确的地址。
符号重定位:重新计算各个目标的地址过程叫做符号重定位(对其分配相应的虚拟地址)。
输入指令:gcc hello.o -o hello
【gcc hello.c -o hello
】