iOS汇编入门教程(三)汇编中的 Section 与数据存取

本文介绍汇编语言中Section的概念及应用,包括数据存储、字符串处理等,并通过实例演示如何使用Section进行变量操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

在前两篇文章中,我们介绍了反汇编的方法,调用栈的基本概念,以及如何通过 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 变量地址的过程如下。

  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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值