目录
一. 计算机系统
计算机系统必须有CPU(运算器、控制器),内存(存储器),IO(输入设备和输出设备)。对于这三块我们可以用不同厂商的硬件,但是操作系统为了屏蔽底层硬件的差异,使应用层在编写程序的时候调用统一的接口(比如open,不仅可以打开一个文件,还可以打开一个socket,甚至还可以打开一个字符设备),为什么会这样?因为操作系统给我们提供了很多抽象的技术,比如
- 为了屏蔽底层IO的差异,基于IO层操作系统提供了VFS虚拟文件系统;
- 为了屏蔽内存和IO,也是作为资源分配和管理的单位,我们有虚拟存储器(虚拟内存);
- 为了屏蔽CPU、内存和IO,也作为这三个部分资源调度的基本单位,操作系统给我们提供了进程。
进程、虚拟内存和VFS也是我们操作系统离不开的三个非常重要的部分。
既然操作系统内核把底层的硬件都屏蔽起来了,那就说明只要有操作系统在,所写的代码生成的可执行文件是不可能直接被加载到物理内存上,而是加载到虚拟内存上。
虚拟内存的大小和CPU的位数有关,如果CPU位数是32,那么它的大小就是2的32次方,即4G。CPU主要是用来运算数据的,CPU的位数指的是它一次能运算的最长的整数的宽度,CPU在ALU(算数逻辑单元)里运算数据,也就是说CPU的位数指的是ALU的宽度。数据是从数据总线来的,所以CPU的位数也指的是数据总线的条数,不是地址总线的条数。
- 32位CPU:数据总线为32条,地址总线32条
- 16位CPU:数据总线为16条,地址总线20条
- 8位CPU:数据总线为8条,地址总线16条
二. 虚拟地址空间
每一个程序在经过编译链接后运行起来内核都会提供虚拟地址空间。如果CPU是32位的,则每个进程就有4G的虚拟地址空间(CPU寻址的能力)
IBM对虚拟地址空间的解释:
- 它存在,你能看得见,它是物理的;
- 它存在,你看不见,它是透明的;
- 它不存在,你却看得见,它是虚拟的;
- 它不存在,你也看不见,它被删除了!
4G虚拟地址空间的分布:
.text,.data和.bss
段在程序运行起来后大小是固定不变的,用来存放指令和数据,指令和数据被生成后是固定不变的,此时还没有.heap
段,只有malloc
时OS才会分配.heap
。程序刚运行起来还必须有.stack
,因为程序运行后马上要进入第一个函数,而函数的运行必须提供栈上的内存。
全局变量、静态全局变量和静态局部变量都属于数据。
- .text:代码段,存放代码,指令,可读可执行
- .rodata:只读数据区,存放常量
- .data:存放初始化的全局变量且初始化值不为0的数据,可读可写
- .bss:存放未初始化和初始化为0的数据(包括全局变量,static修饰的变量),可读可写
- .heap:堆区
- .stack:栈区(可存放函数形参和局部变量)
每一个进程的用户空间独立,内核空间共享
内核空间分为三部分:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM
- ZONE_DMA:16M,加快磁盘和内存交换数据用的,再没有DMA之前,磁盘和内存之间交互数据必须通过总线,流经CPU的寄存器才能到达对方,这是对CPU极大的浪费。有了DMA,比如在磁盘加载一个文件到内存当中,数据从磁盘经过系统总线流向物理内存当中的时候不用经过CPU的寄存器,此时如果一个进程在打开一个文件涉及了磁盘上的数据流向物理内存中,此时CPU就可以空闲下来调度其他的进程。
- ZONE_NORMAL:892M,常用的部分,也是内核最重要的部分。
- ZONE_HIGHMEM:128M,主要是用来在32位Linux系统在内核里面映射高于1G物理内存的时候,会用到高端内存,64为系统没有高端内存,因为64位系统达到了512G,不需要使用这一区域。
三、 编译链接过程
1. 预编译
删除注释、处理以#开头的预编译指令,不做任何有效的类型信息检查
gcc -E main.c -o main.i
2. 编译
语法分析、语义分析、词法分析、代码的优化、汇总所有的符号
gcc -S main.i -o main.s
3. 汇编
- 编译完后得到的main.s汇编文件后,里面有各种汇编指令:move,add,lea,sub。汇编指令只有两种:inter x86和AT&T,这两种汇编指令非常相似,汇编根据特定平台(windows和linux)把汇编指令转化成特定平台的机器码
- 构建*o或者*obj文件的格式。在*.o文件中有符号表,只有数据(包括全局变量,static修饰的变量)才产生符号,局部变量生成的是指令
gcc -c main.s -o main.o
4. 链接
- 合并所有obj文件的段,并调整段偏移和段长度,合并符号表,进行符号解析,然后再给符号分配内存地址
- 链接的核心—符号的重定位
gcc main.o -o main
四. 解析ELF文件头
#include<stdio.h>
int gdata1 = 10; // .data
int gdata2 = 0; // .bss
int gdata3; // .bss
static int gdata4 = 11; // .data
static int gdata5 = 0; // .bss
static int gdata6; // .bss
int main() {
// 生成汇编指令,而不是产生数据
// 运行到该段代码时,才在函数栈帧上开辟4字节的空间存放数据
int a = 12;
int b = 0;
int c;
// 放在数据段,程序启动的时候不会初始化,运行到该代码后再初始化
// 程序运行起来后,.bss段被清0
static int d = 13; // .data
static int e = 0; // .bss
static int f; // .bss
return 0;
}
1. 查看ELF文件头
readelf -h main.o
2. 查看所有段
readelf -S main.o
- ELF文件头的大小为52B,即0x34
- .text:偏移地址为0x34,段大小为0x1b,0x34 + 0x1b = 0x4f,无法被4字节整除(对齐方式),所以补了一个字节,.data从0x50开始
- .data:偏移地址为0x50,段大小为0x0c
- .bss:偏移地址为0x5c,段大小为0x14,表示占20B,可是我们推测的是有6个int变量存储在.bss段,而且为什么.bss的下一个段的偏移地址还是0x50,而不是0x50+0x14。原因在后文
- .comment:偏移地址为0x5c,段大小为0x2d,偏移地址和.bss的偏移地址重合,并把.bss覆盖,说明该ELF没有存储.bss段,.bss省的是ELF文件的空间
- ELF文件就是一个文件头,加上各种段,最后一个段的偏移地址 + 该段的大小,就是整个ELF文件的大小
所以obj文件的格式为
查看代码汇编完成后得到的机器码用16进制的表示
objdump -d main.o
打印常用的段表
objdump -h main.o
打印段的内容
objdump -s main.o
3..bss
段并没有在文件中存储,运行的时候如何知道这些初始值为0的变量存在?
.bss
为什么叫better save space
,因为它不像.data段,不需要存储初始值,因为编译系统认为这没必要存储,存储是为了记录它的初始值,既然它的初始值将来都为0,那就没必要存储。虽然不用存储,但是操作系统应该知道它的存在,是通过把它的信息(将来需要占用多大内存)记录在段表中得知。
通过读文件头就能知道当前文件段表的位置,在段表里面记录了文件里所有文件的详细信息
段大小为0x14,表示占20B,可是我们推测的是有6个int变量(24B)存储在.bss段,为什么少了一个整数?
和强弱符号有关系,因为gdata3
是弱符号,obj
完成后需要将所有obj
文件一块链接,有可能找到同名的gdata3
的强符号或者内存占用更大的弱符号,所以不一定选择的就是本文件中的gdata3
。
gdata6
和f
也没有初始化,也是弱符号但是却存在在.bss
段,因为它们是static
静态变量,只是本文件可见,链接的时候也无法被覆盖。
链接器在链接的时候只对所有的global符号处理,local的符号不做处理。
可以看到所有变量存放正常,gdata3
存放在*COM
,因为global
弱符号链接前暂时记录在*COM*
块,这就是为什么我们之前分析的6个未初始化和初始化为0的数据应该全部存放在.bss
段,实际上只存了5个。
4. 强弱符号
在C语言工程里规定,
- 有初始化的叫强符号,没初始化的叫弱符号;
- 出现多个同名强符号,编译肯定出错;
- 如果出现同名强符号和弱符号,最终选择强符号;
- 如果出现同名的多个弱符号,选择内存占用最大的符号。
test.c
//x没初始化,程序运行起来x不一定是当前弱符号x
//因为在链接的时候有可能遇到同名的强符号或者内存占用更大的弱符号
int x;
void fun()
{
x = 20;
}
main.c
#include<stdio.h>
short x = 10;
short y = 10;
void fun();
int main()
{
fun();
printf("x=%d,y=%d", x, y);
return 0;
}
一个工程里所有文件都是独自编译,不是一起编译。test.c编译时会产生test.o文件,main.c在编译会产生main.o文件。
test.c
中x=20
在编译的时候访问的x
是当前文件的x
,因为它看不见其他文件的x
,这里编译把20
往x
的内存上写4B
,但是在链接的时候同时发现弱符号int x
和强符号short x=10
,选择了main.c
中强符号的short x
,确定x
的地址就是short x
的地址,汇编指令把20
写入x
的地址,小端模式,所以写的是14 00 00 00,14 00给x
,00 00 给y
,所以结果输出是20
和0
。