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 V
  ABI 版本:                          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.o

main.o:     文件格式 elf32-i386

Contents 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-little

SYMBOL 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
    
生成如下文件:
    $ ls
	add.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.o

main.o:     文件格式 elf32-i386

SYMBOL 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 V
  ABI 版本:                          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 run

Elf 文件类型为 EXEC (可执行文件)
Entry point 0x80480a1
There are 3 program headers, starting at offset 52

程序头:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x08048000 0x08048000 0x00144 0x00144 R E 0x1000
  LOAD           0x001000 0x0804a000 0x0804a000 0x00010 0x00028 RW  0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10

 Section 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文件该如何被操作系统映射到进程的虚拟空间。

猜你喜欢

转载自blog.csdn.net/HuangChen666/article/details/133493602