記憶體數據

来源:http://www.cnblogs.com/caolinsummer/archive/2016/07/05/5643632.html
-Advertisement-
Play Games

3. 記憶體數據 前面我們知道了,記憶體是按位元組編址,每個地址的存儲單元可以存放8bit的數據。我們也知道CPU通過記憶體地址獲取一條指令和數據,而他們存在存儲單元中。現在就有一個問題。我們的數據和指令不可能剛好是8bit,如果小於8位,沒什麼問題,頂多是浪費幾位(或許按位元組編址是為了節省記憶體空間考慮)。 ...


3. 記憶體數據

 

前面我們知道了,記憶體是按位元組編址,每個地址的存儲單元可以存放8bit的數據。我們也知道CPU通過記憶體地址獲取一條指令和數據,而他們存在存儲單元中。現在就有一個問題。我們的數據和指令不可能剛好是8bit,如果小於8位,沒什麼問題,頂多是浪費幾位(或許按位元組編址是為了節省記憶體空間考慮)。但是當數據或指令的長度大於8bit呢?因為這種情況是很容易出現的,比如一個16bit的Int數據在記憶體是如何存儲的呢?

 

 

3.1 記憶體數據存放

 

其實一個簡單的辦法就是使用多個存儲單元來存放數據或指令。比如Int16使用2個記憶體單元,而Int32使用4個記憶體單元。當讀取數據時,一次讀取多個記憶體單元。於是這裡又出現2個問題:

  1. 多個存儲單元存儲的順序?
  2. 如何確定要讀幾個記憶體單元?

 

3.1.1 大端和小端存儲

  1. Little-Endian 就是低位位元組排放在記憶體的低地址端,高位位元組排放在記憶體的高地址端。
  2. Big-Endian 就是高位位元組排放在記憶體的低地址端,低位位元組排放在記憶體的高地址端。

需要說明的是,電腦採用大端還是小端存儲是CPU來決定的, 我們常用的X86體系的CPU採用小端,一下ARM體系的CPU也是用小端,但有一些CPU卻採用大端比如PowerPC、Sun。判斷CPU採用哪種方式很簡單:

[cpp] view plain copy    print?在CODE上查看代碼片派生到我的代碼片
  1. bool IsBigEndian()    
  2. {    
  3.     int vlaue = 0x1234;    
  4.     char lowAdd =  *(char *)&value;     
  5.     if( lowAdd == 0x12)    
  6.     {    
  7.         return true;    
  8.     }    
  9.     return false;    
  10. }  


既然不同電腦存儲的方式不同,那麼在不同電腦之間交互就可能需要進行大小端的轉換。這一點我們在Socket編程中可以看到。這裡就不介紹了,對以我們單一CPU來說我們可以不需要管這個轉換的問題,另外我們目前個人PC都是採用小端方式,所以我們後面預設都是這種方式。

 

 

3.1.2 CPU指令

 

前面我們多次提到了指令的概念,也知道指令是0和1組成的,而彙編代碼提高了機器碼的可讀性。為什麼突然在這裡介紹CPU指令呢? 主要是解釋上面的第二個問題,當我讀取一個數據或指令時,我怎麼知道需要讀取多少個記憶體單元。

 

 

3.1.2.1 CPU指令格式

 

首先我們來看看CPU指令的格式,我們知道CPU質量主要就是告訴CPU做什麼事情,所以一條CPU指令一般包含操作碼(OP)和操作

 

  操作碼欄位    地址碼欄位

 

 

 

根據一條指令中有幾個操作數地址,可將該指令稱為幾操作數指令或幾地址指令。

 

 操作碼  A1  A2  A3

 

 

三地址指令: (A1) OP (A2) --> A3

 

 操作碼  A1  A2

 

 

二地址指令: (A1) OP (A2) --> A1

 

 操作碼   A1

 

 

一地址指令: (AC) OP (A) --> AC   

 

 操作碼  

 

 

    零地址指令

A1為被操作數地址,也稱源操作數地址; A2為操作數地址,也稱終點操作數地址; A3為存放結果的地址。 同樣,A1,A2,A3以是記憶體中的單元地址,也可以是運算器中通用寄存器的地址。所以就有一個定址的問題。關於指令定址後面會介紹。

CPU指令設計是十分複雜的,因為在電腦中都是0和1保存,那電腦如何區分一條指令中的操作數和操作碼呢?如何保證指令不會重覆呢?這個不是我們討論的重點,有興趣的可以看看電腦體繫結構的書,裡面都會有介紹。從上圖來看我們知道CPU的指令長度是變長的。所以CPU並不能確定一條指令需要占用幾個記憶體單元,那麼CPU又是如何確定一條指令是否讀取完了呢?

 

 

3.1.2.2 指令的獲取

現在的CPU多數採用可變長指令系統。關鍵是指令的第一位元組。 當CPU讀指令時,並不是一下把整個指令讀近來,而是先讀入指令的第一個位元組。指令解碼器分析這個位元組,就知道這是幾位元組指令。接著順序讀入後面的位元組。每讀一個位元組,程式計數器PC加一。整個指令讀入後,PC就指向下一指令(等於為讀下一指令做好了準備)。

Sample1:

[plain] view plain copy    print?在CODE上查看代碼片派生到我的代碼片
  1. MOV AL,00  機器碼是1011 0000 0000 0000  


機器碼是16位在記憶體中占用2個位元組:

【00000000】 <- 0x0002

【10110000】 <- 0x0001

 

比如上面這條MOV彙編指令,把立即數00存入AL寄存器。而CPU獲取指令過程如下:

  1. 從程式計數器獲取當前指令的地址0x0001。
  2. 存儲控制器從0x0001中讀出整個位元組,發送給CPU。PC+1 = 0X0002.
  3. CPU識別出【10110000】表示:操作是MOV AL,並且A2是一個立即數長度為一個位元組,所以整個指令的字長為2位元組。
  4. CPU從地址0x0002取出指令的最後一個位元組
  5. CPU將立即數00存入AL寄存器。

 

這裡的疑問應該是在第3步,CPU是怎麼知道是MOV AL 立即數的操作呢?我們在看下麵一個列子。

 

Sample2:

[plain] view plain copy    print?在CODE上查看代碼片派生到我的代碼片
  1. MOV AL,[0000] 機器碼是1010 0000 0000 0000 0000 0000  

 

這裡同樣是一條MOV的彙編指令,整個指令需要占用3個位元組。

【00000000】 <-0x0003

【00000000】 <- 0x0002

【10100000】 <- 0x0001

 

我們可以比較一下2條指令第一個位元組的區別,發現這裡的MOV  AL是1010 0000,而不是Sample1中的1011 000。CPU讀取了第一個位元組後識別出,操作是MOV AL [D16],表示是一個寄存器間接定址,A3操作是存放的是一個16位就是地址偏移量(為什麼是16位,後面文章會介紹),CPU就判定這條指令長度3個位元組。於是從記憶體0x0002~0x0003讀出指令的後2個位元組,進行定址找到真正的數據記憶體地址,再次通過CPU讀入,並完成操作。

 

從上面我們可以看出一個指令會根據不同的定址格式,有不同的機器碼與之對應。而每個機器碼對應的指令的長度都是在CPU設計時就規定好了。8086採用變長指令,指令長度是1-6個位元組,後面可以添加8位或16位的偏移量或立即數。 下麵的指令格式相比上面2個就更加複雜。

 

  • 第一個位元組的高6位是操作碼,W表示傳說的數據是字(W=1)還是位元組(W=0),D表示數據傳輸方向D=0數據從寄存器傳出,D=1數據傳入寄存器。
  • 第二個位元組中REG表示寄存器號,3位可以表示8種寄存器,根據第一位元組的W,可以表示是8位還是16位寄存器。表3-1中列出了8086寄存器編碼表
  • 第二個位元組中的MOD和R/M指定了操作數的定址方式,表3-2列出了8086的編碼

這裡沒必要也無法更詳細介紹CPU指令的,只需要知道,CPU指令中已經定義了指令的長度,不會出現混亂讀取記憶體單元的現象。有興趣的可以查看引用中的連接。

 

 

3.1.3  記憶體數據

 

3.1.3.1 記憶體數據的操作

 

從上面我們可以知道,操作數可以是立即數,可以存放在寄存器,也可以存放在記憶體。對於第一個例子,指令已經說明,操作時是一個位元組,於是CPU可以從下一個記憶體地址讀取操作時,而對於第二個列子,操作數只是地址偏移,所以當CPU獲得這個數據後,需要轉換成實際的記憶體地址,在進行一次記憶體訪問,把數據讀入到寄存器中。這裡就出現我們前面提到的問題,這個數據我們要讀幾個存儲單元呢?

[cpp] view plain copy    print?在CODE上查看代碼片派生到我的代碼片
  1.     MyClass cla;  
  2. 008C3EC9  lea         ecx,[cla]    
  3. 008C3ECC  call        MyClass::MyClass (08C1050h)    
  4. 008C3ED1  mov         dword ptr [ebp-4],0    
  5.     cla.num5 = 500;  
  6. 008C3ED8  mov         dword ptr [ebp-6Ch],1F4h    
  7.     int b1 = MyClass::num1;  
  8. 008C3EDF  mov         dword ptr [b1],64h    
  9.     int b2 = MyClass::num2;  
  10. 008C3EE6  mov         dword ptr [b2],0C8h    
  11.     int b3 = MyClass::num3;  
  12. 008C3EF0  mov         eax,dword ptr ds:[008C9008h]    
  13. 008C3EF5  mov         dword ptr [b3],eax    
  14.     int b4 = cla.num4;  
  15. 008C3EFB  mov         eax,dword ptr [cla]    
  16. 008C3EFE  mov         dword ptr [b4],eax    
  17.     int b5 = cla.num5;  
  18. 008C3F04  mov         eax,dword ptr [ebp-6Ch]    
  19. 008C3F07  mov         dword ptr [b5],eax    


讓我們看一段C++代碼和對應的彙編代碼,操作很簡單,創建一個Myclass對象後,對成員變數賦值。而賦值都是試用Mov操作符。對於這些變數我們有賦值操作和取值操作,那麼是如何確定要讀取或寫入數據的大小呢?

[cpp] view plain copy    print?在CODE上查看代碼片派生到我的代碼片
  1. cla.num5 = 500;  
  2. 08C3ED8  mov         dword ptr [ebp-6Ch],1F4h    


我看先看看賦值操作,往dword ptr [ebp-6Ch]記憶體存入一個立即數, [ebp-6Ch]是num5的記憶體地址,而前面的dword ptr 表示這是進行一個雙子操作。還記得上面指令格式中第一個位元組的W欄位嗎? 在8086中只能進行位元組或字操作,而現在CPU都可以進行雙字操作。

[cpp] view plain copy    print?在CODE上查看代碼片派生到我的代碼片
  1. int b5 = cla.num5;  
  2. 08C3F04  mov         eax,dword ptr [ebp-6Ch]    


同樣,當我們要從一個記憶體讀取數據的時候,也要指定讀取數據的操作類型,這裡也是雙字操作。這樣以來,就能從記憶體中正確的讀出需要的長度了。就這麼一個簡單的賦值操作,獲取你從來沒想過在記憶體中怎麼存放,又是怎麼讀取的。這一切都是編譯器和CPU在背後為我們完成了。

 

 

3.1.3.2 記憶體對齊

 前面我們清楚了CPU是如何正確讀取數大小不同的數據的,最後一部分來看看有關記憶體對齊的問題。對於大部分程式員來說,記憶體對齊應該是透明的。記憶體對齊是編譯器的管轄範圍。編譯器為程式中的每個數據單元安排在適當的位置上。

 

 

3.1.3.2.1 對齊原因

從前面我們知道,目前電腦記憶體按照位元組編址,每個地址的記憶體大小為1個位元組。而讀取數據的大小和數據線有關。比如數據線為8位那麼一次讀取一個位元組,而如果數據線為32位,那麼一次需要讀取32個位元組,這樣是為了一次更多的獲取數據提高效率。否則讀取一個int變數就需要進行4次記憶體操作。對於記憶體訪問一般有以下兩個條件:

  1. CPU進行一次記憶體訪問讀取的數據和字長相同。
  2. 有些CPU只能對字長倍數的記憶體地址進行訪問。

對於第一個條件一般來說,目前存儲器一個cell是8bit,進行位擴展使他和字長還有數據線位數是相同,那麼一次就能傳送CPU可以處理最多的數據。而前面我們說過目前是按位元組編址可能是因為一個cell是8bit,所以一次記憶體操作讀取的數據就是和字長相同。

也正是因為和存儲器擴展有關(參考1.2.1的圖),每個DRAM位擴展晶元使用相同RAS。如果需要跨行訪問,那麼需要傳遞2次RAS。所以以32位CPU為例,CPU只能對0,4,8,16這樣的地址進行定址。而很多32位CPU禁掉了地址線中的低2位A0,A1,這樣他們的地址必須是4的倍數,否則會發送錯誤。

如上圖,當電腦數據線為32位時,一次讀入4個地址範圍的數據。當一個int變數存放在0-3的地址中時,CPU一次就記憶體操作就可以取得int變數的值。但是如果int變數存放在1-4的地址中呢? 根據上麵條件2的解釋,這個時候CPU需要進行2次記憶體訪問,第一次讀取0-4的數據,並且只保存1-3的內容,第二次訪問讀取4-7的數據並且只保存4的數據,然後將1-4組合起來。如下圖:

所以記憶體對齊不但可以解決不同CPU的相容性問題,還能減少記憶體訪問次數,提高效率。當然目前關於這個原因爭論很多,可以看看CSDN上的討論:http://bbs.csdn.net/topics/30388330

 

3.1.3.2.2 如何對齊記憶體

記憶體對齊有一個對齊繫數,一般是2,4,8,16位元組這樣。而不同平臺上的對齊方式不同,這個主要是編譯器來決定的。

具體的規則可以參考之前轉的一篇文章,這裡就不詳細寫了: http://blog.csdn.net/cc_net/article/details/2908600

 

 

 

總結

 

 

通過這一篇對記憶體工作的介紹,我們從記憶體的硬體結構,存儲方式過渡到了記憶體的編址方式,然後又探討了按位元組編址帶來的問題和解決的辦法。這裡就涉及到了CPU的指令格式,編譯器的支持。最後我們也是從硬體和軟體方面討論了記憶體對齊的問題。

 

我自己感覺,記憶體的訪問管理是電腦中最重要的部分,也是電腦硬體和軟體之間交互的過渡的一個地方。所以理解了記憶體的工作原理,對於後面理解不同的記憶體模型很有幫助。

 參考 http://blog.csdn.net/cc_net/article/details/11097267

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 最近因為工作的需要看了看powershell相關的知識,個人總結了一點有關於powershell遠程連接需要做的步驟,希望對別人有所幫助。 使用powershell遠程連接,需要進行 設備的配置: 1. 開啟ps遠程管理:enable-psremoting -force (如果出現提示: 按 y 鍵 ...
  • Windows 7 與 Vmware Ubuntu 15.10_64 共用文件夾 ...
  • FreeBSD用戶手冊學習筆記 freeBSD安裝:http://my.oschina.net/lsgx/blog/540980 第一章 介紹 1.1 FreeBSD歷史和簡介 FreeBSD 自身的源代碼是完全公開的,所以可以對系統進行最大程度的定製。 FreeBSD 項目的目標是無附加條件地提供 ...
  • 安裝openssh-serversudo apt-get install openssh-server 查看server是否啟動: ps -ef |grep ssh 如果看到/usr/sbin/sshd -D,說明服務已經啟動,否則服務尚未啟動,那麼需要啟動server: /etc/init.d/s ...
  • ROP的全稱為Return-oriented programming(返回導向編程),這是一種高級的記憶體攻擊技術可以用來繞過現代操作系統的各種通用防禦(比如記憶體不可執行和代碼簽名等)。雖然現在大家都在用64位的操作系統,但是想要扎實的學好ROP還是得從基礎的x86系統開始,但看官請不要著急。 ...
  • 第一部分:安裝redis 現在我們將redis安裝到此目錄 /usr/local/redis 希望將安裝包下載到此目錄 /usr/local/src 那麼安裝過程指令如下: (註:redis官網地址:http://www.redis.io/ 最新版本:3.0.6) 註意上面的最後一行,我們通過PRE ...
  • 1,伺服器系統的安裝會出現錯誤的地方一般都是在Raid 卡驅動 略過Raid 卡配置, 具體 http://jingyan.baidu.com/article/ca41422fddfd201eae99ed30.html 2.準備好2008R2 系統光碟 以下所舉例的是由"用安裝光碟引導啟動安裝"的方 ...
  • 在ASP.NET 2.0 站點根目錄下,只要存在 App_Offline.htm 文件,那麼所有對.aspx的請求都將轉向App_Offline.htm 。而且瀏覽器的地址欄顯示的是所請求的.aspx的URL。這樣當我們的站點需要維護時,只要把App_Offline.htm 拷貝到站點根目錄下即可。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...