ReactNative有Profiler的功能,能计算每个函数的执行耗时。为了尽量减少测量代码对函数本身执行的影响,ReactNative采用汇编实现Trampoline来实现该功能。本文主要记录该部分核心代码的解读,顺便记录阅读汇编代码的一些技巧。

关键字

  • arm指令查询地址
  • arm64调用约定
  • pseudo-ops

什么是Trampoline

Trampoline通常都和跳转相关。本文提到的Trampoline是一个特定的地址,该地址指向特定的功能,待功能执行完毕之后,立马跳出Trampoline回到正常执行路径。

就跟蹦床(英文名:Trampoline)一样,一直在bouncing。

代码实现

ReactNative的Trampoline都是纯手工汇编实现的。因为iOS真机有armv7s、arm64等架构,模拟器有i386、x86_64架构,因此其实现也有四种方式。

  • i386
  • x86_64
  • armv7s
  • arm64

他们的实现是大同小异的,只是其汇编指令和寄存器不太一样。但阅读代码的方式是一样的。本文仅已arm64为例。

ABI

在阅读汇编之前,需要先了解ABI(Application Binary Interface)这个概念。

ABI是两个二进制程序模块之间的调用接口。一般我们写的代码经过编译器编译链接之后生成二进制代码,因此平常我们并不直接和ABI打交道。ABI定义了机器码如何访问数据结构和函数,而机器码是一种低级的,和硬件相关的格式

因此ABI的制定一般都是编译器、操作系统等设计开发者的活,一般的开发者较少接触到ABI。

而针对arm64的芯片,arm公司则制定了自己的ABI标准。因此在阅读汇编之前,我们先要了解一下ARM公司制定的这个ABI标准。

一般ABI都包括一下几个部分的内容:

  • 处理器指令集
  • 数据类型的大小、布局和对齐方式
  • 调用约定,确定函数的参数是如何传递的,函数的返回值是如何获取的。例如,是所有的参数都通过栈传递,还是部分参数通过寄存器传递;哪个寄存器用于哪个函数参数;通过栈传递的第一个函数参数是最先push到栈上还是最后。
  • 系统调用的编码和一个应用如何向操作系统进行系统调用。
  • 以及在一个完整的操作系统ABI中,目标文件的二进制格式、程序库等等。

以上的五大块,每一块都有厚厚的文档详细来规范和描述。我们现在只关心处理器指令集调用约定部分。因为处理器指令集在arm64上非常繁杂,我们一般都是遇到了再去查询其真正的用法和意义,arm指令的查询地址在此。但调用约定对我们阅读汇编代码非常重要,我们需要不断的熟悉调用约定的说明文档,对于阅读和编写汇编代码极其重要。

arm64的调用约定说明在此,ABI一般都是保持相对稳定的,大可下载之后珍藏起来。我们将在后面的分析中频繁的用到这里面的信息。

寄存器

在分析代码之前,最基本的需要先了解下每个寄存器的功能。

该截图来自于arm64调用约定,该文档里有针对每个寄存器功能的说明。

这里需要划几个重点:

  • r0-r7被函数用来传递参数,也被用来保存返回的值。
  • 在64-bit的机器上,r0-r30寄存器被命名为x0-x30。
  • 在32-bit的机器上,r0-r30寄存器被命名为w0-w30。

调用栈

理解汇编代码之前,还需要对调用栈非常熟悉。

要点记录:

  • S栈往下生长
  • SP指向最新的栈地址

代码分析

下面进入代码分析阶段。

.align 5

当我们去arm指令查询地址查询 .align 的时候是查不到的。这是为什么呢?

原来汇编器处理汇编命令分成两种类型,一种是在代码执行的时候处理,也就是之前提到的机器指令。还有一种是在汇编编译编译阶段就已经处理调的,这种指令一般都被叫做 pseduo-op ,通常也叫作 assembler instructions, assembler operators, or assembler directives。后一种指令的处理是由汇编器来做的,在iOS中就是由llvm-as来处理的。

pseduo-op 一般都是已 . 开头的,在这里我们可以找到绝大部分的 pseduo-op

因此 .align 5 的意思就是内存已 2^5 = 32 位对齐,也就是4字节对齐。注意这里 .align 后面的数字根据硬件不同是有不同的定义的,大家可以尝试查询上面的文档来了解,但在arm上,一般都 2 的指数。

.globl SYMBOL_NAME(RCTProfileTrampoline)
SYMBOL_NAME(RCTProfileTrampoline):

这个就比较简单了,就是定义一个全局的符号,这里相当于定义了一个全局的函数,名字叫 RCTProfileTrampoline。就是一个专门实现 ProfileTrampoline

// Basic prolog: save the frame pointer and the link register (caller address)
stp fp, lr, [sp, #-16]!
mov fp, sp

看这里注释提到了 prolog,这在汇编函数里面是比较基本的一个概念。他是函数最开始的几行汇编代码,用来保存部分寄存器,成对在函数末尾出现的 epilog,用来恢复 prolog做的事情,并将程序的控制权返回给函数调用者。

苹果的官方文档给出了 prologepilog 的编译器实现。

prolog:

  1. 将LR的值push到栈上
  2. 将FP的值push到栈上
  3. 设置FP的值为SP的值
  4. 将必须要保存的寄存器的值push到栈上
  5. 在栈上为本地存储分配空间

epilog:

  1. 释放为本地存放分配的空间
  2. 从栈上恢复必须要要保存的寄存器的值
  3. 从栈上恢复FP的值
  4. 通过从栈上恢复LR的值到PC,返回函数

prolog 保存了栈指针和调用者的地址,函数执行结束之后能够通过通过保存的这些数据调回函数调用地址,继续往下执行。

/*** Store the value of all the parameter registers (x0-x8, q0-q7) so we can* restore everything to the initial state at the time of the actual function* call*/
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8,     [sp, #(8*16+8*8)]

x0-x8 是用来传递参数的,这个操作就是将参数保存到栈上,具体可以参考 调用栈 部分的图示。

注意 sub sp, sp, #(10*8 + 8*16),sp是被减小的,因为栈是往下生长。

/*** Allocate 16-bytes for the values that have to be preserved across the call* to the actual function, since the stack has to be in the exact initial* state. During its lifetimewe use it to store the initial value of the* callee saved registers we use to point the memory, the actual address of* the implementation and the caller address.*/
mov x0, #0x10
bl SYMBOL_NAME(RCTProfileMalloc)
// store the initial value of r19, the callee saved register we'll use
str x19, [x0]
mov x19, x0

该部分代码就是申请了16个字节大小的内存空间,该空间用来存在两部分信息。

  1. 用来存放 x19 指向的内容,因为后面需要用 x19 来传递一些参数。
  2. 被profile函数的实现。

值得注意的是,前面已经说到 x0-x8 是用来传递参数和获取返回值的,这里可以看到 malloc 的大小通过 x0 传入,返回的地址也是在 x0中。初步看汇编的话,会感觉比较懵逼。

/*** void RCTProfileGetImplementation(id object, SEL selector)** Load the 2 first arguments from the stack, they are the same used to call* this function*/
ldp x0, x1, [sp, #(8*16+0*8)]
bl SYMBOL_NAME(RCTProfileGetImplementation)
str x0, [x19, #0x8] // store the actual function address

获取前两个参数,然后调用 RCTProfileGetImplementation,并把放回的结果放在 x19 后8个字节。

/*** void RCTProfileTrampolineStart(id, SEL) in RCTProfile.m** start the profile, it takes the same first 2 arguments as above.*/
ldp x0, x1, [sp, #(8*16+0*8)]
bl SYMBOL_NAME(RCTProfileTrampolineStart)

在函数开始执行之前,执行 RCTProfileTrampolineStart

// Restore all the parameter registers to the initial state.
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8,     [sp, #(8*16+8*8)]

恢复所有参数

// Restore the stack pointer, frame pointer and link register
mov sp, fp
ldp fp, lr, [sp], #16

以上代码就是 epilog

ldr x9, [x19, #0x8] // Load the function
str lr, [x19, #0x8] // store the address of the callerblr x9 // call the actual function

之前将被 profile 函数的实现放在了 x19 的后半部分,现在取出来执行。

/*** allocate 32-bytes on the stack, for the 2 return values + the caller* address that has to preserved across the call to `free`*/
sub sp, sp, #0x20
str q0, [sp, #0x0] // 16-byte return value
str x0, [sp, #0x10] // 8-byte return value// void RCTProfileTrampolineEnd(void) in RCTProfile.m - just ends this profile
bl SYMBOL_NAME(RCTProfileTrampolineEnd)

执行 RCTProfileTrampolineEnd

整个流程就是用汇编执行 Start => 被profile函数 => End

/*** restore the callee saved registers, move the values we still need to the* stack and free the allocated memory*/
mov x0, x19 // move the address of the memory to x0, first argument
ldr x10, [x19, #0x8] //  load the caller address
ldr x19, [x19] // restore x19
str x10, [sp, #0x18] // store x10 on the stack space allocated above
bl SYMBOL_NAME(RCTProfileFree)// Load both return values and link register from the stack
ldr q0, [sp, #0x0]
ldr x0, [sp, #0x10]
ldr lr, [sp, #0x18]// restore the stack pointer
add sp, sp, #0x20// jump to the caller, without a link
br lr

恢复 x19,然后调回到被调用地址。 br lr是常见的函数返回汇编语句。

特别说明一下,为啥需要用 x19,而不是 x9-x15。因为在这个汇编的实现中,有很多的函数调用,x9-x15是不能保证在函数调用之后还能保持不变的。而子函数调用是一定要保证 x19-x28 不变的,所以可以在调用函数之后,继续使用 x19。这也是为什么在函数返回之前,一定要回复 x19 的原因。

参考文献

  • Trampoline)
  • Application Binary Interface
  • Assembler Language Reference
  • prologue

react-native trampoline实现 1相关推荐

  1. fragment中文网_更新 · React Native 中文网

    时刻将 React Native 更新到最新的版本,可以获得更多 API.视图.开发者工具以及其他一些好东西(译注:官方开发任务繁重,人手紧缺,几乎不会对旧版本提供维护支持,所以即便更新可能带来一些兼 ...

  2. React Native —— App

    使用 React Native 作为 app 框架,Redux 作为数据层,因此我们需要选用一些支持性的技术和工具: 开源的 Parse Server 做数据存储 - 运行在 Node.js 上. F ...

  3. 【React Native】iOS原生导航跳转RN页面

    上一篇介绍了React Native使用react-navigation进行导航跳转页面,现在我们介绍下原生iOS中怎么导航进一个新的React Native页面. 一.原生跳转React Nativ ...

  4. 【React Native】react-navigation导航使用方法

    目录 集成react-navigation 使用react-navigation 上一篇介绍了如何在已有iOS项目中集成React Native.这一篇我们把上一篇的demo做下拓展,添加点击电影跳转 ...

  5. 给iOS开发者的React Native入门使用教程

    目录 一. 原生iOS项目集成React Native 二. 原生跳转RN页面 三. 显示豆瓣热门电影列表 四. 改为导航 五.完整源代码 一. 原生iOS项目集成React Native 创建一个新 ...

  6. 对比React Native、dcloud、LuaView三个框架技术(内部)

    转载自:http://www.jianshu.com/p/ee1cdb33db8d 主要对比React Native和5+SDK(就是dcloud的SDK)两个: 开发语言:三个都是用其他语言来统一开 ...

  7. 初步了解React Native的新组件库firstBorn

    first-born is a React Native UI Component Framework, which follows the design methodology Atomic Des ...

  8. 我在React Native中构建时获得的经验教训

    by Amanda Bullington 通过阿曼达·布林顿(Amanda Bullington) 我在React Native中构建时获得的经验教训 (Lessons I learned while ...

  9. React Native开源项目如何运行(附一波开源项目)

    学习任何技术,最快捷的方法就是学习完基础语法,然后模仿开源项目进行学习,React Native也不例外.React Native推出了1年多了, 开源项目太多了,我们以其中一个举例子.给大家演示下如 ...

  10. React Native常用组件之ListView

    1. ListView常用属性 ScrollView 相关属性样式全部继承 dataSource ListViewDataSource 设置ListView的数据源 initialListSize n ...

最新文章

  1. matlab 神经网络ann用于分类方法
  2. java全面的知识体系结构总结
  3. Django从理论到实战(part13)--include函数
  4. [转]HTTP/3 未来可期?
  5. [转+整理]十道海量数据处理面试题与十个方法大总结
  6. UNIX(多线程):18---异步任务提供者(Provider) 介绍(续)
  7. IDC Q1中国云服务报告:公有云IaaS市场增速持续高于全球
  8. docker Redis集群
  9. 编程之美读书笔记之-高效率的安排见面会
  10. java蓝桥杯算法训练 相l邻字母(题解)
  11. 特斯拉限时赠送4.6万元完全自动驾驶功能,马斯克:市值能涨1000%
  12. YUM只下载软件不安装的设定
  13. distinct效率更高还是group by效率更高?
  14. 手机网页版微信分享以及微信授权
  15. Vue--搭建个人博客
  16. LinuxProbe 0x14 虚拟网站主机功能(基于端口)、Vsftpd服务传输文件、TFTP简单文件传输协议
  17. 新手玩转Linux Kernel漏洞之Null Pointer Dereference
  18. AV1标准特色编码工具简介
  19. mp4box-h264-ftyp-moov-mvhd-thkd-avcC-sps-pps 详解
  20. Glide加载动态图片

热门文章

  1. 线性代数---之正交向量
  2. 这个项目把100多个知名网站都克隆出来了!
  3. PCB走线宽度和电流关系
  4. 【转载】林萧教会你--如何做到招聘要求中的“要有扎实的Java基础”。
  5. 014 方程组的通解 方法一:通解方程组 方法二:基础解析解
  6. 最全白话文git学习教程,最适合入门及深造
  7. Java时间日期格式转换Date转String和String转Date
  8. 三菱R系列PLC程序 全部采用ST语言编写,内部使用函数块和结构体,程序思路清晰
  9. HTML基础篇(2)
  10. 群晖系统共享服务器,群晖共享服务器