call指令 和 ret指令
-
call 指令:
- 将 call指令 的下一条指令的偏移地址入栈
- 跳转到 call指令 定位的目标地址处执行指令
-
ret 指令:
- 将栈顶的值 pop 给 IP寄存器,以让代码返回 call指令的下一条指令 处执行
- 即将原先入栈的 call指令的下一条指令的偏移地址 赋值给 IP寄存器
补充:
1.call指令 和 ret指令 结合起来 ,一般用于函数调用
2.ret指令 有点类似于高级语言中的 return关键字,但是 ret指令 和 return关键字 还是有区别的。ret指令 的作用是将栈顶的值 pop 给 IP寄存器,以让代码返回到原先 call指令 的下一句处执行。高级语言中 return关键字 的作用:不仅可以返回到调用方的下一句代码处,还可以携带返回值。return关键字是对ret指令的扩展和包装
3.在底层中(汇编语言中)数据的传递,要么通过寄存器,要么通过内存 -
call 指令使用格式
call指令格式: call 标号 或者 call 物理地址(地址偏移量) 标号在经编译器编译后,也会变成地址偏移量 实际上 call指令 使用的是(cs寄存器:标号)来定位指令地址 ; xcode演示部分记录下来 ; 视频33:21部分 ; 视频004 05:00 - 08:00
-
call 指令使用示例
assume ds:data, ss:stack, cs:code ; 数据段 data segment db 20 dup(0) string0 db 'hello world!$' data ends ; 栈段 stack segment db 20 dup(0) stack ends ; 代码段 code segment code_tag: ; 为保险起见,显式设置一下 ds寄存器 和 ss寄存器 mov ax, data mov ds, ax mov ax, stack mov ss, ax ; 使用 call指令 调用屏幕打印功能 call PrintFunc ; 使用 call指令 调用栈平衡示例 call StackBalanceFunc mov ax, 1122H ; 退出程序 mov ah, 4cH int 21H ; 使用标号确定功能的位置,相当于定义了一个函数 ; 打印功能 PrintFunc: mov dx, offset string0 mov ah, 09H int 21H ret ; 使用标号确定功能的位置,相当于定义了一个函数 ; 栈平衡功能: StackBalanceFunc: mov ax, 3344H push ax mov dx, offset string0 mov ah, 09H int 21H pop ax ret code ends end code_tag
-
使用 Call指令 进行函数调用,参数使用寄存器传递
assume ds:data, ss:stack, cs:code ; 数据段 data segment db 20 dup(0) data ends ; 栈段 stack segment db 20 dup(0) stack ends ; 代码段 code segment code_tag: ; 为保险起见,显式设置一下 ds寄存器 和 ss寄存器 mov ax, data mov ds, ax mov ax, stack mov ss, ax ; 业务逻辑代码 ; 从这里开始调用 SumFunc函数,其中 bx寄存器 和 dx寄存器 保存 SumFunc函数 的形参 mov bx, 0003h mov dx, 0004h call SumFunc ; 退出程序 mov ah, 4cH int 21H ; 函数调用传参使用寄存器: ; SumFunc函数的参数:传递两个字型(Word)参数,参数分别用 bx寄存器 和 dx寄存器 存放 ; SumFunc函数的返回值:返回值存放在 ax寄存器 中 ; Question:当函数调用时,传递的参数个数很多,导致寄存器不够用的时候,怎么办? ; Answer:把参数 Push 入栈进行传递 ; 函数调用完毕之后,为保持栈平衡,函数内部的局部变量、传递给函数的参数会被 pop 掉 ; 这就是为什么高级语言中,函数内部的局部变量、传递给函数的参数,我们在函数外面拿不到的原因 SumFunc: mov ax, bx add ax, dx ret code ends end code_tag
-
使用 Call指令 进行函数调用,参数使用栈传递(外平栈 - 一般不这么写)
assume ds:data, ss:stack, cs:code ; 数据段 data segment db 20 dup(0) data ends ; 栈段 stack segment db 20 dup(0) stack ends ; 代码段(外平栈 - 一般不这么写) code segment code_tag: ; 为保险起见,显式设置一下 ds寄存器 和 ss寄存器 mov ax, data mov ds, ax mov ax, stack mov ss, ax ; 业务逻辑代码 ; 从这里开始调用 SumFunc函数,将 SumFunc函数 的两个形参入栈 push 0003H push 0004H call SumFunc ; 外平栈:因为上面传参时,push了两次,而且每个栈单元格占2个字节(0004H = 2 * 2) ; 因此,函数调用结束后,栈顶指针往下移动4字节 add sp, 0004H ; 退出程序 mov ah, 4cH int 21H ; 功能函数 SumFunc: mov bp, sp mov ax, ss:[bp + 0004H] add ax, ss:[bp + 0002H] ret code ends end code_tag
-
使用 Call指令 进行函数调用,参数使用栈传递(内平栈 - 主流写法)
assume ds:data, ss:stack, cs:code ; 数据段 data segment db 20 dup(0) data ends ; 栈段 stack segment db 20 dup(0) stack ends ; 代码段(内平栈 - 主流写法) code segment code_tag: ; 为保险起见,显式设置一下 ds寄存器 和 ss寄存器 mov ax, data mov ds, ax mov ax, stack mov ss, ax ; 业务逻辑代码 push 0003H push 0004H call SumFunc ; 退出程序 mov ah, 4cH int 21H ; 函数调用传参使用栈: ; SumFunc函数的参数:传递两个字型(Word)参数,参数依次 push 进栈空间 ; SumFunc函数的返回值:返回值存放在 ax寄存器 中 ; 注意: ; 1.函数内部不能使用 pop 指令取参数,因为会破坏栈平衡。要通过偏移地址来取参数 ; 2.栈空间中压入了很多数据或者地址,我们肯定希望通过 sp寄存器 来访问这些数据和地址 ; 但是SP为栈顶指针,它是不能够随便改动的,所以不允许用下面这种方式来寻址 SumFunc_Invalid: mov ax, ss:[sp + 0004H] ; 实际 8086语法不允许这么写:在8086的寻址里面,sp为栈顶指针寄存器,只能通过 push,pop,add 等方法来更改,不允许直接拿来做运算 add ax, ss:[sp + 0002H] ; 实际 8086语法不允许这么写 ret 4 ; 内平栈 ; 这时候,我们就需要使用 bp寄存器,来代替 sp寄存器(栈顶指针寄存器) ; bp寄存器 就是专门用来代替 sp寄存器,用于堆栈里面的寻址 ; 其实这里把 bp寄存器 换成 其他通用寄存器(ax、bx、cx、dx)也可以 ; 但是,每个寄存器的设计都是有其特定用途的,bp寄存器 就是专门用来代替 sp寄存器,用以堆栈里面的寻址的,所以建议用 bp寄存器。 ; 而且,通过编译器编译生成的汇编指令,也都是用标准的 bp寄存器 来代替 sp寄存器(即反汇编的时候,栈段的寻址,都是用的 bp寄存器) SumFunc: mov bp, sp mov ax, ss:[bp + 0004H] add ax, ss:[bp + 0002H] ; 此时栈顶指针,指向的是 push 0003H 处的地址 ; 要保证函数调用前和调用后,栈顶的位置是一样的,所以要恢复栈顶指针的位置 ; 内平栈,这句指令执行的功能: ; 1.将栈顶的值(实际上是call指令的下一条指令的偏移地址) pop 给 IP寄存器 ; 2.将 SP寄存器的值 + 4,即将 SP寄存器 往下移动移动两个单元格,其中 4 = 2 * 2 ret 4 code ends end code_tag
为什么用栈段来存放函数的参数
Question:为什么是用栈段来存放调用的目标函数的参数,而不是用数据段来存放调用的目标函数的参数?
Answer:
1.当我们使用 call指令,执行函数调用的时候,call指令会先把 call指令的下一条指令的 偏移地址 push 入栈,从此刻开始直到函数执行结束为止,整个栈空间都是在为这个函数服务。当函数执行完毕之后,函数使用栈空间就退出了(恢复到之前的状态了)
2.函数内部在栈空间中取参数的时候,参数的偏移地址是固定的(这里的参数,指的是函数的形参),例如
mov ax, ss:[bp + 0004H]
add ax, ss:[bp + 0002H]
中, 0004H和0002H这两个偏移值是固定的
栈平衡
函数栈平衡:函数在return之前,一定会干一件事情,就是把之前本函数内部 push 进栈的数据,都全部 pop 出栈。这样做是为了保证函数调用前后的栈顶的位置是一致的(防止内存泄漏导致的栈溢出)
函数栈平衡可以通过以下两种方式实现:
1.外平栈:由函数外部保持栈平衡
2.内平栈:由函数内部保持栈平衡
编译器在编译代码的过程中,大部分情况下,都会使用内平栈。
所以在反汇编代码里面看到的,都是以内平栈为主。
可以利用栈平衡的特性来判断函数的调用位置:
比如 IP寄存器的值 突然上升,之后又突然恢复,想都不用想,肯定调用了函数。
在编写iOS代码时,汇编代码是由编译器自动翻译OC代码而生成的。用OC编写函数时,如果在函数内部用到局部变量,那么在编译器将函数编译成汇编代码的时候,就有可能用到栈,编译器就会将局部变量 pop 到当前栈顶指针处。
注意
- 汇编语言函数调用的返回值,一般放在 ax寄存器 中(这也是为什么高级语言中,一个函数只能有一个返回值的原因)。一个寄存器,可以存放任何类型的返回值,不管是数值,结构体(实质上是地址),还是对象(实质上是地址)。函数不能返回数组,只能返回数组的首地址(注意:NSArray不是一个数组,是一个数组对象)。
- Question:为什么使用寄存器传参的速度要比使用栈传参的速度要快?
Answer:如果使用寄存器传参,在CPU内部,一次操作就能完成。如果使用栈传参,CPU还需要通过地址总线,控制总线,数据总线和内存进行交互,需要进行多次操作。虽然在定性上,使用寄存器传参比使用栈传参速度要快,但是在定量上,这对于性能的影响,微乎其微。