静态链接过程分析

news2024/12/24 8:10:52

前期准备

这边使用《程序员的自我修养》中的例子

//a.cpp
extern int shared;

void swap(int* a, int *b);

int main(){

    int a = 100;
    swap(&a, &shared);
}
//b.cpp
int shared = 1;

void swap(int* a, int* b){
    *a ^= *b ^= *a ^= *b;
}

通过gcc -c 命令编译出相对应的.o文件,通过使用realelf对这些.o文件进行分析,可以看到b.o中有定义两个全局符号,一个是shared的变量,一个是_Z4swapPiS_的函数。而a.o中可以看到对这两个符号的引用。

接下来要分析如何将这两个文件合并成一个可执行文件。

空间地址的分配

链接器的主要功能就是将多个输入文件进行加工,然后输出到一个文件中。而静态链接是如何做到将不同目标文件的各个段进行合并的呢?

按序叠加

首先可以想到一种最简单的方法,就是按顺序叠加,即将各个目标文件依次进行合并

这种方式看起来简单易于实现,但会有一个问题,那就是在目标文件数量很多的情况下,链接形成的可执行文件会有非常非常多的段,而对于x86的机器来说,段加载到虚拟空间后的对齐单位是按页来的,也就是说这么多的段会占用非常大的空间。

这边所讲到的空间有两个意思:

  1. 一个是可执行文件的磁盘空间

  1. 一个是可执行文件被装载之后的虚拟地址空间

这边分析的时候指的都是第2个意思,即指只分析被加载之后的空间地址分配。

相似段合并

基于上面的空间浪费,另一种更好的解决方法的是将相同性质的段进行合并。比如将a.o 和b.o的.text段合成成一个.text,这样就可以减少内存碎片的产生。

链接过程

实际上现在链接器基本上也是采用相似段合并这种方式。这种方式的链接会分成两个部分

  • 空间与地址分配: 扫描所有输入的文件,获取各个文件中段的属性,如长度、位置等。然后将所有目标文件的符号表的定义和引用收集起来放到统一的全局符号表中。在这一步,链接器就能进行合并相似段的合并。

  • 符号解析与重定位:使用第一步收集到的信息,读取收集到的数据、重定位信息等,进行符号解析与重定位、调整代码中的地址等。第二部实际上是链接过程的核心,特别是重定位过程。

使用ld链接器将a.o 和 b.o链接起来:

ld a.o b.o -e main -o ab
// -e 选项是选择main作为程序入口,默认使用的程序入口是_start
// -o 表示链接输出的文件时ab.

可以通过objdump来链接前后各个段的属性

>>> objdump -h a.o 

a.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000029  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  00000069  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0
 
>>> objdump -h b.o

b.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000004b  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  0000000000000000  0000000000000000  0000008c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000090  2**0
                  ALLOC 

 
>>> objdump -h ab

ab:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .note.gnu.property 00000020  0000000000400190  0000000000400190  00000190  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .text         00000074  0000000000401000  0000000000401000  00001000  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .eh_frame     00000058  0000000000402000  0000000000402000  00002000  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .data         00000004  0000000000404000  0000000000404000  00003000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  4 .comment      0000001c  0000000000000000  0000000000000000  00003004  2**0
                  CONTENTS, READONLY

VMA(virtual memory address, 虚拟地址) 和 LMA(load memory address,加载地址) 一般来说是一样的,但在某些嵌入式系统中是由区别的,具体可以看https://github.com/cisen/blog/issues/887这篇帖子的解释。

这边只关心VMA 和 SIZE即可,从上面的例子中可以看到链接之前各个段的VMA是0,这是因为虚拟地址还没确定;而在链接之后VMA就由对应的值了, .text段被分配到0X0000000000401000这个地址了(因为编译环境是64位,所以和书上略由出入),大小为0X74个字节(0X4B+0X29)。.date段被分配到0000000000404000这个地址中,大小为0X4个字节。

符号解析与重定位

首先看一下在编译之前,a.o中是如何使用swap函数 和 变量share,使用objdump -d 可以看到a.o的 main的反编译代码。

>>> objdump -d a.o 

a.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 83 ec 10             sub    $0x10,%rsp
   8:   c7 45 fc 64 00 00 00    movl   $0x64,-0x4(%rbp)
   f:   48 8d 45 fc             lea    -0x4(%rbp),%rax
  13:   48 8d 35 00 00 00 00    lea    0x0(%rip),%rsi        # 1a <main+0x1a>
  1a:   48 89 c7                mov    %rax,%rdi
  1d:   e8 00 00 00 00          callq  22 <main+0x22>
  22:   b8 00 00 00 00          mov    $0x0,%eax
  27:   c9                      leaveq 
  28:   c3                      retq  

可以看到main函数一共41(0X29)个字节,在0X16~0X19个字节就是对share 变量的引用(也就是 #1a<main + 0x1a>的那行), 而 0X1E出就是对函数swap的引用(也就是<main + 0x22>的位置)。 因为a.o中还不知道这些需要重定位对象的地址,所以引用的地址都先赋值成0。

然后在对ab使用objdump,查看ab重定位之后的反汇编代码。

>>> objdump -d ab

ab:     file format elf64-x86-64


Disassembly of section .text:

0000000000401000 <main>:
  401000:       55                      push   %rbp
  401001:       48 89 e5                mov    %rsp,%rbp
  401004:       48 83 ec 10             sub    $0x10,%rsp
  401008:       c7 45 fc 64 00 00 00    movl   $0x64,-0x4(%rbp)
  40100f:       48 8d 45 fc             lea    -0x4(%rbp),%rax
  401013:       48 8d 35 e6 2f 00 00    lea    0x2fe6(%rip),%rsi        # 404000 <shared>
  40101a:       48 89 c7                mov    %rax,%rdi
  40101d:       e8 07 00 00 00          callq  401029 <_Z4swapPiS_>
  401022:       b8 00 00 00 00          mov    $0x0,%eax
  401027:       c9                      leaveq 
  401028:       c3                      retq   

0000000000401029 <_Z4swapPiS_>:
  401029:       55                      push   %rbp
  ........

可以看到在main中,已经将之前不知道的函数和变量重新进行了定位,将正确的地址填入了之前都是0的位置。

重定位表

编译器将需要重新定位的变量或者函数统一放在一个位置中,用一个结构进行管理,这个位置结构就是重定位表,在ELF文件中,它会是一个或者多个的section,比如.rel.text就是对.text的重定位表。

使用readelf -r 或者objdump -r 可以看到所有重定位表的内容

>>> readelf -r a.o

Relocation section '.rela.text' at offset 0x430 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000016  000e00000002 R_X86_64_PC32     0000000000000000 shared - 4
00000000001e  001000000004 R_X86_64_PLT32    0000000000000000 _Z4swapPiS_ - 4

...

重定位表根据section 的类型决定使用使什么样的结构进行存储

/* Relocation table entry without addend (in section of type SHT_REL).  */
typedef struct
{
  Elf32_Addr    r_offset;       /* Address */
  Elf32_Word    r_info;         /* Relocation type and symbol index */
} Elf32_Rel;

typedef struct
{
  Elf64_Addr    r_offset;       /* Address */
  Elf64_Xword   r_info;         /* Relocation type and symbol index */
} Elf64_Rel;

/* Relocation table entry with addend (in section of type SHT_RELA).  */

typedef struct
{
  Elf32_Addr    r_offset;       /* Address */
  Elf32_Word    r_info;         /* Relocation type and symbol index */
  Elf32_Sword   r_addend;       /* Addend */
} Elf32_Rela;

typedef struct
{
  Elf64_Addr    r_offset;       /* Address */
  Elf64_Xword   r_info;         /* Relocation type and symbol index */
  Elf64_Sxword  r_addend;       /* Addend */
} Elf64_Rela;

当section的类型是SHT_RELA 的时候,使用的是Elf32_Rela/Elf64_Rela进行存储;当secion的类型是SHT_REL的时候,使用的是Elf32_Rel/Elf64_Rel进行存储。

  • r_offset对于目标文件来说,就是重定位符号在对应段中的偏移,相当于readelf中的Offset;而对于动态库文件来说,是需要重定位符号的第一个字节的虚拟地址。

  • r_info 在32位的情况下,低8位是符号的重定位方式,高24位是符号在符号表中的下标;而在64位的情况下,低32位是 符号的重定位方式,高32位是符号在符号表中的下标。

  • r_append是在SHT_RELA中计算重定位偏移的时候使用的。

重定位修正

这边定义三个符号:

  • A:保存在被修正位置的值,当重定位项是RELA类型时,这个值被保存在r_append中

  • P:被修正位置的地址

  • S:符号的实际地址

修正指令的方式分为两种,一种是是相对寻址修正,一种是绝对寻址修正。

  • 绝对地址修正公式: S + A

  • 相对地址修正公式: S + A - P

分析ab的重定位过程

先通过readelf -s得到符号的实际地址

>>> readelf -s ab

Symbol table '.symtab' contains 19 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
    ......
    11: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS b.cpp
    12: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS a.cpp
    13: 0000000000401029    75 FUNC    GLOBAL DEFAULT    2 _Z4swapPiS_
    14: 0000000000404000     4 OBJECT  GLOBAL DEFAULT    4 shared
    15: 0000000000404004     0 NOTYPE  GLOBAL DEFAULT    4 __bss_start
    16: 0000000000401000    41 FUNC    GLOBAL DEFAULT    2 main
    ....

可以知道shared的实际地址是0X404000,由之前的重定位表可知,shared的重定位类型是R_X86_64_PC32,这个类型的寻址方式是相对寻址,所以重新修正位置应该填入的值为:0X404000- 0x4 - 0X401016 = 2FE6.可以看到0X401016这个位置被修正后确实是2FE6的小端序存储。

接下来是_Z4swapPiS_函数的重定位,_Z4swapPiS_函数的实际地址是0X401029, 重定位类型是R_X86_64_PLT32,这个类型也是一个相对寻址,所以重新修正位置应该填入的值为:0X401029- 0x4 - 0X40101e = 0X7.

总结

这边通过一系列的实验来分析了静态链接时链接器要做的主要的事情,其中重定位是链接过程中比较重要的过程。

这边实验的平台使用的X64环境下的, 与书中的环境不太一致,所以导致readelf -r的输出的重定位类型不太一样,但基本结果还是服合预期的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/165550.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

五,Spring Bean生命周期

1 Spring Bean的生命周期&#xff08;概念重点&#xff09; 对象的生命周期&#xff1a;对象从生到死的过程。 Spring工厂中Bean的生命周期并不像想象的那么简单&#xff0c;Spring对工厂中的Bean的生命周期进行了细致的划分&#xff0c;并允许开发者通过编码或配置的方式定制…

Ubuntu18 sqlyog配置mysql5.7远程连接

mysql 配置远程连接 1. mysql安装和配置 sudo apt-get install mysql-server-5.7 systemctl status mysql service mysql status修改mysql的配置文件&#xff1a; sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf以下为mysqld.cnf文件主要内容&#xff0c;这里的skip-grant-ta…

基于51单片机的pm2.5空气质量监测仪仿真设计

51单片机pm2.5监测仪仿真设计( proteus仿真程序报告讲解视频&#xff09; 仿真图proteus 7.8及以上 程序编译器&#xff1a;keil 4/keil 5 编程语言&#xff1a;C语言 设计编号&#xff1a;S0032 51单片机pm2.5监测仪仿真设计主要功能&#xff1a;讲解演示视频仿真程序设计…

代码整洁提升方案

验-言 公共方法都要做参数的校验&#xff0c;参数校验不通过明确抛出异常或对应响应码&#xff1a; Java Bean验证已经是一个很古老的技术了&#xff0c; 会避免我们很多问题&#xff1b; 在接口中也明确使用验证注解修饰参数和返回值&#xff0c; 作为一种协议要求调用方按…

win11 arm 系统安装安卓子系统

一般的x86电脑如果安装android子系统&#xff0c;运行安卓子系统&#xff0c;由于要将android arm代码转译为x86代码&#xff0c;所以效率不一定高&#xff0c;但是如果电脑是arm架构的&#xff0c;通过安卓子系统运行android的程序执行效率就会 高不少&#xff0c;本文参考,都…

JVM面试题

Java内存区域 说一下 JVM 的主要组成部分及其作用&#xff1f; JVM包含两个子系统和两个组件&#xff0c;两个子系统为Class loader(类装载)、Execution engine(执行引擎)&#xff1b;两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。 Class loader…

Es6的Promise

Promise是异步编程的一种解决方案。简单来说Promise就是一个用来存储数据的对象&#xff0c;它有着一套特殊的存取数据的方式。可以解决异步回调函数/回调地狱问题。创建Promise1.创建Promise时需要一个 回调函数 作为参数这个回调函数会在Promise时 自动调用2.调用回调函数时&…

基于matlab的指纹图像处理、脊线增强、脊线分割、脊线细化、细节点检测和细节点验证

需求分析对于指纹的特征提取包含几个步骤&#xff0c;脊线增强、脊线分割、脊线细化、细节点检测和细节点验证&#xff0c;本次大作业需要针对已经增强的指纹图片进行后续几个步骤&#xff0c;通过多种形态学算法进行分割、细化、细化后处理&#xff0c;找到其中的端点和分叉点…

elasticsearch基础2——es配置文件、jvm配置文件详解

文章目录一、配置文件详解1.1 elasticsearch.yml文件1.1. 1 基础参数1.1.1.1 自定义数据/日志目录1.1.1.2 锁定物理内存1.1.1.3 跨域设置1.1.1.4 其他参数1.1.2 集群类1.1.3 分片类1.1.4 IP绑定类1.1.5 端口类1.1.6 交互类1.1.5 Xpcak安全认证1.1.5.1 xpack内置用户1.1.5.2 xpa…

LabVIEW使用VI脚本向VI添加对象

LabVIEW使用VI脚本向VI添加对象可使用VI脚本向前面板和程序框图添加对象。该教程以向程序框图添加对象为例。按照下列步骤&#xff0c;通过VI脚本向VI添加对象。创建VI前&#xff0c;需先了解VI脚本的基本内容。必须启用VI脚本&#xff0c;才能显示VI脚本选板&#xff0c;使用相…

aws beanstalk 理解和使用eb工作线程环境

参考资料 beanstalk 工作线程环境beanstalk 工作线程环境管理https://catalog.us-east-1.prod.workshops.aws/workshops/b317e4f5-cb38-4587-afb1-2f75be25b2c0/en-US/03-provisionresources 理解 beanstalk 工作线程环境 https://docs.amazonaws.cn/elasticbeanstalk/latest…

【Java基础】-【线程】

文章目录创建线程的方式Thread类的常用方法run()和start()有什么区别&#xff1f;线程是否可以重复启动&#xff0c;有什么后果&#xff1f;线程的生命周期实现线程同步Java多线程之间的通信方式sleep()和wait()的区别notify()、notifyAll()的区别如何实现子线程先执行&#xf…

GaussDB(DWS)数据库的数据迁移实操【玩转PB级数仓GaussDB(DWS)】

GaussDB(DWS)数据库的数据迁移实操【玩转PB级数仓GaussDB(DWS)】 1.1先了解一下Gauss数据库 Gauss数据库并非完全自主开发。它可以看作是基于PostgreSQL 9.2的一次神奇的修改。正如Redhat和Android都源于LINUX的研发&#xff0c;IBM AIX和IOS都源于UNIX的研发一样&#xff0c;…

16、ThingsBoard-配置OAuth2

1、概述 如果你的系统想要接入第三方认证来登录,就像国内很多网站都支持微信、QQ等授权登录,其实thingsboard也提供了OAuth2.0来支持,ThingsBoard 是支持授权码授权类型来交换访问令牌的授权码,同时它自己也提供了几种方式 Google、GitHub、Facebook、Apple 同时也支持自定…

使用numpy进行深度学习代码实战

使用方法定义网络from net import ConvNet net ConvNet() if not net.load(MODEL_PATH): net.addConvLayout([3,3,1,4],bias True,paddingVAILD,init_typeinit_type,st_funcLEAKY_RELU_0.01)net.addConvLayout([3,3,4,8],bias True,paddingVAILD,init_typeinit_type,st…

weblogic反序列化之T3协议

前言 weblogic 的反序列化漏洞分为两种 &#xff0c;一种是基于T3 协议的反序列化漏洞&#xff0c;一个是基于XML的反序列化漏洞&#xff0c;这篇来分析一下基于T3 协议的反序列化漏洞。 环境搭建&#xff1a; [JAVA安全]weblogic反序列化介绍及环境搭建_snowlyzz的博客-CSDN…

Virtualbox设置固定IP

Virtualbox桥接实现静态固定IP内外网访问 super_kancy 2018-10-20 11:55:28 6024 收藏 7 展开 桥接实现静态固定IP内外网访问 第一步、安装好一个虚拟机linux01 第二步、配置网卡&#xff0c;选择桥接网卡模式&#xff0c;并且指定桥接的具体的网卡 第三步、正常启动虚拟机lin…

【Java集合】Collection 体系集合详解(ArrayList,LinkedList,HashSet,TreeSet...)

文章目录1. 概念2. 集合和数组的区别3. 集合的体系结构4. Collection父接口5. List 子接口6. List 实现类6.1 ArrayList 类6.2 Vector 类6.3 LinkedList 类6.4 ArrayList和LinkedList的区别7. Set 子接口8. Set 实现类8.1 HashSet 类8.2 TreeSet 类9. Collections 工具类Java编…

【链表经典题目】总结篇

【链表经典题目】总结篇1 虚拟头结点2 链表的基本操作3 反转链表4 删除倒数第N个节点5 链表相交6 环形链表总结【链表】关于链表&#xff0c;你该了解这些&#xff01; 1 虚拟头结点 在链表&#xff1a;听说用虚拟头节点会方便很多&#xff1f; 中&#xff0c;我们讲解了链表…

简单了解OSI网络模型

本文为学习笔记&#xff0c;根据了解需求摘抄自下篇文章 参考&#xff1a;原文地址 作者&#xff1a;sunsky303 目录 OSI模型 TCP/IP分层模型 OSI模型 OSI 模型(Open System Interconnection model)&#xff08;七层网络模型&#xff09;是一个由国际标准化组织提出的概念模…