深入编译链接和运行

参考资料:程序员的自我修养、深入理解计算机系统

*首先能称得上是计算机系统,必须具备以下:CPU、内存、I/O;对于着三个部分,我们可以使用不同厂商的硬件, 但是操作系统为了屏蔽底层硬件的差异,使应用层的用户在编写程序的时候使用统一的接口,就像我们使用的open不仅可以打开一个文件还可以打开一个socket,还可以打开一个字符设备,因为操作系统给我们提供了很多抽象的技术:基于I/O层,操作系统提供了VFS虚拟文件系统,屏蔽了I/O层的差异;为了屏蔽内存和I/O,作为资源分配的管理单位,提供了虚拟内存;为了屏蔽CPU、内存、I/O底层的差异,作为这三个部件的资源调度单位,操作系统提供了进程的概念。VFS、虚拟内存、进程也是操作系统内核离不开的三个部分。
深入编译链接和运行_第1张图片
操作系统内核将底层的硬件都屏蔽起来了,那么只要有操作系统在,我们写的代码生成的可执行文件是不可能直接被加载到物理内存上去的,他会被加载到虚拟内存上。虚拟内存的大小,与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为内核空间,主要是供内核代码运行的;每一个进程的用户控件都是独立的,所有进程的内核空间都是共享的。下图表示了虚拟地址空间的分配情况。
深入编译链接和运行_第2张图片
其中用户空间依次为:
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张图片
一共有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函数。虚拟地址空间上的虚拟页是通过多级页表的方式映射到物理内存上的。
深入编译链接和运行_第4张图片

你可能感兴趣的:(机器人)