程序是怎樣跑起來的
時間開銷:
共計11h,273頁。
閱讀+隨手記
181112: 2h
181113: 2h
181114: 2.5h
181121: 1.5h
181126: 1.5h
181127: 0.5h
小計:10h
總結筆記
181127:1h
分章節筆記
00 序言
這本書原本2001年10月出版第1版,這是翻譯的原版第二版的內容,不過是2015年4月的譯版第1版。
整體來說,這本書更加偏向底層硬件,計算機組成原理+操作系統+匯編語言,這部分剛好更是自己不了解的地方,僅僅只是看目錄就可以看到很多地方都不是很懂了,有很多專業詞匯,雖然這本書(2001年)寫在那本書之前(2003年)但是其實可能更難理解。
全文共計12章+附錄,每章關鍵詞:CPU、二進制表示、小數運算、內存使用、內存和磁盤、壓縮、程序運行環境、代碼編譯、操作系統和應用、程序實際構成(匯編)、硬件控制、AI、C語言附錄。
看完之後的整體感覺章節與章節之間的取名or關聯性有點體現不出來,講解的都是一些比較小的點,如果要講解程序是怎麽跑起來的,可能可以考慮先從計算機的底層硬件開始講起,然後講解操作系統是怎麽屏蔽程序直接調用底層硬件控制的結構,最後上升到上層應用程序調用操作系統的接口,怎麽進行編譯成本地代碼然後運行的。——這麽一想,可能這本書的確是這麽考慮的,但是在章節名與章節名之間有點看不出來內部的關聯性,對初學者有點難以理解吧。
01 對程序員來說CPU是什麽
181112:1917:整體感覺這本書的理解程度比03年的計算機是如何跑起來的要難一些,專業性更強,很多地方的理解如果不是有一定基礎還真的不太好理解,這章就更偏向計算機組成原理那門課的那本書了。
CPU就是寄存器的集合,一共有8種寄存器,其中有5種寄存器只有一個,而其他3個寄存器則有多個,CPU能夠完成的功能其實很少,主要就是數據傳送、運算、跳轉、call/return這四種,實現了所有高級復雜的功能。
02 數據是用二進制數表示的
181112:2332:這章講解二進制比較關鍵的點其實是補數相關的操作,其他都還是比較容易理解。因為計算機做減法運算時,內部實際上在做加法運算,所以當需要表示負數時,如1的負數即-1,就是所謂1的補數,使用1的二進制數取反加1得到。當計算結果是負數的時候,如3-5,即3+(-5)的補數,計算出來的結果再取反加一求補數,就能夠得到絕對值的數。左移運算時直接低位補零即可,但是右移運算時,則需要把空白出來的高位補充成該數值原始最高位符號位的值,即-4右移之後,應該變成-1才對,11111111;同理在符號擴充也是,需要把符號位的數字,擴充到所有的高位中。剩下的就是進制與進制之間的轉換公式,以及加減乘除四則算術運算和與或非異或四個邏輯運算的東西,比較簡單了。
03 計算機進行小數運算時出錯的原因
181113:1444:二進制表示小數比表示整數復雜很多。先不考慮計算機內部的小數表示,先從紙質手寫上看二進制小數轉換到十進制小數的方法,方法和整數轉化類似,還是通過基數位權求和,小數點左邊的數字按照位權從0,1,2遞增,小數點右邊的按照位權-1,-2,-3遞減;十進制的小數轉換為二進制,則是小數部分乘以2,取整數部分依次從左往右放在小數點後,直到小數部分為0或者位數已經夠了。——其他進制轉換成十進制,其實都是采用基數位權求和的方法,只是不同進制,基數不同而已;而十進制轉換成其他進制,也是同理,除基數取余,然後倒序排列,高位補零;而如果是某種進制的小數的話,則是乘基數取整數,依次放到後面,直至小數部分為0或位數已夠。
然後考慮計算機內部的小數表示(最終都是二進制表示),無論單精度還是雙精度,都是用浮點數表示法,他倆的區別主要在於支持的位數不同,32(1+8+23)/64(1+11+52),分別是符號位+指數+尾數。浮點數表示有一個統一的規範,寫成十進制小數時,要遵循十進制小數點前面為0,小數點後面第1位不能為0的規則,而寫成二進制小數時,則要對原本的小數點為進行邏輯左移操作,直至小數點左邊第一位為1,左移了多少位,就是所謂的指數,而小數點右邊的數字則是尾數部分,不夠23/52位直接補0即可。由於二進制存儲的時候小數點左邊的第一個1沒有存進去,實際上二進制23/52位的尾數,可以表示十進制中小數點後24/53位的小數。指數部分采用了EXPRESS系統表現,即通過把能夠表示的範圍中間的那個數作為0,以消除符號位,比如說8位的指數部分,不考慮符號位,能夠正常表示0~255的數字,現在把11111111除以2,舍棄小數,得到1/2中間的數01111111,普通情況下其值表示127,在EXPRESS中認為其值為0,這樣從00000000-01111111就是-127~0的值了。
正因為二進制小數在計算機中如此保存,有一些在十進制中能夠表示的小數,在二進制中就無法表示,如0.1等,在二進制表示中能夠發現其是二進制中的無限循環小數,如1/3在10進制中也是無限循環小數一樣。
避免這種計算機計算錯誤的方式:
- 回避,即無視這些錯誤(可能就直接按需要位數進行截斷):在一些精度要求不高的場景下,只要得到近似值即可
- 把小數轉化成整數進行計算,先乘10^x把小數變大,然後算完之後再除回來
- 使用BSD方法,這裏沒有詳細介紹,但是說如果精度要求很高,則一定需要使用第二和第三種方法才行。
整體來說,進制之間的轉換一直沒有弄得很清楚,並且數字到底在計算機中如何存儲的也不是很明白,這章算是把之前沒有弄明白的地方做了很好的補充。
04 熟練使用有棱有角的內存
181113:1543:先簡單介紹了物理上的內存表現形式,通過地址信號、數據信號、控制信號來進行物理上的內存操作,數據信號的引腳個數,決定了一次性能夠讀入內存的數據大小範圍,地址信號引腳的個數,決定了內存中能夠指向的內存塊個數,兩者相乘得到的就是該內存IC的大小,多數情況下,地址信號引腳的個數會更多,使得內存容量更大。所謂指針也是一種變量,表示其值為內存地址的變量,所以指針變量的大小和CPU的內存地址位數一致,而該指針指向的數據類型無論是什麽,指針變量的大小都一樣,只是該指針能夠讀取的內存長度受數據類型的限制。後面介紹了邏輯上的內存操作,主要是基於數組的各種數據結構的介紹,和前一本書差不多,多了一個環狀緩沖區的內容,差別不大。
05 內存和磁盤的親密關系
181114:2034:動態鏈接和靜態鏈接的區別在於,一個是程序運行時才被調用加載到內存,一個是本身就編碼在程序中,程序一運行就已經被加載到內存了;扇區是磁盤保存數據的最小物理單位,但是簇是保存文件的最小邏輯單位,無論文件有多小,只要沒有超過1簇的大小,就必須要占有1簇的物理磁盤扇區位置,雖然看起來有點浪費,但是簇如果越小,存取大文件的時候就回需要更多的磁盤讀寫操作,降低運行速度,簇的大小是磁盤讀寫速度和磁盤存儲容量的平衡;程序運行必須要先從磁盤中加載到內存中,然後才能夠被CPU根據內存地址進行調用執行;磁盤緩存指把數據從磁盤中調用到內存中存儲,改善磁盤獲取數據的速度(預取),是假想的內存;虛擬內存則是把假想的磁盤,把磁盤的一部分當做內存來使用,實際運行中其實是在虛擬內存(磁盤)和真實內存之間做了數據的置換,把程序運行暫時沒有用到的放在虛擬內存中,真正運行的放在真實內存中;置換分為分頁式和分段式,windows和linux貌似都是分頁式;無論是磁盤緩存還是虛擬內存都是為了解決真實內存容量小的問題,但都是治標不治本,如果要從根本上解決還是只能增加內存,不過反過來也可以減少程序內存開銷;有兩種在編程時節約內存的方法(雖然現在磁盤、內存都越來越大),一個是善於使用動態鏈接庫,把公共使用的函數盡可能的抽象出來成dll;一個是使用C中_stdcall的思路,默認的棧調用後的清理程序由發起調用的函數執行,因為清理程序必須要知道函數的參數個數和類型,但如果是在函數前加這個符號,即可告訴被調用程序,該函數的參數和類型是不變的,即可讓被調用函數自己執行棧調用後的清理程序,從匯編語言上看,能夠節約3個字節的內存,積少成多還是不錯的。
06 親自嘗試壓縮數據
181114:2115:雖然感覺有點莫名其妙在這裏加了一章講解壓縮文件的東西,不過算是復習了下,之前學習信息論的時候就了解過信息的編碼及壓縮算法,圖像處理的時候也接觸過一些,還算是比較簡單;主要復習了下RLE行程長度編碼和哈夫曼編碼;以及可逆壓縮和非可逆壓縮,對於文本文件及一些EXE執行文件來說,只能使用可逆編碼,或者說,大多數情況下大家都是要求可逆編碼的,而對於一些圖像處理、圖像文件保存不同格式來說,即使非可逆壓縮損失了一些信息,只要肉眼分辨不出來也都是OK的;此外需要強調的一點是,所有的文件其實都是二進制數字保存在文件中。
07 程序是在何種環境中運行的
181114:2141:這一章簡單介紹了硬件(CPU)、(BIOS)、操作系統、應用之間的關系;不同的CPU,具有其獨有的機器語言,只能解釋運行它自己的那一種機器語言,所以不同的硬件需要不同的編譯器,把相同的源代碼編譯成不同的硬件能夠理解的(早期就存在基於不同硬件的專用應用)本地代碼;操作系統非常棒的地方在於,它屏蔽了各種不同硬件帶來的區別,雖然不同硬件也需要裝上同一種操作系統的不同專用版本,但是對於上層應用來說,它都是在同一種操作系統中進行運行的,已經省了很大一筆工夫,原本直接調用硬件接口的東西,現在都通過調用操作系統的API接口,由操作系統來間接調用了;同時,如果是類似FreeBSD這種提供ports移植源代碼機制的,從源碼倉庫中下載源碼,本地重新編譯得到本地代碼,可以說是很方便了,但是windows不是;同時,考慮到不同的硬件又可以安裝不同種類的操作系統,如Windows、Linux、MacOS等,同一個應用想要在不同種類的操作系統中進行運行,就還必須要針對每種操作系統開發不同的版本,因為每種操作系統提供的API不同(為什麽不能只存在一種操作系統呢?);這裏提供了兩種解決方案,一個是在操作系統A中安裝支持操作系統B的虛擬機,在虛擬機中直接安裝一個操作系統B,然後在操作系統B中運行特定版本的應用(多數使用Mac的都會裝一個支持Windows的虛擬機實例),一個是使用Java虛擬機環境;Java虛擬機環境和前面直接裝個虛擬機的區別在於,前面是直接虛擬了一個操作系統B,但是如果要在操作系統C中運行,還得再裝一個操作系統C的虛擬機實例,而Java不同,它提供的是一個程序運行環境的平臺(當然不同的操作系統需要安裝特定版本的Java虛擬機),它其實也是一個應用,運行在操作系統之上,屏蔽了前面不同種類操作系統帶來的API接口的不同,這樣如果一個應用的源碼想在不同的操作系統中都能夠運行的話,就在Java虛擬環境中進行編譯,它每次運行的時候把Java源碼先編譯成在Java虛擬環境能夠處理的字節代碼,然後JavaVM再把字節代碼處理成本地代碼,JavaVM雖然屏蔽了操作系統的區別,但是也有它的不足之處,首先是有些應用調用的API可能不能支持在所有的JavaVM中都能夠運行,其次由於每次都要生成字節代碼,運行速度受到了一定影響;不過也有一些對JavaVM的優化方式,比如第一次生成的本地代碼保存起來,以後直接運行,或者改善生成的本地代碼的質量優化耗時較長的部分;除此之外需要了解到BIOS這個程序,它是內置在計算機主機內部的程序,一開機就會啟動,它的作用是啟動“引導程序”,而引導程序的作用則是把操作系統加載到內存運行,然後操作系統才能夠開機運行支持其他應用的運行。
08 從原文件到可執行文件
181121:2307:這章雖然講得比較簡單入門,但是還是有一些東西是第一次才看到。之前知道的是,源碼編譯之後得到目標文件, 然後再經過鏈接得到最終的可執行文件,整個過程是沒有問題的,但是很多細節的地方沒有深入了解到。首先,源碼編譯之後得到的目標文件,其實已經是機器語言,即本地代碼,能夠被CPU解釋看懂的了;其次,僅僅只是經過編譯,得到的文件雖然CPU能夠看懂,但是卻不能加載到內存中運行,因為其中調用了其他的庫文件函數,需要把那些庫文件中的對應的函數本體包含在一起,才能夠運行;這裏有個問題是,如果任何外部函數都沒有調用,是不是只用編譯即可?這裏以一個編譯器舉例說,即使沒有調用任何庫函數,仍然需要鏈接,因為需要和編譯器本身提供的一個目標文件進行結合,即程序的啟動,它是同所有程序起始位置相結合的處理內容(這句話說實話沒看懂),gcc編譯的時候並沒有看到gcc有需要一個什麽啟動,或者已經被隱藏了沒有看到;然後鏈接時,之前只知道有靜態鏈接庫.lib和動態鏈接庫.dll/.so,這裏還講了一種庫文件,導入庫,後綴也還是.lib庫文件的後綴,但是它內部存儲的其實並不是函數本體,而是存儲著動態鏈接庫的索引位置,包括具體的這個函數需要的那一個庫文件以及整個動態鏈接庫在主機中存儲的位置,相當於“指針”的作用,編譯器根據它的索引,找到對應真正存儲調用函數本體的動態鏈接庫。所謂DUMP,就是把文件中的內容按照每個字節2位十六進制的數表示。決定不同編譯器的有3個關鍵點:編程語言、CPU型號、操作系統環境,這裏了解到一個比較新的概念是交叉編譯器,即能夠生成和運行環境CPU不同的CPU型號的本地代碼。編譯器通過對源代碼進行語法解析、句法解析、語義解析等,得到本地代碼,理解就是翻譯器。這裏想到一個問題:是不是可以通過變成同一種機器語言,來做到多種編程語言之間的轉換呢?用Java編寫的源碼編譯之後得到的本地代碼,通過某種反編譯C編譯器是否能夠自動生成C語言的代碼呢?後續可以調研一下。鏈接形成的EXE文件,是由再配置信息、變量組、函數組構成的,再配置信息保存的是轉換內存地址所必需的信息,因為EXE給變量和函數分配了虛擬的內存地址,但是實際上最終CPU分配的真實地址每次程序運行時都是不一樣的。再配置信息就是變量和函數的相對地址,其實就是相對於組基點的偏移量。簡單的說,鏈接後,調整了源碼中變量和函數的順序,把它們編成了變量組和函數組,然後在再配置信息中存儲著調整後的變量組和函數組的偏移量。EXE運行時則還會在內存中添加2種組,棧和堆,棧保存著函數調用時的局部變量,當函數調用完畢,其後續的內存回收機制則是由編譯器自動完成;而堆則是需要自己申請自己釋放,如果只申請不釋放就會導致內存泄漏,直至最終無內存可用,程序可能就會出現萬惡的segementation fault。分割編譯將多個源碼分別編譯,便於程序管理。編譯器和解釋器的區別在於,編譯器是在程序運行前對源碼整個一次性進行解釋處理,而解釋器則是在運行時對源碼一行一行的解釋處理。不鏈接導入庫也可以在程序運行調用dll文件中的函數,只要通過使用LoadLibrary()等API(之前用過Python加載C編譯完成的.so,就是使用的Python的一個加載函數接口API,而不是使用Python解釋器對.so進行的解釋,因為語言不通根本解釋不了)。疊加鏈接指把不會同時執行的函數交替加載到同一個地址中運行,需要使用疊加編譯器才行,這是為了節約內存,不知道當前是否還存在這種功能。C/C++都需要自己申請自己釋放的垃圾回收機制,而Java、C#等,程序運行環境會自動進行垃圾回收。
09 操作系統和應用的關系
181126:1958:這章比較短也比較簡單,主要強調了操作系統、應用、網絡數據庫等中間件之間的關系,大多數程序員編寫的都是應用,並不直接與計算機硬件進行交互,造作計算機硬件接口,而是由操作系統把底層與硬件的交互屏蔽,提供其系統調用的API,供高級編程語言進行調用和實現其自身應用的功能。WYSIWYG表示所見即所得。所謂監控程序可以說是操作系統的原型,OS是由它慢慢演化而來。設備驅動是新的設備連接到計算機時自動安裝的能夠控制設備的程序,也可以說是該設備提供給想要調用它的應用,可調用的API接口。後面對Windows操作系統的一些功能進行了介紹,如GUI/多任務等。這裏需要強調一點的是,GUI編程的邏輯和傳統的命令行程序不同,傳統的程序是由程序員決定程序的執行邏輯,用戶根據默認已經制定好的程序邏輯往下進行即可,而GUI的執行邏輯由用戶決定,用戶想要如何點擊按鈕,這個順序是不可控的,GUI編程則需要考慮這一點。
10 通過匯編語言了解程序的實際構成
181126:2033:整章用了個例子,詳細講解了程序過程中匯編語言的運行機制。本地代碼和匯編語言是一一對應的。匯編語言的語法是操作碼+操作數,即指令動作+指令對象。CPU中各種名字的寄存器的作用是不一樣的。函數的參數通過棧來傳遞,函數的返回值通過寄存器來返回。編譯器具有表示段定義的偽指令,偽指令負責把程序的構造及匯編的方法告訴匯編器(轉換程序)。後面對全局變量和局部變量簡單說明了一下,局部變量只在函數運行的時候保存在寄存器和棧上,CPU更傾向使用寄存器,因為它的處理速度更快。另外只對局部變量進行定義是不行的,一定要對局部變量進行賦值之後才會給局部變量分配寄存器。最後指出雖然C語言等建議不使用goto語句進行跳轉,但是在匯編語言這一層級,它必須使用類似goto語句的跳轉指令,才能夠實現循環和條件分支的功能。最最後說了下了解程序底層運行機制的必要性,知其然,更知其所以然。
11 硬件控制方法
181126:2336:匯編語言中利用IN/OUT指令對硬件的輸出輸出進行控制;而外圍設備通過中斷請求IRQ來中斷CPU的操作,外圍設備的中斷端口與其他I/O設備不同,叫中斷編號;利用DMA功能,磁盤等存儲大量數據的外圍設備能夠不經過CPU直接同主內存進行數據傳輸,省去了CPU切換處理的時間;顯示器中顯示的信息一直存儲在VRAM內存中,以前VRAM是作為主存的一部分,而現在,顯卡中一般配備與主存獨立的VRAM和GPU專門用來做圖形圖像處理。感覺這章內容還挺新鮮,但是也沒講特別深的東西,挺容易理解。
12 讓計算機“思考”
181127:0916:這一章有點神奇,循序漸進的通過實現猜拳遊戲的例子,完成了程序實現人類思考方式的模擬。先是把人類思考的習慣添加進去,使得程序習慣性的喜歡出某一種拳;然後采用隨機數,讓計算機隨機出拳(這裏簡單介紹了隨機數和偽隨機數,以及隨機數種子,舉例線性同余法作為生成偽隨機數的算法),把直覺嵌入;以及活用計算機的記憶功能,把經驗嵌入進去,提高出拳勝率;最後還把思考方式,作為思考方法的節奏給嵌入進去,當連輸兩局的時候就換一種思考方式提高勝率。整體來說猜拳遊戲比較簡單,涉及了一點點AI的皮毛,跟AlphaGo那種比不了,不過還是有點點啟發,直覺、習慣、經驗、思考方式,當前的計算機有思考功能嗎?感覺是有的,只是還沒有自我意誌。
附錄:讓我們開始C語言之旅
這裏只是簡單的介紹了下C語言的基礎知識,如果要入門的話,還是得看一本系統一點專門講解C語言入門的書比較好。
全書總結:
181127:1004:一共花了10h看完這本書,但是時間上跨度有兩周。整體來看這本書一些章節構造不是很能理解為何要這樣寫。比如第2章和第三章,主要講解了二進制數以及其涉及到的二進制計算的問題,但是和整體的程序運行機制貌似沒有很直接的關聯,或許只要提一句計算機中的任何文件都是由二進制表示的即可?感覺缺了這兩章也沒有太大影響。另外比如第6章講解壓縮的部分,完全不明白為什麽要突然轉頭說到壓縮,感覺和程序怎麽跑起來也沒有什麽太大關系,壓縮和內存or數據保存有一定關聯但是好像刪掉這一張也沒有太大影響。剩下的第1/4/5/7/8/9/10/11中,7章之前的內容算是計算機基礎環境,有助於後面的理解,第7章之後的內容就直接和標題相關了,程序是怎麽運行的,是怎麽生成可執行文件的,怎麽在操作系統的屏蔽下調用硬件的,第10章程序的實際構成感覺應該放在可執行文件之後or之前講解比較順暢,anyway。最後第12章算是一個程序例子,說明程序是怎麽用的,怎麽表示人類的思考方式的。
這本書封面說它自己是“計算機組成原理”圖解趣味版,不過也有涉及到其他方面的知識。對個人來說,第2和第3章中對二進制的介紹又深入理解了一下,第5章的動靜態鏈接,第7章的計算機從底層硬件到上層應用的關系,第8章編譯的過程等還是挺有收獲的。下一本是這個系列的最後一本了,網絡是怎麽連接的,這本應該看起來更快一些。
暫定閱讀計劃順序:
- 網絡是怎樣連接的(這三本書是同一個系列的,總是成套賣,還是比較經典的,想一次性看完)
- 程序員的自我修養——鏈接、裝載與庫(很想把編譯這塊沒有了解的弄明白)
- 編譯原理(這本書感覺會比上面這本難,所以先看上面這本打個底)
- 程序員的數學(看完編譯原理看這個放松一下)
- 操作系統概念(算是復習一下)
- 七周七語言:理解多種編程範型(接觸過的編程語言也算是有一些了,可能現在看這本書會比較有感覺了)
- 多核編程入門(之前一直不太了解多核、並行、分布式這堆東西,想看看了解下)
程序是怎樣跑起來的