目录
写在开头
内存与磁盘的关系
基本关系
磁盘缓存
虚拟内存
节约内存的编程方法
通过DLL文件实现函数共有
通过调用_stdcall来降低文件程序的大小
磁盘的物理结构
结尾
写在开头
本文继续阅读总结《程序是怎样跑起来的》这本书(作者:矢泽久雄)。前两篇博客介绍了这本书的阅读感受,并分别对第一章CPU、第四章内存相关的知识进行了总结。详情见:
【计算机组成原理】读书笔记第一期:对程序员来说CPU是什么-CSDN博客
【计算机组成原理】读书笔记第二期:使用有棱有角的内存_Bossfrank的博客-CSDN博客
本文将介绍本书的第五章:内存与磁盘的亲密关系。主要介绍了内存与磁盘的相互关系、节约内存的编程方法和磁盘的物理结构。
内存与磁盘的关系
本文内存主要指主内存,即存储CPU中运行的程序指令和数据的区域,磁盘主要指硬盘。从存储容量上看,内存是高速(读取速度上)高价(价格上),磁盘则是低速廉价。
基本关系
磁盘中存储的程序,必须要加载到内存中才能运行。在磁盘中保存的原始程序是不能直接运行的。原因:负责解析和运行程序内容的CPU,需要通过内部程序计数器来指定内存地址才能读出程序(详见第一章总结)。即使CPU可以直接读出并运行磁盘中保存的程序,由于磁盘读取速度慢,程序的运行速度也会降低。
程序要加载到内存中才可运行(图5-1),这是理解内存和磁盘关系的基础。接下来介绍两种机制,分别是将内存空间暂时充当磁盘的磁盘缓存和将磁盘空间暂时充当内存的虚拟内存。
磁盘缓存
磁盘缓存(disk cache)是指把从磁盘中读出的数据存储到内存空间中的方式。通过这样的方式,当之后需要读取同一数据时,就不用再去读取访问速度缓慢的磁盘空间,而是直接从内存(即磁盘缓存)中把内容读出,使用磁盘缓存可以大幅度提高磁盘数据的访问速度。Windows提供了磁盘缓存机制,不过随着硬盘访问速度的大幅度改善,磁盘缓存的效果没有那么明显了。
虚拟内存
虚拟内存(virtual memory)是指把磁盘的一部分作为假想内存来使用。通过这一机制,当程序的大小略超过内存的剩余空间时,也可以运行程序。不过由于CPU只能执行加载到内存中的程序。虚拟内存虽说是把磁盘作为内存的一部分来使用,但实际上正在运行的程序部分,在这个时间点上是必须存在在内存中的。也就是说,为了实现虚拟内存,就必须把实际内存(也可称为物理内存)的内容,和磁盘上的虚拟内存的内容进行部分置换(swap),并同时运行程序。
虚拟内存有分段式和分页式两种,介绍如下:
分段式虚拟内存:把要运行的程序分割成以处理集合及数据集合等为单位的段落,然后再以分割后的段落为单位在内存和磁盘之间进行数据置换。
分页式虚拟内存:在不考虑程序构造的情况下,把运行的程序按照一定大小的页(page)进行分割,并以页为单位在内存和磁盘间进行置换。
在windows中,虚拟内存采用分页式。在分页式中,把磁盘的内容读出到内存称为Page In,把内存的内容写入磁盘称为Page Out。一般情况下,Windows 计算机的页的大小是4KB。也就是说,把大程序用4KB的页来进行切分,并以页为单位放入磁盘(虚拟内存)或内存中(图5-3)。
顺道一提,在Windows中的控制面板可以查看或变更当前虚拟内存(设置搜索”调整Windows的外观和性能“选择”高级“)
做个对比:
磁盘缓存 | 虚拟内存 | |
---|---|---|
定义 | 将近期从磁盘中读出的数据存储到内存空间中。 | 把磁盘的一部分作为假想的内存来使用。 |
目的 | 提高磁盘读取速度。 | 在内存不足时也能运行程序。 |
本质 | 内存(假想的磁盘) | 磁盘(假想的内存) |
节约内存的编程方法
虚拟内存是无法从根本上解决内存不足的问题。这是因为使用虚拟内存时发生的Page In 和Page Out 往往伴随着低速的磁盘访问,因此在这个过程中应用的运行会变得迟钝起来,同时这个机制并不能无限扩展内存空间(实际内存的总空间大小不变),因此我们在编程时还是要想办法节约内存。
通过DLL文件实现函数共有
DLL(Dynamic Link Library)文件:动态链接库文件,指在程序运行时可以动态加载库(函数和数据的的集合)的文件。多个应用可以共有同一个DLL 文件。而通过共有同一个DLL 文件则可以达到节约内存的效果。要想了解动态链接库,先要了解静态链接:
静态链接:在实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个.c文件会形成一个.o文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。
静态链接的局限性:内存占用大、更新困难。
例如,假设我们编写了一个具有某些处理功能的函数MyFunc()。应用A和应用B都会使用这个函数。在各个应用的运行文件中内置函数MyFunc()(此处就是Static Link,静态链接)后同时运行这两个应用,内存中就存在了具有同一函数的两个程序。但这会导致内存的利用效率降低。所以,有两个同样的函数,还是有点浪费(图5-5)。
如果采用动态链接库dll文件的方式 ,可以将函数MyFunc()设置为独立的DLL文件,在运行时可以被多个应用共有,因此内存中存在的函数MyFunc()的程序只有一个,从而提高了内存的利用效率:
Windows 的操作系统本身也是多个DLL 文件的集合体。有时在安装新应用时,DLL 文件也会被追加。应用则会通过利用这些DLL 文件的功能来运行。像这样,之所以要利用多个DLL 文件,其中一个原因就是可以节约内存。而且DLL 文件还有一个优点就是更新方便,在不变更EXE文件的情况下,只通过升级DLL 文件就可以更新。
通过调用_stdcall来降低文件程序的大小
通过调用_stdcall来减小程序文件的方法,是用C 语言编写应用时可以利用的高级技巧。不过,这一思路应该也可以应用在其他编程语言中。这一方法的核心逻辑是针对栈清理处理的所在方(调用方/被调用方)。栈清理是指,把不需要的数据从接受和传递参数时使用的内存上的栈区域中清理掉。
在C语言中,调用函数后,需要执行栈清理处理的指令,该指令不是由程序记述的,而是哎程序编译时由编译器自动附加到程序中的。编译器默认将该处理附加在函数的调用方。
下面举例说明栈清理操作默认附加在函数调用方和被调方在内存中的占用情况:
例如,在代码清单5-1 中,从函数main() 中调用了函数MyFunc()。按照默认设定,栈的清理处理会附加在函数main() 这一方。在同一个程序中,同样的函数可能会被多次反复调用。而如果是同样的函数,栈清理处理的内容也是一样的。由于该处理是在调用函数一方,因此就会导致同一处理被反复进行,造成了内存的浪费。
// 代码5-1 C语言的函数调用程序示例
// 函数调用方
void main()
{
int a;
a = MyFunc(123, 456);
}
// 被调用方
int MyFunc(int a, int b)
{
...
}
将代码清单5-1 中调用函数MyFunc() 的部分用汇编语言来表示,就如代码清单5-2 所示。最后1行的处理就是清理处理:
// 代码5-2 调用MyFunc() 的部分程序(汇编语言)
push 1C8h ←将参数 456 (= 1c8h) 存入栈中
push 7Bh ← 将参数123 (= 7Bh) 存入栈中
call @LTD+15 (MyFunc)(00401014) ←调用MyFunc() 函数
add esp, 8 ←运行栈清理
C 语言通过栈来传递函数的参数。push是往栈中存入数据的指令。32 位CPU 中,1 次push 指令可以存储4 个字节的数据。代码5-2中,由于使用了两次push指令把两个参数(456 和123)存入到了栈中,因此总的来说就是存储了8字节的数据。通过call指令调用函数MyFunc() 后,栈中存储的数据就不再需要了。于是这时就通过add esp, 8 这个指令,使存储着栈数据的esp 寄存器B前进8 位(设定为指向高8位字节地址),来进行数据清理(CPU 中,栈中堆积的最高位的数据地址是保存在esp寄存器中的。连续运行两次pop指令,可以消除两个存储在栈中的4字节数据,而同样的功能也可以通过把esp 的数值加8来实现)。
栈清理处理,比起在函数调用方进行,在反复被调用的函数一方进行时,程序整体要小一些。这时所使用的就是_stdcall。在函数前加上_stdcall,就可以把栈清理处理变为在被调用函数一方进行:
int _stdcall MyFunc(int a, int b)
{
...
}
设置为_stdcall编译后,和代码5-2中add esp, 8 同样的处理就会在函数MyFunc() 一方执行。虽然该处理只能节约3个字节(add esp, 8 是机器语言的3个字节)的程序大小,不过在整个程序中还是有效果的(图5-7)。
磁盘的物理结构
磁盘是通过把其物理表面划分成多个空间来使用的。划分的方式有扇区方式和可变长方式两种。一般的Windows计算机所使用的硬盘和软盘,采用的都是扇区方式。
扇区方式:将磁盘划分为固定长度的空间。
磁道:扇区方式中,把磁盘表面分成若干个同心圆的空间就是磁道
扇区:把磁道按照固定大小(能存储的数据长度相同)划分而成的空间就是扇区。 扇区是对磁盘进行物理读写的最小单位。扇区的示意图见图5-8。
可变长方式:把磁盘划分为长度可变的空间。
扇区是对磁盘进行物理读写的最小单位。Windows 中使用的磁盘,一般1 个扇区是512 字节。不过,Windows 在逻辑方面(软件方面)对磁盘进行读写的单位是扇区的整数倍簇。根据磁盘容量的不同,1 簇可以是512 字节(1 簇 = 1 扇区)、1KB(1 簇 = 2 扇区)、2KB、4KB、8KB、16KB、32KB(1 簇 = 64 扇区)。磁盘的容量越大,簇的容量也越大。 不管是硬盘还是软盘,不同的文件是不能存储在同一个簇中的,因此不管是多么小的文件,都会占用1 簇的空间。
以簇为单位进行读写时,1簇中没有填满的区域会保持不被使用的状态。虽然这看起来是有点浪费,不过该机制就是如此规定的,所以我们也没有什么好办法。另外,如果减少簇的容量,磁盘访问次数就会增加,就会导致读写文件的时间变长。由于在磁盘表面上,扇区之间的分界必要的,因此,如果簇的容量过小,磁盘的整体容量也会减少。扇区和簇的大小,是由处理速度和存储容量的平衡来决定的。
结尾
本文总结了磁盘的相关内容,介绍了内存和磁盘的关系,并通过磁盘缓存和虚拟内存两种机制体现内存和磁盘的密不可分;由于内存的有限,编程时应该尽可能节约内存,本章总结了两种节约内存的编程方法,即使用dll动态链接库代替静态链接和通过_stdcall改变栈清理机制的处理方;最后在物理层面介绍了磁盘的结构,包括扇区和簇的概念。
这篇文章就总结到这里吧,下一篇可能重点总结程序从编写到运行的过程相关的内容。除此之外还会进一步更新红队打靶的解析和渗透测试相关的技术分享,恳请希望读者们多多支持。