C/C++程序的编译链接过程

13 篇文章 4 订阅
10 篇文章 0 订阅

在以前学习C语言的时候,想必大家写的第一个代码都是“hello world”吧。在以前我们调试一个代码的时候是在vc++6.0或者是在vs上面调试的,这种就是集成开发环境,它为我们简化了一个代码的编译链接的过程但是却对初学者又蒙上了面纱。而当我们学习了更多一些知识的时候就该看看这个面纱下面到底隐藏着什么。
大家都知道我们所编写的C语言程序只是一个后缀为“.c”的文件,这个文件是不能直接被计算机所运行的,我们要将这高级语言转化成计算机所能执行的二进制语言。而把这个普通文件进过处理转换成计算机处理的可执行文件的过程就是编译链接。而其实这个过程要细分的话要分解4个步骤:预处理、编译、汇编、链接。而用GCC来编译这个“hello world”程序的过程为下图:
这里写图片描述
而编译的流程图为:
这里写图片描述

一、预处理(Preproceessing)
预处理是根据预处理指令组装新的C、C++程序,进过预处理会产生一个没有宏定义、没有条件编译指令、没有特殊符号输出的文件。在linux下命令为:gcc -E hello.c -o hello .i 或者 cpp hello .c > hello.i
预编译条件下的处理规则如下:
1、将所有的“#define”删除,并且展开所有的宏定义。
2、处理所有的条件编译指令。
比如“#if”“#ifdef”“#elif”“#else”“#endif”。
3、处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
4、删除所有的注释“//”和“/* */”。
5、添加行号和文件标识,比如#2“hello.c”2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
6、保留所有的#pragma编译器指令,编译器要使用它们。

二、编译(Compilation)
编译是对于预处理完的文件进行一些列的词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件。gcc -S hello.i -o hello.s
或者 gcc -S hello.c -o hello.s
注:现代版本的GCC把预处理和编译两个步骤合成一个步骤,用cc1工具来完成,gcc就是对于一些后台程序的封装,比如:预编译编译程序cc1、汇编器as、链接器Id。
编译后的汇编代码(hello.s)如下:

   .file   "hello.c"
    .section    .rodata
.LC0:
    .string "Hello, world."
    .text
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $16, %esp
    movl    $.LC0, (%esp)
    call    puts
    movl    $0, %eax
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits


编译时的技巧:
编译的作用是对源程序进行词法检查、语法检查和中间代码生成。编译时对文件中的全部内容进行检查,如果有语法错误,编译结束后会显示出所有的编译出错信息,开发人员可以根据错误提示修改程序。对于新写的一个保护多个文件的工程,一开始采用源文件分别编译,这样容易发现每个源文件的自身错误,限定了错误的范围,如果一开始就采用全部编译,多个源文件可能会产生许多错误,无形中增加了开发难度。如果每个源文件都通过了编译,再将所有文件进行编译。对源文件分别编译对于调试,纠错是一种很好的方法。

三、汇编(Assembly)
汇编器是将汇编代码转化成机器可以执行的命令,每一条汇编语句都对应一条机器指令,并生成可重定位目标程序的.o文件,该文件为二进制文件,字节编码时机器指令。汇编相对于编译的过程比较简单,根据汇编指令表和机器指令表一一进行翻译就可以了。所以汇编器的汇编过程相对与编译器是比较简单的,命令如下:
as hello.s -o hello.o
或者
gcc -c hello.s -o hello.o

四、链接(Linking)
链接分为:静态链接、动态链接。
静态链接:在编译阶段就把静态库就加到可执行文件中去,这样可执行文件就会比较大。
动态链接:在链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。
链接时通过调用连接器来链接程序运行需要的一大堆目标文件,以及所依赖的其他库的文件,最后生成可执行文件。链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
静态链接的过程:
这里写图片描述
“hello world”程序的编译链接过程就是这样的,那么编译器和连接器到底是做了什么呢?
其实我们回到编译器本身的职责上来看,编译的过程一般是分为6个步骤:扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化。
如图所示:
这里写图片描述
扫描:扫描也就是词法分析。扫描器把源代码的字符序列分割成一系列的记号,有一个叫lex的程序可以实现词法扫描,可以按照用户描述好的词法规则将输入的字符串分割成一个个记号。
语法分析:语法分析器产生语法树,yacc工具实现语法分析。
语义分析:静态语义(编译器可以确定的语义)、动态语义(运行期才可以确定的语义)。
源代码优化:源代码优化器将整个语法树转化为中间代码(与目标机器和环境变量无关),中间代码使编译器分为前端和后端,编译器前端负责产生无关的中间代码,编译器后端将中间代码转化为目标机器代码。
目标代码生成:代码生成器。
目标代码优化:目标代码优化器。

扩展:

extern:这是告诉编译器,这个符号在别的编译单元里定义,也就是要把这个符号放到未解决符号表里去。(外部链接)

外部链接的利弊:外部链接的符号,可以在整个程序范围内使用(因为导出了符号),但是同时要求其他的编译单元不能导出相同的符号。

内部链接的利弊:内部链接的符号,不能在别的编译单元内使用。但是不同的编译单元可以拥有同样名称的内部链接符号。

为什么头文件只可以有声明不能有定义?
头文件可以被多个编译单元包含,如果头文件里有定义,那么每个包含这个头文件的编译单元就都会对同一个符号进行定义,如果该符号为外部链接,则会导致duplicated external simbols。因此如果头文件里要定义,必须保证定义的符号只能具有内部链接。

为什么常量默认为内部链接而变量不是?
这就是为了能够在头文件里如const int n = 0这样的定义常量。由于常量是只读的,因此即使每个编译单元都拥有一份定义也没有关系。如果一个定义于头文件里的变量拥有内部链接,那么如果出现多个编译单元都定义该变量,则其中一个编译单元对该变量进行修改,不会影响其他单元的同一变量,会产生意想不到的后果。

为什么函数默认是外部链接?
虽然函数是只读的,但是和变量不同,函数在代码编写的时候非常容易变化,如果函数默认具有内部链接,则人们会倾向于把函数定义在头文件里,那么一旦函数被修改,所有包含了该头文件的编译单元都要被重新编译。另外,函数里定义的静态局部变量也将被定义在头文件里。

为什么公共使用的内联函数要定义在头文件里?
因为编译时编译单元之间互相不知道,如果内联函数被定义于.cpp文件中,编译其他使用该函数的编译单元的时候没有办法找到函数的定义,因此无法对函数进行展开。所以说如果内联函数定义于.cpp文件里,那么就只有这个cpp文件可以是用这个函数。

  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值