1. 程式人生 > >程式設計師不得不瞭解的硬核知識大全

程式設計師不得不瞭解的硬核知識大全

我們每個程式設計師或許都有一個夢,那就是成為大牛,我們或許都沉浸在各種框架中,以為框架就是一切,以為應用層才是最重要的,你錯了。在當今計算機行業中,會應用是基本素質,如果你懂其原理才能讓你在行業中走的更遠,而計算機基礎知識又是重中之重。下面,跟隨我的腳步,為你介紹一下計算機底層知識。

CPU

還不瞭解 CPU 嗎?現在就帶你瞭解一下 CPU 是什麼

CPU 的全稱是 Central Processing Unit,它是你的電腦中最硬核的元件,這種說法一點不為過。CPU 是能夠讓你的計算機叫計算機的核心元件,但是它卻不能代表你的電腦,CPU 與計算機的關係就相當於大腦和人的關係。CPU 的核心是從程式或應用程式獲取指令並執行計算。此過程可以分為三個關鍵階段:提取,解碼和執行。CPU從系統的主存中提取指令,然後解碼該指令的實際內容,然後再由 CPU 的相關部分執行該指令。

CPU 內部處理過程

下圖展示了一般程式的執行流程(以 C 語言為例),可以說了解程式的執行流程是掌握程式執行機制的基礎和前提。

在這個流程中,CPU 負責的就是解釋和執行最終轉換成機器語言的內容。

CPU 主要由兩部分構成:控制單元算術邏輯單元(ALU)

  • 控制單元:從記憶體中提取指令並解碼執行
  • 算數邏輯單元(ALU):處理算數和邏輯運算

CPU 是計算機的心臟和大腦,它和記憶體都是由許多電晶體組成的電子部件。它接收資料輸入,執行指令並處理資訊。它與輸入/輸出(I / O)裝置進行通訊,這些裝置向 CPU 傳送資料和從 CPU 接收資料。

從功能來看,CPU 的內部由暫存器、控制器、運算器和時鐘四部分組成,各部分之間通過電訊號連通。

  • 暫存器是中央處理器內的組成部分。它們可以用來暫存指令、資料和地址。可以將其看作是記憶體的一種。根據種類的不同,一個 CPU 內部會有 20 - 100個暫存器。
  • 控制器負責把記憶體上的指令、資料讀入暫存器,並根據指令的結果控制計算機
  • 運算器負責運算從記憶體中讀入暫存器的資料
  • 時鐘 負責發出 CPU 開始計時的時鐘訊號

CPU 是一系列暫存器的集合體

在 CPU 的四個結構中,我們程式設計師只需要瞭解暫存器就可以了,其餘三個不用過多關注,為什麼這麼說?因為程式是把暫存器作為物件來描述的。

不同型別的 CPU ,其內部暫存器的種類,數量以及暫存器儲存的數值範圍都是不同的。不過,根據功能的不同,可以將暫存器劃分為下面這幾類

種類 功能
累加暫存器 儲存執行的資料和運算後的資料。
標誌暫存器 用於反應處理器的狀態和運算結果的某些特徵以及控制指令的執行。
程式計數器 程式計數器是用於存放下一條指令所在單元的地址的地方。
基址暫存器 儲存資料記憶體的起始位置
變址暫存器 儲存基址暫存器的相對地址
通用暫存器 儲存任意資料
指令暫存器 儲存正在被執行的指令,CPU內部使用,程式設計師無法對該暫存器進行讀寫
棧暫存器 儲存棧區域的起始位置

其中程式計數器、累加暫存器、標誌暫存器、指令暫存器和棧暫存器都只有一個,其他暫存器一般有多個。

下面就對各個暫存器進行說明

程式計數器

程式計數器(Program Counter)是用來儲存下一條指令所在單元的地址。

程式執行時,PC的初值為程式第一條指令的地址,在順序執行程式時,控制器首先按程式計數器所指出的指令地址從記憶體中取出一條指令,然後分析和執行該指令,同時將PC的值加1指向下一條要執行的指令。

我們還是以一個事例為準來詳細的看一下程式計數器的執行過程

這是一段進行相加的操作,程式啟動,在經過編譯解析後會由作業系統把硬碟中的程式複製到記憶體中,示例中的程式是將 123 和 456 執行相加操作,並將結果輸出到顯示器上。

地址 0100 是程式執行的起始位置。Windows 等作業系統把程式從硬碟複製到記憶體後,會將程式計數器作為設定為起始位置 0100,然後執行程式,每執行一條指令後,程式計數器的數值會增加1(或者直接指向下一條指令的地址),然後,CPU 就會根據程式計數器的數值,從記憶體中讀取命令並執行,也就是說,程式計數器控制著程式的流程。

條件分支和迴圈機制

高階語言中的條件控制流程主要分為三種:順序執行、條件分支、迴圈判斷三種,順序執行是按照地址的內容順序的執行指令。條件分支是根據條件執行任意地址的指令。迴圈是重複執行同一地址的指令。

  • 順序執行的情況比較簡單,每執行一條指令程式計數器的值就是 + 1。
  • 條件和迴圈分支會使程式計數器的值指向任意的地址,這樣一來,程式便可以返回到上一個地址來重複執行同一個指令,或者跳轉到任意指令。

下面以條件分支為例來說明程式的執行過程(迴圈也很相似)

程式的開始過程和順序流程是一樣的,CPU 從0100處開始執行命令,在0100和0101都是順序執行,PC 的值順序+1,執行到0102地址的指令時,判斷0106暫存器的數值大於0,跳轉(jump)到0104地址的指令,將數值輸出到顯示器中,然後結束程式,0103 的指令被跳過了,這就和我們程式中的 if() 判斷是一樣的,在不滿足條件的情況下,指令會直接跳過。所以 PC 的執行過程也就沒有直接+1,而是下一條指令的地址。

標誌暫存器

條件和迴圈分支會使用到 jump(跳轉指令),會根據當前的指令來判斷是否跳轉,上面我們提到了標誌暫存器,無論當前累加暫存器的運算結果是正數、負數還是零,標誌暫存器都會將其儲存

CPU 在進行運算時,標誌暫存器的數值會根據當前運算的結果自動設定,運算結果的正、負和零三種狀態由標誌暫存器的三個位表示。標誌暫存器的第一個位元組位、第二個位元組位、第三個位元組位各自的結果都為1時,分別代表著正數、零和負數。

CPU 的執行機制比較有意思,假設累加暫存器中儲存的 XXX 和通用暫存器中儲存的 YYY 做比較,執行比較的背後,CPU 的運算機制就會做減法運算。而無論減法運算的結果是正數、零還是負數,都會儲存到標誌暫存器中。結果為正表示 XXX 比 YYY 大,結果為零表示 XXX 和 YYY 相等,結果為負表示 XXX 比 YYY 小。程式比較的指令,實際上是在 CPU 內部做減法運算。

函式呼叫機制

接下來,我們繼續介紹函式呼叫機制,哪怕是高階語言編寫的程式,函式呼叫處理也是通過把程式計數器的值設定成函式的儲存地址來實現的。函式執行跳轉指令後,必須進行返回處理,單純的指令跳轉沒有意義,下面是一個實現函式跳轉的例子

圖中將變數 a 和 b 分別賦值為 123 和 456 ,呼叫 MyFun(a,b) 方法,進行指令跳轉。圖中的地址是將 C 語言編譯成機器語言後執行時的地址,由於1行 C 程式在編譯後通常會變為多行機器語言,所以圖中的地址是分散的。在執行完 MyFun(a,b)指令後,程式會返回到 MyFun(a,b) 的下一條指令,CPU 繼續執行下面的指令。

函式的呼叫和返回很重要的兩個指令是 callreturn 指令,再將函式的入口地址設定到程式計數器之前,call 指令會把呼叫函式後要執行的指令地址儲存在名為棧的主存內。函式處理完畢後,再通過函式的出口來執行 return 指令。return 指令的功能是把儲存在棧中的地址設定到程式計數器。MyFun 函式在被呼叫之前,0154 地址儲存在棧中,MyFun 函式處理完成後,會把 0154 的地址儲存在程式計數器中。這個呼叫過程如下

在一些高階語言的條件或者迴圈語句中,函式呼叫的處理會轉換成 call 指令,函式結束後的處理則會轉換成 return 指令。

通過地址和索引實現陣列

接下來我們看一下基址暫存器和變址暫存器,通過這兩個暫存器,我們可以對主存上的特定區域進行劃分,來實現類似陣列的操作,首先,我們用十六進位制數將計算機記憶體上的 00000000 - FFFFFFFF 的地址劃分出來。那麼,凡是該範圍的記憶體地址,只要有一個 32 位的暫存器,便可檢視全部地址。但如果想要想陣列那樣分割特定的記憶體區域以達到連續檢視的目的的話,使用兩個暫存器會更加方便。

例如,我們用兩個暫存器(基址暫存器和變址暫存器)來表示記憶體的值

這種表示方式很類似陣列的構造,陣列是指同樣長度的資料在記憶體中進行連續排列的資料構造。用陣列名錶示陣列全部的值,通過索引來區分陣列的各個資料元素,例如: a[0] - a[4],[]內的 0 - 4 就是陣列的下標。

CPU 指令執行過程

幾乎所有的馮·諾伊曼型計算機的CPU,其工作都可以分為5個階段:取指令、指令譯碼、執行指令、訪存取數、結果寫回。

  • 取指令階段是將記憶體中的指令讀取到 CPU 中暫存器的過程,程式暫存器用於儲存下一條指令所在的地址
  • 指令譯碼階段,在取指令完成後,立馬進入指令譯碼階段,在指令譯碼階段,指令譯碼器按照預定的指令格式,對取回的指令進行拆分和解釋,識別區分出不同的指令類別以及各種獲取運算元的方法。
  • 執行指令階段,譯碼完成後,就需要執行這一條指令了,此階段的任務是完成指令所規定的各種操作,具體實現指令的功能。
  • 訪問取數階段,根據指令的需要,有可能需要從記憶體中提取資料,此階段的任務是:根據指令地址碼,得到運算元在主存中的地址,並從主存中讀取該運算元用於運算。
  • 結果寫回階段,作為最後一個階段,結果寫回(Write Back,WB)階段把執行指令階段的執行結果資料“寫回”到某種儲存形式:結果資料經常被寫到CPU的內部暫存器中,以便被後續的指令快速地存取;

記憶體

CPU 和 記憶體就像是一堆不可分割的戀人一樣,是無法拆散的一對兒,沒有記憶體,CPU 無法執行程式指令,那麼計算機也就失去了意義;只有記憶體,無法執行指令,那麼計算機照樣無法執行。

那麼什麼是記憶體呢?記憶體和 CPU 如何進行互動?下面就來介紹一下

什麼是記憶體

記憶體(Memory)是計算機中最重要的部件之一,它是程式與CPU進行溝通的橋樑。計算機中所有程式的執行都是在記憶體中進行的,因此記憶體對計算機的影響非常大,記憶體又被稱為主存,其作用是存放 CPU 中的運算資料,以及與硬碟等外部儲存裝置交換的資料。只要計算機在執行中,CPU 就會把需要運算的資料調到主存中進行運算,當運算完成後CPU再將結果傳送出來,主存的執行也決定了計算機的穩定執行。

記憶體的物理結構

記憶體的內部是由各種 IC 電路組成的,它的種類很龐大,但是其主要分為三種儲存器

  • 隨機儲存器(RAM): 記憶體中最重要的一種,表示既可以從中讀取資料,也可以寫入資料。當機器關閉時,記憶體中的資訊會 丟失
  • 只讀儲存器(ROM):ROM 一般只能用於資料的讀取,不能寫入資料,但是當機器停電時,這些資料不會丟失。
  • 快取記憶體(Cache):Cache 也是我們經常見到的,它分為一級快取(L1 Cache)、二級快取(L2 Cache)、三級快取(L3 Cache)這些資料,它位於記憶體和 CPU 之間,是一個讀寫速度比記憶體更快的儲存器。當 CPU 向記憶體寫入資料時,這些資料也會被寫入快取記憶體中。當 CPU 需要讀取資料時,會直接從快取記憶體中直接讀取,當然,如需要的資料在Cache中沒有,CPU會再去讀取記憶體中的資料。

記憶體 IC 是一個完整的結構,它內部也有電源、地址訊號、資料訊號、控制訊號和用於定址的 IC 引腳來進行資料的讀寫。下面是一個虛擬的 IC 引腳示意圖

圖中 VCC 和 GND 表示電源,A0 - A9 是地址訊號的引腳,D0 - D7 表示的是控制訊號、RD 和 WR 都是好控制訊號,我用不同的顏色進行了區分,將電源連線到 VCC 和 GND 後,就可以對其他引腳傳遞 0 和 1 的訊號,大多數情況下,+5V 表示1,0V 表示 0。

我們都知道記憶體是用來儲存資料,那麼這個記憶體 IC 中能儲存多少資料呢?D0 - D7 表示的是資料訊號,也就是說,一次可以輸入輸出 8 bit = 1 byte 的資料。A0 - A9 是地址訊號共十個,表示可以指定 00000 00000 - 11111 11111 共 2 的 10次方 = 1024個地址。每個地址都會存放 1 byte 的資料,因此我們可以得出記憶體 IC 的容量就是 1 KB。

記憶體的讀寫過程

讓我們把關注點放在記憶體 IC 對資料的讀寫過程上來吧!我們來看一個對記憶體IC 進行資料寫入和讀取的模型

來詳細描述一下這個過程,假設我們要向記憶體 IC 中寫入 1byte 的資料的話,它的過程是這樣的:

  • 首先給 VCC 接通 +5V 的電源,給 GND 接通 0V 的電源,使用 A0 - A9 來指定資料的儲存場所,然後再把資料的值輸入給 D0 - D7 的資料訊號,並把 WR(write)的值置為 1,執行完這些操作後,即可以向記憶體 IC 寫入資料
  • 讀出資料時,只需要通過 A0 - A9 的地址訊號指定資料的儲存場所,然後再將 RD 的值置為 1 即可。
  • 圖中的 RD 和 WR 又被稱為控制訊號。其中當WR 和 RD 都為 0 時,無法進行寫入和讀取操作。

記憶體的現實模型

為了便於記憶,我們把記憶體模型對映成為我們現實世界的模型,在現實世界中,記憶體的模型很想我們生活的樓房。在這個樓房中,1層可以儲存一個位元組的資料,樓層號就是地址,下面是記憶體和樓層整合的模型圖

我們知道,程式中的資料不僅只有數值,還有資料型別的概念,從記憶體上來看,就是佔用記憶體大小(佔用樓層數)的意思。即使物理上強制以 1 個位元組為單位來逐一讀寫資料的記憶體,在程式中,通過指定其資料型別,也能實現以特定位元組數為單位來進行讀寫。

二進位制

我們都知道,計算機的底層都是使用二進位制資料進行資料流傳輸的,那麼為什麼會使用二進位制表示計算機呢?或者說,什麼是二進位制數呢?在拓展一步,如何使用二進位制進行加減乘除?下面就來看一下

什麼是二進位制數

那麼什麼是二進位制數呢?為了說明這個問題,我們先把 00100111 這個數轉換為十進位制數看一下,二進位制數轉換為十進位制數,直接將各位置上的值 * 位權即可,那麼我們將上面的數值進行轉換

也就是說,二進位制數代表的 00100111 轉換成十進位制就是 39,這個 39 並不是 3 和 9 兩個數字連著寫,而是 3 * 10 + 9 * 1,這裡面的 10 , 1 就是位權,以此類推,上述例子中的位權從高位到低位依次就是 7 6 5 4 3 2 1 0。這個位權也叫做次冪,那麼最高位就是2的7次冪,2的6次冪 等等。二進位制數的運算每次都會以2為底,這個2 指得就是基數,那麼十進位制數的基數也就是 10 。在任何情況下位權的值都是 數的位數 - 1,那麼第一位的位權就是 1 - 1 = 0, 第二位的位權就睡 2 - 1 = 1,以此類推。

那麼我們所說的二進位制數其實就是 用0和1兩個數字來表示的數,它的基數為2,它的數值就是每個數的位數 * 位權再求和得到的結果,我們一般來說數值指的就是十進位制數,那麼它的數值就是 3 * 10 + 9 * 1 = 39。

移位運算和乘除的關係

在瞭解過二進位制之後,下面我們來看一下二進位制的運算,和十進位制數一樣,加減乘除也適用於二進位制數,只要注意逢 2 進位即可。二進位制數的運算,也是計算機程式所特有的運算,因此瞭解二進位制的運算是必須要掌握的。

首先我們來介紹移位 運算,移位運算是指將二進位制的數值的各個位置上的元素坐左移和右移操作,見下圖

補數

剛才我們沒有介紹右移的情況,是因為右移之後空出來的高位數值,有 0 和 1 兩種形式。要想區分什麼時候補0什麼時候補1,首先就需要掌握二進位制數表示負數的方法。

二進位制數中表示負數值時,一般會把最高位作為符號來使用,因此我們把這個最高位當作符號位。 符號位是 0 時表示正數,是 1 時表示 負數。那麼 -1 用二進位制數該如何表示呢?可能很多人會這麼認為: 因為 1 的二進位制數是 0000 0001,最高位是符號位,所以正確的表示 -1 應該是 1000 0001,但是這個答案真的對嗎?

計算機世界中是沒有減法的,計算機在做減法的時候其實就是在做加法,也就是用加法來實現的減法運算。比如 100 - 50 ,其實計算機來看的時候應該是 100 + (-50),為此,在表示負數的時候就要用到二進位制補數,補數就是用正數來表示的負數。

為了獲得補數,我們需要將二進位制的各數位的數值全部取反,然後再將結果 + 1 即可,先記住這個結論,下面我們來演示一下。

具體來說,就是需要先獲取某個數值的二進位制數,然後對二進位制數的每一位做取反操作(0 ---> 1 , 1 ---> 0),最後再對取反後的數 +1 ,這樣就完成了補數的獲取。

補數的獲取,雖然直觀上不易理解,但是邏輯上卻非常嚴謹,比如我們來看一下 1 - 1 的這個過程,我們先用上面的這個 1000 0001(它是1的補數,不知道的請看上文,正確性先不管,只是用來做一下計算)來表示一下

奇怪,1 - 1 會變成 130 ,而不是0,所以可以得出結論 1000 0001 表示 -1 是完全錯誤的。

那麼正確的該如何表示呢?其實我們上面已經給出結果了,那就是 1111 1111,來論證一下它的正確性

我們可以看到 1 - 1 其實實際上就是 1 + (-1),對 -1 進行上面的取反 + 1 後變為 1111 1111, 然後與 1 進行加法運算,得到的結果是九位的 1 0000 0000,結果發生了溢位,計算機會直接忽略掉溢位位,也就是直接拋掉 最高位 1 ,變為 0000 0000。也就是 0,結果正確,所以 1111 1111 表示的就是 -1 。

所以負數的二進位制表示就是先求其補數,補數的求解過程就是對原始數值的二進位制數各位取反,然後將結果 + 1。

算數右移和邏輯右移的區別

在瞭解完補數後,我們重新考慮一下右移這個議題,右移在移位後空出來的最高位有兩種情況 0 和 1

將二進位制數作為帶符號的數值進行右移運算時,移位後需要在最高位填充移位前符號位的值( 0 或 1)。這就被稱為算數右移。如果數值使用補數表示的負數值,那麼右移後在空出來的最高位補 1,就可以正確的表示 1/2,1/4,1/8等的數值運算。如果是正數,那麼直接在空出來的位置補 0 即可。

下面來看一個右移的例子。將 -4 右移兩位,來各自看一下移位示意圖

如上圖所示,在邏輯右移的情況下, -4 右移兩位會變成 63, 顯然不是它的 1/4,所以不能使用邏輯右移,那麼算數右移的情況下,右移兩位會變為 -1,顯然是它的 1/4,故而採用算數右移。

那麼我們可以得出來一個結論:左移時,無論是圖形還是數值,移位後,只需要將低位補 0 即可;右移時,需要根據情況判斷是邏輯右移還是算數右移。

下面介紹一下符號擴充套件:將資料進行符號擴充套件是為了產生一個位數加倍、但數值大小不變的結果,以滿足有些指令對運算元位數的要求,例如倍長於除數的被除數,再如將資料位數加長以減少計算過程中的誤差。

以8位二進位制為例,符號擴充套件就是指在保持值不變的前提下將其轉換成為16位和32位的二進位制數。將0111 1111這個正的 8位二進位制數轉換成為 16位二進位制數時,很容易就能夠得出0000 0000 0111 1111這個正確的結果,但是像 1111 1111這樣的補數來表示的數值,該如何處理?直接將其表示成為1111 1111 1111 1111就可以了。也就是說,不管正數還是補數表示的負數,只需要將 0 和 1 填充高位即可。

記憶體和磁碟的關係

我們大家知道,計算機的五大基礎部件是 儲存器控制器運算器輸入和輸出裝置,其中從儲存功能的角度來看,可以把儲存器分為記憶體磁碟,我們上面介紹過記憶體,下面就來介紹一下磁碟以及磁碟和記憶體的關係

程式不讀入記憶體就無法執行

計算機最主要的儲存部件是記憶體和磁碟。磁碟中儲存的程式必須載入到記憶體中才能執行,在磁碟中儲存的程式是無法直接執行的,這是因為負責解析和執行程式內容的 CPU 是需要通過程式計數器來指定記憶體地址從而讀出程式指令的。

磁碟構造

磁碟快取

我們上面提到,磁碟往往和記憶體是互利共生的關係,相互協作,彼此持有良好的合作關係。每次記憶體都需要從磁碟中讀取資料,必然會讀到相同的內容,所以一定會有一個角色負責儲存我們經常需要讀到的內容。 我們大家做軟體的時候經常會用到快取技術,那麼硬體層面也不例外,磁碟也有快取,磁碟的快取叫做磁碟快取

磁碟快取指的是把從磁碟中讀出的資料儲存到記憶體的方式,這樣一來,當接下來需要讀取相同的內容時,就不會再通過實際的磁碟,而是通過磁碟快取來讀取。某一種技術或者框架的出現勢必要解決某種問題的,那麼磁碟快取就大大改善了磁碟訪問的速度。

虛擬記憶體

虛擬記憶體是記憶體和磁碟互動的第二個媒介。虛擬記憶體是指把磁碟的一部分作為假想記憶體來使用。這與磁碟快取是假想的磁碟(實際上是記憶體)相對,虛擬記憶體是假想的記憶體(實際上是磁碟)。

虛擬記憶體是計算機系統記憶體管理的一種技術。它使得應用程式認為它擁有連續可用的記憶體(一個完整的地址空間),但是實際上,它通常被分割成多個物理碎片,還有部分儲存在外部磁碟管理器上,必要時進行資料交換。

通過藉助虛擬記憶體,在記憶體不足時仍然可以執行程式。例如,在只剩 5MB 記憶體空間的情況下仍然可以執行 10MB 的程式。由於 CPU 只能執行載入到記憶體中的程式,因此,虛擬記憶體的空間就需要和記憶體中的空間進行置換(swap),然後執行程式。

虛擬記憶體與記憶體的交換方式

虛擬記憶體的方法有分頁式分段式 兩種。Windows 採用的是分頁式。該方式是指在不考慮程式構造的情況下,把執行的程式按照一定大小的頁進行分割,並以為單位進行置換。在分頁式中,我們把磁碟的內容讀到記憶體中稱為 Page In,把記憶體的內容寫入磁碟稱為 Page Out。Windows 計算機的頁大小為 4KB ,也就是說,需要把應用程式按照 4KB 的頁來進行切分,以頁(page)為單位放到磁碟中,然後進行置換。

為了實現記憶體功能,Windows 在磁碟上提供了虛擬記憶體使用的檔案(page file,頁檔案)。該檔案由 Windows 生成和管理,檔案的大小和虛擬記憶體大小相同,通常大小是記憶體的 1 - 2 倍。

磁碟的物理結構

之前我們介紹了CPU、記憶體的物理結構,現在我們來介紹一下磁碟的物理結構。磁碟的物理結構指的是磁碟儲存資料的形式。

磁碟是通過其物理表面劃分成多個空間來使用的。劃分的方式有兩種:可變長方式扇區方式。前者是將物理結構劃分成長度可變的空間,後者是將磁碟結構劃分為固定長度的空間。一般 Windows 所使用的硬碟和軟盤都是使用扇區這種方式。扇區中,把磁碟表面分成若干個同心圓的空間就是 磁軌,把磁軌按照固定大小的儲存空間劃分而成的就是 扇區

扇區是對磁碟進行物理讀寫的最小單位。Windows 中使用的磁碟,一般是一個扇區 512 個位元組。不過,Windows 在邏輯方面對磁碟進行讀寫的單位是扇區整數倍簇。根據磁碟容量不同功能,1簇可以是 512 位元組(1 簇 = 1扇區)、1KB(1簇 = 2扇區)、2KB、4KB、8KB、16KB、32KB( 1 簇 = 64 扇區)。簇和扇區的大小是相等的。

壓縮演算法

我們想必都有過壓縮解壓縮檔案的經歷,當檔案太大時,我們會使用檔案壓縮來降低檔案的佔用空間。比如微信上傳檔案的限制是100 MB,我這裡有個資料夾無法上傳,但是我解壓完成後的檔案一定會小於 100 MB,那麼我的檔案就可以上傳了。

此外,我們把相機拍完的照片儲存到計算機上的時候,也會使用壓縮演算法進行檔案壓縮,檔案壓縮的格式一般是JPEG

那麼什麼是壓縮演算法呢?壓縮演算法又是怎麼定義的呢?在認識演算法之前我們需要先了解一下檔案是如何儲存的

檔案儲存

檔案是將資料儲存在磁碟等儲存媒介的一種形式。程式檔案中最基本的儲存資料單位是位元組。檔案的大小不管是 xxxKB、xxxMB等來表示,就是因為檔案是以位元組 B = Byte 為單位來儲存的。

檔案就是位元組資料的集合。用 1 位元組(8 位)表示的位元組資料有 256 種,用二進位制表示的話就是 0000 0000 - 1111 1111 。如果檔案中儲存的資料是文字,那麼該檔案就是文字檔案。如果是圖形,那麼該檔案就是影象檔案。在任何情況下,檔案中的位元組數都是連續儲存的。

壓縮演算法的定義

上面介紹了檔案的集合體其實就是一堆位元組資料的集合,那麼我們就可以來給壓縮演算法下一個定義。

壓縮演算法(compaction algorithm)指的就是資料壓縮的演算法,主要包括壓縮和還原(解壓縮)的兩個步驟。

其實就是在不改變原有檔案屬性的前提下,降低檔案位元組空間和佔用空間的一種演算法。

根據壓縮演算法的定義,我們可將其分成不同的型別:

有損和無損

無失真壓縮:能夠無失真地從壓縮後的資料重構,準確地還原原始資料。可用於對資料的準確性要求嚴格的場合,如可執行檔案和普通檔案的壓縮、磁碟的壓縮,也可用於多媒體資料的壓縮。該方法的壓縮比較小。如差分編碼、RLE、Huffman編碼、LZW編碼、算術編碼。

有失真壓縮:有失真,不能完全準確地恢復原始資料,重構的資料只是原始資料的一個近似。可用於對資料的準確性要求不高的場合,如多媒體資料的壓縮。該方法的壓縮比較大。例如預測編碼、音感編碼、分形壓縮、小波壓縮、JPEG/MPEG。

對稱性

如果編解碼演算法的複雜性和所需時間差不多,則為對稱的編碼方法,多數壓縮演算法都是對稱的。但也有不對稱的,一般是編碼難而解碼容易,如 Huffman 編碼和分形編碼。但用於密碼學的編碼方法則相反,是編碼容易,而解碼則非常難。

幀間與幀內

在視訊編碼中會同時用到幀內與幀間的編碼方法,幀內編碼是指在一幀影象內獨立完成的編碼方法,同靜態影象的編碼,如 JPEG;而幀間編碼則需要參照前後幀才能進行編解碼,並在編碼過程中考慮對幀之間的時間冗餘的壓縮,如 MPEG。

實時性

在有些多媒體的應用場合,需要實時處理或傳輸資料(如現場的數字錄音和錄影、播放MP3/RM/VCD/DVD、視訊/音訊點播、網路現場直播、可視電話、視訊會議),編解碼一般要求延時 ≤50 ms。這就需要簡單/快速/高效的演算法和高速/複雜的CPU/DSP晶片。

分級處理

有些壓縮演算法可以同時處理不同解析度、不同傳輸速率、不同質量水平的多媒體資料,如JPEG2000、MPEG-2/4。

這些概念有些抽象,主要是為了讓大家瞭解一下壓縮演算法的分類,下面我們就對具體的幾種常用的壓縮演算法來分析一下它的特點和優劣

幾種常用壓縮演算法的理解

RLE 演算法的機制

接下來就讓我們正式看一下檔案的壓縮機制。首先讓我們來嘗試對 AAAAAABBCDDEEEEEF 這 17 個半形字元的檔案(文字檔案)進行壓縮。雖然這些文字沒有什麼實際意義,但是很適合用來描述 RLE 的壓縮機制。

由於半形字元(其實就是英文字元)是作為 1 個位元組儲存在檔案中的,所以上述的檔案的大小就是 17 位元組。如圖

那麼,如何才能壓縮該檔案呢?大家不妨也考慮一下,只要是能夠使檔案小於 17 位元組,我們可以使用任何壓縮演算法。

最顯而易見的一種壓縮方式我覺得你已經想到了,就是把相同的字元去重化,也就是 字元 * 重複次數 的方式進行壓縮。所以上面檔案壓縮後就會變成下面這樣

從圖中我們可以看出,AAAAAABBCDDEEEEEF 的17個字元成功被壓縮成了 A6B2C1D2E5F1 的12個字元,也就是 12 / 17 = 70%,壓縮比為 70%,壓縮成功了。

像這樣,把檔案內容用 資料 * 重複次數 的形式來表示的壓縮方法成為 RLE(Run Length Encoding, 行程長度編碼) 演算法。RLE 演算法是一種很好的壓縮方法,經常用於壓縮傳真的影象等。因為影象檔案的本質也是位元組資料的集合體,所以可以用 RLE 演算法進行壓縮

哈夫曼演算法和莫爾斯編碼

下面我們來介紹另外一種壓縮演算法,即哈夫曼演算法。在瞭解哈夫曼演算法之前,你必須捨棄半形英文數字的1個字元是1個位元組(8位)的資料。下面我們就來認識一下哈夫曼演算法的基本思想。

文字檔案是由不同型別的字元組合而成的,而且不同字元出現的次數也是不一樣的。例如,在某個文字檔案中,A 出現了 100次左右,Q僅僅用到了 3 次,類似這樣的情況很常見。哈夫曼演算法的關鍵就在於 多次出現的資料用小於 8 位的位元組數表示,不常用的資料則可以使用超過 8 位的位元組數表示。A 和 Q 都用 8 位來表示時,原檔案的大小就是 100次 * 8 位 + 3次 * 8 位 = 824位,假設 A 用 2 位,Q 用 10 位來表示就是 2 * 100 + 3 * 10 = 230 位。

不過要注意一點,最終磁碟的儲存都是以8位為一個位元組來儲存檔案的。

哈夫曼演算法比較複雜,在深入瞭解之前我們先吃點甜品,瞭解一下 莫爾斯編碼,你一定看過美劇或者戰爭片的電影,在戰爭中的通訊經常採用莫爾斯編碼來傳遞資訊,例如下面

接下來我們來講解一下莫爾斯編碼,下面是莫爾斯編碼的示例,大家把 1 看作是短點(嘀),把 11 看作是長點(嗒)即可。

莫爾斯編碼一般把文字中出現最高頻率的字元用短編碼 來表示。如表所示,假如表示短點的位是 1,表示長點的位是 11 的話,那麼 E(嘀)這一資料的字元就可以用 1 來表示,C(滴答滴答)就可以用 9 位的 110101101來表示。在實際的莫爾斯編碼中,如果短點的長度是 1 ,長點的長度就是 3,短點和長點的間隔就是1。這裡的長度指的就是聲音的長度。比如我們想用上面的 AAAAAABBCDDEEEEEF 例子來用莫爾斯編碼重寫,在莫爾斯曼編碼中,各個字元之間需要加入表示時間間隔的符號。這裡我們用 00 加以區分。

所以,AAAAAABBCDDEEEEEF 這個文字就變為了 A * 6 次 + B * 2次 + C * 1次 + D * 2次 + E * 5次 + F * 1次 + 字元間隔 * 16 = 4 位 * 6次 + 8 位 * 2次 + 9 位 * 1 次 + 6位 * 2次 + 1位 * 5次 + 8 位 * 1次 + 2位 * 16次 = 106位 = 14位元組。

所以使用莫爾斯電碼的壓縮比為 14 / 17 = 82%。效率並不太突出。

用二叉樹實現哈夫曼演算法

剛才已經提到,莫爾斯編碼是根據日常文字中各字元的出現頻率來決定表示各字元的編碼資料長度的。不過,在該編碼體系中,對 AAAAAABBCDDEEEEEF 這種文字來說並不是效率最高的。

下面我們來看一下哈夫曼演算法。哈夫曼演算法是指,為各壓縮物件檔案分別構造最佳的編碼體系,並以該編碼體系為基礎來進行壓縮。因此,用什麼樣的編碼(哈夫曼編碼)對資料進行分割,就要由各個檔案而定。用哈夫曼演算法壓縮過的檔案中,儲存著哈夫曼編碼資訊和壓縮過的資料。

接下來,我們在對 AAAAAABBCDDEEEEEF 中的 A - F 這些字元,按照出現頻率高的字元用盡量少的位數編碼來表示這一原則進行整理。按照出現頻率從高到低的順序整理後,結果如下,同時也列出了編碼方案。

字元 出現頻率 編碼(方案) 位數
A 6 0 1
E 5 1 1
B 2 10 2
D 2 11 2
C 1 100 3
F 1 101 3

在上表的編碼方案中,隨著出現頻率的降低,字元編碼資訊的資料位數也在逐漸增加,從最開始的 1位、2位依次增加到3位。不過這個編碼體系是存在問題的,你不知道100這個3位的編碼,它的意思是用 1、0、0這三個編碼來表示 E、A、A 呢?還是用10、0來表示 B、A 呢?還是用100來表示 C 呢。

而在哈夫曼演算法中,通過藉助哈夫曼樹的構造編碼體系,即使在不使用字元區分符號的情況下,也可以構建能夠明確進行區分的編碼體系。不過哈夫曼樹的演算法要比較複雜,下面是一個哈夫曼樹的構造過程。

自然界樹的從根開始生葉的,而哈夫曼樹則是葉生枝

哈夫曼樹能夠提升壓縮比率

使用哈夫曼樹之後,出現頻率越高的資料所佔用的位數越少,這也是哈夫曼樹的核心思想。通過上圖的步驟二可以看出,枝條連線資料時,我們是從出現頻率較低的資料開始的。這就意味著出現頻率低的資料到達根部的枝條也越多。而枝條越多則意味著編碼的位數隨之增加。

接下來我們來看一下哈夫曼樹的壓縮比率,用上圖得到的資料表示 AAAAAABBCDDEEEEEF 為 000000000000 100100 110 101101 0101010101 111,40位 = 5 位元組。壓縮前的資料是 17 位元組,壓縮後的資料竟然達到了驚人的5 位元組,也就是壓縮比率 = 5 / 17 = 29% 如此高的壓縮率,簡直是太驚豔了。

大家可以參考一下,無論哪種型別的資料,都可以用哈夫曼樹作為壓縮演算法

檔案型別 壓縮前 壓縮後 壓縮比率
文字檔案 14862位元組 4119位元組 28%
影象檔案 96062位元組 9456位元組 10%
EXE檔案 24576位元組 4652位元組 19%

可逆壓縮和非可逆壓縮

最後,我們來看一下影象檔案的資料形式。影象檔案的使用目的通常是把影象資料輸出到顯示器、印表機等裝置上。常用的影象格式有 : BMPJPEGTIFFGIF 格式等。

  • BMP : 是使用 Windows 自帶的畫筆來做成的一種影象形式
  • JPEG:是數碼相機等常用的一種影象資料形式
  • TIFF: 是一種通過在檔案中包含"標籤"就能夠快速顯示出資料性質的影象形式
  • GIF: 是由美國開發的一種資料形式,要求色數不超過 256個

影象檔案可以使用前面介紹的 RLE 演算法和哈夫曼演算法,因為影象檔案在多數情況下並不要求資料需要還原到和壓縮之前一摸一樣的狀態,允許丟失一部分資料。我們把能還原到壓縮前狀態的壓縮稱為 可逆壓縮,無法還原到壓縮前狀態的壓縮稱為非可逆壓縮

一般來說,JPEG格式的檔案是非可逆壓縮,因此還原後有部分影象資訊比較模糊。GIF 是可逆壓縮

作業系統

作業系統環境

程式中包含著執行環境這一內容,可以說 執行環境 = 作業系統 + 硬體 ,作業系統又可以被稱為軟體,它是由一系列的指令組成的。我們不介紹作業系統,我們主要來介紹一下硬體的識別。

我們肯定都玩兒過遊戲,你玩兒遊戲前需要幹什麼?是不是需要先看一下自己的筆記本或者電腦是不是能肝的起遊戲?下面是一個遊戲的配置(懷念一下 wow)

圖中的主要配置如下

  • 作業系統版本:說的就是應用程式執行在何種系統環境,現在市面上主要有三種作業系統環境,Windows 、Linux 和 Unix ,一般我們玩兒的大型遊戲幾乎都是在 Windows 上執行,可以說 Windows 是遊戲的天堂。Windows 作業系統也會有區分,分為32位作業系統和64位作業系統,互不相容。
  • 處理器:處理器指的就是 CPU,你的電腦的計算能力,通俗來講就是每秒鐘能處理的指令數,如果你的電腦覺得卡帶不起來的話,很可能就是 CPU 的計算能力不足導致的。想要加深理解,請閱讀博主的另一篇文章:程式設計師需要了解的硬核知識之CPU

  • 顯示卡:顯示卡承擔圖形的輸出任務,因此又被稱為圖形處理器(Graphic Processing Unit,GPU),顯示卡也非常重要,比如我之前玩兒的劍靈開五檔(其實就是影象變得更清晰)會卡,其實就是顯示卡顯示不出來的原因。
  • 記憶體:記憶體即主存,就是你的應用程式在執行時能夠動態分析指令的這部分儲存空間,它的大小也能決定你電腦的執行速度,想要加深理解,請閱讀博主的另一篇文章 程式設計師需要了解的硬核知識之記憶體

  • 儲存空間:儲存空間指的就是應用程式安裝所佔用的磁碟空間,由圖中可知,此遊戲的最低儲存空間必須要大於 5GB,其實我們都會遺留很大一部分用來安裝遊戲。

從程式的執行環境這一角度來考量的話,CPU 的種類是特別重要的引數,為了使程式能夠正常執行,必須滿足 CPU 所需的最低配置。

CPU 只能解釋其自身固有的語言。不同的 CPU 能解釋的機器語言的種類也是不同的。機器語言的程式稱為 原生代碼(native code),程式設計師用 C 等高階語言編寫的程式,僅僅是文字檔案。文字檔案(排除文字編碼的問題)在任何環境下都能顯示和編輯。我們稱之為原始碼。通過對原始碼進行編譯,就可以得到原生代碼。下圖反映了這個過程。

uploading-image-703074.png

Windows 作業系統克服了CPU以外的硬體差異

計算機的硬體並不僅僅是由 CPU 組成的,還包括用於儲存程式指令的資料和記憶體,以及通過 I/O 連線的鍵盤、顯示器、硬碟、印表機等外圍裝置。

在 WIndows 軟體中,鍵盤輸入、顯示器輸出等並不是直接向硬體傳送指令。而是通過向 Windows 傳送指令實現的。因此,程式設計師就不用注意記憶體和 I/O 地址的不同構成了。Windows 操作的是硬體而不是軟體,軟體通過操作 Windows 系統可以達到控制硬體的目的。

不同作業系統的 API 差異性

接下來我們看一下作業系統的種類。同樣機型的計算機,可安裝的作業系統型別也會有多種選擇。例如:AT 相容機除了可以安裝 Windows 之外,還可以採用 Unix 系列的 Linux 以及 FreeBSD (也是一種Unix作業系統)等多個作業系統。當然,應用軟體則必須根據不同的作業系統型別來專門開發。CPU 的型別不同,所對應機器的語言也不同,同樣的道理,作業系統的型別不同,應用程式向作業系統傳遞指令的途徑也不同。

應用程式向系統傳遞指令的途徑稱為 API(Application Programming Interface)。Windows 以及 Linux 作業系統的 API,提供了任何應用程式都可以利用的函式組合。因為不同作業系統的 API 是有差異的。所以,如何要將同樣的應用程式移植到另外的作業系統,就必須要覆蓋應用所用到的 API 部分。

鍵盤輸入、滑鼠輸入、顯示器輸出、檔案輸入和輸出等同外圍裝置進行互動的功能,都是通過 API 提供的。

這也就是為什麼 Windows 應用程式不能直接移植到 Linux 作業系統上的原因,API 差異太大了。

在同類型的作業系統下,不論硬體如何,API 幾乎相同。但是,由於不同種類 CPU 的機器語言不同,因此原生代碼也不盡相同。

作業系統功能的歷史

作業系統其實也是一種軟體,任何新事物的出現肯定都有它的歷史背景,那麼作業系統也不是憑空出現的,肯定有它的歷史背景。

在計算機尚不存在作業系統的年代,完全沒有任何程式,人們通過各種按鈕來控制計算機,這一過程非常麻煩。於是,有人開發出了僅具有載入和執行功能的監控程式,這就是作業系統的原型。通過事先啟動監控程式,程式設計師可以根據需要將各種程式載入到記憶體中執行。雖然仍舊比較麻煩,但比起在沒有任何程式的狀態下進行開發,工作量得到了很大的緩解。

隨著時代的發展,人們在利用監控程式編寫程式的過程中發現很多程式都有公共的部分。例如,通過鍵盤進行文字輸入,顯示器進行資料展示等,如果每編寫一個新的應用程式都需要相同的處理的話,那真是太浪費時間了。因此,基本的輸入輸出部分的程式就被追加到了監控程式中。初期的作業系統就是這樣誕生了。

類似的想法可以共用,人們又發現有更多的應用程式可以追加到監控程式中,比如硬體控制程式程式語言處理器(彙編、編譯、解析)以及各種應用程式等,結果就形成了和現在差異不大的作業系統,也就是說,其實作業系統是多個程式的集合體。

Windows 作業系統的特徵

Windows 作業系統是世界上使用者數量最龐大的群體,作為 Windows 作業系統的資深使用者,你都知道 Windows 作業系統有哪些特徵嗎?下面列舉了一些 Windows 作業系統的特性

  • Windows 作業系統有兩個版本:32位和64位
  • 通過 API 函式整合來提供系統呼叫
  • 提供了採用圖形使用者介面的使用者介面
  • 通過 WYSIWYG 實現列印輸出,WYSIWYG 其實就是 What You See Is What You Get ,值得是顯示器上顯示的圖形和文字都是可以原樣輸出到印表機列印的。
  • 提供多工功能,即能夠同時開啟多個任務
  • 提供網路功能和資料庫功能
  • 通過即插即用實現裝置驅動的自設定

這些是對程式設計師來講比較有意義的一些特徵,下面針對這些特徵來進行分別的介紹

32位作業系統

這裡表示的32位作業系統表示的是處理效率最高的資料大小。Windows 處理資料的基本單位是 32 位。這與最一開始在 MS-DOS 等16位作業系統不同,因為在16位作業系統中處理32位資料需要兩次,而32位作業系統只需要一次就能夠處理32位的資料,所以一般在 windows 上的應用,它們的最高能夠處理的資料都是 32 位的。

比如,用 C 語言來處理整數資料時,有8位的 char 型別,16位的short型別,以及32位的long型別三個選項,使用位數較大的 long 型別進行處理的話,增加的只是記憶體以及磁碟的開銷,對效能影響不大。

現在市面上大部分都是64位作業系統了,64位作業系統也是如此。

通過 API 函式集來提供系統呼叫

Windows 是通過名為 API 的函式集來提供系統呼叫的。API是聯絡應用程式和作業系統之間的介面,全稱叫做 Application Programming Interface,應用程式介面。

當前主流的32位版 Windows API 也稱為 Win32 API,之所以這樣命名,是需要和不同的作業系統進行區分,比如最一開始的 16 位版的 Win16 API,和後來流行的 Win64 API

API 通過多個 DLL 檔案來提供,各個 API 的實體都是用 C 語言編寫的函式。所以,在 C 語言環境下,使用 API 更加容易,比如 API 所用到的 MessageBox() 函式,就被儲存在了 Windows 提供的 user32.dll 這個 DLL 檔案中。

提供採用了 GUI 的使用者介面

GUI(Graphical User Interface) 指得就是圖形使用者介面,通過點選顯示器中的視窗以及圖示等視覺化的使用者介面,舉個例子:Linux 作業系統就有兩個版本,一種是簡潔版,直接通過命令列控制硬體,還有一種是視覺化版,通過游標點選圖形介面來控制硬體。

通過 WYSIWYG 實現列印輸出

WYSIWYG 指的是顯示器上輸出的內容可以直接通過印表機列印輸出。在 Windows 中,顯示器和印表機被認作同等的圖形輸出裝置處理的,該功能也為 WYSIWYG 提供了條件。

藉助 WYSIWYG 功能,程式設計師可以輕鬆不少。最初,為了是現在顯示器中顯示和在印表機中列印,就必須分別編寫各自的程式,而在 Windows 中,可以藉助 WYSIWYG 基本上在一個程式中就可以做到顯示和列印這兩個功能了。

提供多工功能

多工指的就是同時能夠執行多個應用程式的功能,Windows 是通過時鐘分割技術來實現多工功能的。時鐘分割指的是短時間間隔內,多個程式切換執行的方式。在使用者看來,就好像是多個程式在同時執行,其底層是 CPU 時間切片,這也是多執行緒多工的核心。

提供網路功能和資料庫功能

Windows 中,網路功能是作為標準功能提供的。資料庫(資料庫伺服器)功能有時也會在後面追加。網路功能和資料庫功能雖然並不是作業系統不可或缺的,但因為它們和作業系統很接近,所以被統稱為中介軟體而不是應用。意思是處於作業系統和應用的中間層,作業系統和中介軟體組合在一起,稱為系統軟體。應用不僅可以利用作業系統,也可以利用中介軟體的功能。

相對於作業系統一旦安裝就不能輕易更換,中介軟體可以根據需要進行更換,不過,對於大部分應用來說,更換中介軟體的話,會造成應用也隨之更換,從這個角度來說,更å換中介軟體也不是那麼容易。

通過即插即用實現裝置驅動的自動設定

即插即用(Plug-and-Play)指的是新的裝置連線(plug) 後就可以直接使用的機制,新裝置連線計算機後,計算機就會自動安裝和設定用來控制該裝置的驅動程式

裝置驅動是作業系統的一部分,提供了同硬體進行基本的輸入輸出的功能。鍵盤、滑鼠、顯示器、磁碟裝置等,這些計算機中必備的硬體的裝置驅動,一般都是隨作業系統一起安裝的。

有時 DLL 檔案也會同裝置驅動檔案一起安裝。這些 DLL 檔案中儲存著用來利用該新追加的硬體API,通過 API ,可以製作出執行該硬體的心應用。

組合語言和原生代碼

我們在之前的文章中探討過,計算機 CPU 只能執行原生代碼(機器語言)程式,用 C 語言等高階語言編寫的程式碼,需要經過編譯器編譯後,轉換為原生代碼才能夠被 CPU 解釋執行。

但是原生代碼的可讀性非常差,所以需要使用一種能夠直接讀懂的語言來替換原生代碼,那就是在各原生代碼中,附帶上表示其功能的英文縮寫,比如在加法運算的原生代碼加上add(addition) 的縮寫、在比較運算子的原生代碼中加上cmp(compare)的縮寫等,這些通過縮寫來表示具體原生代碼指令的標誌稱為 助記符,使用助記符的語言稱為組合語言。這樣,通過閱讀組合語言,也能夠了解原生代碼的含義了。

不過,即使是使用匯編語言編寫的原始碼,最終也必須要轉換為原生代碼才能夠執行,負責做這項工作的程式稱為編譯器,轉換的這個過程稱為彙編。在將原始碼轉換為原生代碼這個功能方面,彙編器和編譯器是同樣的。

用匯編語言編寫的原始碼和原生代碼是一一對應的。因而,原生代碼也可以反過來轉換成組合語言編寫的程式碼。把原生代碼轉換為彙編程式碼的這一過程稱為反彙編,執行反彙編的程式稱為反彙編程式

哪怕是 C 語言編寫的原始碼,編譯後也會轉換成特定 CPU 用的原生代碼。而將其反彙編的話,就可以得到組合語言的原始碼,並對其內容進行調查。不過,原生代碼變成 C 語言原始碼的反編譯,要比原生代碼轉換成彙編程式碼的反彙編要困難,這是因為,C 語言程式碼和原生代碼不是一一對應的關係。

通過編譯器輸出組合語言的原始碼

我們上面提到原生代碼可以經過反彙編轉換成為彙編程式碼,但是隻有這一種轉換方式嗎?顯然不是,C 語言編寫的原始碼也能夠通過編譯器編譯稱為彙編程式碼,下面就來嘗試一下。

首先需要先做一些準備,需要先下載 Borland C++ 5.5 編譯器,為了方便,我這邊直接下載好了讀者直接從我的百度網盤提取即可 (連結:https://pan.baidu.com/s/19LqVICpn5GcV88thD2AnlA 密碼:hz1u)

下載完畢,需要進行配置,下面是配置說明 (https://wenku.baidu.com/view/22e2f418650e52ea551898ad.html),教程很完整跟著配置就可以,下面開始我們的編譯過程

首先用 Windows 記事本等文字編輯器編寫如下程式碼

// 返回兩個引數值之和的函式
int AddNum(int a,int b){
  return a + b;
}

// 呼叫 AddNum 函式的函式
void MyFunc(){
  int c;
  c = AddNum(123,456);
}

編寫完成後將其檔名儲存為 Sample4.c ,C 語言原始檔的副檔名,通常用.c 來表示,上面程式是提供兩個輸入引數並返回它們之和。

在 Windows 作業系統下開啟 命令提示符,切換到儲存 Sample4.c 的資料夾下,然後在命令提示符中輸入

bcc32 -c -S Sample4.c

bcc32 是啟動 Borland C++ 的命令,-c 的選項是指僅進行編譯而不進行連結,-S 選項被用來指定生成組合語言的原始碼

作為編譯的結果,當前目錄下會生成一個名為Sample4.asm 的組合語言原始碼。組合語言原始檔的副檔名,通常用.asm 來表示,下面就讓我們用編輯器開啟看一下 Sample4.asm 中的內容

    .386p
    ifdef ??version
    if    ??version GT 500H
    .mmx
    endif
    endif
    model flat
    ifndef  ??version
    ?debug  macro
    endm
    endif
    ?debug  S "Sample4.c"
    ?debug  T "Sample4.c"
_TEXT   segment dword public use32 'CODE'
_TEXT   ends
_DATA   segment dword public use32 'DATA'
_DATA   ends
_BSS    segment dword public use32 'BSS'
_BSS    ends
DGROUP  group   _BSS,_DATA
_TEXT   segment dword public use32 'CODE'
_AddNum proc    near
?live1@0:
   ;    
   ;    int AddNum(int a,int b){
   ;    
    push      ebp
    mov       ebp,esp
   ;    
   ;    
   ;        return a + b;
   ;    
@1:
    mov       eax,dword ptr [ebp+8]
    add       eax,dword ptr [ebp+12]
   ;    
   ;    }
   ;    
@3:
@2:
    pop       ebp
    ret 
_AddNum endp
_MyFunc proc    near
?live1@48:
   ;    
   ;    void MyFunc(){
   ;    
    push      ebp
    mov       ebp,esp
   ;    
   ;        int c;
   ;        c = AddNum(123,456);
   ;    
@4:
    push      456
    push      123
    call      _AddNum
    add       esp,8
   ;    
   ;    }
   ;    
@5:
    pop       ebp
    ret 
_MyFunc endp
_TEXT   ends
    public  _AddNum
    public  _MyFunc
    ?debug  D "Sample4.c" 20343 45835
    end

這樣,編譯器就成功的把 C 語言轉換成為了彙編程式碼了。

不會轉換成原生代碼的偽指令

第一次看到彙編程式碼的讀者可能感覺起來比較難,不過實際上其實比較簡單,而且可能比 C 語言還要簡單,為了便於閱讀彙編程式碼的原始碼,需要注意幾個要點