程序员的自我修养:(1)目标文件
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)
- all header:
- 查看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文件的结构。
/*
* SimpleSection.c
*
* Linux:
* gcc -c SimpleSection.c
*
* Windows:
* cl SimpleSection.c /c /Za
*/
int printf(const char* format, ...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i)
{
printf("%d\n", i);
}
int main(void)