官术网_书友最值得收藏!

第1章 逆向工程簡介

1.1 匯編簡介

如果你正在翻閱本書,那么你可能已經聽說過Arm匯編語言,并且知道理解它是分析在Arm上運行的二進制文件的關鍵。但這種語言是什么,為什么會有這種語言?畢竟,程序員通常使用C/C++等高級語言來編寫代碼,幾乎沒有人會直接用匯編語言來編程。因為對于程序員來說,使用高級語言編程更加方便。

不幸的是,這些高級語言對于處理器來說過于復雜,無法直接解析。程序員需要將這些高級程序編譯成處理器能夠運行的二進制機器碼。

這種機器碼并不完全等同于匯編語言。如果你直接在文本編輯器中查看它,會發現它看起來非常難理解。處理器也不會直接運行匯編語言,處理器只運行機器碼,那么,為什么匯編語言在逆向工程中如此重要呢?

為了理解匯編語言的用途,讓我們快速回顧一下計算機發展歷史,了解一下計算機是如何達到現在的狀態的,以及所有事物是如何互相聯系的。

1.1.1 位和字節

在計算機發展的早期,人們決定創建計算機并讓它們執行簡單的任務。計算機不會說我們人類的語言——畢竟,它們只是電子設備——因此我們需要一種電子通信方式。在底層,計算機是通過電信號運作的,這些信號是通過在兩個電壓水平之間進行切換(開和關)來形成的。

第一個問題是,我們需要一種方法來描述這些“開”和“關”,才能將它們用于通信、存儲和簡單的系統狀態。既然有兩種狀態,那么使用二進制系統對這些值進行編碼是非常自然的。每個二進制位可以是0或1。盡管每個位(bit)只能存儲盡可能小的信息量,但將多個位串聯在一起可以表示非常大的數字。例如,數字30 284 334 537只需要35位就可以表示出來,如下所示:

這個系統已經允許對比較大的數字進行編碼,但現在我們面臨一個新的問題:在內存(或磁帶)中,一個數字在哪里結束,下一個數字從哪里開始?對于現代讀者來說,這可能是一個奇怪的問題,但在計算機剛剛被設計出來的時候,這是一個嚴重的問題。最簡單的解決方案是創建固定大小的位分組。計算機科學家從不想錯過一個好的命名雙關語,他們將這組二進制位稱為字節。

那么,一個字節應該有多少位?對于現代人來說,這個問題的答案似乎是顯而易見的,因為我們都知道一個字節是8位。但并非一開始就是這樣的。

最初,不同的系統對其字節中的位數做出了不同的選擇。我們今天知道的8位字節的前身是6位二十進制交換碼(Binary Coded Decimal Interchange Code,BCDIC)格式,用于表示早期IBM計算機(如1959年的IBM 1620)的字母數字信息。在此之前,字節的長度通常為4位,更早的時候,一個字節代表大于1的任意位數。直到IBM于20世紀60年代在其大型計算機產品線System/360中引入8位擴充的二十進制交換碼(Extended Binary Coded Decimal Interchange Code,EBCDIC),并具有8位字節的可尋址內存,字節才開始圍繞8位進行標準化。這隨后促使其他廣泛使用的計算機系統(包括Intel 8080和Motorola 6800)采用了8位存儲大小。

以下這段內容摘自1962年出版的Planning a Computer System[1]一書,列出了采用8位字節的三個主要原因:

1)其256個字符的總容量被認為足以滿足絕大多數應用程序的需求。

2)在這種容量的限制下,一個字符由一個字節來表示,因此任何特定記錄的長度不取決于該記錄中字符的重合度。

3)8位字節在存儲空間上相當經濟。

一個8位字節只可以存儲從00000000到11111111的256個不同的值中的一個。當然,這些值的解釋取決于使用它的軟件。例如,我們可以在這些字節中存儲正數,以表示從0到255(含)的正數。我們還可以使用二進制補碼方案來表示從-128到127(含)的有符號數字。

1.1.2 字符編碼

當然,計算機并不僅僅使用字節來編碼和處理整數。它們還經常存儲和處理人類可讀的字母和數字——稱為字符。

早期的字符編碼(如ASCII)已經確定使用每個字節的7位,但這只能提供有限的128個可能的字符。這允許對英語字母和數字以及一些符號字符和控制字符進行編碼,但無法表示許多其他語言中使用的字母。EBCDIC標準使用8位字節,選擇了一個完全不同的字符集,其代碼頁可以“交換”到不同的語言。但最終這種字符集過于煩瑣和不靈活。

隨著時間的推移,人們逐漸認識到需要一個真正通用的字符集來支持世界上所有現存的語言和特殊符號。這最終促成了1987年Unicode項目的建立。存在不同的Unicode編碼,但在Web上使用的主要編碼方案是UTF-8。ASCII字符集中的字符都被包含在了UTF-8中,而“擴展字符”可以分布在多個連續的字節中。

由于字符現在被編碼為字節,因此我們可以用兩個十六進制數字來表示字符。例如,字符A、R和M通常用圖1.1所示的八位數(octet)進行編碼。

圖1.1 字符A、R和M以及它們的十六進制值

每個十六進制數字都可以用從0000到1111的4位模式進行編碼,如圖1.2所示。

圖1.2 十六進制的ASCII值及其等效的8位二進制值

由于編碼一個ASCII字符需要兩個十六進制的數字,8位似乎是存儲世界上大多數書面語言的文本的理想位數,對于無法僅用8位表示的字符,可以使用多個8位來存儲。

使用這種模式,我們可以更容易地解釋一長串位的含義。以下位模式編碼了單詞Arm:

1.1.3 機器碼和匯編

與之前的機械計算器相比,計算機的一個獨特的強大之處在于,它們也可以將邏輯編碼為數據。這種代碼也可以存儲在內存或磁盤上,并根據需要進行處理或更改。例如,軟件更新可以完全改變計算機的操作系統,而不需要購買一臺新機器。

我們已經看到了數字和字符是如何編碼的,但是邏輯如何編碼呢?這就是處理器架構及其指令集發揮作用的地方。

如果要從頭開始創建自己的計算機處理器,那么我們可以設計自己的指令編碼,將二進制模式映射為處理器可以解釋和響應的機器碼,這實際上是創建我們自己的“機器語言”。由于機器碼是為了“指示”電路執行“操作”,因此也被稱為指令碼,或者更常見的操作碼(opcode)。

在實踐中,大多數人使用現有的計算機處理器,因此使用處理器制造商定義的指令編碼。在Arm處理器上,指令編碼具有固定的大小,可以是32位或16位,具體取決于程序使用的指令集。處理器獲取并解釋每條指令,然后依次運行每條指令以執行程序的邏輯。每條指令都是一個二進制模式或指令編碼,它遵循Arm架構定義的特定規則。

舉例來說,假設我們正在建立一個小型的16位指令集,并定義每條指令的模樣。我們的第一項任務是指定部分編碼,即指定要運行的指令類型——稱為操作碼。例如,我們可以將指令的前7位設置為操作碼,并指定加法和減法的操作碼,如表1.1所示。

因此手動編寫機器碼是可能的,但過于煩瑣。實際上,我們更希望用一些人類可讀的“匯編語言”來編寫匯編代碼,并將這些代碼轉換為機器碼的等效形式。為了做到這一點,我們還應該定義指令的簡寫形式,它們稱為指令助記符,如表1.2所示。

表1.1 加法和減法的操作碼

表1.2 加法和減法的助記符

當然,僅僅告訴處理器執行“加法”是不夠的。我們還需要告訴它要將哪兩個值相加以及如何處理結果。例如,如果我們編寫一個執行a=b+c操作的程序,bc的值需要在指令開始執行前存儲在某個地方,而且指令需要知道將結果a寫到哪里。

在大多數處理器中,特別是在Arm處理器中,這些臨時值通常存儲在寄存器中,寄存器存儲一小部分“工作”值。程序可以將數據從內存(或磁盤)中讀入寄存器中,以便進行處理,并且可以在處理后將結果數據存放到長期存儲器中。

寄存器的數量和命名規則取決于架構。隨著軟件變得越來越復雜,程序往往需要同時處理更多的數值。在寄存器中存儲和操作這些值比直接在內存中進行操作要快,這意味著寄存器減少了程序需要訪問內存的次數,并且提升了執行速度。

回到我們之前的例子,假設我們設計了一條16位的指令來執行一個操作,該操作將一個值加到一個寄存器中,并將結果寫入另一個寄存器。由于我們用7位來完成操作(ADD/SUB),因此剩下的9位可以用于編碼源寄存器(操作數寄存器)、目標寄存器和我們想要加或減的常量值。在這個例子中,我們將剩余的位數平均分配,并分配了表1.3所示的快捷方式和相應的機器碼。

表1.3 手動分配機器碼

我們可以編寫一個小程序將語法ADD R1R0#2R1=R0+2)轉換為相應的機器碼模式,而不是手動生成這些機器碼(見表1.4)。然后,將這個機器碼模式交給我們的示例處理器。

表1.4 機器碼編程

我們構建的位模式表示T32指令集中16位ADDSUB指令的一個指令編碼。在圖1.3中,你可以看到它的組成部分以及它們在指令編碼中的順序。

當然,這只是一個簡化的例子。現代處理器提供了數百條可能的指令,這些指令通常具有更復雜的子編碼。例如,Arm定義了加載寄存器指令(使用LDR助記符),該指令可以將一個32位的值從內存加載到一個寄存器中,如圖1.4所示。

圖1.3 16位Thumb編碼的ADD和SUB立即數指令

在這條指令中,要加載的“地址”在寄存器2(R2)中指定,讀取的值被寫入寄存器3(R3)。

R2的兩邊使用括號的語法表示R2寄存器中的值將被解釋為內存中的一個地址,而不是普通值。換句話說,我們不想將R2寄存器中的值復制到R3寄存器中,而是要獲取R2寄存器給定地址處內存的內容,并將該值加載到R3寄存器中。程序引用內存位置的原因有很多,其中包括調用函數或將內存中的值加載到寄存器中。

圖1.4 LDR指令從R2中的地址向寄存器R3加載一個值

這本質上是機器碼和匯編代碼之間的區別。匯編語言具有可讀性較強的語法,可以顯示如何解釋每條編碼指令。相比之下,機器碼是實際由處理器處理的二進制數據,其編碼由處理器設計者精確指定。

1.1.4 匯編

由于處理器只能理解機器碼而不能理解匯編語言,因此我們需要一個程序將手寫的匯編指令轉換為它們的機器碼等效形式。執行這個任務的程序被稱為匯編器。

實際上,匯編器不僅能夠理解指令,還能將單條指令轉換為機器碼,而且能夠解釋匯編器指令[2],匯編器指令可以指導匯編器執行其他任務,例如在數據和代碼之間切換或匯編不同的指令集。因此,匯編語言和匯編器語言只是看待同一件事情的兩種方式。匯編器指令和表達式的語法及含義取決于特定的匯編器。

這些指令和表達式是匯編程序中可用的快捷方式。然而,嚴格來說,它們并不屬于匯編語言,而是匯編器應該如何操作的指示。

在不同的平臺上有不同的匯編器,例如用于匯編Linux內核的GNU匯編器as,以及ARM工具鏈匯編器armasm和包含在Visual Studio中具有相同名稱(armasm)的Microsoft匯編器。

舉個例子,假設我們想要在名為myasm.s的文件中匯編以下兩條16位指令:

在這個程序中,前三行是匯編器指令。這些指令告訴匯編器數據應該在哪里被匯編(在本例中,放在.text節),將代碼的入口點的標簽(在本例中,稱為_start)定義為全局符號,最后指定它應該使用Thumb指令集(T32)進行編碼。Thumb指令集(T32)是Arm架構的一部分,它允許指令的寬度為16位。

我們可以使用GNU匯編器as,在運行于Arm處理器上的Linux操作系統機器上編譯這個程序:

匯編器讀取匯編語言程序myasm.s并創建一個名為myasm.o的目標文件。這個文件包含4個字節的機器碼,對應于我們的兩條2字節的十六進制指令:

匯編器另一個特別有用的功能是標簽,它引用內存中的特定地址,如分支目標、函數或全局變量的地址。

讓我們以匯編程序為例:

這個程序首先給兩個寄存器填充數值,然后跳轉到標簽mylabel執行ADD指令。在執行完ADD指令后,程序跳轉到result標簽,執行移動指令,然后跳轉到_exit標簽結束。匯編器將使用這些標簽為鏈接器提供提示,鏈接器為它們分配相對的內存位置。圖1.5說明了程序的流程。

圖1.5 匯編程序示例的程序流程

標簽不僅可以用來引用跳轉指令,還可以用來獲取內存位置的內容。例如,下面的匯編代碼片段使用標簽從內存位置獲取內容或跳轉到代碼中的不同指令:

首先用ADR指令將變量myvalue的地址加載到寄存器R2中,并使用LDR指令將該地址的內容加載到寄存器R3中。然后程序跳轉到標簽mylabel所引用的指令,執行ADD指令,再跳轉到標簽result所引用的指令,如圖1.6所示。

圖1.6 ADR和LDR指令邏輯的說明

作為一個稍微有趣的例子,下面的匯編代碼將Hello World!輸出到控制臺,然后退出。它使用一個標簽來引用字符串hello,方法是通過ADR指令將標簽mystring的相對地址放入寄存器R1中。

在支持Arm架構和指令集的處理器上匯編并鏈接此程序后,執行時會輸出Hello

現代匯編器通常被整合到編譯器工具鏈中,并且輸出可以合并成更大的可執行程序的文件。因此,匯編程序通常不僅僅是將匯編指令直接轉換為機器碼,而是創建一個目標文件,其中包括匯編指令、符號信息和編譯器鏈接程序的提示,最終負責創建在現代操作系統上運行的完整可執行文件。

交叉匯編器

如果在不同的處理器架構上運行我們的Arm程序,會怎樣?在Intel x86-64處理器上執行myasm2程序將產生一個錯誤,它會告訴我們由于可執行格式的錯誤,二進制文件不能被執行。

我們不能在x64機器上運行Arm二進制文件,因為這兩個平臺上的指令編碼方式不同。即使我們想在不同的架構上執行相同的操作,匯編語言和分配的機器碼也會有很大的不同。假設你想在三種不同的處理器架構上執行一條將十進制數字1移到第一個寄存器的指令。盡管操作本身是一樣的,但指令編碼和匯編語言卻取決于架構。以下列三種一般的架構類型為例:

●Armv8-A:64位指令集(AArch64)

●Armv8-A:32位指令集(AArch32)

●Intel x86-64指令集

不僅是語法不同,而且不同指令集之間相應的機器碼字節也有很大的差異。這意味著,為Arm 32位指令集匯編的機器碼字節在不同指令集的架構(如x64或A64)上具有完全不同的含義。

反過來也是如此。相同的字節序列在不同的處理器上可能會有顯著不同的解釋,例如:

●Armv8-A:64位指令集(AArch64)

●Armv8-A:32位指令集(AArch32)

換句話說,匯編程序需要使用我們想要運行這些匯編程序的架構的匯編語言編寫,并且必須用支持這種指令集的匯編器進行匯編。

然而,可能令人感到意外的是,可以在不使用Arm機器的情況下創建Arm二進制文件。當然,匯編器本身需要了解Arm語法,但如果該匯編器是為x64編譯的,則在x64機器上運行它將使你能夠創建Arm二進制文件。這種匯編器稱為交叉匯編器,允許你針對不同于當前正在使用的目標架構的架構進行代碼匯編。

例如,你可以在x86-64的Ubuntu機器上下載一個AArch32的匯編器,然后從那里匯編代碼。

使用Linux命令file,我們可以看到,我們創建了一個32位Arm可執行文件。

主站蜘蛛池模板: 禄丰县| 柳河县| 宜宾县| 高邮市| 高阳县| 扶绥县| 密云县| 商水县| 赤城县| 黄石市| 镇雄县| 香河县| 万年县| 三门峡市| 敦煌市| 乌苏市| 全南县| 赤峰市| 广元市| 南岸区| 屯昌县| 南充市| 凤山市| 新和县| 孟村| 永仁县| 美姑县| 同江市| 弥渡县| 崇州市| 门头沟区| 自贡市| 永顺县| 环江| 墨玉县| 额敏县| 县级市| 泾阳县| 湖南省| 昌黎县| 罗定市|