深入编译链接和运行

可执行文件他们都在磁盘上存放,当要运行程序(指令和数据)时会加载到内存上。但因为操作系统要屏蔽底层硬件的差异,所以不会直接将程序加载到内存上,而是加载到虚拟地址空间上。

为了屏蔽底层硬件的差异,操作系统提供了很多方法。

为了屏蔽底层I/O(磁盘)的差异操作系统提供了VFS(虚拟文件系统)。

为了屏蔽内存与I/O的差异操作系统提供了虚拟存储器(虚拟内存)

虚拟内存:交换分区就是在磁盘上划分的一块区域,指的就是I/O的那部分

为了屏蔽CPU,内存与I/O的差异操作系统提供了进程,其也作为CPU,内存与I/O资源部件调度的单位

虚拟地址空间的大小与cpu的位数有关,我们讨论的都是x86 32bit Linux内核

虚拟地址空间的大小是2^(cpu的位数),32位即2^32==4G 

Cpu的位数(CPU的寻址能力)指的不是地址总线的条数而是一次性加以运算的最长整数的宽度,因为他在ALU中运算,即Cpu的位数是ALU的宽度。又因为数据是从数据总线来的,即数据总线的条数也是Cpu的位数。但我们经常说32位地址总线是因为32位的操作系统数据总线和地址总线都是32条。但是16位的数据总线是16条,地址总线是20条,8位的数据总线是8条,地址总线是16条。

32位   4G的虚拟地址空间(每一个进程都有4G的虚拟地址空间)

程序运行起来的时候是没有堆的,当我们运行到malloc或new时才会开辟堆(堆区还没有被申请时我们称他为一个空洞)

程序运行时必须的内存是代码段,数据段及栈区。因为我们运行时就要进入第一个函数,函数的运行必须给他提供栈上的内存。(符号是在符号表中,不占内存,占的是磁盘的空间即可执行文件的空间,可执行文件里有这些所有段和段表,符号表等各种表,但是在运行的时候只加载代码段和数据段,其他东西都不加载。

Libc.so   C语言的动态链接库    libc++.so   C++的动态链接库

每一个进程的用户空间都是独立的,而内核空间都是共享的(因为只有一个操作系统呀……)。

编译(main.c)

预编译  --->  生成main.i文件

编译   --->  生成main.s文件

汇编  --->  生成main.o/main.obj文件(二进制可重定位目标文件)

预编译阶段:代码文本的替换,处理#开头的指令<eg:拷贝包含#include包含的文件代码,#define宏定义的替换,条件编译等>

编译阶段:语法语义及词法的分析,代码的优化,汇总所有的符号(ps:数据产生符号,指令只产生一个符号(函数名))

汇编阶段:将汇编指令转化为机器码,构建*.o文件或*.obj文件的格式

链接

1:合并所有.o /.obj文件的段,并调整段偏移和段长度,合并符号表,进行符号解析(符号解析完才分配内存地址)。

2:链接的核心:符号重定位。

链接器只对所有obj文件的global符号进行处理,local的符号不做任何处理

这里的start of Section headers记录的是段表(section table)的位置,段表记录了所有段的信息,所以即便你文件中并没有.bss,但是段表中记录了,还是知道它占多大字节,因为其.bss段中的数据初始值都是0,所以没必要存放在文件中,节省了空间。

下图只是一部分数据和代码段,还有很多段(例如符号表段等等并未画出)

由上图的地址(都是00000000)可以看出编译的时候并不分配内存地址,实际上在链接的时候才分配的内存地址。

.bss段可称为best save spac1e,即节省空间,但他节省的说虚拟地址空间还是文件的空间呢?由上图可以看到.bss段的起始与.comment的起始一样,说明.bss在这个main.o文件中并不存在,所以它是节省了文件的空间

C语言中有强弱符号之分(非静态全局变量才区分,因为静态的变量是本文件可见的),凡是初始化的都是强符号,未初始化的都为弱符号。如果两个文件中都是强符号则会链接错误,如果一强一弱,则采用强符号,如果都是弱符号,则采用内存占用量最大的那个。

由上图可以看出.bss段中只有14(即20字节)大小,但是我们定义的未初始化或初始化为0的有24个字节,哪那个没有存放在.bss段呢?答案就是gdata3,因为它是一个弱符号,所以我不能保证我程序中用的gdata3就是我这个文件中的gdata3,因为有可能是其他文件中的,所以就没有把他放在.bss段中。

可以看出gdata3在common块中存放,common块中存放的是一些未决定的符号,弱符号,在链接的时候才选择。

定义了一个字符串常量,可以看出这个常量hello,world在.rodata段,即只读数据段,即常量都在rodata段存放。

下面我们来看这个程序:

Sum.o的符号表

Main.o的符号表

因为编译都是单独编译的,所以在main.c中看不到sum.c中的定义,所以这个符号在main.c中只能存放在UND区(即undefine),所以不能用这个变量给数据赋值,因为编译时没办法找到其定义,只能给在栈区的指令赋值。

我们将COM/UND块中的都称为符号的引用,因为看不到符号定义的地方。

所以在连接时进行的符号解析是什么呢?即所有obj符号表中对符号引用的

地方都要找到改符号定义的地方。

Main.o的指令段

Pc寄存器保存的是下一行指令的地址 ,编译过程中使用数据的地方都是0,使用函数名的地方都是一个跟下一行指令地址的偏移量(-4因为32位系统指针即4个字节)

 

我们可知道在elf文件中是以2^2次方字节对齐的,但是在可执行程序中是以页面对齐(4k)的,所以我们如果在链接过程中以下图方式合并,将会非常浪费空间。

所以我们链接要合并段,我们以何种方式进行合并呢?

所有相同属性(即读写执行属性)的段进行合并,组织在一个页面上。

因为合并了所有obj文件的段,所以我们就要调整段偏移和长度,每一个obj文件都有各自的符号表,所以我们要合并符号表,合并符号表的目的就是为了进行符号解析,下来就是给符号分配内存地址,以上这些其实就是链接的第一步。接下来链接的第二步就是符号重定位。

链接完成之后可以看出bss段中已成24个字节,即gdata3加入bss段。

由run的符号表可看出每个符号已存在他合法的虚拟地址空间上的地址,此时我们将指令段中之前没有写正确的地址改成正确的地址,这个过程就叫做符号的重定位。

数据都传的是绝对地址,而函数存在指令跳转,它传的都是与下一行地址的偏移量。所以用当前行的偏移量加上下一行指令的地址即跳转地址(0x80480ca

+ca=0x80480d4

下图是可执行文件的头部信息:可以看出这时候程序的入口地址已经更改为主函数main的地址,不是之前编译时的0地址了。

不同于编译时的main.o文件的头部信息,多了一个program header,我们来看看这个run文件存储的段表信息

从段表中可以看出实际上在run文件的存储中也是一个段一个段存储的,哪为什莫没有用页面存储呢?我们可以看出.text段的起始偏移量并不是0x34,而是0x94,说明elf header下面并不是直接连的.text段,而是我们说的这个program header,它的起始位置是0x34(52byte),它的大小是32*3=96byte,加上起始偏移量52字节再转换为16进制,即0x94.

为什么要存在这个program header呢?

我们来看看它的存储结构

正是因为有了这个program header才使得.o文件不能被执行,而.exe文件却可以被调度。它的两个LOAD项指明了那些段在加载的时候放在一个页。

 

磁盘往虚拟地址空间上映射用的是mmap这个函数

虚拟地址空间里存放的是磁盘上数据和指令的地址,实际上就是存放的一种映射关系,从虚拟地址空间和磁盘的映射。再从虚拟地址空间和物理内存映射,最终将磁盘上的数据和指令加载到物理内存上。

程序的运行过程(进程)

1:创建虚拟地址空间到物理内存的映射(创建内核地址映射结构体),创建页目录和页表

2:加载代码段和数据段

3:把可执行文件的入口地址写道CPU的pc寄存器中。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值