GCC编译器原理(二)------编译原理一:ELF文件(1)

发布时间:2024-08-22  

二、ELF 文件介绍


2.1 可执行文件格式综述

相对于其它文件类型,可执行文件可能是一个操作系统中最重要的文件类型,因为它们是完成操作的真正执行者。可执行文件的大小、运行速度、资源占用情况以及可扩展性、可移植性等与文件格式的定义和文件加载过程紧密相关。研究可执行文件的格式对编写高性能程序和一些黑客技术的运用都是非常有意义的。


可执行链接格式 ( Executable and Linking Format)最初是由 UNIX 系统实验室 ( UNIX System Laboratories, USL)开发并发布的, 作为应用程序二进制接口 ( Application Binary Interface, ABI)的一部分。


不管何种可执行文件格式,一些基本的要素是必须的,显而易见的,文件中应包含代码和数据。因为文件可能引用外部文件定义的符号(变量和函数),因此重定位信息和符号信息也是需要的。一些辅助信息是可选的,如调试信息、硬件信息等。基本上任意一种可执行文件格式都是按区间保存上述信息,称为段(Segment)或节(Section)。不同的文件格式中段和节的含义可能有细微区别,但根据上下文关系可以很清楚的理解,这不是关键问题。最后,可执行文件通常都有一个文件头部以描述本文件的总体结构。


相对可执行文件有三个重要的概念:编译(compile)、连接(link,也可称为链接、联接)、加载(load)。源程序文件被编译成目标文件,多个目标文件被连接成一个最终的可执行文件,可执行文件被加载到内存中运行。


ELF全称Executable and Linkable Format,可执行连接格式,ELF 格式的文件用于存储 Linux 程序。


2.2 LINUX平台下ELF文件加载过程的简单描述

内核首先读 ELF 文件的头部,然后根据头部的数据指示分别读入各种数据结构,找到标记为可加载(loadable)的段,并调用函数 mmap() 把段内容加载到内存中。在加载之前,内核把段的标记直接传递给 mmap() ,段的标记指示该段在内存中是否可读、可写,可执行。显然,文本段是只读可执行,而数据段是可读可写。这种方式是利用了现代操作系统和处理器对内存的保护功能。

内核分析出 ELF 文件标记为 PT_INTERP 的段中所对应的动态连接器名称,并加载动态连接器。现代 LINUX 系统的动态连接器通常是 /lib/ld-linux.so.2。

内核在新进程的堆栈中设置一些标记-值对,以指示动态连接器的相关操作。

内核把控制传递给动态连接器。

动态连接器检查程序对外部文件(共享库)的依赖性,并在需要时对其进行加载。

动态连接器对程序的外部引用进行重定位,通俗的讲,就是告诉程序其引用的外部变量/函数的地址,此地址位于共享库被加载在内存的区间内。动态连接还有一个延迟(Lazy)定位的特性,即只在 '真正' 需要引用符号时才重定位,这对提高程序运行效率有极大帮助。

动态连接器执行在 ELF 文件中标记为 .init 的节的代码,进行程序运行的初始化。在早期系统中,初始化代码对应函数 _init(void)(函数名强制固定),在现代系统中,则对应形式为

1 void __attribute((constructor)) init_function(void)

2 {

3     ……

4 }

 

其中函数名为任意。


8.  动态连接器把控制传递给程序,从 ELF 文件头部中定义的程序进入点开始执行。在 a.out 格式和 ELF 格式中,程序进入点的值是显式存在的,在 COFF 格式中则是由规范隐含定义。


从上面的描述可以看出,加载文件最重要的是完成两件事情:

加载程序段和数据段到内存;

进行外部定义符号的重定位。

重定位是程序连接中一个重要概念。我们知道,一个可执行程序通常是由一个含有 main() 的主程序文件、若干目标文件、若干共享库(Shared Libraries)组成。(注:采用一些特别的技巧,也可编写没有 main 函数的程序)


一个 C 程序可能引用共享库定义的变量或函数,换句话说就是程序运行时必须知道这些变量/ 函数的地址。在静态连接中,程序所有需要使用的外部定义都完全包含在可执行程序中,而动态连接则只在可执行文件中设置相关外部定义的一些引用信息,真正的重定位是在程序运行之时。


静态连接方式有两个大问题:

如果库中变量或函数有任何变化都必须重新编译连接程序;

如果多个程序引用同样的变量/函数,则此变量/函数会在文件/内存中出现多次,浪费硬盘/内存空间。

比较两种连接方式生成的可执行文件的大小,可以看出有明显的区别


三、 文件格式分析

3.1 a.out 文件格式分析

a.out 格式在不同的机器平台和不同的 UNIX 操作系统上有轻微的不同,下面我们讨论的是最 '标准' 的格式。


a.out 文件包含 7 个 section,格式如下:


执行头部的数据结构(/usr/include/x86_64-linux-gnu/a.out.h):


 1 struct exec {

 2     unsigned long   a_info;    /* 魔数和其它信息 */

 3     unsigned long   a_text;      /* 文本段的长度 */

 4     unsigned long   a_data;      /* 数据段的长度 */

 5     unsigned long   a_bss;       /* BSS段的长度 */

 6     unsigned long   a_syms;      /* 符号表的长度 */

 7     unsigned long   a_entry;     /* 程序进入点 */

 8     unsigned long   a_trsize;    /* 文本重定位表的长度 */

 9     unsigned long   a_drsize;    /* 数据重定位表的长度 */

10 };

 

文件头部主要描述了各个 section 的长度,比较重要的字段是 a_entry(程序进入点),代表了系统在加载程序并初试化各种环境后开始执行程序代码的入口。


由 a.out 格式和头部数据结构我们可以看出,a.out 的格式非常紧凑,只包含了程序运行所必须的信息(文本、数据、BSS),而且每个 section 的顺序是固定的。这种结构缺乏扩展性,如不能包含 '现代' 可执行文件中常见的调试信息,最初的 UNIX 黑客对 a.out 文件调试使用的工具是 adb,而 adb 是一种机器语言调试器!


a.out 文件中包含符号表和两个重定位表,这三个表的内容在连接目标文件以生成可执行文件时起作用。在最终可执行的 a.out 文件中,这三个表的长度都为 0 。


a.out 文件在连接时就把所有外部定义包含在可执行程序中,如果从程序设计的角度来看,这是一种硬编码方式,或者可称为模块之间是强藕和的。


a.out 是早期UNIX系统使用的可执行文件格式,由 AT&T 设计,现在基本上已被 ELF 文件格式代替。a.out 的设计比较简单,但其设计思想明显的被后续的可执行文件格式所继承和发扬。


2.2 COFF 文件格式分析

COFF 格式比 a.out 格式要复杂一些,最重要的是包含一个节段表(section table),因此除了 .text,.data,和 .bss 区段以外,还可以包含其它的区段。另外也多了一个可选的头部,不同的操作系统可一对此头部做特定的定义。


COFF 文件格式如下:

结构体位于文件:

ubuntu中:/usr/include/linux/coff.h

Linux 内核中:include/uapi/linux/coff.h

文件头部的数据结构:


 1 struct COFF_filehdr

 2 {

 3     unsigned short  f_magic[2];    /* 魔数 */

 4     unsigned short  f_nscns[2];    /* 节个数 */

 5     long            f_timdat[4];   /* 文件建立时间 */

 6     long            f_symptr[4];   /* 符号表相对文件的偏移量 */

 7     long            f_nsyms[4];    /* 符号表条目个数 */

 8     unsigned short  f_opthdr[2];   /* 可选头部长度 */

 9     unsigned short  f_flags[2];    /* 标志 */

10 }


COFF 文件头部中魔数与其它两种格式的意义不太一样,它是表示针对的机器类型,例如 0x014c 相对于 I386 平台,而 0x268 相对于 Motorola 68000系列等。当 COFF 文件为可执行文件时,字段 f_flags 的值为 F_EXEC(0X00002),同时也表示此文件没有未解析的符号,换句话说,也就是重定位在连接时就已经完成。由此也可以看出,原始的 COFF 格式不支持动态连接。为了解决这个问题以及增加一些新的特性,一些操作系统对 COFF 格式进行了扩展。Microsoft 设计了名为 PE(Portable Executable)的文件格式,主要扩展是在 COFF 文件头部之上增加了一些专用头部,某些 UNIX 系统也对 COFF 格式进行了扩展,如 XCOFF(extended common object file format)格式,支持动态连接。


紧接文件头部的是可选头部,COFF 文件格式规范中规定可选头部的长度可以为 0,但在 LINUX 系统下可选头部是必须存在的。下面是 LINUX 下可选头部的数据结构:


 1 typedef struct 

 2 {

 3     char   magic[2];                /* 魔数 */

 4     char   vstamp[2];               /* 版本号 */

 5     char   tsize[4];                /* 文本段长度 */

 6     char   dsize[4];                /* 已初始化数据段长度 */

 7     char   bsize[4];                /* 未初始化数据段长度 */

 8     char   entry[4];                /* 程序进入点 */

 9     char   text_start[4];           /* 文本段基地址 */

10     char   data_start[4];           /* 数据段基地址 */

11 } COFF_AOUTHDR;


字段 magic 为 0413 时表示 COFF 文件是可执行的,注意到可选头部中显式定义了程序进入点,标准的 COFF 文件没有明确的定义程序进入点的值,通常是从 .text 节开始执行,但这种设计并不好。


前面我们提到,COFF 格式比 a.out 格式多了一个节段表,一个节头条目描述一个节数据的细节,因此 COFF 格式能包含更多的节,或者说可以根据实际需要,增加特定的节,具体表现在 COFF 格式本身的定义以及稍早提及的 COFF 格式扩展。下面将简单描述 COFF 文件中节的数据结构,因为节的意义更多体现在程序的编译和连接上,所以本文不对其做更多的描述。此外,ELF 格式和 COFF 格式对节的定义非常相似,在随后的 ELF 格式分析中,将省略相关讨论。


 1 struct COFF_scnhdr 

 2 {

 3     char    s_name[8];              /* 节名称 */

 4     char    s_paddr[4];             /* 物理地址 */

 5     char    s_vaddr[4];             /* 虚拟地址 */

 6     char    s_size[4];              /* 节长度 */

 7     char    s_scnptr[4];            /* 节数据相对文件的偏移量 */

 8     char    s_relptr[4];            /* 节重定位信息偏移量 */

 9     char    s_lnnoptr[4];           /* 节行信息偏移量 */

10     char    s_nreloc[2];            /* 节重定位条目数 */

11     char    s_nlnno[2];             /* 节行信息条目数 */

12     char    s_flags[4];             /* 段标记 */

13 };


LINUX 系统中头文件 coff.h 中对字段 s_paddr 的注释是 'physical address' ,但似乎应该理解为 '节被加载到内存中所占用的空间长度' 。字段 s_flags 标记该节的类型,如文本段、数据段、BSS 段等。在 COFF 的节中也出现了行信息,行信息描述了二进制代码与源代码的行号之间的对映关系,在调试时很有用。


文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。

相关文章

    提高单片机代码编译速度的几种方法;程序员做软件开发,讨厌一件事,那就是编译代码速度很慢的问题。 编码一分钟,编译十分钟,这谁能受的了? 今天就来说说嵌入式软件开发中,常见的提高编译......
    项目;关闭本窗口。注意:这里灵活处理,打开文件夹和项目都可以。后续教程将不在重复本步操作。 第三节 MDK代码 第一步,编译代码,检查STM32CubeMX生成的代码是否正常。如下图,点击1处编译,之后......
    的文件名,是一个字符串,而这里说的是行号,是一个整型变量,这是这两者的区别, 比如在工程中添加源代码: char BuildLine = __LINE__;printf("编译代码所在行:%d......
    lcd_hd44780_stml4xx.c文件(右键单击该子菜单,然后浏览选项,将三个LCD文件复制到keil的源文件夹后附加该文件。) 步骤11:编译代码编译代码并在出现任何错误时进行调试。 ......
    ,选择时钟树部分,并设置时钟位32MHZ。 6,如下图,分别是项目的名字和路径,编译器和编译器的版本。 7,选择文件分类。 8,生成项目。 第三节编写代码 1,编译代码,0错误0警告。证明......
    Makefile工程生成后,移植 nr shell 下面 源码 添加 makefile ,将添加的文件加入编译: 移植之后在顶层 make 编译代码编译完成后下载代码 四、代码修改 修改......
    )然后便可以在c文件里编写51单片机代码了;此时还差一个步骤,需要选择keil软件编译代码后自动生成HEX文件; (7)以下编写一段单片机控制LED闪烁的代码例程。编写好了后点击编译按钮; (8......
    是可以直接将CUDA编译代码直接转换过来。 今年4月份,这一项目被正式命名为“Radeon开放计算平台”(Radeon Open Compute Platform),简称ROCm,并发布了1.0版本......
    ,10k电阻作用是上拉,当需要点亮LED时,将51单片机对应的IO拉低即可。 四、编译烧录代码编译代码之前,我们还要开启生成hex文件的选项,该文件就是我们用来烧录的文件。 接下来就是编译代码......
    编写了程序。具体步骤如下 在 Keil 窗口创建一个新项目并选择目标(微控制器)。 在项目下创建一个新文件并编写代码。 以 .c 扩展名保存代码,并将文件添加到目标文件夹下的源代码组文件夹中。 编译代码......

我们与500+贴片厂合作,完美满足客户的定制需求。为品牌提供定制化的推广方案、专属产品特色页,多渠道推广,SEM/SEO精准营销以及与公众号的联合推广...详细>>

利用葫芦芯平台的卓越技术服务和新产品推广能力,原厂代理能轻松打入消费物联网(IOT)、信息与通信(ICT)、汽车及新能源汽车、工业自动化及工业物联网、装备及功率电子...详细>>

充分利用其强大的电子元器件采购流量,创新性地为这些物料提供了一个全新的窗口。我们的高效数字营销技术,不仅可以助你轻松识别与连接到需求方,更能够极大地提高“闲置物料”的处理能力,通过葫芦芯平台...详细>>

我们的目标很明确:构建一个全方位的半导体产业生态系统。成为一家全球领先的半导体互联网生态公司。目前,我们已成功打造了智能汽车、智能家居、大健康医疗、机器人和材料等五大生态领域。更为重要的是...详细>>

我们深知加工与定制类服务商的价值和重要性,因此,我们倾力为您提供最顶尖的营销资源。在我们的平台上,您可以直接接触到100万的研发工程师和采购工程师,以及10万的活跃客户群体...详细>>

凭借我们强大的专业流量和尖端的互联网数字营销技术,我们承诺为原厂提供免费的产品资料推广服务。无论是最新的资讯、技术动态还是创新产品,都可以通过我们的平台迅速传达给目标客户...详细>>

我们不止于将线索转化为潜在客户。葫芦芯平台致力于形成业务闭环,从引流、宣传到最终销售,全程跟进,确保每一个potential lead都得到妥善处理,从而大幅提高转化率。不仅如此...详细>>