参考:
ELF文件格式解析
1.6 Linux下ELF文件文件格式【补充】:
典型的ELF可重定位目标文件:
典型的ELF可执行目标文件:
1.7 Linux下ELF文件中各个section的含义【补充】:
命令:
readelf -S xxx //查看该目标文件中有哪些section
readelf -S a.out //查看该可执行目标文件中有哪些section
readelf -S sum.o, readelf -S sum.a //查看可重定位目标中有哪些section
readelf -S sum.o //查看共享目标文件中有哪些section
注意如上3个目标文件的section内容不完全一样。 且注意观察,.o, .a种的所有section是没有被分配虚拟内存地址的。
1.7.1 ELF Header
命令:
readelf -h xxx //可以看到ELF Header结构的内容。
内容:
其中Type:是指目标文件类型。其对应于vim xxx(目标文件)的0x0000010的第1第2Byte。
Type的值是1h,表示REL (Relocatable file)
Type的值是2h,表示EXEC (Executable file)
Type的值是3h,表示DYN (Shared object file)
其中Machine:是指文件目标的体系结构类型。如ARM或X86-64。占2个Byte
其中Entry point address:是指程序入口的虚拟地址。可以发现sum.o种值为0,这表示Relocatable file不会有程序进入点。
其中Start of program headers:,Size of program headers:,Number of program headers: 分别是指Program Header Table偏移地址,Program Header的每个Entry的大小,Program Header的Entry的个数。注意“Start of program headers:”的值正好等于“Size of this header:”的值。可重定位目标文件没有此项。
其中Start of section headers:,Size of section headers:,Number of section headers: 分别是指Section Header Table偏移地址,Section Header的每个Entry的大小,Section Header的Entry的个数。注意这个值正好对应了上面readelf -S xxx都出来的个数。
其中Size of this header:是指ELF Header的大小。注意观察ELF Header在vim xxx(目标文件)中结束的位置。
其中Section header string table index:是指.shstrtab在Section Header Table中的索引。如值25表示的是Section Header Table中第25项Entry是.shstrtab。注意此值一般等于“Number of section headers: - 1”
1.7.2 Section Header Table
在Section Head Table中,针对每一个Section都设置有一个entry,用来描述section。其主要内容包括该section 的名称、类型、大小以及在整个ELF文件中的字节偏移位置等等。
内容:
其中Name的值表示每个section entry在.shstrtab中的字节偏移(从0开始); 而非表示每个section entry在.shstrtab中的索引(索引从1开始)。 该.shstrtab中存储着所有section的名字。
其中Address的值表示如果此节区将出现在进程的内存映像中,则此成员给出节区的第一个字节应处的位置;否则,此值为0。
其中Offset的值表示节区的第一个字节和文件头之间的偏移。 当使用readelf -S xxx查看section时,Offset就对应的是每个section在vim xxx中的地址。
其中Size的值表示节区的长度(Bytes)。
其中EntSize的值,因为某些节区中包含固定大小的项目,比如符号表,对于这类节区,这里的值表示每个表项的长度(Bytes)。
命令:
即上,readelf -S xxx //查看该目标文件中有哪些section
1.7.3 Section
.text .data
命令: (注意,objdump -j 后可以跟任何section Name,以此选中想要查看的section)
objdump -d xxx -j .text/.data //可以查看相应section的反汇编
objdump -s xxx -j .text/.data //可以查看相应section的内容
另外,我们也常常使用objdump -s -d main.o 来查看可重定位目标文件和内容(.data.bss.rodata)和反汇编(.text)
符号表: .symtab(符号表) .dynsym(动态链接符号表)
--- 注意区分理解.dynsym(动态链接符号表)和.symtab(符号表)。
存放的是程序中定义和引用的函数和全局变量和static变量的信息。
【1】【符号】符号表中包含三种不同的符号:
由模块m定义并能被其它模块引用的全局符号。对应于非static的C函数和全局变量。
由其它模块定义并被模块m引用的全局符号。称为外部符号。对应于在其它模块中定义的非static的C函数和全局变量。
只被模块m定义和引用的局部符号。对应于带staic属性的C函数和变量。
(但注意,局部变量是在栈中被管理,连接器对此类符号不感兴趣。)
而,每个符号都被分配到目标文件的某个节。并且,有三个特殊的伪section(pseudo section),在section header table中是没有条目的。但注意,只有可重定位目标文件中才有这些伪section,可执行目标文件中是没有的。
【2】【符号解析】即把每个符号引用正好和一个符号定义关联起来。
局部符号:编译器只允许每个模块中每个局部符号有一个定义。
static 函数符号,编译器确保有唯一的名字。Error:redinition。
static 变量符号,编译器确保有唯一的名字。a.1724, a.1728
全局符号:编译器遇到一个不是当前模块中定义的符号(变量或函数)时,会假设该符号是在其他某个模块中定义的,生成一个链接符号表条目,并把它交给链接器处理。
链接器如何解析多重定义的全局符号(-要么报错;要么以某规则选出一个定义抛弃其他定义)
首先,编译器会输出每个全局符号:或是强(strong)或是弱(week),而汇编器把这个信息隐含地编码在可重定位目标文件地符号表里。函数和已初始化地全局变量是强符号,未初始化地全局变量是若符号。
其次,链接器按下面地规则来处理多重定义的符号名:
规则1:不允许有多个同名的强符号; Error:multiple definition
规则2:如果有一个强符号和多个弱符号同名,那么选择强符号;
规则3:如果有多个弱符号同名,那么任意选出一个;
规则2和规则3会引入一些不易察觉的运行错误,在编译阶段检查出它们:GCC-fno-common或-Werror
【3】【重定位】即为每个符号分配运行时地址。
当汇编器生成一个可重定位目标文件时,它并不知道数据和代码最终将放在内存中的什么位置;它也不知道这个目标文件中引用的任何外部定义的函数和全局变量的位置。所以,汇编器针对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器再将目标文件合并成可执行文件时如何修改这个引用。
注意,代码的重定位条目放在.rel.txt中;已初始化数据的重定位条目放在.rel.data中。
另外注意,重定位表(.rel.dyn, .rel.plt)包含如何修改section内容的信息。 可使用
readelf -r xxx 查看。
命令:
readelf -s a.out //可以查看.dynsym(动态链接符号表)和.symtab(符号表)。
另外,
(注意,readelf -x/-R/-p 后可以跟任何section Name或section Index,以此选中想要查看的section)
readelf -x/-R .dynsym/5 a.out //可以查看.dynsym(动态链接符号表)的具体类容,以16进制形式。
readelf -p .dynsym/5 a.out //可以查看.dynsym(动态链接符号表)的具体类容,以字符串形式。
内容:
其中Name的值表示符号表的每个符号名在字符串表中的字节偏移(从0开始)。而非表示符号表的每个符号名在字符串表中的索引。
其中Value的值表示,对于可重定位目标文件来说,此值表示距定义符号的section的起始位置的偏移; 对于可执行目标文件来说,此值表示的是一个绝对的运行地址,即符号的虚拟内存地址。
其中Size的值表示所定义符号的大小(Bytes)。
其中Ndx的值表示符号在Section Header Table中的索引(即对应的是readelf -S xxx中的Nr值), 或三个特殊的伪section。
其中Bind的值表示LOCAL还是GLOBAL。 LOCAL代表链接器内部使用的局部符号。
其中Type的值表示变量对象或函数或其它等等。
字符串表: .strtab(字符串表) .dynstr(动态链接字符串表) .shstrtab(节头字符串表)
--- 注意观察.dynstr(动态链接字符串表)和.strtab(字符串表)和.shstrtab(节头字符串表)的结构完全相同,不过一个存储的是.dynsym(动态链接符号表)中名称的字符串,一个是.symtab(符号表)中名称的字符串,另一个是section名称的字符串。
存放着所有符号的名称字符串。
小结1.7
使用
readelf -h main.o, readelf -S main.o, readelf -s main.o 即可完全解析出main.o的内容。 如下图用不同颜色标出主要的section等:
1.8 链接
链接:是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存执行。
链接发生在:可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序执行。
1.9 静态库
静态库链接时,链接器将只复制被程序引用的目标模块,这减少了可执行文件在磁盘和内存中的大小。同时,程序员也只需包含较少的库文件名字。
静态库以一种存档(archive)的特殊文件格式存放在磁盘中,该存档文件是一组连接起来的可重定位目标文件的集合。后缀.a标识。
1.10 可执行目标文件
可执行目标文件,告诉系统如何加载到内存。
命令:
readelf -l a.out //查看program header table. 其中需要记住的是,把从可执行目标文件Offset开始的FileSiz的字节,初始化到内存地址为VirtAddr开始的MemSiz中去。
1.11 动态库
静态链接库有个明显缺点是,对于标准的IO函数(如printf,scanf),这些函数的代码都会被复制到每个运行进程的.text中,这对内存是极大的浪费。
共享库以2种方式来”共享“的:
首先,在任何给定的文件系统中,对于一个库只有1个.so文件,所有引用该库的可执行目标文件共享这个so文件中的代码和数据。而不是像静态库的内容那样被复制和嵌入到引用他们的可执行目标文件中。
其次,在内存中,一个共享库的.text的一个副本可以被不同的正在云翔的进程共享。
共享库的使用过程:
当使用共享库创建可执行目标文件时,链接器复制了一些重定位和符号表信息,使得运行时可以解析对libvector.so中对代码和数据的引用。
当加载和运行可执行文件时,”动态链接过程“(见下章)。
另外需要注意的是,应用程序还可以在它运行时执行(加载和链接)某个共享库。