計算機體系結(jié)構(翻譯)
本文翻譯自《Programming from the Ground Up》一書第二章 "Computer Architecture".
該書是講x86匯編語言編程的, 可從 http://savannah.nongnu.org/projects/pgubook/ 下載(英文版).
我出于興趣看過前面部分章節(jié), 發(fā)現(xiàn)第二章是很好的計算機入門讀物, 并不涉及匯編. 我未見該書有中文版, 因此嘗試翻譯, 以期幫助人們了解計算機, 揭開并破除計算機的神秘面紗.
現(xiàn)代計算機是基于一種叫做馮諾依曼結(jié)構的體系結(jié)構, 該結(jié)構根據(jù)其創(chuàng)建者的名字命名. 馮諾依曼結(jié)構把計算機分成兩個主要部分:CPU(中央處理器)和主存(譯注:即通常所說的內(nèi)存). 所有的現(xiàn)代計算機, 包括個人電腦(PC), 超級計算機, 大型機, 甚至手機, 全部都采用這種結(jié)構.
計算機主存的結(jié)構
要理解計算機如何看待內(nèi)存, 請想象一下當?shù)氐泥]局. 他們通常有一間屋子, 里邊放滿了郵政信箱. 這些信箱和計算機內(nèi)存有些類似, 他們都是有編號的連續(xù)的固定大小的存儲單元. 例如, 如果你有256M的計算機內(nèi)存, 那等于是你的計算機有大約2億5千6百萬個固定的存儲單元. 如果用剛才的比喻, 就是有大約2億5千6百萬個信箱. 每一個存儲單元都有一個編號, 而且每個存儲單元都有相同的固定的大小. 一個郵政信箱和一個存儲單元的區(qū)別在于, 你在一個郵政信箱里可以放各種不同的東西, 而在計算機內(nèi)存的一個存儲單元里就只能放一個數(shù).
你可能想知道為什么計算機要被設計成這樣. 這是因為這樣容易生產(chǎn)制造. 假如計算機是由許多大小不同的存儲單元構成, 或者你可以在其中放各種東西, 那要制作起來就既困難又昂貴.
計算機的內(nèi)存被用來做許多不同的事情. 任何計算的結(jié)果全都存在里邊. 實際上, 任何被"存儲"了的東西, 都是存儲在內(nèi)存里. 考慮一下你家的電腦, 想象一下你的電腦內(nèi)存里都存了些什么.
- 你的光標(鼠標指針)在屏幕上的位置
- 屏幕上每個窗口的位置
- 正在使用的每個字體中每個字符的形狀
- 每個窗口中的所有控件(按鈕,列表框,文本框等各種元素,譯注)的布局
- 所有的工具欄圖標的圖像
- 每個錯誤消息和對話框的文本內(nèi)容
- 等等等等
除了以上這些, 馮諾依曼體系還規(guī)定不僅計算機的數(shù)據(jù)要放在內(nèi)存里, 而且控制計算機操作的程序也要在內(nèi)存里. 事實上, 在計算機里, 程序和它的數(shù)據(jù)是一樣的, 其差別僅在于如何被計算機使用. 它們都以相同的方式被存儲和訪問.
CPU
那么計算機是怎么工作的呢? 顯然, 簡單存儲數(shù)據(jù)沒有多大用處, 你得能訪問, 修改和移動它. 這就是CPU的用武之地. CPU從主存每次一條地讀取指令, 然后執(zhí)行. 這一過程被稱為 指令周期. CPU包含如下組成部分以完成這一功能:
- 程序計數(shù)器
- 指令譯碼器
- 數(shù)據(jù)總線
- 通用寄存器
- 算數(shù)邏輯單元
程序計數(shù)器 用來告訴計算機下一個指令從哪里獲取. 我們前面提到數(shù)據(jù)和程序的存儲方式是一樣的, 它們只是被CPU拿來做了不同的解釋. 程序計數(shù)器保存著下一條要被執(zhí)行的指令的內(nèi)存地址. CPU一開始就查看程序計數(shù)器, 并按照其指定的位置, 在內(nèi)存中讀取那個數(shù), 無論是幾. 之后那個數(shù)會被送到 指令譯碼器, 以便明確它表示什么指令. 這包括需要執(zhí)行什么過程(加,減,乘,移動數(shù)據(jù),等等)以及該過程涉及到那些存儲單元. 計算機指令通常包含實際的操作以及執(zhí)行該操作所涉及的一系列存儲單元這兩部分.
這時計算機使用 數(shù)據(jù)總線 來獲取計算中用到的存儲單元. 數(shù)據(jù)總線是CPU和內(nèi)存之間的橋梁. 它是連接它們的真實的電線. 如果你看一下電腦主板, 那么從內(nèi)存出來的線路就是你的數(shù)據(jù)總線.
除了處理器外邊的主存, 處理器自己也有一些特殊的, 高速的存儲單元, 稱為寄存器. 寄存器分為兩類, 通用寄存器 和 專用寄存器. 通用寄存器是完成主要工作的地方. 加,減,乘,比較,和其他操作通常使用通用寄存器來進行操作. 但是, 計算機只有很少的通用寄存器. 大部分信息都存儲在主存, 拿到寄存器里進行處理, 處理完畢之后再放回主存. 專用寄存器 是一些有特殊用途的寄存器. 我們以后遇到的時候再討論它們.
既然CPU已經(jīng)取得了需要的全部數(shù)據(jù), 它就把這些數(shù)據(jù)連同已經(jīng)解碼的指令一起傳給 算數(shù)邏輯單元 做進一步處理. 這里是指令真正被執(zhí)行的地方. 在計算完成得出結(jié)果后, 按照指令指定的, 結(jié)果將被放到 數(shù)據(jù)總線 并送到合適的存儲單元或者放到寄存器.
這是一個非常簡化的解釋. 處理器在近年發(fā)展很快, 而且也更復雜得多了. 雖然基本的操作還是一樣, 但是多級緩存, 超標量結(jié)構處理器, 流水線, 分支預測, 亂序執(zhí)行, 微代碼翻譯, 協(xié)處理器, 以及其他的優(yōu)化等使之變得復雜. 如果你不理解這些詞語那也沒什么可擔心的, 如果你想多了解一些關于CPU的信息, 可以上網(wǎng)搜索這些詞匯.
一些概念
計算機內(nèi)存是有編號的連續(xù)的固定大小的存儲單元. 每個存儲單元所附帶的編號被稱為它的 地址. 單獨的存儲單元的大小稱為一個 字節(jié). 在x86處理器上, 一個字節(jié)是取值在0-255之間的一個數(shù)字.
你可能想知道, 既然計算機只能存儲0到255之間的數(shù)字, 那么它是怎么顯示和使用文本, 圖像, 甚至是更大的數(shù)字的. 首先, 專門的硬件, 如顯卡, 對每一個數(shù)字有特定的解釋. 當要顯示到屏幕時, 計算機根據(jù) ACSII 碼表格把你發(fā)出的數(shù)字翻譯成在屏幕上顯示的字母, 每一個數(shù)字準確翻譯成一個字母或者數(shù)字. [1] 例如, 大寫字母 A 用數(shù)字65表示. 字符 1 用數(shù)字49表示. 因此, 要打印出"HELLO", 你實際上給計算機的是72, 69, 76, 76, 79 這一串數(shù)字. 要打印出數(shù)字 100, 你要給計算機 49, 48, 48 這一串數(shù)字. 附錄D包含ASCII碼字符和其對應的數(shù)字的表格.
除了用數(shù)字來表示ASCII字符, 作為程序員, 你也可以用數(shù)字來表示任何你想讓它表示的東西. 例如, 如果我開了家商店, 我會用一個數(shù)字來表示我出售的每一種商品. 每一個數(shù)字會關聯(lián)到一系列其他的數(shù)字, 那些是ASCII字符, 用來表示在掃描的時候要顯示的文字. 我還需要更多的數(shù)字來表示價格, 庫存, 等等.
那么比255大的數(shù)怎么辦呢? 我們可以簡單的組合字節(jié)來表示更大的數(shù)字. 兩個字節(jié)表示的數(shù)字范圍是0到65535. 4個字節(jié)能表示的數(shù)字范圍是0到4294967295. 現(xiàn)在, 寫程序把字節(jié)組合起來增加數(shù)字的范圍是很難的, 那需要一定的數(shù)學功底. 幸運的是, 計算機會替我們做4個字節(jié)以內(nèi)的組合. 事實上, 我們默認會用到的就是4字節(jié)的數(shù)字. (譯注: 這里4字節(jié)是指32位的x86處理器; 原書成于2004年, 講解x86匯編編程, 當時PC處理器主要是32位的; 目前常見的64位處理器支持8個字節(jié)以內(nèi)的組合.)
我們前面提到計算機除了有常規(guī)的內(nèi)存, 還有被稱為 寄存器 的特殊用途的存儲單元. 寄存器是計算機用來進行計算的. 把寄存器想象成你桌子上的一個地方, 那里放的是你正在用著的東西. 你的文件夾和抽屜里可能放著許多資料, 但你現(xiàn)在工作正用的東西在桌面上. 寄存器存儲的就是你正在操作的數(shù)字的內(nèi)容.
在我們用著的計算機里, 寄存器都是4字節(jié)的. 典型的寄存器長度被稱為計算機的 字 長. x86處理器的字有4字節(jié). 這意味著在這些計算機上一次操作4個字節(jié)的是最自然的. 這個數(shù)值是大約40億.
地址同樣是4字節(jié)(1個字)的長度, 因此也能放進寄存器. 如果安裝足夠的內(nèi)存, x86處理器能最多訪問4294967296個字節(jié). 注意, 這意味著我們可以像存儲其他數(shù)字那樣來存儲地址. 事實上, 計算機無法分辨一個數(shù)值到底是地址, 數(shù)字, ASCII碼, 或者是你存的別的什么東西. 一個數(shù), 當你要顯示它的時候, 它就是ASCII碼, 當你查詢它指向的字節(jié)的時候, 它就是地址. 請花點時間思考一下這一點, 它對于理解計算機如何工作是至關重要的.
存儲在內(nèi)存里的地址也被稱為 指針, 因為它并不包含一個通常的數(shù)值, 而是指引你到內(nèi)存里的另一個地方去.
如前所述, 計算機的指令也是存儲在內(nèi)存里的. 事實上他們和其他數(shù)據(jù)存儲的方式完全一樣. 計算機知道一個存儲單元里是指令的唯一方法, 就是一個叫做 程序計數(shù)器 的專用寄存器在某一點或另一點指向了它. 如果程序計數(shù)器指向了內(nèi)存里的一個字, 那個字就被作為指令加載. 除此之外, 計算機沒有辦法分辨程序和其他數(shù)據(jù)的區(qū)別. [2]
解釋內(nèi)存
計算機是非常精確的. 因為它們精確, 所以程序員也不得不同樣精確. 一臺電腦不知道你的程序打算要干嘛. 因此, 它只能精確的做你告訴它要做的事情. 如果你意外地打印了一個數(shù)字, 而不是那個數(shù)字對應一串的ASCII碼, 計算機會照做不誤, 而你會因為屏幕上的亂碼而氣憤(它會在ASCII表中找查那個數(shù)字對應的字符并打印出來). 如果你讓計算機開始執(zhí)行內(nèi)存中某處的指令, 而那里其實存的是數(shù)據(jù), 天知道計算機會怎么解釋, 但它肯定會去試的. 計算機會嚴格按照你提供的順序來執(zhí)行指令, 即使那是沒有意義的.
重點在于, 計算機會嚴格按照你的命令來做, 不管多么沒有意義. 因此, 作為程序員, 你需要精確的知道你怎樣在內(nèi)存中組織你的數(shù)據(jù). 記住, 計算機只能存儲數(shù)字, 所以字母, 圖片, 音樂, 網(wǎng)頁, 文檔, 以及任何其他的東西, 在計算機里都只是一長串的數(shù)字, 而某些特定的程序知道怎么解釋它們.
比如說, 你想在內(nèi)存里存儲客戶的信息. 一個方法是設置客戶的姓名和地址的最大長度, 算每個有50個ASCII字符, 那就是每項50個字節(jié). 然后, 有一個數(shù)字存用戶的年齡和他們的客戶id號. 這樣, 你會有如下分布的內(nèi)存狀況:
記錄起始:
客戶的姓名 (50字節(jié)) - 記錄起始
客戶的地址 (50字節(jié)) - 記錄起始 + 50字節(jié)
客戶的年齡 (1字 - 4字節(jié)) - 記錄起始 + 100字節(jié)
客戶的id號 (1字 - 4字節(jié)) - 記錄起始 + 104字節(jié)
這樣, 給出了客戶記錄的地址的話, 你知道怎么找其他的數(shù)據(jù). 但是它畢竟限制了客戶的姓名和地址的最大長度分別是50個ASCII碼.
如果我們不做這個限制會怎么樣呢? 另一種方法是只記錄中存放信息的指針. 比如, 我們不存姓名, 而是存姓名的一個指針. 這樣我們會得到如下的內(nèi)存分布:
記錄起始:
客戶姓名的指針 (1字) - 記錄起始
客戶地址的指針 (1字) - 記錄起始 + 4
客戶的年齡 (1字) - 記錄起始 + 8
客戶的id號 (1字) - 記錄起始 + 12
實際的姓名和地址會存到內(nèi)存的其他地方. 這種方式, 我們可以容易的知道哪一部分信息離記錄的開始有多遠, 同時又不限制姓名的地址的大小. 如果我們的記錄中的一條信息的長度會變化, 我們就不知道下一條信息從哪開始. 因為記錄的長度會變化, 所以找到下一條記錄也同樣困難. 因此, 幾乎所有的記錄都是固定大小的. 變成的數(shù)據(jù)通常和記錄的其余部分分開存儲.
數(shù)據(jù)訪問方式
處理器(指令)訪問數(shù)據(jù)有幾個不同的方式, 稱之為尋址模式. 最簡單的一種是 立即尋址模式, 此時數(shù)據(jù)就包含字指令里頭. 例如, 如果我們想吧一個寄存器地值初始化設置為0, 我們可以使用立即模式, 給它個數(shù)值0, 而不用給它打地址, 然后從那里再讀一個0.
在 寄存器尋址模式, 指令包含要訪問的寄存器, 而不是內(nèi)存地址. 剩下的模式將是處理地址的.
在 直接尋址模式, 指令包含要訪問的內(nèi)存地址. 比如, 我可能說, 請把地址2002位置的數(shù)據(jù)加載的這個寄存器. 計算機就會直接到2002編號的存儲單元, 并把其中的內(nèi)容拷貝到寄存器.
在 變址尋址模式, 指令包含要訪問的內(nèi)存地址, 同時指定一個 變址寄存器 來做地址偏移. 例如, 我們可以指定地址2002和一個變址寄存器, 如果變址寄存器里的數(shù)是4, 實際的數(shù)據(jù)地址將是2006. 用這種方式, 如果你有從2002地址開始的一系列的數(shù), 你可以用變址寄存器來循環(huán)操作它們. 在x86處理器中, 你還可以指定一個乘法系數(shù)來計算偏移. 這能允許你一次訪問一個字節(jié)或者一個字(4字節(jié)). 如果你要訪問一個字, 對于某個元素, 你的偏移量需要乘以4才能得到準確的位置. 例如, 如果你要訪問2002開始的第4個字節(jié), 那么你要設置變址寄存器為3(記住, 我們從0開始計數(shù)), 乘法系數(shù)為1, 因為我們是單個字節(jié)訪問的. 這樣我們得到2005的位置. 但是, 如果你要訪問從2002開始的第4個字, 那么你要設置變址寄存器為3, 并設置乘法系數(shù)為4, 這樣得到的位置上2014, 第4個字. 花點時間自己計算一下, 確保你理解如何計算.
在 間接尋址模式, 指令包含一個寄存器, 而其中是個指針, 指向了數(shù)據(jù)所在位置. 例如, 我們使用間接尋址并制定 %eax 寄存器, 而%eax里的值是4, 那么內(nèi)存中編號為4的存儲單元, 用的就是那里的數(shù)據(jù), 不論里邊是什么. 在直接尋址中, 我們將只是加載數(shù)值4, 但在間接尋址中, 我們用4作為地址去找我們要的數(shù)據(jù).
最后, 還有 基址尋址模式. 這個和間接尋址類似, 但你同時使用一個稱為 偏移 的數(shù)來加到寄存器里的數(shù)值上, 然后再查詢. 本書中將大量使用這種模式.
在 解釋內(nèi)存 的章節(jié)中, 我們討論了用一個內(nèi)存中的結(jié)構來放置客戶信息. 現(xiàn)在假定我們要獲取客戶的年齡, 那是數(shù)據(jù)的第8個字節(jié), 而我們在寄存器中存儲了結(jié)構開始的地址. 我們可以用基址尋址, 指定那個寄存器作為基址, 以8為偏移. 這和變址尋址很像, 區(qū)別在于偏移量是常數(shù), 而地址放在寄存器里, 在變址尋址中, 偏移量在寄存器里而地址是常數(shù).
還有其他一些尋址方式, 但前面這些是最重要的.
2013-08-16