iOS彙編入門教程(三)彙編中的 Section 與數據存取

簡介

在前兩篇文章中,咱們介紹了反彙編的方法,調用棧的基本概念,以及如何經過 Xcode 去調試彙編代碼,在這篇文章中,咱們將介紹如何在彙編中經過 Section 來實現數據存取。html

Segment 與 Section

在彙編代碼中各個部分的頭部,咱們經常能看到 .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

爲何須要 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 變量地址的過程以下。

  1. 使用 adrp 命令計算出 _counter label 基於 PC 的偏移量的高 21 位,並存儲在 x8 寄存器中,@PAGE 表明頁偏移的高 21 位;

    adrp x8, _counter@PAGE
    複製代碼
  2. 使用 add 命令將餘下的 12 位補齊,經過 @PAGEOFF 表明頁偏移的低 12 位;

    add x8, x8, _counter@PAGEOFF
    複製代碼
  3. 此時,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 代碼。

  1. 最上面的 extern 聲明用於將彙編代碼定義的變量和函數引入文件。

    extern int sectionVersion;
    extern const char * sectionName;
    extern uint64_t getSectionNameAddress(void);
    extern const char * getSectionName(void);
    複製代碼
  2. dyld 函數用於獲取主二進制 (ASM.app) 加載的基址,Mach-O 文件加載時,將以基址爲偏移量,將全部虛擬地址映射到內存空間,所以獲取到基址和變量在內存空間中的地址後,經過 實際地址 - 基址 便可獲得變量的虛擬地址,即在 Section 中分配的地址;

  3. 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 的存儲位置和值。

參考資料

  1. How can I get load address of an iOS app?
  2. What are @PAGE and @PAGEOFF symbols in IDA?
  3. What's the difference of section and segment in ELF file format
  4. BSS段、數據段、代碼段、堆與棧
  5. ARM Document
  6. The A64 instruction set
相關文章
相關標籤/搜索