合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。--(老子·道德经 )html
对于一个闭源系统来讲若是想研究某些逻辑的内部实现就须要对汇编语言进行掌握和了解、对于某些须要高性能实现的逻辑来讲用汇编语言实现多是最好的选择、对于某些逻辑来讲可能只能用汇编来实现。以最后一个能力来讲:当咱们要实现一个HOOK全部OC方法调用的逻辑时,由于HOOK的方法不能破坏原有函数的参数栈,并且还须要在适当的时候调用原始的函数而不关注原始函数的入参时就只能选择用汇编语言来实现。linux
其实更多的时候咱们不要求去编写一段汇编代码或者机器指令,而是若是可以读懂简单的汇编代码就能窥探一些系统底层的实现逻辑和原理。固然市面上也有不少的反汇编的工具软件可以将汇编代码转化为高级语言的伪代码,缺点就是这些工具大可能是静态分析工具以及反汇编出来的代码不必定彻底正确,有时候咱们可能更加但愿在运行时去调试或者分析一些问题,这样可以阅读汇编代码的话效果会更好一些。c++
Xcode提供了三种查看程序汇编代码的方式:git
经过上述的第三种方式查看生成的汇编代码的方式实际上是经过clang
命令完成的。clang
是一个C/C++/Objective-C语言的编译器,它包含了预处理、语法分析、优化、代码生成、汇编装配、连接等功能。咱们经过菜单来进行的构建程序的操做其实内部实现都是借助clang来完成的。你能够在命令终端中键入man clang
来查看这个命令的全部参数和使用介绍,你还能够在Xcode工程中使用command + 9快捷键就能够看到你每次构建工程的详细流程,这里面有对程序使用clang命令的进行编译和连接的具体实践。github
能够看出不管是源代码编译仍是程序连接都是用clang命令来实现的,不要被命令中大量的编译连接选项所吓倒,其实这些参数都是咱们在可视化的工程的Build Settings
里面设置的objective-c
要想了解完整的编译选项的设置和意义能够参考:pewpewthespells.com/blog/builds…数组
咱们只介绍clang命令的几个主要的参数选项:xcode
clang [-arch <arm|arm64|x86_64>] [-x <objective-c|objective-c++|c|c++|assembler-with-cpp>] [-L<库路径>] [-I<头文件路径>] [-F<框架头文件路径>] [-isysroot 系统SDK路径] [-fobjc-arc | -fno-objc-arc] [-lxxx] [-framework XXX] [-Xlinker option] [-Xlinker value] [-E 源代码文件] [-rewrite-objc 源代码文件] [-c 源代码文件] [-S 源代码文件] [-filelist LinkFileList文件] [-o 输出文件]
复制代码
☞ -arch <arm|arm64|x86_64|i386>: 生成的代码的体系结构,四选一。sass
☞ -x <objective-c|objective-c++|c|c++|assembler-with-cpp: 指定编译的文件的语言,五选一,默认为objective-c。这个选项用在编译阶段。bash
☞ -I<头文件路径>: 指定#import或者#include .h文件的搜索路径。
☞ -L<库路径>: 指定连接时的动态库或者静态库文件的搜索路径。这个选项用在连接阶段。
☞ -F<框架头文件路径>: 指定#import一个框架库时的头文件搜索路径。
☞ -isysroot 系统SDK路径: 指定程序使用的系统框架SDK的路径。好比: -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.1.sdk
代表使用真机版的iOS12.1版本的SDK来编译或者连接当前程序。
☞ -fobjc-arc | -fno-objc-arc: 代表当前程序是使用arc编译仍是mrc来编译。
☞ -lxxx: 只在连接时使用,代表将名字为libxxx的库连接到程序中来。
☞ -framework XXX: 只在连接时使用,代表将名字为XXX的framework库连接到程序中来。
☞ -Xlinker option -Xlinker value: 设置连接的选项,这里必需要成对出现,其意义表示: option = value。
☞ -E 源代码文件 -o 输出文件: 对源代码进行预处理。也就是将全部#include和#import的头文件展开、将全部宏定义展开、将全部枚举值转化为常量值的处理。你能够借助**Product菜单->Perform Action->Preprocess "xxxxx"**来查看一个源代码文件的预处理结果。
☞ -rewrite-objc 源代码文件: 将OC代码转化为对应的C++语言实现。并在源代码文件的当前目录下生成一个对应的后缀为.cpp的C++代码。你能够经过这种方法来详细了解arc的实现原理、block的实现以及调用原理、各类OC关键字的实现逻辑原理、OC类属性和方法的实现逻辑、类方法的定义以及runtime的机制等等逻辑。所以用这个参数能够帮助咱们窥探不少iOS系统的秘密。在使用这个命令时可能会遇到一个常见的错误:
In file included from xxxx.m:9:
xxxx.h:9:29: fatal error: module 'UIKit' not found
#pragma clang module import UIKit /* clang -E: implicit import for #import <UIKit/UIKit.h> */
~~~~~~~^~~~~
1 warning and 1 error generated.
复制代码
这个主要是由于找不到系统SDK的路径文件所致,所以能够带上-isysroot参数来同时指定系统SDK路径。下面就是一个使用的示例:
clang -rewrite-objc -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.1.sdk xxxx.m
复制代码
这里的-isysroot后面的路径要确保是对应系统SDK的路径,同时-arch中的值要和路径中的SDK要是相同的结构体系。
☞ -S 源代码文件 -o 输出文件: 要将某个源代码文件生成汇编代码时须要在 -S 参数后面指定源代码文件。而-o 后面的输出文件就是对应的汇编代码文件,通常这个输出文件以.s为扩展名。这里要注意同时使用-arch参数指定输出的体系架构。
☞ -c 源代码文件 -o 输出文件:要编译某个源代码文件时使用这两个参数选项,其中-c后面跟着的是要编译的源代码文件,而-o后面输出的是.o为扩展名的目标文件。
☞ -filelist LinkFileList文件 -o 输出文件: 执行连接时要把全部目标.o文件做为输入参数,可是为了管理方即可以将这些.o文件的路径保存到一个扩展名为.LinkFileList的文件中,而后再使用-filelist 参数后面跟随对应的.LinkFileList文件来指定目标文件集合。而-o后面的输出文件就是对应的可执行程序文件。
你也能够在xcode工程中直接引入汇编代码或者使用汇编代码来编写程序和函数,添加汇编文件的方法是:File菜单->New->File...->在列表中选择:Assembly File便可。通常状况下汇编代码都是以.s为扩展名,生成的文件是一个空文件,而后你就能够在文件里面编写对应的汇编代码了。系统也支持在汇编代码中设置断点进行调试。由于iOS系统支持多种体系结构,因此能够在汇编代码中使用几个宏来区分代码是x86_64的仍是arm或者arm64的, 就好比下面的代码:
//你能够像高级语言同样经过#include引入头文件。
#include <xxx.h>
//arm体系
#ifdef __arm__
//指令和数据定义
//arm64体系
#elif __arm64__
//指令和数据定义
//x86 32位体系
#elif __i386__
//指令和数据定义
//x86_64位体系
#elif __x86_64__
//指令和数据定义
//其余体系
#else
#endif
复制代码
当你在项目中添加了一个汇编文件时,就须要掌握和了解汇编代码的编写。关于汇编指令的详细描述因为太过庞大这里就不介绍了,这里主要介绍一些经常使用的汇编关键字,以便帮助你们能更好的阅读和编写程序。
在Xcode中不管是AT&T仍是arm汇编语言的关键字都以.开头。编写汇编代码主要就是数据的定义以及代码指令。一个汇编语言文件中还可使用和C语言相似的文件引入以及各类预编译指令,还能够引用高级语言中定义的变量和符号以及函数。
汇编指令中注释和C/C++/OC相同。arm体系下的汇编代码特有的行注释是代码后面的 ;号注释,而x86_64体系下的汇编代码的特有的行注释是##。
不管是指令仍是数据管理的单位都是节(Section)。由于在iOS系统的mach-o文件格式中的数据和指令的存储都是以段(Segment)和节为单位划分的。任何代码和数据老是在某个节内被定义。每一个节都归属于某个段,每一个节有一个惟一的名字。节定义的关键字和语法以下:
.section <段名>,<节名>,<节属性>
复制代码
相同的段名和节名能够出如今多出,数据和代码都是定义在由.section指定的节下开始,并结束于下一个节的定义开始处。系统最终在生成代码时会将相同的段名和节名的内容统一汇总到一块儿存储。通常状况下全部的指令代码都是在__TEXT段下的节中被定义,而数据定义则是在__DATA段下的节中被定义。若是汇编代码中不指定节名则数据和代码默认是在__TEXT,__text
下。系统还提供了两个简化代码段和数据段的节定义关键字。
//代码段的定义,等价于 .section __TEXT,__text
.text
//数据段的定义,等价于 .section __DATA,__data
.data
复制代码
在反汇编代码中的节定义中除了指定名称外你还会看到一些好比:regular,pure_instructions,no_dead_strip,cstring_literals
等等节定义的属性。这些属性所表明的意义和mach-o文件格式中的结构体struct section_64
中的flags
字段所表示的意义一致。flags
可设置的值就是<mach-o/loader.h>
中那些以S_开头的宏定义值。
标签是一个可被理解的地址偏移表示,是一个地址的别名。使用标签的目标是为了让程序代码更具备可读性。标签订义后能够在其余指令中引用,也能够在数据变量中被引用。标签的定义规则为:
标签名1:
//代码和数据
标签名2:
//代码和数据
复制代码
标签能够当作是一个文件中的局部指针变量,对于数据段中定义的标签一般用来当作访问变量的地址,而对于代码段中定义的标签一般用来作指令跳转用。好比下面的代码:
//x86_64中的代码
.data
AGE: //标签的定义处
.long 13
.text
LAB1: //标签的定义处
mov AGE(%rip), %rax //标签的使用处
jmp LAB1 //标签的使用处
复制代码
有的时候还能够定义方向标签,方向标签只能是数字,而后能够在使用这些方向标签时,在方向标签后面带一个b代表跳转到当前指令前面定义的某个最近的方向标签,而方向标签后面带一个f代表跳转到当前指令后面定义的某个最近的方向标签。就好比下面演示的代码:
//x86_64中的演示代码,这里面定义了方向标签,同时也有如何跳转到这些方向标签的使用方法。
.text
mov %rax, %rax
1: //a
mov %rax, %rax
2: //b
mov %rax, %rax
2: //c
mov %rax, %rax
jmp 2b //跳转到c处
jmp 1b //跳转到a处
jmp 1f //跳转到d处
1: //d
mov %rax, %rax
复制代码
标签只是文件内地址偏移的别名,只能在定义的文件内部引用。要想让这个标签被外部引用和访问就须要将标签声明为符号。高级语言文件中定义的能被外部访问的函数和全局变量其实都是一个符号,不论是函数地址仍是全局变量的内存地址,其实都是一个地址位置,而地址的别名则是能够用标签表示,所以要想将一个标签订义为外部可访问,就须要将标签名声明为符号。就如高级语言中的静态函数和静态变量以及全局函数和全局变量同样,汇编语言中的符号声明也有两种:
//对外可见的全局符号,能够被外部程序引用和访问。
.global 全局符号名
全局符号名:
//私有外部符号,只在程序内可引用和访问。
.private_extern 私有外部符号名
私有外部符号名:
复制代码
符号名要和标签名匹配。由于C语言的函数名称以及全局变量等符号在编译时生成的符号前面添加一个下划线_。因此在高级语言中的名称对应的真实符号都是带一个下划线前缀的,所以通常状况下咱们在汇编语言中声明的符号和标签名最好带一个下划线。而且在其余高级语言的声明中不要使用这个下化线,就好比下面的例子:
//xxx.s
//在数据段中定义一个全局变量符号_testSymbol。
.data
.global _testSymbol
_testSymbol:
.int 10
.............................................
//xxx.m
//高级语言中声明使用这个符号。
extern int testSymbol;
int main(int argc, char *argv[])
{
printf("testSymbol = %d",testSymbol);
return 0;
}
复制代码
同时在汇编代码中引用高级语言定义的符号时,也要多带上一个下划线前缀。
由于内存寻址访问的一些特性,要求咱们的某些代码或者数据的存放地址必须是某个数字的倍数,也就是所谓的对齐。设置对齐的关键字以下:
//代表此处的地址是(2^3)8的倍数。这里面p2align貌似和align所表达的意义类似,不知道为何会有两个关键字。
.align 3
.p2align 3
复制代码
汇编语言也能够和C语言同样使用宏定义,来作一些代码复用处理。宏定义的语法以下:
//宏的开始
.macro 宏名称
//这里面能够编写任何其余的汇编代码和关键字
// 宏能够带参数,宏内使用参数老是从$0开始。
//宏的结束
.endmacro
复制代码
在使用定义的宏时就直接在相应的地方插入宏的名字便可,若是宏有参数则参数跟在宏名称后面而且参数之间以逗号分隔。下面就是一个宏定义和使用的例子:
//宏定义
.macro Test
mov x0, $0
mov x1, $1
.endmacro
//宏使用
Test 10,20
复制代码
数据的定义相似C语言中变量的定义,汇编代码中也支持多种类型的数据定义。定义一个数据的语法以下:
.<数据类型> 值
复制代码
一共有以下的数据类型:
类型 | 描述 | 举例 |
---|---|---|
.byte | 单个字节 | .byte 0x10 |
.long | 长整型4字节 | .long 0x10 |
.quad | 4倍类型,8字节长度 | .quad 0x10 |
.asciz | 以0结尾的字符串 | .asciz "Hello world!" |
.ascii | 不以0结尾的字符串 | .ascii "Hello world!" |
.space | 空字节数,后面跟数量 | .space 4 |
.short | 短整型2字节 | .short 0x10 |
数据类型的值能够是一个常量也但是一个表达式,也能够是一个标签符号。若是咱们想给某个数据定义指定一个相似于变量的名称,则能够和标签来结合。好比:
name:
.asciz "欧阳大哥"
age:
.long 13
nickname:
.quad name //这里的昵称变量是一个指针代表和name是相同的。
复制代码
若是要想在代码块中访问上面定义了标签名的变量,则能够采用以下指令:
//x86体系的指令访问符号变量
leaq name(%rip), %rax
movl age(%rip), %ebx
movq nickname(%rip), %rcx
//arm64体系的指令访问符号变量
adrp x0, name@PAGE
add x0, x0, name@PAGEOFF
adrp x1, age@PAGE
add x1, x1, age@PAGEOFF
ldr x1, [x1]
adrp x2, nickname@PAGE
add x2, x2, nickname@PAGEOFF
复制代码
汇编语言中并无专门用于函数定义的关键字,汇编语言中只有代码块的定义,全部可执行的代码块都存放在代码段中。所谓函数调用其实就是调用函数代码对应的首地址。所以对于文件内的函数调用其实能够借助标签来完成,而其余文件对函数的调用则能够借助符号来完成。对于函数中的参数部分的处理则是按照函数调用参数传递的ABI规则来指定,具体详情能够参考个人深刻iOS系统底层之CPU寄存器介绍中的介绍。
下面就是一个求两个参数和的加法函数在x86_64位体系结构下的实现:
//x86_64位下的函数实现
.text
.global _add
.align 3
_add:
movq %rdi,%rbx
movq %rsi,%rax
addq %rbx,%rax
ret
LExit_add:
复制代码
关于在汇编语言中编写指令这里就不赘述了,不然一本书也说不完,你们能够参考相关的汇编代码的书籍便可,最好的方法是阅读CPU体系结构手册:
汇编语言有相应的进行比较和跳转的指令,可是咱们仍然能够借助伪条件语句来使得咱们的代码更加具备可读性。伪条件语句的语法以下:
.if 逻辑表达式
.elseif 逻辑表达式
.else
.endif
复制代码
这部分伪指令以.cfi开头。主要用来记录函数的帧栈信息和用于异常处理。具体的指令介绍请参考:blog.csdn.net/permike/art…
由于汇编代码源文件没有所谓的.h头文件声明。因此当你在其余文件中要想使用汇编语言中定义的函数或者全局变量时,能够在你的源代码文件的顶部进行符号使用的声明:
//xxxxx.m
//函数声明
extern void 不带下划线的函数符号(参数列表);
//变量使用声明
extern 类型 不带下划线的变量符号;
复制代码
咱们还能够在高级语言中嵌入汇编代码,嵌入的主要目的是为了优化代码的性能,还有一些高级语言完成不了能力好比获取当前执行指令的地址以及读取一些状态寄存器和特殊寄存器的值,还有一些场景甚至能够用汇编代码来解决高级语言须要用锁来解决的多线程的问题等等。具体的嵌入方法和规则我这里就偷一下懒,直接访问这个连接:
就能够很清楚的知道嵌入的规则了,这篇文章已经介绍得很仔细了。下面我将举3个具体的例子:
//计算两个数相加
long add(long a, long b)
{
long c = 0;
#if __arm64__
__asm__(
"ldr x11, %1\n"
"ldr x12, %2\n"
"add %0, x11, x12\n"
:"=r"(c)
:"m"(a),"m"(b)
);
#elif __x86_64__
__asm__(
"movq %1,%%rdi\n"
"movq %2,%%rsi\n"
"addq %%rdi,%%rsi\n"
"movq %%rsi,%0\n"
:"=r"(c)
:"m"(a),"m"(b)
);
#else
c = a + b;
#endif
return c;
}
复制代码
//打印当前指令的地址以及当前线程ID
void foo()
{
unsigned long pc = 0;
unsigned long threadid = 0;
#if __arm64__
//arm64限制了直接读写PC寄存器的方式,而是改动相对偏移
//TPIDRRO_EL0是指内核中的线程ID,用专门的指令mrs来读取
__asm__(
"adr x0, #0\n"
"stur x0, %0\n"
"mrs %1,TPIDRRO_EL0\n"
:"=m"(pc),"=r"(threadid)
);
#elif __x86_64__
//x86体系的CPU没有专门的寄存器保存线程ID
__asm__(
"leaq (%%rip), %%rdi\n"
"movq %%rdi, %0\n"
:"=m"(pc)
);
#else
NSAssert(0, @"oops!");
#endif
NSLog(@"pc=%ld, threadid=%ld",pc, threadid);
}
复制代码
ldp,stp
两个条指令来实现指令级别上的原子操做,由于无需加锁从而达到最佳的性能。//假设x,y变量保存在全局变量critical数组中。
long critical[2];
void read(long *px, long *py)
{
#if __arm64__
__asm__(
"ldp x9, x10, %2\n"
"stur x9,%0\n"
"stur x10,%1\n"
:"=m"(*px),"=m"(*py):"m"(critical)
);
#else
//其余体系结构在读取时必需要加锁处理。
*px = critical[0];
*py = critical[1];
#endif
}
void write(long x, long y)
{
#if __arm64__
__asm__(
"stp %1, %2, %0":"=m"(critical):"r"(x),"r"(y)
);
#else
//其余体系结构在写入两个变量时必需要加锁处理。
critical[0] = x;
critical[1] = y;
#endif
}
复制代码
👉【返回目录】
欢迎你们访问个人github地址