一、 如何得到 .o 可重定位目标文件文件

          cpp、cc1预编译         cc1编译                  as汇编
***.c —————————————————> a.i ————————————————> a.s ———————————————————> ***.o

经过以上流程后就能得到.o文件,在.o中放的都是机器指令,
我们在前面讲过,在没有链接之前,.o中的机器指令是无法执行的


二、如何得到“静态库文件”

  1. 先从各.c得到各个.o文件
  2. 将各.o文件打包成静态库

后面介绍C库时,会讲如何具体制作静态库


三、ELF格式的“可重定位目标文件”的组成结构

前面就是说过,在Linux下面,目标文件的格式都是ELF格式,所以接下来我们就看看ELF格式的“可重定位目标文件”,

.o可重定位目标文件,是机器代码,所以我们可以通过反汇编来查看ELF格式的可重定位目标文件格式

objdump -D main.o > hw1.s

ELF 格式如图:

红色部分是重点

  1. 总共12个节
  2. 每个节都有一个编号
    ELF头开始编号,编号从0开始,编号的作用就是用来索引(找到)不同节的
  3. 每个.o的都是这样的结构
    链接时要做的就是,将ELF格式的.o全部合成为一个完整的ELF格式可执行文件,后面再详细介绍

四、ELF头(ELF格式头)

1. 格式头放什内容?

放ELF格式所需要的一些基本信息,比如:

系统所规定的字的大小

  • 64 OS:字大小是64bit
  • 32 OS:字大小是32bit

字节顺序(字节序)

  • 用于说明系统是大端序的还是小端序的

其它

  1. ELF格式头的大小
  2. 目标文件类型(可重定位目标文件、可执行目标文件、共享目标文件)
  3. CPU架构:说明编译出来的机器指令是运行在什么CPU上的
  4. 等等

2. 使用 readelf,查看 “可重定位目标文件” 的 ELF 头信息

readelf:读取目标文件的ELF格式信息的,跟-h选项的话,就是查看ELF格式头信息

演示:

$ readelf -h helloworld.o
ELF Header:
Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 # ELF 魔数,7f是固定的,45 4C 46为ELF的ascii码
Class:                             ELF64 # 表示该ELF格式面对的是64位系统,在32位系统里面就是ELF32
Data:                              2's complement, little endian # 数据按小端序存储
Version:                           1 (current)   # ELF头版本号
OS/ABI:                            UNIX - System V
ABI Version:                       0
Type:                              REL (Relocatable file) # 目标文件类型,这里标注的是“可重定位目标文件”
Machine:                           Advanced Micro Devices X86-64 # cpu类别
Version:                           0x1 # 目标文件版本号
Entry point address:               0x0 # 起始地址(入口地址),前面说过.o逻辑地址是从0开始的
Start of program headers:          0 (bytes into file)
Start of section headers:          672 (bytes into file)
Flags:                             0x0
Size of this header:               64 (bytes)
Size of program headers:           0 
Number of program headers:         0
Size of section headers:           64 (bytes)
Number of section headers:         13
Section header string table index: 10

五、 .text 代码段

放所有函数的机器指令,不过某些常量也会直接和指令一起存在.text当中
.text:也是只读节

比如:

int main(void)
{
    int a = 20;
    a = a + 100; //表达式中的100会直接和指令放在一起
}

六、.data、.bss、.rodata 都属于数据段节

  • .rodata 只读数据段节,此段的数据不可修改,存放常量
  • .data 保存已初始化的全局变量和局部静态变量,可读可写
  • .bss 存放未初始化的全局变量、静态变量,所以此段数据均为0,仅作占位

.rodata

只读数据节,放只读数据(放某些常量数据)

比如:

int a = 100;
printf("%d", a);
char *p = "hello world";

格式字符串"%d""hello world"这两个字符串常量,都放在了.rodata

.data

保存已初始化的全局变量和局部静态变量,可读可写

比如:
(1)初始化了全局变量

int a = 100; //初始化了的全局变量,`a`就是在`.data`节中
int main(void)
{
    printf("%d\n", a);
}

(2)初始化了的静态局部变量

int main(void)
{
    static int b = 101; //已经初始化了的静态局部变量,后面讲static关键字时还会介绍到
    printf("%d", a);
}

注意:如果没有static的话,b就是自动局部变量,b空间开辟于栈中

但是只有程序运行起来后才有栈这个东西,因此作为还处在编译阶段的.o来说,自动局部变量还不存在,
或者说还只是以函数代码的形式存在于.text中。

当程序运行起来有了栈之后,该函数的代码会在栈中开辟自动局部变量的空间,并将数据101存入开辟的空间

.bss

放未初始化的全局变量、静态变量,

比如
(a)未初始化的全局变量

int a; //未初始化了的全局变量
int main(void)
{
    printf("%d", a);
}

(b)未初始化的静态局部变量

int main(void)
{
    static int b; //未初始化的静态局部变量
    printf("%d", a);
}

由于没有初始化数据,所以其实不占用空间,因此在.o中,.bss只是一个占位符,只有当程序真正运行起来后,
才会在内存上真正的开辟.bss的空间,并在.bss空间中开辟ab的空间,并制自动初始化为0

.o为什么没有开辟.bss空间?
没有实际要存放的数据,开辟空间只是浪费空间,
你要知道.o这个文件是存在我们的电脑硬盘上的,.o如果有.bss空间的话,.bss是要占硬盘空间的


七 .symtab 符号表

1. symtab 记录什么

每一个.o文件都有一个符号表,用于存放.o中所定义和引用的全局符号信息(函数和全局变量的符号信息)

文件 a.c

int a = 100;
int fun(int a)
{
    ...
}

extern int b; //定义在了b.c中

int main(void)
{
    b = 10000;
    fun2(1000); //fun2定义在了c.c中
}
a.c -> a.o
b.c -> b.o
c.c -> c.o

a.o.symtab符号表就记录如下符号的信息

  • aa.o自己定义的全局变量符号
  • funa.o自己定义的函数符号
  • maina.o自己定义的函数符号

a.o中引用的符号信息

  • ba.o引用的在b.o中定义的全局变量符号
  • fun2a.o引用的在c.o中定义的fun2函数符号

2. 注意:.symtab符号表并不记录符号的名字

.symtab 记录符号的基本信息,符号是否有定义,符号对应的空间在哪个节中等,但是符号的名字本身并不存在符号表中

疑问:符号的名字记录在了哪里呢?后面回答

3. symtab符号表的意义

众多的.o之所以能被链接在一起,这张符号表所记录的信息功不可没

比如,a.o中引用的bfun2,被定义在了b.o中,将a.ob.o链接在一起时,
必须查看各个.o中的.symtab表,才能将各自符号的定义和引用关联起来

注意:符号表并不记录自动局部变量的符号

int fun()
{
    int a = 100;
    ...
}

为什么不记录自动局部变量的符号?

  • 自动局部变量是在程序运行起来有了栈以后才有的,在编译阶段fun函数的int a = 100
    fun函数中只是压栈和弹栈的代码

  • 当程序运行起来有了栈后,当fun函数开始运行时,fun的压栈代码会开辟自动局部变量的空间,
    fun函数结束时会调用弹栈代码自动释放空间

但是如果是static修饰的静态局部变量的话,符号表中会记录符号,因为在编译阶段就会处理静态局部变量


八、 .rel.text

将多个.o链接到一起时,每个.o.text会被整合为一个.text
整合.text时就必须依赖.rel.text所记录的一些有关.text中指令的位置信息,
至于具体的位置信息是怎么样的,我们这里不需要知道


九、 .rel.data

将多个.o链接到一起时,每个.o.data会被整合为一个.data,整合到一起时,
就必须要依赖.rel.data所记录的一些有关.data的信息,具体什么信息我们这里不需要掌握


十、 .debug

符号调试表,记录调试信息,编译时必须加-g选项,编译时才会在.debug节中加入调试信息


十一、.line

存放代码行号,因为调试的时候往往需要显示源码的行号

只有gcc编译时加了-g选项后,才会加入行号信息


十二、 .strtab

字符串表,比如:
.symtab.debug所用到符号名字、每个节的节名字(比如.text等)、源文件名字(***.c)等,都存在.strtab

比如:
.text\0.rodata\0......fun\0main\0\0 是字符结束的标志


十三、 节头部表

描述目标文件中的每个节的某些相关信息,对于节头部表来说,我们这里不在介绍。