簡述計算機體系結構

計算機體系結構(翻譯)

本文翻譯自《Programming from the Ground Up》一書第二章 "Computer Architecture".
該書是講x86匯編語言編程的, 可從 http://savannah.nongnu.org/projects/pgubook/ 下載(英文版).
我出于興趣看過前面部分章節, 發現第二章是很好的計算機入門讀物, 并不涉及匯編. 我未見該書有中文版, 因此嘗試翻譯, 以期幫助人們了解計算機, 揭開并破除計算機的神秘面紗.


現代計算機是基于一種叫做馮諾依曼結構的體系結構, 該結構根據其創建者的名字命名. 馮諾依曼結構把計算機分成兩個主要部分:CPU(中央處理器)和主存(譯注:即通常所說的內存). 所有的現代計算機, 包括個人電腦(PC), 超級計算機, 大型機, 甚至手機, 全部都采用這種結構.

計算機主存的結構

要理解計算機如何看待內存, 請想象一下當地的郵局. 他們通常有一間屋子, 里邊放滿了郵政信箱. 這些信箱和計算機內存有些類似, 他們都是有編號的連續的固定大小的存儲單元. 例如, 如果你有256M的計算機內存, 那等于是你的計算機有大約2億5千6百萬個固定的存儲單元. 如果用剛才的比喻, 就是有大約2億5千6百萬個信箱. 每一個存儲單元都有一個編號, 而且每個存儲單元都有相同的固定的大小. 一個郵政信箱和一個存儲單元的區別在于, 你在一個郵政信箱里可以放各種不同的東西, 而在計算機內存的一個存儲單元里就只能放一個數.

計算機內存就像郵政信箱

你可能想知道為什么計算機要被設計成這樣. 這是因為這樣容易生產制造. 假如計算機是由許多大小不同的存儲單元構成, 或者你可以在其中放各種東西, 那要制作起來就既困難又昂貴.

計算機的內存被用來做許多不同的事情. 任何計算的結果全都存在里邊. 實際上, 任何被"存儲"了的東西, 都是存儲在內存里. 考慮一下你家的電腦, 想象一下你的電腦內存里都存了些什么.

  • 你的光標(鼠標指針)在屏幕上的位置
  • 屏幕上每個窗口的位置
  • 正在使用的每個字體中每個字符的形狀
  • 每個窗口中的所有控件(按鈕,列表框,文本框等各種元素,譯注)的布局
  • 所有的工具欄圖標的圖像
  • 每個錯誤消息和對話框的文本內容
  • 等等等等

除了以上這些, 馮諾依曼體系還規定不僅計算機的數據要放在內存里, 而且控制計算機操作的程序也要在內存里. 事實上, 在計算機里, 程序和它的數據是一樣的, 其差別僅在于如何被計算機使用. 它們都以相同的方式被存儲和訪問.

CPU

那么計算機是怎么工作的呢? 顯然, 簡單存儲數據沒有多大用處, 你得能訪問, 修改和移動它. 這就是CPU的用武之地. CPU從主存每次一條地讀取指令, 然后執行. 這一過程被稱為 指令周期. CPU包含如下組成部分以完成這一功能:

  • 程序計數器
  • 指令譯碼器
  • 數據總線
  • 通用寄存器
  • 算數邏輯單元

程序計數器 用來告訴計算機下一個指令從哪里獲取. 我們前面提到數據和程序的存儲方式是一樣的, 它們只是被CPU拿來做了不同的解釋. 程序計數器保存著下一條要被執行的指令的內存地址. CPU一開始就查看程序計數器, 并按照其指定的位置, 在內存中讀取那個數, 無論是幾. 之后那個數會被送到 指令譯碼器, 以便明確它表示什么指令. 這包括需要執行什么過程(加,減,乘,移動數據,等等)以及該過程涉及到那些存儲單元. 計算機指令通常包含實際的操作以及執行該操作所涉及的一系列存儲單元這兩部分.

這時計算機使用 數據總線 來獲取計算中用到的存儲單元. 數據總線是CPU和內存之間的橋梁. 它是連接它們的真實的電線. 如果你看一下電腦主板, 那么從內存出來的線路就是你的數據總線.

除了處理器外邊的主存, 處理器自己也有一些特殊的, 高速的存儲單元, 稱為寄存器. 寄存器分為兩類, 通用寄存器專用寄存器. 通用寄存器是完成主要工作的地方. 加,減,乘,比較,和其他操作通常使用通用寄存器來進行操作. 但是, 計算機只有很少的通用寄存器. 大部分信息都存儲在主存, 拿到寄存器里進行處理, 處理完畢之后再放回主存. 專用寄存器 是一些有特殊用途的寄存器. 我們以后遇到的時候再討論它們.

既然CPU已經取得了需要的全部數據, 它就把這些數據連同已經解碼的指令一起傳給 算數邏輯單元 做進一步處理. 這里是指令真正被執行的地方. 在計算完成得出結果后, 按照指令指定的, 結果將被放到 數據總線 并送到合適的存儲單元或者放到寄存器.

這是一個非常簡化的解釋. 處理器在近年發展很快, 而且也更復雜得多了. 雖然基本的操作還是一樣, 但是多級緩存, 超標量結構處理器, 流水線, 分支預測, 亂序執行, 微代碼翻譯, 協處理器, 以及其他的優化等使之變得復雜. 如果你不理解這些詞語那也沒什么可擔心的, 如果你想多了解一些關于CPU的信息, 可以上網搜索這些詞匯.

一些概念

計算機內存是有編號的連續的固定大小的存儲單元. 每個存儲單元所附帶的編號被稱為它的 地址. 單獨的存儲單元的大小稱為一個 字節. 在x86處理器上, 一個字節是取值在0-255之間的一個數字.

你可能想知道, 既然計算機只能存儲0到255之間的數字, 那么它是怎么顯示和使用文本, 圖像, 甚至是更大的數字的. 首先, 專門的硬件, 如顯卡, 對每一個數字有特定的解釋. 當要顯示到屏幕時, 計算機根據 ACSII 碼表格把你發出的數字翻譯成在屏幕上顯示的字母, 每一個數字準確翻譯成一個字母或者數字. [1] 例如, 大寫字母 A 用數字65表示. 字符 1 用數字49表示. 因此, 要打印出"HELLO", 你實際上給計算機的是72, 69, 76, 76, 79 這一串數字. 要打印出數字 100, 你要給計算機 49, 48, 48 這一串數字. 附錄D包含ASCII碼字符和其對應的數字的表格.

除了用數字來表示ASCII字符, 作為程序員, 你也可以用數字來表示任何你想讓它表示的東西. 例如, 如果我開了家商店, 我會用一個數字來表示我出售的每一種商品. 每一個數字會關聯到一系列其他的數字, 那些是ASCII字符, 用來表示在掃描的時候要顯示的文字. 我還需要更多的數字來表示價格, 庫存, 等等.

那么比255大的數怎么辦呢? 我們可以簡單的組合字節來表示更大的數字. 兩個字節表示的數字范圍是0到65535. 4個字節能表示的數字范圍是0到4294967295. 現在, 寫程序把字節組合起來增加數字的范圍是很難的, 那需要一定的數學功底. 幸運的是, 計算機會替我們做4個字節以內的組合. 事實上, 我們默認會用到的就是4字節的數字. (譯注: 這里4字節是指32位的x86處理器; 原書成于2004年, 講解x86匯編編程, 當時PC處理器主要是32位的; 目前常見的64位處理器支持8個字節以內的組合.)

我們前面提到計算機除了有常規的內存, 還有被稱為 寄存器 的特殊用途的存儲單元. 寄存器是計算機用來進行計算的. 把寄存器想象成你桌子上的一個地方, 那里放的是你正在用著的東西. 你的文件夾和抽屜里可能放著許多資料, 但你現在工作正用的東西在桌面上. 寄存器存儲的就是你正在操作的數字的內容.

在我們用著的計算機里, 寄存器都是4字節的. 典型的寄存器長度被稱為計算機的 長. x86處理器的字有4字節. 這意味著在這些計算機上一次操作4個字節的是最自然的. 這個數值是大約40億.

地址同樣是4字節(1個字)的長度, 因此也能放進寄存器. 如果安裝足夠的內存, x86處理器能最多訪問4294967296個字節. 注意, 這意味著我們可以像存儲其他數字那樣來存儲地址. 事實上, 計算機無法分辨一個數值到底是地址, 數字, ASCII碼, 或者是你存的別的什么東西. 一個數, 當你要顯示它的時候, 它就是ASCII碼, 當你查詢它指向的字節的時候, 它就是地址. 請花點時間思考一下這一點, 它對于理解計算機如何工作是至關重要的.

存儲在內存里的地址也被稱為 指針, 因為它并不包含一個通常的數值, 而是指引你到內存里的另一個地方去.

如前所述, 計算機的指令也是存儲在內存里的. 事實上他們和其他數據存儲的方式完全一樣. 計算機知道一個存儲單元里是指令的唯一方法, 就是一個叫做 程序計數器 的專用寄存器在某一點或另一點指向了它. 如果程序計數器指向了內存里的一個字, 那個字就被作為指令加載. 除此之外, 計算機沒有辦法分辨程序和其他數據的區別. [2]

解釋內存

計算機是非常精確的. 因為它們精確, 所以程序員也不得不同樣精確. 一臺電腦不知道你的程序打算要干嘛. 因此, 它只能精確的做你告訴它要做的事情. 如果你意外地打印了一個數字, 而不是那個數字對應一串的ASCII碼, 計算機會照做不誤, 而你會因為屏幕上的亂碼而氣憤(它會在ASCII表中找查那個數字對應的字符并打印出來). 如果你讓計算機開始執行內存中某處的指令, 而那里其實存的是數據, 天知道計算機會怎么解釋, 但它肯定會去試的. 計算機會嚴格按照你提供的順序來執行指令, 即使那是沒有意義的.

重點在于, 計算機會嚴格按照你的命令來做, 不管多么沒有意義. 因此, 作為程序員, 你需要精確的知道你怎樣在內存中組織你的數據. 記住, 計算機只能存儲數字, 所以字母, 圖片, 音樂, 網頁, 文檔, 以及任何其他的東西, 在計算機里都只是一長串的數字, 而某些特定的程序知道怎么解釋它們.

比如說, 你想在內存里存儲客戶的信息. 一個方法是設置客戶的姓名和地址的最大長度, 算每個有50個ASCII字符, 那就是每項50個字節. 然后, 有一個數字存用戶的年齡和他們的客戶id號. 這樣, 你會有如下分布的內存狀況:

記錄起始:
  客戶的姓名 (50字節) - 記錄起始
  客戶的地址 (50字節) - 記錄起始 + 50字節
  客戶的年齡 (1字 - 4字節) - 記錄起始 + 100字節
  客戶的id號 (1字 - 4字節) - 記錄起始 + 104字節

這樣, 給出了客戶記錄的地址的話, 你知道怎么找其他的數據. 但是它畢竟限制了客戶的姓名和地址的最大長度分別是50個ASCII碼.

如果我們不做這個限制會怎么樣呢? 另一種方法是只記錄中存放信息的指針. 比如, 我們不存姓名, 而是存姓名的一個指針. 這樣我們會得到如下的內存分布:

記錄起始:
  客戶姓名的指針 (1字) - 記錄起始
  客戶地址的指針 (1字) - 記錄起始 + 4
  客戶的年齡 (1字) - 記錄起始 + 8
  客戶的id號 (1字) - 記錄起始 + 12

實際的姓名和地址會存到內存的其他地方. 這種方式, 我們可以容易的知道哪一部分信息離記錄的開始有多遠, 同時又不限制姓名的地址的大小. 如果我們的記錄中的一條信息的長度會變化, 我們就不知道下一條信息從哪開始. 因為記錄的長度會變化, 所以找到下一條記錄也同樣困難. 因此, 幾乎所有的記錄都是固定大小的. 變成的數據通常和記錄的其余部分分開存儲.

數據訪問方式

處理器(指令)訪問數據有幾個不同的方式, 稱之為尋址模式. 最簡單的一種是 立即尋址模式, 此時數據就包含字指令里頭. 例如, 如果我們想吧一個寄存器地值初始化設置為0, 我們可以使用立即模式, 給它個數值0, 而不用給它打地址, 然后從那里再讀一個0.

寄存器尋址模式, 指令包含要訪問的寄存器, 而不是內存地址. 剩下的模式將是處理地址的.

直接尋址模式, 指令包含要訪問的內存地址. 比如, 我可能說, 請把地址2002位置的數據加載的這個寄存器. 計算機就會直接到2002編號的存儲單元, 并把其中的內容拷貝到寄存器.

變址尋址模式, 指令包含要訪問的內存地址, 同時指定一個 變址寄存器 來做地址偏移. 例如, 我們可以指定地址2002和一個變址寄存器, 如果變址寄存器里的數是4, 實際的數據地址將是2006. 用這種方式, 如果你有從2002地址開始的一系列的數, 你可以用變址寄存器來循環操作它們. 在x86處理器中, 你還可以指定一個乘法系數來計算偏移. 這能允許你一次訪問一個字節或者一個字(4字節). 如果你要訪問一個字, 對于某個元素, 你的偏移量需要乘以4才能得到準確的位置. 例如, 如果你要訪問2002開始的第4個字節, 那么你要設置變址寄存器為3(記住, 我們從0開始計數), 乘法系數為1, 因為我們是單個字節訪問的. 這樣我們得到2005的位置. 但是, 如果你要訪問從2002開始的第4個字, 那么你要設置變址寄存器為3, 并設置乘法系數為4, 這樣得到的位置上2014, 第4個字. 花點時間自己計算一下, 確保你理解如何計算.

間接尋址模式, 指令包含一個寄存器, 而其中是個指針, 指向了數據所在位置. 例如, 我們使用間接尋址并制定 %eax 寄存器, 而%eax里的值是4, 那么內存中編號為4的存儲單元, 用的就是那里的數據, 不論里邊是什么. 在直接尋址中, 我們將只是加載數值4, 但在間接尋址中, 我們用4作為地址去找我們要的數據.

最后, 還有 基址尋址模式. 這個和間接尋址類似, 但你同時使用一個稱為 偏移 的數來加到寄存器里的數值上, 然后再查詢. 本書中將大量使用這種模式.

解釋內存 的章節中, 我們討論了用一個內存中的結構來放置客戶信息. 現在假定我們要獲取客戶的年齡, 那是數據的第8個字節, 而我們在寄存器中存儲了結構開始的地址. 我們可以用基址尋址, 指定那個寄存器作為基址, 以8為偏移. 這和變址尋址很像, 區別在于偏移量是常數, 而地址放在寄存器里, 在變址尋址中, 偏移量在寄存器里而地址是常數.

還有其他一些尋址方式, 但前面這些是最重要的.

2013-08-16


  1. 隨著國際字符集和Unicode的到來, 事情并不總是這樣(比如, 一個中文字符需要兩個或更多字節, 譯注). 但是, 為了方便初學者, 我們假設一個數字翻譯成一個字符. 更多的信息請參考附錄D. ?

  2. 注意, 我們在這里討論的是通用計算機理論. 有些處理器和操作系統實際上用特殊的標記來標識一塊可執行的內存區域. ?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容