程序员的自我修养——第六章

进程与程序:

      程序是一个静态的概念,它就是这些预编译好的指令和数据集合的一个文件;进程则是一个动态的概念,它是程序运行时的一个过程,很多时候把动态库叫做运行时也有一定的含义。

     一般来来说,C语言指针大小的位数与虚拟空间的位数相同,如果32位平台下指针为32位,即4字节;64位平台下的指针为64位,即8字节。

Intel自从1995年的Pentium Pro CPU开始采用了36位的物理地址,也就是可以访问高达64GB的物理内存。Intel把这个地址扩展方式叫做PAE(Physical Address Extension)。

 

应用程序如何使用这些大于常规内存的空间?

一个很常见的方法就是操作系统提供一个窗口映射的方法,把这些额外的内存呢映射到进程地址空间中来。在Windows下,这个访问内存的操作方式叫做AWE(Address Windowing Extensions);在Linux中采用mmap()系统调用来实现。

 

覆盖装入和页映射是两种很典型的动态装载方法,它们都利用了程序的局部性原理。

程序的局部性原理:是指程序在执行时呈现出局部性规律,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。局部性原理又表现为:时间局部性和空间局部性。时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。空间局部性是指一旦程序访问了某个存储单元,则不久之后。其附近的存储单元也将被访问。

 

源文档 <http://baike.baidu.com/view/3253308.htm>

 

从操作系统的角度,一个进程最关键的特征是它拥有独立的虚拟地址空间,这使得它有别于其他进程。

 

一个程序被执行同时都伴随着一个新进程的创建:创建一个进程,然后装载相应的可执行文件并且执行。该工程需要做三件事:

·创建一个独立的虚拟地址空间

  将虚拟空间的各个页映射至相应的物理空间,实际上只是分配了一个页目录(Page Directory)就可以了

·读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系

  建立虚拟地址空间与可执行文件的映射关系。当发生缺页故障时,操作系统应该知道当前所需要的页在可执行文件中的哪个位置,这就是虚拟空间与可执行文件之间的映射关系。

  这种映射关系只是保存在操作系统内部的一个数据构。Linux中将进程虚拟空间中的一个段叫做虚拟内存区域(VMA,Virtual Memory Area);在Windows中将这个叫做虚拟段(Virtual Section)。

·将CPU的指令寄存器设置成可执行文件的入口地址,启动并运行

 

页错误的处理:当发生Page Fault的时候,操作系统查询VMA,计算出页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该页与分配的物理页间建立映射关系,然后把控制权再还给进程,进程从刚才页错误的位置开始重新开始运行。

 

ELF文件中, 段的权限只有为数不多的几种组合:

·以代码段为代表的权限为可读可执行的段

·以数据段和BSS段为代表的权限为可读可写的段

·以只读数据段为代表的权限为只读的段。

 

对于相同权限的段,把它们合并到一起当做一个段进行映射。

 

root@ubuntu:~/Desktop/ezCode# readelf -S SectionMapping.elf

There are 29 section headers, starting at offset 0x868dc:

 

Section Headers:

  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al

  [ 0]                   NULL            00000000 000000 000000 00      0   0  0

  [ 1] .note.ABI-tag     NOTE            080480f4 0000f4 000020 00   A  0   0  4

  [ 2] .note.gnu.build-i NOTE            08048114 000114 000024 00   A  0   0  4

  [ 3] .rel.plt          REL             08048138 000138 000028 08   A  0   5  4

  [ 4] .init             PROGBITS        08048160 000160 000030 00  AX  0   0  4

  [ 5] .plt              PROGBITS        08048190 000190 000050 00  AX  0   0  4

  [ 6] .text             PROGBITS        080481e0 0001e0 065d5c 00  AX  0   0 16

  [ 7] __libc_freeres_fn PROGBITS        080adf40 065f40 000b57 00  AX  0   0 16

  [ 8] .fini             PROGBITS        080aea98 066a98 00001c 00  AX  0   0  4

  [ 9] .rodata           PROGBITS        080aeac0 066ac0 0191b0 00   A  0   0 32

  [10] __libc_subfreeres PROGBITS        080c7c70 07fc70 000030 00   A  0   0  4

  [11] __libc_atexit     PROGBITS        080c7ca0 07fca0 000004 00   A  0   0  4

  [12] .eh_frame         PROGBITS        080c7ca4 07fca4 0054d8 00   A  0   0  4

  [13] .gcc_except_table PROGBITS        080cd17c 08517c 00011a 00   A  0   0  1

  [14] .tdata            PROGBITS        080cef8c 085f8c 000010 00 WAT  0   0  4

  [15] .tbss             NOBITS          080cef9c 085f9c 000018 00 WAT  0   0  4

  [16] .ctors            PROGBITS        080cef9c 085f9c 00000c 00  WA  0   0  4

  [17] .dtors            PROGBITS        080cefa8 085fa8 00000c 00  WA  0   0  4

  [18] .jcr              PROGBITS        080cefb4 085fb4 000004 00  WA  0   0  4

  [19] .data.rel.ro      PROGBITS        080cefb8 085fb8 000030 00  WA  0   0  4

  [20] .got              PROGBITS        080cefe8 085fe8 00000c 04  WA  0   0  4

  [21] .got.plt          PROGBITS        080ceff4 085ff4 000020 04  WA  0   0  4

  [22] .data             PROGBITS        080cf020 086020 000740 00  WA  0   0 32

  [23] .bss              NOBITS          080cf760 086760 001b9c 00  WA  0   0 32

  [24] __libc_freeres_pt NOBITS          080d12fc 086760 000018 00  WA  0   0  4

  [25] .comment          PROGBITS        00000000 086760 00006c 01  MS  0   0  1

  [26] .shstrtab         STRTAB          00000000 0867cc 000110 00      0   0  1

  [27] .symtab           SYMTAB          00000000 086d64 0083e0 10     28 974  4

  [28] .strtab           STRTAB          00000000 08f144 00758a 00      0   0  1

Key to Flags:

  W (write), A (alloc), X (execute), M (merge), S (strings)

  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)

  O (extra OS processing required) o (OS specific), p (processor specific)

 

ELF可执行文件中有一个专门的数据结构叫做程序头表(Program Header Table)用来保存“Segment”的信息。

 

root@ubuntu:~/Desktop/ezCode# readelf -l SectionMapping.elf

 

Elf file type is EXEC (Executable file)

Entry point 0x80481e0

There are 6 program headers, starting at offset 52

 

Program Headers:

  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align

  LOAD           0x000000 0x08048000 0x08048000 0x85296 0x85296 R E 0x1000

  LOAD           0x085f8c 0x080cef8c 0x080cef8c 0x007d4 0x02388 RW  0x1000

  NOTE           0x0000f4 0x080480f4 0x080480f4 0x00044 0x00044 R   0x4

  TLS            0x085f8c 0x080cef8c 0x080cef8c 0x00010 0x00028 R   0x4

  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

  GNU_RELRO      0x085f8c 0x080cef8c 0x080cef8c 0x00074 0x00074 R   0x1

 

 Section to Segment mapping:

  Segment Sections...

   00     .note.ABI-tag .note.gnu.build-id .rel.plt .init .plt .text __libc_freeres_fn .fini .rodata __libc_subfreeres __libc_atexit .eh_frame .gcc_except_table

   01     .tdata .ctors .dtors .jcr .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs

   02     .note.ABI-tag .note.gnu.build-id

   03     .tdata .tbss

   04    

   05     .tdata .ctors .dtors .jcr .data.rel.ro .got

 

Elf32_Phdr结构的几个成员与使用readelf -l 命令打印文件头表显示的结果一一对应。

 

 

/usr/src/linux/include/linux/elf.h

typedef struct elf32_phdr{

Elf32_Word p_type;

Elf32_Off p_offset;

Elf32_Addr p_vaddr;

Elf32_Addr p_paddr;

Elf32_Word p_filesz;

Elf32_Word p_memsz;

Elf32_Word p_flags;

Elf32_Word p_align;

} Elf32_Phdr;

 

Elf32_Phdr->p_type

段的类型,它能告诉我们这个段里存放着什么用途的数据。此字段的值是在elf.h中定义了一些常量。例如1(PT_LOAD)表示是可加载的段,这样的段将被读入程序的进程空间成为内存映像的一部分。段的种类再不断增加,例如7(PT_TLS)在以前就没有定义,它表示用于线程局部存储。

 

Elf32_Phdr->p_flags

段的属性。它用每一个二进制位表示一种属,相应位为1表示含有相应的属性,为0表示不含那种属性。其中最低位是可执行位,次低位是可写位,第三低位是可读位。如果这个字段的最低三位同时为1那就表示这个段中的数据加载以后既可读也可写而且可执行的。同样在elf.h文件中也定义了一此常量(PF_X、 PF_W、PF_R)来测试这个字段的属性,做为一个好习惯应该尽量使用这此常量。

 

Elf32_Phdr->p_offset

该段在文件中的偏移。这个偏移是相对于整个文件的。

 

Elf32_Phdr->p_vaddr

该段加载后在进程空间中占用的内存起始地址。

 

Elf32_Phdr->p_paddr

该段的物理地地址。这个字段被忽略,因为在多数现代操作系统下物理地址是进程无法触及的。

 

Elf32_Phdr->p_filesz

该段在文件中占用的字节大小。有些段可能在文件中不存在但却占用一定的内存空间,此时这个字段为0。

 

Elf32_Phdr->p_memsz

该段在内存中占用的字节大小。有些段可能仅存在于文件中而不被加载到内存,此时这个字段为0。

 

Elf32_Phdr->p_align

对齐。现代操作系统都使用虚拟内存为进程序提供更大的空间,分页技术功不可没,页就成了最小的内存分配单位,不足一页的按一页算。所以加载程序数据一般也从一页的起始地址开始,这就属于对齐。

 

源文档 <http://hi.baidu.com/zengzhaonong/blog/item/3b9f5e347f52c24d251f14b9.html>

 

 

在Linux下,我们可以通过查看“/proc”来查看进程的虚拟空间分布

 

root@ubuntu:~/Desktop/ezCode# ./SectionMapping.elf  &

[1] 2458

 

root@ubuntu:~/Desktop/ezCode# cat  /proc/2458/maps

00da3000-00da4000 r-xp 00000000 00:00 0          [vdso]

08048000-080ce000 r-xp 00000000 08:01 209272     /root/Desktop/ezCode/SectionMapping.elf

080ce000-080d0000 rw-p 00085000 08:01 209272     /root/Desktop/ezCode/SectionMapping.elf

080d0000-080d2000 rw-p 00000000 00:00 0

08bb6000-08bd8000 rw-p 00000000 00:00 0          [heap]

bfc81000-bfca2000 rw-p 00000000 00:00 0          [stack]

 

第一列表示VMA的地址范围; 第二列是VMA的权限, rwx, “p”表示(COW),“s”表示共享,第三列表示偏移,表示VMA对应的Segment在映像文件中的偏移,第四列表示映像文件所在设备的主次设备号,第五列表示文件的节点号。最后一列表示映像文件的路径。

 

操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟空间;基本原则是将相同权限属性的、有相同映像文件的映射成一个VMA。

 

一个进程可以分为如下几种VMA区域:

·代码VMA,rx,有映像文件

·数据VMA,rwx,有映像文件

·堆VMA,rwx,无映像文件,匿名,可向上扩展

·栈VMA,rx,无映像文件,匿名,可向下扩展

 

进程虚拟空间

 

 

Linux内核中ELF可执行文件的装载/load_elf_binary()函数解析

 

源文档 <http://www.the2ndmoon.net/weblog/?p=313>

 

ELF文件的装载过程:

fork   ->   execve() -> sys_execve() -> do_execve()

do_execve() 读取文件的前128个字节判断文件的格式(一般根据魔数来判断,比如elf的头四个字节为:0x7F, e, l, f)。

然后调用search_binary_handle()去搜索和匹配合适的可执行文件装载处理过程,对于elf则调用load_elf_binary():

·检查ELF可执行文件格式的有效性

·寻找动态链接的“.interp”段,设置动态连接器路径

·根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据。

·根据ELF进程环境,比如进程启动是EDX寄存器的地址应该是DT_FINI的地址。

·将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,静态ELF可执行文件为e_entry所指的地址,对于动态ELF入口点为动态连接器。

 

Load_elf_binary()执行完毕,返回至do_execve()再返回至sys_execve(),最后一步的系统调用返回地址改成了被装在的ELF程序入口地址。当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到了ELF程序的入口地址,新程序开始执行。

 

Windows PE文件的装载 略。

 

posted @ 2012-05-06 22:11  KingsLanding  阅读(2054)  评论(0编辑  收藏  举报