目录
1.温故知新
2.ELF文件介绍
3.ELF文件组成
4.ELF文件形成到加载
5.连接过程
1.温故知新
上一篇博客,我们介绍了我们的动静态,知道了我们的库其实也是文件,如果我们想写一个库也是可以的,我们的把我们的库文件编译成.o文件,然后把它们进行打包.a文件,形成静态库,然后我们把我们的程序和库进行打包编译,就把我们的静态库和我们的代码进行合并,我们就可以使用库的函数了。动态库需要我们把它的库文件放在我们的默认查找路径下/lib/64下,让我们可以找到我们的库文件就可以了,而至于我们的头文件,我们只需要让我们的文件找到它就可以了。
2.ELF文件介绍
首先我们知道ELF文件是什么?是文件啊。是文件就有内容和属性,了解它无非就是内容上了解,属性上了解。
我们的.o文件,可执行程序,.so文件,core dump都是我们的ELF文件,所以我们想要进一步了解编译链接的细节就需要来了解我们的ELF文件。
3.ELF文件组成
我们的ELF文件由四部分组成:ELF头,程序头表,节头表,节组成。
我们的ELF header里面定位文件的其他部分。ELF文件内容对我来讲,就是一个数组。
因为我只要知道他这个文件的起始位置和它们的大小,我们就可以找到这个文件的任意位置了。
ELF文件内容里面的四个部分。
我们内核里面ELF的代码是有这个程序的入口的,它位于我们文件的开始位置,可以定位文件的其他部分,它也有我们程序的入口,让我们编译器知道从程序的哪个位置开始执行。
4.ELF文件形成到加载
我们进行动静态库链接需要先把.c文件编译成.o文件,我们还需要对多个.o文件进行合并。
但是这个合并是在我们链接的时候进行的。我们做的是把我们.o库文件进行打包。
我们的ELF文件有多种不同的Section,在加载到内存的时候,也会进行Section的合并
我们的节头表在链接时起作用,节头表里面有对每个节的描述,链接的时候可以根据对节的描述把节进行合并成段,提高空间利用效率,而我们的程序头表里面列举了每个段的描述和属性,知道每个段在哪里分开,把每个段进行分开,这样可以帮助我们加载的时候告诉我们OS怎样加载可执行文件完成进程内存的初始化。
说白了就是节是具体的,告诉怎么合并,程序头表是告诉段的划分是怎么样的,让OS进行内存初始化。节头表是我们链接的时候,程序头表告诉我们怎么把我们链接好的文件放到内存里进行执行。
上面我们说过,我们ELF头可以帮助我们定位ELF文件每个部分,所以我们编译器只需要知道我们ELF节头表,就可以对节进行合并了,合并好了就链接好了啊。
当我们想要执行的时候我们的OS可以找到这个文件ELF头表,再根据头表找到程序头表,知道哪些模块要加载进内存。知道我们程序的加载。
所以我们上面说我们OS和编译器都要知道ELF头表因为我们链接时要用,加载时也要用。不知道
ELF怎么完成这个工作呢?
实践中,当我们将一个.c文件编译成我们的.o文件我们去查看我们的文件,发现我们文件中调库函数地方的地址是没有的。
这是因为我们只有函数的声明,我们还没有对我们的,o文件进行链接,找不到具体的实现方法,所以这里学习的是不是和我们以前介绍的对上了呢?因为没有链接,所以找不到具体的实现,但是头文件里面有我们函数的声明,所以并没有报错。
我们静态链接就是把我们的.o进行合并,链接合并的时候,链接器会根据我们的重定位表对函数的地址进行重定位从而形成我们的可执行文件。这就是静态链接的过程。
我们链接之后我们发现我们的函数地址就被填充上了,我们就可以找到对应的实现方法了。
所以链接在干什么?就是把我们不知道函数具体实现的地方找到了函数实现的地址,对地址实现了重定位。
这里我们终于知道了为什么.o叫做可重定位文件了,因为它需要重定位才可以执行,不然我们只有函数声明,没有函数实现,执行个毛!
两个.o代码合并到一起,进行统一的编制,链接的时候,修改.o中没有确定的函数地址,进行相关call地址,完成代码调用。
5.连接过程
事实上,我们对ELF进行编制,采用平坦模式进行编制,说白了就是从000-fff进行线性统一编制。
磁盘可执行文件,就是起始地址+偏移量这种地址,只不过我们平坦模式的起始地址是0.
我们给磁盘中这种文件编制的地址叫逻辑地址。它和我们加载到内存的虚拟地址是一样的。
如同一个硬币的两面,硬币翻面了还是那个硬币啊!
在我们的磁盘中,我们的文件就有了逻辑地址了,所以我们可以看到我们的文件还没有被加载到内存的时候,就已经被编制了,并不是加载到内存才有得地址。
而且我们的虚拟地址初始化的时候还需要依赖ELF文件,每个segment有自己的起始地址和结束地址,就可以用来初始化虚拟地址了。
根据下面这张图,我们来做一个总结,我们的ELF文件在我们的磁盘中的时候就有了虚拟地址了,然后当我们的可执行程序被加载到内存的时候,我们会拿到它的虚拟地址,在页表中为它建立映射关系,根据ELF合并之后的段对我们的虚拟地址空间进行初始化,也就是虚拟地址对物理内存映射的建立过程,然后我们就根据ELF文件提供的内容完成了我们可执行程序从硬盘加载到内存调用的过程了。
给到我们的虚拟地址,当我们调用我们的程序的时候,根据我们的MMU,我们就找到了物理内存的实际位置进行调用。
这里要说明的是,当我们建立虚拟和物理的映射关系时,不会立即在内存分配空间,而是当你调用的时候再为你分配空间。这个是我们缺页异常导致的,就好比我们fork一个子进程,只有当我们修改的时候我们才会写实拷贝,这是因为我们进行权限检查发现越界了,OS检查后对它进行了修改。类似!
我们下期再见!