文章目录
为什么要有elf格式的文件?
可执行程序要解决的问题
- 一句话回答,要解决怎么让被调用者把自己加载到内存,并执行自己代码段的问题。
如果没有源码中的伪指令指示,汇编器把第一条指令码的地址设置成0,之后的代码和数据以此为准进行计算。 - BIOS引导操作系统时,首先加载引导盘的前512字节(MBR)到和MBR约定的内存地址0x7c00,然后pc指向0x7c00开始执行代码。汇编器在编译MBR的程序时,按照约定把第一条指令码的地址设置成0x7c00,之后的代码和数据地址的计算,都是指令码在文件中的实际偏移加上0x7c00,以此得到。这种思路可以解决运行可执行程序的运行问题,但是有个缺点,调用者必须事先知道被调用程序期望被加载到的内存地址。如果所有程序都用这种解决方法,那么需要额外提供一张各个程序运行地址的表格,每调用一个程序,就去这个表格里面查找其期望加载到的内存地址和其它信息。无疑,这个方法可以解决问题,但是需要多维护一张描述程序的元数据表格。
- 为了不维护这张表,有一种方法是把元数据写到被调用程序的头部,调用者和被调用者约定读取这个头部信息的规则,告诉调用者通过怎么样的方式可以找到我期望被加载的内存地址。elf格式的二进制文件就是这样做的。
没有elf格式文件的世界
- 重新认识Linux进程地址空间中通过bios加载软盘固件,解释了没有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就是这个作用。
- ELF header
可重定向文件元数据分析实例
下面是mbr.o的elf头部元数据分析,对照elf header 和section header,可以理解其含义
- elf header
- section header
- program header
- elf 头部元数据
- 日常应用中不可能通过查看二进制数据分析元数据,elf提供了readelf工具解析这个头部。可以对照验证上面的分析。
- 读取elf header
readelf -h mbr.o
- 读取program header table
readelf -l mbr.o
,由于是用于重定向的对象文件,这部分数据为空
- 读取section header table
readelf -S mbr.o
- 读取elf header
一个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格式,解释各字段含义:
- Type(p_type): 表示program header一条entry描述的segment类型,可以是以下类型,这里是PT_LOAD类型:
- Offset(p_offset): 表示entry描述的segment与文件起始位置的偏移,这里是0x100(256byte)
- VirtAddr(p_vaddr):
- PhysAddr(p_paddr):
- FileSz(p_filesz):
- 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