《加密与解密》_1_基础知识
ʕ •ᴥ•ʔ ɔ:
1 文本字符
1.1 ASCII和Unicode字符集
- ASCII:单字节编码(最高位没有使用,实际上是7位编码标准),编码取值范围为00h~7Fh,包含26个小写字母、26个大写字母、10个数字、33个控制代码及空格,共128个字符。 (不同的计算机厂商对ASCII进行了扩充使其变为8位,取值范围为
00h~FFh
) - Unicode:双字节编码,编码取值范围为0000h~FFFFh,使用
0~65535
无符号(正整数)的整数对每个字符进行编码。
1.2 字节存储顺序
大端序:Big-endian,字符高字节存入存储器低地址,低字节存入高地址。
小端序:Little-endian,字符高字节存入存储器高地址,低字节存入低地址。
Intel处理器使用小端序在内存中将一个字存入相邻的2字节存储器空间中去。
如将pediy
存入到内存中,Unicode为:0070 0065 0064 0069 0079
:
- 一般来说,x86系列CPU都是Little-endian 字节序;
- PowerPC 通常是Big-endian字节序。
- 因为网络协议也都是采用Big-endian方式传输数据的,所以有时也把 Big-endian方式称为网络字节序。
2 Windows操作系统
2.1 Win32 API函数
- 用于16位Windows的API(Windows 1.0~Windows3.1)称作“Win16”;
- 用于32位Windows的 API(Windows 9x/NT/2000/XP/7/10)称作“Win32” ;
- 64位Windows API的名称和功能基本没有变化,还是使用Win32的函数名,只不过是用64位代码实现的。
API 函数调用在从Win16到Win32的转变中保持兼容,并在数量和功能上不断增强。W indows 1.0只支持不到450个函数调用,现在已有几千个函数了。
所有32位Windows都支持Win16 API(以确保与旧的应用程序兼容)和Win32 API(以运行新的应用程序)。
非常有趣的是,Windows NT/2000/XP/7与Windows 9x 的工作方式不同。在Windows NT/2000/XP/7中,Win16 函数调用通过一个转换层转换为Win32函数调用,然后被操作系统处理。 在Windows 9x中,该操作正好相反,Win32 函数调用通过转换层转换为Win16函数调用,再由操作系统进行处理。
Windows运转的核心是动态链接。Windows 提供了丰富的应用程序可利用的函数调用,这些函数采用动态链接库(DLL)实现。在Windows 9x中,DLL 通常位于\WINDOWS\SYSTEM
子目录中。 在Windows NT/2000/XP/7中,DLL通常位于系统安装目录的\SYSTEM
和\SYSTEM32
子目录中。
在早期, Windows的主要部分只需要在3个动态链接库中实现,它们分别代表Windows的3个主要子系统,叫作Kernel、User和GDI。
- Kernel(由KERNEL32.DLL实现):操作系统核心功能服务,包括进程与线程控制、内存管理、文件访问等。
- User(由USER32.DLL实现):负责处理用户接口,包括键盘和鼠标输入、窗口和菜单管理等。
- GDI (由GDI32.DLL实现):图形设备接口,允许程序在屏幕和打印机上显示文本和图形。
在Win32 API 函数字符集中,A表示ANSI(ASCII的扩展,8bit编码), W表示Widechars(即Unicode)。前者就是通常使用的单字节方式,后者是宽字节方式,以便处理双字节字符。每个以字符串为参数的Win32函数在操作系统中都有这两种方式的版本。
例如,在编程时使用MessageBox函数,而在USER32.DLL 中却没有32位MessageBox函数的入口。实际上有两个入口,一个名为MessageBoxA
(ANSI版), 另一个名为MessageBoxW
(宽字符版)。幸运的是,程序员通常不必关心这个问题,只需要在编程时使用MessageBox 函数,开发工具的编译模块就会根据设置来决定是采用MessageBoxA 还是 MessageBoxW了。但是在使用OD调试的时候得区分开。
NT系统是使用 Unicode 标准字符集重新开发的,其系统核心完全是用Unieode函数工作的。如果希望调用一个 Windows函数并向它传递一个 ANSI字符串,系统会先将ANSI字符串转换成Unicode字符串,再将Unicode字符串传递给操作系统。相反,如果希望函数返回ANSI字符串,系统会先将Unicode字符串转换成ANSI 字符串,然后将结果返回应用程序。也就是说,在NT架构下,Win32 API能接受Unicode和ASCII两种字符集,而其内核只能使用Unicode字符集。尽管这些操作对用户来说都是透明的,但字符串的转换需要占用系统资源。
2.2 WOW64
WOW64(Windows-on-Windows 64-bit)是64位Windows操作系统的子系统,可以使大多数32位应用程序在不进行修改的情况下运行在64位操作系统上。
64位的Windows,除了带有64位操作系统应有的系统文件,还带有32位操作系统应有的系统文件。
Windows的64位系统文件都放在一个叫作
System32
的文件夹中,\Windows\System32
文件夹中包含原生的64位映像文件。为了兼容32位操作系统,还增加了\Windows\SysWOW64
文件夹,其中存储了32位的系统文件。64位应用程序会加载
System32
目录下64位的kernel32.dll
、user32.dll
和ntdll.dll
。当32位应用程序加载时,WOW64建立32位ntdll.dll所要求的启动环境,将CPU模式切换至32位,并开始执行32位加载器,就如同该进程运行在原生的32位系统上一样。
WOW64会对32位
ntdll.dll
的调用重定向ntdll.dll
(64位),而不是发出原生的32位系统调用指令。WOW64转换到原生的64位模式,捕获与系统调用有关的参数,发出对应的原生64位系统调用。当原生的系统调用返回时,WOW64在返回32位模式之前将所有输出参数从64位转换成32位。WOW64既不支持16位应用程序的执行(32位Windows支持16 位应用程序的执行),也不支持加载32位内核模式的设备驱动程序。WOW64进程只能加载32位的DLL,不能加载原生的64位DLL。类似的,原生的64位进程不能加载32位的DLL。
2.3 虚拟内存
CPU的运行模式:实模式、保护模式、虚拟8086模式。实模式和保护模式
X86机器主要使用实模式和保护模式:
实模式:16位的操作系统,应用程序直接使用物理内存,无虚拟内存的概念。内存并没有分段,段的划分来自于CPU,由于8086CPU用$段地址*16+偏移地址=物理地址$的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存物理地址,寻址使用段寄存器:
CS:代码段寄存器;
DS:数据段寄存器;
SS:堆栈段寄存器;
ES:扩展段寄存器;
IP:指令指针寄存器。
当CPU要访问内存时,有这四个段寄存器提供内存单元的段地址CS和IP是CPU中最关键的寄存器,他们指示了CPU当前要读取指令的地址。在任何时候 CPU将CS、IP中的内容当做指令的段地址和偏移地址,用他们合成指令的物理地址到内存中读取指令码。$物理地址=CS*16+IP$
保护模式:32位系统开始采用的模式,使用虚拟内存技术,给每个运行的程序分配逻辑上的4GB虚拟内存(32位和64位进程),使应用程序不直接操作物理内存、各应用程序独立运行互不影响、提高物理内存的使用效率,CPU使用段页式内存管理。
虚拟内存:
- 当一个 PE 文件被执行时,操作系统会先为该程序创建一个 4GB 的进程虚拟地址空间,当创建完虚拟地址空间所需要的数据结构后,进程开始读取 PE 文件的第一页。在PE 文件的第一页包含了 PE 文件头和段表等信息,进程根据文件头和段表等信息,将 PE 文件中所有的段一一映射到虚拟地址空间中相应的页(PE 文件中的段的长度都是页长的整数倍)。这时 PE 文件的真正指令和数据还没有被装入内存中,操作系统只是据 PE 文件的头部等信息建立了 PE 文件和进程虚拟地址空间中页的映射关系而已。
- 当 CPU 要访问程序中用到的某个虚拟地址时,当 CPU 发现该地址并没有相关联的物理地址时, CPU 认为该虚拟地址所在的页面是个空页面, CPU 会认为这是个页错误(Page Fault), CPU 也就知道了操作系统还未给该 PE 页面分配内存,CPU 会将控制权交还给操作系统。
- 操作系统于是为该 PE 页面在物理空间中分配一个页面,然后再将这个物理页面与虚拟空间中的虚拟页面映射起来,然后将控制权再还给进程,进程从刚才发生页错误的位置重新开始执行。由于此时已为 PE 文件的那个页面分配了内存,所以就不会发生页错误了。随着程序的执行,页错误会不断地产生,操作系统也会为进程分配相应的物理页面来满足进程执行的需求。
1)程序执行时,PE文件与虚拟地址映射过程如下图(Windows虚拟内存机制):
2)虚拟地址与物理地址的映射如下图(物理地址、虚拟地址、虚拟内存、分段、分页以及Windows/Linux内存管理):
3)当CPU访问数据时将已经从虚拟地址与物理地址映射好的数据装进物理内存过程如下图(Windows API(四)Win32 内存结构):
段的定义:把程序与其相关的数据划分到几个段中,比如数据段、代码段和堆栈段。段的长度有限制,但是不要求所有程序的所有段长度都一样。
现代操作系统使用段页式内存管理,在段页式系统中,程序的地址空间首先被分成若干个逻辑段(.data、.text、.bss、栈、堆等),每段都有自己的段号,然后再将每一段分成若干个大小固定的页。对内存空间的管理仍然和分页存储管理一样,将其分成若干个和页面大小相同的存储块,对内存的分配以存储块为单位。
段,是从编程的角度来看的,比如PE文件的各个分段就是一种划分,每个段的属性可以不同。段的使用方法先于分页,然后到现在使用的段页式内存管理方式。
程序(PE文件)载入虚拟内存使用分段方式,MMU将虚拟内存转换为物理内存使用分页方式。PE文件中也是进行分段处理。
- 分段机制:实现虚拟地址(由段和偏移构成的逻辑地址)到线性地址(
0xXXXX XXXX
)的转换。 - 分页机制:分页管理机制实现线性地址到物理地址的转换。
先分段 , 再分页
将进程按逻辑模块分段,再将各段分页(如每个页面 4KB) 再将内存空间分为大小相同的内存块/页框/页帧/物理块进程前将各页面分别装入各内存块中。
实现线性地址到物理地址的转换这个对应是由MMU硬件实现的,不用程序员操心!而且分页和分段对于虚拟地址的映射方式基本一致,分段是通过段表GDT,LDT中的段描述符来确定段的属性和物理地址范围;而分页是通过页表项(页表描述符)来记录页框的属性和物理地址位置!
在段页式存储管理系统中,作业的地址空间首先被分成若干个逻辑分段,每段都有自己的段号,然后再将每段分成若干个大小相等的页。对于主存空间也分成大小相等的页,主存的分配以页为单位。
分段和分页的区别:
- 在分段的方法中,每次程序运行时总是把程序全部装入内存,而分页的方法则有所不同。分页的思想是程序运行时用到哪页就为哪页分配内存,没用到的页暂时保留在硬盘上。当用到这些页时再在物理地址空间中为这些页分配内存,然后建立虚拟地址空间中的页和刚分配的物理内存页间的映射。
- 分页仅仅是由于系统管理的需要而不是用户的需要。段则是信息的逻辑单位,它含有一组其意义相对完整的信息。分段的目的是为了能更好地满足用户的需要。
- 页的大小固定且由系统决定,由系统把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的,因而在系统中只能有一种大小的页面;而段的长度却不固定,决定于用户所编写的程序,通常由编译程序在对源程序进行编译时,根据信息的性质来划分。
- 分页的作业地址空间是一维的,即单一的线性地址空间,程序员只需利用一个记忆符,即可表示一个地址;而分段的作业地址空间则是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址。
- 在段式存储管理中,将程序的地址空间划分为若干段(segment),如代码段,数据段,堆栈段;这样每个进程有一个二维地址空间,相互独立,互不干扰。段式管理的优点是:没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)。
磁盘一个物理扇区的大小为512字节, 即200h(字节),PE文件的区块在磁盘中以这个粒度为大小进行对齐。
- 在x86系列CPU中,内存页是按4KB (
1000h
)排列的,所以,在x86系统中(虚拟内存),PE文件区块的内存对齐值般为1000h
,每个区块从1000h
的倍数的内存偏移位置开始。(4(KB)*1024=4096 Byte=1000h Byte) - 在x64中, 内存页是按8KB(
2000h
)排列的。
32位处理器将分配给每个应用程秀的虚拟地址空间做如下划分,一般给用户模式划分2GB虚拟内存,也可扩大3GB。无论是32位还是64位windows若要让32位程序能使用3GB内存,必须在链接时加上参数: /LARGEADDRESSAWARE
、32位Exe程序突破系统内存限制
在早期版本的Windows中,Microsoft不允许用户程序访问2GB以上的地址空间,为了让此类应用程序即使在用模式分区大于2GB的环境下仍能正常运行,Microsoft提供代了一种模式来增大用户模式分区,最多不超过3GB。 当前系统即将运行一个应用程序时,它会检查应用程序在链接时是否使用了/LARGEADDRESSAWARE
链接器开关。如果是,则相应于应用程序在声明它会充分利用大用户模式地址空间,而不会对内在地址进行任何不当的操作。反之,如果应用程序在链接时没有使用/LARGEADDRESSAWARE
开关,那么操作系统会保留用户模式分区中2GB以上到内核模式开始处的整个部分。
本文以默认情况2GB用户模式为例。
- 64KB NULL指针区 (
0x00000000~0x0000FFFF
): 如果进程中的一个线程试图操作这个分区中的数据,CPU就会引发非法访问。他的作用是,调用malloc
等内存分配函数时,如果无法找到足够的内存空间,它将返回NULL
。而不进行安全性检查。它只是假设地址分配成功,并开始访问内存地址0x00000000
(NULL)。由于禁止访问内存的这个分区,因此会发生非法访问现象,并终止这个进程的运行。没有任何办法可以让我们分配到位于这一地址空间的虚拟内存,即使是使用Win32 API也不例外。 - 用户模式分区 (
0x00010000~0xBFFEFFFF
):这个分区中存放进程的私有地址空间,DLL被装入这一分区,内存映射文件映射到该分区。一个进程无法以任何方式访问另外一个进程驻留在这个分区中的数据 (相同 exe,通过 copy-on-write 来完成地址隔离)。(在Windows中,所有 .exe 和动态链接库都载入到这一区域。系统同时会把该进程可以访问的所有内存映射文件(系统DLL)映射到这一分区,对所有应用程序来说,进程的大部分数据都保存在这一分区。)。 - 64KB 禁入区 (
0xBFFF0000~0xBFFFFFFF
):这个分区禁止进入,禁止访问分区只有在Win2000中有。这个分区是用户分区和内核分区之间的一个隔离带,目的是为了防止用户程序违规访问内核分区。任何试图访问这个内存分区的操作都是违规的。微软保留这块分区的目的是为了简化操作系统的现实。 - 内核区 (0xC0000000~0xFFFFFFFF):装入Windows NT执行体、内核和设备驱动。这个分区存放操作系统驻留的代码、内核对象也驻留在此。线程调度、内存管理、文件系统支持、网络支持和所有设备驱动程序代码都在这个分区加载。内核分区对用户的程序来说是禁止访问的,操作系统的代码在此。内核对象也驻留在此。这个分区被所有进程共享。
给每个应用程序分配4GB虚拟内存(如上图),但是只有用户模式的2/3GB可以被应用程序直接使用,各个应用程序的用户区都是隔离的,逻辑上的,虚拟的。但是内核区的1/2GB,内核区保存的是系统线程调度、内存管理、设备驱动等数据,这部分数据供所有的进程共享,但应用程序是不能直接访问的。
Windows系统在进程空间中专门划出一块0x7000 0000 - 0x8000 0000
(共256MB)区域,用于映射这些常用的系统DLL(如kernel32.dll、ntdll.dll等)
2GB用户模式分区:
参考:
32位:【Windows 内存机制说明】、【windows虚拟内存机制】、【浅谈进程地址空间与虚拟存储空间】、【内存、虚拟内存的布局】
Win98和Win2000虚拟内存分区的区别:【进程的虚拟地址空间】
Linux:【进程虚拟地址空间的分布详解】、Linux内存寻址和内存管理、【《CS:APP》】、【Linux可执行文件与进程的虚拟地址空间】
程序使用的虚拟地址:
①当一个应用程序启动时,操作系统就创建一个进程,并给该进程分配 2GB 的虚拟地址(不是内存,只是地址);
②虚拟内存管理器将应用程序的代码映射到那个应用程序的虚拟地址中的某个位置,并把当前需要的代码读入物理地址(注意:虚拟地址与应用程序代码在物理内存中的位置是没有关系的)。
③如果使用 DLL,DLL 也会被映射到进程的虚拟地址空间中,在需要的时候才会被读入物理内存。
④其他项目(数据、堆等)的空间是从物理内存中分配的,并被映射到虚拟地址空间中。
⑤应用程序通过使用其虚拟地址空间中的地址开始执行。然后,虚拟内存管理器把每次内存访问映射到物理位置。
总结为以下几点:
- 应用程序不会直接访问物理地址。
- 虚拟内存管理器通过虚拟地址的访问请求来控制所有的物理地址访问。
- 每个应用程序都有独立的 4B 寻址空间,不同应用程序的地址空间是彼此隔离的。
- DLL 程序没有“私有”空间,它们总是被映射到其他应用程序的地址空间中,作为其他应
用程序的一部分运行。其原因是:如果 DLL 不与其他程序处于同一个地址空间,应用程序
64 位 Windows 操作系统提供了 16TB 的有效寻址空间,其中的一半可用于用户模式的应用程序。