汇编(九)

目录

    • 函数的局部变量
    • 死循环 与 无穷递归 的区别
    • 函数内部再调用函数 & 现场保护
    • 栈帧
    • 站在汇编的视角看高级语言
    • 函数调用参数的传递过程
    • 全局变量
    • 注意

函数的局部变量

  • 定义高级语言 SunFunc 函数如下

    SumFunc(3, 4);
    
    int SunFunc(int a, int b)
    {
    	int c = 1;
    	int d = 2;
      	return a + b + c + d;
    }
    
  • 则调用 SunFunc 函数的汇编代码为

    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   
      
    	; 退出程序
    	mov ah, 4cH
    	int 21H  
    
    	; 功能函数
    	SumFunc:
    	 
    	; 用 bp指针 记录调用前 sp指针 的值
    	mov bp, sp
    	 
    	; 任何一个函数的调用,编译器首先会为函数预留(或者叫开辟)存储局部变量的内存空间。这个过程叫做提升栈空间。
    	; 这里为 SunFunc 函数开辟了 20H(32) Byte 的内存空间,注意,20H(32) Byte 只是打个比方
    	; 具体分配多少个 Byte 作为存储局部变量的内存空间,由编译器根据函数内局部变量的使用情况决定
    	sub sp, 20H
    		
    	; 定义两个局部变量 c = 1, d = 2
    	mov ss:[bp - 2], 0001H
    	mov ss:[bp - 4], 0002H
    	
    	; 执行 a + b + c + d
    	mov ax, ss:[bp + 0004H]
    	add ax, ss:[bp + 0002H]
    	add ax, ss:[bp - 0002H]
    	add ax, ss:[bp - 0004H]
    	 
    	; 恢复 sp指针 的位置,此时函数内部的局部变量被释放(清空)
    	mov sp, bp
    	
    	ret 4
    code ends       
     
    end code_tag
    
  • 函数调用的栈空间示意图
    汇编(九)_第1张图片

死循环 与 无穷递归 的区别

  • 死循环,虽然不断调用函数,但是每次调用都保持栈平衡。因此,不论调用多少次,都不会导致栈溢出。

    int main (int argc, char* argv[])
    {
    	short a = 0;
    	while (true)
    	{
    		printf("%d", a++);
    	}
    }
    
  • 无穷递归,每次调用都会开辟额外的栈空间用于存储:

    1. 函数的形参(如果有的话)
    2. bp寄存器 的初始值(用于函数调用完毕后,恢复 bp寄存器)
    3. call指令 的下一条指令的 偏移地址
    4. 函数的局部变量(如果有的话)

    无穷递归,迟早会耗尽栈空间,导致栈溢出。因此,如果递归调用的层级太深,需要用循环代替。

    int main (int argc, char* argv[])
    {
    	sum(1, 2);
    }
     
    int sum (int a, int b)
    {
    	sum(a, b);
    	return a + b;
    }
    

函数内部再调用函数 & 现场保护

  • 现场保护:函数内部如果需要使用到寄存器(任何寄存器),则需要将寄存器(任何寄存器)中原先的值保存起来,以备使用完寄存器后恢复寄存器里面的值,破坏现场
    assume ds:data, ss:stack, cs:code
    
    ; 数据段         
    data segment
    	db 20 dup(0)
    	str 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
    	
    	; 业务逻辑代码
    	; 从这里开始调用 SumFunc 函数,将 SumFunc 函数的两个形参入栈
    	push 0003H
    	push 0004H 
    	call SumFunc   
      
    	; 退出程序
    	mov ah, 4cH
    	int 21H  
    	
    	;--------------------------------------------------------------
    	; 功能函数 - 求和
    	SumFunc: 
    	
    	; 1.保存 bp寄存器 的值,bp寄存器 一定是在进入函数的第一刻,就被保护
    	push bp
    	
    	; 2.使用 bp寄存器 记录 sp寄存器 的值
    	mov bp, sp
    	
    	; 3.通过减小 sp寄存器 的值,来提升栈空间,用于存储函数的局部变量
    	sub sp, 20H
    	
    	; 4.保护函数内部需要用到的 通用寄存器
    	push bx
    	push cx
    	push dx
    	
    	; 5.函数功能具体实现
    	; 修改寄存器的值:这里这么写没有任何实际意义
    	; 只是想说明,函数的具体实现中有可能会用到到通用寄存器的值
    	; 在修改通用寄存器的值之前,需要先对原先通用寄存器里面的值进行保存
    	; 以备使用完毕后恢复
    	mov bx, 0000H
    	mov cx, 0000H
    	mov dx, 0000H
    	; 定义两个局部变量 c = 1, d = 2
    	mov ss:[bp - 2], 0001H
    	mov ss:[bp - 4], 0002H
    	; 执行 a + b + c + d
    	mov ax, ss:[bp + 0006H]
    	add ax, ss:[bp + 0004H]
    	add ax, ss:[bp - 0002H]
    	add ax, ss:[bp - 0004H]
    	; 调用 PrintFunc 函数
    	; 函数内部再调用函数,都是在 sp指针 恢复之前
    	; 因为调用目标函数时,源函数内部的局部变量可能还需要再使用
    	; 如果 sp指针 恢复了,那么源函数内部的局部变量就被释放了,目标函数就拿不到源函数内部的局部变量了
    	call PrintFunc
    	
    	; 6.恢复 通用寄存器 的值,根据栈后进先出的特性,最后 push 进栈的通用寄存器,最先被 pop 出栈
    	pop dx
    	pop cx
    	pop bx
    	
    	; 7.通过增加 sp指针 的值来恢复 sp指针 的位置,此时函数内部的局部变量被释放(清空)
    	; 前面提升栈空间的时候,sp指针的值减少了多少,现在恢复栈空间的时候,sp指针 的值就增加多少
    	add sp, 20H ; 也可以写成 mov sp, bp
    	 
    	; 8.恢复 bp指针 的位置
    	pop bp
    	
    	; 9.函数返回,并且进行内平栈
    	ret 4
    	
    	;--------------------------------------------------------------
    	; 功能函数 - 打印
    	PrintFunc:
    	; 1.保存 bp寄存器 的值,bp寄存器 一定是在进入函数的第一刻,就被保护
    	push bp
    	; 2.使用 bp寄存器 记录 sp寄存器 的值
    	mov bp, sp
    	; 3.通过减小 sp寄存器 的值,来提升栈空间,用于存储函数的局部变量
    	sub sp, 20H
    	; 4.保护函数内部需要用到的 通用寄存器
    	push dx
    	push ax
    	; 5.函数功能具体实现
    	mov dx, offset str
    	mov ah, 09H
    	int 21H
    	; 6.恢复 通用寄存器 的值,根据栈后进先出的特性,最后 push 进栈的通用寄存器,最先被 pop 出栈
    	pop ax
    	pop dx
    	; 7.通过增加 sp指针 的值来恢复 sp指针 的位置,此时函数内部的局部变量被释放(清空)
    	; 前面提升栈空间的时候,sp指针的值减少了多少,现在恢复栈空间的时候,sp指针 的值就增加多少
    	; 也可以写成 mov sp, bp
    	add sp, 20H
    	; 8.恢复 bp指针 的位置
    	pop bp
    	; 9.函数返回,并且进行内平栈
    	ret
    	
    code ends       
     
    end code_tag
    
  • 上面汇编代码对应的栈空间如下图所示
    汇编(九)_第2张图片

栈帧

在数据结构中, 栈是限定仅在表尾进行插入或删除操作的线性表。
在汇编语言中,栈是存储函数的方法调用、参数值、局部变量,的一块地址连续的内存区域。
栈用于维护函数调用的上下文,离开了栈函数调用就没法实现。
每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame)。每个独立的栈帧一般包括:
1.函数的形参
2.函数的返回地址
3.函数的临时变量(包括函数的非静态局部变量以及编译器自动生成的其他临时变量)
4.函数调用的上下文
栈空间是从内存高地址向低地址延伸的,一个函数的栈帧用 bp 和 sp 这两个寄存器来划定范围
bp 指向当前的栈帧的底部,sp 始终指向栈帧的顶部(栈帧的长度 = bp - sp)
bp 寄存器又被称为帧指针(Frame Pointer)
sp 寄存器又被称为栈指针(Stack Pointer)
汇编(九)_第3张图片

站在汇编的视角看高级语言

  • 立即寻址是CPU的一种寻址方式。立即寻址方式就是将操作数紧跟在操作码后面,与操作码一起放在指令代码段中。在程序运行时,程序直接调用该操作数,而不需要到其他地址单元中去取相应的操作数。上述的写在指令中的操作数也被称作立即数(立即数是一种特殊的操作数)。立即数可以是8位、16位、32位或64位,该数值紧跟在操作码之后。因为立即数是存储在程序代码段中的常数,所以是写死的,运行时改变不了。

  • sizeof()
    注意:以下 sizeof(x),中的 x 为变量名或者常量名
    sizeof(x) 不是一个函数,只是一个符号,编译器在编译时看到 sizeof(x) 这个符号后,就会把 sizeof(x) 变成一个立即数
    虽然 sizeof(x) 中 x的值(变量或者常量) 是不确定的,但是我们在定义 x(变量或者常量) 的时候,x(变量或者常量) 的类型是确定的,即编译器在编译的时候是知道 x(变量或者常量 ) 占几个 Byte 的。
    在 Objective-C 代码中,使用 sizeof

    int main(int argc, const char * argv[]) {
    	short a = 10;
    	int aSize = sizeof(a);
    	// int aSize = sizeof a;
    
    	int b = 11;
    	int bSize = sizeof(b);
    	// int bSize = sizeof b;
    
    	long c = 12;
    	int cSize = sizeof(c);
    	// int cSize = sizeof c;
    
    	long long d = 13;
    	int dSize = sizeof(d);
    	// int dSize = sizeof d;
    
    	NSLog(@"aSize = %d, bSize = %d, cSize = %d, dSize = %d", aSize, bSize, cSize, dSize);
    
    	return 0;
    }
    

    通过反汇编,我们并没有看到函数的调用,sizeof(x) 变成了一个立即数

    test`main:
      0x100000ef0 <+0>:   pushq  %rbp
      0x100000ef1 <+1>:   movq   %rsp, %rbp
      0x100000ef4 <+4>:   subq   $0x40, %rsp
      0x100000ef8 <+8>:   leaq   0x109(%rip), %rax         ; @"aSize = %d, bSize = %d, cSize = %d, dSize = %d"
      0x100000eff <+15>:  movl   $0x0, -0x4(%rbp)
      0x100000f06 <+22>:  movl   %edi, -0x8(%rbp)
      0x100000f09 <+25>:  movq   %rsi, -0x10(%rbp)
      0x100000f0d <+29>:  movw   $0xa, -0x12(%rbp)			; short a = 10
      0x100000f13 <+35>:  movl   $0x2, -0x18(%rbp)			; int aSize = sizeof(a)sizeof(a)变成了一个立即数
      0x100000f1a <+42>:  movl   $0xb, -0x1c(%rbp)			; int b = 11
      0x100000f21 <+49>:  movl   $0x4, -0x20(%rbp)			; int bSize = sizeof(b)sizeof(b)变成了一个立即数
      0x100000f28 <+56>:  movq   $0xc, -0x28(%rbp)			; long c = 12
      0x100000f30 <+64>:  movl   $0x8, -0x2c(%rbp)			; int cSize = sizeof(c)sizeof(c)变成了一个立即数
      0x100000f37 <+71>:  movq   $0xd, -0x38(%rbp)			; long long d = 13
      0x100000f3f <+79>:  movl   $0x8, -0x3c(%rbp)			; int dSize = sizeof(d)sizeof(d)变成了一个立即数
      0x100000f46 <+86>:  movl   -0x18(%rbp), %esi
      0x100000f49 <+89>:  movl   -0x20(%rbp), %edx
      0x100000f4c <+92>:  movl   -0x2c(%rbp), %ecx
      0x100000f4f <+95>:  movl   -0x3c(%rbp), %r8d
      0x100000f53 <+99>:  movq   %rax, %rdi
      0x100000f56 <+102>: movb   $0x0, %al
      0x100000f58 <+104>: callq  0x100000f66               ; symbol stub for: NSLog
      0x100000f5d <+109>: xorl   %eax, %eax
      0x100000f5f <+111>: addq   $0x40, %rsp
      0x100000f63 <+115>: popq   %rbp
      0x100000f64 <+116>: retq   
    
  • 自增(++)与 自减(- -)
    很多操作系统底层的 API 是通过 C 语言编写的,iOS 开发会用到 C 语言,Android 开发会用到 C 语言,Mac OS 开发会用到 C 语言,Windows 开发也会用 C 语言。C 语言在不同的平台(或者操作系统)上有不同的编译器,因此同一份 C 代码,可以在不同的平台下被编译,从而做到跨平台(即C语言是跨平台的)。
    但是,C语言在设计的时候,对自增符、自减符与数学运算符的优先级,没有一个明确的规定。因此不同的编译器,对自增符、自减符与数学运算符的优先级都有自己的实现。对于下面的代码,经过不同的编译器编译、运行,得出的结果可能是不一样的:

    int main(int argc, const char * argv[]) {
    
        int a = 10;
        int b = ++a + ++a + ++a;
        // b的输出结果与编译器有关
        NSLog(@"b = %d", b);
      
        return 0;
    }
    

    这样就造成了一个问题:相同的的 C 代码,在不同的平台上,经过不同的编译器编译,然后运行,可能会产生不一样的结果。
    在跨平台的时候如果出现了此类 bug,将会非常难以定位到错误
    基于这点,在 Swift 中 去掉了 自增符、自减符
    上面代码在 XCode 下对应的汇编如下

    test`main:
      0x100000f30 <+0>:  pushq  %rbp
      0x100000f31 <+1>:  movq   %rsp, %rbp
      0x100000f34 <+4>:  subq   $0x20, %rsp
      0x100000f38 <+8>:  leaq   0xc9(%rip), %rax          ; @"b = %d"
      0x100000f3f <+15>: movl   $0x0, -0x4(%rbp)
      0x100000f46 <+22>: movl   %edi, -0x8(%rbp)
      0x100000f49 <+25>: movq   %rsi, -0x10(%rbp)
      0x100000f4d <+29>: movl   $0xa, -0x14(%rbp)			; int a = 10
      0x100000f54 <+36>: movl   -0x14(%rbp), %ecx			; cx = a = 10
      0x100000f57 <+39>: addl   $0x1, %ecx					; cx = cx + 1 = 11
      0x100000f5a <+42>: movl   %ecx, -0x14(%rbp)			; a = cx = 11
      0x100000f5d <+45>: movl   -0x14(%rbp), %edx			; dx = a = 11
      0x100000f60 <+48>: addl   $0x1, %edx					; dx = dx + 1 = 12
      0x100000f63 <+51>: movl   %edx, -0x14(%rbp)			; a = dx = 12
      0x100000f66 <+54>: addl   %edx, %ecx					; cx = dx + cx = 11 + 12 = 23
      0x100000f68 <+56>: movl   -0x14(%rbp), %edx			; dx = a = 12
      0x100000f6b <+59>: addl   $0x1, %edx					; dx = dx + 1 = 13
      0x100000f6e <+62>: movl   %edx, -0x14(%rbp)			; a = dx = 13
      0x100000f71 <+65>: addl   %edx, %ecx					; cx = cx + dx = 23 + 13 = 36
      0x100000f73 <+67>: movl   %ecx, -0x18(%rbp)			; b = cx = 36
      0x100000f76 <+70>: movl   -0x18(%rbp), %esi
      0x100000f79 <+73>: movq   %rax, %rdi
      0x100000f7c <+76>: movb   $0x0, %al
      0x100000f7e <+78>: callq  0x100000f8c               ; symbol stub for: NSLog
      0x100000f83 <+83>: xorl   %eax, %eax
      0x100000f85 <+85>: addq   $0x20, %rsp
      0x100000f89 <+89>: popq   %rbp
      0x100000f8a <+90>: retq   
    
  • ++ 与 += 性能的比较
    每一句汇编指令的执行时间,基本上没有什么区别。我们可以通过比较 ++ 与 += 生成汇编代码的多少,来定性分析 ++ 与 += 的性能,谁高谁低。

    int a = 10;
    ++a;
    // 对应汇编代码如下
     0x100000fa4 <+20>: movl   $0xa, -0x14(%rbp)
     0x100000fab <+27>: movl   -0x14(%rbp), %ecx
     0x100000fae <+30>: addl   $0x1, %ecx
     0x100000fb1 <+33>: movl   %ecx, -0x14(%rbp)
    ---------------------------------------------------------------------------------------
    int a = 10;
    a += 1;
    // 对应汇编代码如下
     0x100000fa4 <+20>: movl   $0xa, -0x14(%rbp)
     0x100000fab <+27>: movl   -0x14(%rbp), %ecx
     0x100000fae <+30>: addl   $0x1, %ecx
     0x100000fb1 <+33>: movl   %ecx, -0x14(%rbp)
    

    ++a 与 a+=1 生成的汇编代码的条数,是一样的。因此,++ 与 += 在性能上没有区别

  • XCode 的 debug 模式和 release 模式的区别
    以前在做 iOS 开发的时候,只知道:在写代码的时候用 debug 模式,方便调试、开发,等要测试、发版、上架的时候,将代码切到 release 模式,打包出 .apk 文件。
    站在汇编的角度,XCode 的 debug 模式和 release 模式,是有区别的:XCode 在 release 模式下对函数的调用,做了一层优化!!!
    注意,是对函数的调用做了优化,不是对方法的调用做了优化

    Question:为什么在 XCode 的 release 模式下,编译、运行时,函数可以被优化,而方法不可以被优化?
    Answer:
    Objective-C 对函数的调用是静态调用,函数的功能在编译器编译的时候就得到了确认(即函数是预编译的)
    Objective-C 对方法的调用是动态调用,方法的功能只有在运行时才能得到确认(方法的调用是基于 RunTime 机制)
    举个例子:
    在 Objective-C 中,如果对函数只声明不实现,即调用函数,那么在 command + B 进行编译的时候,编译器就会报 link 错误。
    在 Objective-C 中,如果只在 .h 文件里面声明方法,不在 .m 文件里面实现方法,那么在 command + B 进行编译的时候,是可以编译通过的,只有在运行时,真正调用到该方法的时候,找不到该方法,才会报错

    接着上面的问题:
    1.在 main.m 文件中,实现了一个 add 函数,如下所示

    int add (int num) {
       int x = num;
       x = x + 1;
       return x;
    }
    
    int main(int argc, const char * argv[]) {
    
        int result = add(10);    
        NSLog(@"result = %d", result);
       
        return 0;
    }
    

    在 Xode 的 release 模式下,反编译,观察其汇编代码,可以看到:由于函数功能过于简单,编译器直接帮我们算出结果,并做了赋值。在汇编底层中,实际上没有进行函数的调用。

    test`main:
      0x100000f6f <+0>:  pushq  %rbp
      0x100000f70 <+1>:  movq   %rsp, %rbp
      0x100000f73 <+4>:  leaq   0x8e(%rip), %rdi          ; @"result = %d"
      0x100000f7a <+11>: movl   $0xb, %esi				  ; 编译器直接帮我们算出结果,并做赋值
      0x100000f7f <+16>: xorl   %eax, %eax
      0x100000f81 <+18>: callq  0x100000f8a               ; symbol stub for: NSLog
      0x100000f86 <+23>: xorl   %eax, %eax
      0x100000f88 <+25>: popq   %rbp
      0x100000f89 <+26>: retq   
    

    如果我们连 NSLog 都没有调用(即没有使用 result ),那么 add 函数的调用和没有使用的局部变量 result,都被优化掉了

    int add (int num) {
     	int x = num;
     	x = x + 1;
     	return x;
    }
    
    int main(int argc, const char * argv[]) {
    
     	int result = add(10);    
     	
     	return 0;
    }
    
    // 对应汇编代码如下
    test`main:
     0x100000fb0 <+0>: pushq  %rbp
     0x100000fb1 <+1>: movq   %rsp, %rbp
     0x100000fb4 <+4>: xorl   %eax, %eax
     0x100000fb6 <+6>: popq   %rbp
     0x100000fb7 <+7>: retq   
    

    2.在 Calc 类中,实现了一个相同功能的 add 方法,如下所示

    // main.m
    #import 
    #import "Calc.h"
    
    int main(int argc, const char * argv[]) {
    
        int result = [Calc add:10];
        NSLog(@"result = %d", result);
       
        return 0;
    }
    
    // Calc.h
    #import 
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Calc : NSObject
    
    +(int)add:(int)num;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    // Calc.m
    #import "Calc.h"
    
    @implementation Calc
    
    +(int)add:(int)num {
       int x = num;
       x = x + 1;
       return x;
    }
    
    @end
    

    在 Xode 的 release 模式下,反编译,观察其汇编代码,可以看到:汇编代码中有关于 add 方法的调用,并且可以看到 add 方法的实现

     // 1.add方法的调用
    test`main:
      0x100000f45 <+0>:  pushq  %rbp
      0x100000f46 <+1>:  movq   %rsp, %rbp
      0x100000f49 <+4>:  movq   0x1170(%rip), %rdi        ; (void *)0x00000001000020f0: Calc
      0x100000f50 <+11>: movq   0x1161(%rip), %rsi        ; "add:"
      0x100000f57 <+18>: movl   $0xa, %edx
      0x100000f5c <+23>: callq  *0x9e(%rip)               ; (void *)0x00007fff6bdd5800: objc_msgSend
      0x100000f62 <+29>: leaq   0xa7(%rip), %rdi          ; @"result = %d"
      0x100000f69 <+36>: movl   %eax, %esi
      0x100000f6b <+38>: xorl   %eax, %eax
      0x100000f6d <+40>: callq  0x100000f76               ; symbol stub for: NSLog
      0x100000f72 <+45>: xorl   %eax, %eax
      0x100000f74 <+47>: popq   %rbp
      0x100000f75 <+48>: retq   
    
     // 2.add方法的实现
    test`+[Calc add:]:
      0x100000f3c <+0>: pushq  %rbp
      0x100000f3d <+1>: movq   %rsp, %rbp
      0x100000f40 <+4>: leal   0x1(%rdx), %eax
      0x100000f43 <+7>: popq   %rbp
      0x100000f44 <+8>: retq   
    
  • 在 XCode 中,可以通过修改编译器优化等级,调整 Debug模式 和 Release模式,编译器对于代码对的优化程度
    Build Settings - Optimization Level

    1. None [-O0],不执行优化,Debug模式 默认应用此种优化等级
    2. Fast [-O, O1],快的(让代码的运行效率变得快)
    3. Faster [-O2],更快的(让代码的运行效率变得更快)
    4. Fastest [-O3],最快的(让代码的运行效率变得最快)
    5. Fastest, Smallest [-Os],最快的和最小的(让代码运行效率变得最快,让可执行文件的体积变得最小),Release模式 默认应用此种优化等级
    6. Fastest, Aggressive Optimizations [-Ofast],最快的和激进的优化选项,不推荐使用
    7. Smallest, Agressive Size Optimizations [-Oz] ,最快的和激进的优化选项,不推荐使用
  • 栈的红色区域
    System V AMD64 ABI标准中,介绍了栈帧的顶部后面有一块 “red zone”

    原文:
    The 128-byte area beyond the location pointed to by %rsp is considered to be reserved and shall not be modified by signal or interrupt handlers. Therefore, functions may use this area for temporary data that is not needed across function calls. In particular, leaf functions may use this area for their entire stack frame, rather than adjusting the stack pointer in the prologue and epilogue. This area is known as the red zone.
    译文:
    在 %rsp 指向的栈顶之后的128字节是被保留的 - - 它不能被信号和终端处理程序使用。因此,函数可以在这个区域放一些临时的数据。特别地,叶子函数可能会将这128字节的区域作为它的整个栈帧,而不是像往常一样在进入函数和离开函数时靠移动栈指针获取栈帧和释放栈帧。这128字节被称作红色区域。

    简单点说,这个红色区域(red zone)就是一个优化。因为这个区域不会被信号或者中断侵占,函数可以在不移动栈顶指针的情况下使用它存取一些临时数据 - - 于是两个移动 rsp 的指令就被节省下来了,以达到提高性能的目的。
    然而,标准只说了不会被信号和终端处理程序侵占,red zone 还是会被接下来的函数调用使用的,这也是为什么大多数情况下都是叶子函数(不会再调用别的函数的函数)使用这种优化。
    并且,当叶子函数内部使用的局部变量超过128字节的时候,在编译时,编译器还是会显式地移动 rsp 以开辟更大的栈空间供函数使用。

    // 函数的内部没有再调用函数,编译器不会显式地移动 rsp 以开辟栈空间,供函数(调用方、源函数)内部(存储形参,局部变量)使用
    // 此时会通过 rbp + 偏移地址 直接使用栈顶上面的红色区域存储形参和局部变量
    
    int sum(int a, int b) {
     	int c = 3;
     	int d = 4;
     	return a + b + c + d;
    }
    
    int main(int argc, const char * argv[]) {
      
     	int result = sum(1, 2);
     	NSLog(@"result = %d", result);
      
     	return 0;
    }
     
    // sum 函数汇编代码 - 没有通过减少 sp 来提升栈空间,而是直接使用红色区域存储局部变量
    test`sum:
      0x100000f00 <+0>:  pushq  %rbp
      0x100000f01 <+1>:  movq   %rsp, %rbp
      0x100000f04 <+4>:  movl   %edi, -0x4(%rbp)
      0x100000f07 <+7>:  movl   %esi, -0x8(%rbp)
      0x100000f0a <+10>: movl   $0x3, -0xc(%rbp)
      0x100000f11 <+17>: movl   $0x4, -0x10(%rbp)
      0x100000f18 <+24>: movl   -0x4(%rbp), %eax
      0x100000f1b <+27>: addl   -0x8(%rbp), %eax
      0x100000f1e <+30>: addl   -0xc(%rbp), %eax
      0x100000f21 <+33>: addl   -0x10(%rbp), %eax
      0x100000f24 <+36>: popq   %rbp
      0x100000f25 <+37>: retq   
    
    // 函数的内部有再调用函数,编译器一定会显式地移动 rsp 以开辟栈空间,供函数(调用方、源函数)内部(存储形参,局部变量)使用
     
    int add(int num) {
     	return num + 1;
    }
    
    int sum(int a, int b) {
     	int c = 3;
     	int d = 4;
     	d = add(d);
     	return a + b + c + d;
    }
    
    int main(int argc, const char * argv[]) {
     	int result = sum(1, 2);
     	NSLog(@"result = %d", result);
     	
     	return 0;
    }
     
    // sum 函数汇编代码
    test`sum:
      0x100000ef0 <+0>:  pushq  %rbp
      0x100000ef1 <+1>:  movq   %rsp, %rbp
      0x100000ef4 <+4>:  subq   $0x10, %rsp
      0x100000ef8 <+8>:  movl   %edi, -0x4(%rbp)
      0x100000efb <+11>: movl   %esi, -0x8(%rbp)
      0x100000efe <+14>: movl   $0x3, -0xc(%rbp)
      0x100000f05 <+21>: movl   $0x4, -0x10(%rbp)
      0x100000f0c <+28>: movl   -0x10(%rbp), %edi
      0x100000f0f <+31>: callq  0x100000ee0               ; add at main.m:11
      0x100000f14 <+36>: movl   %eax, -0x10(%rbp)
      0x100000f17 <+39>: movl   -0x4(%rbp), %eax
      0x100000f1a <+42>: addl   -0x8(%rbp), %eax
      0x100000f1d <+45>: addl   -0xc(%rbp), %eax
      0x100000f20 <+48>: addl   -0x10(%rbp), %eax
      0x100000f23 <+51>: addq   $0x10, %rsp
      0x100000f27 <+55>: popq   %rbp
      0x100000f28 <+56>: retq   
    
    // 函数的内部没有再调用函数,但是函数内部使用的形参和局部变量加起来超过了128字节
    // 编译器会显式地移动 rsp 以开辟栈空间,供函数(调用方、源函数)内部(存储形参,局部变量)使用
     
    int sum(int a, int b) {
     	int c = 3;
     	int d = 4;
     	long arr[20];
     	return a + b + c + d;
    }
    
    int main(int argc, const char * argv[]) {
      
     	int result = sum(1, 2);
     	NSLog(@"result = %d", result);
     	
     	return 0;
    }
     
    // sum 函数汇编代码
    test`sum:
      0x100000e90 <+0>:   pushq  %rbp
      0x100000e91 <+1>:   movq   %rsp, %rbp
      0x100000e94 <+4>:   subq   $0xd0, %rsp
      0x100000e9b <+11>:  movq   0x15e(%rip), %rax         ; (void *)0x00007fff93aa2d40: __stack_chk_guard
      0x100000ea2 <+18>:  movq   (%rax), %rax
      0x100000ea5 <+21>:  movq   %rax, -0x8(%rbp)
      0x100000ea9 <+25>:  movl   %edi, -0xb4(%rbp)
      0x100000eaf <+31>:  movl   %esi, -0xb8(%rbp)
      0x100000eb5 <+37>:  movl   $0x3, -0xbc(%rbp)
      0x100000ebf <+47>:  movl   $0x4, -0xc0(%rbp)
      0x100000ec9 <+57>:  movl   -0xb4(%rbp), %ecx
      0x100000ecf <+63>:  addl   -0xb8(%rbp), %ecx
      0x100000ed5 <+69>:  addl   -0xbc(%rbp), %ecx
      0x100000edb <+75>:  addl   -0xc0(%rbp), %ecx
      0x100000ee1 <+81>:  movq   0x118(%rip), %rax         ; (void *)0x00007fff93aa2d40: __stack_chk_guard
      0x100000ee8 <+88>:  movq   (%rax), %rax
      0x100000eeb <+91>:  movq   -0x8(%rbp), %rdx
      0x100000eef <+95>:  cmpq   %rdx, %rax
      0x100000ef2 <+98>:  movl   %ecx, -0xc4(%rbp)
      0x100000ef8 <+104>: jne    0x100000f0d               ; <+125> at main.m
      0x100000efe <+110>: movl   -0xc4(%rbp), %eax
      0x100000f04 <+116>: addq   $0xd0, %rsp
      0x100000f0b <+123>: popq   %rbp
      0x100000f0c <+124>: retq   
      0x100000f0d <+125>: callq  0x100000f6a               ; symbol stub for: __stack_chk_fail
      0x100000f12 <+130>: ud2    
    

    自己的一点理解:为什么栈顶后面的那一块空间,叫做 red zone(红色区域)?
    虽然上面通过各种约束,让使用红色空间变得十分安全。但是因为做优化的原因,没有通过移动 rsp 来使用栈空间,这和常规的移动 rsp 来提升栈空间有很大的区别,从直观上看起来就十分"危险"。所以,栈顶后面的那一块区域才叫做红色空间。

函数调用参数的传递过程

源函数(Caller)调用目标函数(Callee)时:

  • 如果,源函数传递给目标函数的参数的个数 小于 CPU中用来存储函数形参的寄存器的个数,则源函数传递给目标函数的参数都通过寄存器来进行传递。

    int sum(int a, int b) {
        return a + b;
    }
    
    int main(int argc, const char * argv[]) {
      
        int result = sum(1, 2);
        NSLog(@"result = %d", result);
      
        return 0;
    }
     
    // main 函数汇编代码
    test`main:
      0x100000f30 <+0>:  pushq  %rbp
      0x100000f31 <+1>:  movq   %rsp, %rbp
      0x100000f34 <+4>:  subq   $0x20, %rsp
      0x100000f38 <+8>:  movl   $0x0, -0x4(%rbp)
      0x100000f3f <+15>: movl   %edi, -0x8(%rbp)
      0x100000f42 <+18>: movq   %rsi, -0x10(%rbp)
      0x100000f46 <+22>: movl   $0x1, %edi				  ; main 函数中,将调用参数的值放入 di 寄存器
      0x100000f4b <+27>: movl   $0x2, %esi				  ; main 函数中,将调用参数的值放入 si 寄存器
      0x100000f50 <+32>: callq  0x100000f10               ; sum at main.m:11
      0x100000f55 <+37>: leaq   0xac(%rip), %rcx          ; @"result = %d"
      0x100000f5c <+44>: movl   %eax, -0x14(%rbp)
      0x100000f5f <+47>: movl   -0x14(%rbp), %esi
      0x100000f62 <+50>: movq   %rcx, %rdi
      0x100000f65 <+53>: movb   $0x0, %al
      0x100000f67 <+55>: callq  0x100000f74               ; symbol stub for: NSLog
      0x100000f6c <+60>: xorl   %eax, %eax
      0x100000f6e <+62>: addq   $0x20, %rsp
      0x100000f72 <+66>: popq   %rbp
      0x100000f73 <+67>: retq   
       
    // sum 函数汇编代码
    test`sum:
      0x100000f10 <+0>:  pushq  %rbp
      0x100000f11 <+1>:  movq   %rsp, %rbp
      0x100000f14 <+4>:  movl   %edi, -0x4(%rbp)		  ; sum 函数中,将存储在 di 寄存器中的值(调用参数)取出,放在自己的栈空间中
      0x100000f17 <+7>:  movl   %esi, -0x8(%rbp)		  ; sum 函数中,将存储在 si 寄存器中的值(调用参数)取出,放在自己的栈空间中
      0x100000f1a <+10>: movl   -0x4(%rbp), %eax
      0x100000f1d <+13>: addl   -0x8(%rbp), %eax
      0x100000f20 <+16>: popq   %rbp
      0x100000f21 <+17>: retq   
    
  • 如果,源函数传递给目标函数的参数的个数 大于 CPU中用来存储函数形参的寄存器的个数,源函数除通过寄存器传递目标函数的调用参数外,还会将剩余的参数记录在自己的栈空间,当目标函数执行时,再到源函数的栈空间中,将调用参数取出到目标函数的栈空间。

    long sum(long a, long b, long c, long d, long e, long f, long g, long h) {
         return a + b + c + d + e + f + g + h;
    }
    
    int main(int argc, const char * argv[]) {
      
        long result = sum(1, 2, 3, 4, 5, 6, 7, 8);
        NSLog(@"result = %ld", result);
      
        return 0;
    }
     
    // main 函数汇编代码
    test`main:
      0x100000f00 <+0>:   pushq  %rbp
      0x100000f01 <+1>:   movq   %rsp, %rbp
      0x100000f04 <+4>:   subq   $0x30, %rsp
      0x100000f08 <+8>:   movl   $0x0, -0x4(%rbp)
      0x100000f0f <+15>:  movl   %edi, -0x8(%rbp)
      0x100000f12 <+18>:  movq   %rsi, -0x10(%rbp)
      0x100000f16 <+22>:  movl   $0x1, %edi
      0x100000f1b <+27>:  movl   $0x2, %esi
      0x100000f20 <+32>:  movl   $0x3, %edx
      0x100000f25 <+37>:  movl   $0x4, %ecx
      0x100000f2a <+42>:  movl   $0x5, %r8d
      0x100000f30 <+48>:  movl   $0x6, %r9d
      0x100000f36 <+54>:  movq   $0x7, (%rsp) 			   ; main 函数中,CPU用来存储函数形参的寄存器用完之后,将调用参数存储在自己(main函数)的栈空间
      0x100000f3e <+62>:  movq   $0x8, 0x8(%rsp) 	 	   ; main 函数中,CPU用来存储函数形参的寄存器用完之后,将调用参数存储在自己(main函数)的栈空间
      0x100000f47 <+71>:  callq  0x100000eb0               ; sum at main.m:11
      0x100000f4c <+76>:  leaq   0xb5(%rip), %rcx          ; @"result = %ld"
      0x100000f53 <+83>:  movq   %rax, -0x18(%rbp)
      0x100000f57 <+87>:  movq   -0x18(%rbp), %rsi
      0x100000f5b <+91>:  movq   %rcx, %rdi
      0x100000f5e <+94>:  movb   $0x0, %al
      0x100000f60 <+96>:  callq  0x100000f6e               ; symbol stub for: NSLog
      0x100000f65 <+101>: xorl   %eax, %eax
      0x100000f67 <+103>: addq   $0x30, %rsp
      0x100000f6b <+107>: popq   %rbp
      0x100000f6c <+108>: retq   
       
    // sum 函数汇编代码
    test`sum:
      0x100000eb0 <+0>:  pushq  %rbp
      0x100000eb1 <+1>:  movq   %rsp, %rbp
      0x100000eb4 <+4>:  movq   0x18(%rbp), %rax		 ; 取出存储在 main 函数中的调用参数,存放到寄存器中(注意,这里是 rbp + 偏移地址,取的是上一个函数的栈空间的内容)
      0x100000eb8 <+8>:  movq   0x10(%rbp), %r10		 ; 取出存储在 main 函数中的调用参数,存放到寄存器中(注意,这里是 rbp + 偏移地址,取的是上一个函数的栈空间的内容)
      0x100000ebc <+12>: movq   %rdi, -0x8(%rbp)
      0x100000ec0 <+16>: movq   %rsi, -0x10(%rbp)
      0x100000ec4 <+20>: movq   %rdx, -0x18(%rbp)
      0x100000ec8 <+24>: movq   %rcx, -0x20(%rbp)
      0x100000ecc <+28>: movq   %r8, -0x28(%rbp)
      0x100000ed0 <+32>: movq   %r9, -0x30(%rbp)
      0x100000ed4 <+36>: movq   -0x8(%rbp), %rcx
      0x100000ed8 <+40>: addq   -0x10(%rbp), %rcx
      0x100000edc <+44>: addq   -0x18(%rbp), %rcx
      0x100000ee0 <+48>: addq   -0x20(%rbp), %rcx
      0x100000ee4 <+52>: addq   -0x28(%rbp), %rcx
      0x100000ee8 <+56>: addq   -0x30(%rbp), %rcx
      0x100000eec <+60>: addq   0x10(%rbp), %rcx		; 取出存储在 main 函数中的调用参数,进行运算。(注意,这里是 rbp + 偏移地址,取的是上一个函数的栈空间的内容)
      0x100000ef0 <+64>: addq   0x18(%rbp), %rcx		; 取出存储在 main 函数中的调用参数,进行运算(注意,这里是 rbp + 偏移地址,取的是上一个函数的栈空间的内容)
      0x100000ef4 <+68>: movq   %rax, -0x38(%rbp)
      0x100000ef8 <+72>: movq   %rcx, %rax
      0x100000efb <+75>: popq   %rbp
      0x100000efc <+76>: retq    
    

    MacBook 的 CPU 为64位 CPU,其专门用来存储函数形参的寄存器,就有6个。如果要触发源函数将目标函数的调用参数存储在自己的栈空间这个现象,源函数至少需要传递7个64位的参数给目标函数。

    根据汇编代码,函数调用的栈空间,如下图所示:
    汇编(九)_第4张图片

    打印 rbp 的地址,通过 rbp 地址 + 偏移量,读取相应内存里面的值,证实函数调用,形参存储在源函数栈空间中

    (lldb) po/x $rbp
    0x00007ffeefbff510
    
    (lldb) memory read 0x00007ffeefbff510+0x10
    0x7ffeefbff520: 07 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00  ................
    0x7ffeefbff530: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    
    (lldb) memory read 0x00007ffeefbff510+0x18
    0x7ffeefbff528: 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0x7ffeefbff538: 00 00 00 00 00 00 00 00 78 f5 bf ef fe 7f 00 00  ........x.......
    

全局变量

  • 64位的 CPU,一个寄存器就能保存指令或数据的物理地址,不需要像 8086CPU(16位) 那样使用地址加法器通过 段地址 + 偏移地址 来生成物理地址。换句话说,iMac、MacBook、iPhone、iPad 使用的 CPU 只有 IP寄存器,没有 CS寄存器。
  • 程序运行在操作系统上,由操作系统为其分配运行所需的资源,当然也包括内存空间。
    一个操作系统上,往往同时运行着许多不同的程序,实时运行环境复杂。同一个程序反复运行,操作系统为其分配的内存空间的地址(虚拟地址也好,物理地址也好),往往是不同的。
    我们可以认为,程序每次运行,其基地址都是变化的。但是程序内部的全局变量,相对于程序基地址的偏移量是固定的,不会因为程序的反复运行而改变。因此,对于一个程序来说 ,可以通过找到其在内存中的基地址,加上其全局变量相对于基地址的固定偏移量,来唯一定位到此全局变量,并对全局变量进行操作
    汇编(九)_第5张图片

如下代码运行 4 次,观察全局变量 a 和 b 对应的汇编代码,可以看到:
1.程序每次运行,全局变量 a 和 b 的物理地址是不一样的(因为程序每次运行,操作系统为其分配的基地址不一样)
2.程序每次运行,全局变量 a 和 b 的偏移地址是一样的(除非修改代码,否则全局变量相对于程序的基地址的偏移量,是固定的)

// 全局变量 a 和 b
int a = 10;
int b = 11;

void sum() {
     NSLog(@"a = %d, b = %d", a, b);
}
 
int main(int argc, const char * argv[]) {
     NSLog(@"a = %d, b = %d", a, b);    
     return 0;
}

// 第一次
0x100000f4d <+29>: movl   0x11bd(%rip), %ecx        ; a
0x100000f53 <+35>: subl   0x11bb(%rip), %ecx        ; b
// 第二次
0x102140f4d <+29>: movl   0x11bd(%rip), %ecx        ; a
0x102140f53 <+35>: subl   0x11bb(%rip), %ecx        ; b
// 第三次    
0x10dfc1f4d <+29>: movl   0x11bd(%rip), %ecx        ; a
0x10dfc1f53 <+35>: subl   0x11bb(%rip), %ecx        ; b
// 第四次
0x107266f4d <+29>: movl   0x11bd(%rip), %ecx        ; a
0x107266f53 <+35>: subl   0x11bb(%rip), %ecx        ; b

CPU 执行指令的过程为:
1.读取指令进入指令缓冲区
2.修改指令指针寄存器(IP),让其指向下一条指令的地址
3.执行指令
我们断点在一条汇编指令上,证明这条汇编指令还没被读取进入指令缓冲区,此时打印 IP寄存器 的值,为本条指令的地址(因为CPU在执行上一条指令的过程中,将 IP寄存器 的值,修改为本条指令的地址)。当我们执行本条指令时,IP寄存器 实际上保存的是,下一条指令的地址。
汇编(九)_第6张图片
不同的函数可以使用同一个全局变量,因此,汇编中操作 全局变量,使用的是地址

注意

  • 在8086汇编中,函数内部取形参是通过 bp指针 +,函数内部取局部变量是通过 bp指针 -

  • 根据函数调用时,需要进行现场保护的特性,我们可以很容易判断出一个函数的开始位置和结束位置
    步骤1 ~ 步骤4 标识着函数的开始
    步骤6 ~ 步骤9 标识着函数的结束

    ; 代码段
    code segment
    
    	; 为保险起见,显式设置一下 ds寄存器 和 ss寄存器
    	mov ax, data
    	mov ds, ax
    	mov ax, stack
    	mov ss, ax
    	
    	; 从这里开始调用 SumFunc 函数,将 SumFunc 函数的两个形参入栈
    	push 0003H
    	push 0004H 
    	call SumFunc   
      
    	; 退出程序
    	mov ah, 4cH
    	int 21H  
    	
    	;--------------------------------------------------------------
    	; 功能函数 - 求和
    	SumFunc: 
    	
    	; 1.保存 bp寄存器 的值,bp寄存器 一定是在进入函数的第一刻,就被保护
    	push bp
    	
    	; 2.使用 bp寄存器 记录 sp寄存器 的值
    	mov bp, sp
    	
    	; 3.通过减小 sp寄存器 的值,来提升栈空间,用于存储函数的局部变量
    	sub sp, 20H
    	
    	; 4.保护函数内部需要用到的 通用寄存器
    	push bx
    	push cx
    	push dx
    	
    	; 5.函数功能具体实现
    	
    	; 6.恢复 通用寄存器 的值,根据栈后进先出的特性,最后 push 进栈的通用寄存器,最先被 pop 出栈
    	pop dx
    	pop cx
    	pop bx
    	
    	; 7.通过增加 sp指针 的值来恢复 sp指针 的位置,此时函数内部的局部变量被释放(清空)
    	; 前面提升栈空间的时候,sp指针的值减少了多少,现在恢复栈空间的时候,sp指针 的值就增加多少
    	; 也可以写成 mov sp, bp
    	add sp, 20H
    	 
    	; 8.恢复 bp指针 的位置
    	pop bp
    	
    	; 9.函数返回,并且进行内平栈
    	ret 4
    	
    code ends       
    
  • 在 8086CPU 中,ax寄存器 一般用于保存函数的返回值。

  • 8086汇编的平栈机制和ARM汇编的平栈机制不一样。

  • 在 Objective-C 中,任何方法的调用,都会有两个默认的隐藏参数:方法调用者(self) 和 方法编号(cmd)。

你可能感兴趣的:(汇编基础)