-
1 # muzilovemusic
-
2 # 壹貳壹壹
二、反彙編技巧(摘取自 天書夜讀)
2.1 首先,將流程程式碼與資料計算的程式碼分開,我們使用
F: 用於標識 呼叫函式或者作為函式被呼叫,出棧、入棧
C:用於標識 流程控制程式碼、判斷、比較、迴圈
D: 用於標識 資料處理
2.2 翻譯程式碼
一般被讀入的為輸入,被寫的內部變數為輸出;
取出D的程式碼進行逐句翻譯,任何一段不加任何跳轉、連續的mov 和 加減乘除都可以還原成為一個表示式;
標識為F的程式碼基本不用翻譯,本身為簡單的函式呼叫。
C的程式碼將其翻譯成為if 、for、do、switch。
2.3 表示式合併 與 控制流程結合,整理出來;
-
3 # 武漢朝夕教育科技
我給你說說二者的區別,再去看看演變歷史!
一、組合語言是什麼?
我們知道,CPU 只負責計算,本身不具備智慧。你輸入一條指令(instruction),它就執行一次,然後停下來,等待下一條指令。這些指令都是二進位制的,稱為操作碼(opcode),比如加法指令就是00000011。編譯器的作用,就是將高階語言寫好的程式,翻譯成一條條操作碼。對於人類來說,二進位制程式是不可讀的,根本看不出來機器幹了什麼。為了解決可讀性的問題,以及偶爾的編輯需求,就誕生了組合語言。組合語言是二進位制指令的文字形式,與指令是一一對應的關係。比如,加法指令00000011寫成組合語言就是ADD。只要還原成二進位制,組合語言就可以被 CPU 直接執行,所以它是最底層的低階語言。二、來歷
最早的時候,編寫程式就是手寫二進位制指令,然後透過各種開關輸入計算機,比如要做加法了,就按一下加法開關。後來,發明了紙帶打孔機,透過在紙帶上打孔,將二進位制指令自動輸入計算機。為了解決二進位制指令的可讀性問題,工程師將那些指令寫成了八進位制。二進位制轉八進位制是輕而易舉的,但是八進位制的可讀性也不行。很自然地,最後還是用文字表達,加法指令寫成ADD。記憶體地址也不再直接引用,而是用標籤表示。 這樣的話,就多出一個步驟,要把這些文字指令翻譯成二進位制,這個步驟就稱為assembling,完成這個步驟的程式就叫做 assembler。它處理的文字,自然就叫做 aseembly code。標準化以後,稱為assembly language,縮寫為 asm,中文譯為組合語言。 每一種 CPU的機器指令都是不一樣的,因此對應的組合語言也不一樣。本文介紹的是目前最常見的 x86 組合語言,即 Intel 公司的 CPU使用的那一種。三、暫存器
學習組合語言,首先必須瞭解兩個知識點:暫存器和記憶體模型。
先來看暫存器。CPU 本身只負責運算,不負責儲存資料。資料一般都儲存在記憶體之中,CPU 要用的時候就去記憶體讀寫資料。但是,CPU的運算速度遠高於記憶體的讀寫速度,為了避免被拖慢,CPU 都自帶一級快取和二級快取。基本上,CPU 快取可以看作是讀寫速度較快的記憶體。但是,CPU 快取還是不夠快,另外資料在快取裡面的地址是不固定的,CPU 每次讀寫都要定址也會拖慢速度。因此,除了快取之外,CPU還自帶了暫存器(register),用來儲存最常用的資料。也就是說,那些最頻繁讀寫的資料(比如迴圈變數),都會放在暫存器裡面,CPU優先讀寫暫存器,再由暫存器跟記憶體交換資料。 暫存器不依靠地址區分資料,而依靠名稱。每一個暫存器都有自己的名稱,我們告訴 CPU去具體的哪一個暫存器拿資料,這樣的速度是最快的。有人比喻暫存器是 CPU 的零級快取。 四、暫存器的種類 早期的 x86 CPU只有8個暫存器,而且每個都有不同的用途。現在的暫存器已經有100多個了,都變成通用暫存器,不特別指定用途了,但是早期暫存器的名字都被儲存了下來。
● EAX● EBX● ECX● EDX● EDI● ESI● EBP● ESP
上面這8個暫存器之中,前面七個都是通用的。ESP 暫存器有特定用途,儲存當前 Stack 的地址(詳見下一節)。 我們常常看到 32位CPU、64位 CPU 這樣的名稱,其實指的就是暫存器的大小。32 位 CPU 的暫存器大小就是4個位元組。
五、記憶體模型:Heap
暫存器只能存放很少量的資料,大多數時候,CPU 要指揮暫存器,直接跟記憶體交換資料。所以,除了暫存器,還必須瞭解記憶體怎麼儲存資料。程式執行的時候,作業系統會給它分配一段記憶體,用來儲存程式和執行產生的資料。這段記憶體有起始地址和結束地址,比如從0x1000到0x8000,起始地址是較小的那個地址,結束地址是較大的那個地址。程式執行過程中,對於動態的記憶體佔用請求(比如新建物件,或者使用malloc命令),系統就會從預先分配好的那段記憶體之中,劃出一部分給使用者,具體規則是從起始地址開始劃分(實際上,起始地址會有一段靜態資料,這裡忽略)。舉例來說,使用者要求得到10個位元組記憶體,那麼從起始地址0x1000開始給他分配,一直分配到地址0x100A,如果再要求得到22個位元組,那麼就分配到0x1020。這種因為使用者主動請求而劃分出來的記憶體區域,叫做 Heap(堆)。它由起始地址開始,從低位(地址)向高位(地址)增長。Heap的一個重要特點就是不會自動消失,必須手動釋放,或者由垃圾回收機制來回收。
六、記憶體模型:Stack
除了 Heap 以外,其他的記憶體佔用叫做 Stack(棧)。簡單說,Stack 是由於函式執行而臨時佔用的記憶體區域。請看下面的例子。
1234上面程式碼中,系統開始執行main函式時,會為它在記憶體裡面建立一個幀(frame),所有main的內部變數(比如a和b)都儲存在這個幀裡面。main函式執行結束後,該幀就會被回收,釋放所有的內部變數,不再佔用空間。如果函式內部呼叫了其他函式,會發生什麼情況?
12345上面程式碼中,main函式內部呼叫了add_a_and_b函式。執行到這一行的時候,系統也會為add_a_and_b新建一個幀,用來儲存它的內部變數。也就是說,此時同時存在兩個幀:main和add_a_and_b。一般來說,呼叫棧有多少層,就有多少幀。 等到add_a_and_b執行結束,它的幀就會被回收,系統會回到函式main剛才中斷執行的地方,繼續往下執行。透過這種機制,就實現了函式的層層呼叫,並且每一層都能使用自己的本地變數。所有的幀都存放在 Stack,由於幀是一層層疊加的,所以 Stack 叫做棧。生成新的幀,叫做”入棧”,英文是push;棧的回收叫做”出棧”,英文是 pop。Stack的特點就是,最晚入棧的幀最早出棧(因為最內層的函式呼叫,最先結束執行),這就叫做”後進先出”的資料結構。每一次函式執行結束,就自動釋放一個幀,所有函式執行結束,整個Stack 就都釋放了。
Stack是由記憶體區域的結束地址開始,從高位(地址)向低位(地址)分配。比如,記憶體區域的結束地址是0x8000,第一幀假定是16位元組,那麼下一次分配的地址就會從0x7FF0開始;第二幀假定需要64位元組,那麼地址就會移動到0x7FB0。
七、CPU 指令
7.1 一個例項
瞭解暫存器和記憶體模型以後,就可以來看組合語言到底是什麼了。下面是一個簡單的程式example.c。
123456gcc 將這個程式轉成組合語言。
1上面的命令執行以後,會生成一個文字檔案example.s,裡面就是組合語言,包含了幾十行指令。這麼說吧,一個高階語言的簡單操作,底層可能由幾個,甚至幾十個 CPU 指令構成。CPU 依次執行這些指令,完成這一步操作。example.s經過簡化以後,大概是下面的樣子。
12345678910111213可以看到,原程式的兩個函式add_a_and_b和main,對應兩個標籤_add_a_and_b和_main。每個標籤裡面是該函式所轉成的CPU 執行流程。 每一行就是 CPU 執行的一次操作。它又分成兩部分,就以其中一行為例。
1這一行裡面,push是 CPU 指令,%ebx是該指令要用到的運運算元。一個 CPU 指令可以有零個到多個運運算元。下面我就一行一行講解這個彙編程式,建議讀者最好把這個程式,在另一個視窗複製一份,省得閱讀的時候再把頁面滾動上來。
7.2 push 指令
根據約定,程式從_main標籤開始執行,這時會在 Stack 上為main建立一個幀,並將 Stack 所指向的地址,寫入 ESP暫存器。後面如果有資料要寫入main這個幀,就會寫在 ESP 暫存器所儲存的地址。 然後,開始執行第一行程式碼。
1push指令用於將運運算元放入 Stack,這裡就是將3寫入main這個幀。 雖然看上去很簡單,push指令其實有一個前置操作。它會先取出ESP 暫存器裡面的地址,將其減去4個位元組,然後將新地址寫入 ESP 暫存器。使用減法是因為 Stack從高位向低位發展,4個位元組則是因為3的型別是int,佔用4個位元組。得到新地址以後, 3 就會寫入這個地址開始的四個位元組。
1第二行也是一樣,push指令將2寫入main這個幀,位置緊貼著前面寫入的3。這時,ESP 暫存器會再減去 4個位元組(累計減去8)。
7.3 call 指令
第三行的call指令用來呼叫函式。
1上面的程式碼表示呼叫add_a_and_b函式。這時,程式就會去找_add_a_and_b標籤,併為該函式建立一個新的幀。下面就開始執行_add_a_and_b的程式碼。
1這一行表示將 EBX暫存器裡面的值,寫入_add_a_and_b這個幀。這是因為後面要用到這個暫存器,就先把裡面的值取出來,用完後再寫回去。這時,push指令會再將 ESP 暫存器裡面的地址減去4個位元組(累計減去12)。
7.4 mov 指令
mov指令用於將一個值寫入某個暫存器。
1這一行程式碼表示,先將 ESP 暫存器裡面的地址加上8個位元組,得到一個新的地址,然後按照這個地址在 Stack 取出資料。根據前面的步驟,可以推算出這裡取出的是2,再將2寫入 EAX 暫存器。下一行程式碼也是幹同樣的事情。
1上面的程式碼將 ESP 暫存器的值加12個位元組,再按照這個地址在 Stack 取出資料,這次取出的是3,將其寫入 EBX 暫存器。
7.5 add 指令
add指令用於將兩個運運算元相加,並將結果寫入第一個運運算元。
1上面的程式碼將 EAX 暫存器的值(即2)加上 EBX 暫存器的值(即3),得到結果5,再將這個結果寫入第一個運運算元 EAX 暫存器。
7.6 pop 指令
pop指令用於取出 Stack 最近一個寫入的值(即最低位地址的值),並將這個值寫入運運算元指定的位置。
1上面的程式碼表示,取出 Stack 最近寫入的值(即 EBX 暫存器的原始值),再將這個值寫回 EBX 暫存器(因為加法已經做完了,EBX 暫存器用不到了)。注意,pop指令還會將 ESP 暫存器裡面的地址加4,即回收4個位元組。
回覆列表
程式語言的發展大致可以分為四個階段:機器語言,組合語言,面向過程語言,面嚮物件語言。
機器語言
機器語言就是一串01程式碼,這串01程式碼可以經硬體識別並執行,所有語言最終都會轉換為機器語言。
組合語言
組合語言就是將一串很枯燥無味的機器語言轉化成一個英文單詞。比如說:add 1, 2;
add 就是一個英文單詞,這樣看起來就稍微有一些含義了,即 1 和 2 相加。
如果直接用機器語言編寫的話,這幾乎是無法實現的。因為用機器語言太難記憶了,也沒人能看得懂。所以後來就設計出了第二種語言,即將 0/1 程式碼翻譯為英文單詞,這些英文單詞直接對應著一串 0/1 指令。這個就是組合語言。
從組合語言到c語言
彙編到c語言的最多變化在於,以函式為單位的呼叫關係。那麼這個呼叫關係在彙編下是怎麼體現的呢?
具體來說:c語言的函式關係在彙編下變成了什麼?
棧就是這個變化的關鍵,對的變化就體現了函式的呼叫關係。每一個新函式的呼叫意味這一個新棧的建立。一個函式呼叫的結束有就伴隨這棧的結束。