前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ELF文件程序表头和代码实现ELF文件加载

ELF文件程序表头和代码实现ELF文件加载

作者头像
望月从良
发布2020-12-15 11:19:51
1.6K0
发布2020-12-15 11:19:51
举报
文章被收录于专栏:Coding迪斯尼

前面章节我们了解了ELF文件的头部结构,这次我们深入了解另一个非常重要的数据结构,那就是程序表头。操作系统严重依赖该结构来加载ELF文件或是实现动态链接。程序表头反映的是当ELF加载到内存后所形成的“视图”或结构,也就是说ELF文件存在硬盘上或者被加载到内存,它展现出来的形态不一致。

我们先看程序表头的数据结构:

代码语言:javascript
复制
typedef  struct {
    unit32_t  p_type;    #数据类型
    uint332_t  p_flags; #标志位
    uint64_t  p_offset; #在ELF文件中的偏移
    uint64_t  p_vaddr;  #虚拟地址
    uint64_t  p_paddr;  #物理地址
    uint64_t  p_fllesz;  #在硬盘上的大小
    uint64_t  p_memsz;  #在内存中大小
    uint64_t  p_align;  #内存对齐方式
} Elf64_Phdr;

使用命令 readelf —wide —segments a.out可以读取程序表头内容信息:

这里需要注意的是,程序表头其实没有什么新意,它其实对应前面说过的若干个段所形成的集合。接下来我们看每个字段的含义。

p_type对应表头的类型,常用的数值有PT_LOAD, PT_DYNAMIC, PT_INTER。如果取值PT_LOAD,意味着表头对应的段需要加装到内存中;从上图看到有两个表头的类型为PT_LOAD,分别为第3和4,而第3个表头对应段的集合为.init_array .fini_array等,第4个表头对应段集合为.dynamic,这意味着这些段需要加载到内存中,同时每个表头对应的段都要合成一个整体加载到表头中所指定的位置。

PT_FLAGS对应段加载到内存后的读写权限,常用的值有PF_X,PF_W,PF_R。PF_X表示表头对应的段可以被执行,PF_W对应加载的那些段可以被修改,PT_R表示加载的段可以被读取。p_offset表示表头对应那些段的起始地址,p_vaddr表示表头对应段该加载的虚拟位置,p_filesz表示表头对应段在硬盘上的大小,p_memsz表示表头对应段在加载到内存后的大小。

你可能会困惑,为何p_filesz和p_memsz的值不一样。这是因为有些段在硬盘上不占据容量,只有加载到内存时才分配容量。最后p_align表示内存对齐方式,它的取值为2的指数,同时p_vaddr必须等于(p_offset % p_align)

了解了ELF二进制内部原理后,我们需要实现手动加载ELF文件,实现这个目标,我们需要依赖一个库叫libbfd,这个库提供很多功能让调用者能解读X86架构下的通用二进制可执行文件。其安装可以使用如下命令:

sudo apt-get install -y libbfd-dev

基本上所有版本的Linux都会附带这个代码库,该代码库提供了一个类叫Binary,用于对可执行二进制文件的抽象,同时还有Section类,它是对前面我们提到的段数据结构的抽象;同时它还提供Symbol类,这是对符号表的抽象,接下来我们先看看其基本使用方法:

代码语言:javascript
复制
#include <stdio.h>
#include <stdint.h>
#include <string>
#include "../inc/loader.h"

int  main(int argc, char *argv[]) {

    size_t i;
    Binary  bin;  //represent elf file 
    Section  *sec;
    Symbol  *sym;
    std::string  fname;

    if  (argc < 2) {
        printf("need to set binary file name");
        return 1;
    }

    fname.assign(argv[1]);
    if (load_binary(fname, &bin, Binary::BIN_TYPE_AUTO) < 0) {
        printf("load binary fail!");
        return 1;
    }


    printf("loaded binary file name:%s", bin.filename.c_str());
    printf("loaded binary file type: %s", bin.type_str.c_str());
    printf("loaded binary file entry@0X%016jx\n: ", bin.entry);
    printf("loaded binary file bits: %u", bin.bits);

    for(i = 0; i < bin.sections.size(); i++) {
        sec = &bin.sections[i];
        printf("   0x%16jx  %-8ju  %-20s  %s\n", sec->vma, sec->size, sec->name.c_str(),
        sec->type == Section::SEC_TYPE_CODE?"CODE":"DATA");    
    }

    if (bin.symbols.size() > 0) {
        printf("scanned symbol tables:\n");
        for (i = 0; i < bin.symbols.size(); i++) {
            sym = &bin.symbols[i];
            printf("   %-40s  0x%016jx  %s\n", sym->name.c_str(), sym->addr, 
            (sym->type & Symbol::SYM_TYPE_FUNC)? "FUNC":"");
        }
    }

    unload_binary(&bin);

    return 0;
}

代码中需要注意的是,loader.h是来自libbfd库的头文件,读者需要修改代码中该文件的路径以对应你电脑上libbfd的安装路径。Binary类用于对整个elf文件的抽象,通过它可以访问ELF文件相关信息,Section是对前面章节描述的段对象的抽象,Symbol是对前面章节符号表对象的抽象。

load_binary是来自libbfd库提供的函数,它将elf文件加载到内存中。上面代码编译时对应的Makefile内容为:

代码语言:javascript
复制
CXX=g++
OBJ=my_loader

.PHONY: all clean

all: $(OBJ)

loader.o: ../inc/loader.cc
    $(CXX) -std=c++11 -c ../inc/loader.cc

my_loader: loader.o my_loader.cc
    $(CXX) -std=c++11 -o my_loader my_loader.cc loader.o -lbfd

clean:
    rm -f $(OBJ) *.o

执行make命令编译后,在本地目录会有my_loader可执行文件,使用命令./my_load a.out即可让程序加载a.out文件并输出一系列信息:

对于libbfd更加详细的使用方法,我们在后续章节会详细介绍。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-12-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Coding迪斯尼 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档