郁金香汇编代码注入怎么写看雪_汇编语言入门五:流程控制(一)
回顧
前面說到過這樣幾個內容:
- 幾條簡單的匯編指令
- 寄存器
- 內存訪問
對應到C語言的學習過程中,無非就是這樣幾個內容:
- 超級簡單的運算
- 變量
好了,到這里,我們繼續接下來的話題,程序中的流程控制。
文中涉及一些匯編代碼,建議讀者自行編程,通過動手實踐來加深對程序的理解。
順序執行
首先,最簡單也最好理解的程序流程,便是從前往后的順序執行。這個非常簡單,還是舉出前面的例子:
現在有1000個計算題:
99+10= 32-20= 14+21= 47-9= 87+3= 86-8= ...需要你一個個地從前往后計算,計算結果需要寫在專門的答題卡上。當你每做完一個題,你需要繼續做下一個題(這不是廢話么)。
那么問題來了,我每次計算完一個題目,回頭尋找下一個題目的時候,到底哪一個題是我接下來要計算的呢?
你可能會說:瞄一眼答題卡就知道了呀。這就尷尬了,計算機其實是比較傻的,它可沒有“瞄一眼”這樣的功能。
那這樣的話,如果是自己做1000個題目,為了保證做題的時候每一個動作都不是多余的,有一個比較好的辦法,就是強行在腦子里記住剛剛那個題目的位置。一會兒回頭的時候,就立馬知道該繼續做哪個題了。
好了,那對于計算機來說呢?前面說到,你做計算題的時候臨時留在腦子里的東西,就對應CPU里寄存器的數據。寄存器就充當了臨時記住一些東西的功能。那么,在這里,CPU也是用的這個套路,在內部有一個寄存器,專門用來記錄程序執行到哪里了。
CPU中的順序執行過程
前面已經有了一個初步的結論,CPU里有一個寄存器專門存放“程序執行到哪里了”這樣一個信息,而且這么做也是說得過去的,那就是:必須有一個東西記錄當前程序執行到的位置,否則CPU執行完一條指令之后,就不知道接下來該干什么了。
在x86體系結構的CPU里面,這個執行位置的信息,是保存在叫做eip的寄存器中的。不過很遺憾,這個寄存器比較特殊,無法通過mov指令進行修改,也就是說,這么寫mov eip, 0x233是行不通的。
(不要問我為什么,我也不知道,這都是人做出來的東西,支不支持就看人家的心情。反正Intel的CPU做出來就是這個樣子的,你可以認為,Intel在做CPU的時候壓根就沒支持這個功能,他們覺得做了也沒什么卵用。雖然你可能覺得有這個功能不是更好么,但是實際上,有時候刻意對功能施加一些限制,可以減少程序員寫代碼誤操作的機會,eip這個東西,很關鍵)
好了,介紹完eip的作用之后,再說一下細節的東西。在執行一條指令的時候,eip此時代表的是下一條指令的位置,eip里保存的就是下一條指令在內存中的地址。這樣,CPU在執行完成一條指令之后,就直接根據eip的值,取出下一條指令,同時還要修改eip,往eip上加一個指令的長度,讓它繼續指向后一條指令。
有了這樣一個過程,CPU就能自動地去從前往后執行每一條指令了。而且,上述過程是在CPU中自動發生的,你寫代碼的時候根本不需要關心這個東西,只需要按照自己的思路從前往后寫就是了。
好了,這一段更多的是講故事,明白CPU里面有個eip寄存器,它的功能很專一,就是用來表示程序現在執行到哪兒了。說得精確一點,eip一直都指向下一個要執行的指令,這一點是由CPU自己保證的。總之,只要CPU沒壞,它就能給你保證eip的精確。
事情沒那么簡單
前面說了eip能記住程序執行的位置,那么CPU就能順溜溜地一路走下去了。然而,世界并不是這么美好。因為:
if( a < 1 ){// some code ... } else if( a >= 10 ) {// yi xie dai ma ... }實際上有時候我們需要程序有一定的流程控制能力。就是有時候它不是老老實實按照順序來執行的,中間可能會跳過一些代碼,比如上述C代碼中的a的值為100的時候。
那么這時候怎么搞呢?照這樣說,程序就得具備“修改eip”的能力了,可是前面說了,mov指令不頂用啊?
放心,那幫做CPU的人沒那么傻,他們早就想好了怎么辦了。他們在設計CPU的時候是這么考慮的:
- 更改eip和更改別的寄存器產生的效果不一樣,所以應該特殊對待
- 要更改有著特殊用途的eip,就用特殊的指令來完成,雖然都是在更改寄存器,但是代碼寫出來,表達給人的意思就不一樣了
首先,我們需要更改eip來實現程序突然跳轉的效果,進而靈活地對程序的流程進行控制。這里不得不祭出一套新的指令了:跳轉指令。
不說了,鋪墊也都差不多了,還是直接上代碼,直觀體驗一把,然后再扯別的。先來一份正常的代碼:
global mainmain:mov eax, 1mov ebx, 2add eax, ebxret如果前面好好學習的話,對這個一定不陌生。還是大致解釋一下吧:
eax = 1 ebx = 2 eax = eax + ebx所以,按照正常邏輯理解,最后eax為3,整個程序退出時會返回3。
好的,到這里,我們來引入新的指令,通過前后對比的變化,來理解新的指令的作用:
global mainmain:mov eax, 1mov ebx, 2jmp gun_kaiadd eax, ebx gun_kai:ret這段代碼相比前面的代碼,多了兩行:
...jmp gun_kai ... gun_kai: ...好了,這段代碼其實沒什么功能,存粹是為了演示,運行這個代碼,得到的返回結果為1。
好了,最后的結果告訴我們,中間的那一條指令:
add eax, ebx根本就沒有執行,所以最后eax的值就是1,整個程序的返回值就是1。
好了,這里也沒什么需要解釋的,動手做,稍微對比分析一下就能夠知道結論了。程序中出現了一條新的指令jmp,這是一個跳轉指令,不解釋。這里直接用一個等價的C語言來說明上述功能吧:
int main() {int a = 1;int b = 2;goto gun_kai;a = a + b;gun_kai:return a; }實際上,C語言中的goto語句,在編譯后就是一條jmp指令。它的功能就是直接跳轉到某個地方,你可以往前跳轉也可以往后跳轉,跳轉的目標就是jmp后面的標簽,這個標簽在經過編譯之后,會被處理成一個地址,實際上就是在往某個地址處跳轉,而jmp在CPU內部發生的作用就是修改eip,讓它突然變成另外一個值,然后CPU就乖乖地跳轉過去執行別的地方的代碼了。
這玩意有啥用?
不對啊,這跳轉指令能用來干啥?反正代碼都直接被跳過去了,那我編程的時候干脆直接不寫那幾條指令不就得了么?使用跳轉指令是不是有種脫了褲子放屁的感覺?
并不是,繼續。
if在匯編里的樣子
前面說到了跳轉,但是仿佛沒卵用的樣子。接下來我們說這樣一個C語言程序:
int main() {int a = 50;if( a > 10 ) {a = a - 10;}return a; }這個程序,最后的返回值是40,這沒什么好解釋的。那對應的匯編程序呢?其實也非常簡單,先直接給出代碼再分析:
global mainmain:mov eax, 50cmp eax, 10 ; 對eax和10進行比較jle xiaoyu_dengyu_shi ; 小于或等于的時候跳轉sub eax, 10 xiaoyu_dengyu_shi:ret這段匯編代碼很關鍵的地方就在于這兩條陌生的指令:
cmp eax, 10 ; 對eax和10進行比較jle xiaoyu_dengyu_shi ; 小于或等于的時候跳轉先細細解釋一下:
- 第一條,cmp指令,專門用來對兩個數進行比較
- 第二條,條件跳轉指令,當前面的比較結果為“小于或等于”的時候就跳轉,否則不跳轉
到這里,至少上面這個程序,每一條指令都是很清楚的。只是你關心的是下面的問題:
- 我會寫a > 10的情況了,那么a < 10怎么辦呢?a == 10怎么辦呢?a <= 10怎么辦呢?a >= 10怎么辦呢?
涼拌炒雞蛋。
別急,先說套路。上面的C語言代碼是這樣的:
if ( a > 10 ) {a = a - 10; }這是表示:“比較a和10,a大于10的時候,進入if塊中執行減法”
而匯編代碼:
cmp eax, 10jle xiaoyu_dengyu_shisub eax, 10 xiaoyu_dengyu_shi:表示的是:“比較eax和10,eax小于等于10的時候,跳過中間的減法”
注意這里最關鍵的兩個表述:
- C語言中:a大于10的時候,進入if塊中執行減法
- 匯編語言中:eax小于等于10的時候,跳過中間的減法
C語言和匯編語言中的條件判斷,其組織的思路是剛好相反的。這就在編程的時候帶來一些思考上的困難,不過這都還是小事情,實在困難你可以先畫出流程圖,然后對流程圖進行改造,就可以了。
有了上面if的套路,接下來趁熱打鐵,再做一個練習:
int main() {int x = 1;if ( x > 100 ) {x = x - 20;}x = x + 1;return x; }好了,這里按照前面的思路,在匯編語言里面,關鍵就是下面幾點:
- 對x對應的東西與100進行比較
- 何時跳過if塊中的減法
- x = x + 1是無論如何都會執行的
按照前面的代碼,稍作類比,很容易地就能寫出下面的代碼來:
global mainmain:mov eax, 1cmp eax, 100jle xiao_deng_yu_100sub eax, 20xiao_deng_yu_100:add eax, 1ret把程序結合著前面的C代碼進行對比,參考前面說的if在匯編里組織的套路,這個程序就很容易理解了。你還可以嘗試把
mov eax, 1更改為:
mov eax, 110試試程序的執行邏輯是不是發生了變化?
再來套路
前面說到了if在匯編中的組織方式,接下來,問題就更加復雜了:
- 我會寫a > 10的情況了,那么a < 10怎么辦呢?a == 10怎么辦呢?a <= 10怎么辦呢?a >= 10怎么辦呢?
涼拌炒雞蛋。
前面實際上只提到了兩個流程控制相關的指令:
- jmp
- jle
以及一個比較指令:
- cmp
專門用來對兩個操作數進行比較。
先從這里入手,總結套路。首先,這兩條跳轉指令是人想出來的,所以,你很容易想到,僅僅是這兩條跳轉指令好像還不夠。其實,人家做CPU的人早也就想到了。所以,還有這樣一些跳轉指令:
ja 大于時跳轉 jae 大于等于 jb 小于 jbe 小于等于 je 相等 jna 不大于 jnae 不大于或者等于 jnb 不小于 jnbe 不小于或等于 jne 不等于 jg 大于(有符號) jge 大于等于(有符號) jl 小于(有符號) jle 小于等于(有符號) jng 不大于(有符號) jnge 不大于等于(有符號) jnl 不小于 jnle 不小于等于 jns 無符號 jnz 非零 js 如果帶符號 jz 如果為零好了,這就是一些條件跳轉指令,將它們配合著前面的cmp指令一起使用,就能夠達到if語句的效果。
What?這該不會都得記住吧?其實不用,這里面是有套路的:
- 首先,跳轉指令的前面都是字母j
- 關鍵是j后面的的字母
比如j后面是ne,對應的是jne跳轉指令,n和e分別對應not和equal,也就是“不相等”,也就是說在比較指令的結果為“不想等”的時候,就會跳轉。
- a: above
- e: equal
- b: below
- n: not
- g: greater
- l: lower
- s: signed
- z: zero
好了,這里列出來了j后面的字母所對應的含義。根據這些字母的組合,和上述大概的規則,你就能清楚怎么寫出這些跳轉指令了。當然,這里有“有符號”和“無符號”之分,后面有機會再扯,讀者也可以自行了解。
那么,接下來,就可以寫出這樣的程序所對應的匯編代碼了:
int main() {int x = 10;if ( x > 100 ) {x = x - 20;}if( x <= 10 ) {x = x + 10;}x = x + 1;return 0; }這個程序沒什么卵用,存粹是為了演示。按照前面的套路,其實寫出匯編代碼也就不難了:
global mainmain:mov eax, 10cmp eax, 100jle lower_or_equal_100sub eax, 20lower_or_equal_100:cmp eax, 10jg greater_10add eax, 10greater_10:add eax, 1ret至于更多可能的寫法,那就可以慢慢玩了。
if都有了,那else if和else怎么辦呢?
這里就不再贅述了,理一下思路:
- 首先根據你的需要,畫出整個程序的流程圖
- 按照流程圖中的跳轉關系,通過匯編表達出來
也就是說,在匯編里面,實際上沒有所謂的if或else的說法,只是前面為方便說明,使用了C語言作類比,實際上匯編還可以寫得比C語言的判斷更加靈活。
事實上,C語言里面的幾種常見的if組織結構,都有對應的匯編語言里的套路。說白了,都是套路。
那你怎么才能知道這些套路呢?很簡單,用C語言寫一個簡單的程序,編譯后按之前文章所說的內容,使用gdb去反匯編然后就能知道這里面的具體做法了。
下面來嘗試下一下:
int main() {register int grade = 80;register int level;if ( grade >= 85 ){level = 1;} else if ( grade >= 70 ) {level = 2;} else if ( grade >= 60 ) {level = 3;} else {level = 4;}return level; }(程序中有一個register關鍵字,是用來限定這個變量在編譯后只能用寄存器來進行表示,方便我們進行分析。讀者可以根據需要,去掉register關鍵字后比較一下反匯編代碼有何不同。)
這是一個很經典的多分支程序結構。先編譯運行,程序返回值為2。
$ gcc -m32 grade.c -o grade $ ./grade ; echo $? 2好了,接下來,用gdb進行反匯編:
$ gdb ./grade (gdb) set disassembly-flavor intel (gdb) disas main得到的反匯編代碼如下:
Dump of assembler code for function main:0x080483ed < +0>: push ebp0x080483ee < +1>: mov ebp,esp0x080483f0 < +3>: push ebx0x080483f1 < +4>: mov ebx,0x500x080483f6 < +9>: cmp ebx,0x540x080483f9 <+12>: jle 0x8048402 <main+21>0x080483fb <+14>: mov ebx,0x10x08048400 <+19>: jmp 0x804841f <main+50>0x08048402 <+21>: cmp ebx,0x450x08048405 <+24>: jle 0x804840e <main+33>0x08048407 <+26>: mov ebx,0x20x0804840c <+31>: jmp 0x804841f <main+50>0x0804840e <+33>: cmp ebx,0x3b0x08048411 <+36>: jle 0x804841a <main+45>0x08048413 <+38>: mov ebx,0x30x08048418 <+43>: jmp 0x804841f <main+50>0x0804841a <+45>: mov ebx,0x40x0804841f <+50>: mov eax,ebx0x08048421 <+52>: pop ebx0x08048422 <+53>: pop ebp0x08048423 <+54>: ret篇幅有限,這里就留給讀者練習分析了。其中有幾個需要注意的地方:
- 部分無關指令可以直接忽略掉,如:push、pop等
- 跳轉指令后的<main+21>,就對應的是反匯編指令前是<+21>的指令
根據上述反匯編代碼,分析出程序的流程圖,與C語言程序的代碼進行比較。仔細分析,你應該就發現jmp指令有什么用了吧。
狀態寄存器
到這里,有一個問題出現了,在匯編語言里面實現“先比較,后跳轉”的功能時,后面的跳轉指令是怎么利用前面的比較結果的呢?
這就涉及到另一個寄存器了。在此之前,先想一下,如果自己在腦子里思考同樣的邏輯,是怎么樣的?
- 先比較兩個數
- 記住比較結果
- 根據比較結果作出決定
好了,這里又來了一個“記住”的動作了。CPU里面也有一個專用的寄存器,用來專門“記住”這個cmp指令的比較結果的,而且,不僅是cmp指令,它還會自動記住其它一些指令的結果。這個寄存器就是:
eflags名為“標志寄存器”,它的作用就是記住一些特殊的CPU狀態,比如前一次運算的結果是正還是負、計算過程有沒有發生進位、計算結果是不是零等信息,而后續的跳轉指令,就是根據eflags寄存器中的狀態,來決定是否要進行跳轉的。
cmp指令實際上是在對兩個操作數進行減法,減法后的一些狀態最終就會反映到eflags寄存器中。
總結
這回著重說到了匯編語言中與流程控制相關的內容。其中主要包括:
- eip寄存器指示著CPU接下來要執行哪里的代碼
- 一系列跳轉指令,跳轉指令根本上就是修改了eip
- 比較指令,比較指令實際上是在做減法,然后把結果的一些狀態放到eflags寄存器中
- eflags寄存器的作用
- 條件跳轉指令也就是根據eflags中的信息來決定是否跳轉
當然,這里講述的僅僅是一部分相關的指令,帶領讀者對這部分內容有一個直觀的認識。實際上匯編語言中與流程相關的指令不止這些,讀者可自行查閱相關的資料:
- x86標志寄存器
- x86影響標志寄存器的指令
- x86跳轉指令
本文內容相比之前要更多一些,若想要完全理解,也需要仔細閱讀,多思考、多嘗試,多驗證,也可以參考更多其它方面的資料。
文中若有疏漏之處,歡迎指正。
總結
以上是生活随笔為你收集整理的郁金香汇编代码注入怎么写看雪_汇编语言入门五:流程控制(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python matplotlib 和P
- 下一篇: 吴恩达 coursera ML 第九课总