2017-2018-1 20155222 《信息安全系统设计基础》第11周学习总结

2017-2018-1 20155222 《信息安全系统设计基础》第11周学习总结

学习目标

  • 理解虚拟存储器的概念和作用
  • 理解地址翻译的概念
  • 理解存储器映射
  • 掌握动态存储器分配的方法
  • 理解垃圾收集的概念
  • 了解C语言中与存储器有关的错误

虚拟存储器的概念和作用

虚拟内存别称虚拟存储器(Virtual Memory)。电脑中所运行的程序均需经由内存执行,若执行的程序占用内存很大或很多,则会导致内存消耗殆尽。为解决该问题,Windows中运用了虚拟内存技术,即匀出一部分硬盘空间来充当内存使用。当内存耗尽时,电脑就会自动调用硬盘来充当内存,以缓解内存的紧张。若计算机运行程序或操作所需的随机存储器(RAM)不足时,则 Windows 会用虚拟存储器进行补偿。它将计算机的RAM和硬盘上的临时空间组合。当RAM运行速率缓慢时,它便将数据从RAM移动到称为"分页文件"的空间中。将数据移入分页文件可释放RAM,以便完成工作。 一般而言,计算机的RAM容量越大,程序运行得越快。若计算机的速率由于RAM可用空间匮乏而减缓,则可尝试通过增加虚拟内存来进行补偿。但是,计算机从RAM读取数据的速率要比从硬盘读取数据的速率快,因而扩增RAM容量(可加内存条)是最佳选择。
虚拟内存设置界面
虚拟内存设置界面
虚拟内存是Windows 为作为内存使用的一部分硬盘空间。虚拟内存在硬盘上其实就是为一个硕大无比的文件,文件名是PageFile.Sys,通常状态下是看不到的。必须关闭资源管理器对系统文件的保护功能才能看到这个文件。虚拟内存有时候也被称为是"页面文件"就是从这个文件的文件名中来的。
内存在计算机中的作用很大,电脑中所有运行的程序都需要经过内存来执行,如果执行的程序很大或很多,就会导致内存消耗殆尽。为了解决这个问题,WINDOWS运用了虚拟内存技术,即拿出一部分硬盘空间来充当内存使用,这部分空间即称为虚拟内存,虚拟内存在硬盘上的存在形式就是 PAGEFILE.SYS这个页面文件。

地址翻译

实地址与虚地址
用户编制程序时使用的地址称为虚地址或逻辑地址,其对应的存储空间称为虚存空间或逻辑地址空间;而计算机物理内存的访问地址则称为实地址或物理地址,其对应的存储空间称为物理存储空间或主存空间。程序进行虚地址到实地址转换的过程称为程序的再定位。
虚存的访问过程
虚存空间的用户程序按照虚地址编程并存放在辅存中。程序运行时,由地址变换机构依据当时分配给该程序的实地址空间把程序的一部分调入实存。每次访存时,首先判断该虚地址所对应的部分是否在实存中:如果是,则进行地址转换并用实地址访问主存;否则,按照某种算法将辅存中的部分程序调度进内存,再按同样的方法访问主存。由此可见,每个程序的虚地址空间可以远大于实地址空间,也可以远小于实地址空间。前一种情况以提高存储容量为目的,后一种情况则以地址变换为目的。后者通常出现在多用户或多任务系统中:实存空间较大,而单个任务并不需要很大的地址空间,较小的虚存空间则可以缩短指令中地址字段的长度。

存储器映射

通过赋予每个任务不同的虚拟–物理地址转换映射,支持不同任务之间的保护。地址转换函数在每一个任务中定义,在一个任务中的虚拟地址空间映射到物理内存的一个部分,而另一个任务的虚拟地址空间映射到物理存储器中的另外区域。...
就是把一个地址连接到另一个地址。
例如,内存单元A的地址为X,把它映射到地址Y,这样访问Y时,就可以访问到A了。当然,访问原来的地址X,也可以访问到A。
再如,在C语言等高级语言里面没有访问IO的指令,所以那样的话在C里面就无法访问IO,只能通过嵌入汇编或者通过调用系统函数来访问IO了。采用IO映射后就不同了,因为IO空间和内存空间本来不同,有不同的访问指令,那么,将IO空间映射到内存空间,就可以通过使用访问内存的方法来访问IO了,例如在C语言里面可以通过指针来访问内存单元,从而访问到被映射的IO。
存储器映射是指把芯片中或芯片外的FLASH,RAM,外设,BOOTBLOCK等进行统一编址。即用地址来表示对象。这个地址绝大多数是由厂家规定好的,用户只能用而不能改。用户只能在挂外部RAM或FLASH的情况下可进行自定义。 ARM7TDMI的存储器映射可以有0X00000000~0XFFFFFFFF的空间,即4G的映射空间,但所有器件加起来肯定是填不满的。一般来说, 0X00000000依次开始存放FLASH--0X00000000,SRAM--0X40000000,BOOTBLOCK,外部存储器 0X80000000,VPB(低速外设地址,如GPIO,UART)--0XE0000000,AHB(高速外设:向量中断控制器,外部存储器控制器)--从0XFFFFFFFF回头。他们都是从固定位置开始编址的,而占用空间又不大,如AHB只占2MB,所以从中间有很大部分是空白区域,用户若使用这些空白区域,或者定义野指针,就可能出现取指令中止或者取数据中止。由于系统在上电复位时要从0X00000000 开始运行,而第一要运行的就是厂家固化在片子里的BOOTBLOCK,这是判断运行哪个存储器上的程序,检查用户代码是否有效,判断芯片是否加密,芯片是否IAP(在应用编程),芯片是否ISP(在系统编程),所以这个BOOTBLOCK要首先执行。而芯片中的BOOTBLOCK不能放在FLASH的头部,因为那要存放用户的异常向量表的,以便在运行、中断时跳到这来找入口,所以BOOTBLOCK只能放在FLSAH尾部才能好找到。而ARM7的各芯片的FLASH大小又不一致,厂家为了BOOTBLOCK在芯片中的位置固定,就在编址的2G靠前编址的位置虚拟划分一个区域作为BOOTBLOCK 区域,这就是重映射,这样访问2G即<0X80000000的位置时,就可以访问到在FLASH尾部的BOOTBLOCK区了。 BOOTBLOCK运行完就是要运行用户自己写的启动代码了,而启动代码中最重要的就是异常向量表,这个表是放在FLASH的头部首先执行的,而异常向量表中要处理多方面的事情,包括复位、未定义指令、软中断、预取指中止、数据中止、IRQ(中断) ,FIQ (快速中断),而这个异常向量表是总表,还包括许多分散的异常向量表,比如在外部存储器,BOOTBLOCK,SRAM中固化的,不可能都由用户直接定义,所以还是需要重映射把那些异常向量表的地址映到总表中。为存储器分配地址的过程称为存储器映射,那么什么叫存储器重映射呢?为了增加系统的灵活性,系统中有部分地址可以同时出现在不同的地址上,这就叫做存储器重映射。重映射主要包括引导块"Boot Block"重映射和异常向量表的重映射。 1.引导块"Boot Block"及其重映射 Boot Block是芯片设计厂商在LPC2000系列ARM内部固化的一段代码,用户无法对其进行修改或者删除。这段代码在复位时被首先运行,主要用来判断运行哪个存储器上面的程序,检查用户代码是否有效,判断芯片是否被加密,系统的在应用编程(IAP)以及在系统编程功能(ISP)等。 Boot Block存在于内部Flash,LPC2200系列大小为8kb,它占用了用户的Flash空间,但也有其他的LPC系列不占用FLash空间的,而部分没有内部Flash空间的ARM处理器仍然存在Boot Block。 重映射的原因: Boot Block中有些程序可被用户调用,如擦写片内Flash的IAP代码。为了增加用户代码的可移植性,所以最好把Boot Block的代码固定的某个地址上。但由于各芯片的片内Flash大小不尽相同,如果把Boot Block的地址安排在内部Flash结束的位置上,那就无法固定Boot Block的地址。 为了解决上面的问题,于是芯片厂家将Boot Block的地址重映射到片内存储器空间的最高端,即接近2Gb的地方,这样无论片内存储器的大小如何,都不会影响Boot Block的地址。因此当Boot Block中包含可被用户调用的IAP操作的代码时,不用修改IAP的操作地址就可以在不同的LPC系列的ARM上运行了。 2.异常向量表及其重映射 ARM内核在发生异常后,会使程序跳转到位于0x00000x001C的异常向量表处,再经过向量跳转到异常服务程序。但ARM单条指令的寻址范围有限,无法用一条指令实现4G范围的跳转,所以应在其后面的0x00200x003F地址上放置跳转目标,这样就可以实现4G范围内的任意跳转,因此一个异常向量表实际上占用了16个字的存储单元。

动态存储器的分配方法

除了使用低级的mmap和munmap函数来创建和删除虚拟存储器的区域,还可以使用动态存储器分配器。动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆。堆是一个请求二进制零的区域(参见浅谈Linux存储器映射),它紧接在未初始化的bss区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(即break),它指向堆顶。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟存储器片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。分配器有两种,即显示分配器和隐式分配器。

垃圾收集

应用通过调用malloc和free来分配和释放堆块。应用要负责释放所有不再需要的已分配块。未能释放已分配块是很常见的。例如下面的程序:

voidgarbage(){
int *p = (int *)malloc(15213);
return
}

因为程序不再需要p,所以在garbage返回前应该释放p。但是由于忘记是的它在程序的生命周期内都保持为已分配状态。
垃圾收集器是一种动态存储器分配器,它自动释放程序不再需要的已分配块。这些块称为垃圾。自动回收堆存储的过程称为垃圾收集。垃圾收集有很多算法,在这里仅仅讨论最简单的标记清楚算法。关于Java垃圾收集的具体知识我会在未来继续展开。

C语言中与存储器有关的错误

1、间接引用坏指针
在进程的虚拟地址空间中有较大的漏洞,没有映射到任何有意义的数据。如果我们试图间接引用一个指向这些洞的指针,那么操作系统就会以段异常终止我们的程序。而且,虚拟存储器的某些区域是只读的。试图写这些区域将造成以保护异常终止这个程序。
间接引用坏指针的一个常见示例是经典的scanf错误。假设我们想要使用scanf从stdin读一个整数到变量。做这件事情正确的方法是传递给scanf一个格式串和变量的地址:
scanf(“%d”, &val)
然而,对于c语言程序员初学者而言,很容易传递val的内容,而不是它的地址:
scanf(“%d”, val)
在这种情况下,scanf将把val的内容解释为一个地址,并试图将一个字写到这个位置。在最好的情况下,程序立即以异常终止。在最糟糕的情况下,val的内容对应于虚拟存储器的某个合法的读/写区域,于是我们就覆盖了存储器,这通常会在相当以后造成灾难性的、令人困惑的后果。
2、读未初始化的存储器
虽然.bss存储器位置(诸如未初始化的全局C变量)总是被加载器初始化为零,但是对于堆存储器却并不是这样的。一个常见的错误就是假设堆存储器被初始化为零:
int *matvec(int **a, int *x, int n)
{
int i, j;

            int *y = (int *)malloc(n * sizeof(int));

            for(i = 0; i < n; i++)
                for(j = 0; j < n; j++)
                    y[i] += a[i][j] * x[j];
            return y;
    }
    在这个示例中,程序员不正确地假设向量y被初始化为零。正确的实现方式是在for循环时将y[i]设置为零,或使用calloc。

3、允许栈缓冲区溢出
如果一个程序不检查输入串的大小就写入栈中的目标换成区,那么这个程序就会有缓冲区溢出错误。例如,下面的函数就有缓冲区错误,因为gets函数拷贝一个任意长度的串到缓冲区。为了纠正这个错误,我们必须使用fgets函数,这个函数限制了输入串的大小:
void bufoverflw()
{
char buf[64];

            gets(buf);
            return;
    }

4、假设指针和它们指向的对象是相同大小的
一种常见的错误是假设指向对象的指针和它们所指向的对象是相同大小的:
int **makearray1(int n, int m)
{
int i;
int **A = (int **)malloc(n * sizeof(int));

            for(i = 0; i < n; i++)
                A[i] = (int *)malloc(m * sizeof(int));
            return A;
       }
    这里的目的是创建一个由n个指针组成的数组,每个指针都指向一个包含m个int的数组。然而,因为程序员将int **A = (int **)malloc(n * sizeof(int));中将sizeof(int)写成了sizeof(int),代码实际创建的是一个int的数组。这段代码只有在int和指向int的指针大小相同的机器上运行良好。

但是,如果我们在像Alpha这样的机器上运行这段代码,其中指针大于int,那么在for(i = 0; i < n; i++) A[i] = (int *)malloc(m * sizeof(int));将写到超过A数组末端的地方。因为这些字中的一个很可能是分配块的边界标记脚部,所以我们可能不会发现这个错误,而没有任何明显的原因。
5、造成错位错位
错位错误是另一种很常见的覆盖错误发生的原因:
int **makearray2(int n, int m)
{
int i;
int **A = (int **)malloc(n * sizeof(int *));

            for(i = 0; i <= n; i++)
                A[i] = (int *)malloc(m * sizeof(int));
            return A;
         }
    这是前面程序的另一个版本。这里我们创建了一个n个元素的指针数组,但是随后试图初始化这个数组的n+1个元素,在这个过程中覆盖了A数组后面的某个存储器。

6、引用指针,而不是它所指向的对象
如果我们不太注意C操作符的优先级和结合性,我们就会错误地操作指针,而不是期望操作指针所指向的对象。比如,考虑下面的函数,其目的是删除一个有size项的二叉堆里的第一项,然后对剩下的size-1项重新建堆。
int *binheapdelete(int **binheap, int *size)
{
int *packet = binheap[0];

            binheap[0] = binheap[*size – 1];
            *size--;
            heapify(binheap, *size, 0);
            return(packet);
         }
    *size—目的是减少size指针指向的整数的值。然而,因为一元—和*运算符优先级相同,从右向左结合,所以代码实际减少的是指针自己的值,而不是它所指向的整数的值。如果幸运的话,程序会立即失败,但是更有可能发生的是,当程序在它执行过程的很后面产生一个不正确的结果时,我们只能在那里抓脑袋了。这里的原则是如果你对优先级和结合性有疑问,就使用括号。使用表达式(*size)--。

7、误解指针运算
另一种常见的错误是忘记了指针的算术操作是以它们指向的对象的大小为单位来进行的,而这种大小单位并不一定是字节。例如,下面函数的目的是扫描一个int的数组,并返回一个指针,指向val的首次出现:
int *search(int p, int val)
{
while(
p && *p != val)
p += sizeof(int);
return p;
}
8、引用不存在的变量
没有太多经验的C程序员不理解栈的规则,有时会引用不再合法的本地变量,如下列所示:
int *stackref()
{
int val;

            return &val;
        }
    这个函数返回一个指针,指向栈里的一个局部变量,然后弹出它的栈帧。尽管p仍然指向一个合法的存储器地址,但是它已经不再指向一个合法的变量了。当以后在程序中调用其他函数时,存储器将重用它们的帧栈。后来,如果程序分配某个值给*p,那么它可能实际正在修改另一个函数的帧栈中的一个条目,从而带来潜在地灾难性的、令人困惑的后果。

9、引用空闲堆块中的数据
一个相似的错误是引用已被释放了的堆块中的数据。如下面的示例,示例中分配了一个整数数组x,之后释放了块x,最后又引用了它。
int *heapref(int n, int m)
{
int I;
int *x, *y;

            x = (int *)mallic(n * sizeof(int));
   
            /*   …    */

            free(x);

            y = (int *)malloc(m * sizeof(int));
            for(i = 0; i < m; i++)
            y[i] = x[i]++;

            return y;
    }

10、引起存储器泄漏
存储器泄漏是缓慢、隐形的杀手,当程序员不小心忘记释放已分配块,而在堆里创建了垃圾时,会发生这种问题。例如,下面的函数分配了一个堆块x,然后不释放它就返回。
void leak(int n)
{
int *x = (int *)malloc(n * sizeof(int));

            return;
    }
    如果leak经常被调用,堆里就会充满了垃圾,最糟糕的情况下,会占有整个虚拟地址空间。对于像守护进程和服务器这样的程序来说,存储器泄漏是特别严重的,根据定义这些程序是不会终止的。
posted @ 2017-12-03 10:38  20155222卢梓杰  阅读(219)  评论(0编辑  收藏  举报