对比参考:剖析.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;
}

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

int main(void)
{
return ;
}

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

栈、堆

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

栈(stack)

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

栈是向下生长的

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

怎么理解栈顶指针?

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

栈的作用

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

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

不过这里有一点需要强调下,对于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:主要放我们自己所写程序的指令

剖析可执行文件ELF组成的更多相关文章

  1. [转]linux,windows 可执行文件(ELF、PE)

    ELF (Executable Linkable Format)UNIX类操作系统中普遍采用的目标文件格式 . 首先要知道它有什么作用:工具接口标准委员会TIS已经将ELF作为运行在Intel32位架 ...

  2. 可执行文件(ELF)格式之讲解

    ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西.以及都以什么样的格式去放这些东西.它自 ...

  3. linux,windows 可执行文件(ELF、PE)

    现在PC平台流行的可执行文件格式(Executable)主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format ...

  4. Linux 可执行文件 ELF结构 及程序载入执行

    Linux下ELF文件类型分为以下几种: 1.可重定位文件,比如SimpleSection.o: 2.可运行文件,比如/bin/bash. 3.共享目标文件,比如/lib/libc.so. 在Linu ...

  5. Linux命令——ldd和ldconfig

    转自:Linux系统中“动态库”和“静态库”那点事儿 前言 在调试lua脚本的时候,报错. 我已经再lua脚本中更改了cpath package.cpath = package.cpath .. &q ...

  6. 预处理、编译、汇编、链接、启动代码、相关command

    被忽略的过程 对于C这种编译性语言,我们平时编译时,不管是通过IDE图形界面,还是通过命令行,总感觉编译一下就完成了,然后就得到了针对某OS和某CPU的二进制可执行文件(机器指令的文件).但是实际上在 ...

  7. 【Intel 汇编】ELF文件

    ELF文件格式是一个开放标准,各种UNIX系统的可执行文件都采用ELF格式,它有三种不同的类型: 可重定位的目标文件(Relocatable,或者Object File) 可执行文件(Executab ...

  8. arm嵌入式交叉编译工具链

    1.arm-linux-gcc 常用的参数:-o[制定输出文件名] -c[只到编译停止,不连接] -g[键入调试信息] -xO[优化级别] -w/W(警告等级) arm-linux-gcc -o de ...

  9. Linux内核分析——期末总结

    Linux内核学习总结 首先非常感谢网易云课堂这个平台,让我能够在课下学习,课上加强,体会翻转课堂的乐趣.孟宁老师的课程循序渐进,虽然偶尔我学习地不是很透彻,但能够在后续的课程中进一步巩固学习,更加深 ...

随机推荐

  1. c# vs2010 连接access数据库(转)

    第一次在博客园写博文,由于文采不怎么好,即使是自己很熟悉的东西,写起来也会感觉到不知从何讲起,我想写的多了就好了. 这篇文章主要是介绍怎么用c# 语言 vs2010连接access数据库的,连接字符串 ...

  2. Uncaught TypeError: TableInit is not a constructor

    我最近在做东西的时候,用到了Bootstrap的表格,我复制了一份代码使用,结果运行报错 Uncaught TypeError: TableInit is not a constructor 我点进去 ...

  3. 基于C#在WPF中使用斑马打印机进行打印【转】——不支持XPS的打印机

    https://www.cnblogs.com/zhaobl/p/4666002.html

  4. 【pip升级导致错误】 多个pip导致明明已经安装了包但是报no module错误

    原来一直用apt install 默认安装的pip 8.01版本,今天因为一些原因,将pip升级到了19.01.升级后就导致了错误. 直接pip installl --upgrade pip,发现报权 ...

  5. SQL Delta实用案例介绍,很好的东西,帮了我不少忙

    SQL Delta实用案例介绍 概述 本篇文章主要介绍SQL DELTA的简单使用.为了能够更加明了的说明其功能,本文将通过实际项目中的案例加以介绍. 主要容 Ÿ   SQL DELTA 简介 Ÿ   ...

  6. 原生JavaScript常用本地浏览器存储方法二(cookie)

    JavsScript Cookie概述 cookie是浏览器提供的一种机制,它将document对象的cookie属性提供给JavaScript.可以由JavaScript对其进行控制,而并不是Jav ...

  7. MySQL语句增加字段,修改字段名,修改类型,修改默认值

    原文地址:https://blog.csdn.net/kimgoo/article/details/54630257 增加字段:alter table 表名 ADD 字段 类型 约束 [默认值 注释] ...

  8. 【VS开发】ClientToScreen 和ScreenToClient 用法

    ClientToScreen( )是把窗口坐标转换为屏幕坐标 pWnd->GetWindowRect(&rc);是获取整个窗体的大小pWnd->GetClientRect(& ...

  9. Django orm练习

    ORM练习题 models生成 from django.db import models # Create your models here. # 书籍管理 class Book(models.Mod ...

  10. MApp_ZUI_CTL_MarqueeTextWinProc字串滚动

    /////////////////////////////////////////////////////////////////////////////// /// global MApp_ZUI_ ...