原文链接:https://blog.fanscore.cn/p/27/

一、 函数调用相关指令

关于栈可以看下我之前的这篇文章x86 CPU与IA-32架构

在开始函数调用约定之前我们需要先了解一下几个相关的指令

1.1 push

  1. pushq 立即数 # q/l是后缀,表示操作对象的大小
  2. pushl 寄存器

push指令将数据压栈。具体就是将esp(stack pointer)寄存器减去压栈数据的大小,再将数据存储到esp寄存器所指向的地址。

1.2 pop

  1. popq 寄存器
  2. popl 寄存器

pop指令将数据出栈并写入寄存器。具体就是将数据从esp寄存器所指向的地址加载到指令的目标寄存器中,再将esp寄存器加上出栈的数据的大小。

1.3 call

  1. call 立即数
  2. call 寄存器
  3. call 内存

call指令会调用由操作数所代表的地址指向的函数,一般都是call一个符号。call指令会将当前指令寄存器中的内容(即这条call指令下一条指令的地址,也就是函数执行完的返回地址)入栈,然后跳到函数对应的地址开始执行。

1.4 ret

ret指令用于从子函数中返回,ret指令会先弹出当前栈顶的数据,这个数据就是先前调用这个函数的call指令压入的“下一条指令的地址”,然后跳转到这个地址执行。

1.5 leave

leave相当于执行了movq %rbp, %rsp; popq %rbp,即释放栈帧。

二、 函数调用约定

函数调用约定约定了caller如何传参即将实参放到何处,应该按照何种顺序保存,以及callee如何返回返回值即将返回值放到何处。

x86的32位机器之上C语言一般是通过栈来传递参数,且一般都是倒序push,即先push最后一个参数再push倒数第二个参数,并通过ax寄存器返回结果,这称为cdecl调用约定(C有三种调用约定,linux系统中使用cdecl),Go与之类似但是区别在于Go通过栈来返回结果,所以Go支持多个返回值。

x64架构中增加了8个通用寄存器,C语言采用了寄存器来传递参数,如果参数超过。在x64系统默认有System V AMD64Microsoft x64两种C语言函数调用约定,System V AMD64实际是System V AMD64 ABI文档的一部分,类UNIX系统多采用System V的调用约定。

System V AMD64 ABI文档地址https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf

本文主要讨论x64架构下Linux系统的函数调用约定即System V AMD64调用约定。

三、 x64架构下Linux系统函数调用

3.1 如何传递参数

System V AMD64调用约定规定了caller将第1-6个整型参数分别保存到rdirsirdxrcxr8r9寄存器中,第7个及之后的整型参数从右往左倒序的压入栈中。前8个浮点类型的参数放到xmm0-xmm7寄存器中,之后的浮点类型的参数从右往左倒序的压入栈中。

3.2 如何返回返回值

对于整型返回值要保存到rax寄存器中,浮点型返回值保存到xmm0寄存器中。

3.3 栈的对齐问题

System V AMD64要求栈必须按照16字节对齐,就是说在通过call指令调用目标函数之前栈顶指针即rsp指针必须是16的倍数。之所以要按照16字节对齐是因为x64架构引入了SSE和AVX指令,这些指令要求必须从16的整数倍地址取数,为了兼顾这些指令所以就要求了16字节对齐。

3.4 变长参数

这部分没看懂,待后续发掘。

四、 实际案例分析

4.1 案例1

看下下面这段C代码

  1. unsigned long long foo(unsigned long long param1, unsigned long long param2) {
  2. unsigned long long sum = param1 + param2;
  3. return sum;
  4. }
  5. int main(void) {
  6. unsigned long long sum = foo(8589934593, 8589934597);
  7. return 0;
  8. }

uname -a: Linux xxx 3.10.0-514.26.2.el7.x86_64 #1 SMP Tue Jul 4 15:04:05 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

gcc -v: gcc 版本 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)

转为汇编代码,gcc -S call.c

  1. .file "call.c"
  2. .text
  3. .globl foo
  4. .type foo, @function
  5. foo:
  6. .LFB0:
  7. .cfi_startproc
  8. pushq %rbp
  9. .cfi_def_cfa_offset 16
  10. .cfi_offset 6, -16
  11. movq %rsp, %rbp
  12. .cfi_def_cfa_register 6
  13. movq %rdi, -24(%rbp)
  14. movq %rsi, -32(%rbp)
  15. movq -32(%rbp), %rax
  16. movq -24(%rbp), %rdx
  17. addq %rdx, %rax
  18. movq %rax, -8(%rbp)
  19. movq -8(%rbp), %rax
  20. popq %rbp
  21. .cfi_def_cfa 7, 8
  22. ret
  23. .cfi_endproc
  24. .LFE0:
  25. .size foo, .-foo
  26. .globl main
  27. .type main, @function
  28. main:
  29. .LFB1:
  30. .cfi_startproc
  31. pushq %rbp
  32. .cfi_def_cfa_offset 16
  33. .cfi_offset 6, -16
  34. movq %rsp, %rbp
  35. .cfi_def_cfa_register 6
  36. subq $16, %rsp
  37. movabsq $8589934597, %rsi
  38. movabsq $8589934593, %rdi
  39. call foo
  40. movq %rax, -8(%rbp)
  41. movl $0, %eax
  42. leave
  43. .cfi_def_cfa 7, 8
  44. ret
  45. .cfi_endproc
  46. .LFE1:
  47. .size main, .-main
  48. .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39)"
  49. .section .note.GNU-stack,"",@progbits

我们先看main函数的汇编代码,main函数中首先执行了三条指令:

  1. pushq %rbp # 将当前栈基底地址压入栈中
  2. movq %rsp, %rbp # 将栈基底地址修改为栈顶地址
  3. subq $16, %rsp # 栈顶地址-16,栈扩容,这里没搞懂为什么要扩容,有懂的同学欢迎评论区指点下

这三条指令是用来分配栈帧的,执行完成后栈变成下方的样子:



继续往下看:

  1. movabsq $8589934597, %rsi # 先将第二个参数保存到rsi寄存器
  2. movabsq $8589934593, %rdi # 再将第一个参数保存到rdi寄存器
  3. call foo # 调用foo函数,这一步会将下一条指令的地址压到栈上

执行完call foo指令后,栈的情况如下:

然后我们跳到foo函数中看下:

  1. pushq %rbp # 将当前栈基底地址压入栈中
  2. movq %rsp, %rbp # 将栈基底地址修改为栈顶地址

开头仍然是建立栈帧的指令,执行完成后,此时栈帧的样子如下:

继续往下看:

  1. movq %rdi, -24(%rbp)
  2. movq %rsi, -32(%rbp)
  3. movq -32(%rbp), %rax # 将第二个参数保存到rax寄存器
  4. movq -24(%rbp), %rdx # 将第一个参数保存到rdx寄存器
  5. addq %rdx, %rax # 执行加法并将结果保存在rax寄存器
  6. movq %rax, -8(%rbp)
  7. movq -8(%rbp), %rax # 将返回值保存到rax寄存器

这里没搞懂为什么需要先挪到内存中再保存到rax寄存器上,可能是编译器实现起来比较方便吧,有懂的同学欢迎评论区指点下

此时栈情况:



foo函数最后执行了以下两条指令:

  1. popq %rbp # 将栈顶值pop出来保存到rbp寄存器,即修改栈基底地址为当前栈顶值,同时栈顶指针-8
  2. ret # 从子函数中返回到main函数中

最终结果如图:

4.2 案例2

我们修改下函数foo,使它接收9个参数验证下上面的理论。

  1. unsigned long long foo(unsigned long long param1, unsigned long long param2, unsigned long long param3, unsigned long long param4, unsigned long long param5, unsigned long long param6, unsigned long long param7, unsigned long long param8, unsigned long long param9) {
  2. unsigned long long sum = param1 + param2;
  3. return sum;
  4. }
  5. int main(void) {
  6. unsigned long long sum = foo(8589934593, 8589934597, 3, 4,5,6,7,8,9);
  7. return 0;
  8. }

编译为汇编后:

  1. foo:
  2. .LFB0:
  3. .cfi_startproc
  4. pushq %rbp
  5. .cfi_def_cfa_offset 16
  6. .cfi_offset 6, -16
  7. movq %rsp, %rbp
  8. .cfi_def_cfa_register 6
  9. movq %rdi, -24(%rbp)
  10. movq %rsi, -32(%rbp)
  11. movq %rdx, -40(%rbp)
  12. movq %rcx, -48(%rbp)
  13. movq %r8, -56(%rbp)
  14. movq %r9, -64(%rbp)
  15. movq -32(%rbp), %rax
  16. movq -24(%rbp), %rdx
  17. addq %rdx, %rax
  18. movq %rax, -8(%rbp)
  19. movq -8(%rbp), %rax
  20. popq %rbp
  21. .cfi_def_cfa 7, 8
  22. ret
  23. .cfi_endproc
  24. .LFE0:
  25. .size foo, .-foo
  26. .globl main
  27. .type main, @function
  28. main:
  29. .LFB1:
  30. .cfi_startproc
  31. pushq %rbp
  32. .cfi_def_cfa_offset 16
  33. .cfi_offset 6, -16
  34. movq %rsp, %rbp
  35. .cfi_def_cfa_register 6
  36. subq $40, %rsp
  37. movq $9, 16(%rsp) # 后6个参数放到栈上
  38. movq $8, 8(%rsp)
  39. movq $7, (%rsp)
  40. movl $6, %r9d # 前6个参数分别使用rdi rsi rdx ecx r8 r9寄存器
  41. movl $5, %r8d
  42. movl $4, %ecx
  43. movl $3, %edx
  44. movabsq $8589934597, %rsi
  45. movabsq $8589934593, %rdi
  46. call foo
  47. movq %rax, -8(%rbp)
  48. movl $0, %eax
  49. leave
  50. .cfi_def_cfa 7, 8
  51. ret

五、 参考资料

x64架构下Linux系统函数调用的更多相关文章

  1. win10 vmware下Linux系统联网

    本来,这个问题网上资源很多的,但是就因为多,就变得杂了,对于许多新手,并不理解为啥,故记录下来方便以后使用.此处我采用配置VWmare虚拟网关(上学期刚刚学计算机网络,正好可以复习下).关于虚拟机下L ...

  2. X86架构下Linux启动过程分析

    1.X86架构下的从开机到Start_kernel启动的整体过程 这个过程简要概述为: 开机-->BIOS-->GRUB/LILO-->Linux Kernel 其执行的流程图和重要 ...

  3. 高并发情况下Linux系统及kernel参数优化

    众所周知在默认参数情况下Linux对高并发支持并不好,主要受限于单进程最大打开文件数限制.内核TCP参数方面和IO事件分配机制等.下面就从几方面来调整使Linux系统能够支持高并发环境. Iptabl ...

  4. 虚拟机下Linux系统如何设置IP地址

    虚拟机下Linux系统设置IP地址三种方法 文章来源:https://jingyan.baidu.com/article/ea24bc399ffeb9da62b3318f.html 工具/原料   V ...

  5. Windows10下Linux系统的安装和使用

    WSL 以往我都是直接安装VirtualBox,然后再下载Linux系统的ISO镜像,装到VirtualBox里运行. 改用Win10系统后,了解到了WSL(Windows Subsystem for ...

  6. vmware下linux系统的安装过程

    虚拟机VMware下CentOS6.6安装教程图文详解 [日期:2016-05-24] 来源:Linux社区  作者:Sungeek [字体:大 中 小]   分享下,虚拟机VMware下CentOS ...

  7. VMware虚拟机下Linux系统的全屏显示

    在VMware虚拟机下的Linux无法全屏的问题的解决方案如下: 1.   启动虚拟机,并启动Redhat6.4. 2.   点击“view”——然后将Autofit window这个选项勾选.(一般 ...

  8. ARM架构下linux设备树加载的方法

    引入设备树后bootloader加载DTB方法: 1. 标准方法 将linux kernel放到内存地址为<kernel img addr>的内存中. 将DTB放到地址为<dtb a ...

  9. 如何设置Vmware下Linux系统全屏显示

    环境:Vmware10+RedHat5 在Vmware10中安装好RedHat5后,即使点击了全屏按钮(或使用快捷键Ctrl+Alt+Enter),全屏的效果依然不尽人意,跟下图中差不多,RedHat ...

随机推荐

  1. ctf-misc-图片隐写术套路总结

    1.直接右键notepad打开,搜索flag,如果图片很多的话,可以写py脚本也    可以打开后搜索全部打开文件 2.是一个压缩包,改了后缀 3.图片中藏了一个二维码,用Stegsolve加几次滤镜 ...

  2. 面试BAT问的最多的27道MyBatis 面试题(含答案和思维导图总结)

    前言 关于MyBatis总结了一个思维导图希望对大家有帮助 什么是 Mybatis? Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身, ...

  3. FL Studio带你走进混音的世界

    混音,是把多种音源整合到一个立体音轨或单音音轨中,通俗讲就是对多种声音进行调整后叠加在一起,这样可以让音乐听起来非常有层次感,尤其是在电音制作过程中,混音的质量更是起到了决定性的作用.音乐制作软件FL ...

  4. kubernetes-dashboard.yaml

    # Copyright 2017 The Kubernetes Authors.## Licensed under the Apache License, Version 2.0 (the " ...

  5. 获取qq头像

    1 <?php 2 header('content-type: image/jpeg'); 3 $QQ = $_GET['qq']; 4 $time2 = date('Y-m-d H:i:s') ...

  6. web文件上传漏洞

    什么是文件上传漏洞? 指利用系统的对文件上传部分的控制不足或处理缺陷,上传可执行的动态脚本文件/webShell进行攻击行为. 原因 对于上传文件的后缀名(扩展名)没有做较为严格的限制 对于上传文件的 ...

  7. LeetCode双周赛#35

    1589. 所有排列中的最大和 #差分 #贪心 题目链接 题意 给定整数数组nums,以及查询数组requests,其中requests[i] = [starti, endi] .第i个查询求 num ...

  8. 【mq读书笔记】mq producer启动流程

    创建MQClientInstance实例.这里有个实例缓存 clienrId为客户端IP+instance+(unitname)注意到之前把instance替换为进程id,是为了instance为默认 ...

  9. python—数据类型和变量

    在python中,能够直接处理的数据类型和变量有整数.浮点数.字符串.布尔值.空值.变量. 一.整数 1.python可处理任意大小的整数,包括负整数,在程序中的表示方法与在数学中的方法一样.例如:0 ...

  10. How tomcat works(深入剖析tomcat)阅读笔记1-4章

    How tomcat works chapter 1 简单的web服务器 这一张的主要内容就是实现一个简单的静态资源服务器,socket编程,利用java提供的socket和serverSocket编 ...