返回 登录
0

程序员的自我修养:(1)目标文件

阅读1060

1.目标文件
1.1 编译与链接

在使用像Visual Studio或Qt Creator等IDE时,通常有一个叫做“构建”的按钮。当编辑完成要运行和测试时点一下它,程序就能跑起来了,所以我们很少关心编译和链接。其实,编译和链接合并在一起就称为 构建(Build)。简单的一次按键,实际背后却是异常复杂的过程:

预编译(Preprocessing)
编译(Compilation)
    扫描:算法类似有限状态机(FSM),将字符转换成Token。
    语法分析:分析Token生成语法树(Syntax Tree)。
    语义分析:静态语义(类型转换)或动态语义分析。
    源代码优化:直接在语法树做优化比较困难,优化器将语法树转换成中间代码。中间代码使得编译器分为前端和后端,前端负责产生机器无关的中间代码,后端将中间代码转换成目标机器代码。
    代码生成:完全依赖目标机器(字长、寄存器、数据类型等),生成目标代码。
    目标代码优化:优化寻址方式、用位移代替乘法运算、删除多余的指令等。
汇编(Assembly)
链接(Linking)
    地址和空间分配(Address/Space Allocation)
    符号解析(Symbol Resolution)
    重定位(Relocation)

1.2 目标文件简介

Object File没有一个很合适的中文名称,书中翻译为目标文件。这一小节介绍有关目标文件的最重要的几个知识:文件类型、段的概念、常用的查看工具。
1.2.1 四种文件类型

目标文件就是源代码编译后未进行链接的中间文件(Windows下的.obj和Linux下的.o),它跟可执行文件的内容和结构相似,所以一般跟可执行文件一起采用一种格式存储。不只是可执行文件,动态链接库和静态链接库(Windows下的.lib、.dll和Linux下的.a、.so)都按照可执行文件的格式存储。以ELF为例,这样ELF就有了下面四种类型:

可重定位文件:可被用来链接成可执行文件或动态链接库的中间文件,静态链接库也可归为这一类。扩展名.o或.a。
可执行文件:可直接执行的程序。ELF可执行文件一般没有扩展名。
共享目标文件:可以在两种情况下使用:1)与其他可重定向文件、共享目标文件链接,形成新的目标文件;2)与可执行文件结合,作为进程映像的一部分。扩展名.so。
核心转储文件:进程意外终止时,系统将进程地址空间的内容及终止时的其他信息转储。典型的是Linux下的core dump。

用file命令可以查看各种文件的类型:

$ file openfile.c
openfile.c: C source, ASCII text, with CRLF line terminators

$ gcc -c openfile.c
$ file openfile.o
openfile.o: ELF 32-bit LSB  relocatable, Intel 80386, version 1 (SYSV), not stripped

$ gcc -o openfile openfile.c
$ file openfile
openfile: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), \for GNU/Linux 2.6.24, BuildID[sha1]=086b5fbe84778f5683f7ef4dbd710fe2837370db, not stripped

$ file /lib/i386-linux-gnu/ld-2.19.so
/lib/i386-linux-gnu/ld-2.19.so: ELF 32-bit LSB  shared object, Intel 80386, version 1 (SYSV), dynamically linked, BuildID[sha1]=12f5bdcd6abd5fa411d5db326afcece6044621c4, stripped

1.2.2 段和段表

讲目标文件是什么样子,先要了解的最重要的一个概念就是 “段”(Segment or Section)。例如,编译后的机器指令被放在代码段.text,全局变量和局部静态变量放在数据段.data。未初始化的全局变量和局部静态变量默认值都为0,本可以都放在.data段,但为了节省空间它们被放在一个.bss段中。ELF的文件头中包含了文件类型、入口地址、目标硬件,以及描述文件中各个段的 段表,后面会详细分析段表的作用。

为什么代码段和数据段要分开?
1)权限:程序装载后,数据和指令被映射到两个区域,可以分别设置为可读写和只读。
2)缓存:指令和数据分离有利于提高程序的局部性,提高缓存命中率。
3)共享:分离后的指令可以在多个进程间共享,节省大量内存。

1.2.3 常用查看命令

之前在六星经典CSAPP-笔记(7)加载与链接(上)中的“2.对象文件查看工具”已经简单列举了学习链接时常用的工具,像nm、objdump、readelf、ldd等。这里重点总结一下objdump和readelf的最常见命令:

查看所有内容:
    readelf -a (-a=–all 相当于-e -r -s)
查看header:
    all header:
        objdump -x (-x=–all-headers 包括file、section header以及符号表和重定位表)
        readelf -e (-e=–headers 包括file、program、section header)
    file header:
        objdump -f (-f=–file-headers)
        readelf -h (-h=–file-header)
    program header:
        readelf -l (-l=–program-header)
    section header:
        objdump -h (-h=–section-headers)
        readelf -S (-S=–section-headers)
        readelf -t (-t=–section-details)
查看section:
    查看数据:
        objdump -s (-s=–full-contents 包括.text、.data、.rodata、.comment的二进制和ASCII码)
    查看代码:
        objdump -d -z -r -l (-d=–disassemble, -z=–disassemble-zeroes, -r=–reloc, -l=–line-numbers 只反汇编.text段)
        objdump -D (-D=–disassemble-all 不只反汇编.text段,也把.data、.bss、.rodata、.comment当成代码反汇编)
        在objdump后加上 | grep -A15 “” 可以查看某个函数的15行反汇编代码
    查看符号表:
        objdump -t (-t=–syms)
        readelf -s (-s=–syms)
    查看重定位表:
        objdump -r (-r=–reloc)
        readelf -r (-r=–relocs)
    查看某section内容:
        objdump -j .text -s/-d (根据section是数据还是代码)

1.3 ELF文件结构详解

书上以一小段代码SimpleSection.c为例,里面包含了各种常见的元素,如外部引用printf()、内部引用func1()、全局变量global_init_var、未初始化的全局变量global_uninit_var、静态变量static_var、未初始化的静态变量static_var2等,详细研究了ELF文件的结构。

评论