程序員硬核知識大全

咱們每一個程序員或許都有一個夢,那就是成爲大牛,咱們或許都沉浸在各類框架中,覺得框架就是一切,覺得應用層纔是最重要的,你錯了。在當今計算機行業中,會應用是基本素質,若是你懂其原理才能讓你在行業中走的更遠,而計算機基礎知識又是重中之重。下面,跟隨個人腳步,爲你介紹一下計算機底層知識。html

CPU

還不瞭解 CPU 嗎?如今就帶你瞭解一下 CPU 是什麼git

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

CPU 內部處理過程

下圖展現了通常程序的運行流程(以 C 語言爲例),能夠說了解程序的運行流程是掌握程序運行機制的基礎和前提。算法

image.png

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

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

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

CPU 是計算機的心臟和大腦,它和內存都是由許多晶體管組成的電子部件。它接收數據輸入,執行指令並處理信息。它與輸入/輸出(I / O)設備進行通訊,這些設備向 CPU 發送數據和從 CPU 接收數據。segmentfault

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

image.png

  • 寄存器是中央處理器內的組成部分。它們能夠用來暫存指令、數據和地址。能夠將其看做是內存的一種。根據種類的不一樣,一個 CPU 內部會有 20 - 100個寄存器。
  • 控制器負責把內存上的指令、數據讀入寄存器,並根據指令的結果控制計算機
  • 運算器負責運算從內存中讀入寄存器的數據
  • 時鐘 負責發出 CPU 開始計時的時鐘信號

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

在 CPU 的四個結構中,咱們程序員只須要了解寄存器就能夠了,其他三個不用過多關注,爲何這麼說?由於程序是把寄存器做爲對象來描述的。數組

不一樣類型的 CPU ,其內部寄存器的種類,數量以及寄存器存儲的數值範圍都是不一樣的。不過,根據功能的不一樣,能夠將寄存器劃分爲下面這幾類緩存

image.png

其中程序計數器、累加寄存器、標誌寄存器、指令寄存器和棧寄存器都只有一個,其餘寄存器通常有多個。

image.png

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

程序計數器

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

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

咱們仍是以一個事例爲準來詳細的看一下程序計數器的執行過程

image.png

這是一段進行相加的操做,程序啓動,在通過編譯解析後會由操做系統把硬盤中的程序複製到內存中,示例中的程序是將 123 和 456 執行相加操做,並將結果輸出到顯示器上。

地址 0100 是程序運行的起始位置。Windows 等操做系統把程序從硬盤複製到內存後,會將程序計數器做爲設定爲起始位置 0100,而後執行程序,每執行一條指令後,程序計數器的數值會增長1(或者直接指向下一條指令的地址),而後,CPU 就會根據程序計數器的數值,從內存中讀取命令並執行,也就是說,程序計數器控制着程序的流程

條件分支和循環機制

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

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

下面以條件分支爲例來講明程序的執行過程(循環也很類似)

image.png

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

標誌寄存器

條件和循環分支會使用到 jump(跳轉指令),會根據當前的指令來判斷是否跳轉,上面咱們提到了標誌寄存器,不管當前累加寄存器的運算結果是正數、負數仍是零,標誌寄存器都會將其保存

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

image.png

CPU 的執行機制比較有意思,假設累加寄存器中存儲的 XXX 和通用寄存器中存儲的 YYY 作比較,執行比較的背後,CPU 的運算機制就會作減法運算。而不管減法運算的結果是正數、零仍是負數,都會保存到標誌寄存器中。結果爲正表示 XXX 比 YYY 大,結果爲零表示 XXX 和 YYY 相等,結果爲負表示 XXX 比 YYY 小。程序比較的指令,其實是在 CPU 內部作減法運算。

函數調用機制

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

image.png

圖中將變量 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 的地址保存在程序計數器中。這個調用過程以下

image.png

在一些高級語言的條件或者循環語句中,函數調用的處理會轉換成 call 指令,函數結束後的處理則會轉換成 return 指令。

經過地址和索引實現數組

接下來咱們看一下基址寄存器和變址寄存器,經過這兩個寄存器,咱們能夠對主存上的特定區域進行劃分,來實現相似數組的操做,首先,咱們用十六進制數將計算機內存上的 00000000 - FFFFFFFF 的地址劃分出來。那麼,凡是該範圍的內存地址,只要有一個 32 位的寄存器,即可查看所有地址。但若是想要想數組那樣分割特定的內存區域以達到連續查看的目的的話,使用兩個寄存器會更加方便。

例如,咱們用兩個寄存器(基址寄存器和變址寄存器)來表示內存的值

image.png

這種表示方式很相似數組的構造,數組是指一樣長度的數據在內存中進行連續排列的數據構造。用數組名錶示數組所有的值,經過索引來區分數組的各個數據元素,例如: 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 引腳示意圖

image.png

圖中 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 進行數據寫入和讀取的模型

image.png

來詳細描述一下這個過程,假設咱們要向內存 IC 中寫入 1byte 的數據的話,它的過程是這樣的:

  • 首先給 VCC 接通 +5V 的電源,給 GND 接通 0V 的電源,使用 A0 - A9 來指定數據的存儲場所,而後再把數據的值輸入給 D0 - D7 的數據信號,並把 WR(write)的值置爲 1,執行完這些操做後,便可以向內存 IC 寫入數據
  • 讀出數據時,只須要經過 A0 - A9 的地址信號指定數據的存儲場所,而後再將 RD 的值置爲 1 便可。
  • 圖中的 RD 和 WR 又被稱爲控制信號。其中當WR 和 RD 都爲 0 時,沒法進行寫入和讀取操做。

內存的現實模型

爲了便於記憶,咱們把內存模型映射成爲咱們現實世界的模型,在現實世界中,內存的模型很想咱們生活的樓房。在這個樓房中,1層能夠存儲一個字節的數據,樓層號就是地址,下面是內存和樓層整合的模型圖

image.png

咱們知道,程序中的數據不只只有數值,還有數據類型的概念,從內存上來看,就是佔用內存大小(佔用樓層數)的意思。即便物理上強制以 1 個字節爲單位來逐一讀寫數據的內存,在程序中,經過指定其數據類型,也能實現以特定字節數爲單位來進行讀寫。

二進制

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

什麼是二進制數

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

image.png

也就是說,二進制數表明的 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 進位便可。二進制數的運算,也是計算機程序所特有的運算,所以瞭解二進制的運算是必需要掌握的。

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

image.png

補數

剛纔咱們沒有介紹右移的狀況,是由於右移以後空出來的高位數值,有 0 和 1 兩種形式。要想區分何時補0何時補1,首先就須要掌握二進制數表示負數的方法。

二進制數中表示負數值時,通常會把最高位做爲符號來使用,所以咱們把這個最高位看成符號位。 符號位是 0 時表示正數,是 1 時表示 負數。那麼 -1 用二進制數該如何表示呢?可能不少人會這麼認爲: 由於 1 的二進制數是 0000 0001,最高位是符號位,因此正確的表示 -1 應該是 1000 0001,可是這個答案真的對嗎?

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

爲了得到補數,咱們須要將二進制的各數位的數值所有取反,而後再將結果 + 1 便可,先記住這個結論,下面咱們來演示一下。

image.png

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

補數的獲取,雖然直觀上不易理解,可是邏輯上卻很是嚴謹,好比咱們來看一下 1 - 1 的這個過程,咱們先用上面的這個 1000 0001(它是1的補數,不知道的請看上文,正確性先無論,只是用來作一下計算)來表示一下

image.png

奇怪,1 - 1 會變成 130 ,而不是0,因此能夠得出結論 1000 0001 表示 -1 是徹底錯誤的。

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

image.png

咱們能夠看到 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 右移兩位,來各自看一下移位示意圖

image.png

如上圖所示,在邏輯右移的狀況下, -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 是須要經過程序計數器來指定內存地址從而讀出程序指令的。

image.png

磁盤構造

磁盤緩存

咱們上面提到,磁盤每每和內存是互利共生的關係,相互協做,彼此持有良好的合做關係。每次內存都須要從磁盤中讀取數據,必然會讀到相同的內容,因此必定會有一個角色負責存儲咱們常常須要讀到的內容。 咱們你們作軟件的時候常常會用到緩存技術,那麼硬件層面也不例外,磁盤也有緩存,磁盤的緩存叫作磁盤緩存

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

image.png

虛擬內存

虛擬內存是內存和磁盤交互的第二個媒介。虛擬內存是指把磁盤的一部分做爲假想內存來使用。這與磁盤緩存是假想的磁盤(其實是內存)相對,虛擬內存是假想的內存(其實是磁盤)。

虛擬內存是計算機系統內存管理的一種技術。它使得應用程序認爲它擁有連續可用的內存(一個完整的地址空間),可是實際上,它一般被分割成多個物理碎片,還有部分存儲在外部磁盤管理器上,必要時進行數據交換。

經過藉助虛擬內存,在內存不足時仍然能夠運行程序。例如,在只剩 5MB 內存空間的狀況下仍然能夠運行 10MB 的程序。因爲 CPU 只能執行加載到內存中的程序,所以,虛擬內存的空間就須要和內存中的空間進行置換(swap),而後運行程序。

虛擬內存與內存的交換方式

虛擬內存的方法有分頁式分段式 兩種。Windows 採用的是分頁式。該方式是指在不考慮程序構造的狀況下,把運行的程序按照必定大小的頁進行分割,並以爲單位進行置換。在分頁式中,咱們把磁盤的內容讀到內存中稱爲 Page In,把內存的內容寫入磁盤稱爲 Page Out。Windows 計算機的頁大小爲 4KB ,也就是說,須要把應用程序按照 4KB 的頁來進行切分,以頁(page)爲單位放到磁盤中,而後進行置換。

image.png

爲了實現內存功能,Windows 在磁盤上提供了虛擬內存使用的文件(page file,頁文件)。該文件由 Windows 生成和管理,文件的大小和虛擬內存大小相同,一般大小是內存的 1 - 2 倍。

磁盤的物理結構

以前咱們介紹了CPU、內存的物理結構,如今咱們來介紹一下磁盤的物理結構。磁盤的物理結構指的是磁盤存儲數據的形式

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

image.png

扇區是對磁盤進行物理讀寫的最小單位。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 。若是文件中存儲的數據是文字,那麼該文件就是文本文件。若是是圖形,那麼該文件就是圖像文件。在任何狀況下,文件中的字節數都是連續存儲的。

image.png

壓縮算法的定義

上面介紹了文件的集合體其實就是一堆字節數據的集合,那麼咱們就能夠來給壓縮算法下一個定義。

壓縮算法(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 字節。如圖

image.png

那麼,如何才能壓縮該文件呢?你們不妨也考慮一下,只要是可以使文件小於 17 字節,咱們可使用任何壓縮算法。

最顯而易見的一種壓縮方式我以爲你已經想到了,就是把相同的字符去重化,也就是 字符 * 重複次數 的方式進行壓縮。因此上面文件壓縮後就會變成下面這樣

image.png

從圖中咱們能夠看出,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位爲一個字節來保存文件的。

哈夫曼算法比較複雜,在深刻了解以前咱們先吃點甜品,瞭解一下 莫爾斯編碼,你必定看過美劇或者戰爭片的電影,在戰爭中的通訊常常採用莫爾斯編碼來傳遞信息,例以下面

image.png

接下來咱們來說解一下莫爾斯編碼,下面是莫爾斯編碼的示例,你們把 1 看做是短點(嘀),把 11 看做是長點(嗒)便可。

image.png

莫爾斯編碼通常把文本中出現最高頻率的字符用短編碼 來表示。如表所示,假如表示短點的位是 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 這種文原本說並非效率最高的。

下面咱們來看一下哈夫曼算法。哈夫曼算法是指,爲各壓縮對象文件分別構造最佳的編碼體系,並以該編碼體系爲基礎來進行壓縮。所以,用什麼樣的編碼(哈夫曼編碼)對數據進行分割,就要由各個文件而定。用哈夫曼算法壓縮過的文件中,存儲着哈夫曼編碼信息和壓縮過的數據。

image.png

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

image.png

在上表的編碼方案中,隨着出現頻率的下降,字符編碼信息的數據位數也在逐漸增長,從最開始的 1位、2位依次增長到3位。不過這個編碼體系是存在問題的,你不知道100這個3位的編碼,它的意思是用 一、0、0這三個編碼來表示 E、A、A 呢?仍是用十、0來表示 B、A 呢?仍是用100來表示 C 呢。

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

image.png

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

哈夫曼樹可以提高壓縮比率

使用哈夫曼樹以後,出現頻率越高的數據所佔用的位數越少,這也是哈夫曼樹的核心思想。經過上圖的步驟二能夠看出,枝條鏈接數據時,咱們是從出現頻率較低的數據開始的。這就意味着出現頻率低的數據到達根部的枝條也越多。而枝條越多則意味着編碼的位數隨之增長。

接下來咱們來看一下哈夫曼樹的壓縮比率,用上圖獲得的數據表示 AAAAAABBCDDEEEEEF 爲 000000000000 100100 110 101101 0101010101 111,40位 = 5 字節。壓縮前的數據是 17 字節,壓縮後的數據居然達到了驚人的5 字節,也就是壓縮比率 = 5 / 17 = 29% 如此高的壓縮率,簡直是太驚豔了。

你們能夠參考一下,不管哪一種類型的數據,均可以用哈夫曼樹做爲壓縮算法

image.png

可逆壓縮和非可逆壓縮

最後,咱們來看一下圖像文件的數據形式。圖像文件的使用目的一般是把圖像數據輸出到顯示器、打印機等設備上。經常使用的圖像格式有 : BMPJPEGTIFFGIF 格式等。

  • BMP : 是使用 Windows 自帶的畫筆來作成的一種圖像形式
  • JPEG:是數碼相機等經常使用的一種圖像數據形式
  • TIFF: 是一種經過在文件中包含"標籤"就可以快速顯示出數據性質的圖像形式
  • GIF: 是由美國開發的一種數據形式,要求色數不超過 256個

圖像文件可使用前面介紹的 RLE 算法和哈夫曼算法,由於圖像文件在多數狀況下並不要求數據須要還原到和壓縮以前一摸同樣的狀態,容許丟失一部分數據。咱們把能還原到壓縮前狀態的壓縮稱爲 可逆壓縮,沒法還原到壓縮前狀態的壓縮稱爲非可逆壓縮

image.png

通常來講,JPEG格式的文件是非可逆壓縮,所以還原後有部分圖像信息比較模糊。GIF 是可逆壓縮

操做系統

操做系統環境

程序中包含着運行環境這一內容,能夠說 運行環境 = 操做系統 + 硬件 ,操做系統又能夠被稱爲軟件,它是由一系列的指令組成的。咱們不介紹操做系統,咱們主要來介紹一下硬件的識別。

咱們確定都玩兒過遊戲,你玩兒遊戲前須要幹什麼?是否是須要先看一下本身的筆記本或者電腦是否是能肝的起遊戲?下面是一個遊戲的配置(懷念一下 wow)

image.png

圖中的主要配置以下

  • 操做系統版本:說的就是應用程序運行在何種系統環境,如今市面上主要有三種操做系統環境,Windows 、Linux 和 Unix ,通常咱們玩兒的大型遊戲幾乎都是在 Windows 上運行,能夠說 Windows 是遊戲的天堂。Windows 操做系統也會有區分,分爲32位操做系統和64位操做系統,互不兼容。
  • 處理器:處理器指的就是 CPU,你的電腦的計算能力,通俗來說就是每秒鐘能處理的指令數,若是你的電腦以爲卡帶不起來的話,極可能就是 CPU 的計算能力不足致使的。想要加深理解,請閱讀博主的另外一篇文章:程序員須要瞭解的硬核知識之CPU
  • 顯卡:顯卡承擔圖形的輸出任務,所以又被稱爲圖形處理器(Graphic Processing Unit,GPU),顯卡也很是重要,好比我以前玩兒的劍靈開五檔(其實就是圖像變得更清晰)會卡,其實就是顯卡顯示不出來的緣由。
  • 內存:內存即主存,就是你的應用程序在運行時可以動態分析指令的這部分存儲空間,它的大小也能決定你電腦的運行速度,想要加深理解,請閱讀博主的另外一篇文章 程序員須要瞭解的硬核知識以內存
  • 存儲空間:存儲空間指的就是應用程序安裝所佔用的磁盤空間,由圖中可知,此遊戲的最低存儲空間必需要大於 5GB,其實咱們都會遺留很大一部分用來安裝遊戲。

從程序的運行環境這一角度來考量的話,CPU 的種類是特別重要的參數,爲了使程序可以正常運行,必須知足 CPU 所需的最低配置。

CPU 只能解釋其自身固有的語言。不一樣的 CPU 能解釋的機器語言的種類也是不一樣的。機器語言的程序稱爲 本地代碼(native code),程序員用 C 等高級語言編寫的程序,僅僅是文本文件。文本文件(排除文字編碼的問題)在任何環境下都能顯示和編輯。咱們稱之爲源代碼。經過對源代碼進行編譯,就能夠獲得本地代碼。下圖反映了這個過程。

image.png

Windows 操做系統克服了CPU之外的硬件差別

計算機的硬件並不只僅是由 CPU 組成的,還包括用於存儲程序指令的數據和內存,以及經過 I/O 鏈接的鍵盤、顯示器、硬盤、打印機等外圍設備。

在 WIndows 軟件中,鍵盤輸入、顯示器輸出等並非直接向硬件發送指令。而是經過向 Windows 發送指令實現的。所以,程序員就不用注意內存和 I/O 地址的不一樣構成了。Windows 操做的是硬件而不是軟件,軟件經過操做 Windows 系統能夠達到控制硬件的目的。

image.png

不一樣操做系統的 API 差別性

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

應用程序向系統傳遞指令的途徑稱爲 API(Application Programming Interface)。Windows 以及 Linux 操做系統的 API,提供了任何應用程序均可以利用的函數組合。由於不一樣操做系統的 API 是有差別的。因此,如何要將一樣的應用程序移植到另外的操做系統,就必需要覆蓋應用所用到的 API 部分。

鍵盤輸入、鼠標輸入、顯示器輸出、文件輸入和輸出等同外圍設備進行交互的功能,都是經過 API 提供的。

這也就是爲何 Windows 應用程序不能直接移植到 Linux 操做系統上的緣由,API 差別太大了。

在同類型的操做系統下,不論硬件如何,API 幾乎相同。可是,因爲不一樣種類 CPU 的機器語言不一樣,所以本地代碼也不盡相同。

操做系統功能的歷史

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

在計算機尚不存在操做系統的年代,徹底沒有任何程序,人們經過各類按鈕來控制計算機,這一過程很是麻煩。因而,有人開發出了僅具備加載和運行功能的監控程序,這就是操做系統的原型。經過事先啓動監控程序,程序員能夠根據須要將各類程序加載到內存中運行。雖然仍舊比較麻煩,但比起在沒有任何程序的狀態下進行開發,工做量獲得了很大的緩解。

image.png

隨着時代的發展,人們在利用監控程序編寫程序的過程當中發現不少程序都有公共的部分。例如,經過鍵盤進行文字輸入,顯示器進行數據展現等,若是每編寫一個新的應用程序都須要相同的處理的話,那真是太浪費時間了。所以,基本的輸入輸出部分的程序就被追加到了監控程序中。初期的操做系統就是這樣誕生了。

image.png

相似的想法能夠共用,人們又發現有更多的應用程序能夠追加到監控程序中,好比硬件控制程序編程語言處理器(彙編、編譯、解析)以及各類應用程序等,結果就造成了和如今差別不大的操做系統,也就是說,其實操做系統是多個程序的集合體。

image.png

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 時間切片,這也是多線程多任務的核心。

image.png

提供網絡功能和數據庫功能

Windows 中,網絡功能是做爲標準功能提供的。數據庫(數據庫服務器)功能有時也會在後面追加。網絡功能和數據庫功能雖然並非操做系統不可或缺的,但由於它們和操做系統很接近,因此被統稱爲中間件而不是應用。意思是處於操做系統和應用的中間層,操做系統和中間件組合在一塊兒,稱爲系統軟件。應用不只能夠利用操做系統,也能夠利用中間件的功能。

image.png

相對於操做系統一旦安裝就不能輕易更換,中間件能夠根據須要進行更換,不過,對於大部分應用來講,更換中間件的話,會形成應用也隨之更換,從這個角度來講,更å換中間件也不是那麼容易。

經過即插即用實現設備驅動的自動設定

即插即用(Plug-and-Play)指的是新的設備鏈接(plug) 後就能夠直接使用的機制,新設備鏈接計算機後,計算機就會自動安裝和設定用來控制該設備的驅動程序

設備驅動是操做系統的一部分,提供了同硬件進行基本的輸入輸出的功能。鍵盤、鼠標、顯示器、磁盤裝置等,這些計算機中必備的硬件的設備驅動,通常都是隨操做系統一塊兒安裝的。

有時 DLL 文件也會同設備驅動文件一塊兒安裝。這些 DLL 文件中存儲着用來利用該新追加的硬件API,經過 API ,能夠製做出運行該硬件的心應用。

彙編語言和本地代碼

咱們在以前的文章中探討過,計算機 CPU 只能運行本地代碼(機器語言)程序,用 C 語言等高級語言編寫的代碼,須要通過編譯器編譯後,轉換爲本地代碼纔可以被 CPU 解釋執行。

可是本地代碼的可讀性很是差,因此須要使用一種可以直接讀懂的語言來替換本地代碼,那就是在各本地代碼中,附帶上表示其功能的英文縮寫,好比在加法運算的本地代碼加上add(addition) 的縮寫、在比較運算符的本地代碼中加上cmp(compare)的縮寫等,這些經過縮寫來表示具體本地代碼指令的標誌稱爲 助記符,使用助記符的語言稱爲彙編語言。這樣,經過閱讀彙編語言,也可以瞭解本地代碼的含義了。

不過,即便是使用匯編語言編寫的源代碼,最終也必需要轉換爲本地代碼纔可以運行,負責作這項工做的程序稱爲編譯器,轉換的這個過程稱爲彙編。在將源代碼轉換爲本地代碼這個功能方面,彙編器和編譯器是一樣的。

用匯編語言編寫的源代碼和本地代碼是一一對應的。於是,本地代碼也能夠反過來轉換成彙編語言編寫的代碼。把本地代碼轉換爲彙編代碼的這一過程稱爲反彙編,執行反彙編的程序稱爲反彙編程序

image.png

哪怕是 C 語言編寫的源代碼,編譯後也會轉換成特定 CPU 用的本地代碼。而將其反彙編的話,就能夠獲得彙編語言的源代碼,並對其內容進行調查。不過,本地代碼變成 C 語言源代碼的反編譯,要比本地代碼轉換成彙編代碼的反彙編要困難,這是由於,C 語言代碼和本地代碼不是一一對應的關係。

經過編譯器輸出彙編語言的源代碼

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

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

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

首先用 Windows 記事本等文本編輯器編寫以下代碼

 // 返回兩個參數值之和的函數
 intAddNum(inta,intb){
  returna+b;
 }
 ​
 // 調用 AddNum 函數的函數
 voidMyFunc(){
  intc;
  c=AddNum(123,456);
 }

編寫完成後將其文件名保存爲 Sample4.c ,C 語言源文件的擴展名,一般用.c 來表示,上面程序是提供兩個輸入參數並返回它們之和。

在 Windows 操做系統下打開命令提示符,切換到保存 Sample4.c 的文件夾下,而後在命令提示符中輸入

 bcc32-c-SSample4.c

做爲編譯的結果,當前目錄下會生成一個名爲Sample4.asm的彙編語言源代碼。彙編語言源文件的擴展名,一般用.asm來表示,下面就讓咱們用編輯器打開看一下 Sample4.asm 中的內容

 .386p
 ifdef??version
 if  ??version GT 500H
 .mmx
 endif
 endif
 modelflat
 ifndef??version
 ?debugmacro
 endm
 endif
 ?debugS"Sample4.c"
 ?debugT"Sample4.c"
 _TEXTsegment dword public use32 'CODE'
 _TEXTends
 _DATAsegment dword public use32 'DATA'
 _DATAends
 _BSSsegment dword public use32 'BSS'
 _BSSends
 DGROUPgroup_BSS,_DATA
 _TEXTsegment dword public use32 'CODE'
 _AddNumprocnear
 ?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
 _AddNumendp
 _MyFuncprocnear
 ?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
 _MyFuncendp
 _TEXTends
 public_AddNum
 public_MyFunc
 ?debugD"Sample4.c"20343 45835
 end

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

不會轉換成本地代碼的僞指令

第一次看到彙編代碼的讀者可能感受起來比較難,不過實際上其實比較簡單,並且可能比 C 語言還要簡單,爲了便於閱讀彙編代碼的源代碼,須要注意幾個要點

彙編語言的源代碼,是由轉換成本地代碼的指令(後面講述的操做碼)和針對彙編器的僞指令構成的。僞指令負責把程序的構造以及彙編的方法指示給彙編器(轉換程序)。不過僞指令是沒法彙編轉換成爲本地代碼的。下面是上面程序截取的僞指令

 _TEXTsegment dword public use32 'CODE'
 _TEXTends
 _DATAsegment dword public use32 'DATA'
 _DATAends
 _BSSsegment dword public use32 'BSS'
 _BSSends
 DGROUPgroup_BSS,_DATA
 ​
 _AddNumprocnear
 _AddNumendp
 ​
 _MyFuncprocnear
 _MyFuncendp
 ​
 _TEXTends
 end

由僞指令 segmentends 圍起來的部分,是給構成程序的命令和數據的集合體上加一個名字而獲得的,稱爲段定義。段定義的英文表達具備區域的意思,在這個程序中,段定義指的是命令和數據等程序的集合體的意思,一個程序由多個段定義構成。

上面代碼的開始位置,定義了3個名稱分別爲_TEXT、_DATA、_BSS的段定義,_TEXT是指定的段定義,_DATA是被初始化(有初始值)的數據的段定義,_BSS是還沒有初始化的數據的段定義。這種定義的名稱是由 Borland C++ 定義的,是由 Borland C++ 編譯器自動分配的,因此程序段定義的順序就成爲了_TEXT、_DATA、_BSS,這樣也確保了內存的連續性

 _TEXTsegment dword public use32 'CODE'
 _TEXTends
 _DATAsegment dword public use32 'DATA'
 _DATAends
 _BSSsegment dword public use32 'BSS'
 _BSSends

段定義( segment ) 是用來區分或者劃分範圍區域的意思。彙編語言的 segment 僞指令表示段定義的起始,ends 僞指令表示段定義的結束。段定義是一段連續的內存空間

group 這個僞指令表示的是將 _BSS和_DATA 這兩個段定義彙總名爲 DGROUP 的組

 DGROUPgroup_BSS,_DATA

圍起_AddNum_MyFun_TEXTsegment 和_TEXTends ,表示_AddNum_MyFun是屬於_TEXT這一段定義的。

 _TEXTsegment dword public use32 'CODE'
 _TEXTends

所以,即便在源代碼中指令和數據是混雜編寫的,通過編譯和彙編後,也會轉換成爲規整的本地代碼。

_AddNum proc_AddNum endp 圍起來的部分,以及_MyFunc proc_MyFunc endp 圍起來的部分,分別表示 AddNum 函數和 MyFunc 函數的範圍。

1 _AddNum proc near

2 _AddNum endp

3

4 _MyFunc proc near

5 _MyFunc endp

編譯後在函數名前附帶上下劃線_ ,是 Borland C++ 的規定。在 C 語言中編寫的 AddNum 函數,在內部是以 _AddNum 這個名稱處理的。僞指令 proc 和 endp 圍起來的部分,表示的是 過程(procedure) 的範圍。在彙編語言中,這種至關於 C 語言的函數的形式稱爲過程。

末尾的end僞指令,表示的是源代碼的結束。

彙編語言的語法是 操做碼 + 操做數

在彙編語言中,一行表示一對 CPU 的一個指令。彙編語言指令的語法結構是操做碼 + 操做數,也存在只有操做碼沒有操做數的指令。

操做碼錶示的是指令動做,操做數表示的是指令對象。操做碼和操做數一塊兒使用就是一個英文指令。好比從英語語法來分析的話,操做碼是動詞,操做數是賓語。好比這個句子Give me money這個英文指令的話,Give 就是操做碼,me 和 money 就是操做數。彙編語言中存在多個操做數的狀況,要用逗號把它們分割,就像是 Give me,money 這樣。

可以使用何種形式的操做碼,是由 CPU 的種類決定的,下面對操做碼的功能進行了整理。

image.png

本地代碼須要加載到內存後才能運行,內存中存儲着構成本地代碼的指令和數據。程序運行時,CPU會從內存中把數據和指令讀出來,而後放在 CPU 內部的寄存器中進行處理。

image.png

若是 CPU 和內存的關係你還不是很瞭解的話,請閱讀做者的另外一篇文章 程序員須要瞭解的硬核知識之CPU 詳細瞭解。

寄存器是 CPU 中的存儲區域,寄存器除了具備臨時存儲和計算的功能以外,還具備運算功能,x86 系列的主要種類和角色以下圖所示

image.png

指令解析

下面就對 CPU 中的指令進行分析

最經常使用的 mov 指令

指令中最常使用的是對寄存器和內存進行數據存儲的 mov 指令,mov 指令的兩個操做數,分別用來指定數據的存儲地和讀出源。操做數中能夠指定寄存器、常數、標籤(附加在地址前),以及用方括號([]) 圍起來的這些內容。若是指定了沒有用([]) 方括號圍起來的內容,就表示對該值進行處理;若是指定了用方括號圍起來的內容,方括號的值則會被解釋爲內存地址,而後就會對該內存地址對應的值進行讀寫操做。讓咱們對上面的代碼片斷進行說明

 mov     ebp,esp
 mov     eax,dword ptr [ebp+8]

mov ebp,esp 中,esp 寄存器中的值被直接存儲在了 ebp 中,也就是說,若是 esp 寄存器的值是100的話那麼 ebp 寄存器的值也是 100。

而在 mov eax,dword ptr [ebp+8] 這條指令中,ebp 寄存器的值 + 8 後會被解析稱爲內存地址。若是 ebp

寄存器的值是100的話,那麼 eax 寄存器的值就是 100 + 8 的地址的值。dword ptr 也叫作 double word pointer 簡單解釋一下就是從指定的內存地址中讀出4字節的數據

對棧進行 push 和 pop

程序運行時,會在內存上申請分配一個稱爲棧的數據空間。棧(stack)的特性是後入先出,數據在存儲時是從內存的下層(大的地址編號)逐漸往上層(小的地址編號)累積,讀出時則是按照從上往下進行讀取的。

image.png

棧是存儲臨時數據的區域,它的特色是經過 push 指令和 pop 指令進行數據的存儲和讀出。向棧中存儲數據稱爲入棧,從棧中讀出數據稱爲出棧,32位 x86 系列的 CPU 中,進行1次 push 或者 pop,便可處理 32 位(4字節)的數據。

函數的調用機制

下面咱們一塊兒來分析一下函數的調用機制,咱們以上面的 C 語言編寫的代碼爲例。首先,讓咱們從MyFunc函數調用AddNum函數的彙編語言部分開始,來對函數的調用機制進行說明。棧在函數的調用中發揮了巨大的做用,下面是通過處理後的 MyFunc 函數的彙編處理內容

 _MyFuncprocnear
 pushebp ; 將 ebp 寄存器的值存入棧中             (1)
 movebp,esp; 將 esp 寄存器的值存入 ebp 寄存器中  (2)
 push456; 將 456 入棧(3)
 push123; 將 123 入棧(4)
 call_AddNum; 調用 AddNum 函數(5)
 addesp,8; esp 寄存器的值 + 8(6)
 popebp; 讀出棧中的數值存入 esp 寄存器中(7)
 ret; 結束 MyFunc 函數,返回到調用源(8)
 _MyFuncendp

代碼解釋中的(1)、(2)、(7)、(8)的處理適用於 C 語言中的全部函數,咱們會在後面展現AddNum函數處理內容時進行說明。這裏但願你們先關注(3) - (6) 這一部分,這對了解函數調用機制相當重要。

(3) 和 (4) 表示的是將傳遞給 AddNum 函數的參數經過 push 入棧。在 C 語言源代碼中,雖然記述爲函數 AddNum(123,456),但入棧時則會先按照 456,123 這樣的順序。也就是位於後面的數值先入棧。這是 C 語言的規定。(5) 表示的 call 指令,會把程序流程跳轉到 AddNum 函數指令的地址處。在彙編語言中,函數名表示的就是函數所在的內存地址。AddNum 函數處理完畢後,程序流程必需要返回到編號(6) 這一行。call 指令運行後,call 指令的下一行(也就指的是 (6) 這一行)的內存地址(調用函數完畢後要返回的內存地址)會自動的 push 入棧。該值會在 AddNum 函數處理的最後經過ret指令 pop 出棧,而後程序會返回到 (6) 這一行。

(6) 部分會把棧中存儲的兩個參數 (456 和 123) 進行銷燬處理。雖然經過兩次的 pop 指令也能夠實現,不過採用 esp 寄存器 + 8 的方式會更有效率(處理 1 次便可)。對棧進行數值的輸入和輸出時,數值的單位是4字節。所以,經過在負責棧地址管理的 esp 寄存器中加上4的2倍8,就能夠達到和運行兩次 pop 命令一樣的效果。雖然內存中的數據實際上還殘留着,但只要把 esp 寄存器的值更新爲數據存儲地址前面的數據位置,該數據也就至關於銷燬了。

我在編譯Sample4.c文件時,出現了下圖的這條消息

image.png

圖中的意思是指 c 的值在 MyFunc 定義了可是一直未被使用,這實際上是一項編譯器優化的功能,因爲存儲着 AddNum 函數返回值的變量 c 在後面沒有被用到,所以編譯器就認爲該變量沒有意義,進而也就沒有生成與之對應的彙編語言代碼

下圖是調用 AddNum 這一函數先後棧內存的變化

image.png

函數的內部處理

上面咱們用匯編代碼分析了一下 Sample4.c 整個過程的代碼,如今咱們着重分析一下 AddNum 函數的源代碼部分,分析一下參數的接收、返回值和返回等機制

 _AddNumprocnear
 pushebp              -----------(1)
 movebp,esp              -----------(2)
 moveax,dword ptr[ebp+8]   -----------(3)
 addeax,dword ptr[ebp+12] -----------(4)
 popebp-----------(5)
 ret----------------------------------(6)
 _AddNumendp

ebp 寄存器的值在(1)中入棧,在(5)中出棧,這主要是爲了把函數中用到的 ebp 寄存器的內容,恢復到函數調用前的狀態。

(2) 中把負責管理棧地址的 esp 寄存器的值賦值到了 ebp 寄存器中。這是由於,在 mov 指令中方括號內的參數,是不容許指定 esp 寄存器的。所以,這裏就採用了不直接經過 esp,而是用 ebp 寄存器來讀寫棧內容的方法。

(3) 使用[ebp + 8] 指定棧中存儲的第1個參數123,並將其讀出到 eax 寄存器中。像這樣,不使用 pop 指令,也能夠參照棧的內容。而之因此從多個寄存器中選擇了 eax 寄存器,是由於 eax 是負責運算的累加寄存器。

經過(4) 的 add 指令,把當前 eax 寄存器的值同第2個參數相加後的結果存儲在 eax 寄存器中。[ebp + 12] 是用來指定第2個參數456的。在 C 語言中,函數的返回值必須經過 eax 寄存器返回,這也是規定。也就是函數的參數是經過棧來傳遞,返回值是經過寄存器返回的

(6) 中 ret 指令運行後,函數返回目的地內存地址會自動出棧,據此,程序流程就會跳轉返回到(6) (Call _AddNum)的下一行。這時,AddNum 函數入口和出口處棧的狀態變化,就以下圖所示

image.png

全局變量和局部變量

在熟悉了彙編語言後,接下來咱們來了解一下全局變量和局部變量,在函數外部定義的變量稱爲全局變量,在函數內部定義的變量稱爲局部變量,全局變量能夠在任意函數中使用,局部變量只能在函數定義局部變量的內部使用。下面,咱們就經過彙編語言來看一下全局變量和局部變量的不一樣之處。

下面定義的 C 語言代碼分別定義了局部變量和全局變量,而且給各變量進行了賦值,咱們先看一下源代碼部分

 // 定義被初始化的全局變量
 inta1=1;
 inta2=2;
 inta3=3;
 inta4=4;
 inta5=5;
 ​
 // 定義沒有初始化的全局變量
 intb1,b2,b3,b4,b5;
 ​
 // 定義函數
 voidMyFunc(){
  // 定義局部變量
  intc1,c2,c3,c4,c5,c6,c7,c8,c9,c10;
 
  // 給局部變量賦值
  c1=1;
  c2=2;
  c3=3;
  c4=4;
  c5=5;
  c6=6;
  c7=7;
  c8=8;
  c9=9;
  c10=10;
 
  // 把局部變量賦值給全局變量
  a1=c1;
  a2=c2;
  a3=c3;
  a4=c4;
  a5=c5;
  b1=c6;
  b2=c7;
  b3=c8;
  b4=c9;
  b5=c10;
 }

上面的代碼挺暴力的,不過不要緊,可以便於咱們分析其彙編源碼就好,咱們用 Borland C++ 編譯後的彙編代碼以下,編譯完成後的源碼比較長,這裏咱們只拿出來一部分做爲分析使用(咱們改變了一下段定義順序,刪除了部分註釋)

上面的代碼挺暴力的,不過不要緊,可以便於咱們分析其彙編源碼就好,咱們用 Borland C++ 編譯後的彙編代碼以下,編譯完成後的源碼比較長,這裏咱們只拿出來一部分做爲分析使用(咱們改變了一下段定義順序,刪除了部分註釋)

 _DATAsegment dword public use32 'DATA'
  align4
  _a1label dword
  dd1
  align4
  _a2label dword
  dd2
  align4
  _a3label dword
  dd3
  align4
  _a4label dword
  dd4
  align4
  _a5label dword
  dd5
 _DATAends
 ​
 _BSSsegment dword public use32 'BSS'
 align4
  _b1label dword
  db4dup(?)
  align4
  _b2label dword
  db4dup(?)
  align4
  _b3label dword
  db4dup(?)
  align4
  _b4label dword
  db4dup(?)
  align4
  _b5label dword
  db4dup(?)
 _BSSends
 ​
 _TEXTsegment dword public use32 'CODE'
 _MyFuncproc near
 ​
 push     ebp
 mov     ebp,esp
 add     esp,-20
 push     ebx
 push     esi
 mov     eax,1
 mov     edx,2
 mov     ecx,3
 mov     ebx,4
 mov     esi,5
 mov     dwordptr[ebp-4],6
 mov     dwordptr[ebp-8],7
 mov     dwordptr[ebp-12],8
 mov     dwordptr[ebp-16],9
 mov     dwordptr[ebp-20],10
 mov     dwordptr[_a1],eax
 mov     dwordptr[_a2],edx
 mov     dwordptr[_a3],ecx
 mov     dwordptr[_a4],ebx
 mov     dwordptr[_a5],esi
 mov     eax,dword ptr [ebp-4]
 mov     dwordptr[_b1],eax
 mov     edx,dword ptr [ebp-8]
 mov     dwordptr[_b2],edx
 mov     ecx,dword ptr [ebp-12]
 mov     dwordptr[_b3],ecx
 mov     eax,dword ptr [ebp-16]
 mov     dwordptr[_b4],eax
 mov     edx,dword ptr [ebp-20]
 mov     dwordptr[_b5],edx
 pop     esi
 pop     ebx
 mov     esp,ebp
 pop     ebp
 ret
 
 _MyFunc  endp
 _TEXTends

編譯後的程序,會被歸類到名爲段定義的組。

  • 初始化的全局變量,會彙總到名爲 _DATA 的段定義中

 _DATAsegment dword public use32 'DATA'
 ...
 _DATAends

  • 沒有初始化的全局變量,會彙總到名爲 _BSS 的段定義中

 _BSSsegment dword public use32 'BSS'
 ...
 _BSSends

  • 被段定義 _TEXT 圍起來的彙編代碼則是 Borland C++ 的定義

 _TEXTsegment dword public use32 'CODE'
 _MyFuncproc near
 ...
 _MyFunc  endp
 _TEXTends

咱們在分析上面彙編代碼以前,先來認識一下更多的彙編指令,此表是對上面部分操做碼及其功能的接續

image.png

咱們首先來看一下_DATA段定義的內容。_a1 label dword定義了_a1這個標籤。標籤表示的是相對於段定義起始位置的位置。因爲_a1_DATA 段定義的開頭位置,因此相對位置是0。_a1就至關因而全局變量a1。編譯後的函數名和變量名前面會加一個(_),這也是 Borland C++ 的規定。dd 1指的是,申請分配了4字節的內存空間,存儲着1這個初始值。 dd指的是define double word表示有兩個長度爲2的字節領域(word),也就是4字節的意思。

Borland C++ 中,因爲int類型的長度是4字節,所以彙編器就把 int a1 = 1 變換成了_a1 label dword 和 dd 1。一樣,這裏也定義了至關於全局變量的 a2 - a5 的標籤_a2 - _a5,它們各自的初始值 2 - 5 也被存儲在各自的4字節中。

接下來,咱們來講一說_BSS段定義的內容。這裏定義了至關於全局變量 b1 - b5 的標籤_b1 - _b5。其中的db 4dup(?)表示的是申請分配了4字節的領域,但值還沒有肯定(這裏用 ? 來表示)的意思。db(define byte)表示有1個長度是1字節的內存空間。於是,db 4 dup(?) 的狀況下,就是4字節的內存空間。

注意:db 4 dup(?) 不要和 dd 4 混淆了,前者表示的是4個長度是1字節的內存空間。而 db 4 表示的則是雙字節( = 4 字節) 的內存空間中存儲的值是 4

臨時確保局部變量使用的內存空間

咱們知道,局部變量是臨時保存在寄存器和棧中的。函數內部利用棧進行局部變量的存儲,函數調用完成後,局部變量值被銷燬,可是寄存器可能用於其餘目的。因此,局部變量只是函數在處理期間臨時存儲在寄存器和棧中的

回想一下上述代碼是否是定義了10個局部變量?這是爲了表示存儲局部變量的不只僅是棧,還有寄存器。爲了確保 c1 - c10 所需的域,寄存器空閒的時候就會使用寄存器,寄存器空間不足的時候就會使用棧。

讓咱們繼續來分析上面代碼的內容。_TEXT段定義表示的是 MyFunc 函數的範圍。在 MyFunc 函數中定義的局部變量所須要的內存領域。會被儘量的分配在寄存器中。你們可能認爲使用高性能的寄存器來替代普通的內存是一種資源浪費,可是編譯器不這麼認爲,只要寄存器有空間,編譯器就會使用它。因爲寄存器的訪問速度遠高於內存,因此直接訪問寄存器可以高效的處理。局部變量使用寄存器,是 Borland C++ 編譯器最優化的運行結果。

代碼清單中的以下內容表示的是向寄存器中分配局部變量的部分

1 mov       eax,1

2 mov       edx,2

3 mov       ecx,3

4 mov       ebx,4

5 mov       esi,5

僅僅對局部變量進行定義是不夠的,只有在給局部變量賦值時,纔會被分配到寄存器的內存區域。上述代碼至關於就是給5個局部變量 c1 - c5 分別賦值爲 1 - 5。eax、edx、ecx、ebx、esi 是 x86 系列32位 CPU 寄存器的名稱。至於使用哪一個寄存器,是由編譯器來決定的 。

x86 系列 CPU 擁有的寄存器中,程序能夠操做的是十幾,其中空閒的最多會有幾個。於是,局部變量超過寄存器數量的時候,可分配的寄存器就不夠用了,這種狀況下,編譯器就會把棧派上用場,用來存儲剩餘的局部變量。

在上述代碼這一部分,給局部變量c1 - c5 分配完寄存器後,可用的寄存器數量就不足了。因而,剩下的5個局部變量c6 - c10 就被分配給了棧的內存空間。以下面代碼所示

 mov     dwordptr[ebp-4],6
 mov     dwordptr[ebp-8],7
 mov     dwordptr[ebp-12],8
 mov     dwordptr[ebp-16],9
 mov     dwordptr[ebp-20],10

函數入口add esp,-20指的是,對棧數據存儲位置的 esp 寄存器(棧指針)的值作減20的處理。爲了確保內存變量 c6 - c10 在棧中,就須要保留5個 int 類型的局部變量(4字節 * 5 = 20 字節)所需的空間。mov ebp,esp這行指令表示的意思是將 esp 寄存器的值賦值到 ebp 寄存器。之因此須要這麼處理,是爲了經過在函數出口處mov esp ebp這一處理,把 esp 寄存器的值還原到原始狀態,從而對申請分配的棧空間進行釋放,這時棧中用到的局部變量就消失了。這也是棧的清理處理。在使用寄存器的狀況下,局部變量則會在寄存器被用於其餘用途時自動消失,以下圖所示。

image.png

1 mov       dword ptr [ebp-4],6

2 mov       dword ptr [ebp-8],7

3 mov       dword ptr [ebp-12],8

4 mov       dword ptr [ebp-16],9

5 mov       dword ptr [ebp-20],10

這五行代碼是往棧空間代入數值的部分,因爲在向棧申請內存空間前,藉助了 mov ebp, esp 這個處理,esp 寄存器的值被保存到了 esp 寄存器中,所以,經過使用[ebp - 4]、[ebp - 8]、[ebp - 12]、[ebp - 16]、[ebp - 20] 這樣的形式,就能夠申請分配20字節的棧內存空間切分紅5個長度爲4字節的空間來使用。例如,mov dword ptr [ebp-4],6 表示的就是,從申請分配的內存空間的下端(ebp寄存器指示的位置)開始向前4字節的地址([ebp - 4]) 中,存儲着6這一4字節數據。

image.png

循環控制語句的處理

上面說的都是順序流程,那麼如今就讓咱們分析一下循環流程的處理,看一下 for 循環以及 if 條件分支等 c 語言程序的 流程控制是如何實現的,咱們仍是以代碼以及編譯後的結果爲例,看一下程序控制流程的處理過程。

 // 定義MySub 函數
 voidMySub(){
  // 不作任何處理
 
 }
 ​
 // 定義MyFunc 函數
 voidMyfunc(){
  inti;
  for(inti=0;i<10;i++){
    // 重複調用MySub十次
    MySub();
 }
 }

上述代碼將局部變量 i 做爲循環條件,循環調用十次MySub函數,下面是它主要的彙編代碼

 xorebx,ebx; 將寄存器清0
 @4 call_MySub; 調用MySub函數
 incebx; ebx寄存器的值 + 1
 cmpebx,10;將ebx寄存器的值和10進行比較
 jlshort@4; 若是小於10就跳轉到 @4

C 語言中的 for 語句是經過在括號中指定循環計數器的初始值(i = 0)、循環的繼續條件(i < 10)、循環計數器的更新(i++) 這三種形式來進行循環處理的。與此相對的彙編代碼就是經過比較指令(cmp)跳轉指令(jl)來實現的。

下面咱們來對上述代碼進行說明

MyFunc函數中用到的局部變量只有 i ,變量 i 申請分配了 ebx 寄存器的內存空間。for 語句括號中的 i = 0 被轉換爲xor ebx,ebx這一處理,xor 指令會對左起第一個操做數和右起第二個操做數進行 XOR 運算,而後把結果存儲在第一個操做數中。因爲這裏把第一個操做數和第二個操做數都指定爲了 ebx,所以就變成了對相同數值的 XOR 運算。也就是說無論當前寄存器的值是什麼,最終的結果都是0。相似的,咱們使用mov ebx,0也能獲得相同的結果,可是 xor 指令的處理速度更快,並且編譯器也會啓動最優化功能。

XOR 指的就是異或操做,它的運算規則是 若是a、b兩個值不相同,則異或結果爲1。若是a、b兩個值相同,異或結果爲0

ebx 寄存器的值初始化後,會經過 call 指定調用 _MySub 函數,從 _MySub 函數返回後,會執行inc ebx 指令,對 ebx 的值進行 + 1 操做,這個操做就至關於 i++ 的意思,++ 表示的就是當前數值 + 1。

這裏須要知道 i++ 和 ++i 的區別

i++ 是先賦值,複製完成後再對 i執行 + 1 操做

++i 是先進行 +1 操做,完成後再進行賦值

inc 下一行的 cmp 是用來對第一個操做數和第二個操做數的數值進行比較的指令。 cmp ebx,10 就至關於 C 語言中的 i < 10 這一處理,意思是把 ebx 寄存器的值與10進行比較。彙編語言中比較指令的結果,會存儲在 CPU 的標誌寄存器中。不過,標誌寄存器的值,程序是沒法直接參考的。那如何判斷比較結果呢?

彙編語言中有多個跳轉指令,這些跳轉指令會根據標誌寄存器的值來判斷是否進行跳轉操做,例如最後一行的 jl,它會根據 cmp ebx,10 指令所存儲在標誌寄存器中的值來判斷是否跳轉,jl 這條指令表示的就是 jump on less than(小於的話就跳轉)。發現若是 i 比 10 小,就會跳轉到 @4 所在的指令處繼續執行。

那麼彙編代碼的意思也能夠用 C 語言來改寫一下,加深理解

 i^=i;
 L4:MySub();
 i++;
 if(i<10)gotoL4;

代碼第一行 i ^= i 指的就是 i 和 i 進行異或運算,也就是 XOR 運算,MySub() 函數用 L4 標籤來替代,而後進行 i 自增操做,若是i 的值小於 10 的話,就會一直循環 MySub() 函數。

條件分支的處理方法

條件分支的處理方式和循環的處理方式很類似,使用的也是 cmp 指令和跳轉指令。下面是用 C 語言編寫的條件分支的代碼

 // 定義MySub1 函數
 voidMySub1(){
 ​
 // 不作任何處理
 }
 ​
 // 定義MySub2 函數
 voidMySub2(){
 
 // 不作任何處理
 }
 ​
 // 定義MySub3 函數
 voidMySub3(){
 ​
 // 不作任何處理
 }
 ​
 // 定義MyFunc 函數
 voidMyFunc(){
 ​
 inta=123;
 // 根據條件調用不一樣的函數
 if(a>100){
  MySub1();
 }
 elseif(a<50){
  MySub2();
 }
 else
 {
  MySub3();
 }
 ​
 }

很簡單的一個實現了條件判斷的 C 語言代碼,那麼咱們把它用 Borland C++ 編譯以後的結果以下

 _MyFuncproc near
 push     ebp
 mov     ebp,esp
 mov     eax,123; 把123存入 eax 寄存器中
 cmp     eax,100; 把 eax 寄存器的值同100進行比較
 jle     short@8; 比100小時,跳轉到@8標籤
 call     _MySub1; 調用MySub1函數
 jmpshort@11; 跳轉到@11標籤
 @8:
 cmp     eax,50; 把 eax 寄存器的值同50進行比較
 jge     short@10; 比50大時,跳轉到@10標籤
 call     _MySub2; 調用MySub2函數
 jmpshort@11; 跳轉到@11標籤
 @10:
 call     _MySub3; 調用MySub3函數
 @11:
 pop     ebp
 ret
 _MyFuncendp

上面代碼用到了三種跳轉指令,分別是jle(jump on less or equal) 比較結果小時跳轉,jge(jump on greater or equal) 比較結果大時跳轉,還有無論結果怎樣都會進行跳轉的jmp,在這些跳轉指令以前還有用來比較的指令 cmp,構成了上述彙編代碼的主要邏輯形式。

瞭解程序運行邏輯的必要性

經過對上述彙編代碼和 C 語言源代碼進行比較,想必你們對程序的運行方式有了新的理解,並且,從彙編源代碼中獲取的知識,也有助於瞭解 Java 等高級語言的特性,好比 Java 中就有 native 關鍵字修飾的變量,那麼這個變量的底層就是使用 C 語言編寫的,還有一些 Java 中的語法糖只有經過彙編代碼才能知道其運行邏輯。在某些狀況下,對於查找 bug 的緣由也是有幫助的。

上面咱們瞭解到的編程方式都是串行處理的,那麼串行處理有什麼特色呢?

image.png

串行處理最大的一個特色就是專心只作一件事情,一件事情作完以後纔會去作另一件事情。

image.png

咱們仍是舉個實際的例子,讓咱們來看一段代碼

 // 定義全局變量
 intcounter=100;
 ​
 // 定義MyFunc1()
 voidMyFunc(){
  counter*=2;
 }
 ​
 // 定義MyFunc2()
 voidMyFunc2(){
  counter*=2;
 }

上述代碼是更新 counter 的值的 C 語言程序,MyFunc1() 和 MyFunc2() 的處理內容都是把 counter 的值擴大至原來的二倍,而後再把 counter 的值賦值給 counter 。這裏,咱們假設使用多線程處理,同時調用了一次MyFunc1 和 MyFunc2 函數,這時,全局變量 counter 的值,理應編程 100 * 2 * 2 = 400。若是你開啓了多個線程的話,你會發現 counter 的數值有時也是 200,對於爲何出現這種狀況,若是你不瞭解程序的運行方式,是很難找到緣由的。

咱們將上面的代碼轉換成彙編語言的代碼以下

 moveax,dword ptr[_counter]; 將 counter 的值讀入 eax 寄存器
 addeax,eax; 將 eax 寄存器的值擴大2倍。
 movdwordptr[_counter],eax; 將 eax 寄存器的值存入 counter 中。

在多線程程序中,用匯編語言表示的代碼每運行一行,處理都有可能切換到其餘線程中。於是,假設 MyFun1 函數在讀出 counter 數值100後,還將來得及將它的二倍值200寫入 counter 時,正巧 MyFun2 函數讀出了 counter 的值100,那麼結果就將變爲 200 。

image.png

爲了不該bug,咱們能夠採用以函數或 C 語言代碼的行爲單位來禁止線程切換的鎖定方法,或者使用某種線程安全的方式來避免該問題的出現。

如今基本上沒有人用匯編語言來編寫程序了,由於 C、Java等高級語言的效率要比彙編語言快不少。不過,彙編語言的經驗仍是很重要的,經過藉助彙編語言,咱們能夠更好的瞭解計算機運行機制。

文章參考

https://www.computerhope.com/jargon/m/memory.htm

https://baike.baidu.com/item/隊列/14580481?fr=aladdin

https://baike.baidu.com/item/棧/12808149?fr=aladdin

https://baike.baidu.com/item/環形緩衝器/22701730?fr=aladdin

《程序是怎樣跑起來的》

https://baike.baidu.com/item/彙編語言/61826?fr=aladdin

https://baike.baidu.com/item/Windows操做系統/852149?fr=aladdin

磁盤

磁盤緩存

虛擬內存

https://baike.baidu.com/item/壓縮算法/2762648

https://en.wikipedia.org/wiki/Central_processing_unit

https://www.digitaltrends.com/computing/what-is-a-cpu/

https://baike.baidu.com/item/寄存器/187682?fr=aladdin

https://baike.baidu.com/item/內存/103614?fr=aladdin

http://www.javashuo.com/article/p-plsuekss-kh.html

https://baike.baidu.com/item/程序計數器/3219536?fr=aladdin

https://zhidao.baidu.com/question/124425422.html

二維碼.png

相關文章
相關標籤/搜索