一个编译链接的过程详解(转载)

1. 操作系统存储管理模块的设计与所基于的硬件环境息息相关.

2. 编译:绑定变量到一个0开始的相对地址空间中.某些变量(如C中的局部变量)存储在运行栈中,其地址要等到在链接时与其他模块绑定到一起才能确定.对于处在不同的可重定位模块中的函数地址,编译的过程也不能确定.故自动注释每个这种对外部地址的访问,待链接时能在代码中设置正确的地址.
链接:转换程序所生成的各种数据段和可写的重定位模块绑定在一起,形成一个数据段,然后链接程序通过改变所有地址的基址,来重定位指令中的地址.
加载:加载时地址再次调整.一旦加载器确定了程序映象要加载的实际位置,就会把程序中的地址与物理存储位置绑定在一起.


3. 动态地址重定位小结:

3.1 采用malloc的方式:实际上是将未分配的堆空间的部分地址帮定到了进程地址空间.当然进程也可以有自己的堆空间,malloc的实现也可以采用在进程堆空间中划出空间的方法.

3.2 硬件重定位的方式:在CPU中包含三个重定位寄存器,将代码,栈,数据段分别作为分离的重定位模块来进行管理.在每个函数的入口点处,初始化代码加载代码和数据段寄存器,使它们指向对应可重定位模块的绝对映象分区(初始化代码使用的地址由链接程序提供).所有的跨模块跳转会导致代码段寄存器的改变.所有跨模块的数据访问会导致数据段寄存器的改变.
在这种模式下:PC=代码段寄存器+段中相对地址
每个可重定位寄存器可以设置一个与之伴随的界限寄存器,其中放存储段长.
重定位寄存器与界限寄存器实际上可以置于MPU core与Memory Controller的物理接连之间.在相对地址从MPU core送出的时候,重定位寄存器和界限寄存器可以并行的进行逻辑计算,若越界,则由界限寄存器相关的硬件逻辑产生一个中断发往Interrupt Controller.


4. 页式虚存

4.1 页表记录逻辑页号与实际页号的对应关系.MMU是连接在CPU地址线与存储控制器之间的地址转换芯片.经MMU转换后的地址直接送往MAR(Memory Address Register).
页表存放在内存中由OS core管理.也可以采用多级页表,这样查找时的访存次数会不同.在进程切换时需要向MMU重新初始化进程的页表地址.
可以在MPU系统设计中采用TLB(Translationlookaside buffer),作为页表项的一个cache,减小页表平均查找时间.

4.2 页的载入:可采取在进程被创建时为其分配固定大小内存空间(固定的页帧数)的方法.在缺页时FIFO、LRU、LFU等调度策略都可以采用.



实例篇——静态地址重定位时一个编译链接加载的简单示例

有代码段:
static int gVar;
extern put_record(int iNum);

int proc_a( int arg ){
    ...
    gVar = 7;
    put_record(gVar);
    ...
}}

1.gVar是一个模块内局部变量(因为static关键字),编译器为其在proc_a同一模块内分配内存空间.假设给其分配到相对位置0x36.并在模块内符号表里记录符号的内存地址.
2.外部应用和定义组成了一张External reference table.编译器在链接时会对ERT中的每一个表项,在全局范围内查找其绝对位置(通过查找别的模块的符号表),将变量的内存地址写入符号表中.
3.对于在当前模块外的put_record()函数入口地址,编译器会在编译生成的中间代码中(.lib中),临时的将变量符号名填入需要用到变量的地方,等待在链接时替换为其绝对地址.

通过编译过程,生成的中间代码如下:
相对地址        代码
0000
....
0008       entry     proc_a
....
0036       [space for gVar variable]
....
0220       load       R1,=7
0224       store      R1,0036
0228       push       0036
0232       call       'put_record'
....
0400      [external definition table]
0404      put_record  0232
    
....
0600      [optional symbol table]
0604      gVar 0036
0630      proc_a  0008
....
0799     [last location in the module]

    链接时将各个可重定位模块绑定到一起.首先用户声明的前后顺序,将各个模块连接起来.在一个个模块绑定的时候,会根据模块被连接到的绝对基地址,对模块内被引用了的地址进行重新运算.
    在将各个模块连接到一起后,链接程序搜索整个模块中的外部地址引用表[external definition table],将相应位置的临时符号(如这里的'put_record')替换为该符号的实际地址.
    在链接过程以后,external definition table失去了作用,被删去,而符号表被汇集成一个全局符号表,可以有选择的保存在程序的某些位置(通常是尾部).
上面的模块经过链接后,得到下面的代码:

相对地址         生成的代码
0000
....
1008          extry proc_a
....
1036          [space for gVar variable]
....          
1220       load       R1,=7
1224       store      R1,1036
1228       push       1036
1232       call       2334
1399       (end of proc_a)
           (other modules)
2334       entry  put_record
....
2670       (optional symbol table)
....        gVar 1036
            proc_a 1008
            put_record 2334
2999        (last location in the module)

通过上面的过程,便形成了一个可以被加载器加载的二进制文件(.out,.bin等等【不清楚.exe算不算,所以没列出来】).当然,二进制文件也可以通过无损压缩,形成容量小的多的镜像文件(如.srec)

在实际加载程序时(如用户要求操作系统加载文件系统中的.out文件,或者用户双击了一个.exe文件),操作系统的加载程序会将二进制代码拷贝到内存中,并在运行前做最后一次地址重定位.

假设程序被加载到从内存地址4000开始放置.则程序中所有要访问到的程序、数据位置都要在加载前调整.调整后的最终内存中程序如下:

0000        (别的程序,这个地址多半被映射到ROM上去了)
4000        (other module)
5008          extry proc_a
....
5036          [space for gVar variable]
....          
5220       load       R1,=7
5224       store      R1,5036
5228       push       5036
5232       call       5334
5399       (end of proc_a)
           (other modules)
6334       entry  put_record
....
6670       (optional symbol table)
....        gVar 5036
            proc_a 5008
            put_record 6334
6999        (last location in the module)  

最后来小结一下:

1.符号表的作用是为了debug的时候能让debugger把代码和当前内存位置对上号.当然如果没有符号表代码尺寸会减小很多.所以final release里面肯定不能带符号表.

2.外部引用符号表主要是在编译阶段使用,在链接确定外部符号地址后,这个表项就可以删除了.

3.试想一下这种情况:一个模块里面的外部符号引用太多了(extern关键字用的太多了),导致编译的时候外部符号表溢出!
一个哥们以前在中兴工作的时候就遇到过这样的情况.

4.通过写这篇读书笔记,感觉自己对编译链接过程的理解也加深了不少.
  当然我们不会去做编译器,有机会做操作系统内存管理部分的人也不会太多.不过每天都接触到的compile and link,了解深一点总是没有错的,有些时候面对一大堆的link error,想一下背后的原理,也就觉得了然了.

你可能感兴趣的:(一个编译链接的过程详解(转载))