• X86_64 栈和函数调用


    引言

      大家都知道函数调用是通过栈来实现的,而且知道在栈中存放着该函数的局部变量。但是对于栈的实现细节可能不一定清楚。本文将介绍一下在 x86 平台下函数栈是如何实现的。

    1、x86-64 汇编

      Intel 系列处理器通常称为x86,目前常用的笔记本或台式机都是 64 位的处理器,这些处理器使用的机器语言一般都是 x86_64,我记得以前学习微机原理课的时候,学习的还是 8086 处理器上的汇编。8086 是Intel的第一代16位的处理器,只有8个16位的寄存器,而现在的 64 位处理器对其进行了扩展,共有16个64位的寄存器。

    需要注意的是,这里采用的汇编代码是 ATT 格式的,与 Intel 格式的汇编码有些不同:

    1. Intel汇编码省略了指示大小的后缀,如ATT格式中的pushq,在Intel中为push
    2. Intel代码访问寄存器时省略了%,如ATT格式是%rbx,在Intel中为rbx
    3. ATT代码使用小括号( )来访问内存中的位置,而Intel代码使用中括号[ ]
    4. 操作数的顺序不同,如 ATT 中 movq %rax, %rbx,第一个操作数为源操作数,第二个为目的操作数,Intel格式正好相反。

    64位处理器中 16 个寄存器对理解 x86-64 汇编十分重要,见下图(图源 CSAPP):

    在这里插入图片描述

    • %rax 作为函数返回值使用。
    • %rsp 栈指针寄存器,指向栈顶
    • %rbp 栈桢指针,指向栈基
    • %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。
    • %rbx,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改
    • %r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
    • %rip: 相当于PC指针指向当前的指令地址,指向下一条要执行的指令

    2、运行时栈

      考虑C语言中的函数调用问题,当在一个程序中调用另一个程序的时候,如在函数 P 中调用函数Q,至少需要做以下几件事情:

    • 首先需要将程序计数器指针指向 Q 的起始地址,在 Q 函数返回的时候需要将程序计数器指向 P 中函数 Q 后面的那条指令;
    • 其次需要完成函数参数传递问题,以及函数返回值的传递问题;
    • 最后是函数Q运行时可能需要保存额外的局部变量。

    为了解决上述的问题,C语言调用时采用「运行时栈」来对函数的调用过程进行维护:
    在这里插入图片描述
    需要注意这里的栈是倒着画的,栈顶在下面,栈顶的内存地址是更小的,换句话说栈增长的方向是内存地址减小的方向。

    从上面图中我们可以看出,在函数 P 调用 Q 时,首先在函数 P 的「栈帧」压入了返回地址(Return address),指示函数 Q 执行完成之后的断点位置。然后函数 Q 执行的时候会开辟它自己的栈帧(实际上就是将栈顶指针%rsp减小),并可能会做以下几件事情:

    • 将一些必要的寄存器的值进行保存——(Saved register)(这些寄存器称为被调用者保存的寄存器(Callee saved),也就是函数 Q 有义务保证自己运行前后这些寄存器的值是保持不变的,因此函数 P 可以放心的将变量存储在这些寄存器中)
    • 保存 Q 中的一些局部变量(Local variables)
    • 若 Q 需要调用其它函数,且该函数的参数大于6个,则在 Q 的栈帧中存储这些参数的值。(后面会有示例)

    2.1 栈帧

      在 x86 系统的CPU中,rsp(stack pointer) 是栈指针寄存器,这个寄存器中存储着栈顶的地址。rbp(base pointer) 是基址指针寄存器,这个寄存器中存储着栈底的地址。函数栈空间主要是由这两个寄存器来确定的。

      当程序运行时,栈指针 rsp 可以移动,栈指针和帧指针 rbp 一次只能存储一个地址,所以,任何时候,这一对指针指向的是同一个函数的栈帧结构。

      而帧指针 rbp 是不移动的,访问栈中的元素可以用-4(%rbp)或者8(%rbp)访问 %rbp 指针下面或者上面的元素。

    2.2 代码讲解

    /* proc.c */
    void func1()
    {
    }
    
    int func2(int a, long b, char *c)
    {
        *c = a * b;
        func1();
        return a * b;
    }
    
    int main()
    {
        char value;
        int rc = func2(1, 2, &value);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    研究一个简单的函数示例对理解该过程有帮助.

    liangjie@liangjie-virtual-machine:~/Desktop/cfp$ gcc proc.c -o proc
    liangjie@liangjie-virtual-machine:~/Desktop/cfp$ objdump -d proc
    ......
    0000000000001149 <func1>:
        1149:	f3 0f 1e fa          	endbr64 
        114d:	55                   	push   %rbp
        114e:	48 89 e5             	mov    %rsp,%rbp
        1151:	90                   	nop
        1152:	5d                   	pop    %rbp
        1153:	c3                   	ret    
    
    0000000000001154 <func2>:
        1154:	f3 0f 1e fa          	endbr64 
        1158:	55                   	push   %rbp
        1159:	48 89 e5             	mov    %rsp,%rbp
        115c:	48 83 ec 18          	sub    $0x18,%rsp
        1160:	89 7d fc             	mov    %edi,-0x4(%rbp)
        1163:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)
        1167:	48 89 55 e8          	mov    %rdx,-0x18(%rbp)
        116b:	8b 45 fc             	mov    -0x4(%rbp),%eax
        116e:	89 c1                	mov    %eax,%ecx
        1170:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
        1174:	89 c2                	mov    %eax,%edx
        1176:	89 c8                	mov    %ecx,%eax
        1178:	0f af c2             	imul   %edx,%eax
        117b:	89 c2                	mov    %eax,%edx
        117d:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
        1181:	88 10                	mov    %dl,(%rax)
        1183:	b8 00 00 00 00       	mov    $0x0,%eax
        1188:	e8 bc ff ff ff       	call   1149 <func1>
        118d:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
        1191:	89 c2                	mov    %eax,%edx
        1193:	8b 45 fc             	mov    -0x4(%rbp),%eax
        1196:	0f af c2             	imul   %edx,%eax
        1199:	c9                   	leave  
        119a:	c3                   	ret    
    
    000000000000119b <main>:
        119b:	f3 0f 1e fa          	endbr64 
        119f:	55                   	push   %rbp
        11a0:	48 89 e5             	mov    %rsp,%rbp
        11a3:	48 83 ec 10          	sub    $0x10,%rsp
        11a7:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
        11ae:	00 00 
        11b0:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
        11b4:	31 c0                	xor    %eax,%eax
        11b6:	48 8d 45 f3          	lea    -0xd(%rbp),%rax
        11ba:	48 89 c2             	mov    %rax,%rdx
        11bd:	be 02 00 00 00       	mov    $0x2,%esi
        11c2:	bf 01 00 00 00       	mov    $0x1,%edi
        11c7:	e8 88 ff ff ff       	call   1154 <func2>
        11cc:	89 45 f4             	mov    %eax,-0xc(%rbp)
        11cf:	b8 00 00 00 00       	mov    $0x0,%eax
        11d4:	48 8b 55 f8          	mov    -0x8(%rbp),%rdx
        11d8:	64 48 2b 14 25 28 00 	sub    %fs:0x28,%rdx
        11df:	00 00 
        11e1:	74 05                	je     11e8 <main+0x4d>
        11e3:	e8 68 fe ff ff       	call   1050 <__stack_chk_fail@plt>
        11e8:	c9                   	leave  
        11e9:	c3                   	ret  
    ......
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    2.2.1 func1

    func1 函数比较简单,由 func1 先讲起。 func1 主要关注函数调用关系流程。

    func2 中的 call 指令意为:
    将 func2 () 中下一条语句的地址(也就是「mov -0x10(%rbp),%rax」这句的地址)压入栈中并修改 rip 的值为 func1() 的地址

      1188:	e8 bc ff ff ff       	call   1149 <func1>
    
    • 1

    在这里插入图片描述

    再看函数 func1,

    0000000000001149 <func1>:
        1149:	f3 0f 1e fa          	endbr64 
        114d:	55                   	push   %rbp         # 将 rbp 压栈
        114e:	48 89 e5             	mov    %rsp,%rbp	# 将 rsp -> rbp 
        1151:	90                   	nop
        1152:	5d                   	pop    %rbp			# 将 rbp 弹出
        1153:	c3                   	ret    				
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 通过执行push %rbp,使用将 rbp 压栈的方式,保存 rbp
    • 通过执行 mov %rsp,%rbp,原来的栈底(rbp 指向的位置)成为了新的栈顶(rsp 指向的位置)

    到这里,函数栈情况如下图
    在这里插入图片描述

    • 通过执行pop %rbp,将 rbp 弹出,恢复调用函数的栈基址寄存器 rbp
    • 通过执行ret,根据 rsp 指向的位置从栈中弹出返回位置(也就是上面 call 指令中压栈的值),并通过修改 rip 的值为返回地址。到这里,函数 func1 执行结束,转而返回到 func2 中
    2.2.2 func2

    我们再看看 func2,func2 则主要关注函数传参以及局部变量的存储。

    0000000000001154 <func2>:
        1154:	f3 0f 1e fa          	endbr64 
        1158:	55                   	push   %rbp					 # rbp 入栈保存值
        1159:	48 89 e5             	mov    %rsp,%rbp			 # 修改 rbp 指向地址为 rsp
        115c:	48 83 ec 18          	sub    $0x18,%rsp			 # 为 func2 函数的入参分配空间
        1160:	89 7d fc             	mov    %edi,-0x4(%rbp)		 # edi -> (rbp - 4)
        1163:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)		 # rsi -> (rbp - 16)
        1167:	48 89 55 e8          	mov    %rdx,-0x18(%rbp)		 # rdx -> (rbp - 24)
        116b:	8b 45 fc             	mov    -0x4(%rbp),%eax       # (rbp - 4) -> eax
        116e:	89 c1                	mov    %eax,%ecx			 # eax -> ecx
        1170:	48 8b 45 f0          	mov    -0x10(%rbp),%rax      # (rbp - 16)-> rax
        1174:	89 c2                	mov    %eax,%edx			 # eax -> edx
        1176:	89 c8                	mov    %ecx,%eax			 # ecx -> eax
        1178:	0f af c2             	imul   %edx,%eax			 # edx * eax -> eax
        117b:	89 c2                	mov    %eax,%edx		     # eax -> edx
        117d:	48 8b 45 e8          	mov    -0x18(%rbp),%rax		 # (rbp - 18)-> rax
        1181:	88 10                	mov    %dl,(%rax)			 # dl -> (rax)
        1183:	b8 00 00 00 00       	mov    $0x0,%eax
        1188:	e8 bc ff ff ff       	call   1149 <func1>
        118d:	48 8b 45 f0          	mov    -0x10(%rbp),%rax		 # (rbp - 16)-> rax
        1191:	89 c2                	mov    %eax,%edx			 # eax -> edx
        1193:	8b 45 fc             	mov    -0x4(%rbp),%eax		 # (rbp - 4)-> eax
        1196:	0f af c2             	imul   %edx,%eax			 # edx * eax -> eax
        1199:	c9                   	leave  
        119a:	c3                   	ret   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在这里插入图片描述

    • 通过以下片段,对 func2 入参进行压栈。这里需要注意的是,为了栈对齐,参数 b 放到了 rbp - 16 的位置,而不是 rbp - 12
        115c:	48 83 ec 18          	sub    $0x18,%rsp			 # 为 func2 函数的入参分配空间
        1160:	89 7d fc             	mov    %edi,-0x4(%rbp)		 # edi -> (rbp - 4)
        1163:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)		 # rsi -> (rbp - 16)
        1167:	48 89 55 e8          	mov    %rdx,-0x18(%rbp)		 # rdx -> (rbp - 24)
    
    • 1
    • 2
    • 3
    • 4
    • 通过以下片段,完成 C 语句:*c = a * b; 的功能
        116b:	8b 45 fc             	mov    -0x4(%rbp),%eax       # (rbp - 4) -> eax
        116e:	89 c1                	mov    %eax,%ecx			 # eax -> ecx
        1170:	48 8b 45 f0          	mov    -0x10(%rbp),%rax      # (rbp - 16)-> rax
        1174:	89 c2                	mov    %eax,%edx			 # eax -> edx
        1176:	89 c8                	mov    %ecx,%eax			 # ecx -> eax
        1178:	0f af c2             	imul   %edx,%eax			 # edx * eax -> eax
        117b:	89 c2                	mov    %eax,%edx		     # eax -> edx
        117d:	48 8b 45 e8          	mov    -0x18(%rbp),%rax		 # (rbp - 18)-> rax
        1181:	88 10                	mov    %dl,(%rax)			 # dl -> (rax)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 通过以下片段,完成 C 语句:a * b; 的功能,结果存储在 eax 寄存中,作为 func2 函数的返回值
        118d:	48 8b 45 f0          	mov    -0x10(%rbp),%rax		 # (rbp - 16)-> rax
        1191:	89 c2                	mov    %eax,%edx			 # eax -> edx
        1193:	8b 45 fc             	mov    -0x4(%rbp),%eax		 # (rbp - 4)-> eax
        1196:	0f af c2             	imul   %edx,%eax			 # edx * eax -> eax
    
    • 1
    • 2
    • 3
    • 4
    • 执行 leave 指令相当于执行如下两条指令:
    mov %rbp, %rsp
    pop %rbp
    
    • 1
    • 2
    • 通过 ret指令,根据 rsp 指向的位置从栈中弹出返回位置(也就是上面 call 指令中压栈的值),并通过修改 rip 的值为返回地址。到这里,函数 func2 执行结束,转而返回到 main 中

    在这里插入图片描述

  • 相关阅读:
    Docker项目部署
    .NET Upgrade Assistant 升级 .NET MAUI
    springboot+vue基于j2ee企业人力资源管理系统设计与实现(论文+项目源码)
    (Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一
    基于energy score的out-of-distribution数据检测,LeCun都说好 | NerulPS 2020
    K210跟随物体(控制舵机转动)
    专注于元宇宙实际应用方案的企业
    【Android】Kotlin学习小结
    【Maven】基础
    关于js_Element对象_属性和获取元素位置的方法
  • 原文地址:https://blog.csdn.net/weixin_43275558/article/details/125820015