沈阳网站建设服务器,网站打开慢怎么回事,网站做超链接薪资多少一个月,企业做网站机器执行的是机器指令#xff0c;而机器指令就是一堆二进制的数字。高级语言编写的程序之所以可以在不同的机器上移植就因为有为不同机器设计的编译器的存在。高级语言的编译器就是把高级语言写的程序转换成某个机器能直接执行的二进制代码。以上的知识在我们学习CS(Computer …机器执行的是机器指令而机器指令就是一堆二进制的数字。高级语言编写的程序之所以可以在不同的机器上移植就因为有为不同机器设计的编译器的存在。高级语言的编译器就是把高级语言写的程序转换成某个机器能直接执行的二进制代码。以上的知识在我们学习CS(Computer Science)的初期老师都会这么对我们讲。但是我就产生疑问了既然机器都是执行的二进制代码那么是不是说只要硬件相互兼容不同操作系统下的可执行文件可以互相运行呢答案肯定是不行。这就要谈到可执行文件的格式问题。每个操作系统都会有自己的可执行文件的格式比如以前的Unix®是用a.out格式的现代的Unix®类系统使用elf格式 WindowsNT®是使用基于COFF格式的可执行文件。那么最简单的格式应该是DOS的可执行格式严格来说DOS的可执行文件没有什么格式可言就是把二进制代码安顺序放在文件里运行时DOS操作系统就把所有控制计算机的权力都给了这个程序。这种方式的不足之处是显而易见的所以现代的操作系统都有一种更好的方式来定义可执行文件的格式。一种常见的方法就是为可执行文件分段一般来说把程序指令的内容放在.text段中把程序中的数据内容放在.data段中把程序中未初始化的数据放在.bss段中。这种做法的好处有很多可以让操作系统内核来检查程序防止有严重错误的程序破坏整个运行环境。比如某个程序想要修改.text段中的内容那么操作系统就会认为这段程序有误而立即终止它的运行因为系统会把.text段的内存标记为只读。在.bss段中的数据还没有初始化就没有必要在可执行文件中浪费储存空间。在.bss中只是表明某个变量要使用多少的内存空间等到程序加载的时候在由内核把这段未初始化的内存空间初始化为0。这些就是分段储存可执行文件的内容的好处。下面谈一下Unix系统里的两种重要的格式a.out和elfExecutable and Linking Format。这两种格式中都有符号表symbol table其中包括所有的符号程序的入口点还有变量的地址等等。在elf格式中符号表的内容会比a.out格式的丰富的多。但是这些符号表可以用 strip工具去除这样的话这个文件就无法让debug程序跟踪了但是会生成比较小的可执行文件。a.out文件中的符号表可以被完全去除但是 elf中的在加载运行是起着重要的作用所以用strip永远不可能完全去除elf格式文件中的符号表。但是用strip命令不是完全安全的比如对未连接的目标文件来说如果用strip去掉符号表的话会导致连接器无法连接。例如Shell代码 $:gcc -c hello.c $:ls hello.c hello.o 用gcc把hello.c编译成目标文件hello.oShell代码 $:strip hello.o 用strip去掉hello.o中的符号信息。Shell代码 $:gcc hello.o /usr/lib/gcc/i686-pc-linux-gnu/3.4.5/../../../crt1.o: In function _start: init.c: (.text0x18) : undefined reference to main collect2: ld returned 1 exit status 再用gcc连接时连接器ld报错。说明在目标文件中的符号起着很重要的作用如果要发布二进制的程序的话在debug后为了减小可执行文件的大小可以用strip来除去符号信息但是在程序的调试阶段还是不要用strip为好。在接下去讨论以前我们还要来讲讲relocations的概念首先有个简单的程序hello.cShell代码 $:cat hello.c main( ) { printf(Hello World\n); } 当我们把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代码 struct exec { unsigned long a_info; /*Use macros N_MAGIC, etc for access */ unsigned a_text; /* length of text, in bytes */ unsigned a_data; /* length of data, in bytes */ unsigned a_bss; /* length of uninitialized data area for file, in bytes*/ unsigned a_syms; /* length of symbol table data in file, in bytes */ unsigned a_entry; /* start address */ unsigned a_trsize; /*length of relocation info for text, in bytes */ unsigned a_drsize; /*length of relocation info for data, in bytes */ }; 在这个结构中更本没有指示每个段在文件中的开始位置内核加载器具有一些非正式的方法来加载可执行文件的。明显的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代码 $:cat hello.c main() { printf(Hello World\n); } $:gcc-elf -c hello.c 还是这个简单的程序用gcc把它编译成目标文件hello.o。然后用readelf工具来探测一下elf文件的内容。readelf是在 binutils软件包里的一个工具大多数Linux发行版都包含它Shell代码 $:readelf -h hello.o ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2s complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 256 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) Number of section headers: 11 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代码 $:readelf -S hello.o There are 11 section headers, starting at offset 0x100: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 00002a 00 AX 0 0 4 [ 2] .rel.text REL 00000000 000370 000010 08 9 1 4 [ 3] .data PROGBITS 00000000 000060 000000 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 000060 000000 00 WA 0 0 4 [ 5] .rodata PROGBITS 00000000 000060 00000e 00 A 0 0 1 [ 6] .note.GNU-stack PROGBITS 00000000 00006e 000000 00 0 0 1 [ 7] .comment PROGBITS 00000000 00006e 00003e 00 0 0 1 [ 8] .shstrtab STRTAB 00000000 0000ac 000051 00 0 0 1 [ 9] .symtab SYMTAB 00000000 0002b8 0000a0 10 10 8 4 [10] .strtab STRTAB 00000000 000358 000015 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific) Name字段显示的是各个段的名字Type显示段的属性Addr是每个段载入虚拟内存的位置Off是每个段在目标文件中的偏移位置Size是每个段的大小后面的一些字段是表示段的可写可读或者可执行。用-r可以列出elf文件中的relocationShell代码 $:readelf -r hello.o Relocation section .rel.text at offset 0x370 contains 2 entries: Offset Info Type Sym.Value Sym. Name 0000001f 00000501 R_386_32 00000000 .rodata 00000024 00000902 R_386_PC32 00000000 printf 在.text段中有两个relocation其中之一就是printf函数的relcation。Offset指出当relocation时要把 printf函数的入口地址贴到离.text段开头00000024处。下面我们可以看一下连接过后的可执行文件中的内容Shell代码 $:gcc -c hello.c $:ls 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_NOW1让连接器在程序执行前把所有的函数地址都连接好这主要是方便调试程序。readelf工具还有很多选项具体内容可以查看man手册。在文章的开头就说elf文件格式很方便运用动态连接技术下面我就写一个就简单的动态连接库的例子Shell代码 $:gcc -c hello.c $:ls hello.c hello.o 1两个简单的文件在mian函数中调用hi()函数下面并不是把两个文件一起编译而是把hi.c编译成动态连接库。注意Dyn_hello.c中并没有包含任何头文件。Shell代码 $:gcc -c hello.c $:ls hello.c hello.o 2现在在当前目录下有一个名字为libhi.so的文件这就就是仅含有一个函数的动态连接库。Shell代码 $:gcc -c hello.c $:ls hello.c hello.o 3在当前目录下有了一个Dyn_hello可执行文件现在就可以执行它了。Shell代码 $:gcc -c hello.c $:ls hello.c hello.o 4执行不成功这就表明了这是一个动态连接的程序连接器找不到libhi.so这个动态连接库。在命令行加上 LD_LIBRARY_PATH...就行了。像这样运行Shell代码 $:gcc -c hello.c $:ls hello.c hello.o 5指出当前目录是连接器的搜索目录就可以了。Elf可执行文件还有一个a.out很难实现的特点就是对dlopen()函数的支持这个函数可以在程序中控制动态的加载动态连接库看下面的一个小程序Shell代码 $:gcc -c hello.c $:ls hello.c hello.o 6用一下命令编译Shell代码 $:gcc -c hello.c $:ls hello.c hello.o 7运行Dl_hello程序加上动态连接库。Shell代码 $:gcc -c hello.c $:ls hello.c hello.o 8命令行成功的打印出了Hello world说明我们的动态连接库运用成功了。在这篇文章中只是讨论了elf可执行文件的执行原理还有很多方面没有涉及到要深入了解elf你也许需要对动态连接器hack一下也要hack一下内核加载程序的loader。但是我想对大多数人来说这篇文章对elf的介绍已经足够让你可以自己对elf在进行比较深入的研究了。