可以做淘宝店铺开关灯网站,隐私空间,企业网站意思,个人免费域名空间建站发表于 2010年05月18日 22:01 分类: 编程coding统计: 1评/465阅 2人收藏此文章#xff0c; 收藏此文章(?)by falconzhangjinwgmail.com 2008-02-22 平时在Linux下写代码#xff0c;直接用gcc -o out in.c就把代码编译好了#xff0c;但是这后面到底做了… 发表于 2010年05月18日 22:01 分类: 编程coding统计: 1评/465阅 2人收藏此文章 收藏此文章(?) by falconzhangjinwgmail.com 2008-02-22 平时在Linux下写代码直接用gcc -o out in.c就把代码编译好了但是这后面到底做了什么事情呢如果学习过编译原理则不难理解一般高级语言程序编译的过程莫过于预处理、编译、汇编、链接。gcc在后台实际上也经历了这几个过程我们可以通过-v参数查看它的编译细节如果想看某个具体的编译过程则可以分别使用-E,-S,-c和 -O对应的后台工具则分别为cpp,cc1,as,ld。下面我们将逐步分析这几个过程以及相关的内容诸如语法检查、代码调试、汇编语言等。 1、预处理 开篇简述预处理是C语言程序从源代码变成可执行程序的第一步主要是C语言编译器对各种预处理命令进行处理包括头文件的包含、宏定义的扩展、条件编译的选择等。 以前没怎么“深入”预处理脑子对这些东西总是很模糊只记得在编译的基本过程词法分析、语法分析之前还需要对源代码中的宏定义、文件包含、条件编译等命令进行处理。这三类的指令很常见主要有#define, #include和#ifdef ... #endif要特别地注意它们的用法。更多预处理的指令请查阅相关资料 #define除了可以独立使用以便灵活设置一些参数外还常常和#ifdef ... #endif结合使用以便灵活地控制代码块的编译与否也可以用来避免同一个头文件的多次包含。关于#include貌似比较简单通过man找到某个函数的头文件copy进去加上就okay。这里虽然只关心一些技巧不过预处理还是蕴含着很多潜在的陷阱可参考C Traps Pitfalls我们也需要注意的。下面仅介绍和预处理相关的几个简单内容。 打印出预处理之后的结果gcc -E hello.c 这样我们就可以看到源代码中的各种预处理命令是如何被解释的从而方便理解和查错。 实际上gcc在这里是调用了cpp的(虽然我们通过gcc的-v仅看到cc1)cpp即The C Preprocessor主要用来预处理宏定义、文件包含、条件编译等。下面介绍它的一个比较重要的选项-D。 在命令行定义宏gcc -Dmacro hello.c 等同于在文件的开头定义宏即#define maco但是在命令行定义更灵活。例如在源代码中有这些语句。 #ifdef DEBUG printf(this code is for debugging/n); #endif 如果编译时加上-DDEBUG选项那么编译器就会把printf所在的行编译进目标代码从而方便地跟踪该位置的某些程序状态。这样-DDEBUG就可以当作一个调试开关编译时加上它就可以用来打印调试信息发布时则可以通过去掉该编译选项把调试信息去掉。 本节参考资料 [1] C语言教程第九章:预处理 http://www.bc-cn.net/Article/kfyy/cyy/jc/200409/9.html [2] 更多 http://www.hemee.com/kfyy/c/6626.html http://www.91linux.com/html/article/program/cpp/20071203/8745.html http://www.janker.org/bbs/programmer/2006-10-13/327.html 2、编译翻译 开篇简要编译之前C语言编译器会进行词法分析、语法分析(-fsyntax-only)接着会把源代码翻译成中间语言即汇编语言。如果想看到这个中间结果可以用-S选项。需要提到的是诸如shell等解释语言也会经历一个词法分析和语法分析的阶段不过之后并不会进行“翻译”而是“解释”边解释边执行。 ************************ A、解释程序 所谓解释程序是高级语言翻译程序的一种它将源语言(如BASIC)书写的源程序作为输入解释一句后就提交计算机执行一句并不形成目标程序。就像外语翻译中的“口译”一样说一句翻一句不产生全文的翻译文本。这种工作方式非常适合于人通过终端设备与计算机会话如在终端上打一条命令或语句解释程序就立即将此语句解释成一条或几条指令并提交硬件立即执行且将执行结果反映到终端从终端把命令打入后就能立即得到计算结果。这的确是很方便的很适合于一些小型机的计算问题。但解释程序执行速度很慢例如源程序中出现循环则解释程序也重复地解释并提交执行这一组语句这就造成很大浪费。 B、编译程序 这是一类很重要的语言处理程序它把高级语言(如FORTRAN、COBOL、Pascal、C等)源程序作为输入进行翻译转换产生出机器语言的目标程序然后再让计算机去执行这个目标程序得到计算结果。 编译程序工作时先分析后综合从而得到目标程序。所谓分析是指词法分析和语法分析所谓综合是指代码优化存储分配和代码生成。为了完成这些分析综合任务编译程序采用对源程序进行多次扫描的办法每次扫描集中完成一项或几项任务也有一项任务分散到几次扫描去完成的。下面举一个四遍扫描的例子第一遍扫描做词法分析第二遍扫描做语法分析第三遍扫描做代码优化和存储分配第四遍扫描做代码生成。 值得一提的是大多数的编译程序直接产生机器语言的目标代码形成可执行的目标文件但也有的编译程序则先产生汇编语言一级的符号代码文件然后再调用汇编程序进行翻译加工处理最后产生可执行的机器语言目标文件。 在实际应用中对于需要经常使用的有大量计算的大型题目采用招待速度较快的编译型的高级语言较好虽然编译过程本身较为复杂但一旦形成目标文件以后可多次使用。相反对于小型题目或计算简单不太费机时的题目则多选用解释型的会话式高级语言如BASIC这样可以大大缩短编程及调试的时间 ************************ 把源代码翻译成汇编语言实际上是编译的整个过程中的第一个阶段之后的阶段和汇编语言的开发过程没有什么区别。这个阶段涉及到对源代码的词法分析、语法检查通过-std指定遵循哪个标准并根据优化(-O)要求进行翻译成汇编语言的动作。 如果仅仅希望进行语法检查可以用-fsyntax-only选项而为了使代码有比较好的移植性避免使用gcc的一些特性可以结合-std和 -pedantic或者-pedantic-erros)选项让源代码遵循某个C语言标准的语法。这里演示一个简单的例子。 $ cat hello.c #include stdio.h int main() { printf(hello, world/n) return 0; } $ gcc -fsyntax-only hello.c hello.c: In function ‘main’: hello.c:5: error: expected ‘;’ before ‘return’ $ vim hello.c $ cat hello.c #include stdio.h int main() { printf(hello, world/n); int i; return 0; } $ gcc -stdc89 -pedantic-errors hello.c #默认情况下gcc是允许在程序中间声明变量的但是turboc就不支持 hello.c: In function ‘main’: hello.c:5: error: ISO C90 forbids mixed declarations and code 语法错误是程序开发过程中难以避免的错误人的大脑在很多条件下都容易开小差不过编译器往往能够通过语法检查快速发现这些错误并准确地告诉你语法错误的大概位置。因此作为开发人员要做的事情不是“恐慌”不知所措而是认真阅读编译器的提示根据平时积累的经验最好在大脑中存一份常见语法错误索引很多资料都提供了常见语法错误列表如C TrapsPitfalls和最后面的参考资料[12]也列出了很多常见问题和编辑器提供的语法检查功能语法加亮、括号匹配提示等快速定位语法出错的位置并进行修改。 语法检查之后就是翻译动作gcc提供了一个优化选项-O以便根据不同的运行平台和用户要求产生经过优化的汇编代码。例如 $ gcc -o hello hello.c #采用默认选项不优化 $ gcc -O2 -o hello2 hello.c #优化等次是2 $ gcc -Os -o hellos hello.c #优化目标代码的大小 $ ls -S hello hello2 hellos #可以看到hellos比较小,hello2比较大 hello2 hello hellos $ time ./hello hello, world real 0m0.001s user 0m0.000s sys 0m0.000s $ time ./hello2 #可能是代码比较少的缘故执行效率看上去不是很明显 hello, world real 0m0.001s user 0m0.000s sys 0m0.000s $ time ./hellos #虽然目标代码小了但是执行效率慢了些 hello, world real 0m0.002s user 0m0.000s sys 0m0.000s 根据上面的简单演示可以看出gcc有很多不同的优化选项主要看用户的需求了目标代码的大小和效率之间貌似存在一个“纠缠”需要开发人员自己权衡。 下面我们通过-S选项来看看编译出来的中间结果汇编语言还是以之前那个hello.c为例。 $ gcc -S hello.c #默认输出是hello.s可自己指定输出到屏幕-o -输出到其他文件-o file $ cat hello.s cat hello.s .file hello.c .section .rodata .LC0: .string hello, world .text .globl main .type main, function main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $4, %esp movl $.LC0, (%esp) call puts movl $0, %eax addl $4, %esp popl %ecx popl %ebp leal -4(%ecx), %esp ret .size main, .-main .ident GCC: (GNU) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2) .section .note.GNU-stack,,progbits 不知道看出来没和我们在课堂里学的intel的汇编语法不太一样这里用的是ATT语法格式。如果之前没接触过ATT的可以看看参考资料[2]。如果想学习Linux下的汇编语言开发从下一节开始哦下一节开始的所有章节基本上覆盖了Linux下汇编语言开发的一般过程不过这里不介绍汇编语言语法。 这里需要补充的是在写C语言代码时如果能够对编译器比较熟悉工作原理和一些细节的话可能会很有帮助。包括这里的优化选项(有些优化选项可能在汇编时采用)和可能的优化措施例如字节对齐(可以看看这本书Linux_Assembly_Language_Programming的第六小节)、条件分支语句裁减(删除一些明显分支)等。 本节参考资料 [1] Guide to Assembly Language Programming in Linux(pdf教程社区有下载) http://oss.lzu.edu.cn/modules/wfdownloads/singlefile.php?cid5lid94 [2] Linux汇编语言开发指南在线 http://www.ibm.com/developerworks/cn/linux/l-assembly/index.html [3] PowerPC 汇编 http://www.ibm.com/developerworks/cn/linux/hardware/ppc/assembly/index.html [4] 用于 Power 体系结构的汇编语言 http://www.ibm.com/developerworks/cn/linux/l-powasm1.html [5] Linux Assembly HOWTO http://mirror.lzu.edu.cn/tldp/HOWTO/Assembly-HOWTO/ [6] Linux 中 x86 的内联汇编 http://www.ibm.com/developerworks/cn/linux/sdk/assemble/inline/index.html [7] Linux Assembly Language Programming http://mirror.lzu.edu.cn/doc/incoming/ebooks/linux-unix/Linux_EN_Original_Books 3、汇编 开篇这里实际上还是翻译过程只不过把作为中间结果的汇编代码翻译成了机器代码即目标代码不过它还不可以运行。如果要产生这一中间结果可用gcc的-c选项当然也可通过as命令_汇编_汇编语言源文件来产生。 汇编是把汇编语言翻译成目标代码的过程在学习汇编语言开发时大家应该比较熟悉nasm汇编工具(支持Intel格式的汇编语言)了不过这里主要用 as汇编工具来汇编ATT格式的汇编语言因为gcc产生的中间代码就是ATT格式的。下面来演示分别通过gcc的-c选项和as来产生目标代码。 Quote: $ file hello.s hello.s: ASCII assembler program text $ gcc -c hello.s #用gcc把汇编语言编译成目标代码 $ file hello.o #file命令可以用来查看文件的类型这个目标代码是可重定位的(relocatable)需要通过ld进行进一步的链接成可执行程序(executable)和共享库(shared) hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped $ as -o hello.o hello.s #用as把汇编语言编译成目标代码 $ file hello.o hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped gcc和as默认产生的目标代码都是ELF格式[6]的因此这里主要讨论ELF格式的目标代码(如果有时间再回顾一下a.out和coff格式当然你也可以参考资料[15]自己先了解一下并结合objcopy来转换它们比较异同)。 目标代码不再是普通的文本格式无法直接通过文本编辑器浏览需要一些专门的工具。如果想了解更多目标代码的细节区分relocatable可重定位、executable可执行、shared libarary共享库的不同我们得设法了解目标代码的组织方式和相关的阅读和分析工具。下面我们主要介绍这部分内容。 BFD is a package which allows applications to use the same routines to operate on object files whatever the object file format. A new object file format can be supported simply by creating a new BFD back end and adding it to the library.[24][25]。 binutils(GNU Binary Utilities)的很多工具都采用这个库来操作目标文件这类工具有objdump,objcopy,nm,strip等(当然你也可以利用它。如果你深入了解ELF格式那么通过它来分析和编写Virus程序将会更加方便)不过另外一款非常优秀的分析工具readelf并不是基于这个库所以你也应该可以直接用elf.h头文件中定义的相关结构来操作ELF文件。 下面将通过这些辅助工具(主要是readelf和objdump可参考本节最后列出的资料[4])结合ELF手册[6](建议看第三篇中文版)来分析它们。 下面大概介绍ELF文件的结构和三种不同类型ELF文件的区别。 ELF文件的结构 ELF Header(ELF文件头) Porgram Headers Table(程序头表实际上叫段表好一些用于描述可执行文件和可共享库) Section 1 Section 2 Section 3 ... Section Headers Table(节区头部表用于链接可重定位文件成可执行文件或共享库) 对于可重定位文件程序头是可选的而对于可执行文件和共享库文件动态连接库节区表则是可选的。这里的可选是指没有也可以。可以分别通过 readelf文件的-h-l和-S参数查看ELF文件头(ELF Header)、程序头部表Program Headers Table段表和节区表(Section Headers Table)。 文件头说明了文件的类型大小运行平台节区数目等。先来通过文件头看看不同ELF的类型。为了说明问题先来几段代码吧。 Code: /* myprintf.c */ #include stdio.h void myprintf(void) { printf(hello, world!/n); } [CtrlA Select All] Code: /* myprintf function declaration */ #ifndef _TEST_H_ #define _TEST_H_ void myprintf(void); #endif [CtrlA Select All] Code: /* test.c */ #include test.h int main() { myprintf(); return 0; } [CtrlA Select All] 下面通过这几段代码来演示通过readelf -h参数查看ELF的不同类型。期间将演示如何创建动态连接库(即可共享文件)、静态连接库并比较它们的异同。 Quote: $ gcc -c myprintf.c test.c #编译产生两个目标文件myprintf.o和test.o它们都是可重定位文件(REL) $ readelf -h test.o | grep Type Type: REL (Relocatable file) $ readelf -h myprintf.o | grep Type Type: REL (Relocatable file) $ gcc -o test myprintf.o test.o #根据目标代码连接产生可执行文件这里的文件类型是可执行的(EXEC) $ readelf -h test | grep Type Type: EXEC (Executable file) $ ar rcsv libmyprintf.a myprintf.o #用ar命令创建一个静态连接库静态连接库也是可重定位文件(REL) $ readelf -h libmyprintf.a | grep Type #因此使用静态连接库和可重定位文件一样它们之间唯一不 #同是前者可以是多个可重定位文件的“集合”。 Type: REL (Relocatable file) $ gcc -o test test.o -llib -L./ #可以直接连接进去也可以使用-l参数-L指定库的搜索路径 $ gcc -Wall myprintf.o -shared -Wl,-soname,libmyprintf.so.0 -o libmyprintf.so.0.0 #编译产生动态链接库并支持major和minor版本号动态链接库类型为DYN $ ln -sf libmyprintf.so.0.0 libmyprintf.so.0 $ ln -sf libmyprintf.so.0 libmyprintf.so $ readelf -h libmyprintf.so | grep Type Type: DYN (Shared object file) $ gcc -o test test.o -llib -L./ #编译时和静态连接库类似但是执行时需要指定动态连接库的搜索路径 $ LD_LIBRARY_PATH./ ./test #LD_LIBRARY_PATH为动态链接库的搜索路径 $ gcc -static -o test test.o -llib -L./ #在不指定static时会优先使用动态链接库指定时则阻止使用动态连接库 #这个时候会把所有静态连接库文件加入到可执行文件中使得执行文件很大 #而且加载到内存以后会浪费内存空间因此不建议这么做 经过上面的演示基本可以看出它们之间的不同。可重定位文件本身不可以运行仅仅是作为可执行文件、静态连接库也是可重定位文件、动态连接库的 “组件”。静态连接库和动态连接库本身也不可以执行作为可执行文件的“组件”它们两者也不同前者也是可重定位文件只不过可能是多个可重定位文件的集合并且在连接时加入到可执行文件中去而动态连接库在连接时库文件本身并没有添加到可执行文件中只是在可执行文件中加入了该库的名字等信息以便在可执行文件运行过程中引用库中的函数时由动态连接器去查找相关函数的地址并调用它们。从这个意义上说动态连接库本身也具有可重定位的特征含有可重定位的信息。对于什么是重定位如何进行静态符号和动态符号的重定位我们将在链接部分和《动态符号链接的细节》一节介绍。 下面来看看ELF文件的主体内容节区Section)。ELF文件具有很大的灵活性它通过文件头组织整个文件的总体结构通过节区表 (Section Headers Table)和程序头Program Headers Table或者叫段表)来分别描述可重定位文件和可执行文件。但不管是哪种类型它们都需要它们的主体即各种节区。在可重定位文件中节区表描述的就是各种节区本身而在可执行文件中程序头描述的是由各个节区组成的段Segment以便程序运行时动态装载器知道如何对它们进行内存映像从而方便程序加载和运行。 下面先来看看一些常见的节区而关于这些节区(section)如何通过重定位构成成不同的段(Segments)以及有哪些常规的段我们将在链接部分进一步介绍。 可以通过readelf的-S参数查看ELF的节区。建议一边操作一边看文档以便加深对ELF文件结构的理解先来看看可重定位文件的节区信息通过节区表来查看 Quote: $ gcc -c myprintf.c #默认编译好myprintf.c将产生一个可重定位的文件myprintf.o $ readelf -S myprintf.o #通过查看myprintf.o的节区表查看节区信息 There are 11 section headers, starting at offset 0xc0: 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 000018 00 AX 0 0 4 [ 2] .rel.text REL 00000000 000334 000010 08 9 1 4 [ 3] .data PROGBITS 00000000 00004c 000000 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 00004c 000000 00 WA 0 0 4 [ 5] .rodata PROGBITS 00000000 00004c 00000e 00 A 0 0 1 [ 6] .comment PROGBITS 00000000 00005a 000012 00 0 0 1 [ 7] .note.GNU-stack PROGBITS 00000000 00006c 000000 00 0 0 1 [ 8] .shstrtab STRTAB 00000000 00006c 000051 00 0 0 1 [ 9] .symtab SYMTAB 00000000 000278 0000a0 10 10 8 4 [10] .strtab STRTAB 00000000 000318 00001a 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) $ objdump -d -j .text myprintf.o #这里是程序指令部分用objdump的-d选项可以看到反编译的结果 #-j指定需要查看的节区 myprintf.o: file format elf32-i386 Disassembly of section .text: 00000000 myprintf: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 ec 0c sub $0xc,%esp 9: 68 00 00 00 00 push $0x0 e: e8 fc ff ff ff call f myprintf0xf 13: 83 c4 10 add $0x10,%esp 16: c9 leave 17: c3 ret $ readelf -r myprintf.o #用-r选项可以看到有关重定位的信息这里有两部分需要重定位 Relocation section .rel.text at offset 0x334 contains 2 entries: Offset Info Type Sym.Value Sym. Name 0000000a 00000501 R_386_32 00000000 .rodata 0000000f 00000902 R_386_PC32 00000000 puts $ readelf -x .rodata myprintf.o #.rodata节区包含只读数据即我们要打印的hello, world!. Hex dump of section .rodata: 0x00000000 68656c6c 6f2c2077 6f726c64 2100 hello, world!. $ readelf -x .data myprintf.o #没有这个节区,.data应该包含一些初始化的数据 Section .data has no data to dump. $ readelf -x .bss mmyprintf.o #也没有这个节区.bss应该包含一些未初始化的数据程序默认初始为0 Section .bss has no data to dump. $ readelf -x .comment myprintf.o #是一些注释可以看到是是GCC的版本信息 Hex dump of section .comment: 0x00000000 00474343 3a202847 4e552920 342e312e .GCC: (GNU) 4.1. 0x00000010 3200 2. $ readelf -x .note.GNU-stack myprintf.o #这个也没有内容 Section .note.GNU-stack has no data to dump. $ readelf -x .shstrtab myprintf.o #包括所有节区的名字 Hex dump of section .shstrtab: 0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab 0x00000010 002e7368 73747274 6162002e 72656c2e ..shstrtab..rel. 0x00000020 74657874 002e6461 7461002e 62737300 text..data..bss. 0x00000030 2e726f64 61746100 2e636f6d 6d656e74 .rodata..comment 0x00000040 002e6e6f 74652e47 4e552d73 7461636b ..note.GNU-stack 0x00000050 00 . $ readelf -symtab myprintf.o #符号表包括所有用到的相关符号信息如函数名、变量名 Symbol table .symtab contains 10 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS myprintf.c 2: 00000000 0 SECTION LOCAL DEFAULT 1 3: 00000000 0 SECTION LOCAL DEFAULT 3 4: 00000000 0 SECTION LOCAL DEFAULT 4 5: 00000000 0 SECTION LOCAL DEFAULT 5 6: 00000000 0 SECTION LOCAL DEFAULT 7 7: 00000000 0 SECTION LOCAL DEFAULT 6 8: 00000000 24 FUNC GLOBAL DEFAULT 1 myprintf 9: 00000000 0 NOTYPE GLOBAL DEFAULT UND puts $ readelf -x .strtab myprintf.o #字符串表用到的字符串包括文件名、函数名、变量名等。 Hex dump of section .strtab: 0x00000000 006d7970 72696e74 662e6300 6d797072 .myprintf.c.mypr 0x00000010 696e7466 00707574 7300 intf.puts. 从上表可以看出对于可重定位文件会包含这些基本节区.text, .rel.text, .data, .bss, .rodata, .comment, .note.GNU-stack, .shstrtab, .symtab和.strtab。为了进一步理解这些节区和源代码的关系这里来看一看myprintf.c产生的汇编代码。 Quote: $ gcc -S myprintf.c $ cat myprintf.s .file myprintf.c .section .rodata .LC0: .string hello, world! .text .globl myprintf .type myprintf, function myprintf: pushl %ebp movl %esp, %ebp subl $8, %esp subl $12, %esp pushl $.LC0 call puts addl $16, %esp leave ret .size myprintf, .-myprintf .ident GCC: (GNU) 4.1.2 .section .note.GNU-stack,,progbits 是不是可以从中看出可重定位文件中的那些节区和汇编语言代码之间的关系在上面的可重定位文件可以看到有一个可重定位的节区即. rel.text它标记了两个需要重定位的项.rodata和puts。这个节区将告诉编译器这两个信息在链接或者动态链接的过程中需要重定位具体如何重定位将根据重定位项的类型比如上面的R_386_32和R_386_PC32(关于这些类型的更多细节请查看ELF手册[6])。 到这里对可重定位文件应该有了一个基本的了解下面将介绍什么是可重定位可重定位文件到底是如何被链接生成可执行文件和动态连接库的这个过程除了进行了一些符号的重定位外还进行了哪些工作呢 本节参考资料 [1] 了解编译程序的过程 http://9iyou.com/Program_Data/linuxunix-3125.html http://www.host01.com/article/server/00070002/0621409075078127.htm [2] C track: compiling C programs. http://www.cs.caltech.edu/courses/cs11/material/c/mike/misc/compiling_c.html [3] Dissecting shared libraries http://www.ibm.com/developerworks/linux/library/l-shlibs.html 4、链接 开篇重定位是将符号引用与符号定义进行链接的过程。因此链接是处理可重定位文件把它们的各种符号引用和符号定义转换为可执行文件中的合适信息(一般是虚拟内存地址)的过程。链接又分为静态链接和动态链接前者是程序开发阶段程序员用ld(gcc实际上在后台调用了ld)静态链接器手动链接的过程而动态链接则是程序运行期间系统调用动态链接器(ld-linux.so)自动链接的过程。比如如果链接到可执行文件中的是静态连接库libmyprintf.a那么. rodata节区在链接后需要被重定位到一个绝对的虚拟内存地址以便程序运行时能够正确访问该节区中的字符串信息。而对于puts因为它是动态连接库libc.so中定义的函数所以会在程序运行时通过动态符号链接找出puts函数在内存中的地址以便程序调用该函数。在这里主要讨论静态链接过程动态链接过程见《动态符号链接的细节》。 静态链接过程主要是把可重定位文件依次读入分析各个文件的文件头进而依次读入各个文件的节区并计算各个节区的虚拟内存位置对一些需要重定位的符号进行处理设定它们的虚拟内存地址等并最终产生一个可执行文件或者是动态链接库。这个链接过程是通过ld来完成的ld在链接时使用了一个链接脚本linker script该链接脚本处理链接的具体细节。由于静态符号链接过程非常复杂特别是计算符号地址的过程考虑到时间关系相关细节请参考ELF手册[6]。这里主要介绍可重定位文件中的节区节区表描述的和可执行文件中段程序头描述的的对应关系以及gcc编译时采用的一些默认链接选项。 下面先来看看可执行文件的节区信息通过程序头段表来查看 Quote: $ readelf -S test.o #为了比较先把test.o的节区表也列出 There are 10 section headers, starting at offset 0xb4: 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 000024 00 AX 0 0 4 [ 2] .rel.text REL 00000000 0002ec 000008 08 8 1 4 [ 3] .data PROGBITS 00000000 000058 000000 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 000058 000000 00 WA 0 0 4 [ 5] .comment PROGBITS 00000000 000058 000012 00 0 0 1 [ 6] .note.GNU-stack PROGBITS 00000000 00006a 000000 00 0 0 1 [ 7] .shstrtab STRTAB 00000000 00006a 000049 00 0 0 1 [ 8] .symtab SYMTAB 00000000 000244 000090 10 9 7 4 [ 9] .strtab STRTAB 00000000 0002d4 000016 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) $ gcc -o test test.o libmyprintf.o $ readelf -l test #我们发现test和test.o,libmyprintf.o相比多了很多节区如.interp和.init等 Elf file type is EXEC (Executable file) Entry point 0x80482b0 There are 7 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4 INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x0047c 0x0047c R E 0x1000 LOAD 0x00047c 0x0804947c 0x0804947c 0x00104 0x00108 RW 0x1000 DYNAMIC 0x000490 0x08049490 0x08049490 0x000c8 0x000c8 RW 0x4 NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame 03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag 06 上表给出了可执行文件的如下几个段(segment) PHDR: 给出了程序表自身的大小和位置不能出现一次以上。 INTERP: 因为程序中调用了puts在动态链接库中定义使用了动态连接库因此需要动态装载器链接器(ld-linux.so) LOAD: 包括程序的指令.text等节区都映射在该段只读(R) LOAD: 包括程序的数据.data, .bss等节区都映射在该段可读写(RW) DYNAMIC: 动态链接相关的信息比如包含有引用的动态连接库名字等信息 NOTE: 给出一些附加信息的位置和大小 GNU_STACK: 这里为空应该是和GNU相关的一些信息 这里的段可能包括之前的一个或者多个节区也就是说经过链接之后原来的节区被重排了并映射到了不同的段这些段将告诉系统应该如何把它加载到内存中。 从上表中通过比较可执行文件(test)中拥有的节区和可重定位文件(test.o和myprintf.o)中拥有的节区后发现链接之后多了一些之前没有的节区这些新的节区来自哪里它们的作用是什么呢先来通过gcc的-v参数看看它的后台链接过程。 Quote: $ gcc -v -o test test.o myprintf.o #把可重定位文件链接成可执行文件 Reading specs from /usr/lib/gcc/i486-slackware-linux/4.1.2/specs Target: i486-slackware-linux Configured with: ../gcc-4.1.2/configure --prefix/usr --enable-shared --enable-languagesada,c,c,fortran,java,objc --enable-threadsposix --enable-__cxa_atexit --disable-checking --with-gnu-ld --verbose --with-archi486 --targeti486-slackware-linux --hosti486-slackware-linux Thread model: posix gcc version 4.1.2 /usr/libexec/gcc/i486-slackware-linux/4.1.2/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crt1.o /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2/../../../../i486-slackware-linux/lib -L/usr/lib/gcc/i486-slackware-linux/4.1.2/../../.. test.o myprintf.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crtn.o 从上边的演示看出gcc在连接了我们自己的目标文件test.o和myprintf.o之外还连接了crt1.o,crtbegin.o等额外的目标文件难道那些新的节区就来自这些文件 另外gcc在进行了相关配置(./configure)后调用了collect2却并没有调用ld通过查找gcc文档中和collect2相关的部分发现collect2在后台实际上还是去寻找ld命令的。为了理解gcc默认连接的后台细节这里直接把collect2替换成ld并把一些路径换成绝对路径或者简化得到如下的ld命令以及执行的效果。 Quote: $ ld --eh-frame-hdr / -m elf_i386 / -dynamic-linker /lib/ld-linux.so.2 / -o test / /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o / test.o myprintf.o / -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/i486-slackware-linux/lib -L/usr/lib/ -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed / /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o $ ./test hello, world! 不出我们所料它完美的运行了。下面通过ld的手册(man ld)来分析一下这几个参数。 --eh-frame-hdr 要求创建一个.eh_frame_hdr节区(貌似目标文件test中并没有这个节区所以不关心它)。 Quote: $ ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o /usr/lib/crti.o test.o myprintf.o -L/usr/lib -lc /usr/lib/crtn.o #后面发现不用链接libgcc也不用--eh-frame-hdr参数 $ readelf -l test Elf file type is EXEC (Executable file) Entry point 0x80482b0 There are 7 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4 INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x003ea 0x003ea R E 0x1000 LOAD 0x0003ec 0x080493ec 0x080493ec 0x000e8 0x000e8 RW 0x1000 DYNAMIC 0x0003ec 0x080493ec 0x080493ec 0x000c8 0x000c8 RW 0x4 NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata 03 .dynamic .got .got.plt .data 04 .dynamic 05 .note.ABI-tag 06 $ ./test hello, world! Quote: $ ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o test.o myprintf.o -L/usr/lib/ -lc /usr/lib/libc_nonshared.a(elf-init.oS): In function __libc_csu_init: (.text0x25): undefined reference to _init Quote: $ readelf -s /usr/lib/crt1.o | grep __libc_csu_init 18: 00000000 0 NOTYPE GLOBAL DEFAULT UND __libc_csu_init $ readelf -s /usr/lib/crti.o | grep _init 17: 00000000 0 FUNC GLOBAL DEFAULT 5 _init Quote: $ ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib/ -lc ld: warning: cannot find entry symbol _start; defaulting to 00000000080481a4 Quote: $ ./test hello, world! Segmentation fault Quote: $ gcc -g -c test.c myprintf.c #产生目标代码, 非交叉编译不指定-m也可以链接成功所以下面可以去掉-m参数 $ ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib -lc ld: warning: cannot find entry symbol _start; defaulting to 00000000080481d8 $ ./test hello, world! Segmentation fault $ gdb ./test ... (gdb) l 1 #include test.h 2 3 int main() 4 { 5 myprintf(); 6 return 0; 7 } (gdb) break 7 #在程序的末尾设置一个断点 Breakpoint 1 at 0x80481bf: file test.c, line 7. (gdb) r #程序都快结束了都没问题怎么会到最后出个问题呢 Starting program: /mnt/hda8/Temp/c/program/test hello, world! Breakpoint 1, main () at test.c:7 7 } (gdb) n #单步执行看看怎么下面一条指令是0x00000001肯定是程序退出以后出了问题 0x00000001 in ?? () (gdb) n #诶当然找不到边了都跑到0x00000001了 Cannot find bounds of current function (gdb) c #原来是这么回事估计是return 0返回之后出问题了看看它的汇编去。 Continuing. Program received signal SIGSEGV, Segmentation fault. 0x00000001 in ?? () $ gcc -S test.c #产生汇编代码 $ cat test.s #后面就这么几条指令难不成ret返回有问题不让它ret返回把return改成_exit直接进入内核退出 ... call myprintf movl $0, %eax addl $4, %esp popl %ecx popl %ebp leal -4(%ecx), %esp ret ... $ vim test.c $ cat test.c #就把return语句修改成_exit了。 #include test.h #include unistd.h /* _exit */ int main() { myprintf(); _exit(0); } $ gcc -g -c test.c myprintf.c $ ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib -lc ld: warning: cannot find entry symbol _start; defaulting to 00000000080481d8 $ ./test #竟然好了再看看汇编有什么不同 hello, world! $ gcc -S test.c $ cat test.s #貌似就把ret指令替换成了_exit函数调用直接进入内核然内核让处理了那为什么ret有问题呢 ... call myprintf subl $12, %esp pushl $0 call _exit ... $ gdb ./test #把代码改回去改成return 0;再调试看看调用main函数返回时的下一条指令地址eip ... (gdb) l warning: Source file is more recent than executable. 1 #include test.h 2 3 int main() 4 { 5 myprintf(); 6 return 0; 7 } (gdb) break 5 Breakpoint 1 at 0x80481b5: file test.c, line 5. (gdb) break 7 Breakpoint 2 at 0x80481bc: file test.c, line 7. (gdb) r Starting program: /mnt/hda8/Temp/c/program/test Breakpoint 1, main () at test.c:5 5 myprintf(); (gdb) x/8x $esp #发现0x00000001刚好是之前我们调试时看到的程序返回后的位置即eip说明程序在初始化的时候 #这个eip就是错误的。为什么呢因为我们根本没有链接进来初始化的代码而是在编译器自己给我们 #初始化了一个程序入口即00000000080481d8也就是说没有任何人调用main,main不知道返回哪里去 #所以我们直接让main结束时进入内核调用_exit而退出则不会有问题 0xbf929510: 0xbf92953c 0x080481a4 0x00000000 0xb7eea84f 0xbf929520: 0xbf92953c 0xbf929534 0x00000000 0x00000001 Quote: $ ld --verbose | grep ^ENTRY #非交叉编译可不用-m参数ld默认找_start入口并不是main哦 ENTRY(_start) Quote: $ cat test.c #include test.h #include unistd.h /* _exit */ int main() { myprintf(); _exit(0); } $ gcc -S test.c $ sed -i -e s#main#_start#g test.s #把汇编中的main全部修改为_start即修改程序入口为_start $ gcc -c test.s myprintf.c $ ld -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib/ -lc #果然没问题了 :-) $ ./test hello, world! Quote: $ ld --verbose | grep PROVIDE | grep -v HIDDEN PROVIDE (__executable_start 0x08048000); . 0x08048000 SIZEOF_HEADERS; PROVIDE (__etext .); PROVIDE (_etext .); PROVIDE (etext .); _edata .; PROVIDE (edata .); _end .; PROVIDE (end .); 这里面有几个我们比较关心的第一个是程序的入口地址__executable_start另外三个是etextedataend分别对应程序的代码段(text)、初始化数据(data)和未初始化的数据(bss)可以参考资料[6]和man etext如何引用这些变量呢看看这个例子。 Code: /*predefinevalue.c */ #include stdio.h extern int
__executable_start, etext, edata, end; int main () { printf (program
entry: 0x%x /n, __executable_start); printf (etext address(textsegment): 0x%x /n, etext); printf (edata address(initilized
data): 0x%x /n, edata); printf (end address(uninitilized data):0x%x /n, end); return 0; } [CtrlA Select All] 到这里程序链接过程的一些细节都介绍得差不多了。在《动态符号链接的细节》中将主要介绍ELF文件的动态符号链接过程。 本节参考资料 [1] An beginners guide to compiling programs under Linux. http://www.luv.asn.au/overheads/compile.html [2] gcc manual http://gcc.gnu.org/onlinedocs/gcc-4.2.2/gcc/ [3] A Quick Tour of Compiling, Linking, Loading, and Handling Libraries on Unix http://efrw01.frascati.enea.it/Software/Unix/IstrFTU/cern-cnl-2001-003-25-link.html [4] Unix 目标文件初探 http://www.ibm.com/developerworks/cn/aix/library/au-unixtools.html [5] Before main()分析 http://www.xfocus.net/articles/200109/269.html [6] A Process Viewing Its Own /proc/PID/map Information http://www.linuxforums.org/forum/linux-kernel/51790-process-viewing-its-own-proc-pid-map-information.html _start竟然是真正的程序入口那在有main的情况下呢为什么在_start之后能够找到main呢这个看看alert7大叔的Before main分析[5]吧这里不再深入介绍。总之呢通过修改程序的return语句为_exit(0)和修改程序的入口为_start我们的代码不链接gcc默认链接的那些额外的文件同样可以工作得很好。并且打破了一个学习C语言以来的常识main函数作为程序的主函数是程序的入口实际上则不然。 再补充一点内容在ld的链接脚本中有一个特别的关键字PROVIDE由这个关键字定义的符号是ld的预定义字符我们可以在C语言函数中扩展它们后直接使用。这些特别的符号可以通过下面的方法获取 原来是这样程序的入口(entry)竟然不是main函数而是_start。那干脆把汇编里头的main给改掉算了看行不行 通过上面的演示和解释发现只要把return语句修改为_exit语句程序即使不链接任何额外的目标代码都可以正常运行原因是不连接那些额外的文件时相当于没有进行初始化操作如果在程序的最后执行ret汇编指令程序将无法获得正确的eip从而无法进行后续的动作。但是为什么会有“找不到 _start符号”的警告呢通过readelf -s查看crt1.o发现里头有这个符号并且crt1.o引用了main这个符号是不是意味着会从_start进入main呢是不是程序入口是 _start而并非main呢 先来看看刚才提到的链接器的默认链接脚本(ld -m elf_386 --verbose)它告诉我们程序的入口(entry)是_start而一个可执行文件必须有一个入口地址才能运行所以这就是说明了为什么ld一定要提示我们“_start找不到”找不到以后就给默认设置了一个地址。 貌似程序运行完了不过结束时冒出个段错误可能是程序结束时有问题用gdb调试看看 这样却说没有找到入口符号_start难道crt1.o中定义了这个符号不过它给默认设置了一个地址只是个警告说明test已经生成不管怎样先运行看看再说。 竟然是crt1.o调用了__libc_csu_init函数而该函数却引用了我们没有链接的crti.o文件中定义的_init符号。这样的话不链接 crti.o和crtn.o文件就不成了罗不对吧要不干脆不用crt1.o算了看看gcc额外连接进去的最后一个文件crt1.o到底干了个啥子 貌似不行竟然有人调用了__libc_csu_init函数而这个函数引用了_init。这两个符号都在哪里呢 完全可以工作而且发现.ctors(保存着程序中全局构造函数的指针数组), .dtors保存着程序中全局析构函数的指针数组,.jcr未知,.eh_frame节区都没有了所以crtbegin.o和crtend.o应该包含了这些节区。 而对于另外两个文件crti.o和crtn.o通过readelf -S查看后发现它们都有.init和.fini节区如果我们不需要让程序进行一些初始化和清理工作呢是不是就可以不链接这个两个文件试试看。 -m elf_i386 这里指定不同平台上的链接脚本可以通过--verbose命令查看脚本的具体内容如ld -m elf_i386 --verbose它实际上被存放在一个文件中(/usr/lib/ldscripts目录下你可以去修改这个脚本具体如何做请参考ld的手册。在后面我们将简要提到链接脚本中是如何预定义变量的以及这些预定义变量如何在我们的程序中使用。需要提到的是如果不是交叉编译那么无须指定该选项。 -dynamic-linker /lib/ld-linux.so.2 指定动态装载器/链接器即程序中的INTERP段中的内容。动态装载器/连接器负责连接有可共享库的可执行文件的装载和动态符号连接。 -o test 指定输出文件即可执行文件名的名字 /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o 链接到test文件开头的一些内容这里实际上就包含了.init等节区。.init节区包含一些可执行代码在main函数之前被调用以便进行一些初始化操作在C中完成构造函数功能更多细节请参考资料[9] test.o myprintf.o 链接我们自己的可重定位文件 -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/i486-slackware-linux/lib -L/usr/lib/ -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed 链接libgcc库和libc库后者定义有我们需要的puts函数 /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o 链接到test文件末尾的一些内容这里实际上包含了.fini等节区。.fini节区包含了一些可执行代码在程序退出时被执行作一些清理工作在C中完成析构造函数功能。我们往往可以通过atexit来注册那些需要在程序退出时才执行的函数。 对于crtbegin.o和crtend.o这两个文件貌似完全是用来支持C的构造和析构工作的[9]所以可以不链接到我们的可执行文件中链接时把它们去掉看看