[linux内核] 5.ELF文件、从源文件到可执行文件过程

学习内容来自庖丁解牛,仅作为个人学习研究用途,如作者认为侵权请联系第一时间删除。

1.ELF文件格式

1.1 相关知识点

ELF(Executable and Linkable Format)可执行的和可链接的格式,是一个目标文件格式的标准。


“目标文件”,是指编译器生成的文件,一般也叫作 ABI。“目标” 指目标平台。
目标文件和目标平台是二进制兼容的。


ELF文件从磁盘装入内存时将磁盘中的几个节进行组合映射为内存中的段。


ELF文件有3三种类型
(1)可重定位文件。 是由汇编后产生的 .o 文件,很多 .o 文件最后可以链接为一个文件。
(2)可执行文件。 由多个可重定位文件经过符号解析和重定位后生成的文件。
(3)共享目标文件(动态库)。 linux下为 .so 后缀文件。


1.2 ELF文件格式及定义

以下以32位ELF文件为例

1.2.1 ELF header定义

#define EI_NIDENT (16) 
typedef struct 
{ 
    unsigned char e_ident[EI_NIDENT]; /* 前四个字节位魔数用来标识ELF->'0x7f','E','L','F'。*/ 
    Elf32_Half e_type; /*指明目标文件的类型是可重定位、可执行、共享库、其他*/ 
    Elf32_Half e_machine; /* 指明可以在哪种机器结构中运行 */ 
    Elf32_Word e_version; /* 指明文件版本信息 */ 
    Elf32_Addr e_entry; /* 指明系统运行该程序时将控制权转交到的虚拟地址的值,如果没有则为零 */ 
    Elf32_Off e_phoff; /* 程序头表在文件中的字节(Byte)偏移offset,如果没有则该值为零 */ 
    Elf32_Off e_shoff; /* 节头表在文件中的字节偏移,如果没有则该值为零 */ 
    Elf32_Word e_flags; /* 有关处理器的信息 */ 
    Elf32_Half e_ehsize; /* elf header的大小*/ 
    Elf32_Half e_phentsize; /* 程序头表中一个表项的大小 */ 
    Elf32_Half e_phnum; /* 程序头表中元素的个数,即表项的个数 */ 
    Elf32_Half e_shentsize; /* 节头表每一个表项的大小,与e_phentsize类似 */ 
    Elf32_Half e_shnum; /* 节头表中元素的个数,即表项的个数 */ 
    Elf32_Half e_shstrndx; /* 指明string name table在section header table中的index */ 
} Elf32_Ehdr; 

1.2.2 节定义与段

节是ELF文件种具有相同特征的最小可处理信息单位,不同节描述了目标文件种不同类型的信息及特征。
段只是对节区的重新组合,将连续的多个节区描述为一段连续区域,对应到一块连续的内存空间。

各种节描述
.text:目标代码部分
.rodata:只读数据
.data:已经初始化的全局变量
.bss:未初始化的全局变量。不占用实际的磁盘空间,仅仅是一个占位符。
.symtab:符号表(symbol table)
.rel.text:.text节相关可重定位信息
.rel.data:.data节相关可重定位信息
.debug:调试符号表
.line: C源程序的行号和.text节种机器指令之间的映射
.strtab:字符串表

1.2.3 节头表定义

节头表由若干个表项组成,每个表项描述相应的一个节的相关信息。
32位下节头表表项数据结构

typedef struct
{
  Elf32_Word	sh_name;		/* Section name (string tbl index) */
  Elf32_Word	sh_type;		/* Section type */
  Elf32_Word	sh_flags;		/* Section flags 该节的访问属性*/
  Elf32_Addr	sh_addr;		/* Section virtual addr at execution */
  Elf32_Off	    sh_offset;		/* Section file offset */
  Elf32_Word	sh_size;		/* Section size in bytes */
  Elf32_Word	sh_link;		/* Link to another section */
  Elf32_Word	sh_info;		/* Additional section information */
  Elf32_Word	sh_addralign;	/* Section alignment */
  Elf32_Word	sh_entsize;		/* Entry size if section holds table */
} Elf32_Shdr;

1.2.4 程序头表(段头表)定义

typedef struct
{
  Elf32_Word    p_type;         /* Segment type */
  Elf32_Off 	p_offset;       /* Segment file offset */
  Elf32_Addr    p_vaddr;        /* Segment virtual address */
  Elf32_Addr    p_paddr;        /* Segment physical address 通常是无效的*/
  Elf32_Word    p_filesz;       /* Segment size in file */
  Elf32_Word    p_memsz;        /* Segment size in memory */
  Elf32_Word    p_flags;        /* Segment flags */
  Elf32_Word    p_align;        /* Segment alignment */
} Elf32_Phdr;

1.3 ELF文件两种不同的试图

ELF文件有两种不同的试图:可重定位目标文件对应的链接视图、可执行目标文件对应的执行试图。

节头表包含文件各节的说明信息,每个节在该表种都有一个对应的项,每一项都指定了节名和节大小的信息。用于链接的目标文件必须具有节头表,例如可重定位目标文件就必须有节头表。
程序头表用来只是系统如何创建进程的存储器影像。用于创建进程存储影像的可执行文件共享库文件必须有程序头表。

左图为ELF两种视图,右图为对应的具体内容


2. 从源码到可执行文件步骤

程序从源代码到可执行文件的步骤:预处理、编译、汇编、链接。


预处理 gcc -E hello.c -o hello.i
预处理时编译器完成如下工作
(1)删除所有的注释
(2)展开所有的宏定义。
(3)处理所有的条件预编译指令。
(4)处理“#include”预编译指令,将被包含的文件插入该预编译指令的位置,这一过程是递归进行的。
(5)添加行号和文件名标识。


编译 gcc -S hello.i -o hello.s -m32
编译时,gcc 首先要检查代码的规范性、是否有语法错误等。检查无误后,gcc 把代码翻译成汇编语言。


汇编 gcc -c hello.s -o hello.o -m32
汇编后形成的.o格式的文件是ELF格式文件,此时就是可重定位的二进制文件。


链接 gcc hello.o -o hello -m32
链接是将各个文件代码和数据收集起来并组合成一个单一文件的过程,这个文件可被加载到内存中并执行。


3. 链接

链接从过程上将分两步:符号解析、重定位。
按照链接时机上分三种:静态链接、装载时动态链接、运行时动态链接。


3.1 符号解析

符号有下面三种类型:(1)在文件m中定义并被其他文件引用的全局符号。(2)由其他文件定义并被文件m引用的外部符号。(3)在文件m中定义并在文件m中引用的本地符号。

符号表就是包含了在程序文件中被定义的所有符号相关信息的表。

本地符号没有强弱之分,全局符号有强弱之分
强符号是指函数名和已初始化的全局变量名,弱符号是指未初始化的全局变量名。
强弱符号在符号解析时出现重名符号是有重要左右,在此不做解释。

符号解析
符号解析目的是将每个模块中引用的符号与某个目标模块中的定义符号建立关联。符号解析也称符号绑定。

符号解析的过程用到三个集合
E:将被合并以组成可执行文件的所有目标文件集合
U:当前所有未解析的引用符号的集合
D:当前所有定义符号的集合

符号解析过程示例

myproc1.c
#include <stdio.h>
void myfunc1(){
	...
}

myproc2.c
#include <stdio.h>
void myfunc2(){
	...
}

先生成可重定位文件,再用ar工具生成静态库
gcc -c myproc1.c
gcc -c myproc2.c
ar rcs mylib.a myproc1.o myproc2.o

main.c
void myfunc1();
int main(){
	myfunc1();
	return 0;
}

gcc -c main.c
gcc -static -o myproc main.o ./mylib.a

① 开始E、U、D为空,首先扫描main.o,把它加入E, 同时把myfun1加入U,main加入D。
② 接着扫描到 mylib.a,将U中所有符号(本例中为myfunc1)与 mylib.a中所有目标模块(myproc1.o和myproc2.o )依次匹配,发现在myproc1.o中定义了myfunc1 ,故myproc1.o加入E,myfunc1从U转移到D。在 myproc1.o中发现还有未解析符号printf,将其加到 U。
③ 不断在mylib.a的各模块上进行迭代以匹配U中的符号,直到U、D都不再变化。
④ 此时U中只有一个未解析符号printf,而D中有main和myfunc1。因为模块 myproc2.o没有被加入E中,因而它被丢弃。
⑤ 接着,扫描默认的库文件libc.a,发现其目标模块printf.o定义了 printf,于是printf也从U移到D,并将 printf.o加入E,同时把它定义的所有符号 加入D,而所有未解析符号加入U。
处理完libc.a时,U一定是空的。


3.2 重定位

重定位是把可重定位文件中的符号的相对地址修改为多个文件链接后正确的引用地址的过程。

可重定位表中的每一条记录对应一个需要重定位的符号。

符号表记录了目标文件中所有的全局函数及其地址;重定位表中记录了所有调用这些函数的代码位置。在链接时,这两大类数据都需要逐一修改为正确的值。


补充:可执行文件的存储器映像
linux中启动一个可执行目标文件时,会调用execve系统调用函数启动加载器,
加载器根据可执行目标文件中的程序头表信息,将可执行目标文件中相关节的内容
与虚拟地址空间中的只读代码段和可读写数据段通过页表建立链接,然后启动可执行目标文件
中的第一条指令执行。
根据ABI规范,一个系统中的每个可执行文件都采用统一的存储映像地址。比如每个可执行目标
文件的只读代码都映射到从0x8048000开始的一块连续区域。。。

所以经过汇编后生成的可重定位文件都是从0相对地址开始标号,经过链接后采用ABI规范定位
存储映像地址(虚拟地址),等到被执行时在由其他机制具体到物理地址。


3.3 静态链接、装载时动态链接、运行时动态链接

补充:静态库文件.a,动态库文件.so
通过ls -l可以看到静态链接生成的可执行文件比动态链接生成的可执行文件要大得多。


静态链接
在编译链接时直接将需要的执行代码复制到最终可执行文件中。

编辑lab.c

#include <stdio.h>
void print(){
        printf("hello");
        return;
}

制作静态库

gcc -c lab.c
ar rcs lab.a lab.o

编辑test.c

void print();
int main(){
        print();
        return 0;
}

编译

gcc -c test.c
gcc -static -o test test.o ./lab.a

装载时动态链接

编辑lab.c,生成动态库so文件

lab.c

#include <stdio.h>
void print(){
        printf("hello world");
}
gcc -shared -fPIC -o lab.so lab.c
-fPIC告诉编译器生成与位置无关的代码

编辑测试程序

test.c

void print();
int main(){
        print();
}

编译的过程中引入动态库文件

gcc -o test test.c ./lab.so

运行时动态链接

借助刚才的so文件。
编辑test.c

#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
        void *handle = dlopen("./lab.so", RTLD_LAZY);
        if (!handle){
                printf("%s\n", dlerror());
                exit(1);
        }
        void(*myfunc)()=dlsym(handle, "print");
        if (!myfunc){
                printf("%s\n", dlerror());
                exit(1);
        }
        myfunc();
        if (dlclose(handle) < 0){
                printf("%s\n", dlerror());
                exit(1);
        }
        return 0;
}

编译文件

gcc -o test test.c -ldl
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

H4ppyD0g

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

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

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

打赏作者

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

抵扣说明:

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

余额充值