在前两篇文章中,咱们介绍了反汇编的方法,调用栈的基本概念,以及如何经过 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 的存储位置和值。