程序员的自我修养——第六章
进程与程序:
程序是一个静态的概念,它就是这些预编译好的指令和数据集合的一个文件;进程则是一个动态的概念,它是程序运行时的一个过程,很多时候把动态库叫做运行时也有一定的含义。
一般来来说,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文件的装载 略。