ELF格式二进制程序的产生,作用,和结构

为什么要有elf格式的文件?

可执行程序要解决的问题

  • 一句话回答,要解决怎么让被调用者把自己加载到内存,并执行自己代码段的问题。
    如果没有源码中的伪指令指示,汇编器把第一条指令码的地址设置成0,之后的代码和数据以此为准进行计算。
  • BIOS引导操作系统时,首先加载引导盘的前512字节(MBR)到和MBR约定的内存地址0x7c00,然后pc指向0x7c00开始执行代码。汇编器在编译MBR的程序时,按照约定把第一条指令码的地址设置成0x7c00,之后的代码和数据地址的计算,都是指令码在文件中的实际偏移加上0x7c00,以此得到。这种思路可以解决运行可执行程序的运行问题,但是有个缺点,调用者必须事先知道被调用程序期望被加载到的内存地址。如果所有程序都用这种解决方法,那么需要额外提供一张各个程序运行地址的表格,每调用一个程序,就去这个表格里面查找其期望加载到的内存地址和其它信息。无疑,这个方法可以解决问题,但是需要多维护一张描述程序的元数据表格。
  • 为了不维护这张表,有一种方法是把元数据写到被调用程序的头部,调用者和被调用者约定读取这个头部信息的规则,告诉调用者通过怎么样的方式可以找到我期望被加载的内存地址。elf格式的二进制文件就是这样做的。

没有elf格式文件的世界

总结

  • elf二进制程序比裸的二进制程序或者固件,多出了程序被加载、执行时需要的元数据。这些数据都放在文件的头部,读取这些程序,调用者和被调用者可以约定各种各样的规则,elf程序的规则就是其中一种。

编译源码生成elf格式文件,到底对文件做了什么?

一个裸的二进制程序长什么样?

  • 源代码mbr.asm,这段程序没有什么具体功能,就是在屏幕中打印一段"Hello, OS World"
		org 	07c00h					; 告诉编译器程序加载到7c00处
        jmp     07c0h:DispStrOff

code:
times   10      db      0
        ;       never reach here

DispStrOff      equ     $       -       $$
DispStr:
		mov 	edx, code				; 取code标号的地址给edx,测试汇编器计算地址
										; 如果没有第一条org指令,nasm计算得到的code标号地址=0+jmp指令长度,
										; 如果有第一条org指令,nasm计算得到的code标号地址=07c00h+jmp指令长度
        mov     ax, BootMessage
        mov     bp, ax                  ; ES:BP = 串地址
        mov     cx, 16                  ; CX = 串长度
        mov     ax, 01301h              ; AH = 13,  AL = 01h
        mov     bx, 000ch               ; 页号为0(BH = 0) 黑底红字(BL = 0Ch,高亮)
        mov     dl, 0
        int     10h                     ; 10h 号中断
        jmp $

BootMessage:            db      "Hello, OS world!"
  • 利用汇编器将其汇编成二进制程序
    nasm -o mbr.bin mbr.asm

  • 反汇编查看其内容ndisasm -o 0x7c00 mbr.bin
    在这里插入图片描述

  • xxd查看文件实际内容 xxd -u -a -g 1 -c 16 mbr.bin
    在这里插入图片描述

  • 对比反汇编代码和文件的数据,两个是一样的。裸的二进制程序包含的仅仅是二进制格式的代码指令,这个程序能跑起来吗?可以的,参见重新认识intel段机制寻址的实验。

一个ELF格式的对象文件长什么样?执行nasm -f的时候我们在做什么?

  • 对上面的源码稍加改动
[SECTION .s16]
[BITS 16]
        global _start
_start:
        jmp 07c0h:OffDispStr

OffDispStr      equ     $ - $$
DispStr:
        mov ax, cs
        mov ds, ax
        mov es, ax
        mov ss, ax
        mov sp, 0100h

        mov ax, BootMessage
        mov bp, ax
        mov cx, 16
        mov ax, 01301h
        mov bx, 000ch
        mov dl, 0
        int 10h
        jmp $

BootMessage:            db "Hello, OS world!"
times   510-($-$$)      db      0
dw      0xaa55
......
  • 使用nasm -f elf mbr.asm -o mbr.o将源码编译成elf格式的可重定向文件,反汇编查看其代码段内容objdump -D mbr.o
    在这里插入图片描述
    和实际的汇编代码有点儿不一样,没关系,这是由于[section .16]告诉编译器把汇编代码编译成16bit 寄存器模式的二进制码,而objdump -D 是按照32bit 寄存器模式来反汇编二进制码,所以不大一样。我不知道怎么让objdump 按照16bit反汇编二进制程序,但nasm可以设置,所以下面的方法,可以正确反汇编出elf文件格式中的代码段

    • 通过readelf -S mbr.o找到elf文件中.s16段在mbr.o中的位置和长度,off=0x130=304,size=0x200=512
      在这里插入图片描述
    • 将.s16段的数据拷贝到文件dd if=mbr.o ibs=1 skip=304 of=mbr.s16 seek=0 count=512,然后反汇编ndisasm -b 16 mbr.s16 | head -n 20,得到反汇编出来的代码
      在这里插入图片描述
    • xdd查看文件实际内容,xxd -u -a -g 1 -c 16 mbr.o,红色方框内的数据是前一步反汇编的代码,也是objdump反汇编代码所用的数据。可以看到,ELF文件除了包含.s16这段代码汇编出的数据,还有其它的数据。这些其它的数据,就是描述这段程序的元数据。查看ELF的规范,可以进一步读懂这些元数据表达的意思。
      在这里插入图片描述

ELF规范

  • ELF文件格式
    下图分别是可重定向目标文件和可执行文件的格式
    在这里插入图片描述
    elf文件元数据包括4个部分:
    • ELF header
      • 为达到程序可以被执行的目的,可以设计各种不同的元数据格式规范,elf只是其中一种格式,为了区分其它格式的文件,elf header的第一个字段时magic,固定不变。
      • elf文件有四个类型,主要用作四个用途:一作可执行文件(executable file),做可执行程序直接加载到内存运行、二作可重定向文件(relocatable file),做可重定向程序和别的重定向程序一起组成可执行程序、三作共享库程序(shared object file),用来链接成重定向程序或者动态链接到内存中的其它进程中、四作Core file,用于调试。elf header中设计了e_type字段用于区分这些不同用途的程序。
      • elf设计目标是可以在不同架构下运行。elf header中设计了e_machine字段用于指明这个二进制程序在哪个架构下运行。
      • elf文件元数据除了header还有其它部分。elf header还作为路标,提供找到其它元数据的地址。
    • Program Header Table
      • elf程序最终目的是被加载到内存,告诉被调用者怎样把自己加载到内存,加载到什么地址,拷贝多长的数据,这些是elf存在的意义,program header table就提供这个信息,它的每个条目描述了一个segment。
      • 这类信息对系统加载一个可执行程序中有用,对系统链接一个可重定向文件没有,因此这部分内容可能为空,当header中的e_type是ET_EXEC时,文件是个可执行的二进制程序,这段信息存在。当header中的e_type时ET_REL时,文件是个用于重定向的对象文件,这段信息不需要,可以为空。
    • Sections
      • ELF全称Executable and Linkable Format,除了为被调用者提供加载执行程序的信息,还有一个特点是提供可链接的信息,Section的设计就是为链接器提供这些信息。
      • 手写的汇编源代码可以有自己定义的section,分别用来存放代码或者数据。但高级语言编译后的汇编程序,代码和数据混杂在一起,需要统一整理,可以将可执行的代码放在一个section,未初始化的数据放在一个section,常量放在一个section,最后生成可重定位的对象文件。C源代码中也可以通过__attribute__((section("section_name")))指定变量或者函数放到哪一个section中。链接程序在处理这些文件时,就可以把section当做基本操作单位,将不同对象文件的同类型的section放在一起,组成segment,并对segment进行地址绑定,告诉调用者这段segment期望加载到的内存地址。
      • 下图是section和segment的关系
        在这里插入图片描述
    • Section Header Table
      • section可以有不同的作用,用于放代码的section,用于放数据的section,一个section有多长,它在elf文件的什么位置,这些都需要元数据去描述,Section Header Table就是这个作用。

可重定向文件元数据分析实例

下面是mbr.o的elf头部元数据分析,对照elf header 和section header,可以理解其含义

  • elf header
    在这里插入图片描述
  • section header
    在这里插入图片描述
  • program header
    在这里插入图片描述
  • elf 头部元数据
    在这里插入图片描述
  • 日常应用中不可能通过查看二进制数据分析元数据,elf提供了readelf工具解析这个头部。可以对照验证上面的分析。
    • 读取elf headerreadelf -h mbr.o
      在这里插入图片描述
    • 读取program header tablereadelf -l mbr.o,由于是用于重定向的对象文件,这部分数据为空
      在这里插入图片描述
    • 读取section header tablereadelf -S mbr.o
      在这里插入图片描述

一个ELF格式的二进制程序长什么样?执行ld的时候我们在做什么?

  • 执行ld -o mbr mbr.o -Tmbr.ld生成可执行的elf文件,mbr.ld内容。
    这段代码的意思只有一个:把所有输入的对象文件(例子中只有一个mbr.o)中的.s16 section集合起来,统一放到.boot section中,将.boot section的期望内存加载地址设置成0。并设置elf文件的入口点为_start标号处的指令。
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)
 
SECTIONS
{
    . = 0;
    .boot : {*(.s16)}
    . = ASSERT(. <= 512, "Boot too big!");
    /* For Load Memory Address test
     * . = 0x10;
     * .boot : {*(.s16)}
     **/
} 
  • elf文件比对象文件多出了什么?有什么不同?
    elf因为要提供程序加载信息,所以肯定多出了programe header table,elf文件是对1个及以上对象文件section的重新安排并设置加载地址,所以输出文件中的section都由加载地址(LMA),例子中LMA=0。
    在这里插入图片描述在这里插入图片描述
  • 参照program header格式,解释各字段含义:
  1. Type(p_type): 表示program header一条entry描述的segment类型,可以是以下类型,这里是PT_LOAD类型:
    在这里插入图片描述
  2. Offset(p_offset): 表示entry描述的segment与文件起始位置的偏移,这里是0x100(256byte)
  3. VirtAddr(p_vaddr):
  4. PhysAddr(p_paddr):
  5. FileSz(p_filesz):
  6. MemSz(p_memsz):
  • TODO

  • 改变LMA 意味着什么?
    LMA是elf期望调用者加载自己到内存的地址,如果调用者不按这个期望值加载会有什么后果?换句话说,改变LMA,elf程序会有什么变化?

    • 对ld链接脚本做如下改动,同时在源代码中添加一句获取符号的mov指令
1)mbr.ld
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)
 
SECTIONS
{
    . = 0x10;
    .boot : {*(.s16)}
}

2)mbr.asm
[SECTION .s16]
[BITS 16]
        global _start
_start:
        jmp 07c0h:OffDispStr

OffDispStr      equ     $ - $$
DispStr:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0100h

    mov eax, DispStr	; for test
    mov ax, BootMessage
  • 重新编译生成mbr,查看其section内容readelf -S mbr,addr 地址变了,.boot section在文件中的偏移也变了,seciont大小没变
    在这里插入图片描述
  • 拷贝.boot section的数据dd if=mbr ibs=1 skip=4112 of=mbr.s16 seek=0 count=512并反汇编。如果程序第一条指令地址按照0来算,DispStr标号的地址应该是jmp指令的下一条指令地址0x5,但mov eax, DispStr语句被汇编成了mov eax,0x15,可见,ld会以LMA为依据,重新计算源代码中的标号值。如果源代码中有位置相关的语句,那么调用者就必须按照elf给定的LMA加载这个程序,否则程序会执行错误,反之,如果源代码中所有语句都位置无关,那么调用者就可以忽略elf中的LMA地址,随便加载程序到某段内存执行。
    在这里插入图片描述
    实验源码见 my github gdb调试elf程序

kernel相关的elf程序

  • vmlinux.o,可重定向文件,是内核镜像的中间文件
    file查看其文件类型是ELF格式文件
[root@compile linux-3.10.0-957.10.1.el7.centos.plus.x86_64]# file vmlinux.o 
vmlinux.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), too many section header sections (32888)

readelf查看其具体类型

1)读取elf header查看文件总体信息
[root@compile linux-3.10.0-957.10.1.el7.centos.plus.x86_64]# readelf -h vmlinux.o 
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64	
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)		// 可重定向文件
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)		// 没有提供程序加载的虚拟地址
  Start of section headers:          502301608 (bytes into file)// section表的文件偏移地址
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)					// 没有程序头,不是可执行程序
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         32888
  Section header string table index: 32887
  
2)读取program header,没有信息,和header描述相同
[root@compile linux-3.10.0-957.10.1.el7.centos.plus.x86_64]# readelf -l vmlinux.o
There are no program headers in this file.

3)读取section header,很多,每个section根据需要,可以单独摘出来,同其他重定向文件一起链接成可执行的elf文件
[root@compile linux-3.10.0-957.10.1.el7.centos.plus.x86_64]# readelf -S vmlinux.o | head -n 10
There are 32888 section headers, starting at offset 0x1df083a8:
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00001000
       0000000000761ebc  0000000000000000  AX       0     0     4096
  [ 2] .rela.text        RELA             0000000000000000  0fa73050
  • vmlinux,ELF的可执行文件
1)查看文件类型,是ELF格式文件
[root@compile linux-3.10.0-957.10.1.el7.centos.plus.x86_64]# file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=4f4ba2c3dfba02ae45401b641594c9df35b4edca, not stripped

2)细看elf文件,是可执行文件,可以供gdb调试。但bios启动时识别不了elf格式,它只能识别固件,所以不是bios可引导的文件。
[root@compile linux-3.10.0-957.10.1.el7.centos.plus.x86_64]# readelf -h vmlinux
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1000000
  Start of program headers:          64 (bytes into file)
  Start of section headers:          439564240 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         5
  Size of section headers:           64 (bytes)
  Number of section headers:         82
  Section header string table index: 81
  • vmlinuz
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

享乐主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值