C6-链接与目标文件

链接概述与目标文件

可执行文件的生成过程

以C语言为例: (): 表示过程

(源程序, 文本) hello.c -> (预处理 cpp) -> hello.i 源程序, 文本 -> (编译 cc1) -> hello.s 汇编语言程序, 文本 -> 汇编(as) -> hello.o 可重定位目标程序, 二进制 -> (链接ld) -> hello, 可执行目标程序, 二进制

预处理

command-LINUX

  • gcc -E x.c -o x.i
  • cpp x.c > x.i

操作细节

  • 删除“#define”并展开所定义的
  • 处理所有条件预编译指令,如“#if”,“#ifdef”, “#endif”等
  • 插入头文件到“#include”处,可以递归方式进行处理
  • 删除所有的注释“//”和“/* */”
  • 添加行号文件名标识,以便编译时编译器产生调试用的行号信息
  • 保留所有#pragma编译指令(编译器需要用)

预处理后, 得到预处理文件(x.i), 仍为可读文件, 但是不包含宏定义

编译

  • 编译过程就是将预处理后得到的预处理文件(如 hello.i)进行词法分析、语法分析、语义分析、优化后,生成汇编代码文件
  • 用来进行编译处理的程序称为编译程序(编译器,Compiler)

command

  • $gcc –S hello.i –o hello.s
  • $gcc –S hello.c –o hello.s
  • $/user/lib/gcc/i486-linux-gnu/4.1/cc1 hello.c

经过编译后,得到的汇编代码文件(如 hello.s)还是可读的文本文件,CPU无法理解和执行.

gcc命令实际上是具体程序(如ccp、cc1、as等)的包装命令,用户通过gcc命令来使用具体的预处理程序ccp、编译程序cc1和汇编程序as等.

汇编

  • 汇编代码文件(由汇编指令构成)称为汇编语言源程序
  • 汇编程序(汇编器)用来将汇编语言源程序转换为机器指令序列(机器语言程序
  • 汇编指令和机器指令一一对应,前者是后者的符号表示,它们都属于机器级指令,所构成的程序称为*机器级代码

command

  • gcc –c hello.s –o hello.o
  • gcc –c hello.c –o hello.o
  • as hello.s -o hello.o(as是一个汇编程序)

汇编结果是一个可重定位目标文件(如,hello.o),其中包含的是二进制代码,必须用相应的工具软件来查看其内容

链接

预处理、编译和汇编三个阶段针对一个模块(一个.c文件)进行处理,得到对应的一个可重定位目标文件(一个.o文件)

链接过程将多个可重定位目标文件合并以生成可执行目标文件

  • 子程序(函数)起始地址和变量起始地址是符号定义(definition)
  • 调用子程序(函数或过程)和使用变量即是符号的引用(reference)
  • 一个模块定义的符号可以被另一个模块引用
  • 最终须链接(即合并),合并时须在符号引用处填入定义处的地址
  • 符号解析: 确定各符号引用关系

command

  • gcc –static –o myproc main.o test.o
  • ld –static –o myproc main.o test.o
  • 注: static 表示静态链接,如果不指定-o选项,则可执行文件名为“a.out”

操作细节

Step 1. 符号解析(Symbol resolution)

1.1 程序中有定义和引用的符号 (包括变量和函数等)

e.g.

  • void swap() {…} 定义符号swap
  • swap(); 引用符号swap
  • int *xp = &x; 定义符号 xp, 引用符号 x

1.2编译器将定义的符号存放在一个符号表( symbol table)中.

  • 符号表是一个结构数组
  • 每个表项包含符号名、长度和位置等信息

1.3链接器将每个符号的引用都与一个确定的符号定义建立关联

Step 2. 重定位

  • 将多个代码段与数据段分别合并为一个单独的代码段和数据段
  • 计算每个定义的符号在虚拟地址空间中的绝对地址
  • 将可执行文件中符号引用处的地址修改为重定位后的地址信息

img

img

目标文件格式 以ELF为例

三类目标文件

可重定位目标文件 (.o)

  • 其代码和数据可和其他可重定位文件合并为可执行文件
  • 每个.o 文件由对应的.c文件生成
  • 每个.o文件代码和数据地址都从0开始

可执行目标文件 (Linux默认为a.out, Windows为*.exe)

  • 包含的代码和数据可以被直接复制到内存并被执行
  • 代码和数据地址为虚拟地址空间中的地址

共享的目标文件 (Linux中*.so)

  • 特殊的可重定位目标文件,能在装入或运行时被装入到内存并自动被链接,称为共享库文件
  • Windows 中称其为 Dynamic Link Libraries (DLLs)

目标文件格式

目标代码(Object Code)指编译器和汇编器处理源代码后所生成的机器语言目标代码

目标文件(Object File)指包含目标代码的文件

最早的目标文件格式是自有格式,非标准的

标准的几种目标文件格式:

  • DOS操作系统(最简单) :COM格式,文件中仅包含代码和数据,且被加载到固定位置
  • System V UNIX早期版本:COFF格式,文件中不仅包含代码和数据,还包含重定位信息、调试信息、符号表等其他信息,由一组严格定义的数据结构序列组成
  • Windows: PE格式(COFF的变种),称为可移植可执行(Portable Executable,简称PE)
  • Linux等类UNIX:ELF格式(COFF的变种),称为可执行可链接(Executable and Linkable Format,简称ELF)

ELF视图

img

ELF链接视图-可重定位目标文件 Relocatable object files

  • 可被链接(合并)生成可执行文件共享目标文件
  • 静态链接库文件由若干个可重定位目标文件组成
  • 包含代码、数据(包含已初始化的全局变量和局部静态变量.data和未初始化的全局变量和局部静态变量.bss)
  • 包含重定位信息(指出哪些符号引用处需要重定位)
  • 文件扩展名为.o(相当于Windows中的 .obj文件)

ELF执行视图-可执行目标文件 Executable object files

  • 包含代码、数据(已初始化.data和未初始化.bss)
  • 定义的所有变量和函数已有确定地址(虚拟地址空间中的地址)
  • 符号引用处已被重定位,以指向所引用的定义符号
  • 没有文件扩展名或默认为a.out(相当于Windows中的 .exe文件)
  • 可被CPU直接执行,指令地址和指令给出的操作数地址都是虚拟地址

可重定位目标文件格式

img

ELF 头: 包括16字节标识信息、文件类型 (.o, exec, .so)、机器类型(如 IA-32)、节头表的偏移、节头表的表项大小以及表项个数

.text 节: 编译后的代码部分

.rodata 节: 只读数据,如 printf 格式串、switch 跳转表等

.data 节: 已初始化的全局变量

.bss 节:未初始化全局变量,仅是占位符,不占据任何实际磁盘空间。区分初始化和非初始化是为了空间效率

C语言规定: 未初始化的全局变量和局部静态变量的默认初始值为0.

分离.bss与.data:

  • .data节中存放具体的初始值, 占用磁盘空间
  • .bss节中不存放初始值, 只说明了.bss中每个变量在执行时需要占用的字节数. 因此, .bss实际上不占用磁盘空间.
  • 所有未初始化的全局变量和局部静态变量都汇总到.bss中, 通过节头表(Section header table)来说明需要为.bss预留多少空间

.symtab 节: 存放函数和全局变量 (符号表)信息 ,它不包括局部变量

.rel.text 节: .text节的重定位信息,用于重新修改代码段的指令中的地址信息

.rel.data 节: .data节的重定位信息,用于对被模块使用或定义的全局变量进行重定位的信息

.debug 节: 调试用符号表 (gcc -g)

strtab 节: 字符串表, 包含symtab和debug节中符号及节名

Section header table(节头表): 每个节的节名、偏移和大小

ELF头

command: readelf -h <file>

ELF头位于ELF文件开始,包含文件结构说明信息。分32位系统对应结构和64位系统对应结构(32位版本、64位版本). 以32为例.

定义了ELF魔数、版本、小端/大端、操作系统平台、目标文件的类型、机器结构类型、程序执行的入口地址、程序头表(段头表)的起始位置和长度、节头表的起始位置和长度等

魔数:文件开头几个字节通常用来确定文件的类型或格式. 加载或读取文件时,可用魔数确认文件类型是否正确

define EI_NIDENT 16

typedef struct {

unsigned char e_ident[EI_NIDENT]; //32位ELF头中, 前4个为魔数, 用来标识是否是ELF文件, 第一个字节0x7f, 后面三个是"E""L""F". 之后包含一些标识信息, 包括32/64位, 小端/大端方式存放, ELF头的版本号等.

Elf32_Half e_type; //目标文件类型: 可重定位/可执行/共享库/其他

Elf32_Half e_machine; //程序运行的操作系统平台或结构(IA-32/AMD64...)

Elf32_Word e_version; //标识目标文件版本

Elf32_Addr e_entry; //程序入口地址(虚拟地址), 可重定位目标文件此处为0

Elf32_Off e_phoff; //程序头表的起始位置

Elf32_Off e_shoff; //节头表在文件中的偏移量 (字节为单位)

Elf32_Word e_flags;

Elf32_Half e_ehsize; //ELF头的大小(字节为单位)

Elf32_Half e_phentsize; //程序头表每个表项的长度(字节为单位)

Elf32_Half e_phnum; //程序头表表项的数量

Elf32_Half e_shentsize; //节头表每个表项的长度 (字节为单位)

Elf32_Half e_shnum; //节头表表项的数量

Elf32_Half e_shstrndx;

} Elf32_Ehdr;

节头表(Section Header Table)

command: readelf -S <file>

  • 除ELF头之外,节头表是ELF可重定位目标文件中最重要的部分内容
  • 描述每个节的节名、在文件中的偏移、大小、访问属性、对齐方式等
  • 以下是32位系统对应的数据结构(每个表项占40B, 每个内容占4B)
  • e.g.: 一个表项

typedef struct {

Elf32_Word sh_name; //节名, 实际, 节名字符串在.strtab中的偏移

Elf32_Word sh_type; //节类型: 无效/代码/数据/符号/字符串...

Elf32_Word sh_flags; //节标志: 该节在虚拟空间中的访问属性 (可执行/可读可写)

Elf32_Addr sh_addr; //虚拟地址. 若可被加载, 则对应虚拟地址, 若为可重定位目标文件, 此处为0

Elf32_Off sh_offset; //在文件中的偏移地址, 对.bss而言无意义

Elf32_Word sh_size; //节在文件中占用的长度

Elf32_Word sh_link;

Elf32_Word sh_info;

//sh_link和sh_info用于与链接相关的节(如.rel.text节、.rel.data节、.symtab节等), 不是全部节都有

Elf32_Word sh_addralign; //节的对齐要求

Elf32_Word sh_entsize; //节中每个表项的长度,0表示无固定长度表项(不是一个表)

} Elf32_Shdr;

img

此处: W(Write), A(Alloc/Read), X(Execute)

其中, 有四个节会分配存储空间, 其他节不分配. .text(X), .data/.bss(WA), .rodata(A)

可执行目标文件格式

img

  • 包含代码, 数据(已初始化.data和未初始化.bss)
  • 定义的所有变量和函数已有确定地址(虚拟空间中的地址)
  • 符号引用处已被重定位, 以指向所引用的定义符号
  • 没有文件扩展名或者默认为a.out(相当于Windows中的.exe文件)
  • 可被CPU直接执行, 指令地址和指令给出的操作数地址都是虚拟地址

存储器映像

img

程序头表

command: readelf -l <file>

  • 程序头表用来说明段信息, 已称为段头表
  • 具有相同访问属性合并成段(Segm). 程序头表说明每个段的属性, 如: 在可执行文件中的位移, 大小; 在虚拟空间中的位置, 对齐方式, 访问属性等
  • 程序头表描述可执行文件中的节与虚拟空间中的存储段之间的映射关系
  • 一个表项(32B)说明虚拟地址空间中一个连续的段一个特殊的节
  • 在表项中, 可装入段typeLOAD, 可装入段需要装入存储器.

e.g.

img

第一可装入段:第0x00000~0x004d3字节(包括ELF头、程序头表、.init、.text和.rodata节),映射到虚拟地址0x8048000开始长度为0x4d4字节的区域,按0x1000=212=4KB对齐,具有只读/执行权限(Flg=RE),是只读代码段。

第二可装入段:第0x000f0c开始长度为0x108字节的.data节,映射到虚拟地址0x8049f0c开始长度为0x110字节的存储区域,在0x110=272B存储区中,前0x108=264B用.data节内容初始化,后面272-264=8B对应.bss节,初始化为0,按0x1000=4KB对齐,具有可读可写权限(Flg=RW),是可读写数据段。(注: .bss节不占磁盘空间, 但是在映射时, 会占用虚拟内存空间)

静态链接与符号解析

静态链接对象: 可重定位目标模块(.o), 静态库(.a, 其中包含多个.o模块, 包括标准库 自定义库)

避免的极端做法: 将所有函数都放在一个源文件中/ 一个源文件中仅包含一个函数

静态共享库

静态库(.a, archive files)

  • command: ar [emulation options] [-]{dmpqrstx}[abcDfilMNoOPsSTuvV] [--plugin ] [member-name] [count] archive-file file...
  • e.g.: ar rcs mylib.a myproc1.o myproc2.o
  • ar命令允许增量更新
  • 将所有相关的目标模块(.o)打包为一个单独的库文件(.a),称为静态库文件 ,也称存档文件(archive)
  • 使用静态库,可增强链接器功能,使其能通过查找一个或多个库文件中定义的符号来解析符号
  • 在构建可执行文件时,只需指定库文件名,链接器会自动到库中寻找那些应用程序用到的目标模块,并且只把用到的模块从库中拷贝出来
  • 在gcc命令行中无需明显指定C标准库libc.a(默认库)

链接器中符号解析过程

e.g.

myproc1.c:

#include <stdio.h>
void myfunc1(){
	printf("This is myfunc1\n");
}

myproc2.c:

#include <stdio.h>
void myfunc2(){
    printf("This is myfunc2!\n);
}

main.c:

void myfunc1(viod);
int main(){
    myfunc1();
    return 0;
}

调用关系: main -> myfunc1 -> printf

  • E: 将被合并以组成可执行文件的所有目标文件集合(所有组成可执行文件的.o文件)
  • U: 当前所有未解析的引用符号的集合
  • D: 当前所有定义(被解析)符号的集合

过程

gcc -c main.c

gcc -static -o myproc main.o ./mylib.a

  1. 开始E、U、D为空,首先扫描main.o,把它加入E,同时把myfun1加入U,main加入D。
  2. 接着扫描到mylib.a,将U中所有符号(本例中为myfunc1)与mylib.a中所有目标模块(myproc1.o和myproc2.o)依次匹配,发现在myproc1.o中定义了myfunc1,故myproc1.o加入E,myfunc1从U转移到D。
  3. 在myproc1.o中发现还有未解析符号printf,将其加到U。不断在mylib.a的各模块上进行迭代以匹配U中的符号,直到U、D都不再变化。此时U中只有一个未解析符号printf,而D中有main和myfunc1。
  4. 因为模块myproc2.o没有被加入E中,因而它被丢弃。
  5. 接着,扫描默认的库文件libc.a,发现其目标模块printf.o定义了printf,于是printf也从U移到D,并将printf.o加入E,同时把它定义的所有符号加入D,而所有未解析符号加入U。
  6. 处理完libc.a时,U一定是空的。

本例结果:

E中有main.o、myproc1.o、printf.o及其调用的模块

D中有main、myproc1、printf及其引用的符号

链接顺序

依照原例, command: gcc -static -o myproc main.o ./mylib.a -> gcc -static -o myproc ./mylib.a main.o

过程:

  1. 首先,扫描mylib,因是静态库,应根据其中是否存在U中未解析符号对应的定义符号来确定哪个.o被加入E。因为开始U为空,故其中两个.o模块都不被加入E中而被丢弃。
  2. 然后,扫描main.o,将myfunc1加入U,直到最后它都不能被解析。链接错误.

故, 被链接模块需要按照调用顺序指定, 可以重复指定. e.g: gcc -static -o myproc main.o x.a y.a z.a x.a

连接器对外部引用的解析算法

  • 按照命令行给出的顺序扫描.o 和.a 文件
  • 扫描期间将当前未解析的引用记录到一个列表U中
  • 每遇到一个新的.o 或 .a 中的模块,都试图用其来解析U中的符号
  • 如果扫描到最后,U中还有未被解析的符号,则发生错误
  • 问题: 能否正确解析与命令行给出的顺序有关
  • 对策: 将静态库放在命令行的最后

库名缩写: -lxxx = libxxx.a

链接举例

  1. 假设调用关系如下:

func.o → libx.a 和 liby.a 中的函数

libx.a → libz.a 中的函数

libx.a 和 liby.a 之间、liby.a 和 libz.a 相互独立

则以下几个命令行都是可行的:

gcc -static –o myfunc func.o libx.a liby.a libz.a
gcc -static –o myfunc func.o liby.a libx.a libz.a
gcc -static –o myfunc func.o libx.a libz.a liby.a
  1. 假设调用关系如下:

func.o → libx.a 和 liby.a 中的函数

libx.a → liby.a 同时 liby.a → libx.a

则以下命令行可行

gcc -static –o myfunc func.o libx.a liby.a libx.a

可以画树状图确定调用顺序, 逐级调用, 按级顺序手动链接

链接操作的步骤

  1. 符号解析, 建立符号引用与符号定义之间的关联. 得到E(将要合并的.o文件的集合), D(定义符号的集合)
  2. 将同类型的节合并为代码段与数据段. 合并E.
  3. 确定符号定义的地址. 确定D中符号的地址.
  4. 修改符号引用的地址

Step1: 符号解析(上述1). 得到E, D

Step2: 重定位(上述234). 合并E. 确定D中符号地址.

重定位

Step1 合并相同的节

  • 将集合E的所有目标模块中相同的节合并成新节. 例如,所有.text节合并作为可执行文件中的.text节

Step2 对定义符号进行重定位(确定地址)

  • 确定新节中所有定义符号在虚拟地址空间中的地址. 例如,为函数确定首地址,进而确定每条指令的地址,为变量确定首地址
  • 完成这一步后,每条指令和每个全局或局部变量都可确定地址

Step3 对引用符号进行重定位(确定地址)

  • 修改.text节和.data节中对每个符号的引用(地址). 需要用到在.rel_data和.rel_text节中保存的重定位信息

IA-32的两种最基本的重定位类型:

  • R_386_32: 绝对地址
  • R_386_PC32: PC相对地址
posted @ 2023-05-23 16:00  CINKUK  阅读(83)  评论(0)    收藏  举报