工欲善其事必先利其器 --《论语·卫灵公》git
一个好的IDE不只要提供温馨简洁和方便的源代码编辑环境,还要提供功能强大的调试环境。XCODE是目前来讲对iOS应用开发支持的最好的IDE(虽然Visual Studio2017也开始支持iOS应用的开发了),毕竟XCODE和iOS都是苹果公司的亲生儿子。惟一要吐槽的就是系统和编译环境绑的太死了,每当手机操做系统的一个小升级,都须要去升级一个好几G的新版本程序,这确实是有点坑爹! 目前市面上有不少反编译的工具,好比IDA、Hopper Disassembler等还有操做系统自带的工具诸如otool、lldb。这些工具里面有的擅长静态分析有的擅长调试的,这里就不展开分析了。若是在程序运行时去窥探一些系统内部实现以及作实时调试分析我以为XCODE自己也很是的棒,既然深刻系统咱们必需要了解和学习一些关于汇编的东西,那么就必需要了解和掌握一些工具,而XCODE其实就是你手头上最方便的工具之一。程序员
你是否在联机运行时由于系统崩溃而出现过以下的画面:github
不要慌!它其实就是XCODE的汇编模式的界面。咱们不只在程序崩溃时能够看到它,咱们也能够人为的进入到这个界面模式里面。这篇文章更像是一个XCODE工具使用上的一些介绍,您能够常常在使用它们,也可能还历来没有接触和了解过它们。对于汇编代码和源代码之间的切换能够经过菜单:Debug -> Debug Workflow -> Always Show Disassembly 来完成。编程
记得要设置有断点并运行到断点处时切换才能看到汇编指令啊!数组
上一篇文章深刻iOS系统底层之指令集介绍中咱们有说过模拟器上运行的是Intel指令,而真机上运行的是arm指令,在这里咱们分别看模拟器和真机下的汇编指令的差别性: 缓存
经过上面三张图你会发现其中的源代码和汇编代码之间有很大的差别,以及不一样指令集下的汇编代码之间也有很大的差别!汇编代码的差别其实就是不一样CPU上运行的指令的差别。还记得前一篇文章所说的指令集吗?前者是在模拟器上运行的因此展现的是x64的指令,然后者是在真机上运行的所以展现的是arm64指令。经过图片对比你可否发现他们之间的相同点和差别吗?sass
可能有的同窗会说为何我打开了汇编模式我仍是看不到汇编代码?那是由于你没有给你的代码设置断点!什么是断点?为何设置了断点程序就会暂停运行? 通常状况下CPU老是按照顺序依次执行指令并完成任务,当正在执行某个任务时若是遇到了特殊事件或者更高优先级的任务时就须要打断现有执行的代码并去执行优先级更高的代码,这种机制就是中断。中断有由于外部硬件设备事件而产生的硬中断, 同时CPU也提供一个软中断指令。当在代码里面执行一条软中断指令时,程序就会暂停运行,同时CPU把操做权限提交给操做系统来执行中断处理程序。当咱们在程序某处设置了断点或者某个指令处设置断点时,系统会将断点处的指令保存到一个临时的断点列表中,同时将断点处的指令替换为软中断指令,这样当程序运行到断点处时由于执行的实际上是软中断指令,而致使系统调用的发生,并执行软中断处理程序,软中断处理程序等待用户处理断点处的操做,好比当用户按下的是键盘上的Ctrl + F7时,软中断处理程序就会把保存在临时断点列表中真实断点处的指令恢复到指定的内存,同时把下次要执行的指令改成真实的指令,而后再次执行真实的指令,这样就完成了断点处指令的继续执行。(要想了解断点的具体实现,须要具备一些汇编的知识,这里就不展开了,后面我会在专门的章节里面详解介绍断点的实现原理)。bash
当咱们在程序代码某处设置了断点或者指令某处设置了断点后,程序执行到断点处时就会暂停下来。这时候若是咱们是在汇编模式下,您看到的就是汇编程序断点,而当你在源代码模式下时,你看到的将是源代码断点。 除了在代码处设置断点外咱们还能够设置符号断点。咱们先来考察下面3个应用场景:多线程
咱们程序的某个视图的frame值在运行时不知道什么缘由老是被莫名其妙的改变了,可是你就是不知道在哪里执行了视图frame的更改设置。这时候一个解决方法就是重载setFrame方法并设置断点来调试查看frame被什么时候调用。app
咱们的上线程序出现了在某个系统方法被调用时的crash问题,可是由于是系统的方法咱们没法看到其中的源代码,从而没法进行crash问题分析(好比咱们遇到的不少没有上下文的crash).
假如我懂汇编语言,我想研究一下系统框架的某个方法是如何实现的。
上面的三个问题我不知道你们会如何去解决? 其实这三种场景咱们均可以借助于符号断点来完成。通常状况下咱们能够在源代码某处设置断点来调试程序,对于没有源代码的状况下咱们则能够经过设置符号断点来实现程序的调试和运行。要设置符号断点很简单。你只须要在XCODE的菜单:Debug -> Breakpoints -> Create Symbolic Breakpoint 或者快捷键:option + command + \ 来创建符号断点:
创建符号断点后,当某个与符号名相同某个函数或者方法在执行开始前就会产生断点,从而能够窥探某个方法的内部实现。还能够帮助咱们对那些没有上下文以及非源代码处产生的崩溃进行分析和重现,从而帮助咱们定位问题。下面是运行符号断点后的咱们看到的两处符号断点的汇编语言内容:
VCTest1`-[ViewController setA:]:
-> 0x1029855e0 <+0>: sub sp, sp, #0x20 ; =0x20
0x1029855e4 <+4>: adrp x8, 4
0x1029855e8 <+8>: add x8, x8, #0x70 ; =0x70
0x1029855ec <+12>: str x0, [sp, #0x18]
0x1029855f0 <+16>: str x1, [sp, #0x10]
0x1029855f4 <+20>: str w2, [sp, #0xc]
0x1029855f8 <+24>: ldr w2, [sp, #0xc]
0x1029855fc <+28>: ldr x0, [sp, #0x18]
0x102985600 <+32>: ldrsw x8, [x8]
0x102985604 <+36>: add x8, x0, x8
0x102985608 <+40>: str w2, [x8]
0x10298560c <+44>: add sp, sp, #0x20 ; =0x20
0x102985610 <+48>: ret
-----------------
libsystem_c.dylib`abs:
-> 0x1813dd984 <+0>: cmp w0, #0x0 ; =0x0
0x1813dd988 <+4>: cneg w0, w0, mi
0x1813dd98c <+8>: ret
复制代码
你是否看到了属性setA的内部实现以及函数abs的内部实现了?
调试程序是一个程序员应该掌握的最基本的工夫,这里就不介绍其余的详细的调试命令以及方法,其余不少文章里面都有介绍了。主要介绍一下调试代码时单步运行的几个菜单和快捷键:
F7 : 代码单步执行,当遇到函数调用时会跳入函数内部。
F6: 代码单独执行,当遇到函数调用时不会跳入函数内部。
F8: 跳出函数执行,返回到调用此函数的下一句代码。
复制代码
control + F7 : 指令单步执行,当遇到函数调用时会跳入函数内部。
control + F6: 指令单独执行,当遇到函数调用时不会跳入函数内部。
复制代码
control + shift + F7: 切换到当前线程,并执行单步指令。
control + shift + F6: 切换到当前线程,并跳转到函数调用的者的下一条指令。
复制代码
在调试运行时当出现断点时咱们能够在lldb命令行中输入各类调试命令,其余的不介绍,就单独介绍一下expr命令。expr命令实际上是p或者po的完整版本,经过expr命令除了可以用来显示外,还能够用来进行数据的修改、方法的调用等强大能力。下面展现一下一些经常使用的expr方法:
expr 变量|表达式 //显示变量或者表达式的值。
expr -f h -- 变量|表达式 //以16进制格式显示变量或表达式的内容
expr -f b -- 变量|表达式 //以二进制格式显示变量或者表达式的内容。
expr -o -- oc对象 //等价于po oc对象
expr -P 3 -- oc对象 //上面命令的增强版本,他还会显示出对象内数据成员的结构,具体的P后面的数字就是你要想显示的层次。
expr my_struct->a = my_array[3] //给my_struct的a成员赋值。
expr (char*)_cmd //显示某个oc方法的方法名。
expr (IMP)[self methodForSelector:_cmd] //执行某个方法调用.
复制代码
程序运行时,操做系统为其构建出一个进程,同时构建出一个虚拟的内存空间。操做系统将进程中的虚拟内存空间划分为代码存储区域、全局数据存储区域、堆存储区域、栈存储区域等区域。每种区域都有特殊的用途:代码存储区域保存的是程序中的代码部分(这部分也可称为映像image);全局数据存储区域保存的是一些全局数据、常量以及一些描述信息(好比runtime里面的全部OC类的定义描述信息也是存储在这个区域中);堆存储区域则用来进行堆内存的动态分配;栈存储区域则保存着函数中的局部变量。所以能够看出不管是代码和数据在运行时都保存在内存中。每一个进程能访问的内存空间的尺寸大小由操做系统决定,通常来讲32位的操做系统中每一个进程的内存空间为2^32 = 4GB;而64位的操做系统中每一个进程的内存空间为2^64 = 4TB。须要注意的是这个空间是虚拟的可访问空间并非真实的物理内存可访问的空间,操做系统内部经过分页映射的方式将虚拟空间转化为真实的物理空间。 进程的虚拟内存空间是一个能够连续存储和访问的线性空间,为了可以访问这些内存空间,操做系统为其进行了编码,这个编码就是内存的地址。地址也被称为指针,所以咱们所说的某个变量的指针其实就是这个变量在内存中的地址。为了更好的理解内存和地址的概念,你能够将内存理解为一个数组,而地址则是访问这个数组元素时所用到的索引。咱们对数组中元素的读写操做老是经过索引进行,一样CPU对内存中的数据访问时也是经过内存地址进行的。进程中的内存地址老是从0开始编码,并以字节为单位进行递增,直到虚拟内存空间的上限。 上面说过进程中的代码和数据都保存在内存中,当咱们要想一览整个进程内存中的代码和数据时,你能够在程序运行时经过菜单:Debug -> Debug Workflow -> View Memory 或者经过快捷键:shift+command + m 来调用内存查看界面:
上面的图片恰好展现的是一个类的全部方法名称在内存中的位置和布局。能够看出咱们能够很方便的借助查看内存地址菜单的功能来了解以及分析代码以及数据在内存中的结构。你能够在地址输入栏中输入你想查看的任意内存地址。好比你想查看某个函数代码的机器指令,那么你只须要在汇编模式下将函数最开始的地址输入到内存查看界面的地址栏中,那么就会展现出这个函数代码的全部机器指令字节码。这里还要注意一点的是由于内存地址是从低位按字节依次排列而来,因此对于好比int类型的值的读取咱们就要从高位到低位开始读取。
程序调试时代码和地址以及一些数据都常常以16进制的形式显示。数据处理时,尤为是计算地址偏移都以16进制的形式进行展现。你能够在lldb中经过expr或者p命令来计算。若是你喜欢界面形式的工具,则能够启动mac OS操做系统中的应用:计算器 来处理各类计算,你要作的就是在显示菜单中选择编程型便可,编程型界面的效果以下(别告诉我做为一个程序员的你不会操做这些功能):
若是你喜欢命令行的方式来作计算,那么还能够介绍给你一个系统提供的命令式计算工具:bc。这个工具的官方定义是:一个任意精度计算器语言(An arbitrary precision calculator language)。咱们能够以交互的方式进入bc: bc -i
使用bc时你能够经过ibase = [2|8|10|16]的值来指定输入数字的进制,能够经过指定obase=[2|8|10|16]的值来指定输出数字的显示格式。你还能够经过scale=n来指定输出的小数位数,你能够在里面用表达式、函数、运算符、甚至能够定义变量和函数。能够看出bc可不是只有计算的功能这么简单,你能够用bc来编写程序!!具体bc的使用你能够在终端下执行 man bc
查看bc的使用手册。下面是一段用bc语言写的代码(请在执行了bc -i 命令后编写以下代码):
sum = 0
for (i = 0; i < 100; i++)
{
sum += i
}
sum
复制代码
👉【返回目录】
欢迎你们访问个人github地址