C语言编译与链接过程详解
C语言编译与链接过程详解
源文件
main.c
#include <stdio.h>extern int data;
extern int add(int a,int b);int a1;
int a2 = 0;
int a3 = 10;static int b1;
static int b2 = 0;
static int b3 = 20;int main()
{int c1;int c2 = 0;int c3 = 30;static int d1;static int d2 = 0;static int d3 = 40;c1 = data;c2 = add(a1,a2);while(1);return 0;
}
add.c
int data = 3;
int add(int a,int b)
{return a+b;
}
两大过程:编译、链接
一、编译过程:
-
预处理 (.i)
-
处理#开头的预处理指令:#include #define #ifndef #if #else 等等
-
去注释、加行号、生成文件索引等等
命令:gcc -E main.c -o main.i,生成 .i 文件
-
-
编译 (.s)
将 .i 文件编译生成 .s 汇编文件
命令:gcc -S main.i 生成 .s 文件
-
汇编 (.o)
将汇编文件翻译成二进程可重定位文件,即 .o 文件
命令:gcc -c main.s 生成 .o 文件
PS:gcc命令只是一些后台程序的包装,它会根据不同的参数调用其他程序:
-
预编译和编译合并成了一个步骤,使用的是程序cc1,也可以通过如下命令生成.s文件
cc1 hello.c
等同于 gcc -S hello.c -o hello.s
-
汇编器 as
-
链接器 ld
分析二进制可重定位文件
main.c文件
#include <stdio.h>int a1;
int a2 = 0;
int a3 = 10;static int b1;
static int b2 = 0;
static int b3 = 20;int main(void)
{int c1;int c2 = 0;int c3 = 30;static int d1;static int d2 = 0;static int d3 = 40;return 0;
}
编译命令:在64位的机器上编译32位的.o文件
*gcc -m32 -fno-PIC -c .c
-m32指定编译生成32位文件;-fno-PIC去除和位置无关的段(只留下.text .data .bss .comment 等)

1. 读取 elf 文件头
$ readelf -h main.o
ELF 头:Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 类别: ELF32数据: 2 补码,小端序 (little endian)版本: 1 (current)OS/ABI: UNIX - System VABI 版本: 0类型: REL (可重定位文件)系统架构: ARM版本: 0x1入口点地址: 0x0程序头起点: 0 (bytes into file)Start of section headers: 268 (bytes into file)标志: 0x5000000, Version5 EABI本头的大小: 52 (字节)程序头大小: 0 (字节)Number of program headers: 0节头大小: 40 (字节)节头数量: 10字符串表索引节头: 7
(1) 魔数
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00

(2) REL (可重定位文件)
(3) 入口点地址: 0x0
(4) Start of section headers: 268 (bytes into file)
(5) 本头的大小: 52 (字节)
2. 获取 elf 文件的 section headers(段头) 信息 (供链接使用)
$ readelf -S main.o
There are 12 section headers, starting at offset 0x2ec:节头:[Nr] Name Type Addr Off Size ES Flg Lk Inf Al[ 0] NULL 00000000 000000 000000 00 0 0 0[ 1] .text PROGBITS 00000000 000034 000044 00 AX 0 0 1[ 2] .rel.text REL 00000000 00026c 000020 08 I 9 1 4[ 3] .data PROGBITS 00000000 000078 00000c 00 WA 0 0 4[ 4] .bss NOBITS 00000000 000084 000014 00 WA 0 0 4[ 5] .comment PROGBITS 00000000 000084 00002a 01 MS 0 0 1[ 6] .note.GNU-stack PROGBITS 00000000 0000ae 000000 00 0 0 1[ 7] .eh_frame PROGBITS 00000000 0000b0 00003c 00 A 0 0 4[ 8] .rel.eh_frame REL 00000000 00028c 000008 08 I 9 7 4[ 9] .symtab SYMTAB 00000000 0000ec 000140 10 10 14 4[10] .strtab STRTAB 00000000 00022c 000040 00 0 0 1[11] .shstrtab STRTAB 00000000 000294 000057 00 0 0 1
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings), I (info),L (link order), O (extra OS processing required), G (group), T (TLS),C (compressed), x (unknown), o (OS specific), E (exclude),p (processor specific)
有12个段头,起始段头偏移为 0x2ec
可以看到每个段的偏移与大小
3. 打印出段的内容
~ $ objdump -s main.omain.o: 文件格式 elf32-i386Contents of section .text:0000 8d4c2404 83e4f0ff 71fc5589 e55183ec .L$.....q.U..Q..0010 14c745ec 00000000 c745f01e 000000a1 ..E......E......0020 00000000 8945f48b 15000000 00a10000 .....E..........0030 000083ec 085250e8 fcffffff 83c41089 .....RP.........0040 45ecebfe E...
Contents of section .data:0000 0a000000 14000000 28000000 ........(...
Contents of section .comment:0000 00474343 3a202855 62756e74 7520372e .GCC: (Ubuntu 7.0010 352e302d 33756275 6e747531 7e31382e 5.0-3ubuntu1~18.0020 30342920 372e352e 3000 04) 7.5.0.
Contents of section .eh_frame:0000 14000000 00000000 017a5200 017c0801 .........zR..|..0010 1b0c0404 88010000 20000000 1c000000 ........ .......0020 00000000 44000000 00440c01 00471005 ....D....D...G..0030 02750043 0f03757c 06000000 .u.C..u|....
4. 读取 .o 文件符号表
~ $ objdump -t main.o
main.o: 文件格式 elf32-littleSYMBOL TABLE:
00000000 l df *ABS* 00000000 main.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000004 l O .bss 00000004 b1
00000008 l O .bss 00000004 b2
00000004 l O .data 00000004 b3
00000008 l O .data 00000004 d3.1881
0000000c l O .bss 00000004 d2.1880
00000010 l O .bss 00000004 d1.1879
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000004 O *COM* 00000004 a1
00000000 g O .bss 00000004 a2
00000000 g O .data 00000004 a3
00000000 g F .text 00000044 main
00000000 *UND* 00000000 data
00000000 *UND* 00000000 add
标出了每个符号处于那个段,占多大内存,其中 a1 标记为 *COM* 表示它是弱符号(未初始化的非静态全局变量,可能其他文件里也定义了同名的)
data 和 add 这两个符号被标记为 *UND* ,表示未定义的符号,在本文件中找不到定义,链接时会从其他文件中寻找
5. 根据 section headers(段头) 信息,画出二进制可重定位文件的组成(.o文件)

可以发现bss段和comment段的起始卫视相同,但实际计算得出bss段在.o文件中并没有存储,但是符号表中对bss段有记录。
得出结论:bss段保存的都是未初始化 / 初始化为0的全局变量,和未初始化 / 初始化为0的静态局部变量,所以他们的默认值都为0 ,故为了节省.o文件的空间,无需存储,但是需要在符号表中记录,在最后执行可执行文件后,将bss段的符号存到虚拟地址空间中。


二、链接过程:
在64位x86机器上编译-链接生成32位目标文件和可执行文件的命令
编译:gcc -m32 -fno-PIC -c *.c
手动链接:ld -e main -melf_i386 *.o -o run生成如下文件:$ lsadd.c add.o main.c main.o run
PS:
-m32指定编译生成32位文件;
-fno-PIC去除和位置无关的段(只留下.text .data .bss .comment 等)
-e 指定程序入口,-e后跟着符号即可,也可以把add函数作为程序入口,即 -e add
-melf_i386指定链接生成32位的,x86架构的可执行文件
链接过程的本质主要是将多个目标文件“粘”在一起,实质上拼合的是目标文件之间对地址的引用,即函数名和全局变量
符号表就是.o文件的一个段,symtab,查看符号表命令
readelf -s main.o
objdump -t main.o
nm main.o
符号表中包含什么,主要关注1和2:
-
- 定义在本目标文件中的全局符号,例如变量名、函数名等
-
- 引用的其他目标文件中的符号,没有在本文件中定义,一般叫做外部符号
-
- 段名,如 “.text”, “.data” 等
-
- 局部符号,只在编译单元内部可见,调试器可以使用这些符号来分析程序或崩溃时的核心转储文件,链接过程中链接器往往忽略它们
$ objdump -t main.omain.o: 文件格式 elf32-i386SYMBOL TABLE:
00000000 l df *ABS* 00000000 main.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000004 l O .bss 00000004 b1
00000008 l O .bss 00000004 b2
00000004 l O .data 00000004 b3
00000008 l O .data 00000004 d3.1877
0000000c l O .bss 00000004 d2.1876
00000010 l O .bss 00000004 d1.1875
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000004 O *COM* 00000004 a1
00000000 g O .bss 00000004 a2
00000000 g O .data 00000004 a3
00000000 g F .text 00000016 main
1. 合并所有 .o 文件的段

如上图所示,text段合并,data段合并,bss段合并的同时,需要将弱符号转化为强符号(或者弱符号被强符号替换),bss段大小增加
并且发现链接后,生成的可执行文件的每个段都分配了内存地址(虚拟内存)
2. 合并符号表、符号解析、重定位

- 合并符号表
可以看出,可执行文件的符号表就是将多个.o文件的符号表简单的合并起来
- 符号解析
将弱符号(*COM*)转化为强符号
在其他文件中找到本文件中未定义的符号(*UND*)
- 重定位
为符号分配虚拟内存地址,符号的地址是根据段的地址加上自身的偏移计算的
可执行文件分析
1. 查看文件头
$ readelf -h run
ELF 头:Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 类别: ELF32数据: 2 补码,小端序 (little endian)版本: 1 (current)OS/ABI: UNIX - System VABI 版本: 0类型: EXEC (可执行文件)系统架构: Intel 80386版本: 0x1入口点地址: 0x80480a1程序头起点: 52 (bytes into file)Start of section headers: 4676 (bytes into file)标志: 0x0本头的大小: 52 (字节)程序头大小: 32 (字节)Number of program headers: 3节头大小: 40 (字节)节头数量: 9字符串表索引节头: 8
入口点地址:0x80480a1。
2. 查看段信息
$ readelf -S run
There are 9 section headers, starting at offset 0x1244:节头:[Nr] Name Type Addr Off Size ES Flg Lk Inf Al[ 0] NULL 00000000 000000 000000 00 0 0 0[ 1] .text PROGBITS 08048094 000094 000051 00 AX 0 0 1[ 2] .eh_frame PROGBITS 080480e8 0000e8 00005c 00 A 0 0 4[ 3] .data PROGBITS 0804a000 001000 000010 00 WA 0 0 4[ 4] .bss NOBITS 0804a010 001010 000018 00 WA 0 0 4[ 5] .comment PROGBITS 00000000 001010 000029 01 MS 0 0 1[ 6] .symtab SYMTAB 00000000 00103c 000170 10 7 14 4[ 7] .strtab STRTAB 00000000 0011ac 000059 00 0 0 1[ 8] .shstrtab STRTAB 00000000 001205 00003f 00 0 0 1
每个段都分配了虚拟地址。
3. 查看 program headers
$ readelf -l runElf 文件类型为 EXEC (可执行文件)
Entry point 0x80480a1
There are 3 program headers, starting at offset 52程序头:Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg AlignLOAD 0x000000 0x08048000 0x08048000 0x00144 0x00144 R E 0x1000LOAD 0x001000 0x0804a000 0x0804a000 0x00010 0x00028 RW 0x1000GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10Section to Segment mapping:段节...00 .text .eh_frame 01 .data .bss 02
二进制可重定位文件只有 “section headers”,只有可执行文件里有 “program headers”,“program headers” 中显示了各个段的虚拟地址、对齐字节(一页4K)
按段的属性合并,只读(text+rodata)、可读可写(data+bss)等等
使用 readelf -l main 查看ELF的 “Segment” (供装载使用)
PS:因为我们是自己链接的,没有链接C库,所以段里的内容比较少
* 如果直接运行 gcc main.c -o main,则会默认链接C库,查看可执行文件的每个段时就有很多内容了
* 可执行文件是被 execve 加载到进程中的
* 可执行文件之所以可以运行,因为其指定了入口地址(main)、program headers(指定加载的虚拟地址)
* 描述 “Segment” 的结构叫 ”程序头” ,它描述了ELF文件该如何被操作系统映射到进程的虚拟空间。
相关文章:
C语言编译与链接过程详解
C语言编译与链接过程详解 源文件 main.c #include <stdio.h>extern int data; extern int add(int a,int b);int a1; int a2 0; int a3 10;static int b1; static int b2 0; static int b3 20;int main() {int c1;int c2 0;int c3 30;static int d1;static int …...
Qt信号和槽 定时器
文章目录 1 信号和槽1.1 信号和槽的概念1.2 信号和槽的应用1.3 信号和槽的连接1.4 信号和槽的特性1.5 生活中的类似例子1.6 信号和槽的优势 2 信号和槽的使用2.1 控件的信号和槽2.2 自定义信号和槽2.3 信号和槽的参数传递 3 定时器3.1 QTimer类的基本使用3.2 QTimer类的成员函数…...
zemax对称式目镜
两个几乎对称的双胶合透镜相对放置,可以达到25度的半视场 为了加工方便,这两个透镜组采用相同的结构 对称式目镜要求各组透镜自行校正色差,这样倍率色差也随之而校正。 它还能校正两种像差,慧差和象散。 对称目镜的结构更紧&…...
层次架构、面向服务架构(四十四)
层次架构设计 表现层、中间层、数据访问层、数据架构规划、物联网层次架构、层次式架构案例分析。 层次结构缺点就是效率问题,上一层调用下一层。 1、着重写中间层 组件设计:面向接口编程,分为接口和实现类。 实体设计:实体表…...
Ubuntu22无法自动进入lightdm图像界面
问题:Ubuntu22无法自动进入lightdm图像界面,必须手动运行 lightdm start解决方案: 方案一: 运行一个终端输入 cat /etc/X11/default-display-manager /etc/init/lightdm.conf不接受lightdm作为设置,但是,/…...
01BFS最短距离的原理和C++实现
时间复杂度 O(n),n是边数。 使用前提 边的权只有两种:0,1。 典型场景 n个端点的无向图,编号范围[0,n)。Edges0表示{{n1,n2},...{n3,n4}}表示n1和n2,n3和n4之间有路联接。Edges1表示{{n1,n2},...{n3,n4}}表示n1和n2,n3和n4之间…...
【洛谷 P5266】【深基17.例6】学籍管理 题解(映射+分支)
【深基17.例6】学籍管理 题目描述 您要设计一个学籍管理系统,最开始学籍数据是空的,然后该系统能够支持下面的操作(不超过 1 0 5 10^5 105 条): 插入与修改,格式1 NAME SCORE:在系统中插入姓…...
10.03
代码 #include <iostream>using namespace std; class cz { private:int num1; //实部int num2; //虚部 public:cz(){}cz(int a,int b):num1(a),num2(b){}cz(const cz &other):num1(other.num1),num2(other.num2){}~cz(){}const cz operator(const cz &othe…...
链表单向链表跳跃链表
单向链表 link list t数组的局限:编译期就需要知道大小; 内存连续,插入困难 // 链表节点类 包含一个信息 和指向下一个 节点的指针clas IntLLNode{public:IntLLNode(){// 默认构造函数 没有info信息nextPtr_ 0;// 空指针}IntLLNode(int …...
博客无限滚动加载(html、css、js)实现
介绍 这是一个简单实现了类似博客瀑布流加载功能的页面,使用html、css、js实现。简单易懂,值得学习借鉴。👍 演示地址:https://i_dog.gitee.io/easy-web-projects/infinite_scroll_blog/index.html 代码 index.html <!DOCT…...
腾讯云南京服务器性能如何?南京服务器测速IP地址
腾讯云服务器南京地域怎么样?南京地域很不错,正好处于中间的位置,南方北方用户均可以选择,网络延迟更低速度更快,并且目前南京地域有活动,南京地域可用区可选南京一区、南京二区和南京三区,腾讯…...
MySQL和Oracle中,语法的不同点以及如何在xml中书写日期比较大小
众所周知mysql和oracle的语法有点相识,又有点不同。 在MySQL和Oracle中,语法的不同点有以下几个方面: 数据类型:MySQL和Oracle支持的数据类型有所不同,比如MySQL支持的数据类型包括:整型、浮点型、字符型、…...
谈谈Redis分布式锁
目录 一、回顾分布式锁 (一)理解分布式锁的定义 (二)分布式锁的约束条件 (三)分布式锁常见实现方式 基于数据库的分布式锁 基于缓存的分布式锁 基于分布式一致性算法的分布式锁 基于文件系统的分布…...
Redis的java客户端-RedisTemplate光速入门
一.创建springboot项目 二.引入2个依赖 <!-- redis依赖-->这个已经引入了,因为创建的时候勾选了<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><…...
格点数据可视化(美国站点的日降雨数据)
获取美国站点的日降雨量的格点数据,并且可视化 导入模块 from datetime import datetime, timedelta from urllib.request import urlopenimport cartopy.crs as ccrs import cartopy.feature as cfeature import matplotlib.colors as mcolors import matplotli…...
YoloV8改进策略:LSKNet加入到YoloV8中,打造更适合小目标的YoloV8
文章目录 摘要论文:LSKNet:大选择核网络在遥感目标检测中的应用1、简介2、相关工作2.1、遥感目标检测框架2.2、大核网络2.3、注意力/选择机制3、方法3.1、LSKNet架构3.2、大核卷积3.3、空间核选择4、实验4.1、数据集4.2、实现细节4.3、消融实验4.4、主要结果4.5、分析5、结论…...
力扣-303.区域和检索-数组不可变
Idea 需计算数组nums在下标right 和 left-1 的前缀和,然后计算两个前缀和的差即可。 需要注意的是,当left为0的时候,如果还是left-1则会发生数组访问越界错误。 AC Code class NumArray { public:vector<int> sum;NumArray(vector<…...
web:[极客大挑战 2019]LoveSQL
题目 打开页面显示如下 查看源代码,查到一个check.php,还是get传参 尝试账号密码输入 题目名为sql,用万能密码 1or 11# 或 admin or 11 给了一段乱码,也不是flag 查看字段数 /check.php?usernameadmin order by 3%23&pass…...
数据结构—快速排序(续)
引言:在上一篇中我们详细介绍了快速排序和改进,并给出了其中的一种实现方式-挖坑法 但其实快速排序有多种实现方式,这篇文章再来介绍其中的另外两种-左右指针法和前后指针法。有了上一篇挖坑法的启示,下面的两种实现会容易许多。 …...
Snapdragon Profiler分析Android GPU
Snapdragon Profiler(骁龙分析器)是一款性能分析软件,在Windows、 Mac、和 Linux平台上都可以运行,主要是用来分析使用了高通骁龙处理器的Android设备。 Snapdragon Profiler通过USB连接这些Android设备,开发者可以用…...
深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...
MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...
黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 : 适配层…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...
中医有效性探讨
文章目录 西医是如何发展到以生物化学为药理基础的现代医学?传统医学奠基期(远古 - 17 世纪)近代医学转型期(17 世纪 - 19 世纪末)现代医学成熟期(20世纪至今) 中医的源远流长和一脉相承远古至…...
浪潮交换机配置track检测实现高速公路收费网络主备切换NQA
浪潮交换机track配置 项目背景高速网络拓扑网络情况分析通信线路收费网络路由 收费汇聚交换机相应配置收费汇聚track配置 项目背景 在实施省内一条高速公路时遇到的需求,本次涉及的主要是收费汇聚交换机的配置,浪潮网络设备在高速项目很少,通…...
GO协程(Goroutine)问题总结
在使用Go语言来编写代码时,遇到的一些问题总结一下 [参考文档]:https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现: 今天在看到这个教程的时候,在自己的电…...
4. TypeScript 类型推断与类型组合
一、类型推断 (一) 什么是类型推断 TypeScript 的类型推断会根据变量、函数返回值、对象和数组的赋值和使用方式,自动确定它们的类型。 这一特性减少了显式类型注解的需要,在保持类型安全的同时简化了代码。通过分析上下文和初始值,TypeSc…...
