Paul C's Blog

To be funny,to grow up!

0%

Chap6-可执行文件的装载和进程

假设程序都是静态链接的,先从整体上把握程序的装载过程,下一章将把程序拆成模块来观察。

Linux下的分段故障Segmentation fault与Windows的“进程因非法操作需要关闭,很多时候是因为进程访问了未经允许的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**Linux操作系统
* |----------------------------------|
* | 操作系统空间 | 1G
* |----------------------------------| 0xC0000000
* | |
* | |
* | 用户进程空间 | 3G
* | |
* | |
* |__________________________________| 0x00000000
* WinXP默认操作系统占据2G内存,可以调整使其只占1G内存。
*/

PAE

(Physical Address Extension),是针对32位CPU内存不足的一种修补。类似的,针对16位CPU,借助段偏移寻址能够达到1MB,每次读块只能读64KB,使用XMS(一种中断处理技术)读取大于1MB内存的地方。

OS采用36位地址线\内存地址时,程序使用的最大虚拟地址空间仍旧不会超过4GB,但是它程序的虚拟地址空间可以映射到的物理内存的范围扩大到64GB。

通过这种页或块映射在Win下访存的操作方式叫地址窗口映射扩展AWE(Address Windowing Extension);在Linu下通过mmap()系统调用来实现。

装载的方式

  • overlay覆盖装入,适合内存受限场景比如木马或者嵌入式设备;

保证调用路径上的块都在内存;禁止跨树间调用。

编写程序时将程序分块,写一小段辅助代码,管理模块在内存的驻留和更替。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* |----------------------------------------------------|
* |----------------------------------------------------|
* | Overlay Manager (几十Bytes) |
* |----------------------------------------------------|
* | Main (1024 bytes) |
* | |
* |----------------------------------------------------|
* | A 模块 (512 bytes) | B 模块 (256 bytes) |
* | |----------------------- |
* |----------------------------------------------------|
*/

如上图所示,A和B模块互不依赖,都被Main模块调用时,则可以采用Overlay的方式,使得需要的内存空间节省256Bytes。

  • Paging页映射 和页替换算法 MMU的地址映射

从操作系统可执行文件的装载

进程的建立

  • 1.创建虚拟地址空间(只创建映射函数需要的数据结构,并不实际创建空间)
    • Linux i386中是分配一个页目录,不设置页映射关系,在后面程序发生页错误的时候才设置)
  • 2.读取可执行文件头,建立虚拟空间与可执行文件的映射关系。(装载)

(Linux中)

  • 3.令EIP=EOP of Executable File

    ​ 内核堆栈和用户堆栈的切换、CPU运行权限切换

1
2
3
/**
页面可执行文件的偏移<----(装载)---->虚拟存储空间<----------->物理内存
*/

Linux将进程虚拟空间中的一个段叫VMA(Virtual Mempry Area),包括

1
start address;size;Attributes(RWE);State;Type;

对于相同权限状态的节Section,把它们合并到一起作为一个段Segment映射。这样在进程虚拟空间中只有一个VMA而不是多个,可以减少页面内部碎片,节省内存空间。

页错误

程序执行时进程虚拟空间中发生Page Fault,控制权从进程转移到OS;

根据数据结构找到VMA,计算页面在磁盘文件中的偏移,

然后物理内存分配物理页,读取该磁盘块到物理页中,

将发生页错误的虚拟页与物理页之间建立映射关系。

控制权归还给进程,从发生页错误的位置继续执行。

ELF文件

从链接角度(Linking View)看,elf文件按照节section存储,描述它的结构叫做节表Section Headers;

从装载角度或者执行视图(Execution View)看,elf文件可以按照段segment划分,描述它的结构叫做程序头Program Header;

elf可执行文件和共享库文件处于装载的需要,比目标文件多一个程序头表(Program Header Table)。

1
2
3
4
5
6
7
8
9
10
11
12

// 程序头表
typedef struct {
Elf32_Word p_type; // 段的类型,如LOAD:1.DYNAMIC,INTERP
Elf32_Addr p_offset; // 段在文件中的偏移量
Elf32_Addr p_vaddr; // 段在内存中的虚拟地址
Elf32_Addr p_paddr; // 段在物理内存中的地址(对于嵌入式系统可能有用)
Elf32_Word p_filesz; // 段在文件中的大小,可能是0
Elf32_Word p_memsz; // 段在进程虚拟地址空间中占用的大小,可能是0
Elf32_Word p_flags; // 段的属性标志,如可读、可写、可执行等
Elf32_Word p_align; // 字节按照2^p_align次方对齐
} Elf32_Phdr;

对于LOAD类型的Segment,p_memsz一定>=p_filesz(bss被合并在数据类型的段里,存放那些未被初始化的数据)。

PE文件的装载

步骤

PE文件可以装载到任何内存位置。PE文件中选用RVA,因为RVA可以始终保持一致。

1.读取文件第一个页,获取到DOS头、PE头、段表。

2.选择装载地址。检查进程空间地址里,目标地址是否可用。若不可用,则另外选择装载地址。

3.段映射。使用段表将PE文件中的段,映射到进程内存空间地址。—>若装载地址≠目标地址,Rebasing。

4.装载所需要的dll文件—>解析PE文件中的导入符号——>

5.建立初始化栈和堆—>建立主线程并启动进程

数据结构

1
2
3
4
5
6
7
8
9
10
coffHeader{
optionalHeader{
...
u32 addressofEntryPoint;//装载后PE文件第一个指令的RVA,病毒感染PE文件后要修改入口点,篡改执行流程。
u32 SectionAlignment;//内存中段对齐粒度,默认是4K=4096字节
u32 FileAlignMent;//文件中段对齐粒度,默认是512字节
u32 baseofCode;
u32 baseofData;//数据段起始RVA
}
}

6.4 进程虚存空间分布