在前兩篇文章中,咱們介紹了反彙編的方法,調用棧的基本概念,以及如何經過 Xcode 去調試彙編代碼,在這篇文章中,咱們將介紹如何在彙編中經過 Section 來實現數據存取。html
在彙編代碼中各個部分的頭部,咱們經常能看到 .section 這樣的聲明,例以下面這段代碼。ios
; Program
.section __TEXT,__text,regular,pure_instructions
.global _someFunc
.p2align 2
_someFunc:
mov x0, #0
ret
複製代碼
用 MachOView 打開一個 Mach-O 格式的可執行文件,能夠看到其中包含了大量 Segment 與 Section,例以下圖。 bash
在 Stack Overflow 上,有一個關與 Section 與 Segment 的討論,回答中提到:app
The segments contain information needed at runtime, while the sections contain information needed during linking.iphone
A segment can contain 0 or more sections.jsp
簡單地說,Segment 是 Section 的集合,Segment 會指引着系統在指定的位置加載 Section,以下圖所示。 async
其中 Segment 爲下劃線開頭的大寫字母組合,Section 爲下劃線開頭的小寫字母組合,例如 __TEXT,__text
表明 __TEXT
Segment 指向的 __text
Section。函數
在編寫彙編代碼的過程當中,咱們只須要關心 Section 的定義,Segment 會由編譯系統自動建立,能夠理解爲咱們定義了一系列離散的代碼和數據,系統在構建 Mach-O 文件時會將這些 Section 組合起來,將他們的地址經過 Section 統一管理。系統在執行 Mach-O 文件時,只須要從頭部讀取 Mach-O Header 便可獲取到整個文件的 Section 信息,隨後再進行後續的運行時加載。ui
看下面一個例子,咱們定義一個全局變量 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 的倍數,則按照最小倍數補全;.long 1
所在的地址,以便後續的讀寫。此外,代碼也是一種數據,被存放在 __TEXT,__text
段,這個段的特色是內存空間只讀,所以適合存放代碼等定值。
讓咱們看一下上面代碼的完整彙編結果,使用以下命令便可將上文的 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
複製代碼
咱們看以下這段代碼。
#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"
的地址。
首先新建一個 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 的存儲位置和值。