• 注册 / 登录
  • 切换到窄版
  • 查看: 1868|回复: 0

    程序编译过程详解

    [复制链接]

    676

    主题

    690

    帖子

    6808

    积分

    版主

    Rank: 7Rank: 7Rank: 7

    积分
    6808
    发表于 2024-5-13 17:06:30 | 显示全部楼层 |阅读模式

    路线栈欢迎您!

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    概述

    我们写出的C语言代码(.c文件),若要在机器上运行,需要经过一个编译过程,主要分为如下四个阶段:

    1.png

    预处理阶段,即完成宏定义和include 文件展开等工作;生成.i文件。GCC命令为:·gcc -E
    根据编译参数进行不同程序的优化,编译成汇编代码;生成.s文件。GCC命令为:·gcc -S
    用汇编器把上一阶段生成的汇编代码进一步生成目标代码;生成.o文件。GCC命令为:·gcc -C
    用链接器把上一阶段生成的目标代码、其他一些相关的系统提供的目标代码(如crtx.o)和系统或用户提供的库链接起来,生成最终的执行代码。生成可执行文件。GCC命令为:·gcc

    GCC工作流程

    2.png

    1. 预处理说明:

    1) 将.h文件展开,即复制到.c文件中
    2)将注释内容删除
    3)将宏进行替换
    • 目标代码: .o即计算机可以识别的1、0的指令
    • 可程序程序:文件后缀.out (Linux系统),.exe(windows系统)


    2. 预处理(Preprocessing)

    预处理主要进行以下几个方面的处理:

    宏定义指令
    如#define Pi 3.1415,预处理阶段会将程序中所有的Pi用3.1415代替。与之对应的#undef 则会取消对某个宏的定义,使之后面出现时不再被替换。

    条件编译指令
    如#ifdef、#ifndef、#else、#elif、#endif等伪指令的引入可以使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理,即预处理阶段将根据有关的文件将不必要的代码过滤掉。

    头文件包含指令
    如#include,头文件中一般通过#define定义了一些宏(如字符常量),同时也包含了各种外部符号的声明。采用头文件可以使一些定义在多个不同的C源程序中使用,而不必在文件中重新定义。预处理阶段会将头文件中的定义加入到引用它的代码中。

    特殊符号
    如在源程序中出现的FUNCTION会被解释为当前被编译的C源程序中的函数名称。预处理阶段会对源程序中出现的这些特殊符号用合适的值进行替换。
    总结:可以看出,预处理阶段主要是完成对源程序的替换工作。经过替换后,会生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件.i,该输出文件中只有常量如数字、字符等,或者是变量的定义,以及C语言的关键字如if、else等。

    3. 编译(Compilation)

    编译阶段所有做的工作就是通过词法分析和语法分析,在确认所有指令都符合语法规则之后,将其翻译成等价的中间代码或者是汇编代码。

    编译阶段会对代码进行优化处理,不仅涉及到编译技术本身,还涉及到机器的硬件环境。优化分为两部分:

    不依赖于具体计算机的优化。主要是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制、已知量的合并等)、无用赋值的删除等
    同机器硬件结构相关的优化。主要考虑如何充分利用机器的硬件寄存器存放的有关变量的值以减少内存的访问次数;根据机器硬件执行指令的特点对指令进行调整使目标代码比较短,执行效率更高等。

    4. 汇编(Assemble)

    汇编是把汇编语言代码翻译成目标机器指令的过程。目标文件中存放的是与源程序等效的目标机器语言代码。目标文件由段组成,通常一个目标文件中至少有两个段:

    代码段:主要包含程序的指令。该段一般是可读和可执行的,一般不可写。
    数据段:主要存放程序中用到的各种全局变量和静态的数据。一般数据段是可读、可写、可执行的。

    5. 链接(Linking)

    链接阶段的主要工作是将有关的目标文件相链接,即将一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有这些目标文件成为一个能够被操作系统执行的统一整体。举例如下:

    某个源文件的函数引用了另一个源文件中定义的变量和函数,因此需要链接阶段将这些变量和函数连接在一起。

    根据开发人员指定的同库函数的链接方式的不同,链接处理分为两种:

    静态链接:
    函数代码将从其所在的静态链接库中被拷贝到最终的可执行程序中。

    动态链接:
    此时,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所做的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的信息,在可执行程序被执行时,动态链接库的全部内容将被影射到运行时相应进程的虚拟地址空间。动态链接程序将根据可执行程序中记录的信息找到对应的函数代码。

    备注:使用动态链接能使最终的可执行文件较小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。

    程序编译

    1. Linux下的程序编译

    GCC(GNU Compiler Collection),是符合GPL和LGPL许可证发行的自由软件,也是类Unix和Mac OS X的标准编译器。GCC可处理C (gcc)、C++(g++)、Java等语言。

    Intel Composer XE 2013,Intel公司为x86平台编译器,针对Intel专门做了优化,在其他平台上表现也不错。

    PGI Accelerator,支持CUDA Fortran。已经被Nvidia收购。

    2. 串行程序的编译和执行

    程序编译流程如下

    • 编译:将源文件编译成目标文件
    • 连接:将目标文件连接成可执行文件
    • 执行
    Linux系统中,可执行文件没有同一的后缀,系统从文件属性来区分。源代码和目标文件的后缀名要保持同意规范,以便区分识别。

    1. #include<stdio.h>
    2. int main()
    3. {
    4.         printf("Hello,World.\n");
    5.     return 0;
    6. }
    复制代码
    1. gcc hello.c                    #编译
    2. ./a.out                         #执行

    3. gcc hello.c -o hello                                                        #自动执行了编译和连接
    4. ./hello

    5. 如果可执行文件的目录加入了PATH环境变量,可以直接使用可执行文件名。
    6. export PATH=$PATH:/home/mzy
    7.                                    #待定,无法成功运行

    8. gcc -c hello.c                #1.只执行编译,不执行连接,生成hello.o
    9. gcc -o hello hello.o        #2.连接目标文件,生成可执行文件
    复制代码

    2.1多个源文件的编译

    主程序源文件main.c

    1. #include<stdio.h>
    2. int main ()
    3. {
    4.     int sum=0,r,i;
    5.     for (i = 1;i <= 10; ++i)
    6.     {
    7.         r = fun(i);
    8.         sum += r;
    9.     }
    10.     printf("sum is %d\n",sum);
    11.     return 0;
    12. }
    复制代码

    子函数源文件fun.c

    1. int fun(int i)
    2. {
    3.     int res = i * i;
    4.     return res;
    5. }
    复制代码
    1. gcc -o sum main.c fun.c                        #同时编译多个源文件
    2. ./sum                                                  #执行(自动执行了连接动作,自动删除了中间过程所产生的目标文件main.o和fun.o)

    3. #也可以将源文件分别编译,最后再连接成可执行文件
    4. gcc -c main.c
    5. gcc -c fun.c
    6. gcc -o sum main.o fun.o
    7. ./sum                                                 #手动编译时目标文件不会消失
    复制代码

    2.2 多个源文件使用头文件的例子

    主程序文件main.c

    1. #include<stdio.h>
    2. #include"myhead.h"
    3. int main()
    4. {
    5.     printf(STRING);
    6.     return 0;
    7. }
    复制代码

    头文件myhead.h

    1. #dedine STRING "Hello,Multi sources\n"
    复制代码
    1. #编译时使用-I参数指定头文件搜索路径
    2. gcc -c -I /home/mzy main.c
    3. gcc -o Mut main.o

    4. #也可以将以上两句话融合
    5. gcc -o Multi -I /home/mzy main.c
    复制代码

    2.3 Linux下函数库文件介绍

    2.3.1 静态库与动态库

    静态库:

    • 命名规范为libXXX.a
    • 库函数会被连接进可执行程序,可执行文件体积较大
    • 可执行文件运行时,不需要从磁盘载入库函数,执行效率较高
    • 库函数更新后,需要重新编译可执行程序

    动态库:

    • 命名规范为libXXX.so
    • 库函数不被连接进可执行程序,可执行文件体积较小
    • 可执行文件运行时,库函数动态载入
    • 使用灵活,库函数更新后,不需要重新编译可执行程序

    2.3.2 使用头文件

    默认搜索头文件的目录优先级:

    • 源文件所在目录(要求源文件中用#include "..." 格式指定)
    • INCLUDE之类的环境变量指定的目录
    • 编译器自己的头文件目录
    • /usr/include操作系统头文件目录

    如果想自己指定头文件搜索路径可以使用 -I 参数

    • 用 -I 指定的目录优先级比默认搜索目录高
    • -I 参数可以指定多个 -I -I ...

    2.3.3 库函数的生成(静态库)

    子函数fun1.c

    1. int fun1 (int i)
    2. {
    3.     return i+i;
    4. }
    复制代码

    子函数fun2.c

    1. int fun2 (int i)
    2. {
    3.     return i*i;
    4. }
    复制代码
    1. #编译子函数源代码
    2. gcc -c fun1.c
    3. gcc -c fun2.c
    4. #使用ar命令将目标文件打包成静态库.a
    5. ar cr libtest.a fun1.o fun2.o
    复制代码

    2.3.4 库函数的生成(动态库)

    1. #编译子函数源码,必须要使用-fPIC(Position-independent code)参数
    2. gcc -c -fPIC fun1.c
    3. gcc -c -fPIC fun2.c
    4. #使用编译器-shared参数将目标文件连接成动态库.so
    5. gcc -o libtest.so -shared fun1.o fun2.o
    复制代码

    2.3.5 库函数的使用

    主函数main.c,调用之前定义的fun1和fun2子函数

    1. #include<stdio.h>
    2. int main ()
    3. {
    4.     int i = 10,sum,product;
    5.     sum = fun1(i);
    6.     product = fun2(i);
    7.     printf("the sum is %d,the product is %d\n",sum,product);
    8.     return 0;
    9. }
    复制代码

    1. gcc -c是编译,gcc -o才是连接。gcc -o时程序自动被编译,并且会删除中间产生的.o文件(目标文件)

    2. gcc -c -<参数> <要被编译的文件1> <要被编译的文件2> ...
    gcc -c -I <头文件的指定路径> <要被编译的文件>

    3. gcc -o <想要生成的可执行文件名> <需要的文件1> <需要的文件2>
    gcc -o <可执行文件名> -I <头文件路径> <需要被编译连接的文件1> <需要被编译连接的文件2> ...
    (因为-o参数默认会进行编译,所以上一行命令能够被执行。)

    4. ar cr libXXX.a <目标文件1> <目标文件2> ... #编译成静态库
    gcc -o <可执行文件名> <静态库的路径/libXXX.a> #在同级目录下也可以不写路径

    5. gcc -c -fPIC fun1.c #编译成动态库时必须指定-fPIC参数

    gcc -c -fPIC fun2.c

    gcc -o libXXX.so -shared <目标文件1> <目标文件2> #用-shared参数将目标文件连接成动态库.so

    2.3.5.1 库函数的使用方式一:直接使用

    gcc -c <需要编译的文件1> <需要编译的文件2> ...
    gcc -o <可执行文件名> <目标文件1> <目标文件2> <路径/libXXX.a> #当目标文件和静态库在同一目录时,可省略静态库的路径
    gcc -o <可执行文件名> <目标文件1> <目标文件2> <路径/libXXX.so> #不能省略动态库的路径,即使目标文件和动态库文件在同一目录下。

    2.3.5.2 库函数的使用方式二:在指定路径下搜索

    使用编译器的-L -lXXX 参数,表示在指定库函数路径下搜索名为libXXX.so或libXXX.a的库文件。

    • 如果库函数路径下同时有静态库和动态库,会选择动态库
    • -L 可以指定多次 -L -L ...
    • -L 指定的目录的搜索优先级最高
    • 如果在-L的目录中没有找到库函数,或者没有指定-L,编译器还会根据以下优先级从高到低搜索。

    • LIBRARY_PATH(静态库)、LD_LIBRARY_PATH(动态库)环境变量指定路径
    • 系统配置文件/etc/ld.so.conf中指定的动态库搜索路径
    • 系统的/lib(64)、/usr/lib(64)等操作系统库文件目录

    1. gcc -c <源文件>
    2. gcc -o <可执行文件名> <目标文件> -L <库文件所在路径> -ltest
    复制代码
    ./Main: error while loading shared libraries: libtest.so: cannot open shared object file: No such file or directory

    PS:如何解决在-L参数的指定目录下找不到动态库的情况?

    动态链接库的文件可能出现了问题,将文件加入到动态库的环境变量的路径中,

    1. export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/mzy
    2. #-L的目录中未找到库函数就会到动态库的环境变量中去找,避免了找不到文件的情况。
    复制代码

    附录

    表1:GCC识别的主要文件扩展名
    文件扩展名文件类型
    .cC语言代码
    .C、.ccC++语言代码
    .i预处理后的C语言代码
    .s、.S汇编语言代码
    .o目标代码
    .a静态链接库(程序编译时使用)
    .so动态链接库(程序运行时使用)

    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    小黑屋|路丝栈 ( 粤ICP备2021053448号 )

    GMT+8, 2024-12-22 09:08 , Processed in 0.048029 second(s), 22 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表