C代码编译过程分析

来源:互联网 发布:商务数据分析 编辑:程序博客网 时间:2024/06/05 09:12

C代码编译过程分析


一.概述

C代码整个编译过程是极其复杂的,其中涉及到的编译器知识,硬件知识,工具链知识是非常多的,深入了解整个编译过程对于程序员理解分析以及编程有很大帮助。本文通过对C代码编译过程的分析,希望对读者对C代码的编译过程有一定的了解。此外,建议大家在平时遇到问题的时候多思考,多实践。

在分析之前我们首先了解一下什么是编译以及整个编译过程分为那几个部分。


编译的概念:编译程序读读取源程序,对其进行词法语法分析,将高级语言(C语言)转换成功能等效的汇编语言,再由汇编语言转换为CPU能够识别的机器语言,并按照操作系统对可执行文件格式的要求链接生成可执行文件。


编译的完整过程:C源代码(file.c)-->预编译处理(file.c) -->编译优化(file.s, file.asm) -->汇编程序(file.obj, file.o, file.a, file.ko) -->链接程序(file.exe, file.axf, file.elf)

我们先写hello.chello.h文件,以便后面演示。


hello.h

#ifndef _HELLO_H #define _HELLO_H #define MAX    188 #define add(x,y)   (x+y) #endi

hello.c

#include "stdio.h" #include "hello.h"  int main(int argv, char **argc) { printf("MAX is %d\n",MAX);     printf("add(2,3) is %d\n",add(2,3));    return 0;}

 

二.过程分析


2-1. 预编译处理

预编译处理阶段主要是读取源程序,对其中的伪指令(以#开头的指令)和特殊符号进行等效替换,经过预编译处理,生成一个没有没有宏定义,没有条件编译,没有特殊符号的 file.c 输出文件。该输出文件与C源程序是等效的。

伪指令主要包括一下几个方面:

(1) 宏定义指令,如 #define NAME TokenString,#undef 以及编译器内建的一些宏,如_DATE_, _FILE_, _LINE_, _TIME_等等。

(2) 条件编译指令,如:#ifndef, #define, #endif,#else, #elif 等等。

(3) 头文件包含指令,如:#include “file.h”或 #include <file.h>。

 

[lwn@VM_255_164_centos bianyi]$ gcc -E hello.c -o hello.i

将hello.c预编译生成c文件hello.i,只是简单的替换。

hello.i

int main(int argv, char **argc){    printf("MAX is %d\n",188);   printf("add(2,3) is %d\n",(2 +3));   return 0;}

2-2. 编译、优化

 [lwn@VM_255_164_centos bianyi]$ gcc -S hello.i-o hello.s

编译程序所要做的工作就是通过此法语法分析,在确认所有指令都符合语法规则之后将其翻译成等价的中间代码或汇编代码表示。

优化处理是编译系统中一项比较艰深的技术,其涉及到的问题不仅同编译技术本身有关,而且依赖于机器的硬件环境。优化一部分是对中间代码的优化,这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行。

对于前一种优化,主要的工作是删除公共表达式,循环优化(代码外提,强度削弱、变换循环控制条件、已知量的合并等)、复写传播以及无用赋值的删除等。后一种类型的优化同机器的硬件结构紧密相关。最主要的是考虑如何充分利用机器的各个硬件寄存器存放的有关变量的值,以减少堆内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。


将c文件hello.i生成汇编文件hello.s

[lwn@VM_255_164_centosbianyi]$ gcc -S hello.i -o hello.s

hello.s

.file   "hello.c"       .section    .rodata  .LC0:  .string "MAX is %d\n"  .LC1:  .string "add(2,3) is %d\n"  .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    $188, %esi movl    $.LC0, %edi movl    $0, %eax call    printf movl    $5, %esi movl    $.LC1, %edi movl    $0, %eax call    printf movl    $0, %eax leave.cfi_def_cfa 7, 8 ret.cfi_endproc.LFE0:.size   main, .-main.ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-4)".section    .note.GNU-stack,"",@progbits


2-3. 汇编

[lwn@VM_255_164_centosbianyi]$ gcc -c hello.s -o hello.o

   汇编过程实际上是把编译生成的汇编语言代码翻译成CPU能够识别的的机器言的过程。对于被翻译系统处理的每一个C源代码,都将最终经过这一处理生成相应的目标文件。目标文件中存放的也就是与源程序等效的目标的机器语言代码。


目标文件至少包含文本段和数据段两个段。

    文本段:也称代码段,其中主要存放程序的指令,该段一般是可读可执行但是不可写。

    数据段:主要存放程序中用到的各种常量和变量。一般数据段都是可读可写可执行。


将汇编文件hello.s生成目标文件hello.o,也就是二进制文件。此时hello.o显示乱码。

[lwn@VM_255_164_centos bianyi]$gcc -c hello.s -o hello.o

 

2-4. 链接

    汇编程序生成的目标文件不能够直接执行,其中还有很多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的变量或函数调用,或者在程序中调用了某个库函数等。所有这些问题都需要经过链接处理才能得到解决。

     链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中

引用的变量或函数同该变量在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体,也就是可执行程序。

  根据开发人员指定的库函数链接方式不同,连接处理分为静态链接和动态链接。

 

    静态链接: 函数的代码将从其所在的静态库中被拷贝到最终的可执行程序中,这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。

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

 

对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法使用动态链接能够是最终生成的可执行文件比较短小。此外,当共享对象被多个进程使用时能够节省内存,因为在内存中只需要保存一份此共享对象的代码,但并不是使用动态链接就一定比使用静态链接优越。某些情况下动态链接可能会带来一下性能上的损害。


三.可执行程序内存布局


以上内容大致分析了C源程序编译生成可执行文件(二进制文件)的整个过程,C源程序通过编译链接生成的可执行文件由代码段和数据段组成。在可执行文件被执行的同时又会产生堆区栈区等。


3-1. 可执行文件组成结构:



3-2. 程序运行中文件组成:

    在程序运行时会产生堆区,栈区等其他部分


(1)  代码段(文本段):

代码段由程序中执行的机器代码组成,在C语言中,程序语句进行编译后,形成机器代码。在执行过程当中,CPU的程序计数器(PC)指向代码段的每一条机器代码,并由处理器依次执行。

(2)  只读数据段:

只读数据段是程序使用的一些不会被更改的数据,使用这些数据的方式类似查表式的操作。由于这些变量不需要更改,因此只需放置只读存储器中。

(3)  已初始化读写数据段:

已初始化数据是在程序中声明,并且具有初始变量值,这些变量需要占用存储器的空间。在程序执行时,他们需要位于可读写的内存区域,并具有初值。以供程序运行时读写。

(4)   未初始化数据段:

未初始化数据段是在程序中声明,但是没有初始化的变量。这些程序在运行程序之前不需要只用存储空间。

(5)   堆区:堆内存只在程序运行时出现,一般由程序员分配和释放。在具有操作系统的情况下,如果程序没有释放,操作系统可能在程序运行结束时收回内存。

(6)  栈区:栈内存只在程序运行时出现,在函数内部使用的变量,函数的参数以及返回值将使用栈空间。栈空间由编译器自动分配和释放。

下面看一个例子:

int a = 0;                        //全局初始化区,.data段static int b = 20;                //全局初始化区,.data段char *p1;                         //全局未初始化区,.bss段const int A = 10;             //.rodata段void main(void){       int b;                      //栈       char s[] = "abc"             //栈;       char *p2;                  //栈       static int c = 0;             //全局初始化区,.data段       char *p3 = "12345";         //12345\0在常量区,p3在栈区       p1 = (char*)malloc(10);      //分配得来的10个字节的空间在堆区       p2 = (char*)malloc(20);      //分配得来的20个字节的空间在堆区       strcpy(p1,"12345");          //12345\0在常量区,p3在栈区,编译器可能会将它与p3指向的123456优化成同一块区域}

代码段,读写数据段,只读数据段未初始化数据段属于静态区域,而堆栈属于动态区域。

代码段,读写数据段,只读数据段将在链接之后产生。未初始化数据段将在程序初始化的时候开辟。而堆和栈将在程序运行中分配和释放。C语音程序分为映像和运行两种状态。在编译链接形成的映像文件中,将只包含代码段,只读数据段和读写数据段。在程序运行之前,将动态生成未初始化数据段,在程序运行时还将动态形成堆区域和栈区域。一般来说,在静态的映像文件中,各个部分称之为节(section),而在运行时各部分称之为段。如果不详细区分可以统称为段。



0 0
原创粉丝点击