关于编译链接原理的总结

编译和链接

编译链接过程大致如下:

 一、预编译(生成*.i文件) 

(1)将所有的“#define”删除,并且展开所有宏

(2)处理掉所有条件预编译指令,如“#if”“#ifdef”“#elif”“#else”“#endif”

(3)处理“#include”指令(这是一个递归过程)

(4)删除所有的注释 (“//”“/* */”)

(5)添加行号和文件名标识

(6)保留所有的#pragma编译器指令,待编译器使用

二、编译(生成*.s文件)

把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相对应的汇编代码文件。

三、汇编(生成*.o文件,也叫目标文件<可重入的二进制文件>)

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。

汇编完成后生成的目标文件中存放在.data和.bss段的变量都要有一个名称来标识这个变量,这个名称称为符号。

存放这些符号的符号表的内容如下:

可以看到gdata3并不是存放在.bss段,这个数据存在*COM*这个块中了。

这个和C语言中的强弱符号有关系。

强符号:全局的已初始化的符号

弱符号:全局的未初始化的符号

强弱符号的选取规则:

  1. 两个强符号  编译报错
  2. 一个强符号 一个弱符号  选择强符号
  3. 两个弱符号 根据不同编译器处理方式不同

 一个项目可能有多个源文件。编译阶段都是每个文件单独编译的。可能在其他文件中存在强符号,所以没办法在编译期间确定具体的符号。因此将本文件的弱符号存放在*COM*块,而不是.bss段。

对于引用的外部符号大家可以看到符号表中是*UND*标志。也就是说这个符号没有确定具体定义的地方。

将链接完成后的可执行文件sum.omain.o手动链接一下。

链接阶段链接器只关注符号表中的全局符号。

       

可以看到链接后,对每个符号给出了具体的虚拟地址。gdata3存放在.bss段了。链接完了以后就可以确定gdata3这个弱符号的符号选择了。对于.o文件中引用的外部符号也确定了具体的定义位置,

把这两个外部符号放到对应的段中。

符号解析和符号重定位就发生在这个过程中。

什么是符号解析:

在每个文件符号引用(引用外部符号)的地方找到符号的定义。这就是符号解析

什么是符号的重定向:

链接前生成的汇编代码:

这两处分别是 0000 0000代表链接前编译器给gdata给出的地址,

FF FF FF FC表示链接前编译器给Sum函数给出的函数的入口地址。

这两个地址都是无效的地址。再看看链接后这两条地址。 

这两个地址变成gdata的具体虚拟地址和Sum函数的相对位移偏移量。

而符号重定向就是对.o文件中.text段指令中的无效地址给出具体的虚拟地址或者相对位移偏移量。

可执行文件的头部信息:

这个时候这个程序的入口地址已经给定080480A4这个就是main函数的入口地址。

现在程序就可以运行了。

函数堆栈调用

1.压实参(给形参开辟内存并初始化)自右向左

2.压下一行指令地址

3.压调用方函数的栈底地址

4.跳转到被调用方函数栈帧

5.开辟被调用方函数运行所需占的空间

问题:

1.函数实参怎么传给形参?形参有没有开辟内存?如果形参开辟内存,在哪里开辟的?

2.函数的返回值怎么返回到调用方函数?

3.函数返回后怎么知道从哪条语句开始继续执行?

  首先看一段代码:

#include<stdio.h>

int Sum(int a,int b)
{
    int tmp;
    tmp = a+b;
    return tmp;
}

int main()
{
    int a1 = 10;
    int b1 = 20;
    int rt = 0;
    rt=Sum(a1,b1);
    printf("Sum(a,b):%d",rt);
    return 0;
}

 

 再来看下在调用点的汇编指令

  rt = sum(a1,b1);

  0040108D   mov         eax,dword ptr [ebp-8]

  00401090   push        eax

  00401091   mov         ecx,dword ptr [ebp-4]

  00401094   push        ecx

  00401095   call        @ILT+0(sum) (00401005)

  0040109A   add         esp,8

  0040109D   mov         dword ptr [ebp-0Ch],eax

  call指令在调用时有两步:

  压入下一行指令地址  0040109A

  jmp跳转到sum函数栈帧中

 下面是进入Sum函数栈帧前的main函数的栈帧布局

可以看到形参变量a,b开辟了内存空间,是在调用方开辟的。

再看下进入sum函数后栈帧布局

接下来看函数在退栈后,具体做了哪些事情。

由上图可知,return后返回值赋给了eax寄存器,由eax寄存器带回main函数赋给rt。

紧接着出栈了Sum函数栈顶的三个寄存器。

然后看 mov  esp,ebp这条指令,这条指令将esp指向的地址指向了ebp指向的地址。这条指令就是将开始进入Sum函数开辟的一些内存“清理”掉了。

接下来pop ebp这条指令,是将当前栈顶的数据出栈并赋给ebp。现在当前栈顶的数据是main函数的栈底地址。也就是Sum函数调用完成后能返回到main函数栈帧上来。

最后执行ret指令,这个过程中执行了一个pop指令,将当前栈顶的数据出栈并赋给下一行指令寄存器。当前的栈底数据是main函数调用Sum函数的call指令的下一条指令地址。也就是add  esp,8这条指令的地址0040109A

回到main函数后就可以接着下一条指令继续执行了。

关于返回值:

返回值大小 

                                     

                                          带出方式

 

0-4字节

                               

                             eax寄存器带回来

 

5-8字节

                           

                             eax + edx 寄存器带回返回值 

                        

大于8字节        

 

                             在调用方开辟临时空间

                             利用开辟的空间将返回值的内容保存起来                                                                                                                                                                

栈:函数活动所需要的空间

栈帧:一个函数活动所需要的空间

调用约定:

1.函数的符号生成

2.入栈顺序 (以下4种调用约定都是从右往左入栈,因此支持可变参数)

3.形参的开辟和清理方式

__cdecl (c的标准调用约定)    形参空间由调用方开辟,调用方清理

__stdcall (windows的标准调用约定)   形参空间由调用方开辟,被调用方清理

__fastcall (快速调用约定)     不开辟形参空间,因此也就没有释放的概念

__thiscall (c++类成员方法调用约定)

四、链接(生成*.exe文件,也叫可执行文件)

1.合并段和符号表

2.符号解析

3.地址和空间分配

4.符号重定位

链接阶段的地址空间分配:

在Linux下的虚拟地址空间中,用户空间、内核空间以3:1划分,即3G大小的用户空间和1G大小的内核空间。

虚拟地址空间各区域划分如图:

虚拟地址空间引入的原因:进程地址空间需要隔离,防止恶意的程序修改其它程序的内存数据,因此计算机中引入虚拟地址空间

静态链接(编译时)

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

优点: 只需保证在开发者的计算机有正确的库文件,在以二进制发布时不需考虑在用户的计算机上库文件是否存在及版本问题.

缺点: 生成的可执行文件体积较大。当初正是为了避免此问题,才开发了动态库技术。

动态链接 (加载, 运行时)

所谓动态链接,就是把一些经常会共用的代码(静态链接的OBJ程序库)制作成DLL档,当可执行文件调用到DLL档内的函数时,操作系统才会把DLL档加载存储器内,DLL档本身的结构就是可执行档,当程序有需求时函数才进行链接。透过动态链接方式,存储器浪费的情形将可大幅降低。静态链接库则是直接链接到可执行文件。

DLL档本身也是可执行文件, 在程序执行的时候直接进行动态调用即可.

静态链接和动态链接的比较

静态链接

动态链接

编译时

加载, 运行时

lib在编译时就组装进exe文件

程序运行时exe文件可以动态的加载dll

不用考虑计算机库文件版本

节省内存, 维护性高

整个软件包只有exe文件

软件包中有exe和dll

lib文件是外部函数和变量, 在编译时复制进目标程序, 后缀为.a   dll文件本身是可执行的, 在运行时动态链接, 可以包含源码, 数据, 资源的多种组合, 后缀为.so

经过链接器的作用形成可执行文件, 最后还要进行一步操作, 进行打包. 即将生成的可执行文件(.exe, .dll, .lib)文件进行打包. 交付给计算机即可运行.

五、运行阶段

运行阶段要做的事情:

1.创建内核映射结构体(PCB)

2.建立虚拟空间到物理内存的映射(通过分页机制)

3.加载指令和数据

4.把程序的入口地址写入下一行指令寄存器中

 

关于全局变量与局部变量各自存储在什么段的思考:

代码:

#include<iostream>


int gdata1=10;//.data

int gdata2=0;//.bss

int gdata3;//.bss


int main()

{

   int ldata1=30;//.text

   int ldata2=0;//.text

   int ldata3;//.text

   static int ldata4=40;//.data

   static int ldata5=0;//.bss

   static int ldata6;//.bss

   return 0;

}

总结出规则如下:

.text 段:存放可执行代码,字符串字面值,只读变量(可读,可执行)

.data段:存放已初始化且初始化不为0的全局变量和静态局部变量(可读,可写)

.bss 段:存放未初始化或初始化为0的全局变量和静态局部变量(可读,可写)

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值