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

文章目录

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

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

前言

在我大一的时候, 我使用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,链接器修正指令代码中各个符号的值时要根据这个地址信息才能从茫茫的二级制代码中找到它们。链接器读取各个目标文件中的重定位表,根据这些符号在可执行文件中的新地址,进行符号重定位,修改指令代码中引用这些符号的地址,并生成新的符号表。重定位过程中的地址修正其实很简单,如下所示。

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode每日精进:876.链表的中间结点

题目链接&#xff1a;876.链表的中间结点 题目描述&#xff1a; 给你单链表的头结点 head &#xff0c;请你找出并返回链表的中间结点。 如果有两个中间结点&#xff0c;则返回第二个中间结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[3,4,5…

数据结构——结构体位域、typedef类型重定义、宏、共用体union、枚举、虚拟内存划分

一、结构体位域 1.1 结构体位域的基础 结构体位域&#xff1a;把结构体字节大小扣到极致的一个类型&#xff0c;以bit单位 格式&#xff1a;struct 位域体名{数据类型 位域名:位域大小;数据类型 位域名:位域大小;...};解析&#xff1a;位域体名&#xff1a;可有可无&#xff…

CZML 格式详解,javascript加载导出CZML文件示例

示例地址&#xff1a;https://dajianshi.blog.csdn.net/article/details/145573994 CZML 格式详解 1. 什么是 CZML&#xff1f; CZML&#xff08;Cesium Zipped Markup Language&#xff09;是一种基于 JSON 的文件格式&#xff0c;用于描述地理空间数据和时间动态场景。它专…

SQL递归技巧

1.读样例 with recursive cet_dpt(id, parent_id, path, org_category, level,depart_name) as (select id ,parent_id,depart_name as path,org_category,1 as level,sd.depart_namefrom isolarerp.sys_depart sdwhere del_flag 0and sd.org_code A09B15union al…

django配置跨域

1、第一种 from django.views.decorators.csrf import csrf_exemptcsrf_exempt第二种 安装 pip install django-cors-headers在配置文件settings.py进入 INSTALLED_APPS [..."corsheaders", # 添加 ]MIDDLEWARE [corsheaders.middleware.CorsMiddleware, # 添加…

设置mysql的主从复制模式

mysql设置主从复制模式似乎很容易&#xff0c;关键在于1&#xff09;主库启用二进制日志&#xff0c;2&#xff09;从库将主库设为主库。另外&#xff0c;主从复制&#xff0c;复制些什么&#xff1f;从我现在获得的还很少的经验来看&#xff0c;复制的内容有表&#xff0c;用户…

蓝耘智算平台:开启企业级 DeepSeek 智能助手的搭建捷径

文章目录 一、深度解密 DeepSeek 技术矩阵1.1 模型架构创新1.2 核心能力全景 二、私有化部署&#xff1a;企业的明智之选2.1 企业级部署场景2.2 硬件选型策略 三、蓝耘平台&#xff1a;部署全流程大揭3.1 环境准备阶段Step 1&#xff1a;访问蓝耘智算云官网完成企业认证Step 2&…

Android原生的HighCPU使用率查杀机制

摘要 原生的HighCPU使用率查杀机制是基于读取/proc/pid/stat中的utime stime后&#xff0c;根据CPU使用率 (utime stime / totalTime)*100%进行实现&#xff0c;当检测后台进程的CPU使用率超过阈值时&#xff0c;执行查杀和统计到电池数据中。 细节点&#xff1a; 1. 原生根…

数据库安全、分布式数据库、反规范化等新技术(高软19)

系列文章目录 3.7数据库安全、分布式数据库、反规范化等新技术 前言 本节数据库安全、分布式数据库、反规范化等新技术相关概念与技术。 一、数据库 1.数据库安全 2.数据库备份 二、分布式数据库 1.数据库分布 2.数据仓库 3.数据仓库结构 4.商业智能&#xff08;BI&#xf…

数据结构实现顺序表的尾插,尾删,按值查找/修改/删除,按下标查找/增加/删除

头文件&#xff1a;head.h #ifndef __HEAD_H__ #define __HEAD_H__#include <stdio.h> #include <string.h> #include <stdlib.h> #define MAXSIZE 20enum num {success,false-1};typedef int datatype;typedef struct {int len;datatype data[MAXSIZE]; }S…

新手自学:如何用gromacs对简单分子复合物进行伞形采样

1、建立体系: 1、将蛋白的pdb文件转化为gmx: gmx pdb2gmx -f 2BEG_model1_capped.pdb -ignh -ter -o complex.gro 这个网页可以实现将多肽序列转化为pdb: ProBuilder On-line 这个教程的蛋白2BFG包含两条链(chain A和B) 在生成的topol文件中,增加如下的内容,效果就…

2025 BabitMF 第一期开源有奖活动正式开启 !

为了促进开源社区的交流与成长&#xff0c;字节跳动开源的多媒体处理框架 BabitMF &#xff08;GitHub - BabitMF/bmf: Cross-platform, customizable multimedia/video processing framework. With strong GPU acceleration, heterogeneous design, multi-language support, e…

Ollama 自定义导入模型

文章目录 一、从 GGUF 导入1.1 CCUF 介绍1.2 导入方式 二、由模型直接导入2.1 模型下载2.2 使用 llama.cpp 进行转换&#xff08;1&#xff09;克隆 llama.cpp 库到本地&#xff0c;并安装相关库&#xff08;2&#xff09;环境验证&#xff08;3&#xff09;执行转换程序 2.3 使…

J6 X8B/X3C切换HDR各帧图像

1、OV手册上的切换命令 寄存器为Ox5074 各帧切换&#xff1a; 2、地平线control tool实现切换命令 默认HDR模式出图&#xff1a; HCG出图&#xff1a; LCG出图 SPD出图 VS出图

GESP5级语法知识(十一):高精度算法(一)

高精度加法&#xff1a; #include<iostream> #include<string> #include<algorithm> using namespace std; const int N501;//高精度数的最长长度 //c[]a[]b[]:高精度加法方案一&#xff1a;对应位相加&#xff0c;同时处理进位 void h_add_1(int a[],int b…

【Git版本控制器】:第二弹——工作区,暂存区,版本库,

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;Linux网络编程 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 ​ 相关笔记&#xff1a; https://blog.csdn.net/djd…

Transformer 模型介绍(一)——综述

Transformer 是一种完全基于注意力机制的神经网络模型&#xff0c;首次在2017年的论文《Attention Is All You Need》中提出。该模型最初用于机器翻译任务&#xff0c;并在特定任务中表现优于谷歌的其他神经网络机器翻译模型。Transformer 也是 Seq2Seq&#xff08;序列到序列&…

【Linux】多线程 -> 从线程概念到线程控制

线程概念 在一个程序里的一个执行路线就叫做线程&#xff08;thread&#xff09;。更准确的定义是&#xff1a;线程是“一个进程内部的控制序列”。一切进程至少都有一个执行线程。线程在进程内部运行&#xff0c;本质是在进程地址空间内运行。在Linux系统中&#xff0c;在CPU眼…

.NET Web-静态文件访问目录浏览

一、Web根目录访问 创建wwwroot文件夹app.UseStaticFiles(); // 启⽤静态⽂件中间件url/路径 进行访问 二、Web根目录之外的文件 app.UseStaticFiles(new StaticFileOptions {FileProvider new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath,&qu…

cap1:TensorRT是什么?

文章目录 1、什么是 TensorRT&#xff1f;2、TensorRT 的优势3、TensorRT 加速 PyTorch 模型的基本流程3.1 训练模型和保存模型3.2 导出模型3.3 转换为 TensorRT 引擎3.4 加载与推理 4、基础环境配置4.1 安装nvidia驱动4.2 安装CUDA4.3 安装cuDNN 在软件工程领域&#xff0c;部…