编译和链接

  我们写的代码是如何变成程序能被计算机执行呢?要运行必须先把"一些东西"加载到内存里边,把什么"一些东西"加载到内存里面?
  其实我们的代码就产生了两种东西,一种是数据,一种就是指令.
  而数据肯定不能混乱着放在一起,必定有区分区域,那么有什么区域呢?划分依据又是什么?
  程序跑起来后,不会直接把数据直接搞到物理内存上,因为操作系统要屏蔽底层的差异,统一管理资源的分配。因此系统会先给每个程序一个虚拟地址空间,每个虚拟地址空间的大小为4个G即2^32,每个程序都会有,因为是虚拟的。不是占用你的实际内存,所以不用担心内存够不够用的问题。
   
  下面为虚拟地址空间配分的示意图
     首先整个4G空间被划分2个部分,一个部分为用户空间,大小为3个G,地址从0x00000000-0xC00000000
另一个部分为内核空间,大小为1G,地址从0xC0000000-0XFFFFFFFF;
其中用户空间每个程序都是独立的,每个程序都有自己的3G虚拟地址空间的用户空间。而内核空间却是共享的。因为里面操作系统就只有一个,一些系统运行的状态,数据等必须统一,如果独立的话就乱套了。
.txt段就是代码段,正文段,存放我们程序指令代码的地方。
.bss段和.data段就是存放数据的地方。 
何为数据何为指令?看看下面这个图

其中这些data开头的变量都是数据,而a,b,c为指令。
为什么要这么区分?因为data前缀的这些数据都是程序运行后始终存在的,不会说一会儿有一会儿没有的。
而a,b,c这些,你可能会认为,不是也把数据赋给他们了吗。你要知道a,b,c的数据都是程序运行后,是开栈放入的,也就是说,如果我完全可以设计一个判断,让它跳过给这个变量放入数据,那么这个变量的存在就变得不确定性,并且这些存在周期只在代码块中的变量,执行过了它的存在域就会出栈销毁,我不可能把他放入始终不会销毁的数据段去占用空间。所以我们把a,b,c这些变量还有其他代码语句当作指令。

而数据段也分为data段和bss段。
bss段全名better save space 更加节省空间。 因此我们就把那些定义初值为0的,或者没有给初值的都放在.bss段 .bss段是不占用文件空间的。他只是记录了有几个bss的数据,反正值都是0。有必要开辟几块4个字节整形 去存放我已知的0?注意这里bss节省的空间指的是节省文件的空间,虚拟地址空间还是占用的。
而那些初值不为0的数据,我就必须要记录下数据究竟是什么,因此要放到data段。是实际占空间的。
虚拟地址空间的主要段先介绍这些。

我们接下来看编译的过程
首先 编译分为下面几步
1.预编译
  删除代码中的注释  //
  处理预编译的命令(#开头的   比如#define    #if等)

  最后生成 ***.i的文件

2.编译
           词法分析
           语法分析
           语义分析
           所有符号的汇总  (数据名都产生符号,指令中函数产生符号即函数名)
           代码的优化


最后生成.  ***.s 的文件


3.汇编
  ***.s 里面都是汇编指令 比如  什么move add lea等
将其翻译为特定平台的机器码  二进制形势的
构建***.o ***.obj文件的组成格式

 最后生成  ***.o  ***.obj文件  (学名 可重定位的二进制文件)
 其实.o和最终的exe差不多,但为啥.o不能运行?
 那是因为缺少链接阶段的最重要的符号的重定位


然后就该 链接
大致分为两步
1. 合并编译后生成的所有***.obj文件的  调整段长度
    合并符号表,解析符号   
   解析完后就要分配内存地址了,因为每个.o文件的地址之前说了都是独立,可能相同的。你我的偏移量完全可能相同。因此就要分配内存地址,避免冲突
2. 符号的重定位


上面这是大概概括了一下编译链接,

我们看看编译汇编生成的.o文件
这是test.c


编译生成test.o文件,使用命令查看下文件的段信息


眼熟的几种段出来了。我们看红色括起的部分,.text段的岂是地址为0x34,换成十进制也就是52。也就是说前面还储存了别的东西。那就是ELF Header,使用查看ELF命令

可以看到的确这个Elf的大小也就是52字节,和.text段的起始偏移符合



上上上个图黄色括起的部分可以看到.bss段和.comment段。但是他们的起始偏移居然一样,也就是说.bss实际并没有占用任何空间,虽然它的size显示为0x14即20个字节。这就证明了我刚说的,bss段并不占文件的空间。
但如果不存储任何数据,那么怎么知道这几个.bss数据的存在呢。虽然.bss的数据都是0,但有几个0呢?

因此我们看看上图中的
start of section headers   208这一行      208也就是十六进制的D0,这个D0是个什么东西?
我们使用命令查看所有的段

 也就是说从D0开始有个 段表,它负责记录之前段的详细信息,D0就是段表段的起始偏移量。
我们就可以通过.o文件中的段表段记录的.bss信息知道有几个.bss数据了。

但是我们回想一下,我们定义的.bss数据应该有几个? 是不是应该有data2,dat3 data5 data6  data 8 data9
6个4字节数据,应该是24字节吧,但是记录的size只有0x14,即20个字节,为什么少了4字节呢?

这就要说C语言的强弱类型(强弱符号)。
强类型就是初始化后的数据。  比如全局变量 int data1=10;
弱类型就是未初始化的数据。  比如                int data3;
如果一个工程中,有两个文件,分别定义的了data1的强类型数据,那么就会编译错误,因为符号重定义了
如果取一个文件中是强类型,另一文件为若类型定义了2个重名数据,那么用的时候就会取强类型那个数据,
如果一个强类型是short a=10 ,一个弱类型是int a ;弱类型那个文件中如果有给其写入数据的代码,翻译成汇编就是给a后的4个字节写入某个数据,然而这个写入的对象a并不是int a,而是最后取的强类型shorta,这样就会覆盖short a那个文件中a数据本身的全部两个字节,并且会把short a后两个字节的内存,累积4个字节全部写入成某个数据。这样就会有问题
两个弱类型的话。一般就会取空间比较大的,假如int 和short俩弱类型,就会取int,当然有时不是这样,这是由编译器决定的。

因此我们就知道其实 data3按理来说应该是.bss段的。但是它是弱类型数据,又是global符号。我们无法得知其他.o文件是否有个 同名的强类型的数据起冲突,因此我们将它放到 "COM"块中,等待链接时的定位。
而static int data6;这个虽然是弱类型,但是由于它前面是static修饰的,这个data6符号为local符号只对自己本身文件可见,因此就不需要判断其他文件中是否有重名的强类型数据。

我们看看符号表

可以看到data3在COM块中

那么字符串常量在.o文件的哪个段中呢。比如我main中写一句char *p="hahaha" 
我们知道p是指针变量,程序运行时开栈才有的。
但后面这个“hahaha”显然是一直存在的。
我们再使用一个查看段内容的命令看看

可以看到红色括起来的部分有个新出的.rodata段,而它的内容正是我们的hahaha。
.rodata段其实只读数据段,r(read)o(only )data(数据)。

我们再来看看.o文件里的指令段内容都是什么
重写个test2.c  里面声明一个sum函数,和一个extern int data10的变量;我们看看里面会出现什么



我们可以看到没有链接时,出现了不是本地的符号时,都用特殊的值替代着,比如call,也就是函数调用,应该先存着函数下一条指令的地址,然后跳到那个函数的指令地址。由于sum是声明。编译时编译器认为真正的定义的可能在别的文件中,因此无法写入真正的函数指令地址,所以用特殊值替代了。

而编译完后链接时,主要工作,就是找那些非本地的glable符号,然后符号表合并,解析符号,得到新的合法的 虚拟空间的地址,然后把  编译时  这些用特殊值替代 无法确定的符号 的地方全部补成真正的值。就是为啥.o叫可重定位的二进制文件,就是为了链接时重定位符号而准备的。

之前说了链接首先是 合并编译后生成的所有***.obj文件的 段 ,我们上面刚已经大致了解了.o中的各个段的内容和作用,那么合并.o的段该如何合并呢?
.o文件里段的对齐方式为4个字节
而可执行文件的对齐方式为系统页面的大小,也就是通常所用4K
如果直接让每个.o文件的段累到一起,那么势必会非常浪费,如果所有.o文件的每个段的内容都几乎没有,这要 将近白花多少个4k啊


怎么才能高效又节约的合并呢?
相信不少人一开始都认为是让 所有的 .o文件的同一种段合并在一起。 

其实这种合法比起上面的好一些,然而却不是最终的合并方法。

最终的合并规则是把具有相同属性的段组织一个页面上。什么是相同属性?就相当于读写权限
把可读可写的划分一起,只读的划分在一起,只写的划分在一起
像.data和.bss都是可读可写的,因此他俩就合并一起。.text是只读的指令,因此它又合并在一个地方。

合并后就是解析符号,给符号分配地址,然后对之前编译后那些未确认的glable符号进行符号重定位。
我写了sum.c 里面就是 sum函数的简单定义 和  data10的定义
我们看看链接后的符号表

data3和data10 连接后 所在的分段就变为它们应在的地方。
而对比链接前后的汇编代码呢
也发现特殊值的地方全部被替换为真正的值了。