結合本人從清華學習經驗,說說本人的深切體會吧,初識它時,認為匯編語言是一種助記符,一種低級語言,直接面對指令,將二進制指令替換成人類便于記憶的字符串,并冠以特殊的格式。每一條匯編指令對應一條二進制指令。根據(jù)內核架構的不同,不同的指令有不同的長度和格式。
大多數(shù)人一開始都以為匯編語言本身很簡單,常用指令沒幾個,語法規(guī)則也不多,看幾個小時資料似乎就明白了,但其實不然。匯編的背后是體系結構,是程序設計拋開各種高層形態(tài)的最根本,最本質的解釋。本人從業(yè)多年,除了跟我一樣搞過很久安全的同學,其余的沒有一個我認為算是精通匯編。而我是怎么掌握匯編的呢?
1早年用匯編手寫病毒。比如處理指令重定位,是真的用匯編計算指令地址,pushpushcall實現(xiàn)函數(shù)調用。
2長期病毒木馬二進制分析。分析明白各種malware的原理,實現(xiàn)查殺防。個別病毒,需要實現(xiàn)修復。
3漏洞挖掘。fuzzer發(fā)現(xiàn)漏洞,匯編級詳細分析,exploit編寫,武器化利用,一條龍。
4各種逆向分析。好的東西沒有代碼,IDA里看就是了。
5跟debuger做朋友。從來看不上print方式的bug定位。所有問題在調試器里分析明白,絕不靠猜。
6編譯器后端研究。什么指令選擇,指令調度,寄存器分配,全都研究一遍。
7底層開發(fā),操作系統(tǒng),設備驅動,虛擬化都走上一遍。X86很熟?ARM學一遍做對比。歷經這么多,終于敢說學明白匯編了。推薦如下的文檔,很基礎又相對全面的介紹了很多計算機常見問題在匯編層面的解釋。《IntroductiontoComputerOrganizationwithx86-64AssemblyLanguage&GNU/Linux》
學匯編不是說一定要用這它做多牛鼻的事情,問題的關鍵在于,學透了匯編會使你真正理解計算機另外一方面,如上面所說,在工作中你遲早會在某個陰暗的角落遇到匯編.不管你承認不承認,現(xiàn)在的CPU沒有直接跑高級語言的,哪怕是虛擬機也都是類似匯編的指令集.當遇到崩潰分析,性能優(yōu)化甚至編譯器抽風等等的時候,匯編是你最后一根救命稻草.
下面再講講匯編語言的基本內容吧:
目前國內的匯編語言教材大多都是上來先講一大堆CPU、總線、寄存器、標志位……再講匯編語言程序設計。這種字典式的編寫方法對入門是很不利的,因為在不知道這些東西都是用來干什么的情況下,全部記憶往往很難。然而這些概念在編程中還不得不用到,于是又得重新往前翻書,這就陷入了一個循環(huán)。
實際上,匯編語言的學習完全可以和高級語言一樣。只不過因為匯編語言是根據(jù)CPU的工作原理進行操作,所以一切代碼都要從CPU和內存的角度考慮問題。理解了指令在內存層面的執(zhí)行過程,編程就水到渠成了。
先從最簡單的開始:給定兩個數(shù)a和b,讓CPU做一次加法,結果儲存在c中。輸出c。
用C語言編寫這個程序:
inta=3;
intb=4;
intc;
intmain()
{
c=a+b;
printf("%d",c);
return0;
}
(注意:如果寫intc=3+4,一行就可以搞定。但是這里沒這樣做,而是先統(tǒng)一聲明所有的變量,然后再在進行運算的主函數(shù)中執(zhí)行相加操作。后面可以看到,這種編程習慣是符合二進制數(shù)據(jù)在內存中的存放規(guī)律的。)
如果用匯編語言編寫,該怎樣寫呢?
再重復一下題目:給定兩個數(shù)a和b,讓CPU做一次加法,結果儲存在c中。輸出c。
要從原理上寫這個過程,就要解決以下問題:
①數(shù)據(jù)a和b怎么存儲②怎么做加法③怎么儲存結果④怎么輸出結果
下面將分別解決這四個問題。
1.匯編語言程序的結構
首先,我們要知道二進制信號在內存中的存放規(guī)律。眾所周知,計算機能直接處理的只能是二進制信號,這些信號以高低電平的方式存放在內存中,既可以作為指令,也可以作為程序使用的數(shù)據(jù)。一塊內存區(qū)域所存放的二進制信號到底是指令還是數(shù)據(jù),是由相應的命令說了算的。
CPU在讀取指令/數(shù)據(jù)時,每讀取一條指令/數(shù)據(jù),內存位置指針就加1,指向下一條指令/數(shù)據(jù)的內存地址。這樣就產生了一個問題:數(shù)據(jù)和指令在內存中應該分塊,并且要連續(xù)存放。否則如果內存位置指針不知道下一個位置是數(shù)據(jù)還是代碼,將會給內存位置指針的尋址帶來極大的不便。所以,在匯編程序中,要人工將內存分為數(shù)據(jù)段(DataSegment),代碼段(CodeSegment),堆棧段(StackSegment)和附加段(ExtraSegment)。
這樣劃分好以后,我們只需要告訴內存位置指針每個段在內存中的起始地址,內存位置指針就可以順利尋址了。怎樣告訴呢?在CPU中,有一組專門的段寄存器用來存放各個段的起始地址。它們是:DS(用來存放數(shù)據(jù)段的起始地址),CS(用來存放代碼段的起始地址),SS(用來存放堆棧段的起始地址),ES(用來存放附加段的起始地址)。程序員在編程時,需要人工指定這些段寄存器對應于程序中的哪個段。
有了段的概念,我們就可以寫出一個匯編程序的基本框架如下:
DATASEGMENT;定義一個叫DATA的段。DATA既是這個段的名稱,也指代這個段的地址。但這里并未規(guī)定這個段是數(shù)據(jù)段、代碼段還是其他段
……
SEGMENTENDS;表示段結束。ENDS是ENDSEGMENT的縮寫。
STACKSEGMENT;定義一個叫STACK的段,這個段的地址用STACK表示。
……
SEGMENTENDS;段結束
CODESEGMENT;定義一個叫CODE的段,,這個段的地址用CODE表示。
ASSUME:CS:CODE,DS:DATA,SS:SEGMENT;告訴編譯器,將代碼中寫的各段分別對應上各個段寄存器。這句話要放在準備用作代碼段的段開頭
……
SEGMENTENDS;段結束
好了。回到我們的問題:怎樣存儲a和b呢?在數(shù)據(jù)段中聲明變量如下:
DATASEGMENT
ADW03H;定義一個名為A的雙字節(jié)(即1個字)的數(shù)據(jù),DW是DefineWord的縮寫。末尾加H表示十六進制。
這相當于C語言中的intA=3,只不過int表示的范圍遠大于DW而已。
BDW04H;定義一個名為B的雙字節(jié)數(shù)據(jù)。由于B是緊挨著A之后定義的,根據(jù)數(shù)據(jù)段的連續(xù)性,B在數(shù)據(jù)段的偏移地址就是A在數(shù)據(jù)段的偏移地址+A的長度。由于A是雙字節(jié)數(shù)據(jù),所以A的長度是2個字。
SEGMENTENDS
2.CPU的運算方式及運算結果的判定
第二個問題:怎樣做一次加法?
CPU只能處理電平信號。學過模電的都知道,有一種東西叫“加法器”,輸入2個電壓信號,經過運算放大器后,就會得到這兩個信號的和。所以CPU做加法的方式就是:把輸入的兩個二進制信號輸入加法器,得到結果。
問題似乎解決了。但是我們突然發(fā)現(xiàn),這樣的結果幾乎沒有任何意義,因為我們無法知道結果的性質。比如,如果結果超出了能容許的最大位數(shù)(溢出),會怎么樣?CPU沒有任何提示。又或者,我們要比較兩個數(shù)的大小,這就要將兩個數(shù)相減。然而結果是正是負?我們無從知曉。
為了獲知運算結果的性質,在CPU中設置了一個“標志寄存器”,專門用于存放運算結果的各種標志。它們都是用電路實現(xiàn)的。比如:
CF(CarryFlag)就是用來標志無符號數(shù)運算是否產生進位。產生進位時,CF=1,反之CF=0。特別指出,CF標志位的值對有符號數(shù)的運算沒有意義。
OF(OverflowFlag)則是用來標志有符號數(shù)運算是否產生溢出。產生溢出時,OF=1,反之OF=0。同理,OF標志位的值對無符號數(shù)的運算沒有意義。
SF(SignFlag)用來標志結果的正負。當結果是負(SF)時,SF=1。反之SF=0。
回到我們的問題:怎么做一次加法?或者更一般地,怎樣做一次運算?
我們不必關心具體的電路實現(xiàn)細節(jié),只需要執(zhí)行相應的運算指令,運算完成后,不僅會得到結果,各個標志位的值也可能發(fā)生相應的改變,從而有利于我們對結果的判斷。例如:
ADDAX,BX;把AX和BX中的內容相加,結果存放在AX中。若AX,BX為有符號數(shù),當產生溢出時,OF=1.CF的值不確定。當結果為負時,SF=1。
3.內存與寄存器的關系
內存(RAM)是存放各種數(shù)據(jù)、指令的地方。根據(jù)用途的不同,又可以把它分成不同的段。而寄存器(Register)則是CPU內部臨時存放運算結果的地方。與容量較大的內存相比,寄存器的容量極小(每個寄存器只有16位),數(shù)量有限(只有少數(shù)幾個),用途專一(各個寄存器有不同的用途,用來存放不同方面的結果)。例如,前面所述的段寄存器(DS,CS,SS,ES)就是用來存放段的起始地址的。除了段寄存器之外,CPU中還設有通用寄存器(AX,BX,CX,DX……)。它們各自有其專門的用途,在不致于產生沖突的情況下,也可以用來存放數(shù)據(jù)或運算結果。
通用寄存器的用途簡述如下:(通用寄存器容量都是16位的)
AX:①用來存放數(shù)據(jù)或運算結果
②AX的高8位AH用于與DOS操作系統(tǒng)通信。向AH中裝入DOS系統(tǒng)的指令碼并執(zhí)行,可以利用DOS系統(tǒng)完成一些操作,如在屏幕上輸出字符。
DX:①用來存放數(shù)據(jù)或運算結果
②與AH的DOS屏幕輸出指令碼配合使用,存放準備輸出到屏幕上的數(shù)據(jù)
CX:在有循環(huán)的程序中,用來存放循環(huán)次數(shù)。相當于for循環(huán)中的計數(shù)變量i。
BX、SI、DI:①用來存放數(shù)據(jù)或運算結果
②用來存放數(shù)據(jù)段中的數(shù)據(jù)在段中的偏移地址
一般而言,需要運算的數(shù)據(jù)存放在內存中。CPU在程序的指令下,通過指針確定它們的位置,將它們讀入寄存器。進行運算后,再將結果返回到內存預留的結果位置中。
回到我們的問題,在內存的DATASEGMENT中存放有兩個雙字節(jié)數(shù)據(jù)A=3和B=4。要將它們讀入寄存器進行相加運算,再將結果寫入到內存中。為了讀入寄存器,首先需要獲取A和B在內存數(shù)據(jù)段中的偏移地址。確定它們的地址后,按地址將它們讀入寄存器(這里可以任選兩個寄存器),然后執(zhí)行運算指令。運算完成后,將儲存在寄存器中的結果寫入到內存DATASEGMENT中事先預留的位置。使用"MOV目標,源"指令完成源對目標的賦值。代碼如下:
DATASEGMENT
ADW03H
BDW04H
CDW?;?表示聲明時不賦值。相當于intc;
SEGMENTENDS
STACKSEGMENT
SEGMENTENDS
CODESEGMENT
ASSUME:CS:CODE,DS:DATA,SS:SEGMENT
START:;指定程序的入口位置(這個位置當然要在代碼段中啦),并命名為START。
MOVAX,DATA
MOVDS,AX;這兩行的意思是,以通用寄存器AX為中介,將數(shù)據(jù)段DATA的起始地址(用句柄DATA表示)送入數(shù)據(jù)段寄存器DS中。在需要使用數(shù)據(jù)段的程序中,這一步是必須的,否則CPU無法確定數(shù)據(jù)段的位置。注意:ASSUME是一個偽代碼,它只是告訴了編譯器各個段與段寄存器的對應關系,并未存入各段的地址。(由于電路結構的原因,AX不能直接對DS賦值。不能直接寫MOVDS,DATA。)
LEASI,A;注意:與DATASEGMENT中DATA的含義不同,數(shù)據(jù)段中ADW03H中的A僅表示變量名,不表示變量的地址。類似于inta=3,a只是名稱,取地址要用&a。在匯編中,取地址用LEABX/SI/DI,A的格式。注:通常用BX、SI、DI這三個寄存器存數(shù)據(jù)的偏移地址。
MOVAX,[SI];將[內存數(shù)據(jù)段中以SI為偏移地址的內容]送入通用寄存器AX中。這里AX也可以換成BX,CX等。
INCSI;SI的值加1。即SI++。
INCSI;因為A是雙字節(jié)數(shù)據(jù),所以SI要加2才能指向下一個數(shù)據(jù)B的偏移地址。當然,這兩條也可用LEASI,B替代。
MOVBX,[SI];將[內存數(shù)據(jù)段中以SI為偏移地址的內容]——就是B,送入通用寄存器BX中。
ADDBX,AX;BX與AX相加,結果存放在BX中。當然也可以寫ADDAX,BX。但之所以不存放在AX中,是因為一會輸出要用到AH,避免沖突。
LEADI,C
MOV[DI],BX;這兩條是將存放在BX中的結果寫回數(shù)據(jù)段中C所在位置
MOVDX,BX;準備在屏幕上輸出結果。屏幕輸出的是寄存器DX中的內容。
ADDDX,30H;由于屏幕上輸出的是文字,因此必須將數(shù)字+30H轉換為對應的ASCII碼
MOVAH,02H;將控制DOS系統(tǒng)輸出數(shù)值的代碼02H裝入AH。這步可理解為printf()中的"%d"
INT21H;INT=Interrupt,中斷,執(zhí)行DOS命令。執(zhí)行后返回程序。
MOVAH,4CH;4CH是程序結束,返回DOS系統(tǒng)的命令。將此命令裝入AH,等待執(zhí)行。
INT21H;中斷,DOS系統(tǒng)執(zhí)行4CH命令。程序結束。這兩步類似于"return0"
SEGMENTENDS
ENDSTART;在程序的尾部,告訴MASM(宏匯編)編譯器程序的入口位置在標號START處運行結果如下:
輸出結果為7。當然,你可以不用MASM編譯工具,而用其它的匯編IDE集成開發(fā)環(huán)境.
最后,希望通過這篇短文,能夠幫助你能更好的認識,理解,學習好這門語言。