[Objc翻译]iOS main函数的汇编

179 阅读10分钟

本文由 简悦 SimpRead转码, 原文地址 suelan.github.io

RY 的博客

如何在 Xcode 中查看 Assemble 代码

debug -> Product -> Action-> Assemble "main.m"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

生成的文件有 541 行代码,其中包含大量供调试器使用的汇编指令。

汇编指令

	.section	__TEXT,__text,regular,pure_instructions
	.ios_version_min 11, 0	sdk_version 13, 6
	.file	1 "/Users/rongyan.zheng/Downloads/AssemblyMain" "AssemblyMain/main.m"
	.globl	_main                   ; -- Begin function main
	.p2align	2

这些是汇编指令,而不是汇编代码。".section "指令指定了下面的内容将进入哪个部分。

接下来,.globl 指令指定 _main 是一个外部符号。.p2align 2 将文件中的当前位置对齐到指定边界,这里是 2^2,4 字节。

然后,就是我们的 main 集合标签。

_main:                                  ; @main
Lfunc_begin0:
	.loc	1 12 0                  ; AssemblyMain/main.m:12:0
	.cfi_startproc
; %bb.0:

.cfi_startproc指令用于大多数函数的开头。CFI 是调用帧信息的简称。一个 frame 粗略地对应一个函数。当你使用调试器 step instep out 时,你实际上是在步入/步出调用帧。在 C 代码中,函数有自己的调用框架,但其他东西也可以。.cfi_startproc "指令为函数提供了一个进入".eh_frame "的入口,其中包含了展开信息--这就是异常如何展开调用框架栈的。该指令还将为 CFI 发送依赖于体系结构的指令。在输出的更下方,它与相应的 .cfi_endproc 相匹配,以标记我们的 main() 函数的结束。- www.objc.io/issues/6-bu…

SP 和堆栈

	sub	sp, sp, #48             ; =48

它在堆栈上设置了一个调用帧。这里的 sp 指的是栈指针寄存器。在 AArch64 中,堆栈指针必须 128 位对齐;这里是 48 * 8 位。

寄存器

处理器的大部分操作都涉及数据处理。这些数据可以存储在内存中,也可以从内存中访问。但是,从内存中读取数据和将数据存储到内存中会减慢处理器的运行速度,因为这涉及到通过控制总线向 "内存存储单元 "发送数据请求并通过同一通道获取数据的复杂过程。为了加快处理器的运行速度,处理器包含一些 "内部内存存储位置",称为寄存器

寄存器存储用于处理的数据元素,而无需访问内存。处理器芯片内置的寄存器数量有限。- www.tutorialspoint.com/assembly_pr…

在 ARM 64 中,下图显示了寄存器的作用。

  • 前八个寄存器(r0-r7)用于向子程序传递参数值和从函数返回结果值。
  • 帧指针寄存器"(FP)应指向最内层帧(属于最近的例程调用)的帧记录。寻址最低的双字应指向 "上一帧记录",寻址最高的双字应包含进入当前函数时在 LR 中传递的值。

堆栈结构

堆栈是内存的一个 "连续区域",可用于存储局部变量,以及在参数寄存器不足时向子程序传递额外参数。堆栈的实现方式是 全降,"堆栈的当前范围 "保存在专用寄存器 "SP "中。-ARM 64 位体系结构的过程调用标准(AArch64)- AArch64 ABI 1.0 版

ARM 环境使用的堆栈在函数调用时是向下增长的,其中包含局部变量和函数参数。堆栈在函数调用时对齐。图 1 显示了子程序调用前和调用过程中的堆栈。

堆栈帧包含以下区域:

  • 参数区(parameter area) 存储调用者传递给被调用函数的参数,或者根据每个参数的类型和寄存器的可用性为参数存储空间。该区域位于调用者的堆栈帧中。
  • 链接区 包含调用者下一条指令的地址。
  • 保存帧指针(可选)包含调用者堆栈帧的基地址。
  • 局部存储区 包含子程序的局部变量,以及被调用函数返回前必须恢复的寄存器值。详情请参见寄存器保存
  • 保存寄存器区 包含在调用函数返回前必须恢复的寄存器值。详情请参见寄存器保存

在这种环境下,堆栈帧的大小并不固定。

另一个堆栈帧布局图来自 ARM 64 位体系结构的过程调用标准(AArch64)- AArch64 ABI 1.0 版

关于堆栈和堆栈指针,请参阅更多内容。

寻址模式

作为初学者,我想知道地址是如何通过方括号内的数字计算出来的。

在这里,我们必须了解一些关于 "寻址模式 "的知识。根据关于 ARMv8 指令集的文档,有几种寻址模式定义了地址的形成方式。

  • 基准寄存器 - 最简单的寻址方式是单寄存器。基寄存器是一个 X 寄存器,包含被访问数据的完整或绝对虚拟地址,如图所示:

  • 偏移寻址模式 - 可选择将偏移应用于基址,如图所示:

    在上图中,X1 包含基地址,#12 是该地址的字节偏移量。这意味着访问地址为X1+12。偏移可以是一个常数,也可以是另一个寄存器。例如,结构体可以使用这种寻址方式。编译器会使用偏移量维护一个指向结构体基部的指针,以选择不同的成员。

  • 预索引寻址模式 - 在指令语法中,预索引通过在方括号后添加感叹号!来表示,如图所示:

    预索引寻址方式与偏移寻址方式类似,"只是基指针会随着指令的执行而更新"。在上图中,指令执行完毕后,X1的值为 X1+12。

  • 后索引寻址模式 - 使用后索引寻址时,值从基指针中的地址加载,然后更新指针,如图所示:

    后索引寻址适用于从堆栈中弹出。该指令从堆栈指针指向的位置加载值,然后将堆栈指针移动到堆栈的下一个完整位置。

下面是我们的 main 函数中的下一条指令:

	stp	x29, x30, [sp, #32] 

stp 将 X29 和 X30 推入堆栈,这意味着这条指令将把 x29x30 的值存储到地址为 sp+32 的内存中。在 ARMv8 中,X29 用于帧指针,X30 用作链接寄存器,可称为 LR。

在堆栈上存储参数

	add	x29, sp, #32            ; =32

设置 x29 的值为 sp + #32

	stur	wzr, [x29, #-4]
	stur	w0, [x29, #-8]
	str	x1, [sp, #16]

wzr 中的值存储到 x29 + #-4,即用 0 写入 x29 + #-4

零寄存器 ZXR 和 WZR 始终读作 0,忽略写入。

w0 中的值存入 x29 + #-8;

x1 中的值存储到 sp + #16

大多数 A64 指令都在寄存器上运行。该架构提供 31 个通用寄存器。每个寄存器可用作 64 位 X 寄存器(X0...X30)或 32 位 W 寄存器(W0...W30)。W0 是 X0 底部的 32 位。

选择 X 还是 W 决定了操作的大小。使用 X 寄存器将导致 64 位计算,而使用 W 寄存器将导致 32 位计算。

参数寄存器

Arm 架构对通用寄存器的使用方式有一些限制。

X0-X7 是参数/结果寄存器。x0-x7 用于向子程序传递参数值和从函数返回结果值。第一个参数传递到 X0,第二个参数传递到 X1

在我们的例子中,main 函数需要两个参数,因此使用了 w0X

用于返回的寄存器

返回结果使用哪个寄存器取决于返回结果的类型:

  • 如果函数结果的类型 T 为

    "void func(T arg)",则返回结果使用的寄存器与传递参数时使用的寄存器相同。例如

  • 否则,调用者应为结果预留足够大小和对齐方式的内存块。内存块的地址应作为附加参数传递给函数 X8.XR |X8.
Ltmp0:
	.loc	1 13 16 prologue_end    ; AssemblyMain/main.m:13:16
	mov	x8, #0                    ; copy #0 to x8
	str	x8, [sp, #8]              ; 
	.loc	1 14 22                 ; AssemblyMain/main.m:14:22

然后,将 x8 中的值存储到 sp + #8 中。

分支指令和函数调用

让我们看看下一条指令。

	bl	_objc_autoreleasePoolPush

通常,处理器按程序顺序执行指令。这意味着处理器执行指令的顺序与内存中设置指令的顺序相同。改变这种顺序的一种方法是使用分支指令。分支指令改变了程序流程,用于循环、决策和函数调用。

A64 指令集还有一些条件分支指令。这些指令会根据前面指令的结果改变执行方式。- developer.arm.com/architectur…

无条件分支指令

无条件分支指令有两种:B表示分支,BR表示带寄存器的分支。

无条件分支指令 B <label> 执行一个直接的、与 PC 有关的分支,指向 .

在我们的例子中,labe_objc_autoreleasePoolPushbl _objc_autoreleasePoolPush 表示我们将跳转到 _objc_autoreleasePoolPush例程。

条件分支指令

条件分支指令 B. 是 B 指令的条件版本。只有当条件为真时,才会执行分支。范围限制为 +/- 1MB。

PC相关地址的标签

www.keil.com/support/man…

标签可以表示 PC 值加上或减去 PC 到标签的偏移量。使用这些标签作为分支指令的目标,或访问嵌入代码段中的小数据项。

	adrp	x8, _OBJC_SELECTOR_REFERENCES_@PAGE
	add	x8, x8, _OBJC_SELECTOR_REFERENCES_@PAGEOFF        ;@selector(class)
	adrp	x9, _OBJC_CLASSLIST_REFERENCES_$_@PAGE 
	add	x9, x9, _OBJC_CLASSLIST_REFERENCES_$_@PAGEOFF     ; objc_cls_ref_AppDelegate
  • adrp 是 PC 相关偏移的 4KB 页面地址

    我将最终的可执行文件拖入 hopper 中,_OBJC_SELECTOR_REFERENCES_@PAGE 的地址是 #0x100009000.

000000010000621c         adrp       x8, #0x100009000                            ; 0x1000093e0@PAGE
0000000100006220         add        x8, x8, #0x3e0                              ; 0x1000093e0@PAGEOFF, &@selector(class)
0000000100006224         adrp       x9, #0x100009000                            ; 0x1000093f0@PAGE
0000000100006228         add        x9, x9, #0x3f0                              ; 0x1000093f0@PAGEOFF, objc_cls_ref_AppDelegate
000000010000622c         ldr        x9, [x9]                                    ; objc_cls_ref_AppDelegate,_OBJC_CLASS_$_AppDelegate
0000000100006230         ldr        x1, [x8]  
	ldr	x9, [x9]                ; load data into x9 from the value in x9.
	ldr	x1, [x8]                ; load data into x1 from the value in x8, which is the address of @selector(class)
	str	x0, [sp]                ; 8-byte Folded Spill
	mov	x0, x9                  ; move the addreess in x9 into x0, which is the address of objc_cls_ref_AppDelegate
	bl	_objc_msgSend           ; call msgSend, [AppDelegate class]
	bl	_NSStringFromClass      ; call NSStringFromClass 

返回值和自动释放

	mov	x29, x29	; marker for objc_retainAutoreleaseReturnValue
	bl	_objc_retainAutoreleasedReturnValue
	ldr	x8, [sp, #8]            ; load data into x8 from memory [sp, #8] 
	str	x0, [sp, #8]            ; store data from x0 into [sp, #8], which is the result of _NSStringFromClass
	mov	x0, x8                  ; move data from x8 to x0
	bl	_objc_release
	ldr	x0, [sp]                ; 8-byte Folded Reload
	bl	_objc_autoreleasePoolPop

str x0, [sp, #8]表示将 x0 中的数据存储到 [sp, #8] 中。如前所述,对于NSString *NSStringFromClass(Class aClass)类型的函数,x0用于存储返回结果。

传递参数

	ldur	w0, [x29, #-8]      ; load data into w0 from [x29, #-8]; which is int argc
	ldr	x1, [sp, #16]         ; load data into x1 from [sp, #16], where argv is stroed
	ldr	x3, [sp, #8]          ; laod data into x3 from [sp, #8], which is the result of  the result of _NSStringFromClass
	mov	x8, #0
	mov	x2, x8                ; nil
	bl	_UIApplicationMain    ; call _UIApplicationMain
	stur	w0, [x29, #-4]      
	add	x8, sp, #8              ; =8
	mov	x0, x8
	mov	x8, #0
	mov	x1, x8
	bl	_objc_storeStrong
	ldur	w0, [x29, #-4]
	ldp	x29, x30, [sp, #32]     ; 16-byte Folded Reload, reset 
	add	sp, sp, #48             ; =48, reset 
	ret

ldp x29, x30, [sp, #32] 用于重置 "fp "和链接器注册表

add sp, sp, #48 表示该函数的调用框架已消失。