2018-05-19 17:39:27 cw312644167 阅读数 856

elf文件格式

我们的程序是通过gcc编译的,在linux下,gcc编译出来的可执行文件是elf格式的二进制文件。那么肯定要elf文件进行解析才能正确的得到进程可执行数据的位置。

下面介绍一下elf格式的几个基本概念

一个程序中最重要的部分是段和节,他们是真正的程序体,存储程序执行所需要的数据,程序中有很多段,常见的有代码段和数据段,段是由节组成的。多个节经过链接之后被合并成一个段。

段和节的信息用header来描述,程序头是program header,节头是section header。

程序中段的大小和数量不固定,节也是如此,因此需要一个专门的数据结构来描述他们,这个就是程序头表和节头表,他们用来存储多个程序头和节头,相当于数组的概念。

由于程序中段和节的数量不固定,程序头表和节头表的大小也就不固定。并且各表在程序中存储的先后顺序不同,所有这些表在程序中存储的位置也是不固定了,为了能方便的找到这些表的位置,获取其信息,需要一个固定的结构来描述他们,记录其存储的位置和大小等信息,这个结构就是elf header

elf格式的作用体现在两方面,一是链接阶段,二是运行阶段。下图是这两方面elf格式数据的布局

下面重点说一下elf header中的数据

elf header

/* 32位elf头 */
struct Elf32_Ehdr
{
    unsigned char e_ident[16];
    Elf32_Half e_type;
    Elf32_Half e_machine;
    Elf32_Word e_version;
    Elf32_Addr e_entry;
    Elf32_Off e_phoff;
    Elf32_Off e_shoff;
    Elf32_Word e_flags;
    Elf32_Half e_ehsize;
    Elf32_Half e_phentsize;
    Elf32_Half e_phnum;
    Elf32_Half e_shentsize;
    Elf32_Half e_shnum;
    Elf32_Half e_shstrndx;
};

上面是elf header中存储的数据,里面涉及这几种数据类型,数据类型的本质是其所占的字节数,赋予数据类型意义是为了方便里面,下图是这几种类型代表的意义。

接下来是e_ident这个成员所表示的意义,见下图

下面是e_type所代表的意义

elf目标文件类型 取值 意义
ET_NONE 0 未知目标文件格式
ET_REL 1 可重定位文件
ET_EXEC 2 可执行文件
ET_DYN 3 动态共享目标文件
ET_CORE 4 core文件
ET_LOPROC 0xff00 特定处理器文件的扩展下边界
ET_HIPROC 0xffff 特定处理器文件的扩展上边界

虽然这里有很多的类型,但我们使用的只有ET_EXEC.

接下来是e_machine,它表示该文件在哪种硬件平台上才能运行

后面还有的数据不一一描述,这里从书上截图来说明

program header

程序头是专门用来描述段信息的,这个段不是内存中的段,内存中的段是记录在全局描述符表中的。程序头描述的段是磁盘上程序中的一个段,常见的如代码段和数据段,下面是其结构

struct Elf32_Phdr
{
    Elf32_Word p_type;
    Elf32_Off p_offset;
    Elf32_Addr p_vaddr;
    Elf32_Addr p_paddr;
    Elf32_Word p_filesz;
    Elf32_Word p_memsz;
    Elf32_Word p_flags;
    Elf32_Word p_align;
};

p_type所表示的意义如下

p_offset表示本段在文件的偏移量
p_vaddr表示本段在内存中起始的虚拟地址
p_paddr仅用于与物理地址相关的系统中
p_fiesz表示本段在文件中的大小
p_memsz表示本段在内存中的大小
p_flags的意义见下图

p_align表示本段在文件和内存中的对齐方式。

目标文件在链接之后代码和数据等资源都是在段中,有了上面这些结构来记录相关信息,程序在加载的时候就根据这些信息从磁盘的某个位置将程序运行所需的资源加载到内存中,接下来通过一个实例对elf文件进行分析一下。

这是我随便找的一个可执行文件查看的数据,用路线标出来的属于elf header中的数据,红线标出来的属于一个program header的数据,具体的意义可以对照着上面结构的字段去看。

接下来的工作就是要实现一个加载器,将一个可执行文件的段数据加载到内存中去。使cs:eip指向其入口地址,一个进程就运行起来了。

实现进程加载器

elf header和program header的数据结构

typedef uint32_t Elf32_Word, Elf32_Addr, Elf32_Off;
typedef uint16_t Elf32_Half;

/* 32位elf头 */
struct Elf32_Ehdr
{
    unsigned char e_ident[16];
    Elf32_Half e_type;
    Elf32_Half e_machine;
    Elf32_Word e_version;
    Elf32_Addr e_entry;
    Elf32_Off e_phoff;
    Elf32_Off e_shoff;
    Elf32_Word e_flags;
    Elf32_Half e_ehsize;
    Elf32_Half e_phentsize;
    Elf32_Half e_phnum;
    Elf32_Half e_shentsize;
    Elf32_Half e_shnum;
    Elf32_Half e_shstrndx;
};

/* 程序头表Program header.就是段描述头 */
struct Elf32_Phdr
{
    Elf32_Word p_type; // 见下面的enum segment_type
    Elf32_Off p_offset;
    Elf32_Addr p_vaddr;
    Elf32_Addr p_paddr;
    Elf32_Word p_filesz;
    Elf32_Word p_memsz;
    Elf32_Word p_flags;
    Elf32_Word p_align;
};

/* 段类型 */
enum segment_type
{
    PT_NULL,    // 忽略
    PT_LOAD,    // 可加载程序段
    PT_DYNAMIC, // 动态加载信息
    PT_INTERP,  // 动态加载器名称
    PT_NOTE,    // 一些辅助信息
    PT_SHLIB,   // 保留
    PT_PHDR     // 程序头表
};

加载一个段

/* 将文件描述符fd指向的文件中,偏移为offset,大小为filesz的段加载到虚拟地址为vaddr的内存 */
static bool segment_load(int32_t fd, uint32_t offset, uint32_t filesz, uint32_t vaddr)
{
    uint32_t vaddr_first_page = vaddr & 0xfffff000;               // vaddr地址所在的页框
    uint32_t size_in_first_page = PG_SIZE - (vaddr & 0x00000fff); // 加载到内存后,文件在第一个页框中占用的字节大小
    uint32_t occupy_pages = 0;
    /* 若一个页框容不下该段 */
    if (filesz > size_in_first_page)
    {
        uint32_t left_size = filesz - size_in_first_page;
        occupy_pages = DIV_ROUND_UP(left_size, PG_SIZE) + 1; // 1是指vaddr_first_page
    }
    else
    {
        occupy_pages = 1;
    }

    /* 为进程分配内存 */
    uint32_t page_idx = 0;
    uint32_t vaddr_page = vaddr_first_page;
    while (page_idx < occupy_pages)
    {
        uint32_t *pde = pde_ptr(vaddr_page);
        uint32_t *pte = pte_ptr(vaddr_page);

        /* 如果pde不存在,或者pte不存在就分配内存.
       * pde的判断要在pte之前,否则pde若不存在会导致
       * 判断pte时缺页异常 */
        if (!(*pde & 0x00000001) || !(*pte & 0x00000001))
        {
            if (get_a_page(PF_USER, vaddr_page) == NULL)
            {
                return false;
            }
        } // 如果原进程的页表已经分配了,利用现有的物理页,直接覆盖进程体
        vaddr_page += PG_SIZE;
        page_idx++;
    }
    sys_lseek(fd, offset, SEEK_SET);
    sys_read(fd, (void *)vaddr, filesz);
    return true;
}
/* 从文件系统上加载用户程序pathname,成功则返回程序的起始地址,否则返回-1 */
static int32_t load(const char *pathname)
{
    int32_t ret = -1;
    struct Elf32_Ehdr elf_header;
    struct Elf32_Phdr prog_header;
    memset(&elf_header, 0, sizeof(struct Elf32_Ehdr));

    int32_t fd = sys_open(pathname, O_RDONLY);
    if (fd == -1)
    {
        return -1;
    }

    if (sys_read(fd, &elf_header, sizeof(struct Elf32_Ehdr)) != sizeof(struct Elf32_Ehdr))
    {
        ret = -1;
        goto done;
    }

    /* 校验elf头 */
    if (memcmp(elf_header.e_ident, "\177ELF\1\1\1", 7) || elf_header.e_type != 2 || elf_header.e_machine != 3 || elf_header.e_version != 1 || elf_header.e_phnum > 1024 || elf_header.e_phentsize != sizeof(struct Elf32_Phdr))
    {
        ret = -1;
        goto done;
    }

    Elf32_Off prog_header_offset = elf_header.e_phoff;
    Elf32_Half prog_header_size = elf_header.e_phentsize;

    /* 遍历所有程序头 */
    uint32_t prog_idx = 0;
    while (prog_idx < elf_header.e_phnum)
    {
        memset(&prog_header, 0, prog_header_size);

        /* 将文件的指针定位到程序头 */
        sys_lseek(fd, prog_header_offset, SEEK_SET);

        /* 只获取程序头 */
        if (sys_read(fd, &prog_header, prog_header_size) != prog_header_size)
        {
            ret = -1;
            goto done;
        }

        /* 如果是可加载段就调用segment_load加载到内存 */
        if (PT_LOAD == prog_header.p_type)
        {
            if (!segment_load(fd, prog_header.p_offset, prog_header.p_filesz, prog_header.p_vaddr))
            {
                ret = -1;
                goto done;
            }
        }

        /* 更新下一个程序头的偏移 */
        prog_header_offset += elf_header.e_phentsize;
        prog_idx++;
    }
    ret = elf_header.e_entry;
done:
    sys_close(fd);
    return ret;
}
2017-01-12 19:25:42 icener 阅读数 3482

ELF文件解析

近期正在进行Android的逆向。自己写出好代码是一个方面,而破解别人的代码则会给人另一种感受。因Java层的Android开发是如此不安全,越来越多的公司把重要的东西写入Native层,逻辑处理均由Native层完成,而Java层只负责进行结果显示。这样便要求对Native层的so文件进行逆向解析,而so文件是linux系统下的ELF(Excutable and Linkable File)文件。而要对so文件进行解析,必须先要了解ELF文件的格式。

网上有很多关于ELF文件格式的介绍文章,比较推荐的是北京大学操作系统实验室标准版 ELF文件格式分析。文章里面详细介绍了ELF文件的格式,但是只看文档是完全不明所以的,要从真正意义上了解ELF文件,还是要自己亲手做一下ELF文件解析才可以。

既然是自己做一个解析器,怎么才能知道自己解析的是正确的呢?这里我们就要用到一个非常有用的工具,即linux下的readelf。如果是Windows 10的用户,可以在 控制面版- 程序和功能- 启用或关闭Windows功能 中启用 适用于Linux的Windows子系统(beta) 来启用Windows 10的bash功能,启用bash后,可能readelf命令并未安装,可以通过sudo apt-get readelf进行安装。如果是其它版本的Windows,则要先装Cywin才能使用此命令。当然如果是Linux或者macOS则可以直接使用。

一、简略的介绍:

详细的readelf的命令可以通过man readelf命令查看命令手册
示例命令:比如我们从手机上面把/system/bin/app_process这个bin文件(linux下的bin文件是ELF格式文件)导出来后

1.Header信息:readelf -h app_process

readelf -h app_process

2.所有节区:readelf -S app_process

readelf -S app_process

3.字符串表:readelf -x 24 app_process或者readelf -x .shtratab app_process

readelf -x 24 app_process
所有节区的名称均是通过指定在这个字符串表的偏移量来得到的,通过解析节区表拿到节区名称在此表中的偏移量并查此表,拿到节区名称的字符串。与目标节区名称比较,相同则读出节区的sh_offset(偏移),sh_size(大小)就可以据此从文件中dump下来节区的所有内容。

二、文件的完整性较验

要知道dump下来的节区内容是否正确,比如dump出了.text节区的内容,怎么确定dump下来文件的正确性呢,用同样的命令readelf -x .text app_process
readelf -x .text app_process
中间红色部分便是.text节区的十六进制表示。当然,dump下来的文件是二进制的,通过一些软件(比如UltraEdit)也可以查看它的十六进制,看dump下来文件的正确性,则需要把文件与此命令得出来的结果进行比较,如此大的工作量当然不能靠人眼来做,这就需要把这个命令读取出来的文档中的代码部分提取出来。可以用如下的命令:
readelf -x .text app_process | awk '/0x/{print $2$3$4$5 > sodump.txt}'
此命令把readelf产生的结果做匹配,如果一行以0x开头,则把它的2、3、4、5列(也就是红框中的代码区)全输出到sodump.txt文件中。
生成的sodump.txt文件为文本文档,我们dump下来的是二进制文档,直接进行比较同样不可行,这就需要一个把文本文档转换为二进行文档或者能够逆向转换的工具,这里借鉴网上的代码,用Python实现:

#!/usr/bin/python

import os
import sys
from struct import *


# hex to bin
def hex_bin(hexfile, binfile):
    fin = open(hexfile)
    fout = open(binfile, 'wb')
    result = ''
    for hexstr in fin.readlines():
        hexstr = hexstr.strip()
        size = int(hexstr[1:3], 16)
        if int(hexstr[7:9], 16) != 0:
            continue
            # end if
        for h in range(0, size):
            b = int(hexstr[9 + h * 2:9 + h * 2 + 2], 16)
            result += pack('B', b)
        # end if
        fout.write(result)
        result = ''
    # end for
    fin.close()
    fout.close()


# bin to hex
def bin_hex(binfile, hexfile):
    fbin = open(binfile, 'rb')
    fhex = open(hexfile, 'w')
    while 1:
        result = ''
        bindata = fbin.read(0x10)
        if len(bindata) == 0:
            break

        for i in range(0, len(bindata)):
            byte = unpack('B', bindata[i])
            result += '%02X' % byte
        # end for
        result += '\n'
        fhex.write(result)
        if len(bindata) < 0x10:
            break
            # end if
    # end while
    fhex.flush()
    fbin.close()
    fhex.close()


# end for


if len(sys.argv) != 4 or (sys.argv[1] != '-h' and sys.argv[1] != '-b'):
    print 'usage:'
    print 'convert binary format to hexadecimal format: '
    print ' hexbin.py -h binfile hexfile'
    print 'convert hexadecimal format to binary format:'
    print ' hexbin.py -b hexfile binfile'
    exit(0)

if sys.argv[1] == '-h':
    bin_hex(sys.argv[2], sys.argv[3])
else:
    hex_bin(sys.argv[2], sys.argv[3])

此脚本可以把binary转换为hex(带参数-h),也可以把hex转换为binary(带参数-b)
把hex转换为binary以后,怎么进行比较呢,推荐使用UltraEdit一家的UltraCompare,这个可以用作二进制文件以及文本文件的比较。还可以高亮不相同的部分、比较时可以忽略大小写、忽略空行等。

三、实际操作解析ELF文件,读取其中某个节区的内容并dump(C++实现)

#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "elf.h"

//get section @params secName of library @params libPath's offset and length, the result is saved to @params result
int getOffsetAndLength(const char* libPath,const char* secName, unsigned int* result){
  File * file;
  file = fopen(libPath,"rb");
  if(file){
    Elf32_Ehdr * ehdr;//elf header pointer
    Elf32_Shdr * shdr;//section header pointer
    char * base;

    fseek(file,0,SEEK_END);//seek to the end of the file
    long fileSize = ftell(file);//get the size of the file

    base = (char*)malloc(fileSize);
    memset(base,0,fileSize);//set
    fseek(file,0,SEEK_SET);//relocate to the start of the libary file
    fread(base,1,fileSize,file);//copy the library file into memory
    if (base == (void*)-1){
      return -1;//copy fail
    }

    fclose(file);
    ehdr = (Elf32_Ehdr * ) base;//header
    int sectionSize = ehdr->e_shnum;//section number

    shdr = (Elf32_Shdr * ) (base+ehdr->e_shoff);//section table start
    int sectionHeaderOff = ehdr->e_shoff;

    Elf32_Shdr* nameSection = (Elf32_Shdr * ) (base + ehdr->e_shoff + ehdr->e_shentsize * ehdr->e_shstrndx);
    Elf32_Off nameSecOff = nameSection->sh_offset;//name section offset
    Elf32_Word nameSecLength = nameSection->sh_size;//name section length

    for (int i = 0; i < sectionSize; ++i) {
      Elf32_Shdr* section = (Elf32_Shdr * ) (base + ehdr->e_shoff + ehdr->e_shentsize * i);
      char * name = base+nameSecOff+section->sh_name;
      if (strstr(name,secName)){ //if we found the targeted section
          Elf32_Off offset = section->sh_offset;
          Elf32_Word size = section->sh_size;
          result[0] = offset; //save result
          result[1] = size;
          return 0;
      }
    }
  } else{
    return -2;
  }
  return 0;
}

int main(int argc, char const * argv[]) {
  unsigned int * result = new unsigned int[2];
  getOffsetAndLength("/system/bin/app_process",".name",result);
  File soFile = fopen("/system/bin/app_process","rb");
  File dumpFile = fopen("/tmp/dumpFile","w+b");
  if(soFile && dumpFile){
    char * fileRead = (char*)malloc(result[1]);
    memset(fileRead,0,result[1]);
    fseek(file,result[0],SEEK_SET);
    fread(fileRead,1,result[1],file);
    fwrite(fileRead,result[1],1,dumpFile);
    fflush(dumpFile);
    free(fileRead);
  }else{
    fclose(soFile);
    fclose(dumpFile);
    return -1;
  }
  fclose(soFile);
  fclose(dumpFile);
  return 0;
}
2019-05-10 19:09:39 QX_a11 阅读数 558

ELF文件、目标文件、可执行文件的关系

目标文件是源代码经过编译但未进行链接的那些中间文件,在linux中的.o文件,它跟可执行文件的内容与结构很相似,所以一般与可执行格式采用一种方式存储,在linux下,我们可以将他们统称ELF文件。ELF文件标准里面把系统中采用ELF格式的文件归为四类:

ELF文件类型 说明 实例

可重定位文件(Relocatable File)

这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也属于这一类

linux的.o

windows的.obj

可执行文件

(Executable File)

这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名

比如/bin/bash文件;window的.exe 

共享目标文件

(Shared Object File)

这种文件包含了代码和数据,可以在以下两种情况下使用。一种是连接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分运行。

linux的.so,如/lib/glibc-2.5.so

windows的DDL

核心转储文件

(Core Dump File)

当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件。 linux下的core dump

以以下代码编译出来的目标文件作为分析对象:

#include <stdio.h>
int global_init_var = 84;
int global_uninit_var;

void func1(int i)
{
	printf("%d\n",i);
}

int main(void)
{
	static int static_var = 85;
	static int static_var2;
	int a = 1;
	int b;
	func1(static_var + static_var2 + a + b);
	return a;
}

 使用objdump -h SimpleSection.o命令打印各个段的基本信息:

如上图显示共有代码段、数据段、BSS段、只读段、注释信息段和堆栈提示段。对应的信息size表示段的长度,VMA File off表示段所在的位置。每个段的第二行中的“CONTENTS”、“ALLOC”等表示段的各种属性;其中“CONTENTS”表示该段在文件中存在。

.text代码段:可以使用objdump 命令;-s 参数可以将所有段的内容以十六进制的方式打印出来。-d参数可以将所有包含指令段反汇编。我们将关于代码段的信息提取出来

Contents of section .text下就是.text段的内容,总共0x51,与使用-h参数输出的.text长度一致,最左边是偏移量,中间是十六进制的内容,最右边是.text段的ASCII码形式。对照下边的反汇编结果,可以很明显的看到fun1和mian函数,.text段的第一个字节"0x55"就是func1()函数的第一条“push %ebp”指令,而最后“0x50”正好是main函数的最后一条指令“ret”。

 

.data数据段:保存初始化了的全局变量和局部静态变量。global_init_var 和static int static_var。大小正好是8个字节。

 

.bss段:存放的是未初始化的全局变量和局部静态变量。也就是说global_uninit_var和static_var2应该存放在.bss段,准确来说.bss段位他们预留了位置,本应该是8个字节,而我们使用objdump -h命令显示的size是4个字节。事实上只有static_var2存放在了.bss段,而global_uninit_var只是一个未定义的“COMMON”符号;这和编译器有关,有的编译器将全局未初始化变量存放在.bss段,有的则不存放,只是预留一个未定义的全局变量符号,等到最终链接后再分配到.bss段。

 

.rodata段:存放的是只读数据,一般是程序里面的只读变量(如const修饰的变量)和字符串常量。在func1里面我们在调用“printf”的时候,用到了一个字符串常量“%d\n”,是4个字节与前面的长度符合。

上面通过一个实例大致了解了ELF文件的基本轮廓,总的来说就包括了指令和数据。下图是ELF文件的总体结构:

上面讲述了指令段和数据段,以下是ELF文件中其它的几个重要结构:

 

文件头(ELF Header)

 

ELF32_Ehdr和 ELF64_Ehdr结构体定义了ELF文件头的相关信息;这两个结构体的成员信息一致,只不过一个是32位版本的,另一个是64版本的。这里以ELF32_Ehdr为例,定义如下:

我们可以使用readelf查看SimpleSection的目标文件头:

e_ident(ELF魔数):该成员与readelf输出的Magic、Class、Data、Version、OS/ABI、ABI Version对应。 readelf输出的Magic的16字节正好对应e_ident这个成员;对于ELF文件前四个字节都必须相同,分别为0x7F、0x45、0x4c、0x46第一个字节对应ASCII控制,后面三个字节刚好是ELF这三个字母的ASCII,这4个字节称为ELF魔数,这种魔数用来确认文件的类型,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载。下一个字节对应Class用来表示ELF文件类的;第六个字节对应Data规定ELF文件是大端还是小端,第七个字节规定ELF文件的主版号,一般是1。对应的数据如下图所示

e_type(文件类型):对应 readelf输出的Type表示ELF文件类型。包括REL (可重定位文件)、EXEC(可执行文件)、DYN(共享文件);系统是根据这三个常量判断文件类型,而不是扩展名。

剩下的成员与readelf输出的参数,一一对应,此处不再赘述。综上文件头中描述整个文件的基本信息,以及段表的位置和大小、程序头的位置和大小。

 

段表(Section header Table)

段表是ELF文件中除了头文件以外最重要的结构,它描述了各个段的信息,比如段的段名、长度、在文件中的偏移、读写权限等等。段表是以一个元素是“ELF32_Shdr”结构体的数组。数组中的每一个元素对应一个段。ELF32_Shdr结构体的定义如下:

使用readelf -S SimpleSection.o查看目标文件的段表

根据上图,总共有11个元素,ELF段表文件的第一个元素时无效的,所以有效的段描述符有十个,也就是说有十个有效的段。readelf命令的输出与ELF32_Shdr结构体定义的成员一一对应,以下是对部分参数的详细解释:

sh_type(段的类型):对应readelf命令输出的Type。段名只是在链接和编译过程中有意义,但是不能代表段的类型。比如我们有可以将一个数据段命名为.text。段对应的类型如下图所示:

sh_flag(段的标志位):对应readelf命令输出的Flg;段的标志位是指该段在进程虚拟空间中的属性。比如是否可执行、可写。对应的值如下图所示:

sh_link、sh_info(段的链接信息):如果段的类型是与链接有关的,比如重定位、符号表。那么这两个成员的意义如下图所示,对于其他段这两个参数没有意义。

 

重定位表(Relocation Table)

重定位是链接器对目标文件中的某些部分进行地址的重新定义。这些信息都会记录在重定位表中 。可能会注意到在讲述段表的时候,使用readelf -S命令输出中就有一个“rel.text”段,该段就是重定位表,并且是作用域.text段,它对应类型是“SHT_REL”。回顾我们最开始写的SimpleSetion文件有对“printf”函数的调用,这个就是引用了绝对地址。它所对应的sh_link的值9也就是说该重定位表使用的符号表在段表中的下标是9;h_info的值为1,表示该重定位表作用于的段在段表的下标,即.text。对比上图段表的信息,与此处结果符合。

 

字符串表

ELF文件中用到了很多的字符串,比如段表、变量名。因为每个字符串的长度不一,所以将所有的字符串放到一张表中,如下图所示:

那么偏移与所对的字符串的关系:

在使用字符串时,只需给出对应的偏移值即可。保存字符串常见的有.strtab和.shstrtab,分别存放普通字符串和段名字符串。在头文件结构中有e_shstrndx这样一个参数,表示是段表字符串在段表中的下标。所以,只有分析文件头,就可以得到段表和段表字符串的位置,从而解析整个elf文件。

 

符号表(Symbol Table)

符号的作用是当多个不同目标链接时函数和变量之间的相互引用。对于链接而言,只关心全局符号的相互引用。局部符号、段名、行号等符号是次要的。ELF文件中符号也是在一个符号表中,是一个ELF_Sym结构;结构定义如下:

使用  readelf -s SimpleSection.o查看目标文件的符号表:

详解:

st_value(符号值):分为以下几种情况

  • 在目标文件中,如果是符号的定义并且该符号不是“COMMON”(未初始化的全局符号)类型,则st_value表示该符号在所在段中中的偏移,即符号所对应的函数或变量位于由st_shndx指定的段,偏移st_value的位置。
  • 在目标文件中。如果符号是“COMMON”类型的,则st_value表示该符号的对齐属性。
  • 在可执行文件中,st_value表示符号的虚拟地址。

st_info(符号类型和绑定信息):低四位表示类型;高28位表示符号绑定信息。具体数值如下图所示:

st_shndx(符号所在段):如果符号定义在本文件中,那么这个成员表示符号所在段在段表中的下标;还有几种特殊的情况,如下图所示:

 

2014-03-17 17:18:20 trochiluses 阅读数 1434
准备工作:C与汇编的混合编程ELF文件格式解析,这是《自己动手写操作系统》第五章的准备工作,主要讲解C与汇编混合编程的一些知识,并讲解ELF文件的相关知识,为有C语言写操作系统做准备。

1.关于系统调用sys_write

Linux的系统调用通过int 80h实现,用系统调用号来区分入口函数,它的作用类似与dos的中断。

      
        mov edx, len     ; 参数三:字符串长度
        mov ecx, msg     ; 参数二:要显示的字符串
        mov ebx, 1       ; 参数一:文件描述符(stdout) 
        mov eax, 4       ; 系统调用号(sys_write) 
        int 0x80         ; 调用内核功能

2.C与汇编混合编程


2.1 64位机器上目标文件不兼容问题: “could not read symbols"

    如果我们按照书上的命令:

nasm -f -o foo.o foo.asm
gcc -c o bar.o bar.c
ld -s -o foobar foo.o bar.o
    假如你是在64b的机器上,就会收到出错信息:
 “could not read symbols"
    这是因为,你的foo.o &&bar.o 分别是32b的目标文件和64位的目标文件,所以没法使用链接器进行链接。
解决方法:统一使用32b的elf文件,命令如下
gcc -m32 -c -o bar.o bar.c
ld -m elf_i386 -s -o foobar foo.o bar.o

2.2参数传递问题

    如果一个汇编中定义的函数想要被外部程序引用,需要用关键字导出——global;如果汇编中用到其他文件定义的函数,需要对函数进行声明,导入——extern;参数入栈顺序——后面的参数先入栈

3.ELF文件格式解析


3.1.为什么需要了解ELF文件格式

    因为linux可执行内核是采用ELF文件格式的,将ELF格式的内核加载到内存之后,如何将控制权转移到内核——我们如何知道程序开始执行的起始地点相对ELF头的位置?加载内核到内存之后,程序应该如何执行?程序执行的时候使用的都是虚拟地址——在ELF文件格式中提供的都是虚拟地址。那么如何将程序加载到对应的虚拟地址?ELF文件格式中存储了很多冗余信息,程序也包含很多个不同的段,那么如何将这些段分别加载到不同的位置?program header一共有多少个,其中分别在文件中的偏移、长度和加载到虚拟地址的位置信息?

    等等这些信息都需要我们了解ELF文件的格式。


3.2ELF文件格式概览


1)想要了解ELF文件的格式,我们需要熟悉一个命令,xxd,这是一个查看二进制文件的常用指令:
    它的常用参数意义如下:
-u:使用大写字母显示内容;
-a:autoskip自动跳过空数据;
-g num:groupby分组大小 1表示1字节
-c cols:每行显示的列数
-l num:一共显示多少单元
-s +/-num:seek,起始单元多远的地方开始显示

2)elf文件格式与后续的相关部分
    想要了解elf文件的具体信息,可以参考这里:elf文件格式完全解析
与后续内容相关的信息如下:

A、在ELF文件头中,包含程序的入口的虚拟地址;

B、每个段在ELF文件中的位置和大小信息,被加载到虚拟内存的位置和大小信息


4.调用原理

首先,我们知道,汇编调用C语言函数实际上是通过call指令来实现的。而call的对象是符号表中的一个符号,实际上,这个符号对应的是一个虚拟地址,也就是说符号表中的符号,是一个虚拟地址。那么call与jmp有什么区别呢

 对于jmp的区别就是:一个是段内调用,一个是段间调用

 对于call则有很大的不同,因为call的调用会对栈产生影响:
   (1)call的近调用不会改变使用的栈,但是栈的内容发生了变化:下一条指令被压入栈;如果有参数,参数被压入栈
   (2)call的远调用会改变使用的栈,由于使用的栈发生变化,因此对于参数有一个拷贝的过程。
      保存当前的ss和esp到被调用过程的栈中
      拷贝参数
      保存当前的cs和eip到被调用过程的栈中

总结:call与jmp的主要区别在于对待返回地址的不同;call远和近调用区别在于对于堆栈ss和sp的信息保存。



2016-12-14 23:05:57 afterlake 阅读数 607

《深入理解计算机系统》看到第七章链接那一块用linux的可执行格式elf为例讲解,因为没有看过elf的文件格式所以一开始看的很迷茫,啥~啥~这是啥~

找了一些elf的讲解看了一下,觉得需要自己分析一个简单的elf才能加深理解。

首先ELF的整个结构如下图:

本文以一个带有static 变量的hello.c为例

 

nt main()
{
        static int a = 255;
}

 

 

用gcc  -c hello.c编译生成hello.o

 

 

查看hello.o的二进制内容(hexdump hello.o)为:

 

0000000 457f 464c 0101 0001 0000 0000 0000 0000
0000010 0001 0003 0001 0000 0000 0000 0000 0000
0000020 00f8 0000 0000 0000 0034 0000 0000 0028
0000030 000b 0008 8955 5de5 00c3 0000 00ff 0000
0000040 4700 4343 203a 5528 7562 746e 2075 2e34
0000050 2e38 2d34 7532 7562 746e 3175 317e 2e34
0000060 3430 332e 2029 2e34 2e38 0034 0014 0000
0000070 0000 0000 7a01 0052 7c01 0108 0c1b 0404
0000080 0188 0000 001c 0000 001c 0000 0000 0000
0000090 0005 0000 4100 080e 0285 0d42 4105 0cc5
00000a0 0404 0000 2e00 7973 746d 6261 2e00 7473
00000b0 7472 6261 2e00 6873 7473 7472 6261 2e00
00000c0 6574 7478 2e00 6164 6174 2e00 7362 0073
00000d0 632e 6d6f 656d 746e 2e00 6f6e 6574 472e
00000e0 554e 732d 6174 6b63 2e00 6572 2e6c 6865
00000f0 665f 6172 656d 0000 0000 0000 0000 0000
0000100 0000 0000 0000 0000 0000 0000 0000 0000
*
0000120 001b 0000 0001 0000 0006 0000 0000 0000
0000130 0034 0000 0005 0000 0000 0000 0000 0000
0000140 0001 0000 0000 0000 0021 0000 0001 0000
0000150 0003 0000 0000 0000 003c 0000 0004 0000
0000160 0000 0000 0000 0000 0004 0000 0000 0000
0000170 0027 0000 0008 0000 0003 0000 0000 0000
0000180 0040 0000 0000 0000 0000 0000 0000 0000
0000190 0001 0000 0000 0000 002c 0000 0001 0000
00001a0 0030 0000 0000 0000 0040 0000 002c 0000
00001b0 0000 0000 0000 0000 0001 0000 0001 0000
00001c0 0035 0000 0001 0000 0000 0000 0000 0000
00001d0 006c 0000 0000 0000 0000 0000 0000 0000
00001e0 0001 0000 0000 0000 0049 0000 0001 0000
00001f0 0002 0000 0000 0000 006c 0000 0038 0000
0000200 0000 0000 0000 0000 0004 0000 0000 0000
0000210 0045 0000 0009 0000 0000 0000 0000 0000
0000220 0368 0000 0008 0000 0009 0000 0006 0000
0000230 0004 0000 0008 0000 0011 0000 0003 0000
0000240 0000 0000 0000 0000 00a4 0000 0053 0000
0000250 0000 0000 0000 0000 0001 0000 0000 0000
0000260 0001 0000 0002 0000 0000 0000 0000 0000
0000270 02b0 0000 00a0 0000 000a 0000 0009 0000
0000280 0004 0000 0010 0000 0009 0000 0003 0000
0000290 0000 0000 0000 0000 0350 0000 0015 0000
00002a0 0000 0000 0000 0000 0001 0000 0000 0000
00002b0 0000 0000 0000 0000 0000 0000 0000 0000
00002c0 0001 0000 0000 0000 0000 0000 0004 fff1
00002d0 0000 0000 0000 0000 0000 0000 0003 0001
00002e0 0000 0000 0000 0000 0000 0000 0003 0002
00002f0 0000 0000 0000 0000 0000 0000 0003 0003
0000300 0009 0000 0000 0000 0004 0000 0001 0002
0000310 0000 0000 0000 0000 0000 0000 0003 0005
0000320 0000 0000 0000 0000 0000 0000 0003 0006
0000330 0000 0000 0000 0000 0000 0000 0003 0004
0000340 0010 0000 0000 0000 0005 0000 0012 0001
0000350 6800 6c65 6f6c 632e 6100 312e 3733 0030
0000360 616d 6e69 0000 0000 0020 0000 0202 0000
0000370

因为我的机器是32的ub,所以用以下格式来解析文件头

 

 #define EI_NIDENT       16

  typedef struct {
      unsigned char       e_ident[EI_NIDENT];
      Elf32_Half          e_type;
      Elf32_Half          e_machine;
      Elf32_Word          e_version;
      Elf32_Addr          e_entry;
      Elf32_Off           e_phoff;
      Elf32_Off           e_shoff;
      Elf32_Word          e_flags;
      Elf32_Half          e_ehsize;
      Elf32_Half          e_phentsize;
      Elf32_Half          e_phnum;
      Elf32_Half          e_shentsize;
      Elf32_Half          e_shnum;
      Elf32_Half          e_shstrndx;
  } Elf32_Ehdr;


关于声明类型的定义如下:

 


  

  大小 对齐 用途
Elf32_Addr 4 4 无符号程序地址
Elf32_Half 2 2 无符号中等大小整数
Elf32_Off 4 4 无符号文件偏移
Elf32_Sword 4 4 有符号大整数
Elf32_Word 4 4 无符号大整数
unsigned char 1 1 无符号小整数

 

 

 

解析结果如下:

magic:     7f45 4c46 0101 0100 0000 0000 0000 0000

type:         0x01

machine:    0x03

version:    0x01

entry:         0

phoff:         0

shoff:         0xf8

flags:         0

ehsize:      0x34

phentsize: 0x0

phnum:      0

shentsize:  0x28

shnum:       0x0b

shstrndx:    0x08

 

 

同elfread -h hello.o得到的结果对比

 

 

 

ELF 头:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (可重定位文件)
  Machine:                           Intel 80386
  Version:                           0x1
  入口点地址:               0x0
  程序头起点:          0 (bytes into file)
  Start of section headers:          248 (bytes into file)
  标志:             0x0
  本头的大小:       52 (字节)
  程序头大小:       0 (字节)
  Number of program headers:         0
  节头大小:         40 (字节)
  节头数量:         11
  字符串表索引节头: 8


结果是对的,只不过elfread对各个值进一步解析出来了。

 

看完的header再看后面的节头信息

直接调用elfread -S hello.o查看:

 

节头:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 000005 00  AX  0   0  1
  [ 2] .data             PROGBITS        00000000 00003c 000004 00  WA  0   0  4
  [ 3] .bss              NOBITS          00000000 000040 000000 00  WA  0   0  1
  [ 4] .comment          PROGBITS        00000000 000040 00002c 01  MS  0   0  1
  [ 5] .note.GNU-stack   PROGBITS        00000000 00006c 000000 00      0   0  1
  [ 6] .eh_frame         PROGBITS        00000000 00006c 000038 00   A  0   0  4
  [ 7] .rel.eh_frame     REL             00000000 000368 000008 08      9   6  4
  [ 8] .shstrtab         STRTAB          00000000 0000a4 000053 00      0   0  1
  [ 9] .symtab           SYMTAB          00000000 0002b0 0000a0 10     10   9  4
  [10] .strtab           STRTAB          00000000 000350 000015 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

在这里我们以.text和.symtab以及.strtab为例

根据节头信息,我们找到0x34处长度为5的一段指令:

 

55 89 e5 5d c3


同objdump -d hello.o得到反汇编对比:

 

 

hello.o:     文件格式 elf32-i386


Disassembly of section .text:

00000000 <main>:
   0:    55                       push   %ebp
   1:    89 e5                    mov    %esp,%ebp
   3:    5d                       pop    %ebp
   4:    c3                       ret    

完全一致。

 

再看符号表symtab,在0x2b0处,长度为0xa0

符号表的表头结构定义如下:

 

typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char type:4,
              binding:4;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;


一个符号结构体占16个字节, 根据长度可知hello.o共有10个符号,用readelf -s hello.o查看符号表结果如下:

 

 

Symbol table '.symtab' contains 10 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1
     3: 00000000     0 SECTION LOCAL  DEFAULT    2
     4: 00000000     0 SECTION LOCAL  DEFAULT    3
     5: 00000000     4 OBJECT  LOCAL  DEFAULT    2 a.1370
     6: 00000000     0 SECTION LOCAL  DEFAULT    5
     7: 00000000     0 SECTION LOCAL  DEFAULT    6
     8: 00000000     0 SECTION LOCAL  DEFAULT    4
     9: 00000000     5 FUNC    GLOBAL DEFAULT    1 main

完全正确。

 

挑出最后一个符号来看,原数据为0x340处,长度为16字节:

 

0010 0000 0000 0000 0005 0000 0012 0001

根据结构体结构可以知道st_name 为0x10,即在字符表strtab的0x10偏移处。

 

字符表strtab在0x350处,长度为0x15:

 

6800 6c65 6f6c 632e 6100 312e 3733 0030 616d 6e69 00

其中"00"为字符串结尾, 所以我们可以读出最后一个字符恰好偏移为0x10:

 

 

6d 61 69 6e 00

前四个字节表示的ascii为"m", "a", "i",  "n", 即符号表的最后一个符号“main”。
 

新开公众号“码家村”,欢迎关注

 

 

ELF文件格式解析

阅读数 79

没有更多推荐了,返回首页