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