精华内容
下载资源
问答
  • 事实上,我们在上一部教程 HelloDjango - Django博客教程(第二版)的 页面侧边栏:使用自定义模板标签 已经讲解了如何获取归档日期列表,只是当时返回的归档日期列表直接用于模板的渲染,而这里我们需要将归档日期...

    作者:HelloGitHub-追梦人物[1]

    文中所涉及的示例代码,已同步更新到 HelloGitHub-Team 仓库[2]

    我们的博客有一个侧边栏功能,分别列出博客文章的分类列表、标签列表、归档时间列表,通过点击侧边栏对应的条目,还可以进入相应的页面。例如点击某个分类,博客将跳转到该分类下全部文章列表页面。这些数据的展示都需要开发对应的接口,以便前端调用获取数据。

    分类列表、标签列表实现比较简单,我们这里给出接口的设计规范,大家可以使用前几篇教程中学到的知识点轻松实现(具体实现可参考 GtiHub 上的源代码)。

    分类列表接口:/categories/

    标签列表接口:/tags/

    归档日期列表的接口实现稍微复杂一点,因为我们需要从已有文章中归纳文章发表日期。事实上,我们在上一部教程 HelloDjango - Django博客教程(第二版)的 页面侧边栏:使用自定义模板标签 已经讲解了如何获取归档日期列表,只是当时返回的归档日期列表直接用于模板的渲染,而这里我们需要将归档日期列表序列化后通过 API 接口返回。

    具体来说,获取博客文章发表时间归档列表的方法是调用查询集(QuerySet)的 dates 方法,提取记录中的日期。核心代码就一句:

    Post.objects.dates('created_time', 'month', order='DESC')
    

    这里 Post.objects.dates 方法会返回一个列表,列表中的元素为每一篇文章(Post)的创建日期(已去重),日期都是 Python 的 date 对象,精确到月份,降序排列。

    有了返回的归档日期列表,接下来就实现相应的 API 接口视图函数:

    blog/views.py
    
    
    from rest_framework import mixins, status, viewsets
    from rest_framework.decorators import action
    from rest_framework.serializers import DateField
    
    
    class PostViewSet(
        mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
    ):
      # ...
    
    
        @action(
            methods=["GET"], detail=False, url_path="archive/dates", url_name="archive-date"
        )
        def list_archive_dates(self, request, *args, **kwargs):
            dates = Post.objects.dates("created_time", "month", order="DESC")
            date_field = DateField()
            data = [date_field.to_representation(date) for date in dates]
            return Response(data=data, status=status.HTTP_200_OK)
    

    注意这里我们涉及到了几个以前没有详细讲解过的用法。

    一是 action 装饰器,它用来装饰一个视图集中的方法,被装饰的方法会被 django-rest-framework 的路由自动注册为一个 API 接口。

    回顾一下我们之前在使用视图集 viewset 时提到过 action(动作)的概念,django-rest-framework 预定义了几个标准的动作,分别为 list 获取资源列表,retrieve 获取单个资源、update 和 partial_update 更新资源、destroy 删除资源,这些 action 具体的实现方法,分别由 mixins 模块中的混入类提供。例如 用类视图实现首页 API 中我们介绍过 mixins.ListModelMixin,这个混入类提供了 list 动作对应的标准实现,即 list 方法。视图集中所有以上提及的以标准动作命名的方法,都会被 django-rest-framework 的路由自动注册为标准的 API 接口。

    django-rest-framework 默认只能识别标准命名的视图集方法并将其注册为 API,但我们可以添加更多非标准的 action,而为了让 django-rest-framework 能够识别这些方法,就需要使用 action 装饰器进行装饰。

    其实我们可以简单地将 action 装饰的方法看作是一个视图函数的实现,因此可以看到方法传入的第一个参数为 request 请求对象,函数体就是这个视图函数需要执行的逻辑,显然,方法最终必须要返回一个 HTTP 响应对象。

    action 装饰器通常用于在视图集中添加额外的接口实现。例如这里我们已有了 PostViewSet 视图集,标准的 list 实现了获取文章资源列表的逻辑。我们想添加一个获取文章归档日期列表的接口,因此添加了一个 list_archive_dates 方法,并使用 action 进行装饰。通常如果要在视图集中添加额外的接口实现,可以使用如下的模板代码:

    @action(
        methods=["allowed http method name"], 
        detail=False or True, 
        url_path="url/path", 
        url_name="url name"
    )
    def method_name(self, request, *args, **kwargs):
        # 接口逻辑的具体实现,返回一个 Response
    

    通常 action 装饰器以下 4 个参数都会设置:

    methods:一个列表,指定访问这个接口时允许的 HTTP 方法(GET、POST、PUT、PATCH、DELETE)

    detail:True 或者 False。设置为 True,自动注册的接口 URL 中会添加一个 pk 路径参数(请看下面的示例),否则不会。

    url_path:自动注册的接口 URL。

    url_name:接口名,主要用于通过接口名字反解对应的 URL。

    当然,我们还可以在 action 中设置所有 ViewSet 类所支持的类属性,例如 serializer_classpagination_classpermission_classes 等,用于覆盖类视图中设置的属性值。

    以上是 action 用法的一个基本介绍,现在来分析一下 list_archive_dates 这个 action 来加深理解。

    methods 参数指定接口需要通过 GET 方法访问,detail 为 Falseurl_path 设置为 archive/dates,因此最终自动生成的接口路由就是 /posts/archive/dates/。如果我们设置 detail 为 True,那么生成的接口路由就是 /posts/<int:pk>/archive/dates/,生成的 URL 中就会多一个 pk 路径参数。

    list_archive_dates 具体的实现逻辑中,以下几点需要注意:

    一是独立使用序列化字段(Field)。之前序列化字段都是在序列化器(Serializer)里面使用的,因为通常来说接口需要序列化一个对象的多个字段。而这个接口中只需要序列化一个时间字段(类型为 Python 标准库中的 datetime.date),所以没必要单独定义一个序列化器了,直接拿 django-rest-framework 提供的用于序列化时间类型的 DateField 就可以了。用法也很简单,实例化序列化字段,调用其 to_representation 方法,将需要序列化的值传入即可(其实序列化器在序列对象的多个字段时,内部也是分别调用对应序列化字段的 to_representation  方法)。

    我们通过列表推导式生成一个序列化后的归档日期列表,这个列表是可被序列化的。接着我们在接口返回一个 ResponseResponse 将序列化后的结果包装返回(保存在 data 属性中),django-rest-framework 会进一步帮我们把这个 Response 中包含的数据解析为合适的格式(例如 JSON)。

    status=status.HTTP_200_OK 指定这个接口返回的状态码,HTTP_200_OK 是一个预定义的常数,即 200。django-rest-framework 将常用 HTTP 请求的状态码常数预定义 status 模块里,使用预定义的变量而不是直接使用数字的好处一是增强代码可读性,二是减少硬编码。

    由于 PostViewSet 视图集已经通过 django-rest-framework 的路由进行了注册,因此 list_archive_dates 也会被连带着自动注册为一个接口。启动开发服务器,访问 /posts/archive/dates/,就可以看到返回的文章归档日期列表。

    注意到红框圈出部分,django-rest-framework API 交互后台会识别到额外定义的 action 并将它们展示出来,点击就可以进入到相应的 API 页面。

    现在,侧边栏所需要的数据接口就开发完成了,接下来实现返回某一分类、标签或者归档日期下的文章列表接口。

    使用视图集简化代码 我们开发了获取全部文章的接口。事实上,分类、标签或者归档日期文章列表的 API,本质上还是返回一个文章列表资源,只不过比首页 API 返回的文章列表资源多了个“过滤”,只过滤出了指定的部分文章而已。对于这样的场景,我们可以在请求 API 时加上查询参数,django-rest-framework 解析查询参数,然后从全部文章列表中过滤出查询所指定的文章列表再返回。

    这在 RESTful API 的设计中肯定是会遇到的,因此第三方库 django-filter 帮我们实现了上述所说的查询过滤功能,而且和 django-rest-framework 有很好的集成,我们可以在 django-rest-framework 中非常方便地使用 django-filter。

    既然要使用它,当然是先安装它(已安装跳过):pipenv install django-filter

    接着我们来配置 PostViewSet,为其设置用于过滤返回结果集的一些属性,代码如下:

    from django_filters.rest_framework import DjangoFilterBackend
    from .filters import PostFilter
    
    
    class PostViewSet(
        mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
    ):
        # ...
        filter_backends = [DjangoFilterBackend]
        filterset_class = PostFilter
    

    非常的简单,仅仅设置了 filter_backendsfilterset_class 两个属性。其中 filter_backends 设置为 DjangoFilterBackend,这样 API 在返回结果时, django-rest-framework 会调用设置的 backend(这里是 DjangoFilterBackend) 的 filter 方法对 get_queryset 方法返回的结果进行进一步的过滤,而 DjangoFilterBackend 会依据 filterset_class(这里是 PostFilter)中定义的过滤规则来过滤查询结果集。

    当然 PostFilter 还没有定义,我们来定义它。首先在 blog 应用下创建一个 filters.py 文件,用于存放自定义 filter 的代码,PostFilter 代码如下:

    from django_filters import rest_framework as drf_filters
    
    
    from .models import Post
    
    
    
    
    class PostFilter(drf_filters.FilterSet):
        created_year = drf_filters.NumberFilter(
            field_name="created_time", lookup_expr="year"
        )
        created_month = drf_filters.NumberFilter(
            field_name="created_time", lookup_expr="month"
        )
    
    
        class Meta:
            model = Post
            fields = ["category", "tags", "created_year", "created_month"]
    

    PostFilter 的定义和序列化器 Serializer 非常类似。

    categorytags 两个过滤字段因为是 Post 模型中定义的字段,因此 django-filter 可以自动推断其过滤规则,只需要在 Meta.fields 中声明即可。

    归档日期下的文章列表,我们设计的接口传递 2 个查询参数:年份和月份。由于这两个字段在 Post 中没有定义,Post 记录时间的字段为 created_time,因此我们需要显示地定义查询规则,定义的规则是:

    查询参数名 = 查询参数值的类型(查询的模型字段,查询表达式)

    例如示例中定义的 created_year 查询参数,查询参数值的类型为 number,即数字,查询的模型字段为 created_time,查询表达式是 year。当用户传递 created_year 查询参数时,django-filter 实际上会将以上定义的规则翻译为如下的 ORM 查询语句:

    Post.objects.filter(created_time__year=created_year传递的值)
    

    现在回到 API 交互后台,先进到 /post/ 接口下,默认返回了全部文章列表。可以看到右上角多了个过滤器(红框圈出部分)。

    点击会弹出过滤参数输入的交互面板,在这里可以交互式地输入查询过滤参数的值。

    例如选择如下的过滤参数,得到查询的 URL 为:

    http://127.0.0.1:10000/api/posts/?category=1&tags=1&created_year=2020&created_month=1

    这条查询返回创建于 2020 年 1 月,id 为 1 的分类下,id 为 1 的标签下的全部文章。

    通过不同的查询参数组合,就可以得到不同的文章资源列表了。

    参考资料

    [1]HelloGitHub-追梦人物: https://www.zmrenwu.com

    [2]HelloGitHub-Team 仓库:https://github.com/HelloGitHub-Team/HelloDjango-REST-framework-tutorial

    关注公众号加入交流群

    讲解开源项目系列——让对开源项目感兴趣的人不再畏惧、让开源项目的发起者不再孤单。跟着我们的文章,你会发现编程的乐趣、使用和发现参与开源项目如此简单。欢迎联系我(微信:xueweihan,备注:讲解)加入我们,让更多人爱上开源、贡献开源~

    第 8 篇:内容支持 Markdown 语法,接口返回包含解析后的 HTML

    第 7 篇:文章详情的 API 接口

    第 6 篇:分页接口

    “阅读原文”获取更多信息、“在看”让本文被更多人看到、“赞赏”支持我们。

    展开全文
  • 第一部分:日志格式详解 nginx的log日志分为:access.log 和 error.log。 access.log:主要是记录了哪些用户,哪些页面以及用户浏览器、ip和其他的访问信息。 error.log:主要记录服务器错误日志。 日志格式语法:...

    第一部分:日志格式详解

    nginx的log日志分为:access.log 和 error.log。
    access.log:主要是记录了哪些用户,哪些页面以及用户浏览器、ip和其他的访问信息。
    error.log:主要记录服务器错误日志。
    日志格式语法:log_format main 格式样式。打印出来的日志样式

    
    #access日志格式配置如下:
    log_format main '$remote_addr - $remote_user [$time_local] '
                    'fwf[$http_x_forwarded_for] tip[$http_true_client_ip] '
                    '$upstream_addr $upstream_response_time $request_time '
                    '$http_host $request '
                    '"$status" $body_bytes_sent "$http_referer" '
                    '"$http_accept_language" "$http_user_agent" ';
    
    #配置access log日志的存储位置及文件,注意:access.log文件是可以按日期进行分割的,方便查看及处理
    access_log /usr/local/nginx/log/access.log main;

    打印出来的日志信息

    201.0.69.2 - - [26/Jul/2018:03:17:20 +0800] fwf[-] tip[-] 127.0.0.1:7029 0.037 0.037  180.159.12.115:7029 POST /HLXY99/dex.htm HTTP/1.1 "200" 2426 "http://abc.com" "es-ES,es;q=0.8" "Jakarta Commons-HttpClient/3.0"

    具体格式分析

    从上面我们可以看出几部分信息:
    01.【$remote_addr】客户端(用户)IP地址。如:上例中的 201.0.69.2
    02.【$remote_user】 记录客户端用户名称
    03.【[$time_local]】访问时间。如:上例中的 [26/Jul/2018:03:17:20 +0800]
    04.【$upstream_addr】访问端口。如:上例中的 127.0.0.1:7029
    05.【$upstream_response_time】响应时间。如:上例中的 0.037
    06.【$request_time】请求时间。如:上例中的 0.037
    07.【$http_host】请求的url地址(目标url地址)的host。如:上例中的 180.159.12.115:7029
    08.【$request】请求方式(GET或者POST等)。如:上例中的 POST
    09.请求url地址(去除host部分)。如:上例中的 /HLXY99/dex.htm
    10.【"$status"】请求状态(状态码,200表示成功,404表示页面不存在,301表示永久重定向等)。如:上例中的 "200"
    11.【$body_bytes_sent】请求页面大小,默认为B(byte)。如:上例中的 2426
    12.【"$http_referer"】来源页面,即从哪个页面转到本页,专业名称叫做“referer”。如:上例中的 "http://abc.com"
    13.【"$http_accept_language"】用户浏览器语言。如:上例中的 "es-ES,es;q=0.8"
    14.【"$http_user_agent"】用户浏览器其他信息,浏览器版本、浏览器类型等。如:上例中的  "Jakarta Commons-HttpClient/3.0"
    15.【$time_iso8601】ISO8601标准格式下的本地时间。
    其实nginx access。log日志的格式不是一成不变的,是可以自定义的。
    在nginx的nginx.conf配置文件找到:log_format 这里就是日志的格式

    第二部分:日志文件按日期进行分割

    步骤1:编写shell脚本(在/usr/local/nginx/shellTask/下,编辑vi nginx_log.sh)

    
    #nginx日志切割脚本
    #author:lixy
    
    #!/bin/bash
    #设置日志文件存放目录
    logs_path="/home/nginx/nginx/logs/"
    #设置pid文件
    pid_path="/home/nginx/nginx/logs/nginx.pid"
    #重命名日志文件
    mv ${logs_path}access.log ${logs_path}access_$(date -d "yesterday" +"%Y%m%d").log
    #向nginx主进程发信号重新打开日志
    
    kill -USR1 `cat ${pid_path}`

    步骤2:设置定时任务,crontab 设置作业

    设置定时任务,crontab 设置作业
    1.编辑任务
    crontab –e
    2.每天凌晨00:00定时执行这个脚本
    00 00 * * * /bin/bash /usr/local/nginx/shellTask/task_log.sh
    3.查看任务
    crontab -l
    4.删除任务(适时使用)
    crontab -r
    展开全文
  • elf文件格式

    2017-07-30 17:09:05
    转载自 @ http://blog.csdn.net/gatieme ...日期 内核版本 架构 作者 GitHub CSDN 2016-06-04 Linux-4.5 X86 & arm gatieme LinuxDeviceDrivers Linux
    
    

    目录(?)[+]

    日期 内核版本 架构 作者 GitHub CSDN
    2016-06-04 Linux-4.5 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度-之-进程的描述

    对象文件格式


    对象文件


    首先,你需要知道的是所谓对象文件(Object files)有三个种类: 
    可重定位的对象文件(Relocatable file) 
    可执行的对象文件(Executable file) 
    可被共享的对象文件(Shared object file)

    • 可重定位的对象文件(Relocatable file)

      适于链接的可重定位文件(relocatable file),包含二进制代码和数据,能与其他可重定位对象文件在编译时合并创建出一个可执行文件。

      这是由汇编器汇编生成的 .o 文件。后面的链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)。我们可以使用 ar 工具将众多的 .o Relocatable object files 归档(archive)成 .a 静态库文件。如何产生 Relocatable file,你应该很熟悉了,请参见我们相关的基本概念文章和JulWiki。另外,可以预先告诉大家的是我们的内核可加载模块 .ko 文件也是 Relocatable object file。

    • 可执行的对象文件(Executable file)

      适于执行的可执行文件(executable file),包含可以直接拷贝进行内存执行的二进制代码和数据。用于提供程序的进程映像,加载的内存执行。

      这我们见的多了。文本编辑器vi、调式用的工具gdb、播放mp3歌曲的软件mplayer等等都是Executable object file。你应该已经知道,在我们的 Linux 系统里面,存在两种可执行的东西。除了这里说的 Executable object file,另外一种就是可执行的脚本(如shell脚本)。注意这些脚本不是 Executable object file,它们只是文本文件,但是执行这些脚本所用的解释器就是 Executable object file,比如 bash shell 程序。

    • 可被共享的对象文件(Shared object file)

      共享目标文件(shared object file),一种特殊的可重定位对象文件,能在加载时或运行时,装载进内存进行动态链接。连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。

      这些就是所谓的动态库文件,也即 .so 文件。如果拿前面的静态库来生成可执行程序,那每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空间;另外如果拿它们放到linux系统上一起运行,也会浪费掉宝贵的物理内存。如果将静态库换成动态库,那么这些问题都不会出现。动态库在发挥作用的过程中,必须经过两个步骤:

      1. 链接编辑器(link editor)拿它和其他Relocatable object file以及其他shared object file作为输入,经链接处理后,生存另外的 shared object file 或者 executable file。

      2. 在运行时,动态链接器(dynamic linker)拿它和一个Executable file以及另外一些 Shared object file 来一起处理,在Linux系统里面创建一个进程映像。

    文件格式


    本质上,对象文件只是保存在磁盘文件中的一串字节,每个系统的文件格式都不尽相同:

    • Bell实验室的第一个Unix系统使用 a.out格式。

    • System V Unix的早期版本使用 Common Object File Format(COFF)。

    • Windows NT使用COFF的变种,叫做 Portable Executable(PE)。

    • 现代Unix系统,包括Linux、新版System V、BSD变种、Solaris都使用 Executable and Linkable Format(ELF)。

    ELF对象文件格式


    ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。它自最早在 System V 系统上出现后,被 xNIX 世界所广泛接受,作为缺省的二进制文件格式来使用。可以说,ELF是构成众多xNIX系统的基础之一。

    ELF代表Executable and Linkable Format。他是一种对可执行文件、目标文件和库使用的文件格式。

    他在Linux下成为标准格式已经很长时间, 代替了早年的a.out格式。ELF一个特别的优点在于, 同一文件格式可以用于内核支持的几乎所有体系结构上, 这不仅简化了用户空间工具程序的创建, 也简化了内核自身的程序设计。例如, 在必须为可执行文件生成装载程序例程时。

    但是文件格式相同并不意味着不同系统上的程序之间存在二进制兼容性, 例如, FreeBSD和Linux都使用ELF作为二进制格式。尽管二者在文件中组织数据的方式相同。但在系统调用机制以及系统调用的语义方面, 仍然有差别。这也是在没有中间仿真层的情况下, FreeBSD程序不能在linux下运行的原因(反过来同样是如此)。

    有一点是可以理解的, 二进制程序不能在不同体系结构交换(例如, 为Alpha CPU编译的Linux二进制程序不能在Sparc Linux上执行), 因为底层的体系结构是完全不同的。但是由于ELF的存在, 对所有体系结构而言, 程序本身的相关信息以及程序的各个部分在二进制文件中编码的方式都是相同的。

    Linux不仅将ELF用于用户空间程序和库, 还用于构建模块。内核本身也是ELF格式。

    ELF文件标准历史


    ELF是一种开放格式, 其规范可以自由获取。

    在ELF格式出来之后,TISC(Tool Interface Standard Committee)委员会定义了一套ELF标准。你可以从这里(http://refspecs.freestandards.org/elf/)找到详细的标准文档

    20世纪90年代,一些厂商联合成立了一个委员会(TISC委员会),起草并发布了一个ELF文件格式标准供公开使用,并且希望所有人能够遵循这项标准并且从中获益。1993年,委员会发布了ELF文件标准。当时参与该委员会的有来自于编译器的厂商,如Watcom和Borland;来自CPU的厂商如IBM和Intel;来自操作系统的厂商如IBM和Microsoft。1995年,委员会发布了ELF 1.2标准,自此委员会完成了自己的使命,不久就解散了。所以ELF文件格式标准的最新版本为1.2。

    文件类型 e_type成员表示ELF文件类型,即前面提到过的3种ELF文件类型,每个文件类型对应一个常量。系统通过这个常量来判断ELF的真正文件类型,而不是通过文件的扩展名。相关常量以“ET_”开头,

    TISC委员会前后出了两个版本,v1.1和v1.2。两个版本内容上差不多,但就可读性上来讲,我还是推荐你读 v1.2的。因为在v1.2版本中,TISC重新组织原本在v1.1版本中的内容,将它们分成为三个部分(books): 
    a) Book I 
    介绍了通用的适用于所有32位架构处理器的ELF相关内容 
    b) Book II 
    介绍了处理器特定的ELF相关内容,这里是以Intel x86 架构处理器作为例子介绍 
    c) Book III 
    介绍了操作系统特定的ELF相关内容,这里是以运行在x86上面的 UNIX System V.4 作为例子介绍

    值得一说的是,虽然TISC是以x86为例子介绍ELF规范的,但是如果你是想知道非x86下面的ELF实现情况,那也可以在http://refspecs.freestandards.org/elf/中找到特定处理器相关的Supplment文档。比方ARM相关的,或者MIPS相关的等等。另外,相比较UNIX系统的另外一个分支BSD Unix,Linux系统更靠近 System V 系统。所以关于操作系统特定的ELF内容,你可以直接参考v1.2标准中的内容。

    本文所使用的测试程序结构


    add.c


    #include <stdio.h>
    #include <stdlib.h>
    
    // 不指定寄存器实现两个整数相加
    int Add(int a, int b)
    {
        __asm__ __volatile__
        (
            //"lock;\n"
            "addl %1,%0;\n"
            : "=m"(a)
            : "r"(b), "m"(a)
          //  :
        );
    
        return a;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    sub.c


    #include <stdio.h>
    #include <stdlib.h>
    
    // 不指定寄存器实现两个参数相减
    int Sub(int a, int b)
    {
        __asm__ __volatile__
        (
            "subl %1, %0;"
            : "=m"(a)
            : "r"(b), "m"(a)
     //       :
        );
    
        return a;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    testelf.c


    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int a = 3, b = 5;
    
        printf("%d + %d = %d\n", a, b, Add(a, b));
        printf("%d - %d = %d\n", a, b, Sub(a, b));
    
    
        return EXIT_SUCCESS;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Makefile


    
    target=testelf_normal testelf_dynamic testelf_static
    
    MAIN_OBJS=testelf.o
    SUBS_OBJS=add.o sub.o
    
    DYNA_FILE=libtestelf.so
    STAT_FILE=libtestelf.a
    
    all:$(target)
    
    %.o : %.c
        $(CC) -c $^ -o $@
    
    clean :
        rm -rf $(MAIN_OBJS) $(SUBS_OBJS)
        rm -rf $(DYNA_FILE) $(STAT_FILE)
        rm -rf $(target)
    
    
    # Complie the execute
    testelf_normal:$(MAIN_OBJS) $(SUBS_OBJS)
        gcc $^ -o $@
    
    testelf_dynamic:$(MAIN_OBJS) $(DYNA_FILE)
        gcc  $^ -o $@ -L./ -ltestelf
    
    testelf_static:$(MAIN_OBJS) $(STAT_FILE)
        gcc  testelf.o -o $@ -static -L./ -ltestelf
    
    
    
    # Complie the Dynamic Link Library libtestelf.so
    libtestelf.so:$(SUBS_OBJS)
        gcc -fPCI -shared $^ -o $@
    
    # Complie the Static Link Library libtestelf.so
    libtestelf.a:$(SUBS_OBJS)
        ar -r $@ $^
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    我们编写了两个库函数分别实现add和sub的功能, 然后编写了一个测试代码testelf.c调用了Add和Sub.

    然后我们的Mmakefile为测试程序编写了3分程序

    1. 普通的程序testelf_normal, 由add.o sub.o 和testelf.o直接链接生成

    2. 动态链接程序testelf_dynamic, 将add.o和sub.o先链接成动态链接库libtestelf.so, 然后再动态链接生成testelf_dynamic

    3. 静态链接程序testelf_static, 将add.o和sub.o先静态链接成静态库libtestelf.a, 然后再静态链接生成可执行程序testelf_staticke

    我们在源代码目录执行make后会完成编译, 编译完成后

    • add.o, sub.o和testelf.o是可重定位的对象文件(Relocatable file)

    • libtestelf.so是可被共享的对象文件(Shared object file)

    • testelf_normal, testelf_dynamic和testelf_static是可执行的对象文件(Executable file)

    如下图所示

    对象文件

    ELF可执行与链接文件格式详解


    布局和结构


    ELF文件由各个部分组成。

    为了方便和高效,ELF文件内容有两个平行的视角:一个是程序连接角度,另一个是程序运行角度

    elf文件的布局和结构

    首先图的左边部分,它是以链接视图来看待elf文件的, 从左边可以看出,包含了一个ELF头部,它描绘了整个文件的组织结构。它还包括很多节区(section)。这些节有的是系统定义好的,有些是用户在文件在通过.section命令自定义的,链接器会将多个输入目标文件中的相同的节合并。节区部分包含链接视图的大量信息:指令、数据、符号表、重定位信息等等。除此之外,还包含程序头部表(可选)和节区 头部表,程序头部表,告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。而节区头部表(Section Heade Table)包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。

    需要注意地是:尽管图中显示的各个组成部分是有顺序的,实际上除了 ELF 头部表以外,其他节区和段都没有规定的顺序。

    右半图是以程序执行视图来看待的,与左边对应,多了一个段(segment)的概念,编译器在生成目标文件时,通常使用从零开始的相对地址,而在链接过程中,链接器从一个指定的地址开始,根据输入目标文件的顺序,以段(segment)为单位将它们拼装起来。其中每个段可以包括很多个节(section)。

    • elf头部

      除了用于标识ELF文件的几个字节外, ELF头还包含了有关文件类型和大小的有关信息, 以及文件加载后程序执行的入口点信息

    • 程序头表(program header table)

      程序头表向系统提供了可执行文件的数据在进程虚拟地址空间中组织文件的相关信息。他还表示了文件可能包含的段数据、段的位置和用途

    • 段segment

      各个段保存了与文件爱你相关的各种形式的数据, 例如,符号表、实际的二进制码、固定值(如字符串)活程序使用的数值常数

    • 节头表section

      包含了与各段相关的附加信息。

    ELF基本数据类型定义


    在具体介绍ELF的格式之前,我们先来了解在ELF文件中都有哪些数据类型的定义:

    ELF数据编码顺序与机器相关,为了使数据结构更加通用, linux内核自定义了几种通用的数据, 使得数据的表示与具体体系结构分离

    但是由于32位程序和64位程序所使用的数据宽度不同, 同时64位机必须兼容的执行32位程序, 因此我们所有的数据都被定义为32bit和64bit两个不同类型的数据

    常规定义在include/uapi/linux中, 各个结构也可以按照需求重新定义

    32位机器上的定义

    名称 常规定义 大小 对齐 目的
    Elf32_Addr __u32 4 4 无符号程序地址
    Elf32_Half __u16 2 2 无符号中等整数
    Elf32_Off __u32 4 4 无符号文件偏移
    Elf32_SWord __u32 4 4 有符号大整数
    Elf32_Word __u32 4 4 无符号大整数
    unsigned char 1 1 无符号小整数

    64位机器上的定义*

    名称 常规定义 大小 对齐 目的
    Elf64_Addr __u64 8 8 无符号程序地址
    Elf64_Half __u16 2 2 无符号小整数
    Elf64_SHalf __s16 2 2 无符号小整数
    Elf64_Off __u64 8 8 无符号文件偏移
    Elf64_Sword __s32 4 4 有符号中等整数
    Elf64_Word __u32 4 4 无符号中等整数
    Elf64_Xword __u64 8 8 无符号大整数
    Elf64_Sxword __s64 8 8 有符号大整数
    unsigned char 1 1 无符号小整数

    ELF头部Elfxx_Ehdr


    elf头部用Elfxx_Ehdr结构(被定义在linux/uapi/linux/elf.h来表示, Elf32_Ehdr(32bit)Elf64_Ehdr(64bit)

    数据成员


    内部成员, 如下

    成员 类型 描述
    e_ident[EI_NIDENT] unsigned char 目标文件标识信息, EI_NIDENT=16, 因此共占用128位
    e_type Elf32_Half/Elf64_Half 目标文件类型
    e_machine Elf32_Half/Elf64_Half 目标体系结构类型
    e_version Elf32_Word/Elf64_Word 目标文件版本
    e_entry Elf32_Addr/Elf64_Addr 程序入口的虚拟地址,若没有,可为0
    e_phoff Elf32_Off/Elf64_Off 程序头部表格(Program Header Table)的偏移量(按字节计算),若没有,可为0
    e_shoff Elf32_Off/Elf64_Off 节区头部表格(Section Header Table)的偏移量(按字节计算),若没有,可为0
    e_flags Elf32_Word/Elf64_Word 保存与文件相关的,特定于处理器的标志。标志名称采用 EF_machine_flag的格式
    e_ehsize Elf32_Half/Elf64_Half ELF 头部的大小(以字节计算)
    e_phentsize Elf32_Half/Elf64_Half 程序头部表格的表项大小(按字节计算)
    e_phnum Elf32_Half/Elf64_Half 程序头部表格的表项数目。可以为 0
    e_shentsize Elf32_Half/Elf64_Half 节区头部表格的表项大小(按字节计算)
    e_shnum Elf32_Half/Elf64_Half 节区头部表格的表项数目。可以为 0
    e_shstrndx Elf32_Half/Elf64_Half 节区头部表格中与节区名称字符串表相关的表项的索引。如果文件没有节区名称字符串表,此参数可以为 SHN_UNDEF

    ELF魔数e_ident


    魔数

    很多类型的文件,其起始的几个字节的内容是固定的(或是有意填充,或是本就如此)。根据这几个字节的内容就可以确定文件类型,因此这几个字节的内容被称为魔数 (magic number)。此外在一些程序代码中,程序员常常将在代码中出现但没有解释的数字常量或字符串称为魔数 (magic number)或魔字符串。

    ELF魔数 我们可以从前面readelf的输出看到,最前面的”Magic”的16个字节刚好对应“Elf32_Ehdr”的e_ident这个成员。这16个字节被ELF标准规定用来标识ELF文件的平台属性,比如这个ELF字长(32位/64位)、字节序、ELF文件版本

    最开始的4个字节是所有ELF文件都必须相同的标识码,分别为0x7F、0x45、0x4c、0x46 
    第一个字节对应ASCII字符里面的DEL控制符, 
    后面3个字节刚好是ELF这3个字母的ASCII码。这4个字节又被称为ELF文件的魔数,几乎所有的可执行文件格式的最开始的几个字节都是魔数。

    比如a.out格式最开始两个字节为 0x01、0x07;

    PE/COFF文件最开始两个个字节为0x4d、0x5a,即ASCII字符MZ。

    这种魔数用来确认文件的类型,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载。

    接下来的一个字节是用来标识ELF的文件类的,0x01表示是32位的,0x02表示是64位的;第6个字是字节序,规定该ELF文件是大端的还是小端的(见附录:字节序)。第7个字节规定ELF文件的主版本号,一般是1,因为ELF标准自1.2版以后就再也没有更新了。后面的9个字节ELF标准没有定义,一般填0,有些平台会使用这9个字节作为扩展标志。

    各种魔数的由来

    a.out格式的魔数为0x01、0x07,为什么会规定这个魔数呢?

    UNIX早年是在PDP小型机上诞生的,当时的系统在加载一个可执行文件后直接从文件的第一个字节开始执行,人们一般在文件的最开始放置一条跳转(jump)指令,这条指令负责跳过接下来的7个机器字的文件头到可执行文件的真正入口。而0x01 0x07这两个字节刚好是当时PDP-11的机器的跳转7个机器字的指令。为了跟以前的系统保持兼容性,这条跳转指令被当作魔数一直被保留到了几十年后的今天。

    计算机系统中有很多怪异的设计背后有着很有趣的历史和传统,了解它们的由来可以让我们了解到很多很有意思的事情。这让我想起了经济学里面所谓的“路径依赖”,其中一个很有意思的叫“马屁股决定航天飞机”的故事在网上流传很广泛,有兴趣的话你可以在google以“马屁股”和“航天飞机”作为关键字搜索一下。 
    其中需要注意地是e_ident是一个16字节的数组,这个数组按位置从左到右都是有特定含义,每个数组元素的下标在标准中还存在别称,如byte0的下标0别名为EI_MAG0,具体如下:

    名称 元素下标值 含义
    EI_MAG0 0 文件标识
    EI_MAG1 1 文件标识
    EI_MAG2 2 文件标识
    EI_MAG3 3 文件标识
    EI_CLASS 4 文件类
    EI_DATA 5 数据编码
    EI_VERSION 6 文件版本
    EI_PAD 7 补齐字节开始处
    EI_NIDENT 16 e_ident[]大小

    e_ident[EI_MAG0]~e_ident[EI_MAG3]即e_ident[0]~e_ident[3]被称为魔数(Magic Number),其值一般为0x7f,’E’,’L’,’F’

    e_ident[EI_CLASS](即e_ident[4])识别目标文件运行在目标机器的类别,取值可为三种值:

    名称 元素下标值 含义
    ELFCLASSNONE 0 非法类别
    ELFCLASS32 1 32位目标
    ELFCLASS64 2 64位目标

    e_ident[EI_DATA](即e_ident[5]):给出处理器特定数据的数据编码方式。即大端还是小端方式。取值可为3种:

    名称 元素下标值 含义
    ELFDATANONE 0 非法数据编码
    ELFDATA2LSB 1 高位在前
    ELFDATA2MSB 2 低位在前

    目标文件类型e_type


    e_type表示elf文件的类型

    文件类型 e_type成员表示ELF文件类型,即前面提到过的3种ELF文件类型,每个文件类型对应一个常量。系统通过这个常量来判断ELF的真正文件类型,而不是通过文件的扩展名。相关常量以“ET_”开头

    如下定义:

    名称 取值 含义
    ET_NONE 0 未知目标文件格式
    ET_REL 1 可重定位文件
    ET_EXEC 2 可执行文件
    ET_DYN 3 共享目标文件
    ET_CORE 4 Core 文件(转储格式)
    ET_LOPROC 0xff00 特定处理器文件
    ET_HIPROC 0xffff 特定处理器文件
    ET_LOPROC~ET_HIPROC 0xff00~0xffff 特定处理器文件

    目标体系结构类型e_machine


    e_machine表示目标体系结构类型

    名称 取值 含义
    EM_NONE 0 未指定
    EM_M32 1 AT&T WE 32100
    EM_SPARC 2 SPARC
    EM_386 3 Intel 80386
    EM_68K 4 Motorola 68000
    EM_88K 5 Motorola 88000
    EM_860 7 Intel 80860
    EM_MIPS 8 MIPS RS3000
    others 9~ 预留

    ELF版本e_version


    这个用来区分ELF标准的各个修订版本, 但是前面提到ELF最新版本就是1(1.2), 仍然是最新版, 因此目前不需要这个特性

    另外ELF头还包括了ELF文件的各个其他部分的长度和索引位置信息。因为这些部分的长度可能依程序而不同。所以在文件头部必须提供相应的数据.

    readelf -h查看elf头部


    可重定位的对象文件(Relocatable file)


    readelf -h add.o
    • 1
    • 1

    REL的elf文件头

    文件类型是, 说明是可重定位文件, 其代码可以移动至任何位置.

    该文件没有程序头表, 对需要进行链接的对象而言, 程序头表是不必要的, 为此所有长度都设置为0

    可执行的对象文件(Executable file)


    readelf -h testelf_dynamic
    • 1
    • 1

    exec的elf文件头

    可被共享的对象文件(Shared object file)


    readelf -h libtestelf.so
    • 1
    • 1

    dynamic的elf文件头

    程序头部Elf32_phdr


    以程序运行的角度看ELF文件, 就需要程序头表,即要运行这个elf文件,需要将哪些东西载入到内存镜像。而节区头部表是以elf资源的角度来看待elf文件的,即这个elf文件到底存在哪些资源,以及这些资源之间的关联关系,

    程序头部是一个表,它的起始地址在elf头部结构中的e_phoff成员指定,数量由e_phnum表示,每个程序头部表项的大小由e_phentsize指出。

    可执行文件或者共享目标文件的程序头部是一个结构数组,每个结构描述了一个段或者系统准备程序执行所必需的其它信息。目标文件的”段”包含一个或者多个”节区”,也就是” 
    段内容(Segment Contents)”。程序头部仅对于可执行文件和共享目标文件有意义。

    下面来看程序头号部表项的数据结构

    成员 类型 描述
    p_type Elf32_Word/Elf64_Word 段类型
    p_offset Elf32_Off/Elf64_Off 段位置
    p_vaddr Elf32_Addr/Elf64_Addr 给出段的第一个字节将被放到内存中的虚拟地址
    p_paddr Elf32_Addr/Elf64_Addr 仅用于与物理地址相关的系统中
    p_filesz Elf32_Word/Elf64_Word 给出段在文件映像中所占的字节数
    p_memsz Elf32_Word/Elf64_Word 给出段在内存映像中占用的字节数
    p_flags Elf32_Word/Elf64_Word 与段相关的标志
    p_align Elf32_Word/Elf64_Word 对齐

    段类型p_type


    名称 取值 说明
    PT_NULL 0 此数组元素未用。结构中其他成员都是未定义的
    PT_DYNAMIC 2 数组元素给出动态链接信息
    PT_INTERP 3 数组元素给出一个 NULL 结尾的字符串的位置和长度,该字符串将被当作解释器调用。这种段类型仅对与可执行文件有意义(尽管也可能在共享目标文件上发生)。在一个文件中不能出现一次以上。如果存在这种类型的段,它必须在所有可加载段项目的前面
    PT_NOTE 4 此数组元素给出附加信息的位置和大小
    PT_SHLIB 5 此段类型被保留,不过语义未指定。包含这种类型的段的程序与 ABI不符
    PT_PHDR 6 此类型的数组元素如果存在,则给出了程序头部表自身的大小和位置,既包括在文件中也包括在内存中的信息。此类型的段在文件中不能出现一次以上。并且只有程序头部表是程序的内存映像的一部分时才起作用。如果存在此类型段,则必须在所有可加载段项目的前面
    PT_LOPROC~PT_HIPROC 0x70000000~0x7fffffff 此范围的类型保留给处理器专用语义

    readelf -l查看程序头表

    在程序头表之后, 列出了6个段, 这些组成了最终在内存中执行的程序.

    其还提供了各段在虚拟地址空间和物理空间的大小, 位置, 标志, 访问权限和对齐方面的信息. 还指定了yui个类型来更精确的描述段.

    各段的语义如下

    描述
    PHDR 保存了程序头表
    INTERP 指定在程序已经从可执行文件映射到内存之后, 必须调用的解释器. 在这里解释器并不意味着二进制文件的内容必须解释执行(比如Java的字节码需要Java虚拟机解释).它指的是这样一个程序:通过链接其他库, 来满足未解决的引用.
    通常/lib/ld-linux.so.2, /lib/ld-linux-ia-64.so.2等库, 用于在虚拟地址空间中国插入程序运行所需的动态库. 对几乎所有的程序来说, 可能C标准库都是必须映射的.还需要添加的各种库, 如GTK, QT, 数学库math, 线程库pthread等等
    LOAD 表示一个需要从二进制文件映射到虚拟地址的段. 其中保存了常量数据(如字符串), 程序的目标代码等
    DYNAMIC 该段保存了由动态链接器(即, INTERP中指定的解释器)使用的信息
    NOTE 保存了专有信息

    可重定位的对象文件(Relocatable file)


    readelf -l add.o
    • 1
    • 1

    rel的elf程序头表

    可重定向文件, 是一个需要链接的对象, 程序头表对其而言不是必要的, 因此这类文件一般没有程序头表

    可执行的对象文件(Executable file)


    readelf -l testelf_dynamic
    • 1
    • 1

    exec的elf程序头表

    可被共享的对象文件(Shared object file)


    readelf -l libtestelf.so
    • 1
    • 1

    dyn的elf程序头表

    虚拟地址空间中的各个段, 填充了来自ELF文件中特定的段的数据. 因而readelf输出的第二部分指定了那些节载入到哪些段(节段映射).

    物理地址信息讲被忽略, 因为该信息是由内盒根据物理页帧到虚拟地址空间中相应位置的映射情况动态分配的.只有在没有MMU(因而没有虚拟内存)的系统上该信息才是由意义的

    节区(Sections)


    节区中包含目标文件中的所有信息

    除了:ELF 头部、程序头部表格、节区头部表格。

    节区满足以下条件:

    1. 目标文件中的每个节区都有对应的节区头部描述它,反过来,有节区头部不意味着有节区。

    2. 每个节区占用文件中一个连续字节区域(这个区域可能长度为 0)。

    3. 文件中的节区不能重叠,不允许一个字节存在于两个节区中的情况发生。

    4. 目标文件中可能包含非活动空间(INACTIVE SPACE)。这些区域不属于任何头部和节区,其内容未指定。

    节区头部表格


    ELF文件在描述各段的内容时, 是指定了哪些节的数据映射到段中. 因此需要一个结构来管理各个节的内容, 即节头表

    节区头部表是以elf资源的角度来看待elf文件的,即这个elf文件到底存在哪些资源,以及这些资源之间的关联关系,而前面提到的程序头部表,则以程序运行来看elf文件的,即要运行这个elf文件,需要将哪些东西载入到内存镜像。

    ELF 头部中,

    e_shoff 成员给出从文件头到节区头部表格的偏移字节数;

    e_shnum给出表格中条目数目;

    e_shentsize 给出每个项目的字节数。

    从这些信息中可以确切地定位节区的具体位置、长度。

    从之前的描述中可知,每一项节区在节区头部表格中都存在着一项元素与它对应,因此可知,这个节区头部表格为一连续的空间,每一项元素为一结构体

    那么这个节区头部由elfxx_shdr(定义在include/uapi/linux/elf.h), 32位elf32_shdr, 64位elf64_shdr

    结构体的成员如下

    成员 类型 描述
    sh_name Elf32_Word/Elf64_Word 节区名,是节区头部字符串表节区(Section Header String Table Section)的索引。名字是一个 NULL 结尾的字符串
    sh_type Elf32_Word/Elf64_Word 节区类型
    sh_flags Elf32_Word/Elf64_Word 节区标志
    sh_addr Elf32_Addr/Elf64_Addr 如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。否则,此字段为 0
    sh_offset Elf32_Off/Elf64_Off 此成员的取值给出节区的第一个字节与文件头之间的偏移
    sh_size Elf32_Word/Elf64_Word 此成员给出节区的长度(字节数)
    sh_link Elf32_Word/Elf64_Word 此成员给出节区头部表索引链接。其具体的解释依赖于节区类型
    sh_info Elf32_Word/Elf64_Word 此成员给出附加信息,其解释依赖于节区类型
    sh_addralign Elf32_Word/Elf64_Word 某些节区带有地址对齐约束
    sh_entsize Elf32_Word/Elf64_Word 某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数

    节区类型sh_type


    sh_type的取值如下:

    名称 取值 说明
    SHT_NULL 0 此值标志节区头部是非活动的,没有对应的节区。此节区头部中的其他成员取值无意义
    SHT_PROGBITS 1 此节区包含程序定义的信息,其格式和含义都由程序来解释
    SHT_SYMTAB 2 此节区包含一个符号表。目前目标文件对每种类型的节区都只能包含一个,不过这个限制将来可能发生变化
    一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言)的符号,尽管也可用来实现动态链接
    SHT_STRTAB 3 此节区包含字符串表。目标文件可能包含多个字符串表节区
    SHT_RELA 4 此节区包含重定位表项,其中可能会有补齐内容(addend),例如 32 位目标文件中的 Elf32_Rela 类型。目标文件可能拥有多个重定位节区
    SHT_HASH 5 此节区包含符号哈希表。所有参与动态链接的目标都必须包含一个符号哈希表。目前,一个目标文件只能包含一个哈希表,不过此限制将来可能会解除
    SHT_DYNAMIC 6 此节区包含动态链接的信息。目前一个目标文件中只能包含一个动态节区,将来可能会取消这一限制
    SHT_NOTE 7 此节区包含以某种方式来标记文件的信息
    SHT_NOBITS 8 这种类型的节区不占用文件中的空间 , 其他方面和SHT_PROGBITS相似。尽管此节区不包含任何字节,成员sh_offset 中还是会包含概念性的文件偏移
    SHT_REL 9 此节区包含重定位表项,其中没有补齐(addends),例如 32 位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区
    SHT_SHLIB 10 此节区被保留,不过其语义是未规定的。包含此类型节区的程序与 ABI 不兼容
    SHT_DYNSYM 11 作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间
    SHT_LOPROC X70000000 这一段(包括两个边界),是保留给处理器专用语义的
    SHT_HIPROC OX7FFFFFFF 这一段(包括两个边界),是保留给处理器专用语义的
    SHT_LOUSER 0X80000000 此值给出保留给应用程序的索引下界
    SHT_HIUSER 0X8FFFFFFF 此值给出保留给应用程序的索引上界

    节区标志sh_flags


    sh_flag标志着此节区是否可以修改,是否可以执行,如下定义:

    名称 取值 含义
    SHF_WRITE 0x1 节区包含进程执行过程中将可写的数据
    SHF_ALLOC 0x2 此节区在进程执行过程中占用内存。某些控制节区并不出现于目标文件的内存映像中,对于那些节区,此位应设置为 0
    SHF_EXECINSTR 0x4 节区包含可执行的机器指令
    SHF_MASKPROC 0xF0000000 所有包含于此掩码中的四位都用于处理器专用的语义

    sh_link和sh_info字段的具体含义依赖于sh_type的值

    sh_type sh_link sh_info
    SHT_DYNAMIC 此节区中条目所用到的字符串表格的节区头部索引 0
    SHT_HASH 此哈希表所适用的符号表的节区头部索引 0
    SHT_REL
    SHT_RELA
    相关符号表的节区头部索引 重定位所适用的节区的节区头部索引
    SHT_SYMTAB
    SHT_DYNSYM
    相关联的字符串表的节区头部索引 最后一个局部符号(绑定 STB_LOCAL)的符号表索引值加一
    其它 SHN_UNDEF 0

    特殊节区


    有些节区是系统预订的,一般以点开头号,因此,我们有必要了解一些常用到的系统节区。

    名称 类型 属性 含义
    .bss SHT_NOBITS SHF_ALLOC +SHF_WRITE 包含将出现在程序的内存映像中的为初始化数据。根据定义,当程序开始执行,系统将把这些数据初始化为 0。此节区不占用文件空间
    .comment SHT_PROGBITS (无) 包含版本控制信息
    .data SHT_PROGBITS SHF_ALLOC + SHF_WRITE 这些节区包含初始化了的数据,将出现在程序的内存映像中
    .data1 SHT_PROGBITS SHF_ALLOC + SHF_WRITE 这些节区包含初始化了的数据,将出现在程序的内存映像中
    .debug SHT_PROGBITS (无) 此节区包含用于符号调试的信息
    .dynamic SHT_DYNAMIC   此节区包含动态链接信息。节区的属性将包含 SHF_ALLOC 位。是否 SHF_WRITE 位被设置取决于处理器
    .dynstr SHT_STRTAB SHF_ALLOC 此节区包含用于动态链接的字符串,大多数情况下这些字符串代表了与符号表项相关的名称
    .dynsym SHT_DYNSYM SHF_ALLOC 此节区包含了动态链接符号表
    .fini SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行这里的代码
    .got SHT_PROGBITS   此节区包含全局偏移表
    .hash SHT_HASH SHF_ALLOC 此节区包含了一个符号哈希表
    .init SHT_PROGBITS SHF_ALLOC +SHF_EXECINSTR 此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码
    .interp SHT_PROGBITS   此节区包含程序解释器的路径名。如果程序包含一个可加载的段,段中包含此节区,那么节区的属性将包含 SHF_ALLOC 位,否则该位为 0
    .line SHT_PROGBITS (无) 此节区包含符号调试的行号信息,其中描述了源程序与机器指令之间的对应关系。其内容是未定义的
    .note SHT_NOTE (无) 此节区中包含注释信息,�9C�独立的格式。
    .plt SHT_PROGBITS   此节区包含过程链接表(procedure linkage table)
    .relname
    .relaname
    SHT_REL
    SHT_RELA
      这些节区中包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text
    .rodata
    .rodata1
    SHT_PROGBITS SHF_ALLOC 这些节区包含只读数据,这些数据通常参与进程映像的不可写段
    .shstrtab SHT_STRTAB   此节区包含节区名称
    .strtab SHT_STRTAB   此节区包含字符串,通常是代表与符号表项相关的名称。如果文件拥有一个可加载的段,段中包含符号串表,节区的属性将包含SHF_ALLOC 位,否则该位为 0
    .symtab SHT_SYMTAB   此节区包含一个符号表。如果文件中包含一个可加载的段,并且该段中包含符号表,那么节区的属性中包含SHF_ALLOC 位,否则该位置为 0
    .text SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含程序的可执行指令

    readelf -S查看节区头表


    可重定位的对象文件(Relocatable file)


    readelf -S add.o
    • 1
    • 1

    rel的elf节区头表

    可重定向文件, 是一个需要链接的对象, 程序头表对其而言不是必要的, 因此这类文件一般没有程序头表

    可执行的对象文件(Executable file)


    readelf -S testelf_dynamic
    • 1
    • 1

    rel的elf节区头表

    可被共享的对象文件(Shared object file)


    readelf -S libtestelf.so
    • 1
    • 1

    dyn的elf程序头表

    字符串表


    字符串表


    首先要知道,字符串表它本身就是一个节区,从第二章描述中可知,每一个节区都存在一个节区头部表项与之对应,所以字符串表这个节区也存在一个节区头部表项对应,而在elf文件头部结构中存在一个成员e_shstrndx给出这个节区头部表项的索引位置。因此可以通过

    shstrab  = (rt_uint8_t *)module_ptr +shdr[elf_module->e_shstrndx].sh_offset;
    • 1
    • 1

    来得到字符串表的起始位置。 
    字符串表节区包含以 NULL(ASCII 码 0)结尾的字符序列,通常称为字符串。ELF目标文件通常使用字符串来表示符号和节区名称。对字符串的引用通常以字符串在字符 
    串表中的下标给出。

    一般,第一个字节(索引为 0)定义为一个空字符串。类似的,字符串表的最后一个字节也定义为 NULL,以确保所有的字符串都以 NULL 结尾。索引为 0 的字符串在 
    不同的上下文中可以表示无名或者名字为 NULL 的字符串。

    允许存在空的字符串表节区,其节区头部的 sh_size 成员应该为 0。对空的字符串表而言,非 0 的索引值是非法的。

    例如:对于各个节区而言,节区头部的 sh_name 成员包含其对应的节区头部字符串表节区的索引,此节区由 ELF 头的 e_shstrndx 成员给出。下图给出了包含 25 个字节的一个字符串表,以及与不同索引相关的字符串。

    字符

    那么上面字符串表包含以下字符串:

    索引 字符串
    0 (无)
    1 name.
    7 Variable
    11 able
    16 able
    24 (空字符串)

    符号表(Symbol Table)


    首先,符号表同样本身是一节区,也存在一对应节区头部表项。

    目标文件的符号表中包含用来定位、重定位程序中符号定义和引用的信息。

    符号表索引是对此数组的索引。索引0表示表中的第一表项,同时也作为未定义符号的索引。

    符号表是由一个个符号元素组成,用elfxx_sym来结构来表示, 定义在include/uapi/linux/elf.h, 同样32位为elf32_sym, 64位对应elf64_sym

    每个元素的数据结构如下定义:

    成员 类型 描述
    st_name Elf32_Word/Elf64_Word 名称,索引到字符串表
    st_value Elf32_AddrElf64_Addr 给出相关联的符号的取值。依赖于具体的上下文
    st_size Elf32_Word/Elf64_Word 相关的尺寸大小
    st_info unsigned char 给出符号的类型和绑定属性
    st_other unsigned char 该成员当前包含 0,其含义没有定义
    st_shndx Elf32_Half/Elf64_Half 给出相关的节区头部表索引。某些索引具有特殊含义

    st_info给出符号的类型和绑定属性


    st_info 中包含符号类型和绑定信息,操纵方式如:

    #define ELF32_ST_BIND(i) ((i)>>4)
    #define ELF32_ST_TYPE(i) ((i)&0xf)
    #define ELF32_ST_INFO(b, t) (((b)<<4) + ((t)&0xf))
    • 1
    • 2
    • 3
    • 1
    • 2
    • 3

    st_info 的高四位(ELF32_ST_BIND(i))

    表示符号绑定,用于确定链接可见性和行为。具体的绑定类型如:

    名称 取值 说明
    STB_LOCAL 0 局部符号在包含该符号定义的目标文件以外不可见。相同名称的局部符号可以存在于多个文件中,互不影响
    STB_GLOBAL 1 全局符号对所有将组合的目标文件都是可见的。一个文件中对某个全局符号的定义将满足另一个文件对相同全局符号的未定义引用
    STB_WEAK 2 弱符号与全局符号类似,不过他们的定义优先级比较低
    STB_LOPROC 13 处于这个范围的取值是保留给处理器专用语义的
    STB_HIPROC 15 处于这个范围的取值是保留给处理器专用语义的

    全局符号与弱符号之间的区别主要有两点:

    1. 当链接编辑器组合若干可重定 位 的 目 标 文 件 时 , 不 允 许 对 同 名 的STB_GLOBAL 符号给出多个定义。另一方面如果一个已定义的全局符号已经存在,出现一个同名的弱符号并不会产生错误。链接编辑器尽关心全局符号,忽略弱符号。类似地,如果一个公共符号(符号的 st_shndx 中包含 SHN_COMMON),那么具有相同名称的弱符号出现也不会导致错误。链接编辑器会采纳公共定义,而忽略弱定义。

    2. 当链接编辑器搜索归档库(archive libraries)时,会提取那些包含未定义全局符号的档案成员。成员的定义可以是全局符号,也可以是弱符号。连接编辑器不会提取档案成员来满足未定义的弱符号。未能解析的弱符号取值为 0。

    在每个符号表中,所有具有 STB_LOCAL 绑定的符号都优先于弱符号和全局符号。符号表节区中的 sh_info 头部成员包含第一个非局部符号的符号表索引。

    st_info的低四位ELF32_ST_TYPE(i)

    定义如下

    名称 取值 说明
    STT_NOTYPE 0 符号的类型没有指定
    STT_OBJECT 1 符号与某个数据对象相关,比如一个变量、数组等等
    STT_FUNC 2 符号与某个函数或者其他可执行代码相关
    STT_SECTION 3 符号与某个节区相关。这种类型的符号表项主要用于重定位,通常具有 STB_LOCAL 绑定
    STT_FILE 4 传统上,符号的名称给出了与目标文件相关的源文件的名称。文件符号具有 STB_LOCAL 绑定,其节区索引是SHN_ABS,并且它优先于文件的其他 STB_LOCAL 符号(如果有的话)
    STT_LOPROC~STT_HIPROC 13~15 此范围的符号类型值保留给处理器专用语义用途

    在共享目标文件中的函数符号(类型为 STT_FUNC)具有特别的重要性。当其他目标文件引用了来自某个共享目标中的函数时,链接编辑器自动为所引用的符号创建过 
    程链接表项。类型不是 STT_FUNC 的共享目标符号不会自动通过过程链接表进行引用。

    如果一个符号的取值引用了某个节区中的特定位置,那么它的节区索引成员(st_shndx)包含了其在节区头部表中的索引。当节区在重定位过程中被移动时,符号的取值也会随之变化,对符号的引用始终会“指向”程序中的相同位置。

    st_shndx


    如前面所述,st_shndx给出相关的节区头部表索引。但其值也存在一些特殊值,具有某些特殊的含义:

    名称 取值 说明
    SHN_ABS   符号具有绝对取值,不会因为重定位而发生变化
    SHN_COMMON   符号标注了一个尚未分配的公共块。符号的取值给出了对齐约束,与节区的 sh_addralign成员类似。就是说,链接编辑器将为符号分配存储空间,地址位于 st_value 的倍数处。符号的大小给出了所需要的字节数
    SHN_UNDEF   此节区表索引值意味着符号没有定义。当链接编辑器将此目标文件与其他定义了该符号的目标文件进行组合时,此文件中对该符号的引用将被链接到实际定义的位置

    st_value


    不同的目标文件类型中符号表项对 st_value 成员具有不同的解释:

    1. 在可重定位文件中,st_value 中遵从了节区索引为 SHN_COMMON 的符号的对齐约束。

    2. 在可重定位的文件中,st_value 中包含已定义符号的节区偏移。就是说,st_value 是从 st_shndx 所标识的节区头部开始计算,到符号位置的偏移。

    3. 在可执行和共享目标文件中,st_value 包含一个虚地址。为了使得这些文件的符号对动态链接器更有用,节区偏移(针对文 件的解释)让位于虚拟地址(针对内存的解释),因为这时与节区号无关。

    尽管符号表取值在不同的目标文件中具有相似的含义,适当的程序可以采取高效的数据访问方式。

    nm查看符号表


    nm *.o
    • 1
    • 1

    rel的elf符号表

    可重定向文件, 是一个需要链接的对象, 程序头表对其而言不是必要的, 因此这类文件一般没有程序头表

    可执行的对象文件(Executable file)


    nm testelf_dynamic
    • 1
    • 1

    exec的elf符号表

    可被共享的对象文件(Shared object file)


    nm libtestelf.so
    • 1
    • 1

    dyn的elf符号表

    重定位信息


    重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时,相关的调用指令必须把控制传输到适当的目标执行地址。

    重定位表项


    可重定位文件必须包含如何修改其节区内容的信息,从而允许可执行文件和共享目标文件保存进程的程序映像的正确信息。重定位表项就是这样一些数据。

    可重定位表项的数据结构如下定义:

    Elf32_Rel

    成员 类型 描述
    r_offset Elf32_Addr/Elf64_Addr 给出了重定位动作所适用的位置
    r_info Elf32_Word/Elf64_Word 给出要进行重定位的符号表索引,以及将实施的重定位类型

    Elf32_Rela

    成员 类型 描述
    r_offset Elf32_Addr/Elf64_Addr 给出了重定位动作所适用的位置
    r_info Elf32_Word/Elf64_Word 给出要进行重定位的符号表索引,以及将实施的重定位类型
    r_addend Elf32_Word 给出一个常量补齐,用来计算将被填充到可重定位字段的数值

    重定位节区会引用两个其它节区:符号表、要修改的节区。节区头部的 sh_info 和sh_link 成员给出这些关系。不同目标文件的重定位表项对 r_offset 成员具有略微不同的解释。 
    r_info通常分为高8位和低8位,分别表示不同的含义:

    #define ELF32_R_SYM(i) ((i)>>8)
    #define ELF32_R_TYPE(i) ((unsigned char)(i))
    #define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t))
    • 1
    • 2
    • 3
    • 1
    • 2
    • 3

    高8位用作要进行重定位的符号表索引,通过它可以得出一个符号表项,而低8位表示将实施的重定位类型,它是和处理器相关的。

    ELF32_R_TYPE(i)


    重定位表项描述如何修改后面的指令和数据字段。一般,共享目标文件在创建时,其基本虚拟地址是 0,不过执行地址将随着动态加载而发生变化。

    参考 
    六星经典CSAPP-笔记(7)加载与链接(上)

    ELF文件结构

    ELF文件格式

    elf文件格式分析

    ELF (文件格式)

    浅谈Linux的可执行文件格式ELF(转帖)

    ELF文件的加载和动态链接过程

    可执行文件(ELF)格式的理解

    elf文件格式

    elf文件格式总结

    elf文件格式与动态链接库(非常之好)—–不可不看

    Executable and Linkable Format (ELF) (这专门介绍ELF文件格式的ABI的好文章,网络版在www.skyfree.org/linux/references/ELF_Format.pdf可以得到)

    展开全文
  • 日期 内核版本 架构 作者 GitHub CSDN 2016-06-04 Linux-4.5 X86 & arm ...对象文件格式对象文件首先,你需要知道的是所谓对象文件(Object files)有三个种类: 可重定位的对象文件(Relocatable file)
    日期 内核版本 架构 作者 GitHub CSDN
    2016-06-04 Linux-4.5 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度-之-进程的描述

    对象文件格式


    对象文件


    首先,你需要知道的是所谓对象文件(Object files)有三个种类:
    可重定位的对象文件(Relocatable file)
    可执行的对象文件(Executable file)
    可被共享的对象文件(Shared object file)

    • 可重定位的对象文件(Relocatable file)

      适于链接的可重定位文件(relocatable file),包含二进制代码和数据,能与其他可重定位对象文件在编译时合并创建出一个可执行文件。

      这是由汇编器汇编生成的 .o 文件。后面的链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)。我们可以使用 ar 工具将众多的 .o Relocatable object files 归档(archive)成 .a 静态库文件。如何产生 Relocatable file,你应该很熟悉了,请参见我们相关的基本概念文章和JulWiki。另外,可以预先告诉大家的是我们的内核可加载模块 .ko 文件也是 Relocatable object file。

    • 可执行的对象文件(Executable file)

      适于执行的可执行文件(executable file),包含可以直接拷贝进行内存执行的二进制代码和数据。用于提供程序的进程映像,加载的内存执行。

      这我们见的多了。文本编辑器vi、调式用的工具gdb、播放mp3歌曲的软件mplayer等等都是Executable object file。你应该已经知道,在我们的 Linux 系统里面,存在两种可执行的东西。除了这里说的 Executable object file,另外一种就是可执行的脚本(如shell脚本)。注意这些脚本不是 Executable object file,它们只是文本文件,但是执行这些脚本所用的解释器就是 Executable object file,比如 bash shell 程序。

    • 可被共享的对象文件(Shared object file)

      共享目标文件(shared object file),一种特殊的可重定位对象文件,能在加载时或运行时,装载进内存进行动态链接。连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。

      这些就是所谓的动态库文件,也即 .so 文件。如果拿前面的静态库来生成可执行程序,那每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空间;另外如果拿它们放到Linux系统上一起运行,也会浪费掉宝贵的物理内存。如果将静态库换成动态库,那么这些问题都不会出现。动态库在发挥作用的过程中,必须经过两个步骤:

      1. 链接编辑器(link editor)拿它和其他Relocatable object file以及其他shared object file作为输入,经链接处理后,生存另外的 shared object file 或者 executable file。

      2. 在运行时,动态链接器(dynamic linker)拿它和一个Executable file以及另外一些 Shared object file 来一起处理,在Linux系统里面创建一个进程映像。

    文件格式


    本质上,对象文件只是保存在磁盘文件中的一串字节,每个系统的文件格式都不尽相同:

    • Bell实验室的第一个Unix系统使用 a.out格式。

    • System V Unix的早期版本使用 Common Object File Format(COFF)。

    • Windows NT使用COFF的变种,叫做 Portable Executable(PE)。

    • 现代Unix系统,包括Linux、新版System V、BSD变种、Solaris都使用 Executable and Linkable Format(ELF)。

    ELF对象文件格式


    ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。它自最早在 System V 系统上出现后,被 xNIX 世界所广泛接受,作为缺省的二进制文件格式来使用。可以说,ELF是构成众多xNIX系统的基础之一。

    ELF代表Executable and Linkable Format。他是一种对可执行文件、目标文件和库使用的文件格式。

    他在Linux下成为标准格式已经很长时间, 代替了早年的a.out格式。ELF一个特别的优点在于, 同一文件格式可以用于内核支持的几乎所有体系结构上, 这不仅简化了用户空间工具程序的创建, 也简化了内核自身的程序设计。例如, 在必须为可执行文件生成装载程序例程时。

    但是文件格式相同并不意味着不同系统上的程序之间存在二进制兼容性, 例如, FreeBSD和Linux都使用ELF作为二进制格式。尽管二者在文件中组织数据的方式相同。但在系统调用机制以及系统调用的语义方面, 仍然有差别。这也是在没有中间仿真层的情况下, FreeBSD程序不能在linux下运行的原因(反过来同样是如此)。

    有一点是可以理解的, 二进制程序不能在不同体系结构交换(例如, 为Alpha CPU编译的Linux二进制程序不能在Sparc Linux上执行), 因为底层的体系结构是完全不同的。但是由于ELF的存在, 对所有体系结构而言, 程序本身的相关信息以及程序的各个部分在二进制文件中编码的方式都是相同的。

    Linux不仅将ELF用于用户空间程序和库, 还用于构建模块。内核本身也是ELF格式。

    ELF文件标准历史


    ELF是一种开放格式, 其规范可以自由获取。

    在ELF格式出来之后,TISC(Tool Interface Standard Committee)委员会定义了一套ELF标准。你可以从这里(http://refspecs.freestandards.org/elf/)找到详细的标准文档

    20世纪90年代,一些厂商联合成立了一个委员会(TISC委员会),起草并发布了一个ELF文件格式标准供公开使用,并且希望所有人能够遵循这项标准并且从中获益。1993年,委员会发布了ELF文件标准。当时参与该委员会的有来自于编译器的厂商,如Watcom和Borland;来自CPU的厂商如IBM和Intel;来自操作系统的厂商如IBM和Microsoft。1995年,委员会发布了ELF 1.2标准,自此委员会完成了自己的使命,不久就解散了。所以ELF文件格式标准的最新版本为1.2。

    文件类型 e_type成员表示ELF文件类型,即前面提到过的3种ELF文件类型,每个文件类型对应一个常量。系统通过这个常量来判断ELF的真正文件类型,而不是通过文件的扩展名。相关常量以“ET_”开头,

    TISC委员会前后出了两个版本,v1.1和v1.2。两个版本内容上差不多,但就可读性上来讲,我还是推荐你读 v1.2的。因为在v1.2版本中,TISC重新组织原本在v1.1版本中的内容,将它们分成为三个部分(books):
    a) Book I
    介绍了通用的适用于所有32位架构处理器的ELF相关内容
    b) Book II
    介绍了处理器特定的ELF相关内容,这里是以Intel x86 架构处理器作为例子介绍
    c) Book III
    介绍了操作系统特定的ELF相关内容,这里是以运行在x86上面的 UNIX System V.4 作为例子介绍

    值得一说的是,虽然TISC是以x86为例子介绍ELF规范的,但是如果你是想知道非x86下面的ELF实现情况,那也可以在http://refspecs.freestandards.org/elf/中找到特定处理器相关的Supplment文档。比方ARM相关的,或者MIPS相关的等等。另外,相比较UNIX系统的另外一个分支BSD Unix,Linux系统更靠近 System V 系统。所以关于操作系统特定的ELF内容,你可以直接参考v1.2标准中的内容。

    本文所使用的测试程序结构


    add.c


    #include <stdio.h>
    #include <stdlib.h>
    
    // 不指定寄存器实现两个整数相加
    int Add(int a, int b)
    {
        __asm__ __volatile__
        (
            //"lock;\n"
            "addl %1,%0;\n"
            : "=m"(a)
            : "r"(b), "m"(a)
          //  :
        );
    
        return a;
    }

    sub.c


    #include <stdio.h>
    #include <stdlib.h>
    
    // 不指定寄存器实现两个参数相减
    int Sub(int a, int b)
    {
        __asm__ __volatile__
        (
            "subl %1, %0;"
            : "=m"(a)
            : "r"(b), "m"(a)
     //       :
        );
    
        return a;
    }

    testelf.c


    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int a = 3, b = 5;
    
        printf("%d + %d = %d\n", a, b, Add(a, b));
        printf("%d - %d = %d\n", a, b, Sub(a, b));
    
    
        return EXIT_SUCCESS;
    }

    Makefile


    
    target=testelf_normal testelf_dynamic testelf_static
    
    MAIN_OBJS=testelf.o
    SUBS_OBJS=add.o sub.o
    
    DYNA_FILE=libtestelf.so
    STAT_FILE=libtestelf.a
    
    all:$(target)
    
    %.o : %.c
        $(CC) -c $^ -o $@
    
    clean :
        rm -rf $(MAIN_OBJS) $(SUBS_OBJS)
        rm -rf $(DYNA_FILE) $(STAT_FILE)
        rm -rf $(target)
    
    
    # Complie the execute
    testelf_normal:$(MAIN_OBJS) $(SUBS_OBJS)
        gcc $^ -o $@
    
    testelf_dynamic:$(MAIN_OBJS) $(DYNA_FILE)
        gcc  $^ -o $@ -L./ -ltestelf
    
    testelf_static:$(MAIN_OBJS) $(STAT_FILE)
        gcc  testelf.o -o $@ -static -L./ -ltestelf
    
    
    
    # Complie the Dynamic Link Library libtestelf.so
    libtestelf.so:$(SUBS_OBJS)
        gcc -fPCI -shared $^ -o $@
    
    # Complie the Static Link Library libtestelf.so
    libtestelf.a:$(SUBS_OBJS)
        ar -r $@ $^

    我们编写了两个库函数分别实现add和sub的功能, 然后编写了一个测试代码testelf.c调用了Add和Sub.

    然后我们的Mmakefile为测试程序编写了3分程序

    1. 普通的程序testelf_normal, 由add.o sub.o 和testelf.o直接链接生成

    2. 动态链接程序testelf_dynamic, 将add.o和sub.o先链接成动态链接库libtestelf.so, 然后再动态链接生成testelf_dynamic

    3. 静态链接程序testelf_static, 将add.o和sub.o先静态链接成静态库libtestelf.a, 然后再静态链接生成可执行程序testelf_staticke

    我们在源代码目录执行make后会完成编译, 编译完成后

    • add.o, sub.o和testelf.o是可重定位的对象文件(Relocatable file)

    • libtestelf.so是可被共享的对象文件(Shared object file)

    • testelf_normal, testelf_dynamic和testelf_static是可执行的对象文件(Executable file)

    如下图所示

    对象文件

    ELF可执行与链接文件格式详解


    布局和结构


    ELF文件由各个部分组成。

    为了方便和高效,ELF文件内容有两个平行的视角:一个是程序连接角度,另一个是程序运行角度

    elf文件的布局和结构

    首先图的左边部分,它是以链接视图来看待elf文件的, 从左边可以看出,包含了一个ELF头部,它描绘了整个文件的组织结构。它还包括很多节区(section)。这些节有的是系统定义好的,有些是用户在文件在通过.section命令自定义的,链接器会将多个输入目标文件中的相同的节合并。节区部分包含链接视图的大量信息:指令、数据、符号表、重定位信息等等。除此之外,还包含程序头部表(可选)和节区 头部表,程序头部表,告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。而节区头部表(Section Heade Table)包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。

    需要注意地是:尽管图中显示的各个组成部分是有顺序的,实际上除了 ELF 头部表以外,其他节区和段都没有规定的顺序。

    右半图是以程序执行视图来看待的,与左边对应,多了一个段(segment)的概念,编译器在生成目标文件时,通常使用从零开始的相对地址,而在链接过程中,链接器从一个指定的地址开始,根据输入目标文件的顺序,以段(segment)为单位将它们拼装起来。其中每个段可以包括很多个节(section)。

    • elf头部

      除了用于标识ELF文件的几个字节外, ELF头还包含了有关文件类型和大小的有关信息, 以及文件加载后程序执行的入口点信息

    • 程序头表(program header table)

      程序头表向系统提供了可执行文件的数据在进程虚拟地址空间中组织文件的相关信息。他还表示了文件可能包含的段数据、段的位置和用途

    • 段segment

      各个段保存了与文件爱你相关的各种形式的数据, 例如,符号表、实际的二进制码、固定值(如字符串)活程序使用的数值常数

    • 节头表section

      包含了与各段相关的附加信息。

    ELF基本数据类型定义


    在具体介绍ELF的格式之前,我们先来了解在ELF文件中都有哪些数据类型的定义:

    ELF数据编码顺序与机器相关,为了使数据结构更加通用, linux内核自定义了几种通用的数据, 使得数据的表示与具体体系结构分离

    但是由于32位程序和64位程序所使用的数据宽度不同, 同时64位机必须兼容的执行32位程序, 因此我们所有的数据都被定义为32bit和64bit两个不同类型的数据

    常规定义在include/uapi/linux中, 各个结构也可以按照需求重新定义

    32位机器上的定义

    名称 常规定义 大小 对齐 目的
    Elf32_Addr __u32 4 4 无符号程序地址
    Elf32_Half __u16 2 2 无符号中等整数
    Elf32_Off __u32 4 4 无符号文件偏移
    Elf32_SWord __u32 4 4 有符号大整数
    Elf32_Word __u32 4 4 无符号大整数
    unsigned char 1 1 无符号小整数

    64位机器上的定义*

    名称 常规定义 大小 对齐 目的
    Elf64_Addr __u64 8 8 无符号程序地址
    Elf64_Half __u16 2 2 无符号小整数
    Elf64_SHalf __s16 2 2 无符号小整数
    Elf64_Off __u64 8 8 无符号文件偏移
    Elf64_Sword __s32 4 4 有符号中等整数
    Elf64_Word __u32 4 4 无符号中等整数
    Elf64_Xword __u64 8 8 无符号大整数
    Elf64_Sxword __s64 8 8 有符号大整数
    unsigned char 1 1 无符号小整数

    ELF头部Elfxx_Ehdr


    elf头部用Elfxx_Ehdr结构(被定义在linux/uapi/linux/elf.h来表示, Elf32_Ehdr(32bit)Elf64_Ehdr(64bit)

    数据成员


    内部成员, 如下

    成员 类型 描述
    e_ident[EI_NIDENT] unsigned char 目标文件标识信息, EI_NIDENT=16, 因此共占用128位
    e_type Elf32_Half/Elf64_Half 目标文件类型
    e_machine Elf32_Half/Elf64_Half 目标体系结构类型
    e_version Elf32_Word/Elf64_Word 目标文件版本
    e_entry Elf32_Addr/Elf64_Addr 程序入口的虚拟地址,若没有,可为0
    e_phoff Elf32_Off/Elf64_Off 程序头部表格(Program Header Table)的偏移量(按字节计算),若没有,可为0
    e_shoff Elf32_Off/Elf64_Off 节区头部表格(Section Header Table)的偏移量(按字节计算),若没有,可为0
    e_flags Elf32_Word/Elf64_Word 保存与文件相关的,特定于处理器的标志。标志名称采用 EF_machine_flag的格式
    e_ehsize Elf32_Half/Elf64_Half ELF 头部的大小(以字节计算)
    e_phentsize Elf32_Half/Elf64_Half 程序头部表格的表项大小(按字节计算)
    e_phnum Elf32_Half/Elf64_Half 程序头部表格的表项数目。可以为 0
    e_shentsize Elf32_Half/Elf64_Half 节区头部表格的表项大小(按字节计算)
    e_shnum Elf32_Half/Elf64_Half 节区头部表格的表项数目。可以为 0
    e_shstrndx Elf32_Half/Elf64_Half 节区头部表格中与节区名称字符串表相关的表项的索引。如果文件没有节区名称字符串表,此参数可以为 SHN_UNDEF

    ELF魔数e_ident


    魔数

    很多类型的文件,其起始的几个字节的内容是固定的(或是有意填充,或是本就如此)。根据这几个字节的内容就可以确定文件类型,因此这几个字节的内容被称为魔数 (magic number)。此外在一些程序代码中,程序员常常将在代码中出现但没有解释的数字常量或字符串称为魔数 (magic number)或魔字符串。

    ELF魔数 我们可以从前面readelf的输出看到,最前面的”Magic”的16个字节刚好对应“Elf32_Ehdr”的e_ident这个成员。这16个字节被ELF标准规定用来标识ELF文件的平台属性,比如这个ELF字长(32位/64位)、字节序、ELF文件版本

    最开始的4个字节是所有ELF文件都必须相同的标识码,分别为0x7F、0x45、0x4c、0x46
    第一个字节对应ASCII字符里面的DEL控制符,
    后面3个字节刚好是ELF这3个字母的ASCII码。这4个字节又被称为ELF文件的魔数,几乎所有的可执行文件格式的最开始的几个字节都是魔数。

    比如a.out格式最开始两个字节为 0x01、0x07;

    PE/COFF文件最开始两个个字节为0x4d、0x5a,即ASCII字符MZ。

    这种魔数用来确认文件的类型,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载。

    接下来的一个字节是用来标识ELF的文件类的,0x01表示是32位的,0x02表示是64位的;第6个字是字节序,规定该ELF文件是大端的还是小端的(见附录:字节序)。第7个字节规定ELF文件的主版本号,一般是1,因为ELF标准自1.2版以后就再也没有更新了。后面的9个字节ELF标准没有定义,一般填0,有些平台会使用这9个字节作为扩展标志。

    各种魔数的由来

    a.out格式的魔数为0x01、0x07,为什么会规定这个魔数呢?

    UNIX早年是在PDP小型机上诞生的,当时的系统在加载一个可执行文件后直接从文件的第一个字节开始执行,人们一般在文件的最开始放置一条跳转(jump)指令,这条指令负责跳过接下来的7个机器字的文件头到可执行文件的真正入口。而0x01 0x07这两个字节刚好是当时PDP-11的机器的跳转7个机器字的指令。为了跟以前的系统保持兼容性,这条跳转指令被当作魔数一直被保留到了几十年后的今天。

    计算机系统中有很多怪异的设计背后有着很有趣的历史和传统,了解它们的由来可以让我们了解到很多很有意思的事情。这让我想起了经济学里面所谓的“路径依赖”,其中一个很有意思的叫“马屁股决定航天飞机”的故事在网上流传很广泛,有兴趣的话你可以在google以“马屁股”和“航天飞机”作为关键字搜索一下。
    其中需要注意地是e_ident是一个16字节的数组,这个数组按位置从左到右都是有特定含义,每个数组元素的下标在标准中还存在别称,如byte0的下标0别名为EI_MAG0,具体如下:

    名称 元素下标值 含义
    EI_MAG0 0 文件标识
    EI_MAG1 1 文件标识
    EI_MAG2 2 文件标识
    EI_MAG3 3 文件标识
    EI_CLASS 4 文件类
    EI_DATA 5 数据编码
    EI_VERSION 6 文件版本
    EI_PAD 7 补齐字节开始处
    EI_NIDENT 16 e_ident[]大小

    e_ident[EI_MAG0]~e_ident[EI_MAG3]即e_ident[0]~e_ident[3]被称为魔数(Magic Number),其值一般为0x7f,’E’,’L’,’F’

    e_ident[EI_CLASS](即e_ident[4])识别目标文件运行在目标机器的类别,取值可为三种值:

    名称 元素下标值 含义
    ELFCLASSNONE 0 非法类别
    ELFCLASS32 1 32位目标
    ELFCLASS64 2 64位目标

    e_ident[EI_DATA](即e_ident[5]):给出处理器特定数据的数据编码方式。即大端还是小端方式。取值可为3种:

    名称 元素下标值 含义
    ELFDATANONE 0 非法数据编码
    ELFDATA2LSB 1 高位在前
    ELFDATA2MSB 2 低位在前

    目标文件类型e_type


    e_type表示elf文件的类型

    文件类型 e_type成员表示ELF文件类型,即前面提到过的3种ELF文件类型,每个文件类型对应一个常量。系统通过这个常量来判断ELF的真正文件类型,而不是通过文件的扩展名。相关常量以“ET_”开头

    如下定义:

    名称 取值 含义
    ET_NONE 0 未知目标文件格式
    ET_REL 1 可重定位文件
    ET_EXEC 2 可执行文件
    ET_DYN 3 共享目标文件
    ET_CORE 4 Core 文件(转储格式)
    ET_LOPROC 0xff00 特定处理器文件
    ET_HIPROC 0xffff 特定处理器文件
    ET_LOPROC~ET_HIPROC 0xff00~0xffff 特定处理器文件

    目标体系结构类型e_machine


    e_machine表示目标体系结构类型

    名称 取值 含义
    EM_NONE 0 未指定
    EM_M32 1 AT&T WE 32100
    EM_SPARC 2 SPARC
    EM_386 3 Intel 80386
    EM_68K 4 Motorola 68000
    EM_88K 5 Motorola 88000
    EM_860 7 Intel 80860
    EM_MIPS 8 MIPS RS3000
    others 9~ 预留

    ELF版本e_version


    这个用来区分ELF标准的各个修订版本, 但是前面提到ELF最新版本就是1(1.2), 仍然是最新版, 因此目前不需要这个特性

    另外ELF头还包括了ELF文件的各个其他部分的长度和索引位置信息。因为这些部分的长度可能依程序而不同。所以在文件头部必须提供相应的数据.

    readelf -h查看elf头部


    可重定位的对象文件(Relocatable file)


    readelf -h add.o

    REL的elf文件头

    文件类型是, 说明是可重定位文件, 其代码可以移动至任何位置.

    该文件没有程序头表, 对需要进行链接的对象而言, 程序头表是不必要的, 为此所有长度都设置为0

    可执行的对象文件(Executable file)


    readelf -h testelf_dynamic

    exec的elf文件头

    可被共享的对象文件(Shared object file)


    readelf -h libtestelf.so

    dynamic的elf文件头

    程序头部Elf32_phdr


    以程序运行的角度看ELF文件, 就需要程序头表,即要运行这个elf文件,需要将哪些东西载入到内存镜像。而节区头部表是以elf资源的角度来看待elf文件的,即这个elf文件到底存在哪些资源,以及这些资源之间的关联关系,

    程序头部是一个表,它的起始地址在elf头部结构中的e_phoff成员指定,数量由e_phnum表示,每个程序头部表项的大小由e_phentsize指出。

    可执行文件或者共享目标文件的程序头部是一个结构数组,每个结构描述了一个段或者系统准备程序执行所必需的其它信息。目标文件的”段”包含一个或者多个”节区”,也就是”
    段内容(Segment Contents)”。程序头部仅对于可执行文件和共享目标文件有意义。

    下面来看程序头号部表项的数据结构

    成员 类型 描述
    p_type Elf32_Word/Elf64_Word 段类型
    p_offset Elf32_Off/Elf64_Off 段位置
    p_vaddr Elf32_Addr/Elf64_Addr 给出段的第一个字节将被放到内存中的虚拟地址
    p_paddr Elf32_Addr/Elf64_Addr 仅用于与物理地址相关的系统中
    p_filesz Elf32_Word/Elf64_Word 给出段在文件映像中所占的字节数
    p_memsz Elf32_Word/Elf64_Word 给出段在内存映像中占用的字节数
    p_flags Elf32_Word/Elf64_Word 与段相关的标志
    p_align Elf32_Word/Elf64_Word 对齐

    段类型p_type


    名称 取值 说明
    PT_NULL 0 此数组元素未用。结构中其他成员都是未定义的
    PT_DYNAMIC 2 数组元素给出动态链接信息
    PT_INTERP 3 数组元素给出一个 NULL 结尾的字符串的位置和长度,该字符串将被当作解释器调用。这种段类型仅对与可执行文件有意义(尽管也可能在共享目标文件上发生)。在一个文件中不能出现一次以上。如果存在这种类型的段,它必须在所有可加载段项目的前面
    PT_NOTE 4 此数组元素给出附加信息的位置和大小
    PT_SHLIB 5 此段类型被保留,不过语义未指定。包含这种类型的段的程序与 ABI不符
    PT_PHDR 6 此类型的数组元素如果存在,则给出了程序头部表自身的大小和位置,既包括在文件中也包括在内存中的信息。此类型的段在文件中不能出现一次以上。并且只有程序头部表是程序的内存映像的一部分时才起作用。如果存在此类型段,则必须在所有可加载段项目的前面
    PT_LOPROC~PT_HIPROC 0x70000000~0x7fffffff 此范围的类型保留给处理器专用语义

    readelf -l查看程序头表

    在程序头表之后, 列出了6个段, 这些组成了最终在内存中执行的程序.

    其还提供了各段在虚拟地址空间和物理空间的大小, 位置, 标志, 访问权限和对齐方面的信息. 还指定了yui个类型来更精确的描述段.

    各段的语义如下

    描述
    PHDR 保存了程序头表
    INTERP 指定在程序已经从可执行文件映射到内存之后, 必须调用的解释器. 在这里解释器并不意味着二进制文件的内容必须解释执行(比如Java的字节码需要Java虚拟机解释).它指的是这样一个程序:通过链接其他库, 来满足未解决的引用.
    通常/lib/ld-linux.so.2, /lib/ld-linux-ia-64.so.2等库, 用于在虚拟地址空间中国插入程序运行所需的动态库. 对几乎所有的程序来说, 可能C标准库都是必须映射的.还需要添加的各种库, 如GTK, QT, 数学库math, 线程库pthread等等
    LOAD 表示一个需要从二进制文件映射到虚拟地址的段. 其中保存了常量数据(如字符串), 程序的目标代码等
    DYNAMIC 该段保存了由动态链接器(即, INTERP中指定的解释器)使用的信息
    NOTE 保存了专有信息

    可重定位的对象文件(Relocatable file)


    readelf -l add.o

    rel的elf程序头表

    可重定向文件, 是一个需要链接的对象, 程序头表对其而言不是必要的, 因此这类文件一般没有程序头表

    可执行的对象文件(Executable file)


    readelf -l testelf_dynamic

    exec的elf程序头表

    可被共享的对象文件(Shared object file)


    readelf -l libtestelf.so

    dyn的elf程序头表

    虚拟地址空间中的各个段, 填充了来自ELF文件中特定的段的数据. 因而readelf输出的第二部分指定了那些节载入到哪些段(节段映射).

    物理地址信息讲被忽略, 因为该信息是由内盒根据物理页帧到虚拟地址空间中相应位置的映射情况动态分配的.只有在没有MMU(因而没有虚拟内存)的系统上该信息才是由意义的

    节区(Sections)


    节区中包含目标文件中的所有信息

    除了:ELF 头部、程序头部表格、节区头部表格。

    节区满足以下条件:

    1. 目标文件中的每个节区都有对应的节区头部描述它,反过来,有节区头部不意味着有节区。

    2. 每个节区占用文件中一个连续字节区域(这个区域可能长度为 0)。

    3. 文件中的节区不能重叠,不允许一个字节存在于两个节区中的情况发生。

    4. 目标文件中可能包含非活动空间(INACTIVE SPACE)。这些区域不属于任何头部和节区,其内容未指定。

    节区头部表格


    ELF文件在描述各段的内容时, 是指定了哪些节的数据映射到段中. 因此需要一个结构来管理各个节的内容, 即节头表

    节区头部表是以elf资源的角度来看待elf文件的,即这个elf文件到底存在哪些资源,以及这些资源之间的关联关系,而前面提到的程序头部表,则以程序运行来看elf文件的,即要运行这个elf文件,需要将哪些东西载入到内存镜像。

    ELF 头部中,

    e_shoff 成员给出从文件头到节区头部表格的偏移字节数;

    e_shnum给出表格中条目数目;

    e_shentsize 给出每个项目的字节数。

    从这些信息中可以确切地定位节区的具体位置、长度。

    从之前的描述中可知,每一项节区在节区头部表格中都存在着一项元素与它对应,因此可知,这个节区头部表格为一连续的空间,每一项元素为一结构体

    那么这个节区头部由elfxx_shdr(定义在include/uapi/linux/elf.h), 32位elf32_shdr, 64位elf64_shdr

    结构体的成员如下

    成员 类型 描述
    sh_name Elf32_Word/Elf64_Word 节区名,是节区头部字符串表节区(Section Header String Table Section)的索引。名字是一个 NULL 结尾的字符串
    sh_type Elf32_Word/Elf64_Word 节区类型
    sh_flags Elf32_Word/Elf64_Word 节区标志
    sh_addr Elf32_Addr/Elf64_Addr 如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。否则,此字段为 0
    sh_offset Elf32_Off/Elf64_Off 此成员的取值给出节区的第一个字节与文件头之间的偏移
    sh_size Elf32_Word/Elf64_Word 此成员给出节区的长度(字节数)
    sh_link Elf32_Word/Elf64_Word 此成员给出节区头部表索引链接。其具体的解释依赖于节区类型
    sh_info Elf32_Word/Elf64_Word 此成员给出附加信息,其解释依赖于节区类型
    sh_addralign Elf32_Word/Elf64_Word 某些节区带有地址对齐约束
    sh_entsize Elf32_Word/Elf64_Word 某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数

    节区类型sh_type


    sh_type的取值如下:

    名称 取值 说明
    SHT_NULL 0 此值标志节区头部是非活动的,没有对应的节区。此节区头部中的其他成员取值无意义
    SHT_PROGBITS 1 此节区包含程序定义的信息,其格式和含义都由程序来解释
    SHT_SYMTAB 2 此节区包含一个符号表。目前目标文件对每种类型的节区都只能包含一个,不过这个限制将来可能发生变化
    一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言)的符号,尽管也可用来实现动态链接
    SHT_STRTAB 3 此节区包含字符串表。目标文件可能包含多个字符串表节区
    SHT_RELA 4 此节区包含重定位表项,其中可能会有补齐内容(addend),例如 32 位目标文件中的 Elf32_Rela 类型。目标文件可能拥有多个重定位节区
    SHT_HASH 5 此节区包含符号哈希表。所有参与动态链接的目标都必须包含一个符号哈希表。目前,一个目标文件只能包含一个哈希表,不过此限制将来可能会解除
    SHT_DYNAMIC 6 此节区包含动态链接的信息。目前一个目标文件中只能包含一个动态节区,将来可能会取消这一限制
    SHT_NOTE 7 此节区包含以某种方式来标记文件的信息
    SHT_NOBITS 8 这种类型的节区不占用文件中的空间 , 其他方面和SHT_PROGBITS相似。尽管此节区不包含任何字节,成员sh_offset 中还是会包含概念性的文件偏移
    SHT_REL 9 此节区包含重定位表项,其中没有补齐(addends),例如 32 位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区
    SHT_SHLIB 10 此节区被保留,不过其语义是未规定的。包含此类型节区的程序与 ABI 不兼容
    SHT_DYNSYM 11 作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间
    SHT_LOPROC X70000000 这一段(包括两个边界),是保留给处理器专用语义的
    SHT_HIPROC OX7FFFFFFF 这一段(包括两个边界),是保留给处理器专用语义的
    SHT_LOUSER 0X80000000 此值给出保留给应用程序的索引下界
    SHT_HIUSER 0X8FFFFFFF 此值给出保留给应用程序的索引上界

    节区标志sh_flags


    sh_flag标志着此节区是否可以修改,是否可以执行,如下定义:

    名称 取值 含义
    SHF_WRITE 0x1 节区包含进程执行过程中将可写的数据
    SHF_ALLOC 0x2 此节区在进程执行过程中占用内存。某些控制节区并不出现于目标文件的内存映像中,对于那些节区,此位应设置为 0
    SHF_EXECINSTR 0x4 节区包含可执行的机器指令
    SHF_MASKPROC 0xF0000000 所有包含于此掩码中的四位都用于处理器专用的语义

    sh_link和sh_info字段的具体含义依赖于sh_type的值

    sh_type sh_link sh_info
    SHT_DYNAMIC 此节区中条目所用到的字符串表格的节区头部索引 0
    SHT_HASH 此哈希表所适用的符号表的节区头部索引 0
    SHT_REL
    SHT_RELA
    相关符号表的节区头部索引 重定位所适用的节区的节区头部索引
    SHT_SYMTAB
    SHT_DYNSYM
    相关联的字符串表的节区头部索引 最后一个局部符号(绑定 STB_LOCAL)的符号表索引值加一
    其它 SHN_UNDEF 0

    特殊节区


    有些节区是系统预订的,一般以点开头号,因此,我们有必要了解一些常用到的系统节区。

    名称 类型 属性 含义
    .bss SHT_NOBITS SHF_ALLOC +SHF_WRITE 包含将出现在程序的内存映像中的为初始化数据。根据定义,当程序开始执行,系统将把这些数据初始化为 0。此节区不占用文件空间
    .comment SHT_PROGBITS (无) 包含版本控制信息
    .data SHT_PROGBITS SHF_ALLOC + SHF_WRITE 这些节区包含初始化了的数据,将出现在程序的内存映像中
    .data1 SHT_PROGBITS SHF_ALLOC + SHF_WRITE 这些节区包含初始化了的数据,将出现在程序的内存映像中
    .debug SHT_PROGBITS (无) 此节区包含用于符号调试的信息
    .dynamic SHT_DYNAMIC 此节区包含动态链接信息。节区的属性将包含 SHF_ALLOC 位。是否 SHF_WRITE 位被设置取决于处理器
    .dynstr SHT_STRTAB SHF_ALLOC 此节区包含用于动态链接的字符串,大多数情况下这些字符串代表了与符号表项相关的名称
    .dynsym SHT_DYNSYM SHF_ALLOC 此节区包含了动态链接符号表
    .fini SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行这里的代码
    .got SHT_PROGBITS 此节区包含全局偏移表
    .hash SHT_HASH SHF_ALLOC 此节区包含了一个符号哈希表
    .init SHT_PROGBITS SHF_ALLOC +SHF_EXECINSTR 此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码
    .interp SHT_PROGBITS 此节区包含程序解释器的路径名。如果程序包含一个可加载的段,段中包含此节区,那么节区的属性将包含 SHF_ALLOC 位,否则该位为 0
    .line SHT_PROGBITS (无) 此节区包含符号调试的行号信息,其中描述了源程序与机器指令之间的对应关系。其内容是未定义的
    .note SHT_NOTE (无) 此节区中包含注释信息,�9C�独立的格式。
    .plt SHT_PROGBITS 此节区包含过程链接表(procedure linkage table)
    .relname
    .relaname
    SHT_REL
    SHT_RELA
    这些节区中包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text
    .rodata
    .rodata1
    SHT_PROGBITS SHF_ALLOC 这些节区包含只读数据,这些数据通常参与进程映像的不可写段
    .shstrtab SHT_STRTAB 此节区包含节区名称
    .strtab SHT_STRTAB 此节区包含字符串,通常是代表与符号表项相关的名称。如果文件拥有一个可加载的段,段中包含符号串表,节区的属性将包含SHF_ALLOC 位,否则该位为 0
    .symtab SHT_SYMTAB 此节区包含一个符号表。如果文件中包含一个可加载的段,并且该段中包含符号表,那么节区的属性中包含SHF_ALLOC 位,否则该位置为 0
    .text SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含程序的可执行指令

    readelf -S查看节区头表


    可重定位的对象文件(Relocatable file)


    readelf -S add.o

    rel的elf节区头表

    可重定向文件, 是一个需要链接的对象, 程序头表对其而言不是必要的, 因此这类文件一般没有程序头表

    可执行的对象文件(Executable file)


    readelf -S testelf_dynamic

    rel的elf节区头表

    可被共享的对象文件(Shared object file)


    readelf -S libtestelf.so

    dyn的elf程序头表

    字符串表


    字符串表


    首先要知道,字符串表它本身就是一个节区,从第二章描述中可知,每一个节区都存在一个节区头部表项与之对应,所以字符串表这个节区也存在一个节区头部表项对应,而在elf文件头部结构中存在一个成员e_shstrndx给出这个节区头部表项的索引位置。因此可以通过

    shstrab  = (rt_uint8_t *)module_ptr +shdr[elf_module->e_shstrndx].sh_offset;

    来得到字符串表的起始位置。
    字符串表节区包含以 NULL(ASCII 码 0)结尾的字符序列,通常称为字符串。ELF目标文件通常使用字符串来表示符号和节区名称。对字符串的引用通常以字符串在字符
    串表中的下标给出。

    一般,第一个字节(索引为 0)定义为一个空字符串。类似的,字符串表的最后一个字节也定义为 NULL,以确保所有的字符串都以 NULL 结尾。索引为 0 的字符串在
    不同的上下文中可以表示无名或者名字为 NULL 的字符串。

    允许存在空的字符串表节区,其节区头部的 sh_size 成员应该为 0。对空的字符串表而言,非 0 的索引值是非法的。

    例如:对于各个节区而言,节区头部的 sh_name 成员包含其对应的节区头部字符串表节区的索引,此节区由 ELF 头的 e_shstrndx 成员给出。下图给出了包含 25 个字节的一个字符串表,以及与不同索引相关的字符串。

    字符

    那么上面字符串表包含以下字符串:

    索引 字符串
    0 (无)
    1 name.
    7 Variable
    11 able
    16 able
    24 (空字符串)

    符号表(Symbol Table)


    首先,符号表同样本身是一节区,也存在一对应节区头部表项。

    目标文件的符号表中包含用来定位、重定位程序中符号定义和引用的信息。

    符号表索引是对此数组的索引。索引0表示表中的第一表项,同时也作为未定义符号的索引。

    符号表是由一个个符号元素组成,用elfxx_sym来结构来表示, 定义在include/uapi/linux/elf.h, 同样32位为elf32_sym, 64位对应elf64_sym

    每个元素的数据结构如下定义:

    成员 类型 描述
    st_name Elf32_Word/Elf64_Word 名称,索引到字符串表
    st_value Elf32_AddrElf64_Addr 给出相关联的符号的取值。依赖于具体的上下文
    st_size Elf32_Word/Elf64_Word 相关的尺寸大小
    st_info unsigned char 给出符号的类型和绑定属性
    st_other unsigned char 该成员当前包含 0,其含义没有定义
    st_shndx Elf32_Half/Elf64_Half 给出相关的节区头部表索引。某些索引具有特殊含义

    st_info给出符号的类型和绑定属性


    st_info 中包含符号类型和绑定信息,操纵方式如:

    #define ELF32_ST_BIND(i) ((i)>>4)
    #define ELF32_ST_TYPE(i) ((i)&0xf)
    #define ELF32_ST_INFO(b, t) (((b)<<4) + ((t)&0xf))

    st_info 的高四位(ELF32_ST_BIND(i))

    表示符号绑定,用于确定链接可见性和行为。具体的绑定类型如:

    名称 取值 说明
    STB_LOCAL 0 局部符号在包含该符号定义的目标文件以外不可见。相同名称的局部符号可以存在于多个文件中,互不影响
    STB_GLOBAL 1 全局符号对所有将组合的目标文件都是可见的。一个文件中对某个全局符号的定义将满足另一个文件对相同全局符号的未定义引用
    STB_WEAK 2 弱符号与全局符号类似,不过他们的定义优先级比较低
    STB_LOPROC 13 处于这个范围的取值是保留给处理器专用语义的
    STB_HIPROC 15 处于这个范围的取值是保留给处理器专用语义的

    全局符号与弱符号之间的区别主要有两点:

    1. 当链接编辑器组合若干可重定 位 的 目 标 文 件 时 , 不 允 许 对 同 名 的STB_GLOBAL 符号给出多个定义。另一方面如果一个已定义的全局符号已经存在,出现一个同名的弱符号并不会产生错误。链接编辑器尽关心全局符号,忽略弱符号。类似地,如果一个公共符号(符号的 st_shndx 中包含 SHN_COMMON),那么具有相同名称的弱符号出现也不会导致错误。链接编辑器会采纳公共定义,而忽略弱定义。

    2. 当链接编辑器搜索归档库(archive libraries)时,会提取那些包含未定义全局符号的档案成员。成员的定义可以是全局符号,也可以是弱符号。连接编辑器不会提取档案成员来满足未定义的弱符号。未能解析的弱符号取值为 0。

    在每个符号表中,所有具有 STB_LOCAL 绑定的符号都优先于弱符号和全局符号。符号表节区中的 sh_info 头部成员包含第一个非局部符号的符号表索引。

    st_info的低四位ELF32_ST_TYPE(i)

    定义如下

    名称 取值 说明
    STT_NOTYPE 0 符号的类型没有指定
    STT_OBJECT 1 符号与某个数据对象相关,比如一个变量、数组等等
    STT_FUNC 2 符号与某个函数或者其他可执行代码相关
    STT_SECTION 3 符号与某个节区相关。这种类型的符号表项主要用于重定位,通常具有 STB_LOCAL 绑定
    STT_FILE 4 传统上,符号的名称给出了与目标文件相关的源文件的名称。文件符号具有 STB_LOCAL 绑定,其节区索引是SHN_ABS,并且它优先于文件的其他 STB_LOCAL 符号(如果有的话)
    STT_LOPROC~STT_HIPROC 13~15 此范围的符号类型值保留给处理器专用语义用途

    在共享目标文件中的函数符号(类型为 STT_FUNC)具有特别的重要性。当其他目标文件引用了来自某个共享目标中的函数时,链接编辑器自动为所引用的符号创建过
    程链接表项。类型不是 STT_FUNC 的共享目标符号不会自动通过过程链接表进行引用。

    如果一个符号的取值引用了某个节区中的特定位置,那么它的节区索引成员(st_shndx)包含了其在节区头部表中的索引。当节区在重定位过程中被移动时,符号的取值也会随之变化,对符号的引用始终会“指向”程序中的相同位置。

    st_shndx


    如前面所述,st_shndx给出相关的节区头部表索引。但其值也存在一些特殊值,具有某些特殊的含义:

    名称 取值 说明
    SHN_ABS 符号具有绝对取值,不会因为重定位而发生变化
    SHN_COMMON 符号标注了一个尚未分配的公共块。符号的取值给出了对齐约束,与节区的 sh_addralign成员类似。就是说,链接编辑器将为符号分配存储空间,地址位于 st_value 的倍数处。符号的大小给出了所需要的字节数
    SHN_UNDEF 此节区表索引值意味着符号没有定义。当链接编辑器将此目标文件与其他定义了该符号的目标文件进行组合时,此文件中对该符号的引用将被链接到实际定义的位置

    st_value


    不同的目标文件类型中符号表项对 st_value 成员具有不同的解释:

    1. 在可重定位文件中,st_value 中遵从了节区索引为 SHN_COMMON 的符号的对齐约束。

    2. 在可重定位的文件中,st_value 中包含已定义符号的节区偏移。就是说,st_value 是从 st_shndx 所标识的节区头部开始计算,到符号位置的偏移。

    3. 在可执行和共享目标文件中,st_value 包含一个虚地址。为了使得这些文件的符号对动态链接器更有用,节区偏移(针对文 件的解释)让位于虚拟地址(针对内存的解释),因为这时与节区号无关。

    尽管符号表取值在不同的目标文件中具有相似的含义,适当的程序可以采取高效的数据访问方式。

    nm查看符号表


    nm *.o

    rel的elf符号表

    可重定向文件, 是一个需要链接的对象, 程序头表对其而言不是必要的, 因此这类文件一般没有程序头表

    可执行的对象文件(Executable file)


    nm testelf_dynamic

    exec的elf符号表

    可被共享的对象文件(Shared object file)


    nm libtestelf.so

    dyn的elf符号表

    重定位信息


    重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时,相关的调用指令必须把控制传输到适当的目标执行地址。

    重定位表项


    可重定位文件必须包含如何修改其节区内容的信息,从而允许可执行文件和共享目标文件保存进程的程序映像的正确信息。重定位表项就是这样一些数据。

    可重定位表项的数据结构如下定义:

    Elf32_Rel

    成员 类型 描述
    r_offset Elf32_Addr/Elf64_Addr 给出了重定位动作所适用的位置
    r_info Elf32_Word/Elf64_Word 给出要进行重定位的符号表索引,以及将实施的重定位类型

    Elf32_Rela

    成员 类型 描述
    r_offset Elf32_Addr/Elf64_Addr 给出了重定位动作所适用的位置
    r_info Elf32_Word/Elf64_Word 给出要进行重定位的符号表索引,以及将实施的重定位类型
    r_addend Elf32_Word 给出一个常量补齐,用来计算将被填充到可重定位字段的数值

    重定位节区会引用两个其它节区:符号表、要修改的节区。节区头部的 sh_info 和sh_link 成员给出这些关系。不同目标文件的重定位表项对 r_offset 成员具有略微不同的解释。
    r_info通常分为高8位和低8位,分别表示不同的含义:

    #define ELF32_R_SYM(i) ((i)>>8)
    #define ELF32_R_TYPE(i) ((unsigned char)(i))
    #define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t))

    高8位用作要进行重定位的符号表索引,通过它可以得出一个符号表项,而低8位表示将实施的重定位类型,它是和处理器相关的。

    ELF32_R_TYPE(i)


    重定位表项描述如何修改后面的指令和数据字段。一般,共享目标文件在创建时,其基本虚拟地址是 0,不过执行地址将随着动态加载而发生变化。

    参考
    六星经典CSAPP-笔记(7)加载与链接(上)

    ELF文件结构

    ELF文件格式

    elf文件格式分析

    ELF (文件格式)

    浅谈Linux的可执行文件格式ELF(转帖)

    ELF文件的加载和动态链接过程

    可执行文件(ELF)格式的理解

    elf文件格式

    elf文件格式总结

    elf文件格式与动态链接库(非常之好)—–不可不看

    Executable and Linkable Format (ELF) (这专门介绍ELF文件格式的ABI的好文章,网络版在 www.skyfree.org/linux/references/ELF_Format.pdf可以得到)

    展开全文
  • 文件格式是在文件中存储信息的一种标准方法。首先,文件格式指定文件是一个二进制或ASCII文件。其次,文件展示了文件的组织形式。例如,逗号分隔值(CSV)文件格式存储在纯文本的表格数据。 2、为什么数据科学家...
  • linux 压缩与归档

    2020-08-16 17:21:28
    6.2tar归档 可以将多个文件和文件夹保存为单个文件,同时...向归档文件中添加文件 (-r) $ tar -rvf original.tar new_file 下面方法列出归档文件中的内容 $ tar -tf archive.tar 可以使用-v或-vv选项获知在归档或者
  • Linux 压缩及归档工具详解常用工具压缩工具gzip / gunzip / zcatbzip2 / bunzip2 / bzcatxz / unxz / xzcat归档工具tar1、创建归档2、展开归档3、查看归档文件和文件列表4、归档压缩工具zcf / zxfjcf / jxfJcf / ...
  • 在上一讲中,我们介绍了DICOM标准中的网络传输功能,即利用通信线路进行DICOM信息交换。这一讲将介绍通过存储介质而进行的信息交换。将图像、诊断、检查的结果等信息存储在如软盘和光盘等存储介质中,实现在不同的...
  • Author:kevinelstri DateTime:2017/3/13译文:How to read most ... 文件格式是在文件中存储信息的一种标准方法。首先,文件格式指定文件是一个二进制或ASCII文件。其次,文件展示了文件的组织形式。例如,逗号分隔
  • 文件格式是在文件中存储信息的一种标准方法。首先,文件格式指定文件是一个二进制或ASCII文件。其次,文件展示了文件的组织形式。例如,逗号分隔值(CSV)文件格式存储在纯文本的表格数据。 image.png 2、为...
  • shell归档与备份

    2017-10-29 15:52:50
    归档程序: tar – 磁带打包工具 zip – 打包和压缩文件 还有文件同步程序: rsync – 同步远端文件和目录 压缩文件[hjw@localhost ~]$ less hjw.txt [hjw@localhost ~]$ ls -l hjw.txt -rw-rw-r--. 1 hjw hjw 1054 ...
  • 有关GNSS的数据文件的下载地址、命名方式介绍。
  • Linux常用命令之归档

    2016-04-06 10:04:56
    在Linux下归档是常用的操作之一,使用tar命令打出来的包我们称为tar包,tar包文件的命令通常都是以.tar结尾的。生成tar包后,就可以用其它的程序来进行压缩。我们也可以在归档的过程中指定压缩类型。1.tar命令格式...
  • Python常用库 - 【持续整理归档

    千次阅读 2020-01-06 12:50:10
    Python常用库 - 【持续整理归档】,比较多,会逐步慢慢细化分类和扩从python常用库。 目录 1、常用库 2、Python文件处理库 3、Python图像处理库 4、Python游戏和多媒体类库 5、大数据与科学计算 6、其功能与...
  • 压缩、解压缩和归档工具压缩:拿时间换空间压缩比:压缩前与压缩后体积大小的比例,压缩后的文件体积越小压缩比越大;压缩后的文件大小与源文件的大小越接近压缩比越小压缩操作很耗CPU压缩工具早期的压缩、解压缩的...
  • RMAN 配置归档日志删除策略

    千次阅读 2018-11-17 22:47:44
    RMAN 配置归档日志删除策略
  • 下面对归档文件格式的描述仅仅是对 1.5 以后的版本是有 效的      ======================================================================================================= RAR archive file ...
  • 下面对归档文件格式的描述仅仅对 1.5 以后的版本是有 效的 本文由CSDN-蚍蜉撼青松【主页: http://blog.csdn.net/howeverpf 】整理翻译,欢迎转载,但请务必注明出处!      ==============...
  • 本文档描述了在不安装连通性选件包的情况下将WinCC归档变量导出到Excel文件的三种方法,供参考。  WinCC的过程值归档数据是通过算法进行了压缩处理的,一般情况,要读取出WinCC的归档变量数据,需安装连通..
  • logback 日志输出格式

    千次阅读 2019-09-24 10:36:13
    【前言】 日志对一个系统的重要性不言而喻;日志通常是在排查问题时给人看,一个友好的...下面为大家共享一下通过设置logback日志输出格式,打印出令人欣喜的日志样式。 【搞一下日志格式】 一、未指定日志格...
  • c++文件操作大全

    万次阅读 多人点赞 2019-01-17 15:43:06
    c++文件操作大全 基于C的文件操作  在ANSI C中,对文件的操作分为两种方式,即流式文件操作和I/O文件操作,下面就分别介绍之。 一、流式文件操作 这种方式的文件操作有一个重要的结构FILE,FILE在stdio.h中定义...
  • Linux文件系统及常用命令

    千次阅读 2016-03-08 22:00:00
    Linux文件系统介绍: 一 、Linux文件结构  文件结构是文件存放在磁盘等存贮设备上的组织方法。主要体现在对文件和目录的组织上。目录提供了管理文件的一个方便而有效的途径。  Linux使用树状目录结构,在安装...
  • Linux下文件的备份

    万次阅读 2004-12-14 16:46:00
    (本文章基于Redhat9)一、使用tar命令进行备份tar命令本来的作用是能够把很多文件放到一个小文件中进行归档,但是同时又能够对文件进行压缩。所以,一般情况下我们都采用tar进行文件整理和压缩。/* 把/root整个目录...
  • linux 归档打包命令 tar

    2015-08-28 10:02:00
    2019独角兽企业重金招聘Python工程师标准>>> ...
  • linux下文件时间戳修改

    千次阅读 2016-09-19 09:56:15
    1.touch命令 ...命令格式: touch [选项]… 文件… 命令参数: -a 或–time=atime或–time=access或–time=use 只更改存取时间。 -c 或–no-create 不建立任何文档。 -d 使用指定的日期时间,而非现在的
  • FAT32文件系统结构详解

    万次阅读 多人点赞 2018-04-13 15:38:58
    1. SD卡中FAT32文件系统快速入门 1.1. 理论知识 1.1.1. MBR(Main Boot Record) 主引导记录,占446字节, 为计算机启动后从可启动介质上首先装入内存并且执行的代码,通常用来解释分区结构 1.1.2. DBR(DOS Boot ...
  • 该工具用于将MODIS标准命名的数据文件按照日期进行重命名,方便后续处理。 二、工具版本 V1.0 三、操作说明 软件工具主界面如下图所示: 1、数据目录 选择指定格式的数据所在目录。 2、数据格式 指定...
  • C++Builder文件操作大全

    千次阅读 2019-04-02 08:57:34
    C++Builder文件操作大全 在编程的过程中,文件的操作是一个经常用到的问题,在C++Builder中,可以使用多种方法对文件操作,下面我就按以下几个部分对此作详细介绍,就是: 1、基于C的文件操作; 2、基于C++的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 11,006
精华内容 4,402
关键字:

归档文件日期标准格式