ARM64汇编入门小记

1. ARM64中的寄存器

程序在计算机中执行时, 依赖最重要的是CPU中的寄存器, 不同的CPU架构体系中的寄存器个数和种类都不一样.

ARM64目前拥有31个 64位(8字节) 通用寄存器(General Purpose Registers), 可以用x0~x30来表示. 其中有一些比较关键的寄存器有一些特殊的用途如下:

  1. x29 -- fp(frame pointer)寄存器, 用来记录函数的调用栈的, 栈底地址
  2. x30 -- lr (link register)寄存器, 连接寄存器, 用来记录当前函数返回时, 执行的下一条代码指令的地址.
  3. x31 -- sp (stack pointer)寄存器, 用来记录内存中, 栈顶地址, 寄存器在任意时刻会保存我们栈顶的地址.

另外还有两个比较特殊的寄存器:

  1. pc (program counter) 程序计数寄存器: 它指示了CPU当前要读取的指令的地址
  2. cpsr (current program status register)程序状态寄存器 : 用来记录CPU计算过程中的一些状态标志位
  3. xzr(零寄存器)
  1. w0~w30是31个32位寄存器, 实际是用x0~x30的低32位表示的
  2. 另外CPU还有其他的浮点寄存器, 向量寄存器等等其他的寄存器

2. 从CPU看程序执行的过程

程序在执行过程经过如下过程:

  1. CPU将 内存(堆/栈等) 中的数据存储到通用寄存器
  2. 然后对通用寄存器中的数据进行运算
  3. CPU将运算结果存储回内存

另外, 在内存或者磁盘中上的二进制, 指令数据没有任何区别!!! CPU在执行时, 有时将二进制解释成指令, 有时将二进制看做数据, 例如 1110 0000 0000 0011 0000 1000 1010 1010:

  1. 当做数据: 0xE003008AA
  2. 当做指令: mov x0, x8

对CPU执行程序的理解可以有如下过程:

  1. pc寄存器指向的内容, CPU都会当做指令去解释并执行
  2. pc执行完当前指令以后, 就将pc寄存器指向当前指令的下一个位置, 继续重复
  3. 我们可以通过改变pc的内容来控制CPU执行目标指令, ARM64中bl指令就是改变pc寄存器的内容,从而实现函数跳转, bl簇指令都称为转移指令

下面是一个简单的汇编代码, 描述从函数void A()内部调用void B()函数的示例:

_A:
    mov x0,#0xa0
    mov x1,#0x00
    add x1, x0, #0x14
    mov x0,x1
    bl _B
    mov x0,#0x0
    ret

_B:
    add x0, x0, #0x10
    ret
复制代码

3. 从CPU的角度看函数

ARM64中, 操作栈是通过sub/add sp指针来进行栈拉伸和栈平衡, 下面是一个实例, 操作栈内存:

sub    sp, sp, #0x40             ; 拉伸0x40(64字节)空间
stp    x29, x30, [sp, #0x30]     ; x29\x30 寄存器入栈保护
add    x29, sp, #0x30            ; x29指向栈帧的底部
... 
ldp    x29, x30, [sp, #0x30]     ; 恢复x29/x30 寄存器的值
add    sp, sp, #0x40             ; 栈平衡
ret
复制代码

函数中的内存操作:

  1. storage pair/register: 使用stp/str来将寄存器内容存储到内存中,

  2. 使用ldp/ldr来读取内存数据到指定寄存器

注意: ARM64的寄存器是64位(8字节), 因此str/ldr一次只能操作8字节数据, 使用stp/ldp可以一次性操作16个字节数据.

两个与函数相关的ARM64指令与寄存器:

  1. bl 标号

    • 将下一条指令的地址放入lr(x30)寄存器

    • 转到标号处执行指令

  2. ret

    • 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址
  3. x30 (lr) 寄存器

    • x30寄存器存放的是函数的返回地址.当ret指令执行时刻,会寻找x30寄存器保存的地址值!

函数的参数与返回值:

  1. ARM64下, 函数的参数是存放在x0x7这8个寄存器里面的.如果超过8个参数,就会提前入栈内存.
  2. 函数的返回值是放在x0寄存器里面的.
  3. 函数的局部变量放在栈内存里面.

4. 从CPU的角度看多线程

从ARM64来看, 整个程序执行过程中CPU并不知道线程的存在, 操作系统内核会为每个线程维护独立的函数调用栈, 并且在系统切换线程时, 使用内核数据结构保存CPUInfo, 包括各个寄存器的关键信息:

//这个结构是linux在arm32CPU上的线程上下文结构,代码来自于:http://elixir.free-electrons.com/linux/latest/source/arch/arm/include/asm/thread_info.h  
//这里并没有保存所有的寄存器,是因为ABI中定义linux在arm上运行时所使用的寄存器并不是全体寄存器,所以只需要保存规定的寄存器的内容即可。这里并不是所有的CPU所保存的内容都是一致的,保存的内容会根据CPU架构的差异而不同。
//因为iOS的内核并未开源所以无法得到iOS定义的线程上下文结构。

//线程切换时要保存的CPU寄存器,
struct cpu_context_save {
	__u32	r4;
	__u32	r5;
	__u32	r6;
	__u32	r7;
	__u32	r8;
	__u32	r9;
	__u32	sl;
	__u32	fp;
	__u32	sp;
	__u32	pc;
	__u32	extra[2];		/* Xscale 'acc' register, etc */
};

//线程上下文结构
struct thread_info {
	unsigned long		flags;		/* low level flags */
	int			preempt_count;	/* 0 => preemptable, <0 => bug */
	mm_segment_t		addr_limit;	/* address limit */
	struct task_struct	*task;		/* main task structure */
	__u32			cpu;		/* cpu */
	__u32			cpu_domain;	/* cpu domain */
	struct cpu_context_save	cpu_context;	/* cpu context */
	__u32			syscall;	/* syscall number */
	__u8			used_cp[16];	/* thread used copro */
	unsigned long		tp_value[2];	/* TLS registers */
#ifdef CONFIG_CRUNCH
	struct crunch_state	crunchstate;
#endif
	union fp_state		fpstate __attribute__((aligned(8)));  /*浮点寄存器*/
	union vfp_state		vfpstate;  /*向量浮点寄存器*/
#ifdef CONFIG_ARM_THUMBEE
	unsigned long		thumbee_state;	/* ThumbEE Handler Base register */
#endif
};


作者:欧阳大哥2013
链接:https://juejin.cn/post/6844903560145010702
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复制代码

5. objc_msgSend 汇编分析

网上很多, 可以参考 www.jianshu.com/p/62ecc3f31…

6. HOOK所有的OC方法, 统计每个方法耗时

可以参考一下两个实现:

  1. github.com/czqasngit/o…

  2. github.com/maniackk/Ti…

参考:

  1. www.cnblogs.com/bwangblog/p…
  2. juejin.cn/post/684490…

猜你喜欢

转载自juejin.im/post/7019115197931470855