文章目录
- 一、引入虚拟地址空间
- 二、虚拟地址空间
- 三、编译链接过程
- 1. 预编译
- 2. 编译
- 3. 汇编
- 4. 链接
- 四、解析ELF文件
- 1. 查看ELF文件头
- 2. 查看段表
- 3. ELF文件不存储.bss段,最后如何给.bss段分配虚拟地址空间?
- 4. 关于强弱符号
- 5. global弱符号链接前暂时记录在\*COM* 块
一、引入虚拟地址空间
C/C++代码经过编译器编译链接后,需要把指令和数据加载到内存执行
计算机由CPU(运算器、控制器)、内存(存储器)、IO(输入设备和输出设备)组成,为了屏蔽硬件的差异,使应用层能够忽略这些差异,操作系统提供了统一调用的接口(比如系统调用open,不仅可以用来打开文件,也可以打开socket,还可以打开字符设备)
- 为了屏蔽I/O的差异,OS提供了VFS(Virtual File System)
- 为了屏蔽内存和I/O的差异,OS提供了虚拟存储器(虚拟内存)
- 为了屏蔽CPU、内存、I/O,OS提供了进程
cpu的位数代表着cpu一次性能够处理的数据的位数(ALU的宽度或者数据总线的条数),32位代表cpu能够处理32位的数据,就是4个字节的大小。64位cpu代表cpu一次性能够处理64位的数据,也就是8个字节的大小的数据。
- 32位CPU:数据总线为32条,地址总线32条
- 16位CPU:数据总线为16条,地址总线20条
- 8位CPU:数据总线为8条,地址总线16条
CPU的位数就是地址总线的条数,这是错误的
二、虚拟地址空间
我们的代码经过编译链接后,运行起来时,OS会给该进程提供一个虚拟地址空间,虚拟地址空间的大小和CPU的位数有关,比如说CPU是32位的,那每个进程的虚拟地址空间就有,虚拟地址空间也可以理解为CPU寻址的能力。
IBM对虚拟地址空间的解释:
如果它存在,而且你能看见它---它是真实的(real)
如果它不存在,但你能看见它---它是虚拟的(virtual)
如果它存在,但你看不见它---它是透明的(transparent)
如果它不存在,而且你也看不见它---那肯定是你把它擦掉了
生成ELF文件后,.text
,.data
,.bss
段的大小在程序运行时都是不变的,这些段是用来存放指令和数据的,指令和数据被生成后都是固定不变的,不可能写的变量一会有一会没有。而这个时候是没有.heap
的,只有我们malloc申请内存时,OS才会分配.heap
。此外程序刚运行起来还需要有.stack
,因为函数就是在栈上运行的
.text
:代码段,存放代码,指令
.rodata
:只读数据区,存放常量
.data
:存放初始化且初始化值不为0的数据
.bss
:存放未初始化和初始化为0的数据(包括全局变量,static修饰的变量)
.heap
:堆区
.stack
:栈区(可存放函数形参和局部变量)
内核空间是共享的,用户空间是独立的
内核也分三部分:ZONE_DMA(0 ~ 16M)、ZONE_NORMAL(16M ~ 896M)、ZONE_HIGHMEM(896M ~ 结束)
- ZONE_DMA(Direct Memory Access):加快磁盘和内存交换数据。没有DMA的时候,磁盘和内存之间交换数据时,数据必须通过总线经过CPU寄存器才能到达内存,这非常浪费CPU资源。有了DMA以后,某进程进行I/O的时候CPU就会空闲下来去调度其他的进程,增大了CPU的使用效率
- ZONE_NORMAL:该区域的物理页面是内核能够直接使用的
- ZONE_HIGHMEM:32位OS在内核里映射高于1G的物理内存时会用到高端内存。64位系统是没有高端内存的,因为64位系统的内核空间高达512G,不需要使用这一区域
#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. 预编译
gcc -E main.c -o main.i
生成预编译文件,进行头文件引入以及宏替换,同时清理注释。不做任何有效的检查
2. 编译
gcc -S main.i -o main.s
词法分析、语法分析、语义分析、代码优化,生成符号(我们编译链接的错误基本上都和符号表有关)
3. 汇编
gcc -c main.s -o main.o
.s
文件中有很多汇编指令,汇编这一过程就是把汇编指令转换为特定平台的机器码
*.o
称为二进制可重定位目标文件,在*.o
文件中有符号表,只有数据(包括全局变量,static修饰的变量)才产生符号,局部变量生成的是指令
4. 链接
gcc main.o -o main
- 合并所有
.o
文件的段,并调整段偏移的长度,合并符号表,进行符号解析,然后再给符号分配内存地址(虚拟地址) - 链接的核心:符号重定位
四、解析ELF文件
1. 查看ELF文件头
readelf -h main.o
2. 查看段表
readelf -S main.o
- ELF文件头的大小为52字节,0x34
-
.text
:偏移地址为0x34,段大小为0x1b,0x34 + 0x1b = 0x4f,无法被4字节整除(对齐方式),所以补了一个字节,.data
从0x50开始 -
.data
:偏移地址为0x50,段大小为0x0c -
.bss
:偏移地址为0x5c,段大小为0x14,表示占20个字节,可是我们推测的是有6个int变量存储在.bss
段,不对??(我们后面解释) -
.comment
:偏移地址为0x5c,段大小为0x2d,偏移地址和.bss
的偏移地址重合,并把.bss
覆盖,说明该ELF没有存储.bss
段,.bss
省的是ELF文件的空间 - ELF文件就是一个文件头,加上各种段,最后一个段的偏移地址 + 该段的大小,就是整个ELF文件的大小
故ELF文件组成格式为:
查看ELF文件的内容
objdump -s main.o
3. ELF文件不存储.bss段,最后如何给.bss段分配虚拟地址空间?
由于.bss
存储的都是没有初始化或者初始化为0的数据,ELF文件中不会存储.bss
段,那程序运行起来的时候,怎么知道存储在.bss
段的信息?
先查看文件头,找到段表的位置,然后查看.bss
段占了多少空间,运行的时候就开辟多少虚拟内存,全部初始化为0
4. 关于强弱符号
C语言里面有强符号(初始化)和弱符号(未初始化)的概念,如果在C语言工程里面:
- 出现多个强符号,编译肯定出错
- 出现同名的强、弱符号,我们选择强符号
- 出现同名的弱符号,我们选择内存占用最大的弱符号
比如:出现多个强符号,编译出错
编译的时候,每个源文件独自编译,链接的时候再整合符号
test.c
// 在这里就应该能看出来,程序运行起来使用的x不一定是当前这个弱符号x
// 因为其他文件中可能还存在同名强符号或者内存占用更大的弱符号
int x;
void func() {
// 编译阶段:20写入x的内存,写4个字节
x = 20;
}
main.c
#include<stdio.h>
short x = 10;
short y = 10;
void func();
int main() {
func();
printf("%d %d\n", x, y); // 20 0
return 0;
}
在func函数中,编译阶段生成 把立即数20写入x的内存,写4个字节的指令(mov dword ptr [x], 14h ),链接阶段同时发现弱符号int x
和强符号short x = 10
。于是确定x的地址,就是这个short x
的地址,汇编指令中把20写入x的地址,实际上是写入了强符号short x
的地址
虽然编译阶段就确定short x
和short y
存放在.data
段,并且确认了初始值都为10。程序执行起来的时候,执行了func函数相关的汇编指令,在x的内存上写入4个字节,写入的是14 00 00 00,所以起始地址为&x,往后的4个字节被赋值为14 00 00 00,于是修改了short x
和short y
的值
5. global弱符号链接前暂时记录在*COM* 块
在查看段表的部分,我们知道.bss
的偏移地址为0x5c,段大小为0x14,表示占20个字节,可是我们推测的是有6个int变量存储在.bss
段,我们解释一下:
链接的时候处理所有的obj文件中的global符号,而不处理local符号(本文件可见)。而int gdata3
是global的,且是弱符号,可能被其他我文件中同名的强符号覆盖。static int gdata6
虽然也是弱符号,但它是local的,只是本文件可见,链接的时候也无法被覆盖。
查看符号表(main.o是最早的那个代码)
objdump -t main.o
shen@NONOR-shen:/mnt/c/Users/shen/Desktop$ objdump -t main.o
main.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df
我们可以看到所有的变量都是存放正常的,除了弱符号gdata3存放在*COM*
块(表示gdata3当前是一个未决定的符号,需要等到链接完成才能决定具体存放在ELF文件的哪个部分),这就是为什么我们之前分析的6个未初始化和初始化为0的数据应该全部存放在.bss
段,实际上只存了5个
我们链接一下,然后查看符号表:
shen@NONOR-shen:/mnt/c/Users/shen/Desktop$ gcc main.o -o main
shen@NONOR-shen:/mnt/c/Users/shen/Desktop$ objdump -t main
1: file format elf64-x86-64
SYMBOL TABLE:
0000000000002000 l d .rodata 0000000000000000 .rodata
0000000000003df0 l d .init_array 0000000000000000 .init_array
0000000000003e00 l d .dynamic 0000000000000000 .dynamic
0000000000004000 l d .data 0000000000000000 .data
000000000000401c l d .bss 0000000000000000 .bss
0000000000000000 l d .comment 0000000000000000 .comment
000000000000401c l O .bss 0000000000000001 completed.8061
0000000000000000 l df
可以看到链接完成后,gdata3已经确定存放在.bss
段了