参考资料:程序员的自我修养、深入理解计算机系统
*首先能称得上是计算机系统,必须具备以下:CPU、内存、I/O;对于着三个部分,我们可以使用不同厂商的硬件, 但是操作系统为了屏蔽底层硬件的差异,使应用层的用户在编写程序的时候使用统一的接口,就像我们使用的open不仅可以打开一个文件还可以打开一个socket,还可以打开一个字符设备,因为操作系统给我们提供了很多抽象的技术:基于I/O层,操作系统提供了VFS虚拟文件系统,屏蔽了I/O层的差异;为了屏蔽内存和I/O,作为资源分配的管理单位,提供了虚拟内存;为了屏蔽CPU、内存、I/O底层的差异,作为这三个部件的资源调度单位,操作系统提供了进程的概念。VFS、虚拟内存、进程也是操作系统内核离不开的三个部分。
操作系统内核将底层的硬件都屏蔽起来了,那么只要有操作系统在,我们写的代码生成的可执行文件是不可能直接被加载到物理内存上去的,他会被加载到虚拟内存上。虚拟内存的大小,与CPU的位数有关,在x86 32bit的Linux内核上CPU是32位;那么它的虚拟内存的大小就是2的32次方也就是4G;
32的CPU,它的数据总线和地址总线都是32条;
16位的CPU,数据总线16条,地址总线20条;
8位CPU数据总线8条,地址总线16条。CPU的位数与地址总线的条数无关。
CPU主要是用来运算数据的,CPU的位数是指它一次性能加以运算的最长的整数的宽度,CPU是在算术逻辑单元ALU中运算的,那么CPU的位数也就是算术逻辑单元的宽度,运算的数据是从数据总线来的,那么CPU的位数也就是数据总线的条数。
程序都是运行在虚拟内存上,那么虚拟内存在哪里?虚拟内存这个概念是IBM提出的,为了给大家普及虚拟内存的概念,IBM提出了几句话:它存在,你能看得见,它是物理的;它存在,你看不见,它是透明的;它不存在,你却看得见,它是虚拟的;实际上虚拟内存是不存在的;
每个程序在运行时,操作系统都会给它分配一个与CPU位数有关的虚拟地址空间,以32位系统为例;其中低3G为用户空间,是供用户的应用程序执行的,高1G为内核空间,主要是供内核代码运行的;每一个进程的用户控件都是独立的,所有进程的内核空间都是共享的。下图表示了虚拟地址空间的分配情况。
其中用户空间依次为:
0x00000000-0x08048000这128M是禁止访问的,操作系统在加载程序的时候都是从0x08048000开始加载的;
.text段存放指令。
.data段存放被初始化了且初始值不为0的数据。
.bss段存放未被初始化或者初始值为0的数据。可以把它想象成better save space,那么它save的是谁的space?是文件的空间,还是虚拟地址空间的空间?
接下来是堆区空间,在堆还没有被申请的时候,为堆区预留的空间称作空洞;
如果程序中有使用到库函数,那么在堆区和栈区中间存放共享库;
在共享库的高地址方向就是供函数运行的.stack栈区;
栈区上方(栈的高地址方向)存放命令行参数和环境变量。
内核空间分为3部分:ZONE_DMA直接访问内存,ZONE_NORMAL,ZONE_HIGHMEM高端内存,低16M,中间的892M,高的128M。
*.text .data .bss在程序运行起来以后是固定不变的。程序运行起来的时候还没有堆,执行到malloc或者new的时候,才会给它分配堆,程序运行起来必需的内存是代码段、数据段还有栈。
*关于指令和数据,用任何语言写代码,无非就产生了两种东西:数据和指令。局部变量属于指令,全局变量、静态全局变量、静态局部变量属于数据。
int gdata1 = 10;//数据
int gdata2 = 0;//数据 强符号
int gdata3;//数据
static int gdata4 = 11;//数据
static int gdata5 = 0;//数据
static int gdata6;//数据
int main()//指令
{
int a = 12;//指令
int b = 0;//指令
int c;//指令
static int d = 13;//数据
static int e = 0;//数据
static int f;//数据
return 0;//指令
}
将上述代码写在linux下进行分析:
main.c源文件 编译和链接
编译过程分为:
预编译:得到main.i。
编译:得到main.s,代码的优化,汇总所有的符号。
汇编:得到.o,根据汇编指令转换成特定平台的机器码,构建.o文件的格式。
最终产生.o文件也就是二进制可重定位目标文件。
链接分为两步:
1.合并所有obj文件的段,并调整段偏移和段长度,合并符号表,进行符号解析,分配内存地址。
2.链接的核心:符号的重定位
分析二进制可重定位目标文件的组成
查看obj文件主要段:objdump -h main.o
*其中.data段存放已初始化且初始值不为0的数据,上述代码中一共三个这种数据,占12个字节所以在.o文件中.data段的大小为0x0000000c;
*从段的信息上来看,虚拟内存地址跟加载的内存地址都是0,所以仅仅在编译阶段,是不给符号分配内存地址的,是在链接时分配的,符号解析完成后,就分配内存地址。
通过命令readelf -h main.o查看文件头
*二进制可重定位目标文件以及可执行文件,文件的开始都是ELF Header这样一个文件头。文件头中包含了程序运行的平台、体系;程序的入口地址,这里显示的是0地址,0地址是不可访问的,所以obj文件是不可能运行的;文件头ELF Header的大小为52个字节,转换成16进制是0x34;
.text段的段偏移是00000034,也就是对于文件组成来说,ELF Header之后就是.text段,text段的大小为0x1b,34+1b=4f,下一个段的偏移是50,因为这里的对齐方式是2的2次方,4f不能被4整除,所以这里补了一个字节。
接下来就是数据段.data,大小为0x0c,下一个段的起始地址就是5c。
接下来是.bss段,它的大小是0x14,20个字节,也就是有5个未初始化或初始值为0的数据在bss段,可是所写的代码中却有6个,这是为什么?
从段信息中可以看出.bss段的下一个段.comment段,它的文件偏移和.bss段一样都是0x5c,也就是说,data段下来根本就不是bss段而是.comment段,前面提到bss是better save space,从这里看出它save的是文件的空间。bss段不占文件的空间。
问题:既然.o文件并没有存储bss段,那么程序运行时又如何得知存储在bss段上的那么未被初始化的或初始值为0的数据呢?
在obj文件的文件头中有一Start of section headers:208个字节,十六进制是d0,表示段表(section table)在文件中的偏移量。obj文件段信息的第一行There are 9 section headers, starting at offset 0xd0,读文件头就可以知道文件的段表的位置,段表中记录了当前obj文件里边都有哪些段,段的起始偏移量,以及段的大小。也就是段表中记录了所有段的详细信息,为什么叫bss段为better save space呢?因为他不需要存初始值,所有数据的初始值都是0,那操作系统又如何知道这些数据的存在呢,就是将它的信息,将来需要占多大的内存,都记录在段表中就可以了。.data段的数据每个初始值都不一样,不记录初始值是不行的,所以这些数据需要单独存储。
[cc@localhost Desktop]$ objdump -h main.o
main.o: file format elf32-i386
Sections:
段的序号 名称 段的大小 虚拟内存地址 加载的内存地址 文件偏移 对齐方式
Idx Name Size VMA LMA File off Algn
0 .text 0000001b 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 0000000c 00000000 00000000 00000050 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000014 00000000 00000000 0000005c 2**2
ALLOC
3 .comment 0000002d 00000000 00000000 0000005c 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 00000000 00000000 00000089 2**0
CONTENTS, READONLY
[cc@localhost Desktop]$ readelf -h main.o
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 208 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 9
Section header string table index: 6
通过命令readelf -S main.o 查看obj文件所有的段:最后一个段的偏移量加上段本身的大小就是整个obj文件的大小348+4c结果是916个字节。使用ll查看mian.o的大小发现刚好是916个字节。
[cc@localhost Desktop]$ readelf -S main.o
There are 9 section headers, starting at offset 0xd0:
Section Headers:
[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 00001b 00 AX 0 0 4
[ 2] .data PROGBITS 00000000 000050 00000c 00 WA 0 0 4
[ 3] .bss NOBITS 00000000 00005c 000014 00 WA 0 0 4
[ 4] .comment PROGBITS 00000000 00005c 00002d 01 MS 0 0 1
[ 5] .note.GNU-stack PROGBITS 00000000 000089 000000 00 0 0 1
[ 6] .shstrtab STRTAB 00000000 000089 000045 00 0 0 1
[ 7] .symtab SYMTAB 00000000 000238 000110 10 8 13 4
[ 8] .strtab STRTAB 00000000 000348 00004c 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
使用命令objdump -s main.o打印.o文件中各个段的内容如下:其中.data的内容:0a000000 0b000000 0d000000 就对应代码中的gdata1=10,gdata4=11,d=13。
[cc@localhost Desktop]$ objdump -s main.o
main.o: file format elf32-i386
Contents of section .text:
0000 5589e583 ec10c745 f40c0000 00c745f8 U......E......E.
0010 00000000 b8000000 00c9c3 ...........
Contents of section .data:
0000 0a000000 0b000000 0d000000 ............
Contents of section .comment:
0000 00474343 3a202847 4e552920 342e342e .GCC: (GNU) 4.4.
0010 36203230 31323033 30352028 52656420 6 20120305 (Red
0020 48617420 342e342e 362d3429 00 Hat 4.4.6-4).
问题:如果程序里有一指针指向一个常量字符串:char *p = “hello world”,那么这个常量字符串存在哪里?
Contents of section .rodata:
0000 68656c6c 6f20776f 726c6400 hello world.
存放在rodata只读数据段。通过查看段信息发现,数据段是可读可写,代码段是可能读并且可执行。
问题:.bss段,它的大小是0x14,20个字节,也就是有5个未初始化或初始值为0的数据在bss段,可是所写的代码中却有6个,这是为什么呢?哪个数据没有被存储呢?
首先看一个例子:
text1.c
#include
short x = 10; //第三部:小端模式写值:14 00 00 00把20写到x中,1400给了x,0000给了y
short y = 10;
void func();
int main()
{
func();// //第二步:链接时选择强符号(short x) 把20 往x的内存上写 写4个字节
printf("x = %d y=%d\n",x,y); //20,0
return 0;
}
text2.c
int x;
void func()
{
x = 20;
//第一步:编译时 把20 往x的内存上写 写4个字节
}
一个工程里的多个文件是分离式编译的,text1.c编译产生text1.obj,text2.c编译产生text2.obj,这个工程在cpp下是不能通过编译的,因为cpp不允许有同名的全局变量。在c语言中可以,因为c语言有强符号和弱符号的概念。简单来说,有初始化的都叫强符号。未初始化的都叫弱符号。C语言的规则是:不能出现多个同名的强符号,出现了同名的强符号和弱符号,选择强符号,出现同名的弱符号,选择内存占用量大的弱符号。
在最初的main.c中有一 int gdata3是未初始化的全局变量,这个gdata3没有被存储在.bss段,这是一个弱符号,在链接时,其他的obj文件中可能会有名为gdata3的强符号,或者内存占用量更大的弱符号,不一定会选择这个gdata3。static int gdata6由于加了static 只有本文件可见,所以不考虑。
链接器在链接时只对所有obj文件的global符号进行处理,local的符号不做任何处理。加static关键字的都是local的符号,链接器不做处理。
符号表:数据是一定产生符号的,函数只产生一个符号,就是函数名,局部变量不产生符号,也就是是说指令只产生一个符号就是函数名。使用命令objdump -t main.o查看符号表:从符号表中可以看到gdata3在COMMON块,并没有在任何的段中,COMMON的意思就是表示它现在是一个未决定的符号,因为它现在是一个弱符号,链接时才等待选择。局部变量a,b,c是指令,不产生符号,所有的指令中只有函数名才产生符号。
[cc@localhost Desktop]$ objdump -t main.o
main.o: file format 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 .data 00000004 gdata4
00000004 l O .bss 00000004 gdata5
00000008 l O .bss 00000004 gdata6
0000000c l O .bss 00000004 f.1703
00000010 l O .bss 00000004 e.1702
00000008 l O .data 00000004 d.1701
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .comment 00000000 .comment
00000000 g O .data 00000004 gdata1
00000000 g O .bss 00000004 gdata2
00000004 O *COM* 00000004 gdata3
00000000 g F .text 0000001b main
链接过程分析
把没有分配到具体的段的符号比如UND、COM块的符号都叫符号的引用,看不到这些符号定义的地方。
在链接的时候,进行符号解析,意思就是,所有obj符号表中对符号引用的地方都要找到该符号定义的地方。比如 *UND*gdata10,一定在链接的过程中在其他的obj文件中找到gdata10的定义。
举个栗子如下的两个.c文件:
sum.c
int gdata10 = 13;
int sum(int a,int b)
{
return a+b;
}
main.c
int gdata1 = 10;
int gdata2 = 0;
int gdata3;
static int gdata4 = 11;
static int gdata5 = 0;
static int gdata6;
int main()
{
int a = 12;
int b = 0;
int c = gdata10;
static int d = 13;
static int e = 0;
static int f;
sum(a,b);
return 0;
}
sum.o中主要的段信息:
[cc@localhost Desktop]$ objdump -h sum.o
sum.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000000e 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 00000000 00000000 00000044 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000048 2**2
ALLOC
3 .comment 0000002d 00000000 00000000 00000048 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 00000000 00000000 00000075 2**0
CONTENTS, READONLY
sum.o的符号表
[cc@localhost Desktop]$ objdump -t sum.o
sum.o: file format elf32-i386
SYMBOL TABLE:
00000000 l df *ABS* 00000000 sum.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .comment 00000000 .comment
00000000 g O .data 00000004 gdata10
00000000 g F .text 0000000e sum
main.o的符号表:其中UND表示未定义的
[cc@localhost Desktop]$ objdump -t main.o
main.o: file format 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 .data 00000004 gdata4
00000004 l O .bss 00000004 gdata5
00000008 l O .bss 00000004 gdata6
0000000c l O .bss 00000004 f.1707
00000010 l O .bss 00000004 e.1706
00000008 l O .data 00000004 d.1705
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .comment 00000000 .comment
00000000 g O .data 00000004 gdata1
00000000 g O .bss 00000004 gdata2
00000004 O *COM* 00000004 gdata3
00000000 g F .text 0000003d main
00000000 *UND* 00000000 gdata10
00000000 *UND* 00000000 sum
用命令objdump -d mian.o查看汇编指令
int c = gdata10;
// 19: a1 00 00 00 00 mov 0x0,%eax
//这行汇编指令中显示的gdata10是0地址,0地址是不可访问的,说明在编译的过程中是不给符号分配内存地址的。
sum(a,b);
// 31: e8 fc ff ff ff call 32
//这行汇编指令显示sum函数的地址为fffffffc(小端模式),这是一内核空间的地址,sum函数不可能在这个地址,说明在编译的过程中是不给符号分配内存地址的。程序新运行的过程中,pc寄存器中存放了下一行指令的地址,由于下一行指令是0地址,减四就得到了sum函数的地址。
//总结:在编译过程中,所有数据填的都是0地址,函数填的都是跟下一行指令地址的偏移量(-4)。
链接过程
1.合并所有obj文件的段:
obj文件是按4字节对齐的,但是可执行文件是按页面对齐的,32位系统的一个页面是4096个字节=4kb,链接过程合并所有obj文件的段,所有相同属性的段进行合并组织在一个页面上。相同属性比如data和bss他们都是数据都可读可写,所以就把他们合并到一块,这样合并比纯粹的按文件堆积合并效率高,最主要的是可执行文件的大小会小一些,因为按页面对齐,所以我们写一个简单的hello world 也会占好几kb的空间。
2.符号的重定位
可执行文件分析
使用命令ld -e main -o run *.o链接所有.o文件,得到可执行文件run
objdump -h run查看段信息,可以看到bss段大小为18,即24个字节,6个整型变量,也包括了gdata3。
[cc@localhost Desktop]$ objdump -h run
run: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000004e 08048094 08048094 00000094 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000010 080490e4 080490e4 000000e4 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000018 080490f4 080490f4 000000f4 2**2
ALLOC
3 .comment 0000002c 00000000 00000000 000000f4 2**0
CONTENTS, READONLY
objdump -t run 查看符号表:每个符号都有内存地址了。
[cc@localhost Desktop]$ objdump -t run
run: file format elf32-i386
SYMBOL TABLE:
08048094 l d .text 00000000 .text
080490e4 l d .data 00000000 .data
080490f4 l d .bss 00000000 .bss
00000000 l d .comment 00000000 .comment
00000000 l df *ABS* 00000000 main.c
080490e8 l O .data 00000004 gdata4
080490f8 l O .bss 00000004 gdata5
080490fc l O .bss 00000004 gdata6
08049100 l O .bss 00000004 f.1707
08049104 l O .bss 00000004 e.1706
080490ec l O .data 00000004 d.1705
00000000 l df *ABS* 00000000 sum.c
080480d4 g F .text 0000000e sum
080490e4 g O .data 00000004 gdata1
080490f4 g *ABS* 00000000 __bss_start
08048094 g F .text 0000003d main
08049108 g O .bss 00000004 gdata3
080490f0 g O .data 00000004 gdata10
080490f4 g *ABS* 00000000 _edata
0804910c g *ABS* 00000000 _end
080490f4 g O .bss 00000004 gdata2
每个符号都有了虚拟地址空间上的地址,这个时候objdump -d run查看汇编指令,发现所有数据符号都填的是绝对地址。函数符号,因为要涉及指令跳转,所以存的都是偏移量。
80480c5: e8 0a 00 00 00 call 80480d4
80480ca: b8 00 00 00 00 mov $0x0,%eax
pc寄存器中存的是下一行指令的地址也就是80480ca,这个地址加上偏移量0a000000等于080480d4。符号表中显示的sum函数的地址刚好就是080480d4。CPU在进行指令访问的时候,永远是从pc寄存器中取地址,当运行到当前指令的时候,pc寄存器里放的是下一行指令的地址,call指令涉及的是指令的跳转,意思也就是从call以后要调到其他的代码段去执行,而不是继续运行下一行。跳转的位置就是偏移量加下一行指令的地址。
[cc@localhost Desktop]$ objdump -d run
run: file format elf32-i386
Disassembly of section .text:
08048094 :
8048094: 55 push %ebp
8048095: 89 e5 mov %esp,%ebp
8048097: 83 e4 f0 and $0xfffffff0,%esp
804809a: 83 ec 20 sub $0x20,%esp
804809d: c7 44 24 14 0c 00 00 movl $0xc,0x14(%esp)
80480a4: 00
80480a5: c7 44 24 18 00 00 00 movl $0x0,0x18(%esp)
80480ac: 00
80480ad: a1 f0 90 04 08 mov 0x80490f0,%eax
80480b2: 89 44 24 1c mov %eax,0x1c(%esp)
80480b6: 8b 44 24 18 mov 0x18(%esp),%eax
80480ba: 89 44 24 04 mov %eax,0x4(%esp)
80480be: 8b 44 24 14 mov 0x14(%esp),%eax
80480c2: 89 04 24 mov %eax,(%esp)
80480c5: e8 0a 00 00 00 call 80480d4
80480ca: b8 00 00 00 00 mov $0x0,%eax
80480cf: c9 leave
80480d0: c3 ret
80480d1: 90 nop
80480d2: 90 nop
80480d3: 90 nop
080480d4 :
80480d4: 55 push %ebp
80480d5: 89 e5 mov %esp,%ebp
80480d7: 8b 45 0c mov 0xc(%ebp),%eax
80480da: 8b 55 08 mov 0x8(%ebp),%edx
80480dd: 8d 04 02 lea (%edx,%eax,1),%eax
80480e0: 5d pop %ebp
80480e1: c3 ret
run可执行文件的组成格式:
首先是文件头ELF Header 用命令readelf -h run来查看
这里的Entry point address已经不是0了而是0x8048094,这是main函数第一行指令的地址。
[cc@localhost Desktop]$ readelf -h run
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048094
Start of program headers: 52 (bytes into file)
Start of section headers: 344 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 3
Size of section headers: 40 (bytes)
Number of section headers: 8
Section header string table index: 5
程序的运行-进程
./a.out
1.创建虚拟地址空间到物理内存的映射(创建内核地址映射结构体),创建页目录和页表
2.加载代码段和数据段。
3.把可执行文件的入口地址写到CPU的pc寄存器里面。
查看可执行文件的段信息:
[cc@localhost Desktop]$ objdump -h run
run: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000004e 08048094 08048094 00000094 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000010 080490e4 080490e4 000000e4 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000018 080490f4 080490f4 000000f4 2**2
ALLOC
3 .comment 0000002c 00000000 00000000 000000f4 2**0
CONTENTS, READONLY
问题1:text段的大小是4e,Size of this header是52字节也就是0x34,4e+34=0x82,可是text段的偏移却是0x94,说明在ELF Header和text中间还有一块,那么这里存的又是什么呢?
文件头里有一个Start of program headers:52bytes,也就是0x34,所以通过偏移量得知在ELF Header和text中间的就是program header。
Number of program headers:3
Size of program header:32
program header的总大小是3*32=96字节 ,它的大小加上偏移量52字节 = 0x94;刚好就是text的偏移量。
一共有3个program header,用命令readelf -l run查看它的内容,有两个LOAD页,它的对齐方式是0x1000就是4k,也就是按页面对齐的。其中00 是.text,01是.data .bss 。这两个LOAD页就指示了操作系统的加载器应该把当前程序的哪些东西加载到内存中去,以及哪些东西在加载时组织在一个页面。
[cc@localhost Desktop]$ readelf -l run
Elf file type is EXEC (Executable file)
Entry point 0x8048094
There are 3 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x000e2 0x000e2 R E 0x1000
LOAD 0x0000e4 0x080490e4 0x080490e4 0x00010 0x00028 RW 0x1000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section to Segment mapping:
Segment Sections...
00 .text
01 .data .bss
02
当程序运行的时候,磁盘上的可执行文件,它本身的存储结构还是按段存储的,只是有两个LOAD项,这两个LOAD项指明了在加载的时候哪些段应该组织在一个页面上。下图中的DP1DP2就是这两个LOAD项。
磁盘上的Disk page往内存上加载的时候是先加载到虚拟地址空间VP上。然后再从虚拟地址空间上映射到物理内存PP。物理内存和虚拟内存的管理方式都是页面。可执行文件为什么要将段按页面组织?因为虚拟地址空间跟物理内存的基本单位都是页面,是为了方便映射。
使用mmap函数把磁盘上的页面映射到虚拟地址空间,也就是在虚拟地址空间上开辟内存的方式是使用mmap函数。虚拟地址空间上的虚拟页是通过多级页表的方式映射到物理内存上的。