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;
}

两大过程:编译、链接

一、编译过程:


  1. 预处理 (.i)

    • 处理#开头的预处理指令:#include #define #ifndef #if #else 等等

    • 去注释、加行号、生成文件索引等等

    命令:gcc -E main.c -o main.i,生成 .i 文件

  2. 编译 (.s)

    将 .i 文件编译生成 .s 汇编文件

    命令:gcc -S main.i 生成 .s 文件

  3. 汇编 (.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

    1. 定义在本目标文件中的全局符号,例如变量名、函数名等
    1. 引用的其他目标文件中的符号,没有在本文件中定义,一般叫做外部符号
    1. 段名,如 “.text”, “.data” 等
    1. 局部符号,只在编译单元内部可见,调试器可以使用这些符号来分析程序或崩溃时的核心转储文件,链接过程中链接器往往忽略它们
$ 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文件该如何被操作系统映射到进程的虚拟空间。

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

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

相关文章

1004. 最大连续1的个数III(滑动窗口)

一、题目 1004. 最大连续1的个数 III - 力扣&#xff08;LeetCode&#xff09; 二、代码 class Solution { public:int longestOnes(vector<int>& nums, int k) {int mark0;//标记0的个数int MaxLength0;for(int left0,right0;right<nums.size();right){if(nums…

Leetcode.965 单值二叉树

本专栏内容为&#xff1a;leetcode刷题专栏&#xff0c;记录了leetcode热门题目以及重难点题目的详细记录 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;八大排序汇总 &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库&#x1f69a; &…

网络运营对职业发展有什么用

大家好&#xff0c;我是网络工程师成长日记实验室的郑老师&#xff0c;您现在正在查看的是网络工程师成长日记专栏&#xff0c;记录网络工程师日常生活的点点滴滴 有个哥们联系我&#xff0c;他说他以前是做it售前&#xff0c;也做过售后&#xff0c;也做it很长时间了&#xff…

一文带你掌握 优先级队列

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;强烈推荐优质专栏: &#x1f354;&#x1f35f;&#x1f32f;C的世界(持续更新中) &#x1f43b;推荐专栏1: &#x1f354;&#x1f35f;&#x1f32f;C语言初阶 &#x1f43b;推荐专栏2: &#x1f354;…

(ubuntu)Docker 安装linux 详情过程

文章目录 前言Docker 安装linux第一步&#xff1a;使用dokcker 拉取镜像&#xff1a;第二步&#xff1a;创建本地目录&#xff08;用于挂载&#xff09;第三步&#xff1a;&#xff08;上传配置文件&#xff09;修改配置文件第四步&#xff1a;创建docker容器第五步: 测试本地连…

JavaSE | 初识Java(一) | JDK \ JRE \ JVM

Java初识 Java 是一门半编译型、半解释型语言。先通过 javac 编译程序把源文件进行编译&#xff0c;编译后生成的 .class 文件是由字节 码组成的平台无关、面向 JVM 的文件。最后启动 java 虚拟机 来运行 .class 文件&#xff0c;此时 JVM 会将字节码转换成平台能够理…

docker-compose 网络配置- IP 主机名 hosts配置

docker-compose 配置IP、hostname、hosts配置 配置IP version: "3" networks:bd-network: # 声明网络external: true services:kafka: # 服务名称networks:bd-network: # 连接的网络名称ipv4_address: 172.2.0.102 # 配置IP配置 主机名 version: "3&quo…

python之股票财务分析

#import akshare as ak import pandas as pd import matplotlib.pyplot as plt symbol1"资产负债表" symbol2"利润表" symbol3"现金流量表" #df1ak.stock_financial_report_sina(stock"601633",symbolsymbol1) #df2ak.stock_financial…

检测防火墙是否开启、判断程序是否加入防火墙白名单(附源码)

VC常用功能开发汇总&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&#xff09;https://blog.csdn.net/chenlycly/article/details/124272585C软件异常排查从入门到精通系列教程&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&a…

车牌超分辨率:License Plate Super-Resolution Using Diffusion Models

论文作者&#xff1a;Sawsan AlHalawani,Bilel Benjdira,Adel Ammar,Anis Koubaa,Anas M. Ali 作者单位&#xff1a;Prince Sultan University 论文链接&#xff1a;http://arxiv.org/abs/2309.12506v1 内容简介&#xff1a; 1&#xff09;方向&#xff1a;图像超分辨率技术…

Java栈的压入、弹出序列(详解)

目录 1.题目描述 2.题解 方法1 方法2 1.题目描述 输入两个整数序列&#xff0c;第一个序列表示栈的压入顺序&#xff0c;请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序&#xff0c;序列4,5,3,2,1是该压栈序…

凉鞋的 Unity 笔记 104. 测试所涉及的窗口

104. 测试所涉及的窗口 在上一篇&#xff0c;笔者简单介绍了检视器窗口&#xff0c;如图所示&#xff1a; 我们接着介绍上图中的最后一个部分内容&#xff0c;测试部分。 测试部分我们只做了一件非常简单的操作&#xff0c;就是点击了一下运行按钮&#xff0c;查看结果&#…

行为型设计模式——责任链模式

摘要 责任链模式(Chain of responsibility pattern): 通过责任链模式, 你可以为某个请求创建一个对象链. 每个对象依序检查此请求并对其进行处理或者将它传给链中的下一个对象。 一、责任链模式意图 职责链模式&#xff08;Chain Of Responsibility&#xff09; 是一种行为设…

c语言练习73:统计位数为偶数的数字

统计位数为偶数的数字 给你⼀个整数数组 nums &#xff0c;请你返回其中位数为 偶数 的数字的个数。 • ⽰例 1&#xff1a; 输⼊&#xff1a;nums [12,345,2,6,7896] 输出&#xff1a;2 解释&#xff1a; 12 是 2 位数字&#xff08;位数为偶数&#xff09; 345 是 3 位…

Python 数据分析与挖掘(一)

Python 数据分析与挖掘&#xff08;数据探索&#xff09; 数据探索 1.1 需要掌握的工具&#xff08;库&#xff09; 1.1.1 Nump库 Numpy 提供多维数组对象和各种派生对象&#xff08;类矩阵&#xff09;&#xff0c;利用应用程序接口可以实现大量且繁琐的数据运算。可以构建…

【Java 进阶篇】MySQL 多表查询详解

MySQL 是一个强大的关系型数据库管理系统&#xff0c;多表查询是数据库操作中的重要部分之一。多表查询允许您从多个表中检索和操作数据&#xff0c;以满足复杂的数据需求。本文将介绍 MySQL 多表查询的基本概念、语法和示例&#xff0c;以及一些常见的多表查询场景。 什么是多…

SpringCloud篇

SpringCloud五大组件是啥&#xff1f; rabbin gateway feign 注册中心&#xff08;nacos,Eureka&#xff09;,服务保护 &#xff08;sentinel&#xff09; &#xff1b; nacos和eureka的区别是什么&#xff1f; 负载均衡是如何实现的&#xff1f;&#xff1f; ribbon负载策略…

【数据结构】排序之插入排序和选择排序

&#x1f525;博客主页&#xff1a;小王又困了 &#x1f4da;系列专栏&#xff1a;数据结构 &#x1f31f;人之为学&#xff0c;不日近则日退 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、排序的概念及其分类 &#x1f4d2;1.1排序的概念 &#x1f4d2;1.2排序…

朝气蓬勃 后生可畏

介绍: 线段树是一棵二叉搜索树,思想与分治很想,把一段区间平分平分再平分,平分到不能平分为止,可以进行方便的区间修改和区间查询,当然,树状数组能做的单点修改、单点查询,线段树也可以更好地实现,总之,线段树是树状数组的升级版,此外,线段树能做的平衡树也能做,但…

Ubuntu镜像源cn.arichinve.ubuntu.com不可用原因分析和解决

文章目录 Ubuntu查看系统版本Ubuntu更新系统不能更新Ubuntu查看APT更新源配置cn.archive.ubuntu.com已经自动跳转到清华镜像站Ubuntu变更镜像源地址备份原文件批量在VIM中变更 Ubuntu国内镜像站推荐推荐阅读 今天想要在Ubuntu环境下搭建一个测试环境&#xff0c;进入Ubuntu系统…