当执行一个elf二进制程序的时候,系统到底做了什么?

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

1)可执行程序要解决的问题

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

2)没有elf格式文件的世界

3)小结

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

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

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

  • 源代码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段机制寻址的实验。

2)一个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的规范,可以进一步读懂这些元数据表达的意思。
      在这里插入图片描述

3)ELF规范

对照elf文件格式的规范手册,分析这个数据

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

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

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

  • elf header
    在这里插入图片描述

  • section header
    在这里插入图片描述

  • elf 头部元数据
    在这里插入图片描述

  • 日常应用中不可能通过查看二进制数据分析元数据,elf提供了readelf工具解析这个头部。可以对照验证上面的分析。

    • 读取elf headerreadelf -h mbr.o
      在这里插入图片描述
    • 读取program header tablereadelf -l mbr.o,由于是用于重定向的对象文件,这部分数据为空
      在这里插入图片描述
    • 读取section header tablereadelf -S mbr.o
      在这里插入图片描述

5)一个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。
    在这里插入图片描述
  • 改变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程序

执行一个elf程序,系统做了什么?

/* TODO */

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章