1. 程式人生 > >2017-2018-1 20155222 《信息安全系統設計基礎》第11周學習總結

2017-2018-1 20155222 《信息安全系統設計基礎》第11周學習總結

地址 占用空間 寫入 ets arm sram 個數 數組 解釋

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內核在發生異常後,會使程序跳轉到位於0x0000~0x001C的異常向量表處,再經過向量跳轉到異常服務程序。但ARM單條指令的尋址範圍有限,無法用一條指令實現4G範圍的跳轉,所以應在其後面的0x0020~0x003F地址上放置跳轉目標,這樣就可以實現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經常被調用,堆裏就會充滿了垃圾,最糟糕的情況下,會占有整個虛擬地址空間。對於像守護進程和服務器這樣的程序來說,存儲器泄漏是特別嚴重的,根據定義這些程序是不會終止的。

2017-2018-1 20155222 《信息安全系統設計基礎》第11周學習總結