当前位置: 首页 > news >正文

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.lryc.cn/news/181275.html

相关文章:

  • Qt信号和槽 定时器
  • zemax对称式目镜
  • 层次架构、面向服务架构(四十四)
  • Ubuntu22无法自动进入lightdm图像界面
  • 01BFS最短距离的原理和C++实现
  • 【洛谷 P5266】【深基17.例6】学籍管理 题解(映射+分支)
  • 10.03
  • 链表单向链表跳跃链表
  • 博客无限滚动加载(html、css、js)实现
  • 腾讯云南京服务器性能如何?南京服务器测速IP地址
  • MySQL和Oracle中,语法的不同点以及如何在xml中书写日期比较大小
  • 谈谈Redis分布式锁
  • Redis的java客户端-RedisTemplate光速入门
  • 格点数据可视化(美国站点的日降雨数据)
  • YoloV8改进策略:LSKNet加入到YoloV8中,打造更适合小目标的YoloV8
  • 力扣-303.区域和检索-数组不可变
  • web:[极客大挑战 2019]LoveSQL
  • 数据结构—快速排序(续)
  • Snapdragon Profiler分析Android GPU
  • Cannot download sources:IDEA源码无法下载
  • 从零开始学习 Java:简单易懂的入门指南之IO字符流(三十一)
  • 监狱工具管理系统-监狱劳动工具管理系统
  • 蓄水池算法
  • 作业 day4
  • erlang练习题(四)
  • YoloV5实时推理最短的代码
  • Tensorflow、Pytorch和Ray(张量,计算图)
  • TinyWebServer学习笔记-让程序跑起来
  • _tkinter.TclError: no display name and no $DISPLAY environment variable 解决
  • 我出手了!