您的位置:首页 > 编程语言 > C语言/C++

C语言编译链接流程以及重要特性在编译器中实现

2016-03-01 20:27 375 查看
C语言算是大学里接触的最早,用的最"多"的语言了,对于大部分学习计算机的学生基本上是从开始学习C语言起,凭借着一句经典的"hello, world!"迈入了计算机的世界的,初体味了一把这个世界还有个叫编程的活。作为系统级的开发首选语言,只诞生以来就屹立不倒,C语言的重要性是不言而喻的。就是怀着这种对C的无比敬意开始了我的伪程序之旅。然而大学里面没写过什么像样的东西,说来惭愧,什么课程设计,或是自称为项目的东西大都由些蹩脚的程序拼凑而成。做为一个菜鸟级别的程序员,使用C有些年,但对于C没有有真正的了解。我想有必要从新了解这门古老的语言背后的东西,知其然还要知其所以然,才能更好的使用这门语言。当然语言是工具,但了解工具的强项、陷阱与缺陷,对于工具威力的发挥
,对于你去驾驭工具的娴熟程度是那是大有裨益啊。C语言的设计哲学就是给你一把锤子嘛, 用不好可是会砸自己的脚。

C程序编译流程

编译一个C程序可以分为四阶段,预处理阶段->生成汇编代码阶段->汇编阶段->链接阶段,这里以linux环境下gcc编译器为例。使用gcc时默认会直接完成这四个步骤生成可以执行的程序,但通过编译选项可以控制值进行某些阶段,查看中间的文件。

 


gcc指令的一般格式为:

     gcc [选项] 要编译的文件 [选项] [目标文件]

其中,目标文件可缺省,gcc默认生成可执行的文件名为:a.out

gcc main.c                                  直接生成可执行文件a.out

gcc -E main.c -o hello.i                 生成预处理后的代码(还是文本文件)

gcc –S main.c -o hello.s               生成汇编代码

gcc –c main.c -o hello.o              生成目标代码

C程序目标文件和可执行文件结构

目标文件和可执行文件可以有几种不同的格式,有ELF(Excutable and linking Format,可执行文件和链接)格式,也有COFF(Common Object-File Format,普通目标文件格式)。虽然格式不一样,但具有一个共同的概念,那就是段(segments),这里段值二进制格式文件中的一块区域。

linux下的可执行文件有三个段文本段(text)、数据段(data)、bss段,可用nm命令查看目标文件的符号清单。

编译过程:  源文件-------->到可执行文件  



其中注意的BSS段,并没有保存未初始化段的映像,只是记录了该段的大小(应为该段没有初值,不管具体值),到了运行时再到内存为未初始化变量分配空间,这样可以节省目标文件空间。对于data段,只是保存在目标文件中,运行时直接载入。

C程序的内存布局

运行过程:           可执行文件->内存空间



 

对于data段,保存的是初始化的全局变量和stataic的局部变量,直接载入内存即可。 text段保存的是代码直接载入。BSS段从目标文件中读取BSS段大小,然后在内存中紧跟data段之后分配空间,并且清零(这也是为什么全局表量和static局部变量不初始化会有0值得原因)

函数调用栈

作为面向过程的语言,C基本的特色就是模块化、过程化。一个C程序或一个模块由一堆函数组成,然后程序执行,按代码的结构调用这些函数,完成功能。那么函数调用的背后编译器到底为我们做了什么呢?

void fun(int a, double b)
{
     int c = 300;
     c += 1;
}
int main()
{
     fun(100, 200);
     return 0;
}

.globl _fun                                   ;全局函数符号
     .def     _fun;     
_fun:                                           ;函数fun入口
     pushl     %ebp                         ;保存ebp值
     movl     %esp, %ebp                ;采用ebp来访问栈顶
     subl     $4, %esp                       ;esp用来扩展堆栈分配局部变量空间
     movl     $300, -4(%ebp)             ;局部变量赋值
     leal     -4(%ebp), %eax              ;得到局部变量有效地址
     incl     (%eax)                           ;访问局部变量
     leave                                        ;相当于movl ebp, esp   pop ebp  
     ret

.globl _main
     .def     _main;    
_main:                                        ;main函数入口
     ;....
     movl     $200, 4(%esp)             ; 参数入栈 
     movl     $100, (%esp)               ; 参数入栈
     call     _fun

     ;.....



 

函数调用过程:

参数按从右到左顺序放到栈顶上

call调用,将返回地址ip入栈保存

在栈上分配局部变量空间

执行函数操作

函数返回过程:

ret会从栈上弹出返回地址

执行调用前后面的代码

由此得的结论是,函数调用一个动态的过程,调用的时候又有一个栈帧,调用的时候展开,结束的时候收缩。局部变量在运行到该函数的时候在栈上分配内存,这些内存实际上没有名字的,不同于数据段,有符号名字,局部变量在函数结束就销毁了。这也是什么局部变量同名互补干涉的原因,因为编译以后 ,根本就不是通过名字来访问的。

全局变量

全局变量有初始化或未初始化之分,初始化了的全局变量保存在data段,未初始化全局变量保存在BSS段,data段和BSS段都是程序的数据段

int global1 = 100;
int main()
{
     global1 = 101;
     extern int global2;
     global2 = 201;
     return 0;
}
int global2 = 200;

     
.globl _global1                    ;全局符号global1
     .data                              ;位于数据段
     .align 4
_global1:
     .long     100                    ;全局变量初值
     ;.....
.globl _main                           ;全局符号main 
     .def     _main;                   ;是一个函数
_main:                                   ;函数入口
     ;...
     movl     $101, _global1          ;通过符号访问全局变量
     movl     $201, _global2          ;通过符号访问全局变量,这个变量还未定义
     movl     $0, %eax
     leave
     ret
.globl _global2               :全局符号golbal2
     .data                       ;位于数据段
     .align 4
_global2:                       ;全局变量的定义,初始化值
     .long     200             

int global1;
int main()
{
     global1 = 101;
     extern int global2;
     global2 = 201;
     return 0;
}
int global2;

    
.globl _main
     .def     _main;     
_main:
    ;....
     movl     $101, _global1  ;通过符号访问全局变量,这个符号可以在之后,或其他文件中定义
     movl     $201, _global2
     movl     $0, %eax
     leave
     ret
     .comm     _global1, 16     # 4         ;标明这是个未初始化全局变量,声明多个,但最后运行时在bss段分配空间
     .comm     _global2, 16     # 4

可以得出结论:全局变量独立于函数存在,所有函数都可以通过符号访问,并且在运行期,其地址不变。

编译与链接

看下面这个程序链接出错,找不符号a,print, 但生成汇编代码并没有问题。这是因为编译的时候只是把符号地址记录下来,等到链接的时候该符号定义了才会变成具体的地址。如果链接的时候所有符号地址都有定义,那么生成可执行文件。如果有不确定地址的符号,则链接出错。

#include<stdio.h>
int main()
{
     extern int a ;
     print("a = %d\n", a);
     return 0;
}

     .file     "fun.c"
     .def     ___main;    
     .section .rdata,"dr"
LC0:
     .ascii "a = %d\12\0"
     .text
.globl _main
     .def     _main;     .
_main:
     ;..
     movl     _a, %eax          ;通过符号访问全局变量a
     movl     %eax, 4(%esp)
     movl     $LC0, (%esp)
     call     _print               ;通过符号访问函数print
     movl     $0, %eax
     leave
     ret
     .def     _print;     ;说明print是个函数符号

全局变量的链接属性

全局变量的默认是extern的,最终存放在数据段,整个程序的所有文件都能访问,如果加上static则表明值能被当前文件访问。

#include<stdio.h>
static int a = 10;
int main()
{
     a = 20;
     return 0;
}

    
     .data
     .align 4
_a:                              ;全局变量a定义,少了glbal的声明
     .long     10
     .def     ___main;    
     .text
.globl _main
     .def     _main;  
_main:
     ; ...
     movl     $20, _a
     movl     $0, %eax

去掉int a前面的static产生的汇编代码为:

.globl _a                    ; global声明符号 a为全局
     .data
     .align 4
_a:
     .long     10
     .def     ___main
     .text
.globl _main
     .def     _main
_main:
     ;...
     call     __alloca
     call     ___main
     movl     $20, _a
     movl     $0, %eax

对于未初始化全局变量

#include<stdio.h>
static int a;
int main()
{
     a = 20;
     return 0;
}

.globl _main
     .def     _main;     .scl     2;     .type     32;     .endef
_main:
    ;..
     movl     $20, _a
     movl     $0, %eax
     leave
     ret
     .lcomm _a,16          ; 多了个l表明是local的未初始化全局变量

去掉int a前面的static
.globl _main
     .def     _main;     .scl     2;     .type     32;     .endef
_main:
     ;..
     movl     $20, _a
     movl     $0, %eax
     leave
     ret
     .comm     _a, 16     # 4          ;extern链接属性的未初始化全局变量

static局部变量

static局部变量具备外部变量的生存期,但作用域却和局部变量一样,离开函数就能访问
#include<stdio.h>
int fun()
{
     static int a = 10;
     return (++a);
}
int main()
{
     printf("a = %d\n",fun());
     printf("a = %d\n",fun());
}

     .data
     .align 4
a.0:                    ;static局部变量是放在代码段
     .long     10     ;分配空间初始化
     .text
.globl _fun
     .def     _fun;   
_fun:
     pushl     %ebp
     movl     %esp, %ebp
     incl     a.0
     movl     a.0, %eax
     popl     %ebp
     ret
     .def     ___main;   
     .section .rdata,"dr"

编译实际还是还是把static局部变量放在数据段存储(要么怎么可能在程序运行期间地址不变呢),值不过符号名会动点手脚(这样出了函数就访问不了了),同时候 多个函数中定义同名的static局部变量,实际上是不同的内存单元,互补干涉了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息