Linux LD链接器 -静、动库编译or链接

news2024/11/29 13:42:20

文章目录

    • 程序链接
      • 动态链接
      • 静态链接
    • 目标文件链接
      • 打包为动态
      • 打包为静态
      • 总结
    • 动态链接
            • - 动态链接:在运行、加载时,在内存中完成链接的过程
            • - 动态共享库:用于动态链接的系统库、特性是可以加载无需重定位的代码
      • got表(Global Offset Table)
      • 延时绑定

程序链接

动态链接

首先存在如下main.c:

#include "stdio.h" //因为这个文件使用了printf函数所以需要引入系统文件

int func(int a,int b);

int main(){
   printf("func -> %d",func(1,2));;
   return 0;
}

使用最简单编译命令gcc main.c是会报错的,该命令会执行编译+链接操作,因为这个代码中func函数只进行了声明,并没有定义,那么可以使用gcc -c main.c来告诉编译器只需要编译,不链接

再来看看编译后的文件信息

$ file main.o
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
#这个信息就表示输出文件为编译后、可重定位的 ELF 文件可以用于链接成可执行文件或共享对象文件,而被剥离的 ELF 文件可以去掉调试信息。SYSV 是 Unix 系统 V 的一种变体,是 Linux 系统采用的标准。

文件内容信息:

$ objdump -d --section=.text main.o 
0000000000000000 <main>:
   0:	f3 0f 1e fa          	endbr64
   4:	55                   	push   %rbp
   5:	48 89 e5             	mov    %rsp,%rbp
   8:	be 02 00 00 00       	mov    $0x2,%esi
   d:	bf 01 00 00 00       	mov    $0x1,%edi
  12:	e8 00 00 00 00       	call   17 <main+0x17> #可以看到这里的函数调用没有实际的调用地址
  17:	89 c6                	mov    %eax,%esi
  19:	48 8d 05 00 00 00 00 	lea    0x0(%rip),%rax        # 20 <main+0x20>
  20:	48 89 c7             	mov    %rax,%rdi
  23:	b8 00 00 00 00       	mov    $0x0,%eax
  28:	e8 00 00 00 00       	call   2d <main+0x2d> #同时调用的printf函数也没有地址
  2d:	b8 00 00 00 00       	mov    $0x0,%eax
  32:	5d                   	pop    %rbp
  33:	c3                   	ret

再来实现上面的func(int a,int b)函数定义,不过为了演示程序链接,我将函数的定义放在另一个文件中实现,新建文件func.c:

int func(int a,int b){
    return a+b;
}

同样使用gcc -c func.c来编译这个文件为一个可用于链接共享程序

$ objdump -d --section=.text func.o
0000000000000000 <func>:
   0:	f3 0f 1e fa          	endbr64
   4:	55                   	push   %rbp
   5:	48 89 e5             	mov    %rsp,%rbp
   8:	89 7d fc             	mov    %edi,-0x4(%rbp)
   b:	89 75 f8             	mov    %esi,-0x8(%rbp)
   e:	8b 55 fc             	mov    -0x4(%rbp),%edx
  11:	8b 45 f8             	mov    -0x8(%rbp),%eax
  14:	01 d0                	add    %edx,%eax
  16:	5d                   	pop    %rbp
  17:	c3                   	ret

可以看到这个文件并没有依赖任何其他外部的引用,只不过这个程序没有main函数并不能直接执行,所以它就起到了一个功能函数的作用,而进行文件链接原理就是将上面的这个汇编嵌入到你的主程序文件中去(本文则指main.o

好,到这文件都编译好了,就差将我的main.o链接系统的printf函数和我的另一个文件func.ofunc()函数了:

$ gcc main.o func.o
#这条命令就是链接了main.o func.o 其原理就是调用ld 链接去单个链接,也包括链接调用的printf函数,为了不加大复杂度,这里直接用gcc来完成自动链接

最后生成a.out文件就是编译+链接的可执行程序了

再来看看生成的程序内部信息:

$  objdump -d --section=.text a.out | grep -A 100 "<main>:"
0000000000001149 <main>:
    1149:	f3 0f 1e fa          	endbr64
    114d:	55                   	push   %rbp
    114e:	48 89 e5             	mov    %rsp,%rbp
    1151:	be 02 00 00 00       	mov    $0x2,%esi
    1156:	bf 01 00 00 00       	mov    $0x1,%edi
    115b:	e8 1d 00 00 00       	call   117d <func> #这里<func>为下面的汇编代码段,可以看到已经将我们刚才编译的func.c文件汇编代码嵌入进来了
    1160:	89 c6                	mov    %eax,%esi
    1162:	48 8d 05 9b 0e 00 00 	lea    0xe9b(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    1169:	48 89 c7             	mov    %rax,%rdi
    116c:	b8 00 00 00 00       	mov    $0x0,%eax
    1171:	e8 da fe ff ff       	call   1050 <printf@plt> #可以看到这里的调用以及被填充了数据,至于为什么是<printf@plt> 下面会详细讲
    1176:	b8 00 00 00 00       	mov    $0x0,%eax
    117b:	5d                   	pop    %rbp
    117c:	c3                   	ret

000000000000117d <func>:
    117d:	f3 0f 1e fa          	endbr64
    1181:	55                   	push   %rbp
    1182:	48 89 e5             	mov    %rsp,%rbp
    1185:	89 7d fc             	mov    %edi,-0x4(%rbp)
    1188:	89 75 f8             	mov    %esi,-0x8(%rbp)
    118b:	8b 55 fc             	mov    -0x4(%rbp),%edx
    118e:	8b 45 f8             	mov    -0x8(%rbp),%eax
    1191:	01 d0                	add    %edx,%eax
    1193:	5d                   	pop    %rbp
    1194:	c3                   	ret

静态链接

但是还是有一个问题没有看到printf函数的汇编代码,而是一个printf@plt那么再重新链接一下:

$  gcc main.o func.o -static

这次再重新看下a.out文件的信息printf的一系列函数的汇编代码就嵌入进来了。

上面的-static参数就是表示使用静态的方式去链接系统帮我们编译好的printf函数库,也就是c标准库,默认情况下的链接是动态链接,那么就是不会将源代码的汇编链接进我们的main.o,需要通过系统加载的方式进入到c标准库中执行汇编代码。

这也从表面了静态链接后的程序大小要比动态链接后的程序大小要大,一个是将源代码全部放入程序中,一个是源代码在我的文件系统里面只需要去调用。

目标文件链接

那么如何将我们刚才写的func.c-func()函数编译成像printf一样的函数直接去调用呢?

打包为动态

$ gcc -c func.c
$ gcc func.o -shared -fpic -o libfunc.so
$ gcc main.c -L. -lfunc
  1. 同样这里先编译func.cfunc.o
  2. 添加-shared-fpic参数将func.o共享对象文件转为动态共享库的形式,libfunc.so这是一个动态共享库的命令规范libxxx.so

在编译动态库时,一般需要使用位置无关代码(Position-Independent Code,PIC)来确保库可以在内存中的任何位置加载并运行

  1. 最后编译main.c+链接libfunc.so,其中-L.表示告诉gcc编译器在当前目录找共享库,-lfunc表示gcc会在所有的共享库目录找到一个名为libfunc.so或者libfunc.a的共享库

在实际开发中,某些算法的完成就是通过打包源代码为共享库(动、静都可以),然后发布出去给第三方,第三方拿到共享库就可以直接通过动态、静态的方式调用了

打包为静态

$ gcc -c func.c
$ ar rcs libfunc.a func.o
$ gcc main.c -L. -lfunc
  1. 步骤和上面步骤类似,不过打包不一样

  2. 使用ar命令将func.o目标文件打包为libfunc.a静态库,后缀名为.a

    • r选项表示将目标文件添加到静态库中

    • c选项表示如果静态库不存在则创建一个新的静态库

    • s选项表示在创建静态库时生成索引

总结

注意打包动态、静态共享库和链接动态、静态库的概念不一样:

  • 使用动态、静态链接只会影响你链接后的可执行文件类型
$ gcc main.c func.o  #动态链接                                    
$ file a.out && du a.out                                            
a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=51c81ac9b9286d36b8cfffeb3f38ef6895d4c48a, for GNU/Linux 3.2.0, not stripped
16K	a.out

$ gcc main.c func.o -static #静态链接                         
$ file a.out && du a.out                                           
a.out: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=6be05823bc9a7da5d33a0b5796d5473957d0a9ea, for GNU/Linux 3.2.0, not stripped
880K	a.out
  • 使用动态、静态打包,并链接不会影响文件大小

如果需要编译静态程序那么链接的共享库是也必须是静态库

#动态链接 动态库
$ gcc func.o -shared -fpic -o libfunc.so && gcc main.c -L. -lfunc
$ file a.out && du a.out    
a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1a2e010e10724e9151dd3ed5565e10a45d2d26c8, for GNU/Linux 3.2.0, not stripped
16K	a.out

#动态链接 静态库
$ rm libfunc.so                                         
$ ar rcs libfunc.a func.o && gcc main.c -L. -lfunc      
$ file a.out && du a.out                                
a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=51c81ac9b9286d36b8cfffeb3f38ef6895d4c48a, for GNU/Linux 3.2.0, not stripped
16K	a.out

#静态链接 动态库 (报错,静态链接的话ld链接器只会去找libxxx.a格式的文件,不会找libxxx.so二进制文件)
$ gcc func.o -shared -fpic -o libfunc.so && gcc main.c -static -L. -lfunc
/usr/bin/ld: cannot find -lfunc: No such file or directory
collect2: error: ld returned 1 exit status

#静态链接 静态库
$ rm libfunc.so
$ file a.out && du a.out    
a.out: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=6be05823bc9a7da5d33a0b5796d5473957d0a9ea, for GNU/Linux 3.2.0, not stripped
880K	a.out

动态链接

在有大量的可执行文件时,比如大部分可执行文件都需要glibc,那么每调用一次静态库就要在该库引用libc.a这样会造成内存巨大,这时就有了动态链接。

再明确下两个重要的概念:

- 动态链接:在运行、加载时,在内存中完成链接的过程
- 动态共享库:用于动态链接的系统库、特性是可以加载无需重定位的代码

got表(Global Offset Table)

因为程序的数据段和代码段的相对距离是固定的,所以指令和变量的距离就是一个常量(偏移、相对地址)就有了全局偏移表(Global Offset Table),用于保存全局变量和库函数的引用,在加载时进行重定位填入真实地址

为了区分数据段和代码段,就有了.got节和.got.plt节。.got节保存着数据因为它不需要延时绑定,.got.plt保存着函数引用需要延时绑定

延时绑定

因为动态链接是在加载时进行的,当重定位的库函数多了后会影响性能,故有了延时绑定。

原理:在函数第一次被调用时,动态链接器才进行符号查找、重定位操作、如未调用则不进行绑定,这样就可以节省资源和性能

程序中的实现:通过plt表(Procedure Linkage Table)和got表配合实现,以上面func和main为例,当main函数要跳转到func函数时,执行call func@plt (这里用gdb动态调试观看过程,插件时gef只有gef才可以看plt前面的地址,如果运行之前编译的func.ELF2文件报错,需要把编译的so文件复制到/usr/bin目录下就可以正常运行)

在这里插入图片描述

此时的plt节

在这里插入图片描述

进入plt节后执行jmp指令跳转到got表处的条目,因为是第一次执行,这个时候got表处的条目还是plt节中的第二条目地址(func@plt+6),然后回到plt表将0x1(.rel.plt中的下标(.rel.plt是对函数引用的修正,它所修正的位置位于.got.plt))进行push压栈,然后再进入plt[0]

在这里插入图片描述

这里就是将got[1]压栈,再跳转到got[2](_dl_runtime_resolve函数)这步就是对符号(变量、函数)重定位操作,将func()真实地址填入func@got.plt也就是前面调用的got表(0x555555754fd0处)这里就完成了真实地址的填充

回到plt表将0x1(.rel.plt中的下标(.rel.plt是对函数引用的修正,它所修正的位置位于.got.plt))进行push压栈,然后再进入plt[0]

[外链图片转存中…(img-JzGi5NHd-1682698436198)]

这里就是将got[1]压栈,再跳转到got[2](_dl_runtime_resolve函数)这步就是对符号(变量、函数)重定位操作,将func()真实地址填入func@got.plt也就是前面调用的got表(0x555555754fd0处)这里就完成了真实地址的填充

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

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

相关文章

SpringBoot核心配置全面总结

目录 1、application. properties核心文件 2、 application.yml配置文件&#xff08;推荐配置风格&#xff09; 3、SpringBoot多环境配置 4、SpringBoot自定义配置 1&#xff09;Value注解 2&#xff09; ConfigurationProperties 5. 远程配置中心&#xff08;目前生产…

http协议(一)/应用层

学习目标&#xff1a;⭐理解应用层的作用&#xff0c;理解协议&#xff0c;理解序列化和反序列化&#xff0c;并且实现网络版计算器⭐HTTP协议。⭐手写一个简单的http协议。 应用层 我们写的一个个解决实际问题, 满足我们日常需求的网络程序, 都是在应用层。 协议/序列化与反…

第三节:支持向量机分类预测

0、介绍 监督学习&#xff08;英语&#xff1a;Supervised learning&#xff09;是机器学习中最为常见、应用最为广泛的分支之一。本次实验将带你了解监督学习中运用十分广泛的支持向量机&#xff0c;并学会使用 scikit-learn 来构建预测模型&#xff0c;用于解决实际问题。 知…

都什么年代了,还在用Excel和ACCESS做应用系统?快来学Eversheet

表格用的越久&#xff0c;就越头疼 稍微有规模的企业&#xff0c;各种表格都会多如牛毛&#xff0c;一堆堆的&#xff0c;有时候这里一张&#xff0c;那里一张&#xff0c;容易整乱&#xff0c;更容易丢失。不管你是用WPS还是用Excel&#xff0c;有些问题你还是依旧解决不了。…

【VM服务管家】VM4.x算法模块开发_4.1 开发配置类

目录 4.1.1 算法开发&#xff1a;算法模块的开发流程4.1.2 参数操作&#xff1a;获取与设置模块参数的方法4.1.3 文件交互&#xff1a;文件交互操作的配置方法4.1.4 输出显示&#xff1a;设置输出并显示在VM界面的方法4.1.5 模板配置&#xff1a;模板配置界面的实现方法4.1.6 命…

中文译英文 模型

Helsinki-NLP/opus-mt-zh-en Hugging FaceWe’re on a journey to advance and democratize artificial intelligence through open source and open science.https://huggingface.co/Helsinki-NLP/opus-mt-zh-en?text%E6%88%91%E5%8F%AB%E6%B2%83%E5%B0%94%E5%A4%AB%E5%86%8…

【Java入门合集】第一章Java概述

【Java入门合集】第一章Java概述 博主&#xff1a;命运之光 专栏&#xff1a;JAVA入门 学习目标 1.理解JVM、JRE、JDK的概念&#xff1b; 2.掌握Java开发环境的搭建,环境变量的配置&#xff1b; 3.掌握Java程序的编写、编译和运行&#xff1b; 4.学会编写第一个Java程序&#x…

Python 科研绘图可视化(后处理)Matplotlib - RGBAxes

Introduction 科研可视化是将数据和信息转化为可视化形式的过程&#xff0c;旨在通过图形化展示数据和信息&#xff0c;使得科研工作者能够更好地理解和分析数据&#xff0c;并从中发现新的知识和洞见。科研可视化可以应用于各种领域&#xff0c;如生物学、物理学、计算机科学…

一文带你入门C++类和对象【十万字详解,一篇足够了】

本文字数较多&#xff0c;建议电脑端访问。不多废话&#xff0c;正文开始 文章目录 ———————————————【类和对象 筑基篇】 ———————————————一、前言二、面向过程与面向对象三、结构体与类1、C中结构体的变化2、C中结构体的具体使用3、结构体 --&…

3.6 Linux shell脚本编程(概念、变量、语句)

目录 shell脚本概述 shell脚本编写步骤 第一个shell脚本文件 shell脚本变量 变量的介绍 变量的作用 变量的命名要求 变量的分类 用户自定义变量 取值 用户自定义变量-数组 只读变量 位置变量与预定义变量 环境变量 shell语句 shell程序 说明性语句&#xff08…

MATLAB连续时间信号的实现和时域基本运算(八)

1、实验目的&#xff1a; 1&#xff09;熟悉常用连续时间信号的实现方法&#xff1b; 2&#xff09;掌握连续时间信号的时域基本运算&#xff1b; 3&#xff09;掌握实现基本函数及其运算的函数的使用方法&#xff1b; 4&#xff09;加深对信号基本运算的理解。 2、实验内容&am…

【VM服务管家】VM4.0平台SDK_2.4 结果获取类

目录 2.4.1 数据结果&#xff1a;通过流程输出或模块输出获取数据结果的方法2.4.2 流程运行&#xff1a;所有流程运行结束的回调方法2.4.3 模块回调&#xff1a;所有模块运行结束的回调方法2.4.4 加密狗回调&#xff1a;获取加密狗状态的回调方法2.4.5 方案加载&#xff1a;方案…

STM32物联网实战开发(4)——基本定时器

我使用的是正点原子的阿波罗F429开发板&#xff0c;他有14个定时器&#xff0c;本次实验使用STM32F429的基本定时器6作定时&#xff0c;在中断中每隔1秒翻转LED电平状态。 1.CubeMX初始化定时器 先开启定时器6 再对定时器6的参数进行配置&#xff0c;将定时器6定时时间配置为…

记一次SSRF漏洞的学习和利用

导语&#xff1a;本文主要记录一次我们在复盘嘶吼网站渗透报告时遇到的一个SSRF漏洞。 1.前言 本文主要记录一次我们在复盘嘶吼网站渗透报告时遇到的一个SSRF漏洞。此漏洞并结合腾讯云的API接口&#xff0c;可以获取大量嘶吼服务器的敏感信息。利用这些敏感信息&#xff0c;又…

阿里测试8年,肝到P8只剩他了····

在阿里工作了8年&#xff0c;工作压力大&#xff0c;节奏快&#xff0c;但是从技术上确实得到了成长&#xff0c;尤其是当你维护与大促相关的系统的时候&#xff0c;熬到P7也费了不少心思&#xff0c;小编也是个爱学习的人&#xff0c;把这几年的工作经验整理成了一份完整的笔记…

玩转ChatGPT提示词 持续更新·······

导语&#xff1a; 众所周知&#xff0c;在AI的世界里&#xff0c;提示词就是和AI沟通语言的桥梁&#xff0c;提示关键词常用于AI对话及AI绘画等相关场景&#xff0c;通过准确的使用关键词&#xff0c;你就能更好的让AI辅助自己的工作&#xff0c;其中的成分重要性不言而喻&…

黑客教程,从零基础入门到精通

学前感言: 1.这是一条坚持的道路,三分钟的热情可以放弃往下看了. 2.多练多想,不要离开了教程什么都不会了.最好看完教程自己独立完成技术方面的开发. 3.有时多google,baidu,我们往往都遇不到好心的大神,谁会无聊天天给你做解答. 4.遇到实在搞不懂的,可以先放放,以后再来解决. …

c++标准模板(STL)(std::array)(三)

定义于头文件 <array> template< class T, std::size_t N > struct array;(C11 起 std::array 是封装固定大小数组的容器。 此容器是一个聚合类型&#xff0c;其语义等同于保有一个 C 风格数组 T[N] 作为其唯一非静态数据成员的结构体。不同于 C 风格数组…

C#非常实用的技巧

1、解压和压缩 .NET Framework 4.5以上版本&#xff1a; string zipFilePath "C:\path\to\file.zip";string destFolder "C:\path\to\destination\folder";using (var archive ZipFile.OpenRead(zipFilePath)){foreach (var entry in archive.Entries…

【Python】【进阶篇】14、Django创建第一个项目

目录 Django创建第一个项目1. 第一个项目BookStore1) BookStore项目创建 2. Django项目配置文件1) manage.py文件2) __init__.py文件3) settings.py文件4) urls.py文件5) wsgi.py文件 Django创建第一个项目 在上一章中&#xff0c;我们完成了开发环境的搭建工作。 本章我们将学…