回覆列表
  • 1 # 校園小喇叭

    結合本人從清華學習經驗,說說本人的深切體會吧,初識它時,認為組合語言是一種助記符,一種低階語言,直接面對指令,將二進位制指令替換成人類便於記憶的字串,並冠以特殊的格式。每一條彙編指令對應一條二進位制指令。根據核心架構的不同,不同的指令有不同的長度和格式。

    大多數人一開始都以為組合語言本身很簡單,常用指令沒幾個,語法規則也不多,看幾個小時資料似乎就明白了,但其實不然。彙編的背後是體系結構,是程式設計拋開各種高層形態的最根本,最本質的解釋。本人從業多年,除了跟我一樣搞過很久安全的同學,其餘的沒有一個我認為算是精通匯編。而我是怎麼掌握彙編的呢 ?

    1 早年用匯編手寫病毒。比如處理指令重定位,是真的用匯編計算指令地址,push push call 實現函式呼叫。

    2 長期病毒木馬二進位制分析。分析明白各種 malware 的原理,實現查殺防。個別病毒,需要實現修復。

    3 漏洞挖掘。fuzzer 發現漏洞,彙編級詳細分析,exploit 編寫,武器化利用,一條龍。

    4 各種逆向分析。好的東西沒有程式碼,IDA 裡看就是了。

    5 跟debuger 做朋友。從來看不上print 方式的bug定位。所有問題在偵錯程式裡分析明白,絕不靠猜。

    6 編譯器後端研究。什麼指令選擇,指令排程,暫存器分配,全都研究一遍。

    7 底層開發,作業系統,裝置驅動,虛擬化都走上一遍。X86很熟? ARM學一遍做對比。歷經這麼多,終於敢說學明白彙編了。推薦如下的文件,很基礎又相對全面的介紹了很多計算機常見問題在彙編層面的解釋。《Introduction to Computer Organization with x86-64 Assembly Language & GNU/Linux》

    學彙編不是說一定要用這它做多牛鼻的事情, 問題的關鍵在於, 學透了彙編會使你真正理解計算機另外一方面, 如上面所說, 在工作中你遲早會在某個陰暗的角落遇到彙編. 不管你承認不承認, 現在的CPU沒有直接跑高階語言的, 哪怕是虛擬機器也都是類似彙編的指令集.當遇到崩潰分析, 效能最佳化甚至編譯器抽風等等的時候, 彙編是你最後一根救命稻草.

    下面再講講組合語言的基本內容吧:

    目前國內的組合語言教材大多都是上來先講一大堆CPU、匯流排、暫存器、標誌位……再講組合語言程式設計。這種字典式的編寫方法對入門是很不利的,因為在不知道這些東西都是用來幹什麼的情況下,全部記憶往往很難。然而這些概念在程式設計中還不得不用到,於是又得重新往前翻書,這就陷入了一個迴圈。

    實際上,組合語言的學習完全可以和高階語言一樣。只不過因為組合語言是根據CPU的工作原理進行操作,所以一切程式碼都要從CPU和記憶體的角度考慮問題。理解了指令在記憶體層面的執行過程,程式設計就水到渠成了。

    先從最簡單的開始:給定兩個數a和b,讓CPU做一次加法,結果儲存在c中。輸出c。

    用C語言編寫這個程式:

    int a=3;

    int b=4;

    int c;

    int main()

    {

    c=a+b;

    printf("%d",c);

    return 0;

    }

    (注意:如果寫int c=3+4,一行就可以搞定。但是這裡沒這樣做,而是先統一宣告所有的變數,然後再在進行運算的主函式中執行相加操作。後面可以看到,這種程式設計習慣是符合二進位制資料在記憶體中的存放規律的。)

    如果用匯編語言編寫,該怎樣寫呢?

    再重複一下題目:給定兩個數a和b,讓CPU做一次加法,結果儲存在c中。輸出c。

    要從原理上寫這個過程,就要解決以下問題:

    下面將分別解決這四個問題。

    1.組合語言程式的結構

    首先,我們要知道二進位制訊號在記憶體中的存放規律。眾所周知,計算機能直接處理的只能是二進位制訊號,這些訊號以高低電平的方式存放在記憶體中,既可以作為指令,也可以作為程式使用的資料。一塊記憶體區域所存放的二進位制訊號到底是指令還是資料,是由相應的命令說了算的。

    CPU在讀取指令/資料時,每讀取一條指令/資料,記憶體位置指標就加1,指向下一條指令/資料的記憶體地址。這樣就產生了一個問題:資料和指令在記憶體中應該分塊,並且要連續存放。否則如果記憶體位置指標不知道下一個位置是資料還是程式碼,將會給記憶體位置指標的定址帶來極大的不便。所以,在彙編程式中,要人工將記憶體分為資料段(Data Segment),程式碼段(Code Segment),堆疊段(Stack Segment)和附加段(Extra Segment)。

    這樣劃分好以後,我們只需要告訴記憶體位置指標每個段在記憶體中的起始地址,記憶體位置指標就可以順利定址了。怎樣告訴呢?在CPU中,有一組專門的段暫存器用來存放各個段的起始地址。它們是:DS(用來存放資料段的起始地址),CS(用來存放程式碼段的起始地址),SS(用來存放堆疊段的起始地址),ES(用來存放附加段的起始地址)。程式設計師在程式設計時,需要人工指定這些段暫存器對應於程式中的哪個段。

    有了段的概念,我們就可以寫出一個彙編程式的基本框架如下:

    DATA SEGMENT ;定義一個叫DATA的段。DATA既是這個段的名稱,也指代這個段的地址。但這裡並未規定這個段是資料段、程式碼段還是其他段

    ……

    SEGMENT ENDS ;表示段結束。ENDS是END SEGMENT的縮寫。

    STACK SEGMENT ;定義一個叫STACK的段,這個段的地址用STACK表示。

    ……

    SEGMENT ENDS ;段結束

    CODE SEGMENT ;定義一個叫CODE的段,,這個段的地址用CODE表示。

    ASSUME:CS:CODE,DS:DATA,SS:SEGMENT ;告訴編譯器,將程式碼中寫的各段分別對應上各個段暫存器。這句話要放在準備用作程式碼段的段開頭

    ……

    SEGMENT ENDS ;段結束

    好了。回到我們的問題:怎樣儲存a和b呢?在資料段中宣告變數如下:

    DATA SEGMENT

    A DW 03H ;定義一個名為A的雙位元組(即1個字)的資料,DW是Define Word 的縮寫。末尾加H表示十六進位制。

    這相當於C語言中的int A=3,只不過int表示的範圍遠大於DW而 已。

    B DW 04H ;定義一個名為B的雙位元組資料。由於B是緊挨著A之後定義的,根據 資料段的連續性,B在資料段的偏移地址就是A在資料段的偏移地 址 + A的長度。由於 A是雙位元組資料,所以A的長度是2個字。

    SEGMENT ENDS

    2.CPU的運算方式及運算結果的判定

    第二個問題:怎樣做一次加法?

    CPU只能處理電平訊號。學過模電的都知道,有一種東西叫“加法器”,輸入2個電壓訊號,經過運算放大器後,就會得到這兩個訊號的和。所以CPU做加法的方式就是:把輸入的兩個二進位制訊號輸入加法器,得到結果。

    問題似乎解決了。但是我們突然發現,這樣的結果幾乎沒有任何意義,因為我們無法知道結果的性質。比如,如果結果超出了能容許的最大位數(溢位),會怎麼樣?CPU沒有任何提示。又或者,我們要比較兩個數的大小,這就要將兩個數相減。然而結果是正是負?我們無從知曉。

    為了獲知運算結果的性質,在CPU中設定了一個“標誌暫存器”,專門用於存放運算結果的各種標誌。它們都是用電路實現的。比如:

    CF(Carry Flag)就是用來標誌無符號數運算是否產生進位。產生進位時,CF=1,反之CF=0。特別指出,CF標誌位的值對有符號數的運算沒有意義。

    OF(Overflow Flag)則是用來標誌有符號數運算是否產生溢位。產生溢位時,OF=1,反之OF=0。同理,OF標誌位的值對無符號數的運算沒有意義。

    SF(Sign Flag)用來標誌結果的正負。當結果是負(SF)時,SF=1。反之SF=0。

    回到我們的問題:怎麼做一次加法?或者更一般地,怎樣做一次運算?

    我們不必關心具體的電路實現細節,只需要執行相應的運算指令,運算完成後,不僅會得到結果,各個標誌位的值也可能發生相應的改變,從而有利於我們對結果的判斷。例如:

    ADD AX,BX ;把AX和BX中的內容相加,結果存放在AX中。若AX,BX為有符號數,當 產生溢位時,OF=1.CF的值不確定。當結果為負時,SF=1。

    3.記憶體與暫存器的關係

    記憶體(RAM)是存放各種資料、指令的地方。根據用途的不同,又可以把它分成不同的段。而暫存器(Register)則是CPU內部臨時存放運算結果的地方。與容量較大的記憶體相比,暫存器的容量極小(每個暫存器只有16位),數量有限(只有少數幾個),用途專一(各個暫存器有不同的用途,用來存放不同方面的結果)。例如,前面所述的段暫存器(DS,CS,SS,ES)就是用來存放段的起始地址的。除了段暫存器之外,CPU中還設有通用暫存器(AX,BX,CX,DX……)。它們各自有其專門的用途,在不致於產生衝突的情況下,也可以用來存放資料或運算結果。

    通用暫存器的用途簡述如下:(通用暫存器容量都是16位的)

    AX:①用來存放資料或運算結果

    ②AX的高8位AH用於與DOS作業系統通訊。向AH中裝入DOS系統的指令碼並執行,可以利用DOS系統完成一些操作,如在螢幕上輸出字元。

    DX:①用來存放資料或運算結果

    ②與AH的DOS螢幕輸出指令碼配合使用,存放準備輸出到螢幕上的資料

    CX:在有迴圈的程式中,用來存放迴圈次數。相當於for迴圈中的計數變數i。

    BX、SI、DI:①用來存放資料或運算結果

    ②用來存放資料段中的資料在段中的偏移地址

    一般而言,需要運算的資料存放在記憶體中。CPU在程式的指令下,透過指標確定它們的位置,將它們讀入暫存器。進行運算後,再將結果返回到記憶體預留的結果位置中。

    回到我們的問題,在記憶體的DATA SEGMENT中存放有兩個雙位元組資料A=3和B=4。要將它們讀入暫存器進行相加運算,再將結果寫入到記憶體中。為了讀入暫存器,首先需要獲取A和B在記憶體資料段中的偏移地址。確定它們的地址後,按地址將它們讀入暫存器(這裡可以任選兩個暫存器),然後執行運算指令。運算完成後,將儲存在暫存器中的結果寫入到記憶體DATA SEGMENT中事先預留的位置。使用"MOV 目標,源"指令完成源對目標的賦值。程式碼如下:

    DATA SEGMENT

    A DW 03H

    B DW 04H

    C DW ? ;?表示宣告時不賦值。相當於int c;

    SEGMENT ENDS

    STACK SEGMENT

    SEGMENT ENDS

    CODE SEGMENT

    ASSUME:CS:CODE,DS:DATA,SS:SEGMENT

    START: ;指定程式的入口位置(這個位置當然要在程式碼段中啦),並命名為START。

    MOV AX,DATA

    MOV DS,AX ;這兩行的意思是,以通用暫存器AX為中介,將資料段DATA的起始地址(用控制代碼DATA表示)送入資料段暫存器DS中。在需要使用資料段的程式中,這一步是必須的,否則CPU無法確定資料段的位置。注意:ASSUME是一個虛擬碼,它只是告訴了編譯器各個段與段暫存器的對應關係,並未存入各段的地址。(由於電路結構的原因,AX不能直接對DS賦值。不能直接寫MOV DS,DATA。)

    LEA SI,A ; 注意:與DATA SEGMENT中DATA的含義不同,資料段中A DW 03H中的A僅表示變數名,不表示變數的地址。類似於int a=3,a只是名稱,取地址要用&a。在彙編中,取地址用LEA BX/SI/DI,A的格式。注:通常用BX、SI、DI這三個暫存器存資料的偏移地址。

    MOV AX,[SI] ;將 [記憶體資料段中以SI為偏移地址的內容] 送入通用暫存器 AX中。這裡AX也可以換成BX,CX等。

    INC SI ;SI的值加1。即SI++。

    INC SI ;因為A是雙位元組資料,所以SI要加2才能指向下一個資料B的偏移地址。當 然,這兩條也可用 LEA SI,B替代。

    MOV BX,[SI] ;將 [記憶體資料段中以SI為偏移地址的內容] ——就是B,送入通用暫存器 BX中。

    ADD BX,AX ;BX與AX相加,結果存放在BX中。當然也可以寫ADD AX,BX。但之所以不 存放在AX中,是因為一會輸出要用到AH,避免衝突。

    LEA DI,C

    MOV [DI],BX ;這兩條是將存放在BX中的結果寫回資料段中C所在位置

    MOV DX,BX ;準備在螢幕上輸出結果。螢幕輸出的是暫存器DX中的內容。

    ADD DX,30H ;由於螢幕上輸出的是文字,因此必須將數字+30H轉換為對應的ASCII碼

    MOV AH,02H ;將控制DOS系統輸出數值的程式碼02H裝入AH。這步可理解為printf()中的"%d"

    INT 21H ;INT=Interrupt,中斷,執行DOS命令。執行後返回程式。

    MOV AH,4CH ;4CH是程式結束,返回DOS系統的命令。將此命令裝入AH,等待執行。

    INT 21H ;中斷,DOS系統執行4CH命令。程式結束。這兩步類似於 "return 0"

    SEGMENT ENDS

    END START ;在程式的尾部,告訴MASM(宏彙編)編譯器程式的入口位置在標號START處執行結果如下:

    輸出結果為7。當然,你可以不用MASM編譯工具,而用其它的彙編IDE整合開發環境.

    最後,希望透過這篇短文,能夠幫助你能更好的認識,理解,學習好這門語言。

  • 中秋節和大豐收的關聯?
  • 鄰居地裡的樹太高了,直接影響我家糧食產量怎麼辦?