技术标签: 计算机系统
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。
链接器必须完成的两个任务:
三种形式:
可重定位目标文件,包含二进制代码和数据的文件,可与其他可重定位文件合并,生成可执行目标文件。
可执行目标文件,包含二进制代码和数据的文件,可直接复制至内存并执行。
共享目标文件,特殊的可重定位目标文件,可在加载或运行时动态地加载至内存并链接。
编译器和汇编器生成可重定位目标文件,链接器生成可执行目标文件。
典型的ELF可重定位目标文件的格式,如下:
图1 典型的ELF可重定位目标文件
ELF中各节功能及意义:
类型 | 功能及意义 |
---|---|
ELF头 | 以16字节序列开始,描述生成该文件的系统的字的大小和字节顺序 |
.text | 已编译程序的机器代码 |
.rodata | 只读数据 |
.data | 已初始化的全局和静态变量 |
.bas | 未初始化的全局和静态变量 |
.symtab | 存放程序中定义和引用的函数和全局变量的信息的符号表 |
.rel .text | .text节中位置列表,在组合目标文件和其他文件时,需修改这些位置 |
.rel .data | 被模块引用或定义的所有全局变量的重定位信息 |
.debug | 调试符号表,包含程序中定义的局部变量、定义和引用的全局变量以及原始的C源文件 |
.line | 原始C源程序中的行号和.text节中机器指令之间的映射 |
.strtab | 字符串表,包括.symtab和.debug节中的符号表,以及节头部中的节名字 节点部表 |
链接的上下文中,三种不同的符号:
符号表由汇编器构造,.symtab节中包含ELF符号表,符号表包含一个条目的数据,每个条目的格式如下:
typedef struct {
int name; /*字符串表中的字节偏移*/
char type : 4, /*函数或数据 4字节*/
binding : 4; /*本地或全局 4字节*/
char reserved; /*未定义的符号*/
short section; /*节头部表的索引,指定分配到目标文件的某个节*/
long value; /*距定义目标的节的起始位置偏移*/
long size; /*目标的大小*/
} Elf64_Symbol;
链接器解析符号是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。
解析多重定义的全局符号规则
与静态库链接
将所有相关的目标模块打包成一个单独的文件,称为静态库。
相关的函数可以被编译为独立的目标模块,然后封装成一个单独的静态库文件。
链接时,链接器只复制被程序引用的目标模块,从而减少了可执行文件在磁盘和内存中的大小。
静态库使用示例:
#include <stdio.h>
#include "vector.h"
#include "windows.h"
int x[2] = {
1, 2 };
int y[2] = {
3, 4 };
int z[2];
int main() {
addvec(x, y, z, 2);
printf("z=[%d %d]\n", z[0], z[1]);
system("pause");
return 0;
}
void addvec(int*, int*, int*, int);
void multivec(int*, int*, int*, int);
int addcnt = 0;
void multivec(int* x, int* y, int* z, int n) {
int i;
addcnt++;
for (i = 0; i < n; i++)
z[i] = x[i] + y[i];
}
int multicnt = 0;
void addvec(int* x, int* y, int* z, int n) {
int i;
multicnt++;
for (i = 0; i < n; i++)
z[i] = x[i] * y[i];
}
分别执行以下指令,则生成可执行目标文件file。
gcc -c addvec.c multivec.c
ar rcs libvector.a addvec.o multivec.o
gcc -c main.c
gcc -static -o file main.c libvector.a
链接器行为如下图所示:
图2 与静态库链接
链接器如何使用静态库来解析引用
符号解析阶段,链接器从左到右按照命令行上出现的次序来扫描可重定位目标文件和存档文件。
链接器维护一个可重定位目标文件集合 E E E,一个未解析符号集合 U U U,一个在前面输入文件已经定义的符号集 D D D。初始时,各集合全空。
因此,命令行上库和目标文件的次序非常重要。因保证定义一个符号的库在引用这个符号的目标文件之后。
如foo.c调用libx.a中的函数,该库又调用liby.a中的函数,而liby.a有调用libx.a中的函数,则命令行格式为:
gcc foo.c libx.a liby.a libx.a
即libx.a需重复出现,亦可将libx.a和liby.a合并。
重定位就是把程序的逻辑地址空间变换成内存中的实际物理地址空间的过程。
完成符号解析后,代码中的每个符号和一个符号定义完成关联,此时链接器开始重定位。
重定位条目
汇编器生成目标模块时,对数据和代码在内存中的位置、模块引用的外部定义的函数或全局变量的位置均未知。对这些未知的引用都会生成一个重定位条目,用于指导链接器在合并阶段如何修改这个引用。
ELF重定位条目的格式:
typedef struct {
long offset; /*需要被修改的引用的节偏移*/
long type : 32, /*告知链接器如何修改新的引用*/
symbol : 32; /*符号表索引*/
long addend; /*有符号常数,对修改引用的偏移做调整*/
}Elf64_Rela;
两种基本的重定位类型:
重定位符号引用
假设每个节s是一个字节数组,每个重定位条目r是一个类型为Elf64_Rela的结构。
重定位符号引用时,链接器已经为每个节(ADDR(s))和每个符号(ADDR(r.symbol))都选择了运行时的地址。
伪重定位算法:
refptr = s + r.offset;
if (r.type == R_X86_64_PC32) {
refaddr = ADDR(s) + r.offset;
*refptr = (unsigned)(ADDR(r.symbol) + r.addend - refaddr);
}
if (r.type == R_X86_64_32)
*refptr = (unsigned)(ADDR(r.symbol) + r.addend);
重定位如下实例程序的引用:
1 int sum(int* a, int n);
2
3 int array[2] = {
1, 2};
4
5 int main() {
6 int val = sum(array, 2);
7 return val;
8 }
1 int sum(int* a, int n) {
2 int i, s = 0;
3
4 for(i = 0; i < n; i++)
5 s += a[i];
6 }
7 return s;
8 }
main.o的反汇编代码:
// main.o
1 0000000000000000 <main>:
2 0: 48 83 ec 08 sub $0x8, %rsp
3 4: be 02 00 00 00 mov $0x2, %esi
4 9: bf 00 00 00 00 mov $0x0, %edi
5 a: R_X86_64_32 array
6 e: e8 00 00 00 00 callq 13 <main+0x13>
7 f: R_X86_64_PC32 sum-0x4
8 13: 48 83 c4 08 add $0x8, %rsp
9 17: c3 retq
链接器修改从偏移量0xf开始的32位PC相对引用,使程序指向sum入口地址。:
r e f a d d r = A D D R ( s ) + r . o f f s e t = 0 × 4004 d 0 + 0 × f = 0 × 4004 d f \,\,\begin{array}{l} refaddr=\,\,ADDR\left( s \right) \,\,+\,\,r.offset\\ \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\ \ =\,\,0\times 4004d0\,\,+\,\,0\times f\\ \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\ \ =\,\,0\times 4004df\\ \end{array} refaddr=ADDR(s)+r.offset =0×4004d0+0×f =0×4004df
∗ r e f a d d r = ( u n s i g n e d ) ( A D D R ( r . s y m b o l ) + r . a d d e n d − r e f a d d r ) = ( u n s i g n e d ) ( 0 × 4004 e 8 + ( − 4 ) − 0 × 4004 d f ) = ( u n s i g n e d ) ( 0 × 5 ) \,\,\begin{array}{l} *refaddr=\,\,\left( unsigned \right) \ \left( ADDR\left( r.symbol \right) \ +\ r.addend\ -\ refaddr \right)\\ \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,=\,\,\left( unsigned \right) \ \left( 0\times 4004e8\ \ \ \ \ \ \ \ \ +\,\,\,\,\, \left( -4 \right) \ \ \ \ \ -\ 0\times 4004df \right) \,\\ \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,=\,\,\left( unsigned \right) \ \left( 0\times 5 \right)\\ \end{array} ∗refaddr=(unsigned) (ADDR(r.symbol) + r.addend − refaddr)=(unsigned) (0×4004e8 +(−4) − 0×4004df)=(unsigned) (0×5)
得到下面的重定位形式:
4004de: e8 05 00 00 00 callq 4004e8 <sum>
运行时,call指令存放在 0 × 4004 d e 0\times4004de 0×4004de处,CPU执行call指令时,PC指向下一条指令即 0 × 4004 e 3 0\times4004e3 0×4004e3。由于相对地址偏移位 0 × 5 0\times5 0×5,读PC新值为 0 × 4004 e 3 + 0 × 5 = 0 × 4004 e 8 0\times4004e3 + 0\times5 = 0\times4004e8 0×4004e3+0×5=0×4004e8,刚好指向sum入口地址。
2.重定位绝对引用
对于array条目:
r . o f f s e t = 0 × a r . s y m b o l = a r r a y r . t y p e = R _ X 86 _ 64 _ 32 r . a d d e n d = 0 \,\begin{matrix}{} \,\,\,\,\,\,\,\,\,\,\,\,\,\,r.offset\ \ =\ 0\times a\\ \,\,\,\,\,\,\,\,\,\,\,\,\,\,r.symbol\ =\ array\\ \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,r.type =\ R\_X86\_64\_32\\ r.addend\ =\ 0\\ \end{matrix} r.offset = 0×ar.symbol = arrayr.type= R_X86_64_32r.addend = 0
已知 m a i n main main首地址 A D D R ( s ) = A D D R ( . t e x t ) = 0 × 4004 d 0 ADDR\left( s \right) \ =\ ADDR\left( .text \right) \ =\ 0\times 4004d0 ADDR(s) = ADDR(.text) = 0×4004d0和 a r r a y array array首地址 A D D R ( r . s y m b o l ) = A D D R ( a r r a y ) = 0 × 601018 ADDR\left( r.symbol \right) \ =\ ADDR\left( array \right) \ =\ 0\times 601018 ADDR(r.symbol) = ADDR(array) = 0×601018
链接器修改从偏移量 0 × 0\times 0×开始的绝对引用,使程序指向 a r r a y array array的第一个字节。
r e f a d d r = A D D R ( s ) + r . o f f s e t = 0 × 4004 d 0 + 0 × a = 0 × 4004 d a \,\,\begin{array}{l} refaddr=\,\,ADDR\left( s \right) \,\,+\,\,r.offset\\ \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\ \ =\,\,0\times 4004d0\,\,+\,\,0\times a\\ \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\ \ =\,\,0\times 4004da\\ \end{array} refaddr=ADDR(s)+r.offset =0×4004d0+0×a =0×4004da
∗ r e f a d d r = ( u n s i g n e d ) ( A D D R ( r . s y m b o l ) + r . a d d e n d ) = ( u n s i g n e d ) ( 0 × 601018 + 0 ) = ( u n s i g n e d ) ( 0 × 601018 ) \,\,\begin{array}{l} *refaddr=\,\,\left( unsigned \right) \ \left( ADDR\left( r.symbol \right) \ +\ r.addend\ \right)\\ \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,=\,\,\left( unsigned \right) \ \left( 0\times 601018\ \ \ \ \ \ \ \ \ +\,\,\,\,\, 0 \ \ \right) \,\\ \,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,\,=\,\,\left( unsigned \right) \ \left( 0\times 601018 \right)\\ \end{array} ∗refaddr=(unsigned) (ADDR(r.symbol) + r.addend )=(unsigned) (0×601018 +0 )=(unsigned) (0×601018)
得到下面的重定位形式:
4004d9: bf 18 10 60 00 mov $0x601018, %edi
已重定位的.text节,如下:
1 00000000004004d0 <main>:
2 4004d0: 48 83 ec 08 sub $0x8, %rsp
3 4004d4: be 02 00 00 00 mov $0x2, %esi
4 4004d9: bf 18 10 60 00 mov $0x601018, %edi
5 4004de: e8 05 00 00 00 callq 4004e8 <sum>
6 4004e3: 48 83 c4 08 add $0x8, %rsp
7 4004e7: c3 retq
8 00000000004004e8 <sum>:
9 4004e8: b8 00 00 00 00 mov $0x0, %eax
10 4004ed: ba 00 00 00 00 mov $0xx, %edx
11 4004f2: eb 09 jmp 4004fd <sum+0x15>
12 4004f4: 48 63 ca movslq %edx, %rcx
13 4004f7: 03 04 8f add (%rdi, %rcx, 4), %eax
14 4004fa: 83 c2 01 add $0x1, %edx
15 4004fd: 39 f2 cmp %esi, %edx
16 4004ff: 7c f3 jl 4004f4 <sum+0xc>
17 400501: f3 c3 repz retq
已重定位的.data节,如下:
1 000000000601018 <array>:
2 601018: 01 00 00 00 02 00 00 00
典型的ELF可执行文件中的各类信息,如下:
图2 典型的ELF可执行目标文件
格式类似于可重定位目标文件格式。.init节中定义_init函数,代码初始化时调用。
可执行文件prog的程序头部表,如下:
Read-only code segment
1 Load off 0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**21
2 filesz 0x000000000000069c memsz 0x000000000000069c flag r-x
Read/write data segment
3 Load off 0x0000000000000df8 vaddr 0x0000000000600df8 paddr 0x0000000000600df8 align 2**21
4 filesz 0x0000000000000228 memsz 0x0000000000000230 flag rw-
off:目标文件中的偏移; vaddr/paddr:内存地址; align:对齐要求; filesz:目标文件中的段大小;memsz:内存中的段大小; flags:运行时访问权限。
1和2行(代码段),只读权限,开始于内存地址 0 × 400000 0\times400000 0×400000处,总共内存大小 0 × 69 c 0\times69c 0×69c,被初始化为可执行目标文件的头 0 × 69 c 0\times69c 0×69c个字节。
3和4行(数据段),读写权限,开始于内存地址0x600df8处,总内存大小 0 × 230 0\times230 0×230字节,初始化为从目标文件中偏移 0 × d f 8 0\times df8 0×df8处开始的.data节中的 0 × 228 0\times228 0×228个字节初始化。
对于任何段s,起始地址满足:vaddr mod align = off mod align。优化对齐,便于目标文件中的段高效地传送至内存。
系统调用加载器将可执行目标文件的代码和数据从磁盘复制到内存,然后跳转至入口地址来运行程序,这一过程称为加载。
图3 Linux x86-64运行时内存映像
代码段总是从 0 × 400000 0\times400000 0×400000处开始,后面是数据段。堆在数据段之后,通过调用malloc向上增长。用户栈总是从最大的合法用户地址 2 48 − 1 2^{48}-1 248−1处开始。
共享库,用于解决多个进程调用相同静态库造成的内存浪费问题。
共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并在内存中的程序链接起来(动态链接)。
共享库(so)中的代码和数据不会复制到引用它们的可执行文件中。
在内存中,共享库的.text节副本可被不同的正在运行的进程共享。
图4 动态链接共享库
使用动态链接共享库的命令行参数,如下。注:后缀.so和.dll均可。
gcc -shared -fpic -o libvector.so addvec.c multivec.c
gcc -o prog main.c libvector.so
动态链接的功能:
可以加载而无需重定位的代码称为位置无关代码(Position-Independent Code,PIC)。
PIC数据引用
无论在内存中的何处加载一个目标模块,数据段和代码段的距离总是保持不变。
因此,代码段中的任何指令和数据段中任何变量之间的距离为常量。
基于上述原理,编译器在数据段开始处创建全局偏移量表(Global Offset Table, GOT),实现对全局变量PIC引用。
PIC函数调用
共享模块在运行时,随机加载到内存的任何位置,编译器无法预测其函数的运行地址。
GNU编译系统使用延迟绑定,将过程地址的绑定推迟到函数的第一次调用时。基于GOT和过程连接表(PLT)的交互实现。
允许截获对共享库函数的调用,取而代之执行自己的代码。
打桩可发生在编译、链接以及程序加载和执行时。
文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文
文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作 导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释: cwy_init/init_123..._达梦数据库导入导出
文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js
文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6
文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输
文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...
文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure
文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割
文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答
文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。
文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入
文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf