@程式設計師,快來速取硬核的彙編語言知識大全!

2020-12-14 CSDN

作者 | cxuan

責編 | maozz

彙編語言作為第二代計算機語言,會用一些容易理解和記憶的字母,單詞來代替一個特定的指令,作為高級程式語言的基礎,有必要系統的了解一下彙編語言,那麼本篇文章希望大家跟我一起來了解一下彙編語言。

彙編語言和本地代碼

計算機 CPU 只能運行本地代碼(機器語言)程序,用 C 語言等高級語言編寫的代碼,需要經過編譯器編譯後,轉換為本地代碼才能夠被 CPU 解釋執行。

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

這樣,通過閱讀彙編語言,也能夠了解本地代碼的含義了。

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

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

哪怕是 C 語言編寫的原始碼,編譯後也會轉換成特定 CPU 用的本地代碼。而將其反彙編的話,就可以得到彙編語言的原始碼,並對其內容進行調查。

不過,本地代碼變成 C 語言原始碼的反編譯,要比本地代碼轉換成彙編代碼的反彙編要困難,這是因為,C 語言代碼和本地代碼不是一一對應的關係。

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

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

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

// 返回兩個參數值之和的函數intAddNum(int a,int b){return a + b;}// 調用 AddNum 函數的函數voidMyFunc(){int c;c = AddNum(123,456);}

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

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

bcc32-c-SSample4.c

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

作為編譯的結果,當前目錄下會生成一個名為Sample4.asm 的彙編語言原始碼。彙編語言源文件的擴展名,通常用.asm 來表示,下面就讓我們用編輯器打開看一下 Sample4.asm 中的內容。

.386pifdef ??versionif ??version GT 500H.mmxendifendifmodel flatifndef??version?debugmacroendmendif?debugS "Sample4.c"?debugT "Sample4.c"_TEXTsegment dword public use32 'CODE'_TEXTends_DATAsegment dword public use32 'DATA'_DATAends_BSSsegment dword public use32 'BSS'_BSSendsDGROUPgroup_BSS,_DATA_TEXTsegment dword public use32 'CODE'_AddNumprocnear?live1@0:; ;int AddNum(int a,int b){ ;push ebpmov ebp,esp ; ; ; return a + b; ;@1:mov eax,dword ptr [ebp+8]add eax,dword ptr [ebp+12] ; ;} ;@3:@2:pop ebpret _AddNumendp_MyFuncprocnear?live1@48: ; ;void MyFunc(){ ;push ebpmov ebp,esp ; ; int c; ; c = AddNum(123,456); ;@4:push456push123call _AddNumadd esp,8 ; ;} ;@5:pop ebpret _MyFuncendp_TEXTendspublic_AddNumpublic_MyFunc?debugD "Sample4.c"2034345835end

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

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

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

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

_TEXTsegment dword public use32 'CODE'_TEXTends_DATAsegment dword public use32 'DATA'_DATAends_BSSsegment dword public use32 'BSS'_BSSendsDGROUPgroup_BSS,_DATA_AddNumprocnear_AddNumendp_MyFuncprocnear_MyFuncendp_TEXTendsend

由偽指令 segment 和 ends 圍起來的部分,是給構成程序的命令和數據的集合體上加一個名字而得到的,稱為段定義。

段定義的英文表達具有區域的意思,在這個程序中,段定義指的是命令和數據等程序的集合體的意思,一個程序由多個段定義構成。

上面代碼的開始位置,定義了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 的 _TEXT segment 和 _TEXT ends ,表示_AddNum 和 _MyFun 是屬於 _TEXT 這一段定義的。

_TEXTsegment dword public use32 'CODE'_TEXTends

因此,即使在原始碼中指令和數據是混雜編寫的,經過編譯和彙編後,也會轉換成為規整的本地代碼。

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

_AddNumprocnear_AddNumendp_MyFuncprocnear_MyFuncendp

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

末尾的 end 偽指令,表示的是原始碼的結束。

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

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

操作碼表示的是指令動作,操作數表示的是指令對象。操作碼和操作數一起使用就是一個英文指令。比如從英語語法來分析的話,操作碼是動詞,操作數是賓語。

比如這個句子 Give me money這個英文指令的話,Give 就是操作碼,me 和 money 就是操作數。彙編語言中存在多個操作數的情況,要用逗號把它們分割,就像是 Give me,money 這樣。

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

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

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

指令解析

下面就對 CPU 中的指令進行分析,最常用的 mov 指令。

指令中最常使用的是對寄存器和內存進行數據存儲的 mov 指令,mov 指令的兩個操作數,分別用來指定數據的存儲地和讀出源。

操作數中可以指定寄存器、常數、標籤(附加在地址前),以及用方括號([]) 圍起來的這些內容。如果指定了沒有用([]) 方括號圍起來的內容,就表示對該值進行處理;如果指定了用方括號圍起來的內容,方括號的值則會被解釋為內存地址,然後就會對該內存地址對應的值進行讀寫操作。讓我們對上面的代碼片段進行說明。

mov ebp,espmov 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)的特性是後入先出,數據在存儲時是從內存的下層(大的地址編號)逐漸往上層(小的地址編號)累積,讀出時則是按照從上往下進行讀取的。

棧是存儲臨時數據的區域,它的特點是通過 push 指令和 pop 指令進行數據的存儲和讀出。向棧中存儲數據稱為 入棧 ,從棧中讀出數據稱為 出棧,32位 x86 系列的 CPU 中,進行1次 push 或者 pop,即可處理 32 位(4位元組)的數據。

函數的調用機制

下面我們一起來分析一下函數的調用機制,我們以上面的 C 語言編寫的代碼為例。首先,讓我們從MyFunc 函數調用AddNum 函數的彙編語言部分開始,來對函數的調用機制進行說明。

棧在函數的調用中發揮了巨大的作用,下面是經過處理後的 MyFunc 函數的彙編處理內容。

_MyFunc proc nearpush ebp ; 將 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)_MyFunc endp

代碼解釋中的(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 文件時,出現了下圖的這條消息。

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

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

函數的內部處理

上面我們用彙編代碼分析了一下 Sample4.c 整個過程的代碼,現在我們著重分析一下 AddNum 函數的原始碼部分,分析一下參數的接收、返回值和返回等機制。

_AddNum procnearpushebp -----------(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 函數入口和出口處棧的狀態變化,就如下圖所示

全局變量和局部變量

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

下面定義的 C 語言代碼分別定義了局部變量和全局變量,並且給各變量進行了賦值,我們先看一下原始碼部分。

// 定義被初始化的全局變量int a1 = 1;int a2 = 2;int a3 = 3;int a4 = 4;int a5 = 5;// 定義沒有初始化的全局變量int b1,b2,b3,b4,b5;// 定義函數voidMyFunc(){// 定義局部變量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;}

上面的代碼挺暴力的,不過沒關係,能夠便於我們分析其彙編源碼就好,我們用 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 的段定義中。

1._DATA segment dword public use32 'DATA'

...

_DATA ends

2.沒有初始化的全局變量,會匯總到名為 _BSS 的段定義中

_BSS segment dword public use32 'BSS'

...

_BSS ends

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

_TEXT segment dword public use32 'CODE'

_MyFunc proc near

...

_MyFunc endp

_TEXT ends

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

我們首先來看一下 _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++ 編譯器最優化的運行結果。

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

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

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

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

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

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

函數入口 add esp,-20 指的是,對棧數據存儲位置的 esp 寄存器(棧指針)的值做減20的處理。為了確保內存變量 c6 - c10 在棧中,就需要保留5個 int 類型的局部變量(4位元組 * 5 = 20 字節)所需的空間。

mov ebp,esp這行指令表示的意思是將 esp 寄存器的值賦值到 ebp 寄存器。之所以需要這麼處理,是為了通過在函數出口處 mov esp ebp 這一處理,把 esp 寄存器的值還原到原始狀態,從而對申請分配的棧空間進行釋放,這時棧中用到的局部變量就消失了。

這也是棧的清理處理。在使用寄存器的情況下,局部變量則會在寄存器被用於其他用途時自動消失,如下圖所示。

用於局部變量的棧空間的申請分配和釋放。

movdwordptr[ebp-4],6movdwordptr[ebp-8],7movdwordptr[ebp-12],8movdwordptr[ebp-16],9movdwordptr[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位元組數據。

循環控制語句的處理

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

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

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

xorebx, ebx ; 將寄存器清0@4 call_MySub; 調用MySub函數incebx; ebx寄存器的值 + 1cmpebx,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。相同數值進行 XOR 運算,運算結果為0。XOR 的運算規則是,值不同時結果為1,值相同時結果為0。例如 01010101 和 01010101 進行運算,就會分別對各個數字位進行 XOR 運算。因為每個數字位都相同,所以運算結果為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) goto L4;

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

條件分支的處理方法

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

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

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

_MyFunc proc nearpush ebpmov ebp,esp mov eax,123; 把123存入 eax 寄存器中 cmp eax,100; 把 eax 寄存器的值同100進行比較 jle short @8; 比100小時,跳轉到@8標籤 call _MySub1; 調用MySub1函數 jmp short @11 ; 跳轉到@11標籤@8: cmp eax,50; 把 eax 寄存器的值同50進行比較 jge short @10; 比50大時,跳轉到@10標籤 call _MySub2; 調用MySub2函數 jmp short @11; 跳轉到@11標籤@10: call _MySub3; 調用MySub3函數@11:pop ebp ret_MyFunc endp

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

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

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

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

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

計算機是支持多線程的,多線程的核心就是 CPU切換,如下圖所示。

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

// 定義全局變量int counter = 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,dwordptr[_counter] ; 將 counter 的值讀入 eax 寄存器addeax,eax; 將 eax 寄存器的值擴大2倍。movdwordptr[_counter],eax; 將 eax 寄存器的值存入 counter 中。

在多線程程序中,用彙編語言表示的代碼每運行一行,處理都有可能切換到其他線程中。

因而,假設 MyFun1 函數在讀出 counter 數值100後,還未來得及將它的二倍值200寫入 counter 時,正巧 MyFun2 函數讀出了 counter 的值100,那麼結果就將變為 200 。

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

現在基本上沒有人用彙編語言來編寫程序了,因為 C、Java等高級語言的效率要比彙編語言快很多。不過,彙編語言的經驗還是很重要的,通過藉助彙編語言,我們可以更好的了解計算機運行機制。

聲明:本文系作者獨立觀點,不代表CSDN立場。

【End】

相關焦點

  • 躲在被窩偷看10W字:作業系統+程式設計師必知硬核知識大全,愛了
    各種語言就好比文字,編程的過程就是用文筆書寫詩篇,假設對於語言的應用已經十分透徹了,那麼編程的難點在哪裡?在使用的思路,還是各種算法,還是以人腦卻用電腦的方式思考問題?首先,大家都是程式設計師,大家都是和計算機打交道的程式設計師,大家都是和計算機軟體硬體打交道的程式設計師,大家都是和CPU打交道的程式設計師,所以,不管你是玩兒硬體的還是做軟體的,你的世界都少不了計算機最核心的——CPU。Part 1 程式設計師必知的硬核知識大全CPU是什麼CPU實際做什麼CPU的內部結構
  • 高級程式設計師的修養,不得不知的硬核知識——CPU
    大家都是程式設計師,大家都是和計算機打交道的程式設計師,大家都是和計算機中軟體硬體打交道的程式設計師,大家都是和CPU打交道的程式設計師,所以,不管你是玩兒硬體的還是做軟體的,你的世界都少不了計算機最核心的 - CPUCPU是什麼CPU 的全稱是 Central Processing Unit,它是你的電腦中最硬核的組件,這種說法一點不為過
  • 程式設計師需要了解的硬核知識之作業系統和應用
    類似的想法可以共用,人們又發現有更多的應用程式可以追加到監控程序中,比如硬體控制程序,程式語言處理器(彙編、編譯、解析)以及各種應用程式等,結果就形成了和現在差異不大的作業系統,也就是說,其實作業系統是多個程序的集合體。我在《程式設計師需要了解的硬核知識之CPU》 這篇文章中提到了彙編語言,這裡簡單再提一下。彙編語言是一種低級語言,也被稱為符號語言。
  • C語言與彙編語言的區別
    如果你問一個程式設計師這樣的問題,他也許會這麼回答你:「C語言可讀性好,代碼便於維護,便於開發;彙編語言編寫的程序不容易看懂,可維護性不好,但是執行效率高。」這樣回答是沒有錯的,但只是一個概括,不夠深入。比方說,彙編語言為什麼執行效率比C語言高呢?C語言的可讀性又好在哪裡呢?彙編語言不同樣可以用註解來提高可讀性嗎?等等這些的問題。
  • 彙編語言知識總結
    彙編語言是門很底層的語言(真的不能再底層了,不然只能用0101的機器語言編碼了╮(╯▽╰)╭),現在很少有人會用彙編語言編程的了(彙編中提供可直接調用的東西太少了,只有一些需要查表的int中斷或者win32彙編的API函數,用彙編寫代碼會很麻煩),那麼是否還有必要學習它呢?答案當然是肯定的。
  • 彙編語言的基本知識
    一、彙編語言的語句格式     由彙編語言編寫的源程序是由許多語句(也可稱為彙編指令)組成的。
  • 內功修煉,彙編語言入門教程
    彙編語言就是低級語言,直接描述/控制 CPU 的運行。如果你想了解 CPU 到底幹了些什麼,以及代碼的運行步驟,就一定要學習彙編語言。彙編語言不容易學習,就連簡明扼要的介紹都很難找到。下面我嘗試寫一篇最好懂的彙編語言教程,解釋 CPU 如何執行代碼。
  • 《彙編語言》——筆記(一)
    基礎知識在講彙編語言之前,先介紹下機器語言。機器語言是機器指令的集合。電子計算機的機器指令是一列二進位數字,計算機將轉變高低電平,來驅動電子器件。計算機是可以執行機器指令,進行運算的機器。這是早期的概念。現在,有一個晶片來完成上面所說的計算機的功能。這個晶片便是CPU(Central Processing Unit,中央處理單元),CPU是一種微處理器。
  • 0基礎手把手入坑CTF逆向(1)——彙編語言學習
    0x01 彙編語言的應用領域彙編語言的優點是能夠直接訪問計算機硬體,所以執行起來要比那些高級語言效率高,而且佔用資源極少,一般用於:系統內核、工業控制、驅動程序、實時系統、核心算法等對運行效率有極高要求的領域,或者說為了突破系統性能的瓶頸而將頻繁使用的子程序或程序段用彙編來編寫。
  • 程式設計師術語:什麼是高級語言/低級語言?解釋語言/編譯語言?
    作為一名合格的程式設計師,你必須知道一些計算機軟體編程方面的術語。這次給大家解釋一下下面兩對常見的術語:高級語言/低級語言、解釋語言/編譯語言。2、低級語言泛指機器語言和彙編語言,其中,機器語言是計算機最原始的語言,由0和1的代碼構成,計算機在工作的時候只認識機器語言,即0和1的代碼;彙編語言,它用人類容易記憶的語言和符號來表示一組0和1的代碼,如AND表示加法助記符。
  • 《彙編語言》
    兩個月前看完王爽著的《彙編語言》經過一段時間的休整,今天再次翻開來又有另一番滋味,趁著最近對底層原理這股熱情,對書本的內容進行一些整理。我的初衷是希望更多的理解程序集在計算機中執行的原理,我中意《深度探索C++對象模型》裡作者說的一句話,他說「我的經驗告訴我,如果一個程式設計師了解底層實現模型,他就能夠寫出效率較高的代碼,自信心也比較高。
  • 不同語言的程式設計師不要再相互鄙視了!
    =======華麗的分界線========= 程式設計師們都有一種自命不凡的氣質,總認為自己是最牛的,其他人都是垃圾,不同的計算機語言之間也存在著鄙視鏈,今天就來盤點一下,這其中的一些事情,看看能否從中得出什麼結論。
  • 彙編語言入門教程
    彙編語言就是低級語言,直接描述/控制 CPU 的運行。如果你想了解 CPU 到底幹了些什麼,以及代碼的運行步驟,就一定要學習彙編語言。彙編語言不容易學習,就連簡明扼要的介紹都很難找到。下面我嘗試寫一篇最好懂的彙編語言教程,解釋 CPU 如何執行代碼。
  • 解密入門教學(二)--彙編語言
    稍微有點兒計算機知識的朋友一定知道,計算機是只識別 0 和 1 的,最初那會兒,要寫程序,就要用 0 和 1 來寫,呵呵,Cool吧!所以曾經有過的對程式設計師的崇拜,可能就源自那個時候吧  後來,人們發現用0和1來寫程序,太不爽了,不但寫起來不上手,而且回過頭來看的話,應該很難再看明白了,總之出於這些原因,就有了彙編語言。
  • 【彙編語言】入門教程
    彙編語言就是低級語言,直接描述/控制 CPU 的運行。如果你想了解 CPU 到底幹了些什麼,以及代碼的運行步驟,就一定要學習彙編語言。彙編語言不容易學習,就連簡明扼要的介紹都很難找到。下面我嘗試寫一篇最好懂的彙編語言教程,解釋 CPU 如何執行代碼。
  • 彙編語言的指令格式和基本語法講解
    與高級語言一樣,並不是所有的操作數都是常數,彙編語言也有自己的變量,變量的值在程序運行期間是可以被改變的;A.定義變量:彙編語言中,變量的定義是通過偽指令來完成的;定義變量的偽指令格式如下:變量名 DB 表達式 ;定義字節變量,又稱單字節變量(1個連續字節),DB-->BYTE變量名 DW 表達式 ;定義字變量,又稱雙字節變量(2個連續字節),DW
  • Keil中C語言與彙編語言混合編程需要注意的幾個地方
    在keil C語言與彙編語言的混合編程中曾經遇到過的一些問題,寫下來留作以後參考。#pragma endasmC語言代碼……}其中紅色為C語言部分,綠色為嵌入的彙編語言部分。知道了原因就好辦一些了,由於本人是菜鳥,單片機水平很有限,目前只想到了如下4種解決方法:(1)避開C語言部分已經使用了的Rn編彙編模塊時,看看C語言部分生成的彙編程序,把那些C語言已經使用到的,且可能對彙編部分構成衝突的Rn避開就好了。比如上面的延時程序中,把R6、R7換成R3、R4,程序就正常了。
  • 彙編語言入門
    雖然現在市面上關於彙編語言的書籍資料無窮多,卻無從下手?
  • 培訓班出身的程式設計師,和科班程式設計師有什麼區別?
    一種認為是否為計算機相關專業對於程式設計師的影響不大,畢竟編碼是一個實踐性很強的工作,用的技術與語言需要在實際工作中不斷積累,學校裡學的都是偏理論性的知識,對於實際工作意義不大。另一種認為科班程式設計師和非科班程式設計師即使看似code水平差不多,在初級階段也看不出太大差距。但是一旦進階到中高級,差距就日漸顯現。
  • 最常被程式設計師們謊稱讀過的計算機書籍
    「這樣的問題會周期性的出現。這樣的問題不斷的被提出、被回答,只是形式不同罷了。相同的幾本書總是會出現在清單的前幾名內,所以,如果想知道人們談論的都是些什麼,你有必要去讀一讀這些書的。大多數程式設計師真正讀過的計算機書籍代碼大全(Code Complete)——兩屆Software Jolt Award震撼大獎得主!