簡介
在前兩篇文章中,我們介紹了反彙編的方法,呼叫棧的基本概念,以及如何通過 Xcode 去除錯彙編程式碼,在這篇文章中,我們將介紹如何在彙編中通過 Section 來實現資料存取。
Segment 與 Section
在彙編程式碼中各個部分的頭部,我們常常能看到 .section 這樣的宣告,例如下面這段程式碼。
; Program
.section __TEXT,__text,regular,pure_instructions
.global _someFunc
.p2align 2
_someFunc:
mov x0, #0
ret
複製程式碼
用 MachOView 開啟一個 Mach-O 格式的可執行檔案,可以看到其中包含了大量 Segment 與 Section,例如下圖。
在 Stack Overflow 上,有一個關與 Section 與 Segment 的討論,回答中提到:
The segments contain information needed at runtime, while the sections contain information needed during linking.
A segment can contain 0 or more sections.
簡單地說,Segment 是 Section 的指標,Segment 會指引著系統在指定的位置載入 Section,如下圖所示。
其中 Segment 為下劃線開頭的大寫字母組合,Section 為下劃線開頭的小寫字母組合,例如 __TEXT,__text
代表 __TEXT
Segment 指向的 __text
Section。
在編寫彙編程式碼的過程中,我們只需要關心 Section 的定義,Segment 會由編譯系統自動建立,可以理解為我們定義了一系列離散的程式碼和資料,系統在構建 Mach-O 檔案時會將這些 Section 組合起來,將他們的地址通過 Section 統一管理。系統在執行 Mach-O 檔案時,只需要從頭部讀取 Mach-O Header 即可獲取到整個檔案的 Section 資訊,隨後再進行後續的執行時載入。
為什麼需要 Section
看下面一個例子,我們定義一個全域性變數 counter,以及一個 getCount 方法。
int counter = 1;
int getCount() {
return counter;
}
複製程式碼
為了實現以上程式碼,編譯器必須為全域性變數 counter 預先分配好虛擬地址,以便程式 load 時建立起全域性變數的儲存區,Section 中的 DATA 段即可完成這樣的工作,它的宣告如下:
.section __DATA,__data
.globl _counter ; @counter
.p2align 2
_counter:
.long 1 ; 0x1
複製程式碼
- 第一行用
.section
宣告瞭該資料位於__DATA,__data
段,這個區段的特點是載入後可讀可寫,因此將變數儲存在這個區域; - 第二行的
.global
宣告說明變數符號 counter 是一個全域性變數,即可在其他檔案中通過 extern 的方式引入; - 第三行的
.p2align
是用於指定程式的對齊方式,這類似於結構體的位元組對齊,為的是加速程式的執行速度,p2align 的單位是指數,即按照 2 的 exp 次方對齊,上文中的.p2align 2
即為按照 2^2 = 4 位元組對齊,也就是說,如果單行指令或資料的長度不足4位元組,將用 0 補全,超過 4 但不是 4 的倍數,則按照最小倍數補全; - 第四行是一個 label,用來表示
.long 1
所在的地址,以便後續的讀寫。
此外,程式碼也是一種資料,被存放在 __TEXT,__text
段,這個段的特點是記憶體空間只讀,因此適合存放程式碼等固定值。
如何讀寫 Section
讓我們看一下上面程式碼的完整彙編結果,使用如下命令即可將上文的 C 程式碼轉成彙編。
clang -S -arch arm64 -isysroot `xcrun --sdk iphoneos --show-sdk-path` -fno-asynchronous-unwind-tables <your_c_file_path>
複製程式碼
彙編的完整結果如下。
.section __TEXT,__text,regular,pure_instructions
.globl _getCount ; -- Begin function getCount
.p2align 2
_getCount: ; @getCount
adrp x8, _counter@PAGE
add x8, x8, _counter@PAGEOFF
ldr w0, [x8]
ret
; -- End function
.section __DATA,__data
.globl _counter ; @counter
.p2align 2
_counter:
.long 1 ; 0x1
複製程式碼
可以看到,底部即為上文講到的用於全域性變數儲存的 __DATA,__data
段的宣告,最上方則是對程式碼段 __TEXT,__text
的宣告,隨後即為 getCount 函式的程式碼。
從上面的結果可以看出,在彙編中,資料和程式碼是儲存在一起的,資料本質上也是一種程式碼,因此讀取 counter 變數本質上是從特定的地址讀取內容,一般而言,基於程式計數器 PC 進行定址即可,在 ARM64 中提供了可在 +/-4GB (33 bits) 範圍內定址的 adrp 命令,該命令的基本用法如下。
例如我們要找到 counter 變數,本質上是計算當前指令距離 counter 變數的距離,即計算基於 PC 的偏移量,能表示的偏移量的最大長度決定了能夠定址的空間大小,可以想象,如果程式碼和資料段之間的距離過大,將難以通過一次運算進行定址。計算 counter 變數地址的過程如下。
-
使用 adrp 命令計算出 _counter label 基於 PC 的偏移量的高 21 位,並儲存在 x8 暫存器中,@PAGE 代表頁偏移的高 21 位;
adrp x8, _counter@PAGE 複製程式碼
-
使用 add 命令將餘下的 12 位補齊,通過 @PAGEOFF 代表頁偏移的低 12 位;
add x8, x8, _counter@PAGEOFF 複製程式碼
-
此時,x8 中即為 counter 變數的實際地址了,通過 ldr 命令將暫存器的值讀取到 w0 中,作為函式返回值。
ldr w0, [x8] ret 複製程式碼
看到這裡,相信你會有個很大的疑問,為什麼不能一次性的將地址載入到 x8,而要拆分成高 21 位和低 12 位呢,這是因為 ARM64 雖然支援 64 位地址,但指令的長度僅有 32 位,因此難以通過一條指令去編碼 64 位地址,所以才拆解成了 adrp + add 的組合,從而支援了正負 32 位地址偏移量範圍的定址。
如果你想深入瞭解基於 PC 的定址,可以閱讀 What are @PAGE and @PAGEOFF symbols in IDA? 中的高票回答。
學會了通過 adrp 讀取變數地址,那麼寫變數其實就是通過 str 將暫存器的值寫入變數地址,假如我們將計算結果儲存在了 w1 暫存器,那麼將 w1 寫入 counter 變數的程式碼如下。
_addCount:
; omit function start
adrp x8, _counter@PAGE
add x8, x8, _counter@PAGEOFF
; omit code for save new value to w1
str w1, [x8]
; omit function end
複製程式碼
字串的 Section 儲存
我們看如下這段程式碼。
#include <stdio.h>
char *secName = "MySec";
int main() {
printf("the secName is %s", secName);
return 0;
}
複製程式碼
這其中涉及到兩個字串,"MySec
" 和 "the secName is %s"
,它們被儲存在 __TEXT,__cstring
段,宣告如下。
.section __TEXT,__cstring,cstring_literals
l_.str: ; @.str
.asciz "MySec"
.section __TEXT,__cstring,cstring_literals
l_.str.1: ; @.str.1
.asciz "the secName is %s"
複製程式碼
所不同的是,"My_Sec"
被作為全域性變數 _secName 的初值,secName 的定義如下。
.section __DATA,__data
.globl _secName ; @secName
.p2align 3
_secName:
.quad l_.str
複製程式碼
需要注意的是,這裡的 _secName 符號是一個指標,它的值是字串 "MySec"
的地址。
通過 Xcode 和 Mach-O 驗證 Section 儲存
首先新建一個 iOS Empty Project,命名為 ASM,之所以使用 iOS Project,是為了獲得 ARM64 的執行環境,然後在工程中新建一個 example.s 檔案,整個工程的配置如下。
; example.s
; Program
.section __TEXT,__text,regular,pure_instructions
.global _getSectionName, _getSectionNameAddress
.p2align 2
_getSectionName:
adrp x8, _sectionName@PAGE
add x8, x8, _sectionName@PAGEOFF
ldr x0, [x8]
ret
_getSectionVersion:
adrp x8, _sectionVersion@PAGE
add x8, x8, _sectionVersion@PAGEOFF
ldr w0, [x8]
ret
_getSectionNameAddress:
adrp x8, _sectionName@PAGE
add x8, x8, _sectionName@PAGEOFF
mov x0, x8
ret
; Global Data
.section __DATA,__data
.global _sectionVersion
.p2align 2
_sectionVersion:
.long 100
.global _sectionName
.p2align 3
_sectionName:
.quad l_str
; String Literal
.section __TEXT,__text,cstring_literals
l_str:
.asciz "MySec"
複製程式碼
// main.m
#import "AppDelegate.h"
#include <mach-o/dyld.h>
extern int sectionVersion;
extern const char * sectionName;
extern uint64_t getSectionNameAddress(void);
extern const char * getSectionName(void);
uint64_t getProcessBaseAddress() {
uint32_t numberImages = _dyld_image_count();
for (uint32_t i = 0; i < numberImages; i++) {
const struct mach_header *header = _dyld_get_image_header(i);
const char *name = _dyld_get_image_name(i);
const char *p = strrchr(name, '/');
if (p && strcmp(p + 1, "ASM") == 0) {
return (uint64_t)header;
}
}
return -1;
}
int main(int argc, char * argv[]) {
uint64_t baseAddress = getProcessBaseAddress();
uint64_t sectionNameAddress = getSectionNameAddress();
printf("process base address at 0x%llx\n", baseAddress);
printf("the version is %d\n", sectionVersion);
printf("get section address is 0x%llx\n", sectionNameAddress - baseAddress);
printf("get section name %s\n", getSectionName());
return 0;
}
複製程式碼
下面我們執行程式碼,觀察控制檯的輸出。
process base address at 0x100640000
the version is 100
get section address is 0x8de0
get section name MySec
複製程式碼
第一行列印出了程式執行的基址,隨後分別列印了變數 sectionVersion 的值以及變數 sectionName 的地址和值,上述彙編程式碼相信通過講解你已能夠讀懂,下面著重講一下用於驗證的 C 程式碼。
-
最上面的 extern 宣告用於將彙編程式碼定義的變數和函式引入檔案。
extern int sectionVersion; extern const char * sectionName; extern uint64_t getSectionNameAddress(void); extern const char * getSectionName(void); 複製程式碼
-
dyld 函式用於獲取主二進位制 (ASM.app) 載入的基址,Mach-O 檔案載入時,將以基址為偏移量,將所有虛擬地址對映到記憶體空間,因此獲取到基址和變數在記憶體空間中的地址後,通過
實際地址 - 基址
即可得到變數的虛擬地址,即在 Section 中分配的地址; -
main 函式部分,為了得到 sectionName 的實際地址,第三個 printf 使用了
實際地址 - 基址
的公式來得到其虛擬地址。
上面程式碼的輸出告訴了我們 sectionName 的值位於地址 0x8de0
,下面我們用 MachOView 開啟這個二進位制檔案,檢視一下 0x8de0
的實際內容。
可以看到,變數位於 __DATA,__data
段,其值為 0x6b0c
,需要注意的是,iOS 採用了小端位元組序,即低位元組在低位,高位元組在高位,所以在讀記憶體的值的時候每 2 個位元組需要倒序讀取,其原理可以用下面一段程式碼解釋和判斷。
uint16_t u = 1;
// for value 0x0001
// address | +0 | +1 |
// big-endian | 00 | 01 |
// little-endian | 01 | 00 |
// first byte big = 0x00, little = 0x01
printf("%s endian\n", *(uint8_t*)&u ? "little" : "big");
複製程式碼
通過上文我們知道,sectionName 的值是 0x6b0c
,是一個地址,這也驗證了 sectionName 本身是個地址,那麼 0x6b0c
儲存的是不是字串 "MySec"
呢,我們繼續通過 MachOView 檢視。
可以看到,0x6b0c
位於 __TEXT,__text
段,其值為 "MySec\0"
,至此我們完成了驗證,讀者可以自己嘗試去驗證 sectionVersion 的儲存位置和值。