编译和链接【四】链接详解

文章目录

  • 编译和链接【四】链接详解
    • 前言
    • 系列文章入口
    • 符号表和重定位表
    • 链接过程
      • 分段组装
      • 符号决议
      • 重定位

编译和链接【四】链接详解

前言

在我大一的时候, 我使用VC6.0对C语言程序进行编译链接和运行 , 然后我接触了VS, Qt creator等众多IDE, 这些IDE界面友好, 使用方便, 例如我最喜欢的VS,一键编译运行。对于大一的我,不需要了解编译的整个过程就可以运行,这无疑是非常棒的,并且增加了我对编程的兴趣,同时也简化了我后续的软件开发, 我只需要关心业务和功能代码即可。

但是今天, 我不想“逃课了”,欢迎来到我的频道,本系列 将会介绍编译中的一系列细节。

在正式开始之前,我要推荐两本书,一本是《程序员的自我修养》,另一本是《鲸书》,这两本书对编译的整个过程做了非常详细,非常完备的介绍,但是恰恰如此,我想很多时候,很多知识在工作上是用不到的,也许这句话在很多年多的我会反驳,但是站在工作一年的现在,我将会给你介绍,我所了解的编译和链接。

系列文章入口

关注我~持续更新

编译和链接【一】总述

编译和链接【二】预处理

编译和链接【三】编译过程

符号表和重定位表

在链接过程中,符号表和重定位表是非常重要的两个表。在汇编阶段,汇编器会分析汇编语言中各个section的信息,收集各种符号,生成符号表,将符号在section内的偏移地址也填充到符号表里。

使用 readelf -s main.o 查看目标文件的符号表信息

在这里插入图片描述

在符号表里,可以看到许多符号信息,比如符号的地址,类型和占用空间的大小。

符号表本质上一个结构体数组,在Arm平台下,定义在Linux内核的/arch/arm/include/asm/elf.h里

typedef struct elf32_sym
{Elf32_word st_name;Elf32_Addr st_value;Elf32_word st_size;unsigned char st_info;unsigned cahr st_other;Elf32_Half st_shndx;
}

符号的类型主要有:

  • OBJECT:对象类型,一般用来标识变量
  • FUNC:函数
  • FILE:当前目标文件的名称
  • SECTION:代表一个section,用来重定位
  • COMMON:公用块数据对象,是一个全局弱符号,在当前文件中未分配空间
  • TLS:表示该符号对应的变量存储在线程局部存储

在 C/C++中,编译器是是源文件为翻译单元进行编译的,如果在我们的程序中,我们引用了其他文件的函数或者全局变量,那么编译器会不会报错呢?

其实是不会的,只要你在调用之前进行声明,那么编译器就会认为你的这个函数或者全局变量在其他文件中定义,编译阶段是不会报错的,链接器会尝试在其他文件或者库里查找这个符号的具体定义,但是如果此时还没找到,那么就会报连链接错误。

main.cpp:undefined reference to ‘Addr’

编译器在给每个目标文件生成符号表的过程中,如果没用找到符号的定义,那么也会把这些符号搜集在一起并保存到一个单独的符号表中,这个符号表就是重定位符号表

使用 readelf -s main.o 查看目标文件的符号表信息

在这里插入图片描述

在这个表的Type列,类型为NOTYPE属于未定义状态,需要后续填充,同时在main.o中会使用一个重定位表**.rel.text**来记录这些需要重定位的符号。使用readelf查看重定位表和section header table信息

readelf -S main.o # 查看section header table信息

readelf -r main.o # 查看重定位信息

可以看到:

Relocation section '.rela.text' at offset 0x4c0 contains 2 entries:Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000004  000200000002 R_X86_64_PC32     0000000000000000 printf - 4
00000000000c  000300000002 R_X86_64_PC32     0000000000000000 puts - 4

我们看到了需要重定位的符号:printf和puts,在后续的链接过程中经过重定位,会更新为新的实际地址

链接过程

在前面的文章里,我介绍了目标文件是由:代码段,数据段,BSS段,符号表等section组成的,这些section从目标文件的零地址处开始顺序排放,而每个符号相对于零地址的偏移,就是每个符号的地址,但是这个地址是暂时的。

在后续的链接过程中,这些目标文件的section会重新拆分组装,每个section的起始参考地址会发生变化,导致每个section定义的函数、全局变量等符号的地址也随之变化,需要重新修改,即:重定位

这些函数、变量等符号被编译器收集并放在符号表,符号表又被放在目标文件中,这些目标文件是不可指定的,它们需经过链接器链接、重定位才能运行。

而整个链接过程主要分为三步:

  • 分段组装
  • 符号决议
  • 重定位

分段组装

顾名思义,在链接的第一步就是将各个目标文件重新分解组装,代码段放在一起,形成最终可执行文件的代码段,其他的section也是如此。

在这里插入图片描述

而需要特别关注的section就是符号表,链接器会在可执行文件里创建一个全局的符号表。通过这步操作,一个可执行文件的所有符号都有的自己的地址,并保存在全局符号表中,但是此时全局符号表的地址还是原来在各个目标文件中的地址,即:相对于零地址的偏移。

显然,当前的任务是需要修改这个地址,而需要确定这个地址,就需要先明白,可执行文件最终是要被加载到内存中执行的,那么会被加载到什么地址呢?

一般来说,会在链接时,指定一个链接地址,链接地址也是程序要加载到内存中的地址。

各个段在可执行文件中先后组装顺序也要是一个需要考量的问题。这个问题一般是通过链接脚本来解决。

链接脚本本质上是一个脚本文件,在这个文件里,不仅规定了各个段的组装顺序、起始地址、位置对齐信息,同时对输出的可执行格式、运行平台、入口地址都有描述。

链接器就是根据链接脚本的规则来组装可执行文件的,并最终将这些信息以section的形式保存在可执行文件的ELF Header中。

下面展示一个简单的链接脚本:

OUTPUT_FORMAT("elf32-littlearm") ;输出ELF文件格式
OUTPUT_ARCH("ARM")	; 运行在arm平台
ENTRY(_start) ;程序入口地址
SECTIONS
{.= 0x60000000 	; 代码段的起始地址.text: {*(.text)}	; 代码段.= 0x6020000	; 数组段起始地址.data: {*(data)}	; 数据段.bss: {*(.bss)}	; BSS段
}

程序运行时,加载器首先会解析可执行文件中的ELFHeader头部信息,验证程序的运行平台和加载地址信息,然后将可执行文件加载到内存中对应的地址,程序就可以正常运行了。

使用ld --verbose来查看链接器默认的链接脚本

在这里插入图片描述

不同的编译器默认的链接地址也是不一样的,在一个由带有MMU的系统中,程序的链接起始地址往往都是一个虚拟地址,程序运行过程中还需要地址转换,通过MMU将虚拟地址转换为物理地址,然后才能访问内存,这部分内容属于CPU硬件底层要关心的内容,和编译原理是不冲突的。

符号决议

当我们在翻译单元里,统一了相同命名的符号的时候,就会发生符号冲突,那么最终的可执行文件会使用哪一个呢?

这就是符号决议的内容,一般规则为:

  • 强符号不能相同命名
  • 强符号可以和弱符号共存
  • 弱符号可以共存。

函数名,初始化的全局变量就是强符号,而未初始化的全局变量则是弱符号。

在一个工程项目里,强符号不能多次定义,否则就会发生重定义错误,而强符号和弱符号可以共存,当共存时,强符号会覆盖弱符号,链接器会选择强符号作为可执行文件的最终符号。

main.c

#include <stdio.h>int Addr;int main()
{int a = 3, b = 4;int c = a + b;printf("Addr=%d\n", Addr);return 0;
}

source.c

int Addr = 1;

使用gcc source.c main.c则可通过编译

链接器在进行符号决议时,选择了强符号(source.c源文件中定义的i符 号),丢弃了弱符号(main.c源文件中定义的未初始化的全局符号i)。如果修改程序,将main.c文件中的Addr也赋一个初值,再去重新编译这两个源文件,就会发现链接器会报重定义错误,因为此时一个项目中出现了两个同名的强符号。

在这里插入图片描述

当然,这段代码在C++中是无法通过编译的,C++对弱符号的定义有所不同,如果此时Addr声明为extern,则可以通过编译。

链接器也允许一个项目中出现多个弱符号共存。在程序编译期间,编译器在分析每个文件中未初始化的全局变量时,并不知道该符号在链接阶段是被采用还是被丢弃,因此在程序编译期间,未初始化的全局变量并没有被直接放置在BSS段中,而是将这些弱符号放到一个叫作COMMON的临时块中,在符号表中使用一个未定义的COMMON来标记,在目标文件中也没有给它们分配存储空间。

在链接期间,链接器会比较多个文件中的弱符号,选择占用空间最大的那一个,作为可执行文件中的最终符号,此时弱符号的大小已经确定,并被直接放到了可执行文件的BSS段中。

main.c

#include <stdio.h>char Addr;int main()
{int a = 3, b = 4;int c = a + b;return 0;
}

source.c

double Addr = 1;

在这里插入图片描述

在main.c里,我将Addr定义为char类型,而source.c里,我定义为double类型,在我的电脑上,double类型占8个字节,那么可以在目标文件里看到实际大小为8个字节,但是在source.o这个目标文件里,可以看到大小为1个字节。

但是最终生成的可执行目标文件的大小为8个字节,符合我说的结论。

如果在项目中有特殊需求,我们也可以将一些强符号显式转化为弱符号。GNU C编译器在ANSI C语法标准的基础上扩展了一系列C语言语法,如提供了一个__attribute__关键字用来声明符号的属性。通过下面的命令,可以将一个强符号转化为弱符号。

_attribute_((weak)) int n = 100;

_attribut_((weak)) void func();

下面进行验证:

main.c

#include <stdio.h>__attribute__((weak)) int Addr = 20;int main()
{printf("Addr = %d\n", Addr);return 0;
}

source.c

int Addr = 10;

在这里插入图片描述

现在在C/C++中,都能通过编译了。

和强符号、弱符号对应的,还有强引用、弱引用的概念。在一个程序中,我们可以定义多个函数和变量,变量名和函数名都是符号,这些符号的本质,或者说这些符号值,其实就是地址。在另一个文件中,我们可以通过函数名去调用该函数,通过变量名去访问该变量。 我们通过符号去调用一个函数或访问一个变量,通常称之为引用(reference),强符号对应强引用,弱符号对应弱引用。

在程序链接过程中,若对一个符号的引用为强引用,链接时找不到其定义,链接器将会报未定义错误;若对一个符号的引用为弱引用,链接时找不到其定义,则链接器不会报错,不会影响最终可执行文件的生成。可执行文件在运行时如果没有找到该符号的定义才会报错。

利用链接器对弱引用的处理规则,我们在引用一个符号之前可以先判断该符号是否存在(定义)。这样做的好处是:当我们引用一个未定义符号时,在链接阶段不会报错,在运行阶段通过判断运行,也可以避免运行错误。

举个例子:我们想实现一个加法模块,并封装成库的形式给应用程序开发者调用,在模块实现的过程中,我们可以将提供给用户的一系列API函数声明为弱符号。

这样做的好处就是:

  • 当我们对某些API的实现不满意的时候,我们可以定义和其同名的函数,这样直接调用不会发生冲突
  • 在库的实现过程中,我们可以将某些还没完成的API定义为弱引用,应用程序在调用之前先判断该函数是否实现,然后才调用,这样,在未来发布新版本的时候,无论这些函数是否实现或者已经删除,都不会影响应用程序的正常链接和运行。

例如:

header.h

#pragma once__attribute__((weak)) int add(int a, int b);

source.c

#include "header.h"__attribute__((weak)) int add(int a, int b )
{return a + b;
}

main.c

#include <stdio.h>
#include "header.h"__attribute__((weak)) int Addr = 20;int main()
{if (add)printf("add(1, 2) = %d\n", add(1, 2));return 0;
}

在上面的代码片里,我们实现了一个加法库,并把接口声明为弱引用,而在main.c里,我们调用了add函数,但是在调用之前,我们先判断了符号这样做的好处就是无论程序是否存在都不影响运行。

在这里插入图片描述

在这里插入图片描述

程序的运行结果也从侧面验证了上面的理论分析是正确的。

重定位

经过符号决议,我们解决了链接过程中多文件符号冲突的问题。经过处理之后,可执行文件的符号表中的每个符号虽然都确定下来了,但是还存在一个问题:符号表中的每个符号值,也就是每个函数、全局变量的地址,还是原来各个目标文件中的值,还都是基于零地址的偏移。链接器将各个目标文件重新分解组装后,各个段的起始地址都发生了变化。

那么各个段中的符号地址也要跟着发生变化。编译器生成的各个目标文件,以零地址为起始地址放置各个函数的指令代码,各个函数相对于零地址的偏移就是各个函数的入口地址。

链接器在链接程序时一般会基于某个链接地址link_addr进行链接,所以最后main()函数和sub()函数的真实地址就被改变了

程序经过重新分解组装后,无论是代码段,还是数据段,各个符号的真实地址都发生了变化。而此时可执行文件的全局符号表中,各个符号的值还是原来的地址,所以接下来还要修改全局符号表中这些符号的值,将它们的真实地址更新到符号表中。修改完毕后,当我们想通过符号引用去调用一个函数或访问一个变量时,就能找到它们在内存中的真实地址了。

在这里插入图片描述

链接器怎么知道哪些符号需要重定位呢?不要忘了,在各个目标文件中还有一个重定位表,专门记录各个文件中需要重定位的符号。重定位的核心工作就是修正指令中的符号地址,是链接过程中的最后一步,也是最核心、最重要的一步,前面两步的操作,其实都是为这一步服务的。

在编译阶段,编译器在将各个C源文件生成目标文件的过程中,遇到未定义的符号一般不会报错,编译器会认为这些符号可能会在其他地方定义。在链接阶段,链接器在其他地方找不到该符号的定义,才会报链接错误。编译器在链接阶段会搜集这些未定义的符号,生成一个重定位表,用来告诉链接器,这些符号在文件中被引用,但是在本文件中没有找到定义,有可能在其他文件或库中定义,“我就先不报错了,你链接的时候找找看”。

无论是代码段,还是数据段,只要这个段中有需要重定位的符号 , 编 译 器 都 会 生 成 一 个 重 定 位 表 与 其 对 应 : .rel.text或.rel.data。这些重定位表记录各个段中需要重定位的各种符号,并以section的形式保存在各个目标文件中。我们可以通过readelf或objdump命令来查看一个目标文件中的重定位表信息。

重定位表中有一个信息比较重要:需要重定位的符号在指令代码中的偏移地址offset,链接器修正指令代码中各个符号的值时要根据这个地址信息才能从茫茫的二级制代码中找到它们。链接器读取各个目标文件中的重定位表,根据这些符号在可执行文件中的新地址,进行符号重定位,修改指令代码中引用这些符号的地址,并生成新的符号表。重定位过程中的地址修正其实很简单,如下所示。

重定位的新地址 = 新的段基址 + 段内偏移

至此,整个链接过程就结束了,我们跟踪的整个编译流程也就结束了。最终生成的文件就是一个可执行目标文件。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

低空经济:开启未来空中生活的全新蓝海

引言 随着科技的进步&#xff0c;我们不再仅仅依赖地面交通和传统物流。你是否曾幻想过&#xff0c;未来的某一天&#xff0c;快递、外卖可以像魔法一样直接从空中送到你手中&#xff1f;或者&#xff0c;你能乘坐小型飞行器&#xff0c;快速穿梭于城市之间&#xff0c;告别拥堵…

IntegrAO整合不完整数据以实现患者分层

高通量组学分析技术的进步极大地推动了癌症患者的分层研究。然而&#xff0c;多组学整合中的数据不完整问题带来了巨大挑战&#xff0c;因为像样本排除或插补这样的传统方法常常会损害真实生物多样性。此外&#xff0c;将具有部分组学数据的新患者准确分类到现有亚型这一关键任…

[创业之路-299]:图解金融体系结构

一、金融体系结构 1.1 概述 金融体系结构是一个国家以行政的、法律的形式和运用经济规律确定的金融系统结构&#xff0c;以及构成这个系统的各种类型的银行和非银行金融机构的职能作用和相互关系。以下是对金融体系结构的详细分析&#xff1a; 1、金融体系的构成要素 现代金…

#渗透测试#批量漏洞挖掘#致远互联AnalyticsCloud 分析云 任意文件读取

免责声明 本教程仅为合法的教学目的而准备&#xff0c;严禁用于任何形式的违法犯罪活动及其他商业行为&#xff0c;在使用本教程前&#xff0c;您应确保该行为符合当地的法律法规&#xff0c;继续阅读即表示您需自行承担所有操作的后果&#xff0c;如有异议&#xff0c;请立即停…

el-table封装一个自定义列配置表格组件(vue3开箱即用)

组件核心功能 拖拽排序&#xff08;使用 vuedraggable&#xff09; 显示/隐藏控制 列宽调整 列固定状态记忆 搜索过滤列 本地存储&#xff08;localStorage&#xff09;可改成接口保存 默认配置恢复 通过 searchText 动态过滤列。 安装拖拽依赖 npm install vuedragg…

关于qtcreator的安装过程遇到的问题和处理方法

打算开发个对windows兼容性好的软件&#xff0c;最终决定用c语言&#xff0c;后来选择了qt&#xff0c;发现qt有个不错的东西qt quick&#xff0c;界面图形效果表现的不错&#xff0c;还能做动画&#xff0c;甚至可以做成游戏。 于是打算安装这个软件&#xff0c;软件虽然开源…

一文通俗理解为什么需要泛型以及泛型的使用

为什么需要泛型&#xff1f; public static void main(String[] args) {ArrayList list new ArrayList();// 由于集合没有做任何限定&#xff0c;任何类型都可以给其中存放list.add("abc");list.add("def");list.add(5);Iterator it list.iterator();wh…

HtmlRAG:RAG系统中,HTML比纯文本效果更好

HtmlRAG 方法通过使用 HTML 而不是纯文本来增强 RAG 系统中的知识表示能力。通过 HTML 清洗和两步块树修剪方法&#xff0c;在保持关键信息的同时缩短了 HTML 文档的长度。这种方法优于现有基于纯文本的RAG的性能。 方法 其实主要看下围绕html提纯思路&#xff0c;将提纯后的…

KEPServerEX 中信道深入介绍

以下是 KEPServerEX 中信道&#xff08;Channel&#xff09; 的详细介绍&#xff0c;涵盖其定义、功能、配置步骤及最佳实践&#xff0c;帮助您快速掌握信道在数据采集中的核心作用&#xff1a; 一、信道&#xff08;Channel&#xff09;的定义 信道 是 KEPServerEX 中 连接物…

C#(Winform)通过添加AForge添加并使用系统摄像机

先展示效果 AForge介绍 AForge是一个专门为开发者和研究者基于C#框架设计的, 也是NET平台下的开源计算机视觉和人工智能库 它提供了许多常用的图像处理和视频处理算法、机器学习和神经网络模型&#xff0c;并且具有高效、易用、稳定等特点。 AForge主要包括: 计算机视觉与人…

迅为RK3568开发板篇OpenHarmony实操HDF驱动配置LED-LED测试

将编译好的镜像全部进行烧写&#xff0c;镜像在源码根目录 out/rk3568/packages/phone/images/目录下。 烧写完成之后&#xff0c;在调试串口查看打印日志&#xff0c;如下图所示&#xff1a; 然后打开 hdc 工具&#xff0c;运行测试程序&#xff0c;输入“led_test 1”&…

在VS2022中配置DirectX12环境,并显示显示一个窗口

1.创建空项目并配置项目&#xff1a; 1.打开VS2022,创建C项目中的空项目 2.新建一个Main.cpp文件 3.配置项目 将属性页的C/C项中的语言栏的符合模式设置为否 再将链接器中的系统栏的子系统设置为窗口 设置完成&#xff01; 2.创建一个Windows窗口&#xff1a; 代码&#…

AI前端开发:蓬勃发展的机遇与挑战

人工智能&#xff08;AI&#xff09;领域的飞速发展&#xff0c;正深刻地改变着我们的生活方式&#xff0c;也为技术人才&#xff0c;特别是AI代码生成领域的专业人士&#xff0c;带来了前所未有的机遇。而作为AI应用与用户之间桥梁的前端开发&#xff0c;其重要性更是日益凸显…

DeepSeek+即梦 做AI视频

DeepSeek做AI视频 制作流程第一步&#xff1a;DeepSeek 生成视频脚本和分镜 第二步&#xff1a;生成分镜图片绘画提示词第三步&#xff1a;生成分镜图片第四步&#xff1a;使用可灵 AI 工具&#xff0c;将生成的图片转成视频。第五步&#xff1a;剪映成短视频 DeepSeek 真的强&…

数组练习(深入理解、实践数组)

1.练习1&#xff1a;多个字符从两端移动&#xff0c;向中间汇聚 编写代码&#xff0c;演示多个字符从两端移动&#xff0c;向中间汇聚 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() {//解题思路&#xff1a;//根据题意再…

学习threejs,使用HemisphereLight半球光

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.HemisphereLight 二、…

vue项目使用vite和vue-router实现history路由模式空白页以及404问题

开发项目的时候&#xff0c;我们一般都会使用路由&#xff0c;但是使用hash路由还是history路由成为了两种选择&#xff0c;因为hash路由在url中带有#号&#xff0c;history没有带#号&#xff0c;看起来更加自然美观。但是hash速度更快而且更通用&#xff0c;history需要配置很…

Fiori APP配置中的Semantic object 小bug

在配置自开发程序的Fiori Tile时&#xff0c;需要填入Semantic Object。正常来说&#xff0c;是需要通过事务代码/N/UI2/SEMOBJ来提前新建的。 但是在S4 2022中&#xff0c;似乎存在一个bug&#xff0c;即无需新建也能输入自定义的Semantic Object。 如下&#xff0c;当我们任…

芯片设计企业的IT支撑点

对于一个芯片设计企业&#xff0c;需要怎么样的IT支撑&#xff0c;这看起来并不是那么重要&#xff0c;并不影响芯片企业是否取得成功&#xff0c;但真正进入这个行业&#xff0c;你会发现&#xff0c;这里还是有一些门道的。 实际上&#xff0c;芯片设计企业对于IT的依赖很重&…

生成对抗网络入门:Mnist手写数字生成

本文为为&#x1f517;365天深度学习训练营内部文章 原作者&#xff1a;K同学啊 一 理论基础 生成对抗网络(Generative Adversarial Networks,GAN)是近年来深度学习领域的一个热点方向。 GAN并不指代某一个具体的神经网络&#xff0c;而是指一类基于博弈思想而设计的神经网络。…