汇编基础
一、程序的本质
1. 程序的执行过程
如下图所示,执行软件的时候,会将软件从硬盘装载到内存中,然后由CPU控制内存读与写,同时也会控制计算机的其他设备进行响应
2. CPU
如下图所示,CPU包括寄存器、运算器、控制器,寄存器负责信息存储,运算器负责信息处理,控制器负责分析指令并发出相应的控制信号
二、寄存器和内存
- 在软件执行过程中,CPU一般会将内存中的数据放到寄存器中,在对寄存器的数据进行运算,如下图的例子
-
假设内存中有块红色的内存空间存放着3,现在想,把3加1,并将结果存放到蓝色内存空间中,让我们来看看CPU是怎么做的:
(1) . 首先CPU会将红色的内存中的值放到寄存器RAX中,对应的汇编语句是这样的:
movq 红色空间地址,%rax
(2) . 然后让rax寄存器与1相加,对应的汇编语句是这样的:
addq $0x1,%rax
-
(3) . 最后将值赋值给蓝色内存空间,对应的汇编语句是这样的:
movq %rax,蓝色内存空间的地址
(PS:汇编代码看不懂没关系,学完下面的就会了,一会可以回过头来再看看)
三、编程语言的发展
最开始是机器语言,由0和1构成
为了方便记忆,人们用符号代替了0和1,这就是汇编语言
为了更接近人类自然语言,又发明了高级语言来代表符号,例如C、C++、Python、JAVA、OC、Swift、JS等等
-
举个例子,如果我们想将寄存器bx的值放到寄存器ax中,用三种语言表示如下,是不是感觉到越来越容易阅读了呢
(1) . 机器语言:10001000100010001
(2) . 汇编语言:movw %bx,%ax
(3) . 高级语言:ax = bx;
-
本例子中高级语言执行过程,如下图所示,从高级语言编译成汇编,再编译成机器语言才能被计算机所执行
总结:
汇编语言和机器语言一一对应,从汇编语言可以编译成机器语言,从机器语言也可以反编译成汇编语言,是一对一的关系
高级语言可以通过编译得到汇编语言,但是汇编语言几乎不可能反编译成高级语言,因为这是一对多的关系,多个高级语言有可能编译成同一条汇编语言。
汇编语言的种类非常多,例如8086汇编、x86汇编、x64汇编等等,汇编语言大部分都非常相似,学会一种,其他的自然也能看懂了 。在iOS领域,最主要的汇编语言就两种:AT&T汇编对应着iOS模拟器,ARM汇编对应着iOS真机,以下重点讲述AT&T汇编 。
四、常见汇编指令
- AT&T汇编中有16个常见的寄存器:
rax、rbx、rcx、rdx、rsi、rdi、rbp、rsp ; r8、r9、r10、r11、r12、r13、r14、r15
- AT&T汇编中有16个常见的寄存器:
-
- 寄存器是CPU内部存放数据的地方,常见的用途如下:
1>.
rax、rdx
寄存器中经常存放函数的返回值2>.
rdi、rsi、rdx、rcx、r8、r9
等寄存器经常用于存放函数的参数3>.
rsp、rbp
经常用于栈操作4>.
rip
一般存放指令指针,存储着CPU下一条要执行的指令的地址
- 下图是两种汇编语言的常用指令,格式非常相似,大家学会一种,另一种自然也就懂了,下面我来详细解释一下这张图的意思
1>. 第一行是寄存器名称的命名规范,在AT&T中需要加上%,在Intel中不需要加,rax就代表寄存器的名称
2>. 第二行是操作顺序,AT&T中,是将左边的rax中的值赋值给右边rdx中;Intel中,是将右边的值赋值给左边,两者顺序不同而已。
3>. 第三行是常数的赋值,将3赋值给寄存器,AT&T是把3放到左边,Intel是把3放到右边
4>. 第四行是
movq $0xa,0x1ff7(%rip)
,将0xa赋值给地址为rip+0x1ff7
的内存空间中,在AT&T中,内存地址的加法是用0x1ff7(%rip)
表示,意思是将rip中存放的内存地址与0x1ff7相加;在Intel中的写法为[rip+ox1ff7]
5>. 第五行
leqp -0x18(%rbp),%rax
,是取内存地址,将rbp寄存器中的地址减去0x18后得到的值,赋值给rax寄存器中6>. 第六行
jmp
是跳转指令,CPU会直接跳转到目标地址去执行-
7>. 大家应该发现了AT&T汇编总比Intel汇编多一位,例如:
AT&T是movl,Intel就是mov
, AT&T汇编的最后一位代表操作数,意思是要操作多少个字节的空间。- 例如
movl $3,%rax
,这句汇编的最后一位l
,是long的缩写,代表操作数的长度是4个字节,要操作4字节的空间,将来要存放3的时候,CPU就分配4个字节的寄存器空间用来存放3
- 例如
注意: movq和leaq的区别:
-
movq 是根据内存地址找出数据后,再将数据赋值给寄存器。
- 例如:
mov -0x18(%rbp),%rax
,就是拿rbp寄存器存的地址减去 0x18 得到的地址,再从这个地址取出具体数据,存放到rax寄存器中
- 例如:
-
leaq 是将算出来的地址值,直接给寄存器。
- 例如:
leaq -0x18(%rbp),%rax
,就是将rbp寄存器存的地址减去0x18得到的地址,直接存放到rax寄存器中
- 例如:
注意: jmp和call的区别:
-
jmp跳到某个地址后,中间的指令就不执行了。
- 例如:刚开始执行在0x00000001处,执行
jmp 0x00000008
后,就直接跳转到目标指令了,中间的指令就跳过不执行了
- 例如:刚开始执行在0x00000001处,执行
-
call指令一般都是执行某个函数,经常与ret指令配合使用,会在函数执行完后返回到函数调用处
- 例如:
call *%rax
调用寄存器rax中存的地址(*代表间接调用,函数地址可能是动态的),调用结束后,会返回到调用函数的这一行指令
- 例如:
五、寄存器
- 寄存器是用来存放数据的,肯定需要存储空间才行,寄存器的大小和CPU有关,你是32位的CPU,那么你的寄存器也都是32位的
-
寄存器也是逐渐发展的,从开始的1个字节的寄存器,慢慢发展为2个字节的,4个字节的,8个字节的。
-
为了兼容以前的寄存器,8个字节的寄存器会分出来4个字节当做4个字节的寄存器,如下图,为了兼容2个字节的寄存器,4个字节的寄存器又拿出最低的2个字节作为2个字节的寄存器,这样做,8位的寄存器,就能兼容之前的4个字节的寄存器、2个字节的寄存器、1个字节的寄存器了。
-
-
-
- 在AT&T汇编中:
r开头的寄存器都是占8个字节,也就是这个寄存器可以存储8个字节的数据,例如rax、rbx、rcx等寄存器;
以e开头的寄存器占4个字节,也就是这个寄存器可以存储4个字节的数据;
ax、bx、cx寄存器占2个字节,也就是这个寄存器可以存储2个字节的数据;
ah、al、bh、bl寄存器占用1个字节,也就是这个寄存器可以存储1个字节的数据;
-
- 我们来看两句汇编代码巩固一下以上知识
- 1>.
movq $0xa,%rax
,这句汇编的意思是将16进制0xa(也就是十进制的10)存放到rax寄存器中,操作数是q,代表操作 8 个字节的空间,所以寄存器rax中的数据就应该是0xa 0x0 0x0 0x0 0x0 0x0 0x0 0x0,如下图所示,由于rax、eax、ax、ah等寄存器共用一段存储空间,所以表面是你修改了rax寄存器,实际上eax、ax、ah等寄存器的值全都变成了0x0了
- 2>.
movl $0xa,%eax
,这句汇编的意思是将16进制0xa存放到eax寄存器中,操作数是l,代表操作 4 个字节的空间,所以寄存器eax中的数据就应该是0xa 0x0 0x0 0x0,由于与rax寄存器共用存储空间,表面是是修改了eax寄存器,实际上rax寄存器也被修改了
六、AT&T汇编语言的常用技巧(这些技巧在以后分析汇编的时候会经常用到,建议熟记)
- 1. 0x712a(%rip) 一般都是全局变量,全局区 (这句汇编的意思是:rip寄存器的值 + 0x712a)
- 2. -0x10(%rbp) 一般都是局部变量,栈空间 (这句汇编的意思是:rbp寄存器的值 - 0x10)
- 3. 0x10(%rax)一般是堆空间 (这句汇编的意思是:rax寄存器的值 + 0x10)
- 4. rax、rdx寄存器一般存储函数返回值
- 6. rdi、rsi、rdx、rcx、r8、r9等寄存器常用来存放函数参数
- 7. rsp、rbp常用于存放栈操作
- 8. rip作为指令指针,存储着CPU下一条要执行的指令的地址,一旦CPU读取一条指令,rip会自动指向下一条指令
七、LLDB常用调试命令(加粗标红的命令以后会经常用,建议熟记)
-
读取某个寄存器的值:
register read/格式
,例如:register read/x rax
-
读取某个寄存器的值:
-
修改某个寄存器的值:
register write 寄存器名称 数值
,例如:register write rax 0
-
修改某个寄存器的值:
-
读取某个内存地址中的值:
x/数量-格式-字节大小 内存地址
,例如:x/3xw 0x0100010
,这句命令的中的第一个x代表读取内存,3代表3组,第二个x代表以16进制的格式,w代表4个字节,所以意思就是从0x0100010
地址开始,以16进制的格式输出3组4个字节的内存数据,也就是以16进制的格式输出12个字节的数据。
-
读取某个内存地址中的值:
- 修改内存中的值:memory write 内存地址 数值,例如:
memory write 0x100010
- 修改内存中的值:memory write 内存地址 数值,例如:
-
上述命令中的的格式补充 :x代表16进制,f是浮点,d是十进制
上述命令中字节大小补充:b - byte - 1字节,h - half word - 2字节,w - word - 4字节,g - giant word - 8字节
-
- 源码单步运行,子函数当做一个整体:next或者n
- 源码单步运行,遇到子函数会进入子函数:step或者s
- 汇编单步运行,子函数当做一个整体:nexti或者ni
-
汇编单步运行,遇到子函数会进入子函数:
stepi或者si
-
汇编单步运行,遇到子函数会进入子函数:
- 直接执行完当前函数所有代码,返回函数调用处:finish
- 打印函数调用栈:bt
八、内存常用知识
- iOS领域,内存地址从低到高分为:代码区、常量区、全局区(数据段)、堆空间、栈空间、动态库
- 我们平常写的代码、函数都是不占用内存的,放在代码区
- 在程序运行过程中是不能往常量区放东西的,全局区是允许的
4.基于引用计数的ARC内存管理是针对堆空间的,代码区、常量区、全局区、栈空间的内存不需要我们管理
- iOS平台中内存是以字节为单位的分配的;malloc分配的堆空间的的内容是以16个字节为单位的,也就是堆空间最少会分配16个字节的空间。
- MacO可执行文件的虚拟内存地址是0x10000000,在MacO文件里找地址需要用真实地址减去虚拟内存的地址,才是MacO文件里的地址