zoukankan      html  css  js  c++  java
  • 剖析可执行文件ELF组成

    对比参考:剖析.o文件ELF组成

    相比.o的ELF格式,有哪些变化?

    .rel.text和.rel.data消失了

    为什么这两个节会消失?

    链接器将各.o中同名的.text和.data节整合到一起时,会对整合后的.text和.data进行重定位。其实重定位时主要针对就是.text和.data节,不过这.text和.data节重定位时需要依赖.rel.text和.rel.data中的信息,一旦重定位结束后,这两个节的使命就完成了,自然也就会消失。

    多出了两个节

    init节

    作用

    这个节会提供_init等函数,专门用于实现程序的一些初始化。程序入口为_start,从_start开始执行后,在正式调用main函数之前,会先调用_init等函数进行程序的初始化(比如建立函数栈等等)。

    init节怎么来的

    回顾gcc链接的过程,

     collect2                         //链接程序
      -dynamic-linker  /lib64/ld-linux-x86-64.so.2     //动态链接器
             crt1.o  crti.o  crtbegin.o               //启动代码
             ccyIcm4A.o                      //自己程序的.o
             -lc                                //libc,常用c函数库——c标准库的子库
             crtend.o  crtn.o                   //扫尾代码
     
             init节就是由gcc提供的crt1.o、crti.o、crtbegin.o等.o构建而来的。
    “段头部表”节
    重定位时,链接器根据“链接脚本文件”所给的运行地址,给.text/.data中指令和变量重定位运行地址时,这些“运行地址”只是链接时理论上安排的。当加载程序到内存中运行时,就需要将硬盘上所存放的程序,搬到“运行地址”所指定的内存位置,段头部表中的内容,就是用来辅助加载程序的。

    “可执行目标文件”的各个节归类

    程序最终运行时,需要搬到内存上的节有:ELF/.init/.text/.rodata/.data/.bss。搬到内存上什么位置呢?搬到重定位的“运行地址”所指定的位置。

    ELF/.init/.text/.rodata:只读的存储段(代码段)

    .data/.bss:可读可写存储段(静态数据段)。之所以称为静态数据段,是因为.data/.bss的空间规划,是在编译时就进行了理论安排,并不是程序运行起来才安排的,所以被称为静态数据段。

     

    程序的加载、运行

    编译得到可执行目标文件后,就可以将“可执行目标文件”加载“运行地址”所指的内存位置,然后运行了。不过这里还是要分两种情况来看,第一种是裸机运行的情况,第二种是基于OS虚拟内存运行的情况。

    裸机的情况

    使用专门针对裸机的编译器来编译程序,最后得到的就是可以在裸机上运行的可执行程序。加载裸机程序时,由专门的加载程序(加载软件)来实现的。

    加载

    其实加载的过程就是将“代码段”和“数据段”复制到内存上。裸机时,链接器重定位后的“运行地址”是真实的物理地址,加载时直接将“代码段”和“数据段”复制到物理内存中“运行地址”所指定的位置。裸机运行地址是多少,可以由我们程序员自己来定。裸机时就不是ELF格式头了,而是bin格式头。

    运行

    ①CPU的PC(程序计数器)存放第一条指令_start的地址,也就是将PC指向第一条指令_start。pc是cpu的寄存器之一。

    ②从_start开始执行启动代码。

    ③启动代码调用_init等函数进行初始化。初始化有一件非常重要的事情就是,从内存划出一片空间出来用作堆和栈,因为空间是以堆和栈的方式来管理的,因此就称为堆 和 栈。

    ④启动代码调用main函数,main函数再调用各个子函数,我们自己写的代码就开始运行了。

    ⑤main函数调用return关键字,返回到启动代码。

    对于裸机的来说,返回到启动代码就结束了。至于return的返回值,有没有返回值,对于裸机来说都没有什么影响。就算有返回值,将返回值返回给启动代码后,这个返回值对启动代码来说也没有什么意义。所以说,对于裸机来说,其实main函数的返回值没有什么意义,所以大家在学习单片机时候,以前的main函数的返回值都是void的。

    void main(void)
    {
        return;
    }
    View Code

    不过现在都规范化了,单片机等裸机里面,也要求main函数的返回值类型为int型。

    int main(void)
    {
        return 0;
    }
    View Code

    尽管在这里要求返回int型的返回值,但是我们自己应该清楚,在裸机下,main函数的返回值并没有什么大的意义。

    栈、堆

    程序运行起来后,初始化代码会从内存中划出一片空间,用来作为程序运行所需要的栈和堆。

    栈(stack)

    栈的意思是,表示内存空间以栈这种数据结构来进行管理,所谓管理就是管理空间的开辟和释放。栈的特点是,只能在栈顶进行操作,不能够在栈的中间和栈底操作。

    栈是向下生长的

    所谓向下生长就是,栈底在最高地址处,当栈中没有任何空间被使用时,栈顶指针就指向栈底,每当栈顶被占用一个字节的空间,栈顶指针就向低地址方向移动一个字节。从高地址向低地址方向移动,就是向下生长,栈顶指针所指的那个字节是没被用的。栈顶和栈底之间的栈空间,就是被占用的空间。反过来,栈顶指针向高地址后退一个字节,就表示释放一个字节的空间。释放的意思就是将空间交出去,让别人可以使用。

    怎么理解栈顶指针?

    就是某个寄存器或者指针变量,专门用于存放栈顶字节的地址。

    栈的作用

    函数自动局部变量、形参等就开辟于栈。

    int fun(int a)
    {
        int b;
        ...
    }
    View Code

    不过这里有一点需要强调下,对于ARM来说,由于arm cpu内部寄存器比较多,所以如果形参在4个以内的,实际上形参是在寄存器中,而并不在栈中。如果超过4个的话,第4个往后的形参才会存在栈中。不过在intel的CPU上又不一样,因为Intel cpu的寄存器比较紧俏,所以形参基本都是存在栈中的。为了方便记忆我们一律认为形参都是在栈中的。从栈中开辟和释放自动局部变量、形参空间的过程,由函数被调用时,在运行的过程中自动完成的,无需程序员关心,开辟空间和释放空间的本质,其实就是栈顶指针移动的过程。

    堆(heap)

    堆空间和栈空间的管理方式是有区别的。

    栈的话只能在栈顶才能进行操作,但是堆不是,堆的话可以在中间任何位置操作。堆的空间是向上生长的,也就是说在堆中开辟空间时与栈相反,是从低地址往高地址方向延伸的。

    栈的空间是自动开辟和释放的,但是堆的空间不是的,堆只能手动开辟和释放。

    从堆里面开辟空间

    程序需要调用malloc函数来手动开辟。所谓手动开辟,就是程序员需要在程序中亲自调用某个函数来实现,至于说在堆中什么位置开辟空间,这个由malloc函数的算法来决定。

    释放在堆中开辟的空间:

    在程序中调用free函数,手动释放。释放的意思,也是将空间让出来,让别人可以使用。

     

    基于OS虚拟内存的情况

    基于OS运行程序时,常见有两种方式

    ①在图形界面,双击快捷图标实现

    ②在命令行,执行./a.out命令实现

    每一个进程都是运行在自己的独立虚拟内存中的,命令行和图形界面本身也是一个程序(进程),所以也是运行在自己独立的虚拟内存上的。

    程序的加载

    当我们双击程序,或者执行./a.out命令时,就开始了程序的加载操作,具体步骤如下:

    ①首先从父进程复制出一个子进程

    图形界面、命令行程序就是父进程,执行程序时会从父进程复制出子进程,复制的目的其实就是从父进程的“虚拟内存”复制出一个子进程的“虚拟内存”,准确讲应该是复制出“虚拟内存”的相关数据结构,用于建立子进程的虚拟内存。有了子进程的虚拟内存,就可以将新的程序加载到虚拟内存中了。虚拟内存空间被分为了两部分,一部分是内核空间,另一个部分是应用空间,应用程序的应该加载到应用空间。在Linux下复制子进程时需要调用Linux OS所提供的fork函数。至于虚拟内存与真实物理内存之间的对应关系,这个事情就留给“虚拟内存机制”来操心。

    ②调用加载器

    将自己程序(新程序)的“代码段”和“数据段”加载到子进程虚拟内存的应用空间中。

    基于Linux运行的话,gcc链接时重定位的运行地址是从0x08048000或者0x0000000000400000开始的,所以程序会被加载到虚拟内存中0x08048000或者0x0000000000400000地址往后的空间中。至于虚拟内存0~0x08048000或者0~0x0000000000400000之间的虚拟空间,则未被使用。基于Linux OS运行时,加载器是由Linux OS提供的,任何一个程序都可以通过execve这个系统API来调用加载器,为了方便称呼,我们就直接将“execve函数”称为加载器。

    运行

    ①cpu的pc指向_start(将第一条指令_start所在位置的虚拟地址存放到pc)

    ②从_start开始执行启动代码。

    ③启动代码调用_init等函数进行初始化。

    其中很重要的就是弄出堆和栈这两个东西,这一点与前面裸机的情况时类似的,这里不再赘述。不过与裸机不同的是,在栈和堆之间,还有一个“共享映射区”。

    ④启动代码调用main函数,main函数再调用子函数,我们自己写的代码就开始运行了。

    ⑤main函数调用return关键字,返回到启动代码。

    有OS时,main函数将返回值return给启动代码后,启动代码会调用exit函数,接着将返回值返回给OS。在裸机情况下,启动代码不存在调用exit函数这一说,只有基于OS时才存在这种情况。

    对比裸机运行和基于os虚拟内存运行时,程序的内存结构(内存布局)

    内存结构,其实就是程序运行时在内存中存储结构。不管是裸机还是基于OS虚拟内存运行的情况,内存布局基本都差不多,下面讲解虚拟内存的程序内存布局。

    程序在内存中存储时,就存储两个东西,一是指令,二是数据。

    指令存储在代码段中的.init和.text节中。.init节:放启动代码相关的指令     .text:主要放我们自己所写程序的指令

  • 相关阅读:
    101与金根回顾敏捷个人:(93)《做最好的自己》之成功同心圆
    ArchiMate - 发布【企业架构语言ArchiMate v0.5.pdf】
    blog推荐 - 软件产品管理之Tyner Blain
    家庭创意:春节寻宝习俗
    流程 - 发布【敏捷方法之Scrum v0.2.pdf】
    个人管理 - 使用Scrum来敏捷自己
    blog推荐 - 电子图书与IT文档资料(ITPUB论坛)
    架构语言ArchiMate -应用层(Application Layer)
    架构语言ArchiMate - ArchiMate提供的基本视角(Viewpoints)介绍一
    故事:两只老虎的悲惨结局
  • 原文地址:https://www.cnblogs.com/kelamoyujuzhen/p/9416117.html
Copyright © 2011-2022 走看看