Elf

机器执行的是机器指令,而机器指令就是一堆二进制的数字。高级语言编写的程序之所以可以在不同的机器上移植就因为有为不同机器设计的编译器的存在。高级语言的编译器就是把高级语言写的程序转换成某个机器能直接执行的二进制代码。以上的知识在我们学习CS(Computer Science)的初期,老师都会这么对我们讲。但是我就产生疑问了:既然机器都是执行的二进制代码,那么是不是说只要硬件相互兼容,不同操作系统下的可执行文件可以互相运行呢?答案肯定是不行。这就要谈到可执行文件的格式问题。

每个操作系统都会有自己的可执行文件的格式,比如以前的Unix®是用a.out格式的,现代的Unix®类系统使用elf格式, WindowsNT®是使用基于COFF格式的可执行文件。那么最简单的格式应该是DOS的可执行格式,严格来说DOS的可执行文件没有什么格式可言,就是把二进制代码安顺序放在文件里,运行时DOS操作系统就把所有控制计算机的权力都给了这个程序。这种方式的不足之处是显而易见的,所以现代的操作系统都有一种更好的方式来定义可执行文件的格式。一种常见的方法就是为可执行文件分段,一般来说把程序指令的内容放在.text段中,把程序中的数据内容放在.data段中,把程序中未初始化的数据放在.bss段中。这种做法的好处有很多,可以让操作系统内核来检查程序防止有严重错误的程序破坏整个运行环境。比如:某个程序想要修改.text段中的内容,那么操作系统就会认为这段程序有误而立即终止它的运行,因为系统会把.text段的内存标记为只读。在.bss段中的数据还没有初始化,就没有必要在可执行文件中浪费储存空间。在.bss中只是表明某个变量要使用多少的内存空间,等到程序加载的时候在由内核把这段未初始化的内存空间初始化为0。这些就是分段储存可执行文件的内容的好处。

下面谈一下Unix系统里的两种重要的格式:a.out和elf(Executable and Linking Format)。这两种格式中都有符号表(symbol table),其中包括所有的符号(程序的入口点还有变量的地址等等)。在elf格式中符号表的内容会比a.out格式的丰富的多。但是这些符号表可以用 strip工具去除,这样的话这个文件就无法让debug程序跟踪了,但是会生成比较小的可执行文件。a.out文件中的符号表可以被完全去除,但是 elf中的在加载运行是起着重要的作用,所以用strip永远不可能完全去除elf格式文件中的符号表。但是用strip命令不是完全安全的,比如对未连接的目标文件来说如果用strip去掉符号表的话,会导致连接器无法连接。例如:
Shell代码
  1. $:gcc -c hello.c  
  2. $:ls  
  3. hello.c      hello.o   

用gcc把hello.c编译成目标文件hello.o
Shell代码
  1. $:strip hello.o  

用strip去掉hello.o中的符号信息。
Shell代码
  1. $:gcc hello.o  
  2. /usr/lib/gcc/i686-pc-linux-gnu/3.4.5/../../../crt1.o: In function `_start':  
  3. init.c:  (.text+0x18)  : undefined reference to `main' collect2: ld returned 1 exit status  

再用gcc连接时,连接器ld报错。说明在目标文件中的符号起着很重要的作用,如果要发布二进制的程序的话,在debug后为了减小可执行文件的大小,可以用strip来除去符号信息但是在程序的调试阶段还是不要用strip为好。

在接下去讨论以前,我们还要来讲讲relocations的概念:首先有个简单的程序hello.c
Shell代码
  1. $:cat hello.c  
  2. main( )  
  3. {  
  4. printf("Hello World\n");  
  5. }   

当我们把hello.c编译为目标文件时,我们并没有在源文件中定义printf这个函数,所以汇编器也不知道printf这个函数的具体的地址,所以在目标文件中就会留下printf这个符号。以下的工作就交给连接器了,连接器会找到这个函数的入口地址然后传递给这个文件最终形成可执行文件。这个过程就叫做relocations。a.out格式的可执行文件是没有这种relocation的功能的,内核不会执行其中还有未知函数的入口地址的可执行文件的。在目标文件中当然可以relocation,只不过连接器需要把未知函数的入口地址完全找到,生成可执行文件才行。这样就有一个很尴尬的问题,在 a.out格式中极其难以实现动态连接技术。要知道为什么现在的Unix几乎都是用的elf格式的可执行文件就要了解a.out格式的短处。

a.out的符号是极其有限的,在/usr/include/linux/asm/a.out.h中定义了一个结构exec就是:
Shell代码
  1. struct exec  
  2. {   
  3.     unsigned long a_info;       /*Use macros N_MAGIC, etc for access */  
  4.     unsigned a_text;        /* length of text, in bytes */  
  5.     unsigned a_data;        /* length of data, in bytes */  
  6.     unsigned a_bss;         /* length of uninitialized data area for file, in bytes*/  
  7.     unsigned a_syms;        /* length of symbol table data in file, in bytes */  
  8.     unsigned a_entry;       /* start address */  
  9.     unsigned a_trsize;      /*length of relocation info for text, in bytes */  
  10.     unsigned a_drsize;       /*length of relocation info for data, in bytes */  
  11. };  

在这个结构中更本没有指示每个段在文件中的开始位置,内核加载器具有一些非正式的方法来加载可执行文件的。明显的,a.out 是不支持动态连接的。(在内部不支持动态连接,用某些技术也是可以实现a.out的动态连接)

要了解elf可执行文件的运行方式,我们有必要讨论一下动态连接技术。很多人对动态连接技术十分熟悉,但是很少有人真正了解动态连接的内部工作方式。回想没有动态连接的日子,程序员写程序时不用什么都从头开始,他们可以调用定义的很好的函数,然后再用连接器与函数库连接。这样的话使得程序员更加有效率,但是一个十分重要的问题出现了:这样产生的可执行文件就会很大。因为连接器把程序需要用的所有函数的代码都复制到了可执行文件中去了。这种连接方式就是所谓的静态连接,与之相对的就是动态连接。连接器在可执行文件中标记出程序调用外部函数的位置,并不把代码复制进去,只是标出函数在动态连接库中的位置。用这样的方式生成的特殊可执行文件就是动态连接的。在运行这种动态程序时,系统在运行时把该程序调用的外部函数地址映射到程序地址,这就是所谓的动态连接,系统就有一个程序叫做动态连接器,在动态连接的程序执行前都要先把地址映射好。很显然的,必须有一种机制保证动态连接的程序中的函数地址正确地指向了动态连接库的某个函数地址。这就需要讨论一下elf可执行文件格式处理动态连接的机制了。

elf的动态连接库是内存位置无关的,就是说你可以把这个库加载到内存的任何位置都没有影响。这就叫做position independent。而a.out的动态连接库是内存位置有关的,它一定要被加载到规定的内存地址才能工作。在编译内存位置无关的动态连接库时,要给编译器加上 -fpic选项,让编译器产生的目标文件是内存位置无关的还会尽量减少对变量引用时使用绝对地址。把库编译成内存位置无关会带来一些花费,编译器会保留一个寄存器来指向全局偏移量表(global offset table (or GOT for short)),这就会导致编译器在优化代码时少了一个寄存器可以使用,但是在最坏的情况下这种性能的减少只有3%,在其他情况下是大大小于3%的。

Elf的另一个特点是它的动态连接库是在运行时处理符号的,这是通过用符号表和再布置(relocation)表来实现的。在载入文件时并不能立即执行,要在处理完符号表把所有的地址都relocation完后才可以执行。这个听起来有点复杂而且可能导致文件运行慢,不过对elf做了很大的优化后,这种减慢已经是微不足道的了。理论上说不是用-fpic选项编译出来的目标文件也可以用作动态连接库,但是在运行时会需要做数目极大的 relocation,这是对运行速度有极大影响的。这样的程序性能是很差的,几乎没有可用性。

当从动态连接库中读一个全局变量时与从非-fpic编译的目标文件读是不同的。读动态连接的库中的变量是通过GOT来寻找到目标变量的,GOT已经由某一个寄存器指向了。GOT本生就是一个指针列表,找到GOT中的某一个指针就可以读到所要的全局变量了,有了GOT我们要读出一个变量只要做一次relocation。

下面我们来看看elf文件中到底有些什么信息:
Shell代码
  1. $:cat hello.c  
  2. main()  
  3. {  
  4.         printf("Hello World\n");  
  5. }  
  6. $:gcc-elf -c hello.c  

还是这个简单的程序,用gcc把它编译成目标文件hello.o。然后用readelf工具来探测一下elf文件的内容。(readelf是在 binutils软件包里的一个工具,大多数Linux发行版都包含它)
Shell代码
  1. $:readelf -h hello.o  
  2.   ELF Header:  
  3.   Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00  
  4.   Class:                             ELF32  
  5.   Data:                              2's complement, little endian  
  6.   Version:                           1 (current)  
  7.   OS/ABI:                            UNIX - System V  
  8.   ABI Version:                       0  
  9.   Type:                              REL (Relocatable file)  
  10.   Machine:                           Intel 80386  
  11.   Version:                           0x1  
  12.   Entry point address:               0x0  
  13.   Start of program headers:          0 (bytes into file)  
  14.   Start of section headers:          256 (bytes into file)  
  15.   Flags:                             0x0  
  16.   Size of this header:               52 (bytes)  
  17.   Size of program headers:           0 (bytes)  
  18.   Number of program headers:         0  
  19.   Size of section headers:           40 (bytes)  
  20.   Number of section headers:         11  
  21.   Section header string table index: 8  

-h选项是列出elf文件的头信息。Magic:字段是一个标识符,只要Magic字段是7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00的文件都是elf文件。Class:字段是表示elf的版本,这是一个32位的elf。Machine:字段是指出目标文件的平台信息,这里是 I386兼容平台。其他的字段可以从其字面上看出它的意义,这里就不一一解释了。

下面用-S选项列出段的头信息:
Shell代码
  1. $:readelf -S hello.o  
  2. There are 11 section headers, starting at offset 0x100:  
  3.   
  4. Section Headers:  
  5.   [Nr] Name                  Type                Addr             Off            Size   ES   Flg     Lk Inf Al  
  6.   [ 0]                             NULL                00000000 000000 000000     00          0   0  0  
  7.   [ 1] .text                     PROGBITS        00000000 000034 00002a     00  AX    0   0  4  
  8.   [ 2] .rel.text                REL                 00000000 000370 000010      08           9   1  4  
  9.   [ 3] .data                    PROGBITS        00000000 000060 000000     00  WA    0   0  4  
  10.   [ 4] .bss                     NOBITS          00000000 000060 000000        00  WA   0   0  4  
  11.   [ 5] .rodata                PROGBITS        00000000 000060 00000e      00   A      0   0  1  
  12.   [ 6] .note.GNU-stack  PROGBITS        00000000 00006e 000000     00           0   0  1  
  13.   [ 7] .comment            PROGBITS        00000000 00006e 00003e     00            0   0  1  
  14.   [ 8] .shstrtab             STRTAB          00000000 0000ac 000051        00           0   0  1  
  15.   [ 9] .symtab               SYMTAB          00000000 0002b8 0000a0      10            10   8  4  
  16.   [10] .strtab                STRTAB          00000000 000358 000015       00           0   0  1  
  17. Key to Flags:  
  18.   W (write), A (alloc), X (execute), M (merge), S (strings)  
  19.   I (info), L (link order), G (group), x (unknown)  
  20.   O (extra OS processing required) o (OS specific), p (processor specific)  

Name字段显示的是各个段的名字,Type显示段的属性,Addr是每个段载入虚拟内存的位置,Off是每个段在目标文件中的偏移位置,Size是每个段的大小,后面的一些字段是表示段的可写,可读,或者可执行。

用-r可以列出elf文件中的relocation:
Shell代码
  1. $:readelf -r hello.o  
  2.   
  3. Relocation section '.rel.text' at offset 0x370 contains 2 entries:  
  4.  Offset     Info    Type            Sym.Value  Sym. Name  
  5. 0000001f  00000501 R_386_32          00000000   .rodata  
  6. 00000024  00000902 R_386_PC32        00000000   printf  

在.text段中有两个relocation,其中之一就是printf函数的relcation。Offset指出当relocation时要把 printf函数的入口地址贴到离.text段开头00000024处。

下面我们可以看一下连接过后的可执行文件中的内容:
Shell代码
  1. $:gcc -c hello.c  
  2. $:ls  
  3. hello.c      hello.o   
0
这里的段比目标文件hello.o的段要多的多,这是因为这个程序需要elf的一个动态连接库libc.so.1。在这里需要简单的介绍一下内核加载 elf可执行文件。内核先是把整个文件加载到用户的虚拟内存空间,如果程序是与动态连接库连接的,则程序中就会包含动态连接器的名称,可能是 /lib/elf/ld-linux.so.1。(动态连接器本身也是一个动态连接库)

在文件的尾部的一些段的Addr值是00000000,因为这些都是符号表,动态连接器并不把这些段的内容加载到内存中。. interp段中只是储存这一个ASCII的字符串,它就是动态连接器的名字(路径)。.hash, .dynsym, .dynstr这三个段是用于动态连接器执行relocation时的符号表。.hash是一个哈希表,可以让我们很快的从.dynsym中找到所需的符号。

.plt段中储存着我们调用动态连接库中的函数入口地址,在默认状态下,程序初始化时,.plt中的指针并不是指向正确的函数入口地址的而是指向动态连接器本身,当你在程序中调用某个动态连接库中的函数时,连接器会找到那个函数在动态连接库中的位置,再把这个位置连接到.plt段中。这样做的好处是如果在程序中调用了很多动态连接库中的函数,会花费掉连接器很长时间把每个函数的地址连接到.plt段中。所以就可以采用连接器只是把要用的函数地址连接进去,以后要用的再连接。但是也可以设置环境变量LD_BIND_NOW=1让连接器在程序执行前把所有的函数地址都连接好,这主要是方便调试程序。

readelf工具还有很多选项,具体内容可以查看man手册。在文章的开头就说elf文件格式很方便运用动态连接技术,下面我就写一个就简单的动态连接库的例子:
Shell代码
  1. $:gcc -c hello.c  
  2. $:ls  
  3. hello.c      hello.o   
1
两个简单的文件,在mian函数中调用hi()函数,下面并不是把两个文件一起编译,而是把hi.c编译成动态连接库。(注意Dyn_hello.c中并没有包含任何头文件。)
Shell代码
  1. $:gcc -c hello.c  
  2. $:ls  
  3. hello.c      hello.o   
2
现在在当前目录下有一个名字为libhi.so的文件,这就就是仅含有一个函数的动态连接库。
Shell代码
  1. $:gcc -c hello.c  
  2. $:ls  
  3. hello.c      hello.o   
3
在当前目录下有了一个Dyn_hello可执行文件,现在就可以执行它了。
Shell代码
  1. $:gcc -c hello.c  
  2. $:ls  
  3. hello.c      hello.o   
4
执行不成功,这就表明了这是一个动态连接的程序,连接器找不到libhi.so这个动态连接库。在命令行加上 LD_LIBRARY_PATH=...就行了。像这样运行:
Shell代码
  1. $:gcc -c hello.c  
  2. $:ls  
  3. hello.c      hello.o   
5
指出当前目录是连接器的搜索目录,就可以了。

Elf可执行文件还有一个a.out很难实现的特点,就是对dlopen()函数的支持,这个函数可以在程序中控制动态的加载动态连接库,看下面的一个小程序:
Shell代码
  1. $:gcc -c hello.c  
  2. $:ls  
  3. hello.c      hello.o   
6
用一下命令编译:
Shell代码
  1. $:gcc -c hello.c  
  2. $:ls  
  3. hello.c      hello.o   
7
运行Dl_hello程序加上动态连接库。
Shell代码
  1. $:gcc -c hello.c  
  2. $:ls  
  3. hello.c      hello.o   
8
命令行成功的打印出了Hello world说明我们的动态连接库运用成功了。

在这篇文章中只是讨论了elf可执行文件的执行原理,还有很多方面没有涉及到,要深入了解elf你也许需要对动态连接器hack一下,也要hack一下内核加载程序的loader。但是我想对大多数人来说,这篇文章对elf的介绍已经足够让你可以自己对elf在进行比较深入的研究了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/434629.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python教程闭包_Python教程 闭包的特性

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~闭包(closure)是函数式编程的重要的语法结构。函数式编…

直接打印报表

在ax中有时可能需要在打印时,不显示报表的预览与设置窗口,而是直接Send到打印机。可以使用ClassFactory、PrintJobSettings、ReportRun来完成。 static void NJ_MF_DirectPrint(Args _args) { Args args new Args(); ReportRun …

如何构建自己的SIP SERVER!

如果你下载了 sip phone, 自己又做了一个 SIP SERVER,那么你就可以当老大了,不要什么MSN,QQ的语音通话了,自己就可以直接同你想要的人通话了。1:软件准备:A: SIP SERVER http://www.brekeke.com/en/download/idx_sipse…

java java.lang_Java之java.lang.IllegalMonitorStateException

今天又中彩了, 原本很简单的多线程程序, 蓦然间冒了个"java.lang.IllegalMonitorStateException" , 杀了个措手不及. 一直纳闷, 为什么为什么? 查资料说该异常由于对象未获取得到Lock就试图操作Lock. 再细细源码, 原来不不小将lock.lock()写错为lock.tryLock(). 坑爹…

CruiseControl.NET ----- mail 配置

最近在用 CruiseControl.NET实现每日构建,其他配置起来都挺方便,就是在邮件设置上费了不少时间,我用的是CC.NET1.6,这个版本已经支持发送附件,如果使用外部邮箱,记得要把邮箱的smtp功能打开,下面是Mail配置的一个例子&…

java编程字_Java编程基本概念

1.标识符①用于给变量、类和方法命名(类名首字母大写,变量和方法名首字母小写并遵循驼峰原则)②标识符的命名规范:■标识符必须以字母、下划线和美元符$开头。■标识符其他部分可以是字母、下划线、美元符和数字的任意组合。■Java标识符大小写敏感&…

ubuntu gedit出错:Failed to connect to the session manager

刚才用su到root后,用命令gedit发现会出错:** (gedit:2976): WARNING **: 连接已关闭(gedit:2976): EggSMClient-WARNING **: Failed to connect to the session manager:None of the authentication protocols specified are supported** (gedit:2976): …

php 类的实现 完整例子

文件目录&#xff1a; --index.php --php --data_info.php index.php 这里要require_once类所在的php文件 <?php require_once(./php/data_info.php);$oneDatanew user;$oneData->setName("username");$oneData->setPassword("password");echo $…

mac java版本 不一致_mac实现不同版本的jdk切换

之前使用jdk11进行java开发(纯粹因为喜欢新版?)但是使用jdk11在布署hadoop伪分布时各种报错, 所以还是下载jdk8回来.接下来就是mac端切换两个版本的jdk(按照网上找的方式好像有bug-文章最后再说.虽然不知道怎么解决,但是至少我可以成功部署hadoop, 所以这里就先忽略)首先下载j…

Meld安装

Ubuntu下文件/目录对比的软件Meld可能有很多用户还不是很熟悉&#xff0c;下文就给大家介绍如何安装Meld和移植到Gedit下。具体内容如下所述。 Meld允许用户查看文件、目录间的变化。很容易移植到Gedit下&#xff0c;方便用户使用。 安装Meld Meld默认在Ubuntu官方源中&#…

ios sqlite3 初级应用

ios sqlite3 初级应用 在ios中&#xff0c;持久化用好几种 方法&#xff0c;前面已经介绍了 两种 &#xff0c;一个是简单的写入文件&#xff0c;另一个是加入了序列化并写入文件中&#xff0c;现在介绍 ios 中嵌入式数据库sqlite3的初级应用 当然在使用sqlite3之前 你需要将l…

java aqs源码_java中AQS源码分析

AQS内部采用CLH队列。CLH队列是由节点组成。内部的Node节点包含的状态有static final int CANCELLED 1;static final int SIGNAL -1;static final int CONDITION -2;static final int PROPAGATE -3;其中取消状态表示任务的取消&#xff0c;SIGNAL状态表示后续节点需要唤…

android4.0.3去掉底部状态栏statusbar,全屏显示示例代码

要去掉android4.0上的状态栏&#xff0c;全屏显示的代码如下&#xff1a; 1、将usleep和killall这二个文件放到assets文件夹下。这二个文件可在下面的附件中下载到。 2、创建Device.java&#xff08;注&#xff1a;附件里有完整的代码&#xff09;: 001import java.io.Buf…

[译]预留位置队列PRQueue:多线程程序中消息输入队列和消息输出队列保持同序...

译自&#xff1a; http://accu.org/var/uploads/journals/overload101.pdf 在多线程应用程序中&#xff0c;要求消息输入队列和消息输出队列顺序要求保持一致&#xff0c;而忽略多线程并发处理的顺序&#xff0c;这种情况是比较难处理的。在本文中&#xff0c;作者设计了一种新…

java 前端工作内容_java前端、java后端、java全栈工作主要内容是什么?哪个薪资高?...

摘要最近&#xff0c;听了一场关于java全栈工程师职位的简介说明&#xff0c;里面很清楚的说明了一下前端&#xff0c;后端&#xff0c;全栈都是做什么工作的。其实&#xff0c;想做这个行业&#xff0c;就应该了解职能以及技能需求&#xff0c;这样学习才能更高效。我知道一些…

用yate2实现软VoIP语音通话(SIP协议)

用yate2实现软VoIP语音通话(SIP协议) 阳光男孩 发表于 2009-01-08 2009年1月7日&#xff0c;工业与信息化部发放了三张3G牌照&#xff0c;标志着中国进入了通信技术的新时代。3G的重要特性之一是高速数据链路&#xff0c;移动上网速度大大提高。同时&#xff0c;中国移动也大…

避免頁面重復提交3/15

在用戶做資料錄入操作時時常會反映重復記錄出現,經過了解是針對新手或者性子急的用戶在儲存時多次點擊引起 有效處理方法:新增一textbox,對儲存按鈕的onclick增設js代碼:btnsave.Attributes.Add("onclick", "var tb15document.getElementById(Textbox15);var nu…

java 日志设计_Java日志设计实践(3) - 开发篇

1.选择恰当的日志级别2.输出明确的提示文字和充分的现场信息3.输出内容一行搞定&#xff0c;不要换行4.其他1.选择恰当的日志级别选择日志级别时需要遵循一些通用规范&#xff0c;不可随意定义log4j的日志级别&#xff0c;由低到高排列&#xff1a;all trace debug info warn e…

ConfigurationManager.AppSettings[] ConfigurationManager智能显示不出来

解决办法&#xff1a;在项目中添加System.Configuration引用。转载于:https://www.cnblogs.com/2008freestyle/archive/2012/03/15/2398046.html

java5的递归算法_java递归算法 java面试题(5)

Java语言是一种具有动态性的解释型语言&#xff0c;类(class)只有被加载到JVM后才能运行。当运行指定程序时&#xff0c;JVM会将编译生成的.class文件按照需求和一定的规则加载到内存中&#xff0c;并组织成为一个完整的Java应用程序。这个加载过程是由类加载器完成&#xff0c…