前面已经能够自己制作.a和.so了。
但是还有一个问题一直比较绕:
hello.c code.c分别编译以后得到:
hello.o code.o这两个.o文件到底是怎么变成最后那个可以直接执行的程序的?
这部分其实就是编译和链接。
再往下研究,还会碰到一个很重要的格式:
ELF
一、.o文件到底是什么?
先写两个简单的文件。
hello.c:
#include<stdio.h>voidrun();intmain(){printf("hello world!\n");run();return0;}code.c:
#include<stdio.h>voidrun(){printf("running...\n");}分别编译:
gcc-chello.c gcc-ccode.c就得到:
hello.o code.o.o就是目标文件。
这时候如果用:
filehello.o可以看到它属于 ELF 格式,而且是可重定位文件。
二、为什么要先生成.o?
因为一个比较大的工程,不可能所有代码都写在一个文件里。
不同的源文件可以分别编译。
比如:
hello.c -> hello.o code.c -> code.o如果之后只修改了code.c,就只需要重新编译:
gcc-ccode.c不用把整个工程全部重新编译一遍。
这也是目标文件比较重要的一个原因。
三、为什么两个.o文件不能直接各自运行?
问题就在于:
hello.o里面用了:
run();但是run并没有定义在hello.c中。
它实际定义在:
code.o同样,两个文件都使用了printf,但printf的实现也不在它们自己的源文件里面。
所以编译的时候,编译器只能先把这些还不知道的地址空出来。
例如反汇编:
objdump-dhello.o会看到类似:
callq ...后面的地址还没有确定。
原因很简单:
编译hello.c的时候,并不知道run最终会在什么位置。
所以这个地址只能先留着。
四、链接到底在干什么?
链接的时候,情况就不一样了。
此时:
hello.o code.o都已经准备好了。
链接器会把它们组合起来,然后处理之前没有确定的符号地址。
简单来说就是:
hello.o code.o ↓ 链接 ↓ 合并各个section ↓ 统一安排地址 ↓ 修正原来没有确定的地址 ↓ 可执行程序两个.o文件的.text最后就被合并到了最终程序的.text中,并重新进行了统一编址。
五、ELF是什么?
Linux 下很多二进制文件其实都是 ELF 格式。
这里主要接触四类:
.o -> 可重定位文件 可执行程序 -> Executable File .so -> Shared Object File core dump -> 内核转储所以 ELF 并不是单独某一种文件,而是一种二进制文件格式。
六、ELF里面都有些什么?
从整体上看,一个 ELF 文件可以看到这些部分:
ELF头(ELF Header) 程序头表(Program Header Table) 节头表(Section Header Table) 节(Sections)ELF Header 在文件开头。
它保存 ELF 文件的一些基本信息,同时还可以帮助定位文件的其他区域。
比如使用:
readelf-hhello.o可以看到:
Class Data Version Type Machine Entry point Start of program headers Start of section headers ...这些信息都可以帮助我们了解一个 ELF 文件到底是什么类型、面向什么平台,以及其他关键结构在哪里。
七、Section到底是什么?
Section 可以理解成 ELF 内部按照不同用途划分出来的一块块区域。
比较常见的有:
.text .data .rodata .bss .symtab .got .plt其中:
.text
主要保存程序的机器指令。
.text ↓ 代码.data
保存已经初始化的全局变量、静态变量等数据。
.data ↓ 已经初始化的数据.rodata
保存只读数据,比如字符串常量。
.bss
给没有初始化的全局变量、静态变量预留空间。
可以通过:
readelf-Sa.out查看一个 ELF 文件有哪些 Section。
八、那Segment又是什么?
前面看到的是 Section。
程序真正加载进内存的时候,又会涉及一个新的概念:
Segment
这两个东西不能混在一起。
ELF 中很多 Section 在加载到内存的时候,会按照属性重新组合成 Segment。比如:
可读 可写 可执行具有相同属性的部分可以放到一起。
比如:
很多代码相关section ↓ 一个可执行segment很多数据相关section ↓ 一个可读写segment这样做可以减少内存页面的浪费。
比如一个 Section 有 4097 字节,单独放需要占两个页面;另一个很小的 Section 如果单独放,又可能额外占一个页面。
把属性相同的内容合并之后,就可以减少这种空间浪费。
九、Section Header Table和Program Header Table有什么区别?
这个地方刚开始很容易混。
可以直接这么理解:
Section Header Table ↓ 更关注 ELF 文件内部是怎么组织的Program Header Table ↓ 更关注程序运行时应该怎么加载也就是:
Section → 链接视图 → 更适合研究链接过程 Segment → 执行视图 → 更适合研究程序加载一个主要服务于链接,一个主要服务于运行加载。
十、静态链接到底发生了什么?
回到最开始的:
hello.o code.o执行:
gcc *.o-omain.exe就可以得到最终程序。
这个过程就是静态链接的一个基本例子。
在这个过程中,主要发生两件事情。
第一件:
把多个模块组合起来。
第二件:
把那些原来不知道的地址补完整。
比如hello.o里面调用:
run编译的时候不知道run在哪里,所以地址暂时不能确定。
链接以后:
hello.o ↓ 找到 run 的定义 ↓ 确定 run 的地址 ↓ 修改原来的调用位置这就是重定位。
十一、怎么查看这些东西?
Linux 下有几个命令特别适合拿来观察 ELF:
file
看文件类型:
filehello.oreadelf
查看 ELF 内部信息:
readelf-hhello.o readelf-Shello.o readelf-shello.o readelf-lmain.exeobjdump
查看机器指令:
objdump-dhello.o objdump-dmain.exe例如:
objdump-dhello.o可以发现call后面的地址在.o文件里还没有真正确定。
而链接完成以后:
objdump-dmain.exe就可以看到类似:
callq 1149 <run>这样的实际跳转位置。
这个变化其实就很直观地把“重定位”表现出来了。
十二、符号表有什么用?
除了机器指令,还可以看看:
readelf-shello.o这里能看到符号表。
比如hello.o里面的:
main run puts会以符号的形式出现。
其中如果某个符号显示:
UND可以理解成这个符号在当前.o中没有定义。
比如:
hello.o里用到了:
run但run实际上定义在:
code.o所以在hello.o的符号表里,run就属于未定义符号。
等两个.o进行链接之后,最终程序里就能够找到真正的run。
十三、把编译、链接整个串起来
现在整个过程可以写成:
hello.c ↓ hello.ocode.c ↓ code.o然后:
hello.o + code.o + 静态库 ↓ 链接 ↓ 地址重定位 ↓ ELF可执行程序所以以前我看到:
gcc hello.c code.c-omain感觉就是一句命令。
现在再拆开看,其实里面经历了:
源代码 ↓ 编译 ↓ .o ↓ 链接 ↓ 地址重定位 ↓ 可执行ELF真正把几个相互独立的模块拼起来的,就是链接阶段。