對不起射窒,學(xué)會這些計算機(jī)基礎(chǔ)知識后我飄了

來自公眾號:Java建設(shè)者
作者cxuan

我們每個程序員或許都有一個夢,那就是成為大牛将塑,我們或許都沉浸在各種框架中脉顿,以為框架就是一切,以為應(yīng)用層才是最重要的点寥,你錯了艾疟。在當(dāng)今計算機(jī)行業(yè)中,會應(yīng)用是基本素質(zhì)敢辩,如果你懂其原理才能讓你在行業(yè)中走的更遠(yuǎn)蔽莱,而計算機(jī)基礎(chǔ)知識又是重中之重。下面戚长,跟隨我的腳步盗冷,為你介紹一下計算機(jī)底層知識。

CPU

還不了解 CPU 嗎同廉?現(xiàn)在就帶你了解一下 CPU 是什么

CPU 的全稱是 Central Processing Unit正塌,它是你的電腦中最硬核的組件,這種說法一點不為過恤溶。CPU 是能夠讓你的計算機(jī)叫計算機(jī)的核心組件乓诽,但是它卻不能代表你的電腦,CPU 與計算機(jī)的關(guān)系就相當(dāng)于大腦和人的關(guān)系咒程。CPU 的核心是從程序或應(yīng)用程序獲取指令并執(zhí)行計算锭沟。此過程可以分為三個關(guān)鍵階段:提取论巍,解碼和執(zhí)行。CPU從系統(tǒng)的主存中提取指令,然后解碼該指令的實際內(nèi)容量九,然后再由 CPU 的相關(guān)部分執(zhí)行該指令。

CPU 內(nèi)部處理過程

下圖展示了一般程序的運行流程(以 C 語言為例)舷丹,可以說了解程序的運行流程是掌握程序運行機(jī)制的基礎(chǔ)和前提涮毫。

image

在這個流程中,CPU 負(fù)責(zé)的就是解釋和運行最終轉(zhuǎn)換成機(jī)器語言的內(nèi)容呢铆。

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

  • 控制單元:從內(nèi)存中提取指令并解碼執(zhí)行
  • 算數(shù)邏輯單元(ALU):處理算數(shù)和邏輯運算

CPU 是計算機(jī)的心臟和大腦晦鞋,它和內(nèi)存都是由許多晶體管組成的電子部件。它接收數(shù)據(jù)輸入棺克,執(zhí)行指令并處理信息悠垛。它與輸入/輸出(I / O)設(shè)備進(jìn)行通信,這些設(shè)備向 CPU 發(fā)送數(shù)據(jù)和從 CPU 接收數(shù)據(jù)娜谊。

從功能來看确买,CPU 的內(nèi)部由寄存器、控制器纱皆、運算器和時鐘四部分組成湾趾,各部分之間通過電信號連通芭商。

image
  • 寄存器是中央處理器內(nèi)的組成部分。它們可以用來暫存指令搀缠、數(shù)據(jù)和地址蓉坎。可以將其看作是內(nèi)存的一種胡嘿。根據(jù)種類的不同蛉艾,一個 CPU 內(nèi)部會有 20 - 100個寄存器。
  • 控制器負(fù)責(zé)把內(nèi)存上的指令衷敌、數(shù)據(jù)讀入寄存器勿侯,并根據(jù)指令的結(jié)果控制計算機(jī)
  • 運算器負(fù)責(zé)運算從內(nèi)存中讀入寄存器的數(shù)據(jù)
  • 時鐘 負(fù)責(zé)發(fā)出 CPU 開始計時的時鐘信號

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

在 CPU 的四個結(jié)構(gòu)中,我們程序員只需要了解寄存器就可以了缴罗,其余三個不用過多關(guān)注助琐,為什么這么說?因為程序是把寄存器作為對象來描述的面氓。

不同類型的 CPU 兵钮,其內(nèi)部寄存器的種類,數(shù)量以及寄存器存儲的數(shù)值范圍都是不同的舌界。不過掘譬,根據(jù)功能的不同,可以將寄存器劃分為下面這幾類

種類 功能
累加寄存器 存儲運行的數(shù)據(jù)和運算后的數(shù)據(jù)呻拌。
標(biāo)志寄存器 用于反應(yīng)處理器的狀態(tài)和運算結(jié)果的某些特征以及控制指令的執(zhí)行葱轩。
程序計數(shù)器 程序計數(shù)器是用于存放下一條指令所在單元的地址的地方。
基址寄存器 存儲數(shù)據(jù)內(nèi)存的起始位置
變址寄存器 存儲基址寄存器的相對地址
通用寄存器 存儲任意數(shù)據(jù)
指令寄存器 儲存正在被運行的指令藐握,CPU內(nèi)部使用靴拱,程序員無法對該寄存器進(jìn)行讀寫
棧寄存器 存儲棧區(qū)域的起始位置

其中程序計數(shù)器、累加寄存器猾普、標(biāo)志寄存器袜炕、指令寄存器和棧寄存器都只有一個,其他寄存器一般有多個初家。

image

下面就對各個寄存器進(jìn)行說明

程序計數(shù)器

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

程序執(zhí)行時,PC的初值為程序第一條指令的地址笤成,在順序執(zhí)行程序時评架,控制器首先按程序計數(shù)器所指出的指令地址從內(nèi)存中取出一條指令,然后分析和執(zhí)行該指令炕泳,同時將PC的值加1指向下一條要執(zhí)行的指令。

我們還是以一個事例為準(zhǔn)來詳細(xì)的看一下程序計數(shù)器的執(zhí)行過程

image

這是一段進(jìn)行相加的操作上祈,程序啟動培遵,在經(jīng)過編譯解析后會由操作系統(tǒng)把硬盤中的程序復(fù)制到內(nèi)存中浙芙,示例中的程序是將 123 和 456 執(zhí)行相加操作,并將結(jié)果輸出到顯示器上籽腕。

地址 0100 是程序運行的起始位置嗡呼。Windows 等操作系統(tǒng)把程序從硬盤復(fù)制到內(nèi)存后,會將程序計數(shù)器作為設(shè)定為起始位置 0100皇耗,然后執(zhí)行程序南窗,每執(zhí)行一條指令后,程序計數(shù)器的數(shù)值會增加1(或者直接指向下一條指令的地址)郎楼,然后万伤,CPU 就會根據(jù)程序計數(shù)器的數(shù)值,從內(nèi)存中讀取命令并執(zhí)行呜袁,也就是說敌买,程序計數(shù)器控制著程序的流程

條件分支和循環(huán)機(jī)制

高級語言中的條件控制流程主要分為三種:順序執(zhí)行阶界、條件分支虹钮、循環(huán)判斷三種,順序執(zhí)行是按照地址的內(nèi)容順序的執(zhí)行指令膘融。條件分支是根據(jù)條件執(zhí)行任意地址的指令芙粱。循環(huán)是重復(fù)執(zhí)行同一地址的指令。

  • 順序執(zhí)行的情況比較簡單氧映,每執(zhí)行一條指令程序計數(shù)器的值就是 + 1宅倒。
  • 條件和循環(huán)分支會使程序計數(shù)器的值指向任意的地址,這樣一來屯耸,程序便可以返回到上一個地址來重復(fù)執(zhí)行同一個指令拐迁,或者跳轉(zhuǎn)到任意指令。

下面以條件分支為例來說明程序的執(zhí)行過程(循環(huán)也很相似)

image

程序的開始過程和順序流程是一樣的疗绣,CPU 從0100處開始執(zhí)行命令线召,在0100和0101都是順序執(zhí)行,PC 的值順序+1多矮,執(zhí)行到0102地址的指令時缓淹,判斷0106寄存器的數(shù)值大于0,跳轉(zhuǎn)(jump)到0104地址的指令塔逃,將數(shù)值輸出到顯示器中讯壶,然后結(jié)束程序,0103 的指令被跳過了湾盗,這就和我們程序中的 if() 判斷是一樣的伏蚊,在不滿足條件的情況下,指令會直接跳過格粪。所以 PC 的執(zhí)行過程也就沒有直接+1躏吊,而是下一條指令的地址氛改。

標(biāo)志寄存器

條件和循環(huán)分支會使用到 jump(跳轉(zhuǎn)指令),會根據(jù)當(dāng)前的指令來判斷是否跳轉(zhuǎn)比伏,上面我們提到了標(biāo)志寄存器胜卤,無論當(dāng)前累加寄存器的運算結(jié)果是正數(shù)、負(fù)數(shù)還是零赁项,標(biāo)志寄存器都會將其保存

CPU 在進(jìn)行運算時葛躏,標(biāo)志寄存器的數(shù)值會根據(jù)當(dāng)前運算的結(jié)果自動設(shè)定,運算結(jié)果的正悠菜、負(fù)和零三種狀態(tài)由標(biāo)志寄存器的三個位表示舰攒。標(biāo)志寄存器的第一個字節(jié)位、第二個字節(jié)位李剖、第三個字節(jié)位各自的結(jié)果都為1時芒率,分別代表著正數(shù)、零和負(fù)數(shù)篙顺。

image

CPU 的執(zhí)行機(jī)制比較有意思偶芍,假設(shè)累加寄存器中存儲的 XXX 和通用寄存器中存儲的 YYY 做比較,執(zhí)行比較的背后德玫,CPU 的運算機(jī)制就會做減法運算匪蟀。而無論減法運算的結(jié)果是正數(shù)、零還是負(fù)數(shù)材彪,都會保存到標(biāo)志寄存器中骇吭。結(jié)果為正表示 XXX 比 YYY 大忽冻,結(jié)果為零表示 XXX 和 YYY 相等,結(jié)果為負(fù)表示 XXX 比 YYY 小怖侦。程序比較的指令篡悟,實際上是在 CPU 內(nèi)部做減法運算。

函數(shù)調(diào)用機(jī)制

接下來匾寝,我們繼續(xù)介紹函數(shù)調(diào)用機(jī)制搬葬,哪怕是高級語言編寫的程序,函數(shù)調(diào)用處理也是通過把程序計數(shù)器的值設(shè)定成函數(shù)的存儲地址來實現(xiàn)的艳悔。函數(shù)執(zhí)行跳轉(zhuǎn)指令后急凰,必須進(jìn)行返回處理,單純的指令跳轉(zhuǎn)沒有意義,下面是一個實現(xiàn)函數(shù)跳轉(zhuǎn)的例子

image

圖中將變量 a 和 b 分別賦值為 123 和 456 抡锈,調(diào)用 MyFun(a,b) 方法疾忍,進(jìn)行指令跳轉(zhuǎn)。圖中的地址是將 C 語言編譯成機(jī)器語言后運行時的地址床三,由于1行 C 程序在編譯后通常會變?yōu)槎嘈袡C(jī)器語言一罩,所以圖中的地址是分散的。在執(zhí)行完 MyFun(a,b)指令后撇簿,程序會返回到 MyFun(a,b) 的下一條指令聂渊,CPU 繼續(xù)執(zhí)行下面的指令。

函數(shù)的調(diào)用和返回很重要的兩個指令是 callreturn 指令四瘫,再將函數(shù)的入口地址設(shè)定到程序計數(shù)器之前汉嗽,call 指令會把調(diào)用函數(shù)后要執(zhí)行的指令地址存儲在名為棧的主存內(nèi)。函數(shù)處理完畢后找蜜,再通過函數(shù)的出口來執(zhí)行 return 指令饼暑。return 指令的功能是把保存在棧中的地址設(shè)定到程序計數(shù)器。MyFun 函數(shù)在被調(diào)用之前洗做,0154 地址保存在棧中弓叛,MyFun 函數(shù)處理完成后,會把 0154 的地址保存在程序計數(shù)器中竭望。這個調(diào)用過程如下

image

在一些高級語言的條件或者循環(huán)語句中邪码,函數(shù)調(diào)用的處理會轉(zhuǎn)換成 call 指令,函數(shù)結(jié)束后的處理則會轉(zhuǎn)換成 return 指令咬清。

通過地址和索引實現(xiàn)數(shù)組

接下來我們看一下基址寄存器和變址寄存器闭专,通過這兩個寄存器,我們可以對主存上的特定區(qū)域進(jìn)行劃分旧烧,來實現(xiàn)類似數(shù)組的操作影钉,首先,我們用十六進(jìn)制數(shù)將計算機(jī)內(nèi)存上的 00000000 - FFFFFFFF 的地址劃分出來掘剪。那么平委,凡是該范圍的內(nèi)存地址,只要有一個 32 位的寄存器夺谁,便可查看全部地址廉赔。但如果想要想數(shù)組那樣分割特定的內(nèi)存區(qū)域以達(dá)到連續(xù)查看的目的的話,使用兩個寄存器會更加方便匾鸥。

例如蜡塌,我們用兩個寄存器(基址寄存器和變址寄存器)來表示內(nèi)存的值

image

這種表示方式很類似數(shù)組的構(gòu)造,數(shù)組是指同樣長度的數(shù)據(jù)在內(nèi)存中進(jìn)行連續(xù)排列的數(shù)據(jù)構(gòu)造勿负。用數(shù)組名表示數(shù)組全部的值馏艾,通過索引來區(qū)分?jǐn)?shù)組的各個數(shù)據(jù)元素,例如: a[0] - a[4],[]內(nèi)的 0 - 4 就是數(shù)組的下標(biāo)琅摩。

CPU 指令執(zhí)行過程

幾乎所有的馮·諾伊曼型計算機(jī)的CPU铁孵,其工作都可以分為5個階段:取指令、指令譯碼房资、執(zhí)行指令蜕劝、訪存取數(shù)、結(jié)果寫回志膀。

  • 取指令階段是將內(nèi)存中的指令讀取到 CPU 中寄存器的過程熙宇,程序寄存器用于存儲下一條指令所在的地址
  • 指令譯碼階段鳖擒,在取指令完成后溉浙,立馬進(jìn)入指令譯碼階段,在指令譯碼階段蒋荚,指令譯碼器按照預(yù)定的指令格式戳稽,對取回的指令進(jìn)行拆分和解釋,識別區(qū)分出不同的指令類別以及各種獲取操作數(shù)的方法期升。
  • 執(zhí)行指令階段惊奇,譯碼完成后,就需要執(zhí)行這一條指令了播赁,此階段的任務(wù)是完成指令所規(guī)定的各種操作颂郎,具體實現(xiàn)指令的功能。
  • 訪問取數(shù)階段容为,根據(jù)指令的需要乓序,有可能需要從內(nèi)存中提取數(shù)據(jù),此階段的任務(wù)是:根據(jù)指令地址碼坎背,得到操作數(shù)在主存中的地址替劈,并從主存中讀取該操作數(shù)用于運算。
  • 結(jié)果寫回階段得滤,作為最后一個階段陨献,結(jié)果寫回(Write Back,WB)階段把執(zhí)行指令階段的運行結(jié)果數(shù)據(jù)“寫回”到某種存儲形式:結(jié)果數(shù)據(jù)經(jīng)常被寫到CPU的內(nèi)部寄存器中懂更,以便被后續(xù)的指令快速地存日R怠;

內(nèi)存

CPU 和 內(nèi)存就像是一堆不可分割的戀人一樣沮协,是無法拆散的一對兒龄捡,沒有內(nèi)存,CPU 無法執(zhí)行程序指令皂股,那么計算機(jī)也就失去了意義墅茉;只有內(nèi)存,無法執(zhí)行指令,那么計算機(jī)照樣無法運行就斤。

那么什么是內(nèi)存呢悍募?內(nèi)存和 CPU 如何進(jìn)行交互?下面就來介紹一下

什么是內(nèi)存

內(nèi)存(Memory)是計算機(jī)中最重要的部件之一洋机,它是程序與CPU進(jìn)行溝通的橋梁坠宴。計算機(jī)中所有程序的運行都是在內(nèi)存中進(jìn)行的,因此內(nèi)存對計算機(jī)的影響非常大绷旗,內(nèi)存又被稱為主存喜鼓,其作用是存放 CPU 中的運算數(shù)據(jù),以及與硬盤等外部存儲設(shè)備交換的數(shù)據(jù)衔肢。只要計算機(jī)在運行中庄岖,CPU 就會把需要運算的數(shù)據(jù)調(diào)到主存中進(jìn)行運算铣卡,當(dāng)運算完成后CPU再將結(jié)果傳送出來守屉,主存的運行也決定了計算機(jī)的穩(wěn)定運行。

內(nèi)存的物理結(jié)構(gòu)

內(nèi)存的內(nèi)部是由各種 IC 電路組成的征堪,它的種類很龐大邦尊,但是其主要分為三種存儲器

  • 隨機(jī)存儲器(RAM):內(nèi)存中最重要的一種背桐,表示既可以從中讀取數(shù)據(jù),也可以寫入數(shù)據(jù)蝉揍。當(dāng)機(jī)器關(guān)閉時链峭,內(nèi)存中的信息會 丟失
  • 只讀存儲器(ROM):ROM 一般只能用于數(shù)據(jù)的讀取又沾,不能寫入數(shù)據(jù)弊仪,但是當(dāng)機(jī)器停電時,這些數(shù)據(jù)不會丟失捍掺。
  • 高速緩存(Cache):Cache 也是我們經(jīng)常見到的撼短,它分為一級緩存(L1 Cache)、二級緩存(L2 Cache)挺勿、三級緩存(L3 Cache)這些數(shù)據(jù)曲横,它位于內(nèi)存和 CPU 之間,是一個讀寫速度比內(nèi)存更快的存儲器不瓶。當(dāng) CPU 向內(nèi)存寫入數(shù)據(jù)時禾嫉,這些數(shù)據(jù)也會被寫入高速緩存中。當(dāng) CPU 需要讀取數(shù)據(jù)時蚊丐,會直接從高速緩存中直接讀取熙参,當(dāng)然,如需要的數(shù)據(jù)在Cache中沒有麦备,CPU會再去讀取內(nèi)存中的數(shù)據(jù)孽椰。

內(nèi)存 IC 是一個完整的結(jié)構(gòu)昭娩,它內(nèi)部也有電源、地址信號黍匾、數(shù)據(jù)信號栏渺、控制信號和用于尋址的 IC 引腳來進(jìn)行數(shù)據(jù)的讀寫。下面是一個虛擬的 IC 引腳示意圖

image

圖中 VCC 和 GND 表示電源锐涯,A0 - A9 是地址信號的引腳磕诊,D0 - D7 表示的是控制信號、RD 和 WR 都是好控制信號纹腌,我用不同的顏色進(jìn)行了區(qū)分霎终,將電源連接到 VCC 和 GND 后,就可以對其他引腳傳遞 0 和 1 的信號升薯,大多數(shù)情況下莱褒,+5V 表示1,0V 表示 0覆劈。

我們都知道內(nèi)存是用來存儲數(shù)據(jù)保礼,那么這個內(nèi)存 IC 中能存儲多少數(shù)據(jù)呢?D0 - D7 表示的是數(shù)據(jù)信號责语,也就是說,一次可以輸入輸出 8 bit = 1 byte 的數(shù)據(jù)目派。A0 - A9 是地址信號共十個坤候,表示可以指定 00000 00000 - 11111 11111 共 2 的 10次方 = 1024個地址。每個地址都會存放 1 byte 的數(shù)據(jù)企蹭,因此我們可以得出內(nèi)存 IC 的容量就是 1 KB白筹。

內(nèi)存的讀寫過程

讓我們把關(guān)注點放在內(nèi)存 IC 對數(shù)據(jù)的讀寫過程上來吧!我們來看一個對內(nèi)存IC 進(jìn)行數(shù)據(jù)寫入和讀取的模型

image

來詳細(xì)描述一下這個過程谅摄,假設(shè)我們要向內(nèi)存 IC 中寫入 1byte 的數(shù)據(jù)的話徒河,它的過程是這樣的:

  • 首先給 VCC 接通 +5V 的電源,給 GND 接通 0V 的電源送漠,使用 A0 - A9 來指定數(shù)據(jù)的存儲場所顽照,然后再把數(shù)據(jù)的值輸入給 D0 - D7 的數(shù)據(jù)信號,并把 WR(write)的值置為 1闽寡,執(zhí)行完這些操作后代兵,即可以向內(nèi)存 IC 寫入數(shù)據(jù)
  • 讀出數(shù)據(jù)時,只需要通過 A0 - A9 的地址信號指定數(shù)據(jù)的存儲場所爷狈,然后再將 RD 的值置為 1 即可植影。
  • 圖中的 RD 和 WR 又被稱為控制信號。其中當(dāng)WR 和 RD 都為 0 時涎永,無法進(jìn)行寫入和讀取操作思币。

內(nèi)存的現(xiàn)實模型

為了便于記憶鹿响,我們把內(nèi)存模型映射成為我們現(xiàn)實世界的模型,在現(xiàn)實世界中谷饿,內(nèi)存的模型很想我們生活的樓房抢野。在這個樓房中,1層可以存儲一個字節(jié)的數(shù)據(jù)各墨,樓層號就是地址指孤,下面是內(nèi)存和樓層整合的模型圖

image

我們知道,程序中的數(shù)據(jù)不僅只有數(shù)值贬堵,還有數(shù)據(jù)類型的概念恃轩,從內(nèi)存上來看,就是占用內(nèi)存大欣枳觥(占用樓層數(shù))的意思叉跛。即使物理上強(qiáng)制以 1 個字節(jié)為單位來逐一讀寫數(shù)據(jù)的內(nèi)存,在程序中蒸殿,通過指定其數(shù)據(jù)類型筷厘,也能實現(xiàn)以特定字節(jié)數(shù)為單位來進(jìn)行讀寫。

二進(jìn)制

我們都知道宏所,計算機(jī)的底層都是使用二進(jìn)制數(shù)據(jù)進(jìn)行數(shù)據(jù)流傳輸?shù)乃盅蓿敲礊槭裁磿褂枚M(jìn)制表示計算機(jī)呢?或者說爬骤,什么是二進(jìn)制數(shù)呢充石?在拓展一步,如何使用二進(jìn)制進(jìn)行加減乘除霞玄?下面就來看一下

什么是二進(jìn)制數(shù)

那么什么是二進(jìn)制數(shù)呢骤铃?為了說明這個問題,我們先把 00100111 這個數(shù)轉(zhuǎn)換為十進(jìn)制數(shù)看一下坷剧,二進(jìn)制數(shù)轉(zhuǎn)換為十進(jìn)制數(shù)惰爬,直接將各位置上的值 * 位權(quán)即可,那么我們將上面的數(shù)值進(jìn)行轉(zhuǎn)換

image

也就是說惫企,二進(jìn)制數(shù)代表的 00100111 轉(zhuǎn)換成十進(jìn)制就是 39撕瞧,這個 39 并不是 3 和 9 兩個數(shù)字連著寫,而是 3 * 10 + 9 * 1雅任,這里面的 10 , 1 就是位權(quán)风范,以此類推,上述例子中的位權(quán)從高位到低位依次就是 7 6 5 4 3 2 1 0沪么。這個位權(quán)也叫做次冪硼婿,那么最高位就是2的7次冪,2的6次冪 等等禽车。二進(jìn)制數(shù)的運算每次都會以2為底寇漫,這個2 指得就是基數(shù)刊殉,那么十進(jìn)制數(shù)的基數(shù)也就是 10 。在任何情況下位權(quán)的值都是 數(shù)的位數(shù) - 1州胳,那么第一位的位權(quán)就是 1 - 1 = 0记焊, 第二位的位權(quán)就睡 2 - 1 = 1,以此類推栓撞。

那么我們所說的二進(jìn)制數(shù)其實就是 用0和1兩個數(shù)字來表示的數(shù)遍膜,它的基數(shù)為2,它的數(shù)值就是每個數(shù)的位數(shù) * 位權(quán)再求和得到的結(jié)果瓤湘,我們一般來說數(shù)值指的就是十進(jìn)制數(shù)瓢颅,那么它的數(shù)值就是 3 * 10 + 9 * 1 = 39弛说。

移位運算和乘除的關(guān)系

在了解過二進(jìn)制之后,下面我們來看一下二進(jìn)制的運算醒第,和十進(jìn)制數(shù)一樣渔嚷,加減乘除也適用于二進(jìn)制數(shù),只要注意逢 2 進(jìn)位即可淘讥。二進(jìn)制數(shù)的運算圃伶,也是計算機(jī)程序所特有的運算,因此了解二進(jìn)制的運算是必須要掌握的蒲列。

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

image

補數(shù)

剛才我們沒有介紹右移的情況蝗岖,是因為右移之后空出來的高位數(shù)值,有 0 和 1 兩種形式榔至。要想?yún)^(qū)分什么時候補0什么時候補1抵赢,首先就需要掌握二進(jìn)制數(shù)表示負(fù)數(shù)的方法。

二進(jìn)制數(shù)中表示負(fù)數(shù)值時唧取,一般會把最高位作為符號來使用铅鲤,因此我們把這個最高位當(dāng)作符號位。符號位是 0 時表示正數(shù)枫弟,是 1 時表示 負(fù)數(shù)邢享。那么 -1 用二進(jìn)制數(shù)該如何表示呢?可能很多人會這么認(rèn)為:因為 1 的二進(jìn)制數(shù)是 0000 0001淡诗,最高位是符號位骇塘,所以正確的表示 -1 應(yīng)該是 1000 0001伊履,但是這個答案真的對嗎?

計算機(jī)世界中是沒有減法的款违,計算機(jī)在做減法的時候其實就是在做加法唐瀑,也就是用加法來實現(xiàn)的減法運算。比如 100 - 50 插爹,其實計算機(jī)來看的時候應(yīng)該是 100 + (-50)哄辣,為此,在表示負(fù)數(shù)的時候就要用到二進(jìn)制補數(shù)赠尾,補數(shù)就是用正數(shù)來表示的負(fù)數(shù)力穗。

為了獲得補數(shù),我們需要將二進(jìn)制的各數(shù)位的數(shù)值全部取反萍虽,然后再將結(jié)果 + 1 即可睛廊,先記住這個結(jié)論,下面我們來演示一下杉编。

image

具體來說超全,就是需要先獲取某個數(shù)值的二進(jìn)制數(shù),然后對二進(jìn)制數(shù)的每一位做取反操作(0 ---> 1 , 1 ---> 0)邓馒,最后再對取反后的數(shù) +1 嘶朱,這樣就完成了補數(shù)的獲取。

補數(shù)的獲取光酣,雖然直觀上不易理解疏遏,但是邏輯上卻非常嚴(yán)謹(jǐn),比如我們來看一下 1 - 1 的這個過程救军,我們先用上面的這個 1000 0001(它是1的補數(shù)财异,不知道的請看上文,正確性先不管唱遭,只是用來做一下計算)來表示一下

image

奇怪戳寸,1 - 1 會變成 130 ,而不是0拷泽,所以可以得出結(jié)論 1000 0001 表示 -1 是完全錯誤的疫鹊。

那么正確的該如何表示呢?其實我們上面已經(jīng)給出結(jié)果了司致,那就是 1111 1111拆吆,來論證一下它的正確性

image

我們可以看到 1 - 1 其實實際上就是 1 + (-1),對 -1 進(jìn)行上面的取反 + 1 后變?yōu)?1111 1111, 然后與 1 進(jìn)行加法運算脂矫,得到的結(jié)果是九位的 1 0000 0000枣耀,結(jié)果發(fā)生了溢出,計算機(jī)會直接忽略掉溢出位羹唠,也就是直接拋掉 最高位 1 奕枢,變?yōu)?0000 0000娄昆。也就是 0,結(jié)果正確缝彬,所以 1111 1111 表示的就是 -1 萌焰。

所以負(fù)數(shù)的二進(jìn)制表示就是先求其補數(shù),補數(shù)的求解過程就是對原始數(shù)值的二進(jìn)制數(shù)各位取反谷浅,然后將結(jié)果 + 1扒俯。

算數(shù)右移和邏輯右移的區(qū)別

在了解完補數(shù)后,我們重新考慮一下右移這個議題一疯,右移在移位后空出來的最高位有兩種情況 0 和 1撼玄。

將二進(jìn)制數(shù)作為帶符號的數(shù)值進(jìn)行右移運算時,移位后需要在最高位填充移位前符號位的值( 0 或 1)墩邀。這就被稱為算數(shù)右移掌猛。如果數(shù)值使用補數(shù)表示的負(fù)數(shù)值,那么右移后在空出來的最高位補 1眉睹,就可以正確的表示 1/2,1/4,1/8等的數(shù)值運算荔茬。如果是正數(shù),那么直接在空出來的位置補 0 即可竹海。

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

image

如上圖所示斋配,在邏輯右移的情況下孔飒, -4 右移兩位會變成 63, 顯然不是它的 1/4艰争,所以不能使用邏輯右移坏瞄,那么算數(shù)右移的情況下,右移兩位會變?yōu)?-1甩卓,顯然是它的 1/4惦积,故而采用算數(shù)右移。

那么我們可以得出來一個結(jié)論:左移時猛频,無論是圖形還是數(shù)值,移位后蛛勉,只需要將低位補 0 即可鹿寻;右移時,需要根據(jù)情況判斷是邏輯右移還是算數(shù)右移诽凌。

下面介紹一下符號擴(kuò)展:將數(shù)據(jù)進(jìn)行符號擴(kuò)展是為了產(chǎn)生一個位數(shù)加倍毡熏、但數(shù)值大小不變的結(jié)果,以滿足有些指令對操作數(shù)位數(shù)的要求侣诵,例如倍長于除數(shù)的被除數(shù)痢法,再如將數(shù)據(jù)位數(shù)加長以減少計算過程中的誤差狱窘。

以8位二進(jìn)制為例,符號擴(kuò)展就是指在保持值不變的前提下將其轉(zhuǎn)換成為16位和32位的二進(jìn)制數(shù)财搁。將0111 1111這個正的 8位二進(jìn)制數(shù)轉(zhuǎn)換成為 16位二進(jìn)制數(shù)時蘸炸,很容易就能夠得出0000 0000 0111 1111這個正確的結(jié)果,但是像 1111 1111這樣的補數(shù)來表示的數(shù)值尖奔,該如何處理搭儒?直接將其表示成為1111 1111 1111 1111就可以了。也就是說提茁,不管正數(shù)還是補數(shù)表示的負(fù)數(shù)淹禾,只需要將 0 和 1 填充高位即可。

內(nèi)存和磁盤的關(guān)系

我們大家知道茴扁,計算機(jī)的五大基礎(chǔ)部件是 存儲器铃岔、控制器運算器輸入和輸出設(shè)備,其中從存儲功能的角度來看恳守,可以把存儲器分為內(nèi)存磁盤就珠,我們上面介紹過內(nèi)存,下面就來介紹一下磁盤以及磁盤和內(nèi)存的關(guān)系

程序不讀入內(nèi)存就無法運行

計算機(jī)最主要的存儲部件是內(nèi)存和磁盤揩瞪。磁盤中存儲的程序必須加載到內(nèi)存中才能運行,在磁盤中保存的程序是無法直接運行的,這是因為負(fù)責(zé)解析和運行程序內(nèi)容的 CPU 是需要通過程序計數(shù)器來指定內(nèi)存地址從而讀出程序指令的隆檀。

image

磁盤構(gòu)造

磁盤緩存

我們上面提到,磁盤往往和內(nèi)存是互利共生的關(guān)系粹湃,相互協(xié)作恐仑,彼此持有良好的合作關(guān)系。每次內(nèi)存都需要從磁盤中讀取數(shù)據(jù)为鳄,必然會讀到相同的內(nèi)容裳仆,所以一定會有一個角色負(fù)責(zé)存儲我們經(jīng)常需要讀到的內(nèi)容。我們大家做軟件的時候經(jīng)常會用到緩存技術(shù)孤钦,那么硬件層面也不例外歧斟,磁盤也有緩存,磁盤的緩存叫做磁盤緩存偏形。

磁盤緩存指的是把從磁盤中讀出的數(shù)據(jù)存儲到內(nèi)存的方式静袖,這樣一來,當(dāng)接下來需要讀取相同的內(nèi)容時俊扭,就不會再通過實際的磁盤队橙,而是通過磁盤緩存來讀取。某一種技術(shù)或者框架的出現(xiàn)勢必要解決某種問題的,那么磁盤緩存就大大改善了磁盤訪問的速度捐康。

image

虛擬內(nèi)存

虛擬內(nèi)存是內(nèi)存和磁盤交互的第二個媒介仇矾。虛擬內(nèi)存是指把磁盤的一部分作為假想內(nèi)存來使用。這與磁盤緩存是假想的磁盤(實際上是內(nèi)存)相對解总,虛擬內(nèi)存是假想的內(nèi)存(實際上是磁盤)贮匕。

虛擬內(nèi)存是計算機(jī)系統(tǒng)內(nèi)存管理的一種技術(shù)。它使得應(yīng)用程序認(rèn)為它擁有連續(xù)可用的內(nèi)存(一個完整的地址空間)倾鲫,但是實際上粗合,它通常被分割成多個物理碎片,還有部分存儲在外部磁盤管理器上乌昔,必要時進(jìn)行數(shù)據(jù)交換隙疚。

通過借助虛擬內(nèi)存,在內(nèi)存不足時仍然可以運行程序磕道。例如供屉,在只剩 5MB 內(nèi)存空間的情況下仍然可以運行 10MB 的程序。由于 CPU 只能執(zhí)行加載到內(nèi)存中的程序溺蕉,因此伶丐,虛擬內(nèi)存的空間就需要和內(nèi)存中的空間進(jìn)行置換(swap),然后運行程序疯特。

虛擬內(nèi)存與內(nèi)存的交換方式

虛擬內(nèi)存的方法有分頁式分段式 兩種哗魂。Windows 采用的是分頁式。該方式是指在不考慮程序構(gòu)造的情況下漓雅,把運行的程序按照一定大小的頁進(jìn)行分割录别,并以為單位進(jìn)行置換。在分頁式中邻吞,我們把磁盤的內(nèi)容讀到內(nèi)存中稱為 Page In组题,把內(nèi)存的內(nèi)容寫入磁盤稱為 Page Out。Windows 計算機(jī)的頁大小為 4KB 抱冷,也就是說崔列,需要把應(yīng)用程序按照 4KB 的頁來進(jìn)行切分,以頁(page)為單位放到磁盤中旺遮,然后進(jìn)行置換赵讯。

image

為了實現(xiàn)內(nèi)存功能,Windows 在磁盤上提供了虛擬內(nèi)存使用的文件(page file耿眉,頁文件)瘦癌。該文件由 Windows 生成和管理,文件的大小和虛擬內(nèi)存大小相同跷敬,通常大小是內(nèi)存的 1 - 2 倍。

磁盤的物理結(jié)構(gòu)

之前我們介紹了CPU、內(nèi)存的物理結(jié)構(gòu)西傀,現(xiàn)在我們來介紹一下磁盤的物理結(jié)構(gòu)斤寇。磁盤的物理結(jié)構(gòu)指的是磁盤存儲數(shù)據(jù)的形式

磁盤是通過其物理表面劃分成多個空間來使用的拥褂。劃分的方式有兩種:可變長方式扇區(qū)方式娘锁。前者是將物理結(jié)構(gòu)劃分成長度可變的空間,后者是將磁盤結(jié)構(gòu)劃分為固定長度的空間饺鹃。一般 Windows 所使用的硬盤和軟盤都是使用扇區(qū)這種方式莫秆。扇區(qū)中,把磁盤表面分成若干個同心圓的空間就是 磁道悔详,把磁道按照固定大小的存儲空間劃分而成的就是 扇區(qū)

image

扇區(qū)是對磁盤進(jìn)行物理讀寫的最小單位镊屎。Windows 中使用的磁盤,一般是一個扇區(qū) 512 個字節(jié)茄螃。不過缝驳,Windows 在邏輯方面對磁盤進(jìn)行讀寫的單位是扇區(qū)整數(shù)倍簇。根據(jù)磁盤容量不同功能归苍,1簇可以是 512 字節(jié)(1 簇 = 1扇區(qū))用狱、1KB(1簇 = 2扇區(qū))、2KB拼弃、4KB夏伊、8KB、16KB吻氧、32KB( 1 簇 = 64 扇區(qū))溺忧。簇和扇區(qū)的大小是相等的。

壓縮算法

我們想必都有過壓縮解壓縮文件的經(jīng)歷医男,當(dāng)文件太大時砸狞,我們會使用文件壓縮來降低文件的占用空間。比如微信上傳文件的限制是100 MB镀梭,我這里有個文件夾無法上傳刀森,但是我解壓完成后的文件一定會小于 100 MB,那么我的文件就可以上傳了报账。

此外研底,我們把相機(jī)拍完的照片保存到計算機(jī)上的時候,也會使用壓縮算法進(jìn)行文件壓縮透罢,文件壓縮的格式一般是JPEG榜晦。

那么什么是壓縮算法呢?壓縮算法又是怎么定義的呢羽圃?在認(rèn)識算法之前我們需要先了解一下文件是如何存儲的

文件存儲

文件是將數(shù)據(jù)存儲在磁盤等存儲媒介的一種形式乾胶。程序文件中最基本的存儲數(shù)據(jù)單位是字節(jié)。文件的大小不管是 xxxKB、xxxMB等來表示识窿,就是因為文件是以字節(jié) B = Byte 為單位來存儲的斩郎。

文件就是字節(jié)數(shù)據(jù)的集合。用 1 字節(jié)(8 位)表示的字節(jié)數(shù)據(jù)有 256 種喻频,用二進(jìn)制表示的話就是 0000 0000 - 1111 1111 缩宜。如果文件中存儲的數(shù)據(jù)是文字,那么該文件就是文本文件甥温。如果是圖形锻煌,那么該文件就是圖像文件。在任何情況下姻蚓,文件中的字節(jié)數(shù)都是連續(xù)存儲的宋梧。

image

壓縮算法的定義

上面介紹了文件的集合體其實就是一堆字節(jié)數(shù)據(jù)的集合,那么我們就可以來給壓縮算法下一個定義史简。

壓縮算法(compaction algorithm)指的就是數(shù)據(jù)壓縮的算法乃秀,主要包括壓縮和還原(解壓縮)的兩個步驟。

其實就是在不改變原有文件屬性的前提下圆兵,降低文件字節(jié)空間和占用空間的一種算法跺讯。

根據(jù)壓縮算法的定義,我們可將其分成不同的類型:

有損和無損

無損壓縮:能夠無失真地從壓縮后的數(shù)據(jù)重構(gòu)殉农,準(zhǔn)確地還原原始數(shù)據(jù)刀脏。可用于對數(shù)據(jù)的準(zhǔn)確性要求嚴(yán)格的場合超凳,如可執(zhí)行文件和普通文件的壓縮愈污、磁盤的壓縮,也可用于多媒體數(shù)據(jù)的壓縮轮傍。該方法的壓縮比較小暂雹。如差分編碼、RLE创夜、Huffman編碼杭跪、LZW編碼、算術(shù)編碼驰吓。

有損壓縮:有失真涧尿,不能完全準(zhǔn)確地恢復(fù)原始數(shù)據(jù),重構(gòu)的數(shù)據(jù)只是原始數(shù)據(jù)的一個近似檬贰」昧可用于對數(shù)據(jù)的準(zhǔn)確性要求不高的場合,如多媒體數(shù)據(jù)的壓縮翁涤。該方法的壓縮比較大桥言。例如預(yù)測編碼萌踱、音感編碼、分形壓縮限书、小波壓縮虫蝶、JPEG/MPEG。

對稱性

如果編解碼算法的復(fù)雜性和所需時間差不多倦西,則為對稱的編碼方法,多數(shù)壓縮算法都是對稱的赁严。但也有不對稱的扰柠,一般是編碼難而解碼容易,如 Huffman 編碼和分形編碼疼约。但用于密碼學(xué)的編碼方法則相反卤档,是編碼容易,而解碼則非常難程剥。

幀間與幀內(nèi)

在視頻編碼中會同時用到幀內(nèi)與幀間的編碼方法劝枣,幀內(nèi)編碼是指在一幀圖像內(nèi)獨立完成的編碼方法,同靜態(tài)圖像的編碼织鲸,如 JPEG舔腾;而幀間編碼則需要參照前后幀才能進(jìn)行編解碼,并在編碼過程中考慮對幀之間的時間冗余的壓縮搂擦,如 MPEG稳诚。

實時性

在有些多媒體的應(yīng)用場合,需要實時處理或傳輸數(shù)據(jù)(如現(xiàn)場的數(shù)字錄音和錄影瀑踢、播放MP3/RM/VCD/DVD扳还、視頻/音頻點播、網(wǎng)絡(luò)現(xiàn)場直播橱夭、可視電話氨距、視頻會議),編解碼一般要求延時 ≤50 ms棘劣。這就需要簡單/快速/高效的算法和高速/復(fù)雜的CPU/DSP芯片俏让。

分級處理

有些壓縮算法可以同時處理不同分辨率、不同傳輸速率呈础、不同質(zhì)量水平的多媒體數(shù)據(jù)舆驶,如JPEG2000、MPEG-2/4而钞。

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

幾種常用壓縮算法的理解

RLE 算法的機(jī)制

接下來就讓我們正式看一下文件的壓縮機(jī)制臼节。首先讓我們來嘗試對 AAAAAABBCDDEEEEEF 這 17 個半角字符的文件(文本文件)進(jìn)行壓縮撬陵。雖然這些文字沒有什么實際意義珊皿,但是很適合用來描述 RLE 的壓縮機(jī)制。

由于半角字符(其實就是英文字符)是作為 1 個字節(jié)保存在文件中的巨税,所以上述的文件的大小就是 17 字節(jié)蟋定。如圖

image

那么,如何才能壓縮該文件呢草添?大家不妨也考慮一下驶兜,只要是能夠使文件小于 17 字節(jié),我們可以使用任何壓縮算法远寸。

最顯而易見的一種壓縮方式我覺得你已經(jīng)想到了抄淑,就是把相同的字符去重化,也就是 字符 * 重復(fù)次數(shù) 的方式進(jìn)行壓縮驰后。所以上面文件壓縮后就會變成下面這樣

image

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

像這樣夜涕,把文件內(nèi)容用 數(shù)據(jù) * 重復(fù)次數(shù) 的形式來表示的壓縮方法成為 RLE(Run Length Encoding, 行程長度編碼) 算法犯犁。RLE 算法是一種很好的壓縮方法,經(jīng)常用于壓縮傳真的圖像等钠乏。因為圖像文件的本質(zhì)也是字節(jié)數(shù)據(jù)的集合體栖秕,所以可以用 RLE 算法進(jìn)行壓縮

哈夫曼算法和莫爾斯編碼

下面我們來介紹另外一種壓縮算法,即哈夫曼算法晓避。在了解哈夫曼算法之前簇捍,你必須舍棄半角英文數(shù)字的1個字符是1個字節(jié)(8位)的數(shù)據(jù)。下面我們就來認(rèn)識一下哈夫曼算法的基本思想俏拱。

文本文件是由不同類型的字符組合而成的暑塑,而且不同字符出現(xiàn)的次數(shù)也是不一樣的。例如锅必,在某個文本文件中事格,A 出現(xiàn)了 100次左右,Q僅僅用到了 3 次搞隐,類似這樣的情況很常見驹愚。哈夫曼算法的關(guān)鍵就在于 多次出現(xiàn)的數(shù)據(jù)用小于 8 位的字節(jié)數(shù)表示,不常用的數(shù)據(jù)則可以使用超過 8 位的字節(jié)數(shù)表示劣纲。A 和 Q 都用 8 位來表示時逢捺,原文件的大小就是 100次 * 8 位 + 3次 * 8 位 = 824位,假設(shè) A 用 2 位癞季,Q 用 10 位來表示就是 2 * 100 + 3 * 10 = 230 位劫瞳。

不過要注意一點倘潜,最終磁盤的存儲都是以8位為一個字節(jié)來保存文件的。

哈夫曼算法比較復(fù)雜志于,在深入了解之前我們先吃點甜品涮因,了解一下 莫爾斯編碼,你一定看過美劇或者戰(zhàn)爭片的電影伺绽,在戰(zhàn)爭中的通信經(jīng)常采用莫爾斯編碼來傳遞信息养泡,例如下面

image

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

image

莫爾斯編碼一般把文本中出現(xiàn)最高頻率的字符用短編碼 來表示钥组。如表所示,假如表示短點的位是 1今瀑,表示長點的位是 11 的話程梦,那么 E(嘀)這一數(shù)據(jù)的字符就可以用 1 來表示,C(滴答滴答)就可以用 9 位的 110101101來表示橘荠。在實際的莫爾斯編碼中屿附,如果短點的長度是 1 ,長點的長度就是 3哥童,短點和長點的間隔就是1挺份。這里的長度指的就是聲音的長度。比如我們想用上面的 AAAAAABBCDDEEEEEF 例子來用莫爾斯編碼重寫贮懈,在莫爾斯曼編碼中匀泊,各個字符之間需要加入表示時間間隔的符號。這里我們用 00 加以區(qū)分朵你。

所以各聘,AAAAAABBCDDEEEEEF 這個文本就變?yōu)榱?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字節(jié)。

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

用二叉樹實現(xiàn)哈夫曼算法

剛才已經(jīng)提到拱她,莫爾斯編碼是根據(jù)日常文本中各字符的出現(xiàn)頻率來決定表示各字符的編碼數(shù)據(jù)長度的睹簇。不過,在該編碼體系中苛败,對 AAAAAABBCDDEEEEEF 這種文本來說并不是效率最高的水孩。

下面我們來看一下哈夫曼算法镰矿。哈夫曼算法是指,為各壓縮對象文件分別構(gòu)造最佳的編碼體系荷愕,并以該編碼體系為基礎(chǔ)來進(jìn)行壓縮衡怀。因此棍矛,用什么樣的編碼(哈夫曼編碼)對數(shù)據(jù)進(jìn)行分割,就要由各個文件而定抛杨。用哈夫曼算法壓縮過的文件中够委,存儲著哈夫曼編碼信息和壓縮過的數(shù)據(jù)。

image

接下來怖现,我們在對 AAAAAABBCDDEEEEEF 中的 A - F 這些字符茁帽,按照出現(xiàn)頻率高的字符用盡量少的位數(shù)編碼來表示這一原則進(jìn)行整理。按照出現(xiàn)頻率從高到低的順序整理后屈嗤,結(jié)果如下潘拨,同時也列出了編碼方案。

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

在上表的編碼方案中饶号,隨著出現(xiàn)頻率的降低铁追,字符編碼信息的數(shù)據(jù)位數(shù)也在逐漸增加,從最開始的 1位茫船、2位依次增加到3位琅束。不過這個編碼體系是存在問題的,你不知道100這個3位的編碼算谈,它的意思是用 1涩禀、0、0這三個編碼來表示 E然眼、A艾船、A 呢?還是用10高每、0來表示 B屿岂、A 呢?還是用100來表示 C 呢觉义。

而在哈夫曼算法中雁社,通過借助哈夫曼樹的構(gòu)造編碼體系,即使在不使用字符區(qū)分符號的情況下晒骇,也可以構(gòu)建能夠明確進(jìn)行區(qū)分的編碼體系霉撵。不過哈夫曼樹的算法要比較復(fù)雜,下面是一個哈夫曼樹的構(gòu)造過程洪囤。

image

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

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

使用哈夫曼樹之后,出現(xiàn)頻率越高的數(shù)據(jù)所占用的位數(shù)越少瘤缩,這也是哈夫曼樹的核心思想喇完。通過上圖的步驟二可以看出,枝條連接數(shù)據(jù)時剥啤,我們是從出現(xiàn)頻率較低的數(shù)據(jù)開始的锦溪。這就意味著出現(xiàn)頻率低的數(shù)據(jù)到達(dá)根部的枝條也越多不脯。而枝條越多則意味著編碼的位數(shù)隨之增加。

接下來我們來看一下哈夫曼樹的壓縮比率刻诊,用上圖得到的數(shù)據(jù)表示 AAAAAABBCDDEEEEEF 為 000000000000 100100 110 101101 0101010101 111防楷,40位 = 5 字節(jié)。壓縮前的數(shù)據(jù)是 17 字節(jié)则涯,壓縮后的數(shù)據(jù)竟然達(dá)到了驚人的5 字節(jié)复局,也就是壓縮比率 = 5 / 17 = 29% 如此高的壓縮率,簡直是太驚艷了粟判。

大家可以參考一下亿昏,無論哪種類型的數(shù)據(jù),都可以用哈夫曼樹作為壓縮算法

文件類型 壓縮前 壓縮后 壓縮比率
文本文件 14862字節(jié) 4119字節(jié) 28%
圖像文件 96062字節(jié) 9456字節(jié) 10%
EXE文件 24576字節(jié) 4652字節(jié) 19%

可逆壓縮和非可逆壓縮

最后档礁,我們來看一下圖像文件的數(shù)據(jù)形式角钩。圖像文件的使用目的通常是把圖像數(shù)據(jù)輸出到顯示器、打印機(jī)等設(shè)備上呻澜。常用的圖像格式有 : BMP彤断、JPEGTIFF易迹、GIF 格式等。

  • BMP :是使用 Windows 自帶的畫筆來做成的一種圖像形式
  • JPEG:是數(shù)碼相機(jī)等常用的一種圖像數(shù)據(jù)形式
  • TIFF: 是一種通過在文件中包含"標(biāo)簽"就能夠快速顯示出數(shù)據(jù)性質(zhì)的圖像形式
  • GIF:是由美國開發(fā)的一種數(shù)據(jù)形式平道,要求色數(shù)不超過 256個

圖像文件可以使用前面介紹的 RLE 算法和哈夫曼算法睹欲,因為圖像文件在多數(shù)情況下并不要求數(shù)據(jù)需要還原到和壓縮之前一摸一樣的狀態(tài),允許丟失一部分?jǐn)?shù)據(jù)一屋。我們把能還原到壓縮前狀態(tài)的壓縮稱為 可逆壓縮窘疮,無法還原到壓縮前狀態(tài)的壓縮稱為非可逆壓縮

image

一般來說冀墨,JPEG格式的文件是非可逆壓縮闸衫,因此還原后有部分圖像信息比較模糊。GIF 是可逆壓縮

操作系統(tǒng)

操作系統(tǒng)環(huán)境

程序中包含著運行環(huán)境這一內(nèi)容诽嘉,可以說 運行環(huán)境 = 操作系統(tǒng) + 硬件 蔚出,操作系統(tǒng)又可以被稱為軟件,它是由一系列的指令組成的虫腋。我們不介紹操作系統(tǒng)骄酗,我們主要來介紹一下硬件的識別。

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

image

圖中的主要配置如下

  • 操作系統(tǒng)版本:說的就是應(yīng)用程序運行在何種系統(tǒng)環(huán)境盒蟆,現(xiàn)在市面上主要有三種操作系統(tǒng)環(huán)境踏烙,Windows 师骗、Linux 和 Unix ,一般我們玩兒的大型游戲幾乎都是在 Windows 上運行讨惩,可以說 Windows 是游戲的天堂辟癌。Windows 操作系統(tǒng)也會有區(qū)分,分為32位操作系統(tǒng)和64位操作系統(tǒng)步脓,互不兼容愿待。

  • 處理器:處理器指的就是 CPU,你的電腦的計算能力靴患,通俗來講就是每秒鐘能處理的指令數(shù)仍侥,如果你的電腦覺得卡帶不起來的話,很可能就是 CPU 的計算能力不足導(dǎo)致的鸳君。想要加深理解农渊,請閱讀博主的另一篇文章:程序員需要了解的硬核知識之CPU

  • 顯卡:顯卡承擔(dān)圖形的輸出任務(wù),因此又被稱為圖形處理器(Graphic Processing Unit或颊,GPU)砸紊,顯卡也非常重要,比如我之前玩兒的劍靈開五檔(其實就是圖像變得更清晰)會卡囱挑,其實就是顯卡顯示不出來的原因醉顽。

  • 內(nèi)存:內(nèi)存即主存,就是你的應(yīng)用程序在運行時能夠動態(tài)分析指令的這部分存儲空間平挑,它的大小也能決定你電腦的運行速度游添,想要加深理解,請閱讀博主的另一篇文章 程序員需要了解的硬核知識之內(nèi)存

  • 存儲空間:存儲空間指的就是應(yīng)用程序安裝所占用的磁盤空間通熄,由圖中可知唆涝,此游戲的最低存儲空間必須要大于 5GB,其實我們都會遺留很大一部分用來安裝游戲唇辨。

從程序的運行環(huán)境這一角度來考量的話廊酣,CPU 的種類是特別重要的參數(shù),為了使程序能夠正常運行赏枚,必須滿足 CPU 所需的最低配置亡驰。

CPU 只能解釋其自身固有的語言。不同的 CPU 能解釋的機(jī)器語言的種類也是不同的饿幅。機(jī)器語言的程序稱為 本地代碼(native code)隐解,程序員用 C 等高級語言編寫的程序,僅僅是文本文件诫睬。文本文件(排除文字編碼的問題)在任何環(huán)境下都能顯示和編輯煞茫。我們稱之為源代碼。通過對源代碼進(jìn)行編譯,就可以得到本地代碼续徽。下圖反映了這個過程蚓曼。

image

Windows 操作系統(tǒng)克服了CPU以外的硬件差異

計算機(jī)的硬件并不僅僅是由 CPU 組成的,還包括用于存儲程序指令的數(shù)據(jù)和內(nèi)存钦扭,以及通過 I/O 連接的鍵盤纫版、顯示器、硬盤客情、打印機(jī)等外圍設(shè)備其弊。

在 WIndows 軟件中,鍵盤輸入膀斋、顯示器輸出等并不是直接向硬件發(fā)送指令梭伐。而是通過向 Windows 發(fā)送指令實現(xiàn)的。因此仰担,程序員就不用注意內(nèi)存和 I/O 地址的不同構(gòu)成了糊识。Windows 操作的是硬件而不是軟件,軟件通過操作 Windows 系統(tǒng)可以達(dá)到控制硬件的目的摔蓝。

image

不同操作系統(tǒng)的 API 差異性

接下來我們看一下操作系統(tǒng)的種類赂苗。同樣機(jī)型的計算機(jī),可安裝的操作系統(tǒng)類型也會有多種選擇贮尉。例如:AT 兼容機(jī)除了可以安裝 Windows 之外拌滋,還可以采用 Unix 系列的 Linux 以及 FreeBSD (也是一種Unix操作系統(tǒng))等多個操作系統(tǒng)。當(dāng)然猜谚,應(yīng)用軟件則必須根據(jù)不同的操作系統(tǒng)類型來專門開發(fā)鸠真。CPU 的類型不同,所對應(yīng)機(jī)器的語言也不同,同樣的道理龄毡,操作系統(tǒng)的類型不同,應(yīng)用程序向操作系統(tǒng)傳遞指令的途徑也不同锡垄。

應(yīng)用程序向系統(tǒng)傳遞指令的途徑稱為 API(Application Programming Interface)沦零。Windows 以及 Linux 操作系統(tǒng)的 API,提供了任何應(yīng)用程序都可以利用的函數(shù)組合货岭。因為不同操作系統(tǒng)的 API 是有差異的路操。所以,如何要將同樣的應(yīng)用程序移植到另外的操作系統(tǒng)千贯,就必須要覆蓋應(yīng)用所用到的 API 部分屯仗。

鍵盤輸入、鼠標(biāo)輸入搔谴、顯示器輸出魁袜、文件輸入和輸出等同外圍設(shè)備進(jìn)行交互的功能,都是通過 API 提供的。

這也就是為什么 Windows 應(yīng)用程序不能直接移植到 Linux 操作系統(tǒng)上的原因峰弹,API 差異太大了店量。

在同類型的操作系統(tǒng)下,不論硬件如何鞠呈,API 幾乎相同融师。但是,由于不同種類 CPU 的機(jī)器語言不同蚁吝,因此本地代碼也不盡相同旱爆。

操作系統(tǒng)功能的歷史

操作系統(tǒng)其實也是一種軟件,任何新事物的出現(xiàn)肯定都有它的歷史背景窘茁,那么操作系統(tǒng)也不是憑空出現(xiàn)的怀伦,肯定有它的歷史背景。

在計算機(jī)尚不存在操作系統(tǒng)的年代庙曙,完全沒有任何程序空镜,人們通過各種按鈕來控制計算機(jī),這一過程非常麻煩捌朴。于是吴攒,有人開發(fā)出了僅具有加載和運行功能的監(jiān)控程序,這就是操作系統(tǒng)的原型砂蔽。通過事先啟動監(jiān)控程序洼怔,程序員可以根據(jù)需要將各種程序加載到內(nèi)存中運行。雖然仍舊比較麻煩左驾,但比起在沒有任何程序的狀態(tài)下進(jìn)行開發(fā)镣隶,工作量得到了很大的緩解。

image

隨著時代的發(fā)展诡右,人們在利用監(jiān)控程序編寫程序的過程中發(fā)現(xiàn)很多程序都有公共的部分安岂。例如,通過鍵盤進(jìn)行文字輸入帆吻,顯示器進(jìn)行數(shù)據(jù)展示等域那,如果每編寫一個新的應(yīng)用程序都需要相同的處理的話,那真是太浪費時間了猜煮。因此次员,基本的輸入輸出部分的程序就被追加到了監(jiān)控程序中。初期的操作系統(tǒng)就是這樣誕生了王带。

image

類似的想法可以共用淑蔚,人們又發(fā)現(xiàn)有更多的應(yīng)用程序可以追加到監(jiān)控程序中,比如硬件控制程序愕撰,編程語言處理器(匯編刹衫、編譯醋寝、解析)以及各種應(yīng)用程序等,結(jié)果就形成了和現(xiàn)在差異不大的操作系統(tǒng)绪妹,也就是說甥桂,其實操作系統(tǒng)是多個程序的集合體。

image

Windows 操作系統(tǒng)的特征

Windows 操作系統(tǒng)是世界上用戶數(shù)量最龐大的群體邮旷,作為 Windows 操作系統(tǒng)的資深用戶黄选,你都知道 Windows 操作系統(tǒng)有哪些特征嗎?下面列舉了一些 Windows 操作系統(tǒng)的特性

  • Windows 操作系統(tǒng)有兩個版本:32位和64位
  • 通過 API 函數(shù)集成來提供系統(tǒng)調(diào)用
  • 提供了采用圖形用戶界面的用戶界面
  • 通過 WYSIWYG 實現(xiàn)打印輸出婶肩,WYSIWYG 其實就是 What You See Is What You Get 办陷,值得是顯示器上顯示的圖形和文本都是可以原樣輸出到打印機(jī)打印的。
  • 提供多任務(wù)功能律歼,即能夠同時開啟多個任務(wù)
  • 提供網(wǎng)絡(luò)功能和數(shù)據(jù)庫功能
  • 通過即插即用實現(xiàn)設(shè)備驅(qū)動的自設(shè)定

這些是對程序員來講比較有意義的一些特征民镜,下面針對這些特征來進(jìn)行分別的介紹

32位操作系統(tǒng)

這里表示的32位操作系統(tǒng)表示的是處理效率最高的數(shù)據(jù)大小。Windows 處理數(shù)據(jù)的基本單位是 32 位险毁。這與最一開始在 MS-DOS 等16位操作系統(tǒng)不同制圈,因為在16位操作系統(tǒng)中處理32位數(shù)據(jù)需要兩次,而32位操作系統(tǒng)只需要一次就能夠處理32位的數(shù)據(jù)畔况,所以一般在 windows 上的應(yīng)用鲸鹦,它們的最高能夠處理的數(shù)據(jù)都是 32 位的。

比如跷跪,用 C 語言來處理整數(shù)數(shù)據(jù)時馋嗜,有8位的 char 類型,16位的short類型吵瞻,以及32位的long類型三個選項葛菇,使用位數(shù)較大的 long 類型進(jìn)行處理的話,增加的只是內(nèi)存以及磁盤的開銷橡羞,對性能影響不大眯停。

現(xiàn)在市面上大部分都是64位操作系統(tǒng)了,64位操作系統(tǒng)也是如此卿泽。

通過 API 函數(shù)集來提供系統(tǒng)調(diào)用

Windows 是通過名為 API 的函數(shù)集來提供系統(tǒng)調(diào)用的莺债。API是聯(lián)系應(yīng)用程序和操作系統(tǒng)之間的接口,全稱叫做 Application Programming Interface,應(yīng)用程序接口又厉。

當(dāng)前主流的32位版 Windows API 也稱為 Win32 API,之所以這樣命名椎瘟,是需要和不同的操作系統(tǒng)進(jìn)行區(qū)分覆致,比如最一開始的 16 位版的 Win16 API,和后來流行的 Win64 API 肺蔚。

API 通過多個 DLL 文件來提供煌妈,各個 API 的實體都是用 C 語言編寫的函數(shù)。所以,在 C 語言環(huán)境下璧诵,使用 API 更加容易汰蜘,比如 API 所用到的 MessageBox() 函數(shù),就被保存在了 Windows 提供的 user32.dll 這個 DLL 文件中之宿。

提供采用了 GUI 的用戶界面

GUI(Graphical User Interface) 指得就是圖形用戶界面族操,通過點擊顯示器中的窗口以及圖標(biāo)等可視化的用戶界面,舉個例子:Linux 操作系統(tǒng)就有兩個版本比被,一種是簡潔版色难,直接通過命令行控制硬件,還有一種是可視化版等缀,通過光標(biāo)點擊圖形界面來控制硬件枷莉。

通過 WYSIWYG 實現(xiàn)打印輸出

WYSIWYG 指的是顯示器上輸出的內(nèi)容可以直接通過打印機(jī)打印輸出。在 Windows 中尺迂,顯示器和打印機(jī)被認(rèn)作同等的圖形輸出設(shè)備處理的笤妙,該功能也為 WYSIWYG 提供了條件。

借助 WYSIWYG 功能噪裕,程序員可以輕松不少蹲盘。最初,為了是現(xiàn)在顯示器中顯示和在打印機(jī)中打印州疾,就必須分別編寫各自的程序辜限,而在 Windows 中,可以借助 WYSIWYG 基本上在一個程序中就可以做到顯示和打印這兩個功能了严蓖。

提供多任務(wù)功能

多任務(wù)指的就是同時能夠運行多個應(yīng)用程序的功能吧彪,Windows 是通過時鐘分割技術(shù)來實現(xiàn)多任務(wù)功能的。時鐘分割指的是短時間間隔內(nèi)拐叉,多個程序切換運行的方式酷愧。在用戶看來,就好像是多個程序在同時運行毒姨,其底層是 CPU 時間切片哑蔫,這也是多線程多任務(wù)的核心。

image

CPU分片弧呐,也是時鐘分割

提供網(wǎng)絡(luò)功能和數(shù)據(jù)庫功能

Windows 中闸迷,網(wǎng)絡(luò)功能是作為標(biāo)準(zhǔn)功能提供的。數(shù)據(jù)庫(數(shù)據(jù)庫服務(wù)器)功能有時也會在后面追加俘枫。網(wǎng)絡(luò)功能和數(shù)據(jù)庫功能雖然并不是操作系統(tǒng)不可或缺的腥沽,但因為它們和操作系統(tǒng)很接近,所以被統(tǒng)稱為中間件而不是應(yīng)用鸠蚪。意思是處于操作系統(tǒng)和應(yīng)用的中間層今阳,操作系統(tǒng)和中間件組合在一起师溅,稱為系統(tǒng)軟件。應(yīng)用不僅可以利用操作系統(tǒng)盾舌,也可以利用中間件的功能墓臭。

image

應(yīng)用可以使用操作系統(tǒng)和中間件

相對于操作系統(tǒng)一旦安裝就不能輕易更換,中間件可以根據(jù)需要進(jìn)行更換妖谴,不過窿锉,對于大部分應(yīng)用來說,更換中間件的話窖维,會造成應(yīng)用也隨之更換榆综,從這個角度來說,更?換中間件也不是那么容易铸史。

通過即插即用實現(xiàn)設(shè)備驅(qū)動的自動設(shè)定

即插即用(Plug-and-Play)指的是新的設(shè)備連接(plug) 后就可以直接使用的機(jī)制鼻疮,新設(shè)備連接計算機(jī)后,計算機(jī)就會自動安裝和設(shè)定用來控制該設(shè)備的驅(qū)動程序

設(shè)備驅(qū)動是操作系統(tǒng)的一部分琳轿,提供了同硬件進(jìn)行基本的輸入輸出的功能判沟。鍵盤、鼠標(biāo)崭篡、顯示器挪哄、磁盤裝置等,這些計算機(jī)中必備的硬件的設(shè)備驅(qū)動琉闪,一般都是隨操作系統(tǒng)一起安裝的迹炼。

有時 DLL 文件也會同設(shè)備驅(qū)動文件一起安裝。這些 DLL 文件中存儲著用來利用該新追加的硬件API颠毙,通過 API 斯入,可以制作出運行該硬件的心應(yīng)用。

匯編語言和本地代碼

我們在之前的文章中探討過蛀蜜,計算機(jī) CPU 只能運行本地代碼(機(jī)器語言)程序刻两,用 C 語言等高級語言編寫的代碼,需要經(jīng)過編譯器編譯后滴某,轉(zhuǎn)換為本地代碼才能夠被 CPU 解釋執(zhí)行磅摹。

但是本地代碼的可讀性非常差,所以需要使用一種能夠直接讀懂的語言來替換本地代碼霎奢,那就是在各本地代碼中户誓,附帶上表示其功能的英文縮寫,比如在加法運算的本地代碼加上add(addition) 的縮寫幕侠、在比較運算符的本地代碼中加上cmp(compare)的縮寫等帝美,這些通過縮寫來表示具體本地代碼指令的標(biāo)志稱為 助記符,使用助記符的語言稱為匯編語言橙依。這樣证舟,通過閱讀匯編語言,也能夠了解本地代碼的含義了窗骑。

不過女责,即使是使用匯編語言編寫的源代碼,最終也必須要轉(zhuǎn)換為本地代碼才能夠運行创译,負(fù)責(zé)做這項工作的程序稱為編譯器抵知,轉(zhuǎn)換的這個過程稱為匯編。在將源代碼轉(zhuǎn)換為本地代碼這個功能方面软族,匯編器和編譯器是同樣的刷喜。

用匯編語言編寫的源代碼和本地代碼是一一對應(yīng)的。因而立砸,本地代碼也可以反過來轉(zhuǎn)換成匯編語言編寫的代碼掖疮。把本地代碼轉(zhuǎn)換為匯編代碼的這一過程稱為反匯編,執(zhí)行反匯編的程序稱為反匯編程序颗祝。

image

本地代碼和匯編語言一對一的轉(zhuǎn)換

哪怕是 C 語言編寫的源代碼浊闪,編譯后也會轉(zhuǎn)換成特定 CPU 用的本地代碼。而將其反匯編的話螺戳,就可以得到匯編語言的源代碼搁宾,并對其內(nèi)容進(jìn)行調(diào)查。不過倔幼,本地代碼變成 C 語言源代碼的反編譯盖腿,要比本地代碼轉(zhuǎn)換成匯編代碼的反匯編要困難,這是因為损同,C 語言代碼和本地代碼不是一一對應(yīng)的關(guān)系翩腐。

通過編譯器輸出匯編語言的源代碼

我們上面提到本地代碼可以經(jīng)過反匯編轉(zhuǎn)換成為匯編代碼,但是只有這一種轉(zhuǎn)換方式嗎揖庄?顯然不是栗菜,C 語言編寫的源代碼也能夠通過編譯器編譯稱為匯編代碼,下面就來嘗試一下蹄梢。

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

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

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

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

// 調(diào)用 AddNum 函數(shù)的函數(shù)
void MyFunc(){
  int c;
  c = AddNum(123,456);
}

編寫完成后將其文件名保存為 Sample4.c ,C 語言源文件的擴(kuò)展名们豌,通常用.c 來表示涯捻,上面程序是提供兩個輸入?yún)?shù)并返回它們之和浅妆。

在 Windows 操作系統(tǒng)下打開 命令提示符,切換到保存 Sample4.c 的文件夾下障癌,然后在命令提示符中輸入

bcc32 -c -S Sample4.c

bcc32 是啟動 Borland C++ 的命令凌外,-c 的選項是指僅進(jìn)行編譯而不進(jìn)行鏈接,-S 選項被用來指定生成匯編語言的源代碼

作為編譯的結(jié)果涛浙,當(dāng)前目錄下會生成一個名為Sample4.asm 的匯編語言源代碼康辑。匯編語言源文件的擴(kuò)展名,通常用.asm 來表示轿亮,下面就讓我們用編輯器打開看一下 Sample4.asm 中的內(nèi)容

.386p
ifdef ??version
if    ??version GT 500H
.mmx
endif
endif
model flat
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
en

這樣疮薇,編譯器就成功的把 C 語言轉(zhuǎn)換成為了匯編代碼了。

不會轉(zhuǎn)換成本地代碼的偽指令

第一次看到匯編代碼的讀者可能感覺起來比較難我注,不過實際上其實比較簡單按咒,而且可能比 C 語言還要簡單,為了便于閱讀匯編代碼的源代碼但骨,需要注意幾個要點

匯編語言的源代碼胖齐,是由轉(zhuǎn)換成本地代碼的指令(后面講述的操作碼)和針對匯編器的偽指令構(gòu)成的。偽指令負(fù)責(zé)把程序的構(gòu)造以及匯編的方法指示給匯編器(轉(zhuǎn)換程序)嗽冒。不過偽指令是無法匯編轉(zhuǎn)換成為本地代碼的呀伙。下面是上面程序截取的偽指令

_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 圍起來的部分,是給構(gòu)成程序的命令和數(shù)據(jù)的集合體上加一個名字而得到的添坊,稱為段定義剿另。段定義的英文表達(dá)具有區(qū)域的意思,在這個程序中贬蛙,段定義指的是命令和數(shù)據(jù)等程序的集合體的意思雨女,一個程序由多個段定義構(gòu)成。

上面代碼的開始位置阳准,定義了3個名稱分別為 _TEXT氛堕、_DATA、_BSS 的段定義野蝇,_TEXT 是指定的段定義讼稚,_DATA 是被初始化(有初始值)的數(shù)據(jù)的段定義,_BSS 是尚未初始化的數(shù)據(jù)的段定義绕沈。這種定義的名稱是由 Borland C++ 定義的锐想,是由 Borland C++ 編譯器自動分配的,所以程序段定義的順序就成為了 _TEXT乍狐、_DATA赠摇、_BSS ,這樣也確保了內(nèi)存的連續(xù)性

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

段定義( segment ) 是用來區(qū)分或者劃分范圍區(qū)域的意思。匯編語言的 segment 偽指令表示段定義的起始藕帜,ends 偽指令表示段定義的結(jié)束烫罩。段定義是一段連續(xù)的內(nèi)存空間

group 這個偽指令表示的是將 _BSS和_DATA 這兩個段定義匯總名為 DGROUP 的組

DGROUPgroup_BSS,_DATA

圍起 _AddNum_MyFun_TEXT segment 和 _TEXT ends ,表示_AddNum_MyFun 是屬于 _TEXT 這一段定義的洽故。

_TEXTsegment dword public use32 'CODE'
_TEXTends

因此嗡髓,即使在源代碼中指令和數(shù)據(jù)是混雜編寫的,經(jīng)過編譯和匯編后收津,也會轉(zhuǎn)換成為規(guī)整的本地代碼。

_AddNum proc_AddNum endp 圍起來的部分浊伙,以及_MyFunc proc_MyFunc endp圍起來的部分撞秋,分別表示 AddNum 函數(shù)和 MyFunc 函數(shù)的范圍。

_AddNumprocnear
_AddNumendp

_MyFuncprocnear
_MyFuncendp

編譯后在函數(shù)名前附帶上下劃線_ 嚣鄙,是 Borland C++ 的規(guī)定吻贿。在 C 語言中編寫的 AddNum 函數(shù),在內(nèi)部是以 _AddNum 這個名稱處理的哑子。偽指令 proc 和 endp 圍起來的部分舅列,表示的是 過程(procedure) 的范圍。在匯編語言中卧蜓,這種相當(dāng)于 C 語言的函數(shù)的形式稱為過程帐要。

末尾的 end 偽指令,表示的是源代碼的結(jié)束弥奸。

匯編語言的語法是 操作碼 + 操作數(shù)

在匯編語言中榨惠,一行表示一對 CPU 的一個指令。匯編語言指令的語法結(jié)構(gòu)是 操作碼 + 操作數(shù)盛霎,也存在只有操作碼沒有操作數(shù)的指令赠橙。

操作碼表示的是指令動作,操作數(shù)表示的是指令對象愤炸。操作碼和操作數(shù)一起使用就是一個英文指令期揪。比如從英語語法來分析的話,操作碼是動詞规个,操作數(shù)是賓語凤薛。比如這個句子 Give me money這個英文指令的話,Give 就是操作碼诞仓,me 和 money 就是操作數(shù)枉侧。匯編語言中存在多個操作數(shù)的情況,要用逗號把它們分割狂芋,就像是 Give me,money 這樣榨馁。

能夠使用何種形式的操作碼,是由 CPU 的種類決定的帜矾,下面對操作碼的功能進(jìn)行了整理翼虫。

image

本地代碼需要加載到內(nèi)存后才能運行屑柔,內(nèi)存中存儲著構(gòu)成本地代碼的指令和數(shù)據(jù)。程序運行時珍剑,CPU會從內(nèi)存中把數(shù)據(jù)和指令讀出來掸宛,然后放在 CPU 內(nèi)部的寄存器中進(jìn)行處理。

image

如果 CPU 和內(nèi)存的關(guān)系你還不是很了解的話招拙,請閱讀作者的另一篇文章 程序員需要了解的硬核知識之CPU 詳細(xì)了解唧瘾。

寄存器是 CPU 中的存儲區(qū)域,寄存器除了具有臨時存儲和計算的功能之外别凤,還具有運算功能饰序,x86 系列的主要種類和角色如下圖所示

image

指令解析

下面就對 CPU 中的指令進(jìn)行分析

最常用的 mov 指令

指令中最常使用的是對寄存器和內(nèi)存進(jìn)行數(shù)據(jù)存儲的 mov 指令,mov 指令的兩個操作數(shù)规哪,分別用來指定數(shù)據(jù)的存儲地和讀出源求豫。操作數(shù)中可以指定寄存器、常數(shù)诉稍、標(biāo)簽(附加在地址前)蝠嘉,以及用方括號([]) 圍起來的這些內(nèi)容。如果指定了沒有用([]) 方括號圍起來的內(nèi)容杯巨,就表示對該值進(jìn)行處理蚤告;如果指定了用方括號圍起來的內(nèi)容,方括號的值則會被解釋為內(nèi)存地址服爷,然后就會對該內(nèi)存地址對應(yīng)的值進(jìn)行讀寫操作罩缴。讓我們對上面的代碼片段進(jìn)行說明

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 后會被解析稱為內(nèi)存地址檬寂。如果 ebp

寄存器的值是100的話,那么 eax 寄存器的值就是 100 + 8 的地址的值戳表。dword ptr 也叫做 double word pointer 簡單解釋一下就是從指定的內(nèi)存地址中讀出4字節(jié)的數(shù)據(jù)

對棧進(jìn)行 push 和 pop

程序運行時桶至,會在內(nèi)存上申請分配一個稱為棧的數(shù)據(jù)空間。棧(stack)的特性是后入先出匾旭,數(shù)據(jù)在存儲時是從內(nèi)存的下層(大的地址編號)逐漸往上層(小的地址編號)累積镣屹,讀出時則是按照從上往下進(jìn)行讀取的。

image

棧是存儲臨時數(shù)據(jù)的區(qū)域价涝,它的特點是通過 push 指令和 pop 指令進(jìn)行數(shù)據(jù)的存儲和讀出女蜈。向棧中存儲數(shù)據(jù)稱為 入棧 ,從棧中讀出數(shù)據(jù)稱為 出棧,32位 x86 系列的 CPU 中伪窖,進(jìn)行1次 push 或者 pop逸寓,即可處理 32 位(4字節(jié))的數(shù)據(jù)。

函數(shù)的調(diào)用機(jī)制

下面我們一起來分析一下函數(shù)的調(diào)用機(jī)制覆山,我們以上面的 C 語言編寫的代碼為例竹伸。首先,讓我們從MyFunc 函數(shù)調(diào)用AddNum 函數(shù)的匯編語言部分開始簇宽,來對函數(shù)的調(diào)用機(jī)制進(jìn)行說明勋篓。棧在函數(shù)的調(diào)用中發(fā)揮了巨大的作用,下面是經(jīng)過處理后的 MyFunc 函數(shù)的匯編處理內(nèi)容

_MyFunc  proc  near
push ebp  ; 將 ebp 寄存器的值存入棧中                                      (1)
movebp,esp ; 將 esp 寄存器的值存入 ebp 寄存器中                      (2)
push456; 將 456 入棧      (3)
push 123; 將 123 入棧      (4)
call_AddNum ; 調(diào)用 AddNum 函數(shù)      (5)
addesp,8; esp 寄存器的值 + 8      (6)
popebp; 讀出棧中的數(shù)值存入 esp 寄存器中      (7)
ret ; 結(jié)束 MyFunc 函數(shù)魏割,返回到調(diào)用源      (8)
_MyFunc end

代碼解釋中的(1)譬嚣、(2)、(7)见妒、(8)的處理適用于 C 語言中的所有函數(shù),我們會在后面展示 AddNum函數(shù)處理內(nèi)容時進(jìn)行說明甸陌。這里希望大家先關(guān)注(3) - (6) 這一部分须揣,這對了解函數(shù)調(diào)用機(jī)制至關(guān)重要。

(3) 和 (4) 表示的是將傳遞給 AddNum 函數(shù)的參數(shù)通過 push 入棧钱豁。在 C 語言源代碼中耻卡,雖然記述為函數(shù) AddNum(123,456),但入棧時則會先按照 456牲尺,123 這樣的順序卵酪。也就是位于后面的數(shù)值先入棧。這是 C 語言的規(guī)定谤碳。(5) 表示的 call 指令溃卡,會把程序流程跳轉(zhuǎn)到 AddNum 函數(shù)指令的地址處。在匯編語言中蜒简,函數(shù)名表示的就是函數(shù)所在的內(nèi)存地址瘸羡。AddNum 函數(shù)處理完畢后,程序流程必須要返回到編號(6) 這一行搓茬。call 指令運行后犹赖,call 指令的下一行(也就指的是 (6) 這一行)的內(nèi)存地址(調(diào)用函數(shù)完畢后要返回的內(nèi)存地址)會自動的 push 入棧。該值會在 AddNum 函數(shù)處理的最后通過 ret 指令 pop 出棧卷仑,然后程序會返回到 (6) 這一行峻村。

(6) 部分會把棧中存儲的兩個參數(shù) (456 和 123) 進(jìn)行銷毀處理。雖然通過兩次的 pop 指令也可以實現(xiàn)锡凝,不過采用 esp 寄存器 + 8 的方式會更有效率(處理 1 次即可)粘昨。對棧進(jìn)行數(shù)值的輸入和輸出時,數(shù)值的單位是4字節(jié)。因此雾棺,通過在負(fù)責(zé)棧地址管理的 esp 寄存器中加上4的2倍8膊夹,就可以達(dá)到和運行兩次 pop 命令同樣的效果。雖然內(nèi)存中的數(shù)據(jù)實際上還殘留著捌浩,但只要把 esp 寄存器的值更新為數(shù)據(jù)存儲地址前面的數(shù)據(jù)位置放刨,該數(shù)據(jù)也就相當(dāng)于銷毀了。

我在編譯 Sample4.c 文件時尸饺,出現(xiàn)了下圖的這條消息

image

圖中的意思是指 c 的值在 MyFunc 定義了但是一直未被使用进统,這其實是一項編譯器優(yōu)化的功能,由于存儲著 AddNum 函數(shù)返回值的變量 c 在后面沒有被用到浪听,因此編譯器就認(rèn)為 該變量沒有意義螟碎,進(jìn)而也就沒有生成與之對應(yīng)的匯編語言代碼

下圖是調(diào)用 AddNum 這一函數(shù)前后棧內(nèi)存的變化

image

函數(shù)的內(nèi)部處理

上面我們用匯編代碼分析了一下 Sample4.c 整個過程的代碼迹栓,現(xiàn)在我們著重分析一下 AddNum 函數(shù)的源代碼部分掉分,分析一下參數(shù)的接收、返回值和返回等機(jī)制

_AddNum procnear
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)中出棧酥郭,這主要是為了把函數(shù)中用到的 ebp 寄存器的內(nèi)容,恢復(fù)到函數(shù)調(diào)用前的狀態(tài)愿吹。

(2) 中把負(fù)責(zé)管理棧地址的 esp 寄存器的值賦值到了 ebp 寄存器中不从。這是因為,在 mov 指令中方括號內(nèi)的參數(shù)犁跪,是不允許指定 esp 寄存器的椿息。因此,這里就采用了不直接通過 esp坷衍,而是用 ebp 寄存器來讀寫棧內(nèi)容的方法寝优。

(3) 使用[ebp + 8] 指定棧中存儲的第1個參數(shù)123,并將其讀出到 eax 寄存器中枫耳。像這樣倡勇,不使用 pop 指令,也可以參照棧的內(nèi)容嘉涌。而之所以從多個寄存器中選擇了 eax 寄存器妻熊,是因為 eax 是負(fù)責(zé)運算的累加寄存器。

通過(4) 的 add 指令仑最,把當(dāng)前 eax 寄存器的值同第2個參數(shù)相加后的結(jié)果存儲在 eax 寄存器中扔役。[ebp + 12] 是用來指定第2個參數(shù)456的。在 C 語言中警医,函數(shù)的返回值必須通過 eax 寄存器返回亿胸,這也是規(guī)定坯钦。也就是 函數(shù)的參數(shù)是通過棧來傳遞泊交,返回值是通過寄存器返回的立膛。

(6) 中 ret 指令運行后串慰,函數(shù)返回目的地內(nèi)存地址會自動出棧导狡,據(jù)此,程序流程就會跳轉(zhuǎn)返回到(6) (Call _AddNum) 的下一行敞嗡。這時丧枪,AddNum 函數(shù)入口和出口處棧的狀態(tài)變化冗锁,就如下圖所示

image

全局變量和局部變量

在熟悉了匯編語言后潘悼,接下來我們來了解一下全局變量和局部變量律秃,在函數(shù)外部定義的變量稱為全局變量,在函數(shù)內(nèi)部定義的變量稱為局部變量治唤,全局變量可以在任意函數(shù)中使用棒动,局部變量只能在函數(shù)定義局部變量的內(nèi)部使用。下面宾添,我們就通過匯編語言來看一下全局變量和局部變量的不同之處船惨。

下面定義的 C 語言代碼分別定義了局部變量和全局變量,并且給各變量進(jìn)行了賦值缕陕,我們先看一下源代碼部分

// 定義被初始化的全局變量
int a1 = 1;
int a2 = 2;
int a3 = 3;
int a4 = 4;
int a5 = 5;

// 定義沒有初始化的全局變量
int b1,b2,b3,b4,b5;

// 定義函數(shù)
void MyFunc(){
  // 定義局部變量
  int c1,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;
}

上面的代碼挺暴力的粱锐,不過沒關(guān)系,能夠便于我們分析其匯編源碼就好榄檬,我們用 Borland C++ 編譯后的匯編代碼如下卜范,編譯完成后的源碼比較長衔统,這里我們只拿出來一部分作為分析使用(我們改變了一下段定義順序鹿榜,刪除了部分注釋)

_DATA segment dword public use32 'DATA'
   align 4
  _a1 label dword
   dd 1
   align 4
  _a2 label dword
   dd 2
   align 4
  _a3 label dword
   dd 3
   align 4
  _a4 label dword
   dd 4
   align 4
  _a5 label dword
   dd 5
_DATA ends

_BSS segment dword public use32 'BSS'
 align 4
  _b1 label dword
   db 4 dup(?)
   align 4
  _b2 label dword
   db 4 dup(?)
   align 4
  _b3 label dword
   db 4 dup(?)
   align 4
  _b4 label dword
   db 4 dup(?)
   align 4
  _b5 label dword
   db 4 dup(?)
_BSS ends

_TEXT segment dword public use32 'CODE'
_MyFunc proc 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       dword ptr [ebp-4],6
 mov       dword ptr [ebp-8],7
 mov       dword ptr [ebp-12],8
 mov       dword ptr [ebp-16],9
 mov       dword ptr [ebp-20],10
 mov       dword ptr [_a1],eax
 mov       dword ptr [_a2],edx
 mov       dword ptr [_a3],ecx
 mov       dword ptr [_a4],ebx
 mov       dword ptr [_a5],esi
 mov       eax,dword ptr [ebp-4]
 mov       dword ptr [_b1],eax
 mov       edx,dword ptr [ebp-8]
 mov       dword ptr [_b2],edx
 mov       ecx,dword ptr [ebp-12]
 mov       dword ptr [_b3],ecx
 mov       eax,dword ptr [ebp-16]
 mov       dword ptr [_b4],eax
 mov       edx,dword ptr [ebp-20]
 mov       dword ptr [_b5],edx
 pop       esi
 pop       ebx
 mov       esp,ebp
 pop       ebp
 ret

_MyFunc   endp
_TEXT ends

編譯后的程序,會被歸類到名為段定義的組锦爵。

  • 初始化的全局變量舱殿,會匯總到名為 _DATA 的段定義中
_DATA segment dword public use32 'DATA'
...
_DATA ends
  • 沒有初始化的全局變量,會匯總到名為 _BSS 的段定義中
_BSS segment dword public use32 'BSS'
 ...
_BSS ends
  • 被段定義 _TEXT 圍起來的匯編代碼則是 Borland C++ 的定義
_TEXT segment dword public use32 'CODE'
_MyFunc proc near
...
_MyFunc   endp
_TEXT ends

我們在分析上面匯編代碼之前险掀,先來認(rèn)識一下更多的匯編指令沪袭,此表是對上面部分操作碼及其功能的接續(xù)

操作碼 操作數(shù) 功能
add A,B 把A和B的值相加,并把結(jié)果賦值給A
call A 調(diào)用函數(shù)A
cmp A,B 對A和B進(jìn)行比較樟氢,比較結(jié)果會自動存入標(biāo)志寄存器中
inc A 對A的值 + 1
ige 標(biāo)簽名 和 cmp 命令組合使用冈绊。跳轉(zhuǎn)到標(biāo)簽行
jl 標(biāo)簽名 和 cmp 命令組合使用。跳轉(zhuǎn)到標(biāo)簽行
jle 標(biāo)簽名 和 cmp 命令組合使用埠啃。跳轉(zhuǎn)到標(biāo)簽行
jmp 標(biāo)簽名 和 cmp 命令組合使用死宣。跳轉(zhuǎn)到標(biāo)簽行
mov A,B 把 B 的值賦給 A
pop A 從棧中讀取數(shù)值并存入A
push A 把A的值存入棧中
ret 將處理返回到調(diào)用源
xor A,B A和B的位進(jìn)行亦或比較,并將結(jié)果存入A中

我們首先來看一下 _DATA 段定義的內(nèi)容碴开。_a1 label dword 定義了 _a1 這個標(biāo)簽毅该。標(biāo)簽表示的是相對于段定義起始位置的位置博秫。由于_a1_DATA 段定義的開頭位置,所以相對位置是0眶掌。_a1 就相當(dāng)于是全局變量a1挡育。編譯后的函數(shù)名和變量名前面會加一個(_),這也是 Borland C++ 的規(guī)定朴爬。dd 1 指的是即寒,申請分配了4字節(jié)的內(nèi)存空間,存儲著1這個初始值寝殴。dd指的是 define double word表示有兩個長度為2的字節(jié)領(lǐng)域(word)蒿叠,也就是4字節(jié)的意思。

Borland C++ 中蚣常,由于int 類型的長度是4字節(jié)市咽,因此匯編器就把 int a1 = 1 變換成了 _a1 label dword 和 dd 1。同樣抵蚊,這里也定義了相當(dāng)于全局變量的 a2 - a5 的標(biāo)簽 _a2 - _a5施绎,它們各自的初始值 2 - 5 也被存儲在各自的4字節(jié)中。

接下來贞绳,我們來說一說 _BSS 段定義的內(nèi)容谷醉。這里定義了相當(dāng)于全局變量 b1 - b5 的標(biāo)簽 _b1 - _b5。其中的db 4dup(?) 表示的是申請分配了4字節(jié)的領(lǐng)域冈闭,但值尚未確定(這里用 ? 來表示)的意思俱尼。db(define byte) 表示有1個長度是1字節(jié)的內(nèi)存空間。因而萎攒,db 4 dup(?) 的情況下遇八,就是4字節(jié)的內(nèi)存空間。

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

臨時確保局部變量使用的內(nèi)存空間

我們知道,局部變量是臨時保存在寄存器和棧中的羊精。函數(shù)內(nèi)部利用棧進(jìn)行局部變量的存儲斯够,函數(shù)調(diào)用完成后,局部變量值被銷毀喧锦,但是寄存器可能用于其他目的读规。所以,局部變量只是函數(shù)在處理期間臨時存儲在寄存器和棧中的燃少。

回想一下上述代碼是不是定義了10個局部變量束亏?這是為了表示存儲局部變量的不僅僅是棧,還有寄存器供汛。為了確保 c1 - c10 所需的域枪汪,寄存器空閑的時候就會使用寄存器涌穆,寄存器空間不足的時候就會使用棧。

讓我們繼續(xù)來分析上面代碼的內(nèi)容雀久。_TEXT段定義表示的是 MyFunc 函數(shù)的范圍宿稀。在 MyFunc 函數(shù)中定義的局部變量所需要的內(nèi)存領(lǐng)域。會被盡可能的分配在寄存器中赖捌。大家可能認(rèn)為使用高性能的寄存器來替代普通的內(nèi)存是一種資源浪費祝沸,但是編譯器不這么認(rèn)為,只要寄存器有空間越庇,編譯器就會使用它罩锐。由于寄存器的訪問速度遠(yuǎn)高于內(nèi)存,所以直接訪問寄存器能夠高效的處理卤唉。局部變量使用寄存器涩惑,是 Borland C++ 編譯器最優(yōu)化的運行結(jié)果。

代碼清單中的如下內(nèi)容表示的是向寄存器中分配局部變量的部分

mov       eax,1
mov       edx,2
mov       ecx,3
mov       ebx,4
mov       esi,5

僅僅對局部變量進(jìn)行定義是不夠的桑驱,只有在給局部變量賦值時竭恬,才會被分配到寄存器的內(nèi)存區(qū)域。上述代碼相當(dāng)于就是給5個局部變量 c1 - c5 分別賦值為 1 - 5熬的。eax痊硕、edx、ecx押框、ebx岔绸、esi 是 x86 系列32位 CPU 寄存器的名稱。至于使用哪個寄存器橡伞,是由編譯器來決定的 盒揉。

x86 系列 CPU 擁有的寄存器中,程序可以操作的是十幾骑歹,其中空閑的最多會有幾個预烙。因而墨微,局部變量超過寄存器數(shù)量的時候道媚,可分配的寄存器就不夠用了,這種情況下翘县,編譯器就會把棧派上用場最域,用來存儲剩余的局部變量。

在上述代碼這一部分锈麸,給局部變量c1 - c5 分配完寄存器后镀脂,可用的寄存器數(shù)量就不足了。于是忘伞,剩下的5個局部變量c6 - c10 就被分配給了棧的內(nèi)存空間薄翅。如下面代碼所示

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

函數(shù)入口 add esp,-20 指的是沙兰,對棧數(shù)據(jù)存儲位置的 esp 寄存器(棧指針)的值做減20的處理。為了確保內(nèi)存變量 c6 - c10 在棧中翘魄,就需要保留5個 int 類型的局部變量(4字節(jié) * 5 = 20 字節(jié))所需的空間鼎天。mov ebp,esp這行指令表示的意思是將 esp 寄存器的值賦值到 ebp 寄存器。之所以需要這么處理暑竟,是為了通過在函數(shù)出口處 mov esp ebp 這一處理斋射,把 esp 寄存器的值還原到原始狀態(tài),從而對申請分配的椀纾空間進(jìn)行釋放罗岖,這時棧中用到的局部變量就消失了。這也是棧的清理處理腹躁。在使用寄存器的情況下桑包,局部變量則會在寄存器被用于其他用途時自動消失,如下圖所示纺非。

image
 mov       dword ptr [ebp-4],6
 mov       dword ptr [ebp-8],7
 mov       dword ptr [ebp-12],8
 mov       dword ptr [ebp-16],9
 mov       dword ptr [ebp-20],10

這五行代碼是往椉穸啵空間代入數(shù)值的部分,由于在向棧申請內(nèi)存空間前铐炫,借助了 mov ebp, esp 這個處理垒手,esp 寄存器的值被保存到了 esp 寄存器中,因此倒信,通過使用[ebp - 4]科贬、[ebp - 8]、[ebp - 12]鳖悠、[ebp - 16]榜掌、[ebp - 20] 這樣的形式,就可以申請分配20字節(jié)的棧內(nèi)存空間切分成5個長度為4字節(jié)的空間來使用乘综。例如憎账,mov dword ptr [ebp-4],6 表示的就是,從申請分配的內(nèi)存空間的下端(ebp寄存器指示的位置)開始向前4字節(jié)的地址([ebp - 4]) 中卡辰,存儲著6這一4字節(jié)數(shù)據(jù)胞皱。

image

循環(huán)控制語句的處理

上面說的都是順序流程,那么現(xiàn)在就讓我們分析一下循環(huán)流程的處理九妈,看一下 for 循環(huán)以及 if 條件分支等 c 語言程序的 流程控制是如何實現(xiàn)的反砌,我們還是以代碼以及編譯后的結(jié)果為例,看一下程序控制流程的處理過程萌朱。

// 定義MySub 函數(shù)
void MySub(){
  // 不做任何處理

}

// 定義MyFunc 函數(shù)
void Myfunc(){
  int i;
  for(int i = 0;i < 10;i++){
    // 重復(fù)調(diào)用MySub十次
    MySub();
  }
}

上述代碼將局部變量 i 作為循環(huán)條件宴树,循環(huán)調(diào)用十次MySub 函數(shù),下面是它主要的匯編代碼

xor ebx, ebx ; 將寄存器清0
@4  call_MySub; 調(diào)用MySub函數(shù)
incebx; ebx寄存器的值 + 1
cmpebx,10;將ebx寄存器的值和10進(jìn)行比較
jlshort @4; 如果小于10就跳轉(zhuǎn)到 @4

C 語言中的 for 語句是通過在括號中指定循環(huán)計數(shù)器的初始值(i = 0)晶疼、循環(huán)的繼續(xù)條件(i < 10)酒贬、循環(huán)計數(shù)器的更新(i++) 這三種形式來進(jìn)行循環(huán)處理的又憨。與此相對的匯編代碼就是通過比較指令(cmp)跳轉(zhuǎn)指令(jl)來實現(xiàn)的。

下面我們來對上述代碼進(jìn)行說明

MyFunc 函數(shù)中用到的局部變量只有 i 锭吨,變量 i 申請分配了 ebx 寄存器的內(nèi)存空間竟块。for 語句括號中的 i = 0 被轉(zhuǎn)換為 xor ebx,ebx 這一處理,xor 指令會對左起第一個操作數(shù)和右起第二個操作數(shù)進(jìn)行 XOR 運算耐齐,然后把結(jié)果存儲在第一個操作數(shù)中浪秘。由于這里把第一個操作數(shù)和第二個操作數(shù)都指定為了 ebx,因此就變成了對相同數(shù)值的 XOR 運算埠况。也就是說不管當(dāng)前寄存器的值是什么耸携,最終的結(jié)果都是0。類似的辕翰,我們使用 mov ebx,0 也能得到相同的結(jié)果夺衍,但是 xor 指令的處理速度更快,而且編譯器也會啟動最優(yōu)化功能喜命。

XOR 指的就是異或操作沟沙,它的運算規(guī)則是 如果a、b兩個值不相同壁榕,則異或結(jié)果為1矛紫。****如果a、b兩個值相同牌里,異或結(jié)果為0颊咬。

相同數(shù)值進(jìn)行 XOR 運算,運算結(jié)果為0牡辽。XOR 的運算規(guī)則是喳篇,值不同時結(jié)果為1,值相同時結(jié)果為0态辛。例如 01010101 和 01010101 進(jìn)行運算麸澜,就會分別對各個數(shù)字位進(jìn)行 XOR 運算。因為每個數(shù)字位都相同奏黑,所以運算結(jié)果為0炊邦。

ebx 寄存器的值初始化后,會通過 call 指定調(diào)用 _MySub 函數(shù)攀涵,從 _MySub 函數(shù)返回后铣耘,會執(zhí)行inc ebx 指令洽沟,對 ebx 的值進(jìn)行 + 1 操作以故,這個操作就相當(dāng)于 i++ 的意思,++ 表示的就是當(dāng)前數(shù)值 + 1裆操。

這里需要知道 i++ 和 ++i 的區(qū)別

i++ 是先賦值怒详,復(fù)制完成后再對 i執(zhí)行 + 1 操作

++i 是先進(jìn)行 +1 操作炉媒,完成后再進(jìn)行賦值

inc 下一行的 cmp 是用來對第一個操作數(shù)和第二個操作數(shù)的數(shù)值進(jìn)行比較的指令。cmp ebx,10 就相當(dāng)于 C 語言中的 i < 10 這一處理昆烁,意思是把 ebx 寄存器的值與10進(jìn)行比較吊骤。匯編語言中比較指令的結(jié)果,會存儲在 CPU 的標(biāo)志寄存器中静尼。不過白粉,標(biāo)志寄存器的值,程序是無法直接參考的鼠渺。那如何判斷比較結(jié)果呢鸭巴?

匯編語言中有多個跳轉(zhuǎn)指令,這些跳轉(zhuǎn)指令會根據(jù)標(biāo)志寄存器的值來判斷是否進(jìn)行跳轉(zhuǎn)操作拦盹,例如最后一行的 jl鹃祖,它會根據(jù) cmp ebx,10 指令所存儲在標(biāo)志寄存器中的值來判斷是否跳轉(zhuǎn),jl 這條指令表示的就是 jump on less than(小于的話就跳轉(zhuǎn))普舆。發(fā)現(xiàn)如果 i 比 10 小恬口,就會跳轉(zhuǎn)到 @4 所在的指令處繼續(xù)執(zhí)行。

那么匯編代碼的意思也可以用 C 語言來改寫一下沼侣,加深理解

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

代碼第一行 i ^= i 指的就是 i 和 i 進(jìn)行異或運算祖能,也就是 XOR 運算,MySub() 函數(shù)用 L4 標(biāo)簽來替代蛾洛,然后進(jìn)行 i 自增操作芯杀,如果i 的值小于 10 的話,就會一直循環(huán) MySub() 函數(shù)雅潭。

條件分支的處理方法

條件分支的處理方式和循環(huán)的處理方式很相似揭厚,使用的也是 cmp 指令和跳轉(zhuǎn)指令。下面是用 C 語言編寫的條件分支的代碼

// 定義MySub1 函數(shù)
void MySub1(){

 // 不做任何處理
}

// 定義MySub2 函數(shù)
void MySub2(){

 // 不做任何處理
}

// 定義MySub3 函數(shù)
void MySub3(){

 // 不做任何處理
}

// 定義MyFunc 函數(shù)
void MyFunc(){

 int a = 123;
 // 根據(jù)條件調(diào)用不同的函數(shù)
 if(a > 100){
  MySub1();
 }
 else if(a < 50){
  MySub2();
 }
 else
 {
  MySub3();
 }

}

很簡單的一個實現(xiàn)了條件判斷的 C 語言代碼扶供,那么我們把它用 Borland C++ 編譯之后的結(jié)果如下

_MyFunc proc near
 push      ebp
 mov       ebp,esp
 mov       eax,123; 把123存入 eax 寄存器中
 cmp       eax,100; 把 eax 寄存器的值同100進(jìn)行比較
 jle       short @8; 比100小時筛圆,跳轉(zhuǎn)到@8標(biāo)簽
 call      _MySub1; 調(diào)用MySub1函數(shù)
 jmp  short @11 ; 跳轉(zhuǎn)到@11標(biāo)簽
@8:
 cmp       eax,50; 把 eax 寄存器的值同50進(jìn)行比較
 jge       short @10; 比50大時,跳轉(zhuǎn)到@10標(biāo)簽
 call      _MySub2; 調(diào)用MySub2函數(shù)
 jmp short @11; 跳轉(zhuǎn)到@11標(biāo)簽
@10:
 call      _MySub3; 調(diào)用MySub3函數(shù)
@11:
 pop       ebp
 ret
_MyFunc endp

上面代碼用到了三種跳轉(zhuǎn)指令椿浓,分別是jle(jump on less or equal) 比較結(jié)果小時跳轉(zhuǎn)太援,jge(jump on greater or equal) 比較結(jié)果大時跳轉(zhuǎn),還有不管結(jié)果怎樣都會進(jìn)行跳轉(zhuǎn)的jmp扳碍,在這些跳轉(zhuǎn)指令之前還有用來比較的指令 cmp提岔,構(gòu)成了上述匯編代碼的主要邏輯形式。

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

通過對上述匯編代碼和 C 語言源代碼進(jìn)行比較笋敞,想必大家對程序的運行方式有了新的理解碱蒙,而且,從匯編源代碼中獲取的知識,也有助于了解 Java 等高級語言的特性赛惩,比如 Java 中就有 native 關(guān)鍵字修飾的變量哀墓,那么這個變量的底層就是使用 C 語言編寫的,還有一些 Java 中的語法糖只有通過匯編代碼才能知道其運行邏輯喷兼。在某些情況下篮绰,對于查找 bug 的原因也是有幫助的。

上面我們了解到的編程方式都是串行處理的季惯,那么串行處理有什么特點呢吠各?

image

串行處理最大的一個特點就是專心只做一件事情,一件事情做完之后才會去做另外一件事情勉抓。

計算機(jī)是支持多線程的走孽,多線程的核心就是 CPU切換,如下圖所示

image

我們還是舉個實際的例子琳状,讓我們來看一段代碼

// 定義全局變量
int counter = 100;

// 定義MyFunc1()
void MyFunc(){
  counter *= 2;
}

// 定義MyFunc2()
void MyFunc2(){
  counter *= 2;
}

</pre>

上述代碼是更新 counter 的值的 C 語言程序磕瓷,MyFunc1() 和 MyFunc2() 的處理內(nèi)容都是把 counter 的值擴(kuò)大至原來的二倍,然后再把 counter 的值賦值給 counter 念逞。這里困食,我們假設(shè)使用多線程處理,同時調(diào)用了一次MyFunc1 和 MyFunc2 函數(shù)翎承,這時硕盹,全局變量 counter 的值,理應(yīng)編程 100 * 2 * 2 = 400叨咖。如果你開啟了多個線程的話瘩例,你會發(fā)現(xiàn) counter 的數(shù)值有時也是 200,對于為什么出現(xiàn)這種情況甸各,如果你不了解程序的運行方式垛贤,是很難找到原因的。

我們將上面的代碼轉(zhuǎn)換成匯編語言的代碼如下

mov eax,dword ptr[_counter] ; 將 counter 的值讀入 eax 寄存器
add eax,eax; 將 eax 寄存器的值擴(kuò)大2倍趣倾。
mov dword ptr[_counter],eax; 將 eax 寄存器的值存入 counter 中聘惦。

在多線程程序中,用匯編語言表示的代碼每運行一行儒恋,處理都有可能切換到其他線程中善绎。因而,假設(shè) MyFun1 函數(shù)在讀出 counter 數(shù)值100后诫尽,還未來得及將它的二倍值200寫入 counter 時禀酱,正巧 MyFun2 函數(shù)讀出了 counter 的值100,那么結(jié)果就將變?yōu)?200 牧嫉。

image

為了避免該bug剂跟,我們可以采用以函數(shù)或 C 語言代碼的行為單位來禁止線程切換的鎖定方法,或者使用某種線程安全的方式來避免該問題的出現(xiàn)。

現(xiàn)在基本上沒有人用匯編語言來編寫程序了浩聋,因為 C观蜗、Java等高級語言的效率要比匯編語言快很多臊恋。不過衣洁,匯編語言的經(jīng)驗還是很重要的,通過借助匯編語言抖仅,我們可以更好的了解計算機(jī)運行機(jī)制坊夫。

文章參考

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/環(huán)形緩沖器/22701730?fr=aladdin

《程序是怎樣跑起來的》

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

https://baike.baidu.com/item/Windows操作系統(tǒng)/852149?fr=aladdin

磁盤

磁盤緩存

虛擬內(nèi)存

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/內(nèi)存/103614?fr=aladdin

https://blog.csdn.net/mark_lq/article/details/44245423

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市撤卢,隨后出現(xiàn)的幾起案子环凿,更是在濱河造成了極大的恐慌,老刑警劉巖放吩,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件智听,死亡現(xiàn)場離奇詭異,居然都是意外死亡渡紫,警方通過查閱死者的電腦和手機(jī)到推,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惕澎,“玉大人莉测,你說我怎么就攤上這事∵蠛恚” “怎么了捣卤?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長八孝。 經(jīng)常有香客問我董朝,道長,這世上最難降的妖魔是什么干跛? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任益涧,我火速辦了婚禮,結(jié)果婚禮上驯鳖,老公的妹妹穿的比我還像新娘闲询。我一直安慰自己,他們只是感情好浅辙,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布扭弧。 她就那樣靜靜地躺著,像睡著了一般记舆。 火紅的嫁衣襯著肌膚如雪鸽捻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機(jī)與錄音御蒲,去河邊找鬼衣赶。 笑死,一個胖子當(dāng)著我的面吹牛厚满,可吹牛的內(nèi)容都是我干的府瞄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼碘箍,長吁一口氣:“原來是場噩夢啊……” “哼遵馆!你這毒婦竟也來了啼辣?” 一聲冷哼從身側(cè)響起古今,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎他宛,沒想到半個月后四濒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體换况,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年盗蟆,在試婚紗的時候發(fā)現(xiàn)自己被綠了戈二。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡姆涩,死狀恐怖挽拂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情骨饿,我是刑警寧澤亏栈,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站宏赘,受9級特大地震影響绒北,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜察署,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一闷游、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贴汪,春花似錦脐往、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至阳懂,卻和暖如春梅尤,著一層夾襖步出監(jiān)牢的瞬間柜思,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工巷燥, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留赡盘,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓缰揪,卻偏偏與公主長得像陨享,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子邀跃,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 王爽匯編全書知識點大綱 第一章 基礎(chǔ)知識 機(jī)器語言 匯編語言的產(chǎn)生 匯編語言的組成 存儲器 cpu對存儲器的讀寫 ...
    2c3ba901516f閱讀 2,417評論 0 1
  • 眾所周知霉咨,計算機(jī)硬件主要由CPU(運算器和控制器)蛙紫、存儲器(內(nèi)存和外存)拍屑、外部設(shè)備(輸入/輸出設(shè)備)等構(gòu)成。那這幾...
    張利鋒閱讀 5,860評論 0 4
  • 一.認(rèn)識匯編語言 要認(rèn)識匯編語言坑傅,還得從編程語言的發(fā)展說起僵驰,語言有以下幾種分類,其發(fā)展都是為了讓我們更容易去操縱計...
    WellsCai閱讀 989評論 0 1
  • CPU執(zhí)行的也不只是一條指令唁毒,一般一個程序包含很多條指令 因為有if…else蒜茴、for這樣的條件和循環(huán)存在,這些指...
    JavaEdge閱讀 566評論 1 2
  • 計算機(jī)系統(tǒng)漫游 代碼從文本到可執(zhí)行文件的過程(c語言示例):預(yù)處理階段浆西,處理 #inlcude 粉私, #defin...
    willdimagine閱讀 3,581評論 0 5