计算机基础- -认识汇编
計算機基礎- -匯編語言
文章目錄
- 計算機基礎- -匯編語言
- 一、匯編語言和本地代碼
- 二、通過編譯器輸出匯編語言的源代碼
- 三、不會轉換成本地代碼的偽指令
- 四、匯編語言的語法是操作碼+操作數
- 1.指令解析
- 最常用的mov指令
- 對棧進行push和pop
- 2.函數的調用機制
- 3.函數的內部處理
- 4.全局變量和局部變量
- 5.臨時確保局部變量使用的內存空間
- 6.循環控制語句的處理
- 7.條件分支的處理方法
- 8.了解程序運行邏輯的必要性
一、匯編語言和本地代碼
- 計算機CPU只能運行本地代碼(機器語言) 程序, 用C語言等高級語言編寫的代碼, 需要經過編譯器編 譯后,轉換為本地代碼才能夠被CPU解釋執行。
- 但是本地代碼的可讀性非常差,所以需要使用一種能夠直接讀懂的語言來替換本地代碼
- ,那就是在各本地代碼中,附帶上表示其功能的英文縮寫, 比如在加法運算的本地代碼加上add(addition)寫、在比較運算符的本地代碼中加上cmp(compare) 的縮寫等, 這些通過縮寫來表示具體本地代碼指 的縮 令的標志稱為助記符,使用助記符的語言稱為匯編語言。這樣,通過閱讀匯編語言,也能夠了解本 地代碼的含義了。
- 不過,即使是使用匯編語言編寫的源代碼,最終也必須要轉換為本地代碼才能夠運行,負責做這項工作的程序稱為編譯器,轉換的這個過程稱為匯編。
- 在將源代碼轉換為本地代碼這個功能方面,匯編器和編譯器是同樣的。
- 用匯編語言編寫的源代碼和本地代碼是一一對應的。因而,本地代碼也可以反過來轉換成匯編語言編寫的代碼。把本地代碼轉換為匯編代碼的這一過程稱為反匯編,執行反匯編的程序稱為反匯編程序。
- 哪怕是C語言編寫的源代碼, 編譯后也會轉換成特定CPU用的本地代碼。而將其反匯編的話, 就可以得到匯編語言的源代碼,并對其內容進行調查。
- 不過,本地代碼變成C語言源代碼的反編譯,要比本地代碼轉換成匯編代碼的反匯編要困難,這是因為,C語言代碼和本地代碼不是一一對應的關系。
二、通過編譯器輸出匯編語言的源代碼
我們上面提到本地代碼可以經過反匯編轉換成為匯編代碼,但是只有這一種轉換方式嗎?
顯然不是,C語言編寫的源代碼也能夠通過編譯器編譯稱為匯編代碼,下面就來嘗試一下。
-
編寫完成后將其文件名保存為Sample 4.c, C語言源文件的擴展名, 通常用.c
來表示,上面程序是提供兩個輸入參數并返回它們之和。 -
在Windows操作系統下打開輸入命令提示符, 切換到保存Sample 4.c的文件夾下, 然后在命令提示符中
- bcc 32是啟動Borland C++的命令,
- -C 的選項是指僅進行編譯而不進行鏈接,
- -S選項被用來指定生成匯編語言的源代碼
- 作為編譯的結果, 當前目錄下會生成一個名為Sample 4.asm的匯編語言源代碼。匯編語言源文件的擴展名, 通常用.asm來表示
下面就讓我們用編輯器打開看一下Sample 4.asm中的內容:
三、不會轉換成本地代碼的偽指令
第一次看到匯編代碼的讀者可能感覺起來比較難,不過實際上其實比較簡單,而且可能比C語言還要簡單,為了便于閱讀匯編代碼的源代碼,需要注意幾個要點
- 匯編語言的源代碼,是由轉換成本地代碼的指令(后面講述的操作碼)和針對匯編器的偽指令構成的。
- 偽指令負責把程序的構造以及匯編的方法指示給匯編器(轉換程序)。不過偽指令是無法匯編轉換成為 本地代碼的。
下面是上面程序截取的偽指令:
-
由偽指令 segment 和 ends圍起來的部分,是給構成程序的命令和數據的集合體上加一個名字而得 到的,稱為段定義。
-
段定義的英文表達具有區域的意思,在這個程序中,段定義指的是命令和數據等程序的集合體的意思,一個程序由多個段定義構成。
-
上面代碼的開始位置, 定義了3個名稱分別為_TEXT、_DATA、_BSS的段定義, _TEXT是指定的段定義,_DATA是被初始化(有初始值)的數據的段定義,_BSS是尚未初始化的數據的段定義。
-
這種定義的名稱是由Borland C++定義的, 是由Borland C++編譯器自動分配的, 所以程序段定義的順序就成為了_TEXT、_DATA、_BSS, 這樣也確保了內存的連續性
段定義(segment) 是用來區分或者劃分范圍區域的意思。匯編語言的segment偽指令表示段定 義的起始,ends偽指令表示段定義的結束。段定義是一段連續的內存空間
- 而group這個偽指令表示的是將_BSS和_DATA這兩個段定義匯總名為D GROUP的組
- 圍起_Add Num和_My Fun的_TEXT segment和_TEXT ends, 表示_Add Num
和_My Fun是屬于_TEXT這一段定義的。
- 編譯后在函數名前附帶上下劃線_, 是Borland C++的規定。在C語言中編寫的Add Num函數, 在內部是以_Add Num這個名稱處理的。
- 偽指令proc和endp圍起來的部分, 表示的是過程(procedure)的范圍。在匯編語言中,這種相當于C語言的函數的形式稱為過程。末尾的end偽指令,表示的是源代碼的結束。
四、匯編語言的語法是操作碼+操作數
- 在匯編語言中, 一行表示一對CPU的一個指令。匯編語言指令的語法結構是操作碼+操作數, 也存 在只有操作碼沒有操作數的指令。
- 操作碼表示的是指令動作,操作數表示的是指令對象。操作碼和操作數一起使用就是一個英文指令。
- 比 如從英語語法來分析的話, 操作碼是動詞, 操作數是賓語。
- 比如這個句子Give me money這個英文 指令的話, Give就是操作碼,me和money就是操作數。
- 匯編語言中存在多個操作數的情況, 要用逗 號把它們分割, 就像是Give me, money這樣。
-
能夠使用何種形式的操作碼, 是由CPU的種類決定的, 下面對操作碼的功能進行了整理。
-
本地代碼需要加載到內存后才能運行, 內存中存儲著構成本地代碼的指令和數據。程序運行時, CPU會從內存中把數據和指令讀出來, 然后放在CPU內部的寄存器中進行處理
-
寄存器是CPU中的存儲區域, 寄存器除了具有臨時存儲和計算的功能之外, 還具有運算功能, x 86系列的主要種類和角色如下圖所示
1.指令解析
下面就對CPU中的指令進行分析
最常用的mov指令
- 指令中最常使用的是對寄存器和內存進行數據存儲的mov 指令, mov指令的兩個操作數, 分別用來 指定數據的存儲地和讀出源。
- 操作數中可以指定寄存器、常數、標簽(附加在地址前),以及用方括號 ([])圍起來的這些內容。
- 如果指定了沒有用([])方括號圍起來的內容,就表示對該值進行處理;如果指定了用方括號圍起來的內容,方括號的值則會被解釋為內存地址,然后就會對該內存地址對應的值進行讀寫操作。
讓我們對上面的代碼片段進行說明
- move bp, esp中, esp寄存器中的值被直接存儲在了ebp中, 也就是說, 如果esp寄存器的值是100的話那么ebp寄存器的值也是100。
- 而在 move eax, dword ptr[ebp+8]這條指令中, ebp寄存器的值+8后會被解析稱為內存地址。
- 如果ebp寄存器的值是100的話, 那么eax寄存器的值就是100+8的地址的值。
- dword ptr也叫做doubleword pointer,簡單解釋一下就是從指定的內存地址中讀出4字節的數據
對棧進行push和pop
- 程序運行時, 會在內存上申請分配一個稱為棧的數據空間。棧(stack) 的特性是后入先出, 數據在存儲時是從內存的下層(大的地址編號)逐漸往上層(小的地址編號)累積,讀出時則是按照從上往下進 行讀取的。
- 棧是存儲臨時數據的區域, 它的特點是通過push指令和pop指令進行數據的存儲和讀出。
- 向棧中存儲 數據稱為入棧,從棧中讀出數據稱為 出棧, 32位x 86系列的CPU中, 進行1次push或者pop,即可處理32位(4字節)的數據。
2.函數的調用機制
- 下面我們一起來分析一下函數的調用機制,我們以上面的C語言編寫的代碼為例。
- 首先,讓我們從MyFunc函數調用Add Num函數的匯編語言部分開始,來對函數的調用機制進行說明。
- 棧在函數的調 用中發揮了巨大的作用,下面是經過處理后的MyFunc函數的匯編處理內容
- 代碼解釋中的(1)、(2)、(7)、(8)的處理適用于C語言中的所有函數,(3)-(6)這一部分,這對了解函數調用機制至關重要。
- (3) 和(4) 表示的是將傳遞給Add Num函數的參數通過push入棧。
- 在C語言源代碼中, 雖然記述為函數Add Num(123, 456) , 但入棧時則會先按照456, 123這樣的順序。也就是位于后面的數值先入棧,這是C語言的規定(參數傳遞的先后次序)。
- (5) 表示的call指令, 會把程序流程跳轉到Add Num函數指令的地址處。在匯編語言中, 函數名表示的就是函數所在的內存地址。
- Add Num函數處理完畢后, 程序流程必須要返回到編號(6) 這一行。call指令運行后, call指令的下一行(也就指的是(6) 這一行) 的內存地址(調用函數完畢后要返回的內存地址) 會自動的push入棧。
- 該值會在Add Num函數處理的最后通過ret指令pop出棧,然后程序會返回到(6)這一行。(6) 部分會把棧中存儲的兩個參數(456和123) 進行銷毀處理。
- 雖然通過兩次的pop指令也可以實現,不過采用esp寄存器+8的方式會更有效率(處理1次即可) 。對棧進行數值的輸入和輸出時, 數值的單位是4字節。
- 因此, 通過在負責棧地址管理的esp寄存器中加上4的2倍8, 就可以達到和運行兩次pop命令同樣的效果。雖然內存中的數據實際上還殘留著, 但只要把esp寄存器的值更新為數據存儲地址前面的數據位置,該數據也就相當于銷毀了。
在編譯Sample 4.c文件時, 出現了下圖的這條消息
- 圖中的意思是指c的值在MyFunc定義了但是一直未被使用, 這其實是一項編譯器優化的功能, 由于存儲著Add Num函數返回值的變量c在后面沒有被用到, 因此編譯器就認為該變量沒有意義, 進而也就沒有生成與之對應的匯編語言代碼。
下圖是調用Add Num這一函數前后棧內存的變化
3.函數的內部處理
上面我們用匯編代碼分析了一下Sample 4.c整個過程的代碼, 現在我們著重分析一下Add Num函數的源代碼部分,分析一下參數的接收、返回值和返回等機制
- 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_Add Num) 的下一行。
這時, Add Num函數入口和出口處棧的狀態變化, 就如下圖所示
4.全局變量和局部變量
在熟悉了匯編語言后,接下來我們來了解一下全局變量和局部變量,在函數外部定義的變量稱為全局變量,在函數內部定義的變量稱為局部變量
全局變量可以在任意函數中使用,局部變量只能在函數定義局部變量的內部使用。
下面定義的C語言代碼分別定義了局部變量和全局變量,并且給各變量進行了賦值,我們先看一下源代碼部分
- 我們分析其匯編源碼就好, 我們用Borland C++編譯后的匯編代碼如下:
- 我們在分析上面匯編代碼之前,先來認識一下更多的匯編指令,此表是對上面部分操作碼及其功能的接續
注意:db 4 dup(?) 不要和dd 4混淆了, 前者表示的是4個長度是1字節的內存空間。而db 4表示的則是雙字節(=4字節)的內存空間中存儲的值是4
5.臨時確保局部變量使用的內存空間
- 我們知道,局部變量是臨時保存在寄存器和棧中的。函數內部利用棧進行局部變量的存儲,函數調用完成后,局部變量值被銷毀,但是寄存器可能用于其他目的。所以,局部變量只是函數在處理期間臨時存 儲在寄存器和棧中的。
- 回想一下上述代碼是不是定義了10個局部變量?這是為了表示存儲局部變量的不僅僅是棧,還有寄存器。
- 為了確保c1-c10所需的域,寄存器空閑的時候就會使用寄存器,寄存器空間不足的時候就會使用棧。
讓我們繼續來分析上面代碼的內容:
- _TEXT段定義表示的是MyFunc函數的范圍。在MyFunc函數中定義的局部變量所需要的內存領域。會被盡可能的分配在寄存器中。大家可能認為使用高性能的寄存器來替代普通的內存是一種資源浪費,但是編譯器不這么認為,只要寄存器有空間,編譯器就會使用它。
- 由于寄存器的訪問速度遠高于內存,所以直接訪問寄存器能夠高效的處理。局部變量使用寄存器,是Borland C++編譯器最優化的運行結果。
代碼清單中的如下內容表示的是向寄存器中分配局部變量的部分
- 僅僅對局部變量進行定義是不夠的,只有在給局部變量賦值時,才會被分配到寄存器的內存區域。
- 上述代碼相當于就是給5個局部變量c 1-c 5分別賦值為1-5。eax、edx、ecx、ebx、esi是x 86系列32位CPU寄存器的名稱。至于使用哪個寄存器, 是由編譯器來決定的。
- x 86系列CPU擁有的寄存器中, 程序可以操作的是十幾, 其中空閑的最多會有幾個。因而, 局部變量超過寄存器數量的時候,可分配的寄存器就不夠用了,這種情況下,編譯器就會把棧派上用場,用來存儲剩余的局部變量。
- 在上述代碼這一部分,給局部變量c1-c5分配完寄存器后,可用的寄存器數量就不足了。于是,剩下的5個局部變量c6-c10就被分配給了棧的內存空間。
如下面代碼所示
- 函數入口add esp, -20指的是, 對棧數據存儲位置的esp寄存器(棧指針) 的值做減20的處理。
- 為了確保內存變量c 6-c 10在棧中, 就需要保留5個int類型的局部變量(4字節*5=20字節) 所需的空間。
- mov ebp, esp這行**指令表示的意思是將esp寄存器的值賦值到ebp寄存器。之所以需要這么處理,是為了通過在函數出口處mov esp ebp這一處理, 把esp寄存器的值還原到原始狀態, 從而對請分配的棧空間進行釋放**,這時棧中用到的局部變量就消失了。這也是棧的清理處理。
- 在使用寄存器的情況下,局部變量則會在寄存器被用于其他用途時自動消失
如下圖所示。
- 這五行代碼是往棧空間代入數值的部分,由于在向棧申請內存空間前,借助了
move bp, 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字節數據。
6.循環控制語句的處理
上面說的都是順序流程,那么現在就讓我們分析一下循環流程的處理,看一下
for循環以及if條件分支等c語言程序的流程控制是如何實現的
我們還是以代碼以及編譯后的結果為例,看一下程序控制流程的處理過程。
- C語言中的for語句是通過在括號中指定循環計數器的初始值(=0) 、循環的繼續條件(i<10) 、循環計數器的更新(i++) 這三種形式來進行循環處理的。與此相對的匯編代碼就是通過比較指令(cmp)和跳轉指令(jl)來實現的。
下面我們來對上述代碼進行說明
- MyFunc函數中用到的局部變量只有i, 變量i申請分配了ebx寄存器的內存空間。
- for語句括號中的x or ebx, ebx 這一處理, x or指令會對左起第一個操作數和右起第二個操作數進行i=0被轉換為X OR運算, 然后把結果存儲在第一個操作數中。
- 由于這里把第一個操作數和第二個操作數都指定為了ebx, 因此就變成了對相同數值的X OR運算。
- 也就是說不管當前寄存器的值是什么, 最終的結果都是0。
- 類似的,我們使用move bx, 0也能得到相同的結果, 但是x or指令的處理速度更快, 而且編譯器也會啟動最優化功能。
- X OR指的就是異或操作, 它的運算規則是如果a、b兩個值不相同, 則異或結果為1。如果a、b兩 個值相同,異或結果為0。
- 例如01010101和01010101進行運算,就會分別對各個數字位進行X OR運算。因為每個 數字位都相同,所以運算結果為0。
-
ebx寄存器的值初始化后, 會通過call指定調用_My Sub函數, 從_My Sub函數返回后, 會執行incebx 指令, 對ebx的值進行+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所在的指令處繼續執行。
7.條件分支的處理方法
- 條件分支的處理方式和循環的處理方式很相似, 使用的也是cmp指令和跳轉指令。
下面是用C語言編寫的條件分支的代碼:
很簡單的一個實現了條件判斷的C語言代碼, 那么我們把它用Borland C++編譯之后的結果如下:
- 上面代碼用到了三種跳轉指令, 分別是jle(jump on lessor equal) 比較結果小時跳轉, jge(jump on greater or equal) 比較結果大時跳轉, 還有不管結果怎樣都會進行跳轉的jmp, 在這些跳轉指令之前還有用來比較的指令cmp, 構成了上述匯編代碼的主要邏輯形式。
8.了解程序運行邏輯的必要性
- 通過對上述匯編代碼和C語言源代碼進行比較,想必大家對程序的運行方式有了新的理解,而且,從匯編源代碼中獲取的知識, 也有助于了解高級語言的特性
- 上面我們了解到的編程方式都是串行處理的,那么串行處理有什么特點呢?
- 串行處理最大的一個特點就是專心只做一件事情,一件事情做完之后才會去做另外一件事情。計算機是支持多線程的, 多線程的核心就是CPU切換
如下圖所示:
我們還是舉個實際的例子,讓我們來看一段代碼:
- 上述代碼是更新counter的值的C語言程序, MyFunc 1() 和MyFunc 2) 的處理內容都是把counter的值擴大至原來的二倍, 然后再把counter的值賦值給counter。
- 這里, 我們假設使用多線程處理, 同時調用了一次MyFunc 1和MyFunc 2函數, 這時, 全局變量counter的值, 理應編程10022=400。
- 如果你開啟了多個線程的話, 你會發現counter的數值有時也是200, 對于為什么出現這種情況,如果你不了解程序的運行方式,是很難找到原因的。
我們將上面的代碼轉換成匯編語言的代碼如下
- 在多線程程序中,用匯編語言表示的代碼每運行一行,處理都有可能切換到其他線程中。
- 因而,假設My Fun 1函數在讀出counter數值100后, 還未來得及將它的二倍值200寫入counter時, 正巧My Fun 2函數讀出了counter的值100, 那么結果就將變為200。
- 為了避免該bug, 我們可以采用以函數或C語言代碼的行為單位來禁止線程切換的鎖定方法, 或者使用某種線程安全的方式來避免該問題的出現。
總結
以上是生活随笔為你收集整理的计算机基础- -认识汇编的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 计算机基础- -操作系统环境
- 下一篇: 计算机基础- -应用和硬件的关系