目标代码不再是普通的文本格式,无法直接通过文本编辑器浏览,需要一些专门的工具。如果想了解更多目标代码的细节,区分 relocatable
(可重定位)、executable
(可执行)、shared libarary
(共享库)的不同,我们得设法了解目标代码的组织方式和相关的阅读和分析工具。下面主要介绍这部分内容。
BFD is a package which allows applications to use the same routines tooperate on object files whatever the object file format. A new object fileformat can be supported simply by creating a new BFD back end and adding it tothe library.
binutils(GNU Binary Utilities)的很多工具都采用这个库来操作目标文件,这类工具有 objdump
,objcopy
,nm
,strip
等(当然,我们也可以利用它。如果深入了解ELF格式,那么通过它来分析和编写 Virus 程序将会更加方便),不过另外一款非常优秀的分析工具 readelf
并不是基于这个库,所以也应该可以直接用 elf.h
头文件中定义的相关结构来操作 ELF 文件。
下面将通过这些辅助工具(主要是 readelf
和 objdump
),结合 ELF 手册来分析它们。将依次介绍 ELF 文件的结构和三种不同类型 ELF 文件的区别。
ELF 文件的结构
ELF Header(ELF文件头)
Program 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 文件比较
先来通过文件头看看不同ELF的类型。为了说明问题,先来几段代码吧。
/* myprintf.c */
#include <stdio.h>
void myprintf(void)
{
printf("hello, world!\n");
}
/* test.h -- myprintf function declaration */
#ifndef _TEST_H_
#define _TEST_H_
void myprintf(void);
#endif
/* test.c */
#include "test.h"
int main()
{
myprintf();
return 0;
}
下面通过这几段代码来演示通过 readelf -h
参数查看 ELF 的不同类型。期间将演示如何创建动态链接库(即可共享文件)、静态链接库,并比较它们的异同。
编译产生两个目标文件 myprintf.o
和 test.o
,它们都是可重定位文件(REL):
$ gcc -c myprintf.c test.c
$ readelf -h test.o | grep Type
Type: REL (Relocatable file)
$ readelf -h myprintf.o | grep Type (查看目标文件)
Type: REL (Relocatable file)
根据目标代码链接产生可执行文件,这里的文件类型是可执行的(EXEC):
$ gcc -o test myprintf.o test.o
$ readelf -h test | grep Type (查看可执行文件)
Type: EXEC (Executable file)
用 ar
命令创建一个静态链接库,静态链接库也是可重定位文件(REL):
$ ar rcsv libmyprintf.a myprintf.o
$ readelf -h libmyprintf.a | grep Type (查看静态库)
Type: REL (Relocatable file)
可见,静态链接库和可重定位文件类型一样,它们之间唯一不同是前者可以是多个可重定位文件的“集合”。
静态链接库可直接链接(只需库名,不要前面的 lib
),也可用 -l
参数,-L
指定库搜索路径。
$ gcc -o test test.o -lmyprintf -L./
编译产生动态链接库,并支持 major
和 minor
版本号,动态链接库类型为 DYN
:
$ gcc -Wall myprintf.o -shared -Wl,-soname,libmyprintf.so.0 -o libmyprintf.so.0.0
$ 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 -lmyprintf -L./
但是执行时需要指定动态链接库的搜索路径,把 LD_LIBRARY_PATH
设为当前目录,指定 test
运行时的动态链接库搜索路径:
$ LD_LIBRARY_PATH=./ ./test
$ gcc -static -o test test.o -lmyprintf -L./
在不指定 -static
时会优先使用动态链接库,指定时则阻止使用动态链接库,这时会把所有静态链接库文件加入到可执行文件中,使得执行文件很大,而且加载到内存以后会浪费内存空间,因此不建议这么做。
经过上面的演示基本可以看出它们之间的不同:
- 可重定位文件(目标文件和静态库)本身不可以运行,仅仅是作为可执行文件、静态链接库(也是可重定位文件)、动态链接库的 “组件”。
- 静态链接库和动态链接库本身也不可以执行,作为可执行文件的“组件”,它们两者也不同,前者也是可重定位文件(只不过可能是多个可重定位文件的集合),并且在链接时加入到可执行文件中去。
- 而动态链接库在链接时,库文件本身并没有添加到可执行文件中,只是在可执行文件中加入了该库的名字等信息,以便在可执行文件运行过程中引用库中的函数时由动态链接器去查找相关函数的地址,并调用它们。
从这个意义上说,动态链接库本身也具有可重定位的特征,含有可重定位的信息。对于什么是重定位?如何进行静态符号和动态符号的重定位,我们将在链接部分和《动态符号链接的细节》一节介绍。