内容简介:在前两篇文章中,我们介绍了反汇编的方法,调用栈的基本概念,以及如何通过 Xcode 去调试汇编代码,在这篇文章中,我们将介绍如何在汇编中通过 Section 来实现数据存取。在汇编代码中各个部分的头部,我们常常能看到 .section 这样的声明,例如下面这段代码。用 MachOView 打开一个 Mach-O 格式的可执行文件,可以看到其中包含了大量 Segment 与 Section,例如下图。
在前两篇文章中,我们介绍了反汇编的方法,调用栈的基本概念,以及如何通过 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 的存储位置和值。
参考资料
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- iOS汇编入门教程(一)ARM64汇编基础
- iOS 汇编入门教程(一):ARM64 汇编基础
- iOS汇编入门教程(二)在Xcode工程中嵌入汇编代码
- EJS入门教程
- SnapKit入门教程
- Socat 入门教程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。