程序的编译链接过程复习

//参考《程序员的自我修养》
本文将以下面代码为例说明问题:

#include <stdio.h>
//g是global的意思,全局
int gdata1 = 10;
int gdata2 = 0;
int gdata3 ;   
//gdata3是弱符号,编译的时候不知道要不要用到这个符号,因为不知道有没有同名的强符号
//所以暂且放到comments段,链接合并完符号表,发现并没有强符号,就会把这个数据放回
//到bss段

//以上部分是数据 

static int gdata4 = 1;
static int gdata5 = 0;
static int gdata6;
//以上部分是数据

int main()
{
	#l是local的意思,代表局部
	int ldata7 = 100;
	int ldata8 = 0;
	int ldata9;
	//从main(包含main)到这里,这一部分是指令
	
	static int ldata10 = 8;
	static int ldata11 = 0;
	static int ldata12;
	//以上部分是数据
	
	//从这里以下的部分都是指令
	return 0;
}
//函数名也会产生符号

第一部分内容:虚拟内存的概念
程序的加载过程:磁盘 ——> 内存——> cpu
加载什么东西:指令 (instructions)+ 数据(data)

操作系统作用的简述:(厂家不同,会有差异)
屏蔽I/O之间的差异,为了调用统一的接口,操作系统有VFS:虚拟文件管理系统
屏蔽内存和I/O间的差异,为了统一,操作系统有虚拟存储器(虚拟内存)来管理物理内存的方案
屏蔽cpu、内存和IO间的差异,为了统一资源调度单位,出现了 进程概念

虚拟内存: x86 32 linux内核
多大:·2^32
与CPU的位数有关,也就是ALU算术逻辑单元的最大宽度,即就是数据总线的条数
在哪里:
不存在,是虚拟的 ~ 逻辑上抽象出来的 ~
什么用:
通过虚拟内存来管理物理内存,每个进程都会有
每个程序只要一运行,就会给分配虚拟内存,而不是真正的物理内存
这个虚拟内存我们一般又叫做虚拟地址空间
在这里插入图片描述

bss和data有什么区别:
初始化了的数据,且初始化不为0的,放在data段
没有初始化和初始化为0数据存放在bss
bss节省了谁的空间:
虚拟地址的空间还是文件的空间?节省的是文件的空间,bss段在虚拟空间一定占
bss段其实没需要存,因为这些值不论是0还是没有初始化,值永远是0,所有根本不需要存储
但是为了让操作系统知道有这种数据,就将这些数据的信息纪录在执行文件的section table中
我们通过ELF Header找到section table然后找到bss数据的信息即可

用户空间和内核空间哪个是共享的
用户空间是每个进程各自拥有各自的,而内核空间只有一份,所有进程共享同一个内核空间


int main(int argc , char ** argv , char ** environ)
{
	//argc:命令行参数的个数,默认是一个,当前可执行程序的绝对路径
	for (int i = 0, i < argc;++i)
	{
		puts(argv[i])
		//这里链接了内存中的动态库
	}
	//argv 就是我们的命令行参数
	//environ存放的是环境变量,如我们使用的库,头文件等的路径
	//argv environ在stack的高内存中存储
}

第二部分内容:编译和链接的过程
预编译:文本替换,删除注释等 ——> main.i
编译 :语法、语义、代码优化、汇总所有的符号到符号表——> main.s (汇编指令)
汇编 :将汇编码翻译为01机器码——>main.o(机器指令) 即:二进制可重定位的目标文件
链接 :
1.合并所有的obj段,并调整段偏移,合并符号表
2.核心:符号重定位——>可执行文件

main point:
1.为什么汇编得到的文件不能执行?
2.链接的意义何在?
3.符号表是做什么的?
4.可执行文件格式,cpu如何知道从哪里开始执行?

查看main.o文件都有什么:
1.生成目标文件
gcc -o main.cpp
2.查看目标文件有什么
objdump -h maim.o
main.o:     文件格式 elf64-x86-64     // 由于博主装了64位的系统,但是理论上基本一样
字节:
Idx Name          Size      VMA               LMA               File off  Algn
 0 .text         00000019  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         0000000c  0000000000000000  0000000000000000  0000005c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000018  0000000000000000  0000000000000000  00000068  2**2
                  ALLOC
  3 .comment      0000002b  0000000000000000  0000000000000000  00000068  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00000093  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  00000098  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA


我们关注目标文件的一个点:
所有数据的地址为0,因此我们从这里可以得到mian.o中我们没有给变量分配地址
(其实是链接的时候才会给分配内存地址,不过这个是虚拟地址空间上的地址)

我们看一下main.o文件的大概组成:
在这里插入图片描述

查看elf文件中保存的内容:
readelf -h main.o
ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              REL (可重定位文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x0
  程序头起点:          0 (bytes into file)
  Start of section headers:          880 (bytes into file)
  标志:             0x0
  本头的大小:       64 (字节)
  程序头大小:       0 (字节)
  Number of program headers:         0
  节头大小:         64 (字节)
  节头数量:         11
  字符串表索引节头: 10

关注两个点:
1.类型:REL 可重定位文件
2.入口地址:0x0
(单单从以上两点就足以说明,可重定位的文件REL不可以执行,即main.o文件不能执行原因)
原因:入口地址是不可访问的,变量还没有给分配虚拟地址

我们再来看main.o中的具体内容:
我们可以通过进制转换看到我们数据的内容

objdump -s main.o
main.o:     文件格式 elf64-x86-64
Contents of section .text:
 0000 554889e5 c745f864 000000c7 45fc0000  UH...E.d....E...
 0010 0000b800 0000005d c3                 .......]. 
 Contents of section .data:
 0000 0a000000 01000000 08000000           ............    
Contents of section .comment:
 0000 00474343 3a202855 62756e74 7520372e  .GCC: (Ubuntu 7.
 0010 332e302d 32377562 756e7475 317e3138  3.0-27ubuntu1~18
 0020 2e303429 20372e33 2e3000             .04) 7.3.0.
 Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 19000000 00410e10 8602430d  .........A....C.
 0030 06540c07 08000000                    .T......  

其中,text可读可执行,data可读可写的,
rodata是只读不可写的,保存着如常量字符串等

查看段表中的内容:section table

readelf  -S main.o
There are 11 section headers, starting at offset 0x370:
节头:
  [号] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000019  0000000000000000  AX       0     0     1
  [ 2] .data             PROGBITS         0000000000000000  0000005c
       000000000000000c  0000000000000000  WA       0     0     4
  [ 3] .bss              NOBITS           0000000000000000  00000068
       0000000000000018  0000000000000000  WA       0     0     4
  [ 4] .comment          PROGBITS         0000000000000000  00000068
       000000000000002b  0000000000000001  MS       0     0     1

第三部分内容:链接的过程
comments段是干什么:存放未决定的符号(弱符号)
链接的时候我们只链接全局符号,如果一个普通全局的变量不初始化,为弱符号,编译的时候不知道其他的问是否要用,所以干脆放到comments段中。
(有的较新的系统在这一方面有着不同的处理,这里只是参考)

链接的2个过程:重点
1.合并各个obj文件中的各个相同属性的段,组织在一个页面上,调整段起始偏移和段长度,合并各个.obj对应的符号表,最终进行符号解析
(A文件引用了B文件的符号,符号解析就是找到B引用A符号的定义的地方,
如果找不到,就发生链接错误:undefined symbol)

我们先来看一下符号表:数据是哪些一目了然,也可以看出局部变量不是数据

obidump   -t  main.o
SYMBOL TABLE:
0000000000000000 l    df *ABS* 0000000000000000 main.cpp
0000000000000000 l    d  .text 0000000000000000 .text
0000000000000000 l    d  .data 0000000000000000 .data
0000000000000000 l    d  .bss 0000000000000000 .bss
0000000000000004 l     O .data 0000000000000004 _ZL6gdata4
0000000000000008 l     O .bss 0000000000000004 _ZL6gdata5
000000000000000c l     O .bss 0000000000000004 _ZL6gdata6
0000000000000008 l     O .data 0000000000000004 _ZZ4mainE7ldata10
0000000000000010 l     O .bss 0000000000000004 _ZZ4mainE7ldata11
0000000000000014 l     O .bss 0000000000000004 _ZZ4mainE7ldata12
0000000000000000 l    d  .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame 0000000000000000 .eh_frame
0000000000000000 l    d  .comment 0000000000000000 .comment
0000000000000000 g     O .data 0000000000000004 gdata1
0000000000000000 g     O .bss 0000000000000004 gdata2
0000000000000004 g     O .bss 0000000000000004 gdata3
0000000000000000 g     F .text 0000000000000019 main

2.进行符号的重定位,即就是给每个符号分配内存地址(虚拟空间上的地址)
(编译过程中,数据的地址都是0,函数的入口地址都是一些不可访问的地址,也就是说编译过程中的地址都是无效的,这也就是为什么需要链接的第二步过程)

第四部分:二进制可执行文件的组成

我们来看一下ELF Header中的内容:

 readelf    -h   main
ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              DYN (共享目标文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x4f0
  程序头起点:          64 (bytes into file)
  Start of section headers:          6704 (bytes into file)
  标志:             0x0
  本头的大小:       64 (字节)
  程序头大小:       56 (字节)
  Number of program headers:         9
  节头大小:         64 (字节)
  节头数量:         28
  字符串表索引节头: 27

同样的我们只需要关注两个点:
程序的入口地址:0x4f0 不再是可重定位文件中的0地址
类别:为ELF

我们再来看一下main文件的内容:

objdump    -d    main
//我们将截取一部分主要的内容来说明问题:
Disassembly of section .text:
00000000000004f0 <_start>:
 4f0: 31 ed                 xor    %ebp,%ebp
 4f2: 49 89 d1              mov    %rdx,%r9
 ......

结合main主要内容和EFL Header内容来看:
程序的入口地址 == text指令开始执行的地方
换一种说法也就是说为什么程序每次从main函数开始执行,因为ELF Header中保存的就是main函数的入口地址

可执行文件内容:
ELF问 比. obj 文件多了个program header段,该段存储这两个Load项,将属性相同的段映射到虚拟地址空间。

第五部分:程序的运行
./main
1.创建虚拟地址空间到物理内存的映射(创建内核地址映射结构体),创建页目录和页表
2.加载代码段 和 数据段到内存中
3.将可执行文件的入口地址写到cpu的pc寄存器即可。

磁盘上文件映射到物理内存的过程:

DP磁盘数据通过(mmap函数) ——> VP 虚拟地址空间页面 (通过多级页表映射) ——> PP物理内存

跟踪一个进程的执行过程

strace  ./main   跟踪程序执行过程
execve("./main", ["./main"], 0x7ffd38b10830 /* 64 vars */) = 0
brk(NULL)                               = 0x562fc2b87000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=83928, ...}) = 0
mmap(NULL, 83928, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f87ae55f000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\34\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f87ae55d000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f87adf5c000
mprotect(0x7f87ae143000, 2097152, PROT_NONE) = 0
mmap(0x7f87ae343000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f87ae343000
mmap(0x7f87ae349000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f87ae349000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f87ae55e4c0) = 0
mprotect(0x7f87ae343000, 16384, PROT_READ) = 0
mprotect(0x562fc10d6000, 4096, PROT_READ) = 0
mprotect(0x7f87ae574000, 4096, PROT_READ) = 0
munmap(0x7f87ae55f000, 83928)           = 0
exit_group(0)                           = ?
+++ exited with 0 +++

从上面的执行结果来看:
有我们熟悉的进程替换函数execve,进程的起源
也有我们熟悉mmap函数,在这里mmap作用就是将磁盘上的执行文件给虚拟地址空间上映射
包括open、read以及close等函数

猜你喜欢

转载自blog.csdn.net/KingOfMyHeart/article/details/89203086
今日推荐