源码

深入iOS系统底层之程序中的汇编代码

合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。--(老子·道德经 )

对于一个闭源系统来说如果想研究某些逻辑的内部实现就需要对汇编语言进行掌握和了解、对于某些需要高性能实现的逻辑来说用汇编语言实现可能是最好的选择、对于某些逻辑来说可能只能用汇编来实现。以最后一个能力来说:当我们要实现一个HOOK所有OC方法调用的逻辑时,因为HOOK的方法不能破坏原有函数的参数栈,而且还需要在适当的时候调用原始的函数而不关注原始函数的入参时就只能选择用汇编语言来实现。

查看程序的汇编代码

其实更多的时候我们不要求去编写一段汇编代码或者机器指令,而是如果能够读懂简单的汇编代码就能窥探一些系统底层的实现逻辑和原理。当然市面上也有很多的反汇编的工具软件能够将汇编代码转化为高级语言的伪代码,缺点就是这些工具大多是静态分析工具以及反汇编出来的代码不一定完全正确,有时候我们可能更加希望在运行时去调试或者分析一些问题,这样能够阅读汇编代码的话效果会更好一些。

查看汇编代码的三种方法

Xcode提供了三种查看程序汇编代码的方式:

在程序运行时的断点处可以通过Debug菜单->Debug Workflow->Always Show Disassembly来切换汇编代码模式和高级语言模式。

通过快捷键 alt + command + / 可以对某个系统函数或者第三方库函数或者类的方法设置符号断点,这样当程序出现相应的函数或者方法调用时就会切换到汇编代码模式。你可以通过这种方式来阅读和了解函数或者方法的实现。

如果你想查看某个高级语言文件生成的伪汇编代码时,你需要在对应的文件处通过Product菜单->Perform Action->Assemble "xxxxx" 来查看这个文件生成的伪汇编代码。当你在模拟器模式下所看到的就是x64系统下的汇编代码,当你在设备模式下时所看到的就是arm系统下的汇编代码。

clang命令的简单介绍

通过上述的第三种方式查看生成的汇编代码的方式其实是通过clang命令完成的。clang是一个C/C++/Objective-C语言的编译器,它包含了预处理、语法分析、优化、代码生成、汇编装配、链接等功能。我们通过菜单来进行的构建程序的操作其实内部实现都是借助clang来完成的。你可以在命令终端中键入man clang来查看这个命令的所有参数和使用介绍,你还可以在Xcode工程中使用command + 9快捷键就可以看到你每次构建工程的详细流程,这里面有对程序使用clang命令的进行编译和链接的具体实践。

image.png

可以看出无论是源代码编译还是程序链接都是用clang命令来实现的,不要被命令中大量的编译链接选项所吓倒,其实这些参数都是我们在可视化的工程的Build Settings里面设置的

要想了解完整的编译选项的设置和意义可以参考:pewpewthespells.com/blog/builds…

我们只介绍clang命令的几个主要的参数选项:

  clang  [-arch ] [-x ] [-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 输出文件]

1.常规参数

 -arch : 生成的代码的体系结构,四选一。

 -x

 -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。

2.预处理

 -E 源代码文件  -o 输出文件: 对源代码进行预处理。也就是将所有#include和#import的头文件展开、将所有宏定义展开、将所有枚举值转化为常量值的处理。你可以借助**Product菜单->Perform Action->Preprocess "xxxxx"**来查看一个源代码文件的预处理结果。

3.生成C++代码

 -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  */
                     ~~~~~~~^~~~~
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要是相同的结构体系。

4.生成汇编代码

 -S 源代码文件  -o 输出文件: 要将某个源代码文件生成汇编代码时需要在 -S 参数后面指定源代码文件。而-o 后面的输出文件就是对应的汇编代码文件,一般这个输出文件以.s为扩展名。这里要注意同时使用-arch参数指定输出的体系架构。

5.编译

 -c 源代码文件  -o 输出文件:要编译某个源代码文件时使用这两个参数选项,其中-c后面跟着的是要编译的源代码文件,而-o后面输出的是.o为扩展名的目标文件。

6.链接

 -filelist LinkFileList文件  -o 输出文件: 执行链接时要把所有目标.o文件作为输入参数,但是为了管理方便可以将这些.o文件的路径保存到一个扩展名为.LinkFileList的文件中,然后再使用-filelist 参数后面跟随对应的.LinkFileList文件来指定目标文件集合。而-o后面的输出文件就是对应的可执行程序文件。

工程中引入汇编代码

你也可以在xcode工程中直接引入汇编代码或者使用汇编代码来编写程序和函数,添加汇编文件的方法是:File菜单->New->File...->在列表中选择:Assembly File即可。一般情况下汇编代码都是以.s为扩展名,生成的文件是一个空文件,然后你就可以在文件里面编写对应的汇编代码了。系统也支持在汇编代码中设置断点进行调试。因为iOS系统支持多种体系结构,所以可以在汇编代码中使用几个宏来区分代码是x86_64的还是arm或者arm64的, 就比如下面的代码:

//你可以像高级语言一样通过#include引入头文件。
#include 

//arm体系
#ifdef __arm__

//指令和数据定义

//arm64体系
#elif __arm64__

//指令和数据定义

//x86 32位体系
#elif __i386__

//指令和数据定义

//x86_64位体系
#elif __x86_64__

//指令和数据定义

//其他体系
#else

#endif

当你在项目中添加了一个汇编文件时,就需要掌握和了解汇编代码的编写。关于汇编指令的详细描述由于太过庞大这里就不介绍了,这里主要介绍一些常用的汇编关键字,以便帮助大家能更好的阅读和编写程序。

常见的汇编语法

在Xcode中无论是AT&T还是arm汇编语言的关键字都以.开头。编写汇编代码主要就是数据的定义以及代码指令。一个汇编语言文件中还可以使用和C语言类似的文件引入以及各种预编译指令,还可以引用高级语言中定义的变量和符号以及函数。

1.注释

汇编指令中注释和C/C++/OC相同。arm体系下的汇编代码特有的行注释是代码后面的 ;号注释,而x86_64体系下的汇编代码的特有的行注释是##。

2.节

无论是指令还是数据管理的单位都是节(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可设置的值就是中那些以S_开头的宏定义值。

3.标签和符号

标签是一个可被理解的地址偏移表示,是一个地址的别名。使用标签的目标是为了让程序代码更具有可读性。标签定义后可以在其他指令中引用,也可以在数据变量中被引用。标签的定义规则为:

标签名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语言的函数名称以及全局变量等符号在编译时生成的符号前面添加一个下划线_。所以在高级语言中的名称对应的真实符号都是带一个下划线前缀的,因此一般情况下我们在汇编语言中声明的符号和标签名最好带一个下划线。并且在其他高级语言的声明中不要使用这个下化线,就比如下面的例子:

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  */
                     ~~~~~~~^~~~~
1 warning and 1 error generated.

0

同时在汇编代码中引用高级语言定义的符号时,也要多带上一个下划线前缀。

4.对齐

因为内存寻址访问的一些特性,要求我们的某些代码或者数据的存放地址必须是某个数字的倍数,也就是所谓的对齐。设置对齐的关键字如下:

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  */
                     ~~~~~~~^~~~~
1 warning and 1 error generated.

1

5.宏定义

汇编语言也可以和C语言一样使用宏定义,来做一些代码复用处理。宏定义的语法如下:

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  */
                     ~~~~~~~^~~~~
1 warning and 1 error generated.

2

在使用定义的宏时就直接在相应的地方插入宏的名字即可,如果宏有参数则参数跟在宏名称后面并且参数之间以逗号分隔。下面就是一个宏定义和使用的例子:

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  */
                     ~~~~~~~^~~~~
1 warning and 1 error generated.

3

6.数据的定义

数据的定义类似C语言中变量的定义,汇编代码中也支持多种类型的数据定义。定义一个数据的语法如下:

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  */
                     ~~~~~~~^~~~~
1 warning and 1 error generated.

4

一共有如下的数据类型:

类型描述举例
.byte单个字节.byte 0x10
.long长整型4字节.long 0x10
.quad4倍类型,8字节长度.quad 0x10
.asciz以0结尾的字符串.asciz "Hello world!"
.ascii不以0结尾的字符串.ascii "Hello world!"
.space空字节数,后面跟数量.space 4
.short短整型2字节.short 0x10
(0)

本文由 投稿者 创作,文章地址:https://blog.isoyu.com/archives/shenruiosxitongdicengzhichengxuzhongdehuibiandaima.html
采用知识共享署名4.0 国际许可协议进行许可。除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。最后编辑时间为:5月 27, 2019 at 11:01 下午

热评文章

发表回复

[必填]

我是人?

提交后请等待三秒以免造成未提交成功和重复