公众号原文链接:https://mp.weixin.qq.com/s/qXYnIezKNuXhOBN7nZouZw

问题引入

不多解释,先写个小程序:

void func1(a,b,c)int a,b,c;{}void func2(a,b,c,d,e,f)int a,b,c,d,e,f;{}void func3(a,b,c,d,e,f,g,h)int a,b,c,d,e,f,g,h;{}int main(){  int a,b,c,d,e,f,g,h;  func1(a,b,c);  func2(a,b,c,d,e,f);  func3(a,b,c,d,e,f,g,h);}

从程序中可以看出,函数分别带有3个,6个,8个入参,那么这些入参是如何做的呢?如果站在汇编的角度,实际上可以采用两种方案:

  • 采用通用寄存器传参;

  • 采用栈传参;

为了证明函数如何传参,首先用GCC生成汇编代码:

gcc arg-pass.c -S

这样,我们就获的C代码对应的x86平台的汇编代码(为了显示清晰,我删除了以"."开头的标记性语句):

func1:  pushq  %rbp  movq  %rsp, %rbp  movl  %edi, -4(%rbp)  movl  %esi, -8(%rbp)  movl  %edx, -12(%rbp)  popq  %rbp  retfunc2:  pushq  %rbp  movq  %rsp, %rbp  movl  %edi, -4(%rbp)  movl  %esi, -8(%rbp)  movl  %edx, -12(%rbp)  movl  %ecx, -16(%rbp)  movl  %r8d, -20(%rbp)  movl  %r9d, -24(%rbp)  popq  %rbp  retfunc3:  pushq  %rbp  movq  %rsp, %rbp  movl  %edi, -4(%rbp)  movl  %esi, -8(%rbp)  movl  %edx, -12(%rbp)  movl  %ecx, -16(%rbp)  movl  %r8d, -20(%rbp)  movl  %r9d, -24(%rbp)  popq  %rbp  retmain:  pushq  %rbp  movq  %rsp, %rbp  subq  $48, %rsp  movl  -12(%rbp), %edx  movl  -8(%rbp), %ecx  movl  -4(%rbp), %eax  movl  %ecx, %esi  movl  %eax, %edi  movl  $0, %eax  call  func1  movl  -24(%rbp), %r8d  movl  -20(%rbp), %edi  movl  -16(%rbp), %ecx  movl  -12(%rbp), %edx  movl  -8(%rbp), %esi  movl  -4(%rbp), %eax  movl  %r8d, %r9d  movl  %edi, %r8d  movl  %eax, %edi  movl  $0, %eax  call  func2  movl  -24(%rbp), %r9d  movl  -20(%rbp), %r8d  movl  -16(%rbp), %ecx  movl  -12(%rbp), %edx  movl  -8(%rbp), %esi  movl  -4(%rbp), %eax  movl  -32(%rbp), %edi  movl  %edi, 8(%rsp)  movl  -28(%rbp), %edi  movl  %edi, (%rsp)  movl  %eax, %edi  movl  $0, %eax  call  func3  leave  ret

接下来,我们就可以进行一次分析。

x86寄存器

首先,我需要先给出X86相关寄存器,又例如我们分析上面的汇编代码。

因为x86都是向后兼容的,所以在x86-64架构下,32bit和16bit的寄存器都是可以使用的,32bit通用寄存器如下:

寄存器 描述
eax 操作数的运算、结果
ebx 指向DS段中的数据的指针
ecx 字符串操作或者循环计数器
edx 输入输出指针
esi 指向DS寄存器所指示的段中某个数据的指针,字符串操作中的复制源
edi 指向ES寄存器所指示的段中某个数据的指针,字符串操作中的目的
esp SP寄存器
ebp 指向栈上数据的指针

x86-64架构寄存器如下:

寄存器 描述
rdi 传递第一个参数
rsi 传递第二个参数
rdx 传递第三个参数或者第二个返回值
rcx 传递第四个参数
r8 传递第五个参数
r9 传递第六个参数
rax 临时寄存器或者第一个返回值
rsp sp寄存器
rbp 栈帧寄存器

在了解了寄存器之后,我们就可以分析上面说的函数调用过程了。

栈结构分析

首先我们看函数1:

void func1(a,b,c)int a,b,c;{}

调用方式为:

  func1(a,b,c);

上面对应的在main中调用汇编为:

  movq  %rsp, %rbp  subq  $48, %rsp  movl  -12(%rbp), %edx  movl  -8(%rbp), %ecx  movl  -4(%rbp), %eax  movl  %ecx, %esi  movl  %eax, %edi  movl  $0, %eax  call  func1

上面做了什么?注意栈指针rsp的移动就能理解了。

栈的起始状态为:

给rsp减值48,这很好玩,就这么个操作,直接在栈上申请了48字节的空间,是不是比brk()系统调用还快。

为啥这里会有

  movl  -12(%rbp), %edx  movl  -8(%rbp), %ecx  movl  -4(%rbp), %eax

到底谁才是abc?说实话,我也糊涂了,所以说,我们还是给他们初始值吧。

int main(){  int a,b,c,d,e,f,g,h;  a = 1;  b = 2;  c = 3;  d = 4;  e = 5;  f = 6;  g = 7;  h = 8;  func1(a,b,c);  func2(a,b,c,d,e,f);  func3(a,b,c,d,e,f,g,h);}

反汇编为:

main:  pushq  %rbp  movq  %rsp, %rbp  subq  $48, %rsp  movl  $1, -4(%rbp)  movl  $2, -8(%rbp)  movl  $3, -12(%rbp)  movl  $4, -16(%rbp)  movl  $5, -20(%rbp)  movl  $6, -24(%rbp)  movl  $7, -28(%rbp)  movl  $8, -32(%rbp)  movl  -12(%rbp), %edx  movl  -8(%rbp), %ecx  movl  -4(%rbp), %eax  movl  %ecx, %esi  movl  %eax, %edi  movl  $0, %eax  call  func1    movl  -24(%rbp), %r8d  movl  -20(%rbp), %edi  movl  -16(%rbp), %ecx  movl  -12(%rbp), %edx  movl  -8(%rbp), %esi  movl  -4(%rbp), %eax  movl  %r8d, %r9d  movl  %edi, %r8d  movl  %eax, %edi  movl  $0, %eax  call  func2    movl  -24(%rbp), %r9d  movl  -20(%rbp), %r8d  movl  -16(%rbp), %ecx  movl  -12(%rbp), %edx  movl  -8(%rbp), %esi  movl  -4(%rbp), %eax  movl  -32(%rbp), %edi  movl  %edi, 8(%rsp)  movl  -28(%rbp), %edi  movl  %edi, (%rsp)  movl  %eax, %edi  movl  $0, %eax  call  func3    leave  ret

那我们重新分析,main是如何调用func1的吧!还是这个代码:

  movq  %rsp, %rbp  subq  $48, %rsp  movl  $1, -4(%rbp)  movl  $2, -8(%rbp)  movl  $3, -12(%rbp)  movl  $4, -16(%rbp)  movl  $5, -20(%rbp)  movl  $6, -24(%rbp)  movl  $7, -28(%rbp)  movl  $8, -32(%rbp)  movl  -12(%rbp), %edx  movl  -8(%rbp), %ecx  movl  -4(%rbp), %eax  movl  %ecx, %esi  movl  %eax, %edi  movl  $0, %eax  call  func1

函数调用前的栈

根据上面的汇编代码分析,实际上就变成了这样:

上面的流程实际上只是main函数中的局部变量的初始化,接下来的汇编就要开始调用func1了,首先使用通用寄存器存放abc:

下面就要像我在描述通用寄存器的时候说的

寄存器 描述
rdi 传递第一个参数
rsi 传递第二个参数
rdx 传递第三个参数或者第二个返回值
rax 临时寄存器或者第一个返回值

分别使用通用寄存器edi、esi、edx保存了三个参数a、b、c,eax会保存函数的返回值,所以需要置零。后续就可以调用func1了。

函数调用的栈

首先看下func1的汇编代码:

func1:  pushq  %rbp  movq  %rsp, %rbp  movl  %edi, -4(%rbp)  movl  %esi, -8(%rbp)  movl  %edx, -12(%rbp)  popq  %rbp  ret

在刚进入func1时,栈空间是下面这样的:

首先将rbp压栈,实际上保存的不是rbp,而是保存的rbp的值:

为了更加清楚动人,还是给之前的rbp设定个数值(0x40000)吧:

接下来,需要把通用寄存器存至函数栈:

为什么要做上面这一步?我直接用寄存器不好吗?为什么非要存?

答案很简单,因为我这里写的函数为:

void func1(a,b,c)int a,b,c;{}

函数体为空,也就是完全没有任何其他的操作,在复杂的函数体中,通用寄存器的使用还有很多,所以编译器必须这么做。【这里抛出一个问题,如果编译器能知道当前函体为空,那么我使用gcc -O3优化编译,会省略上图的压栈操作吗?感兴趣可以试一试】。

执行完上面的操作,函数可以返回了,在返回前(调用ret前),需要恢复栈空间为调用func1之前:

接着就可以返回了。下面我们分析超过6个参数的函数调用。

超过6个入参的函数调用

下面我们分析超过6个参数的函数调用。假定func2已经返回【因为func2位六个入参,流程基本上和func1函数相同,直接看超过6个入参的情况】,此时的栈空间为:

直接看main函数中的函数调用部分汇编代码:

movl  -24(%rbp), %r9dmovl  -20(%rbp), %r8dmovl  -16(%rbp), %ecxmovl  -12(%rbp), %edxmovl  -8(%rbp), %esimovl  -4(%rbp), %eaxmovl  -32(%rbp), %edimovl  %edi, 8(%rsp)movl  -28(%rbp), %edimovl  %edi, (%rsp)movl  %eax, %edimovl  $0, %eaxcall  func3

首先,先给寄存器赋值:

接着使用栈上的空闲空间存储edi【8】:

接着,在将【7】入栈:

然后,将【1】保存到edi寄存器,这与我们上面表格中给出的rdi保存第一个参数是一致的。

在这一波操作之后,就可以调用func3了,当然,还是先看一眼func3的汇编代码:

func3:  pushq  %rbp  movq  %rsp, %rbp  movl  %edi, -4(%rbp)  movl  %esi, -8(%rbp)  movl  %edx, -12(%rbp)  movl  %ecx, -16(%rbp)  movl  %r8d, -20(%rbp)  movl  %r9d, -24(%rbp)  popq  %rbp  ret

函数原型为:

void func3(a,b,c,d,e,f,g,h)int a,b,c,d,e,f,g,h;{}

首先还是先将rbp入栈:

然后分别将通用寄存器入栈:

然后将rbp出栈,恢复到main调用func3之前的栈结构:

结论

在x86平台上,函数调用过程中,如果没有超过6个入参,那么直接使用寄存器就可以了,如果超过6个入参,那么超出6个的参数将通过栈传递。

x86-64函数调用参数传递相关推荐

  1. 第19部分- Linux x86 64位汇编GDB单步调试

    第19部分- Linux x86 64位汇编GDB单步调试 本篇我们使用gdb来调试上篇中的汇编代码. gdb调试 使用gdb进行调试. #gdb ./addsum_arg 设置参数: (gdb) s ...

  2. 第41部分-Linux x86 64位汇编MMX使用

    第41部分-Linux x86 64位汇编MMX使用 使用MMX架构需要一下步骤 从整数值创建打包整数值 把打包整数值加载到MMX寄存器中 对打包整数值执行MMX数学操作. 从MMX寄存器获得结果放到 ...

  3. 第77部分- Linux x86 64位汇编 优化编译器代码

    第77部分- Linux x86 64位汇编 优化编译器代码-O1/-O2/-O3 仅仅使用汇编语言代码替换C或者C++不会必然使得程序执行的更好,因为编译器已经把所有高级语言代码都转化成了汇编语言. ...

  4. linux的x64与x86_在Linux x86 64机器上链接

    linux的x64与x86 Linking is the process of combining various pieces of code and files in order to const ...

  5. 64位x86的函数调用栈布局

    作者:gfree.wind@gmail.com 博客:blog.focus-linux.net    linuxfocus.blog.chinaunix.net 在看本文之前,如果不了解x86的32位 ...

  6. qemu+linux+x86+64,qemu以64位跟32位的方式跑busybox

    qemu以64位和32位的方式跑busybox 两种方式x86_64 和32位的i386方式 -----------x86_64------------------------------------ ...

  7. 阿里云发布全新开源操作系统『龙蜥』,支持 X86 64 和 ARM 64 架构及鲲鹏、飞腾等芯片...

    公众号关注 「奇妙的 Linux 世界」 设为「星标」,每天带你玩转 Linux ! 近日,2021 云栖大会上,阿里云发布了全新操作系统 "龙蜥"(Anolis OS),并宣布开 ...

  8. X86 64位和32位

    不同的CPU都能够解释的机器语言的体系称为指令集架构(ISA,Instruction Set Architecture),也可以称为指令集(instruction set).Intel将x86系列CP ...

  9. 栈溢出攻击系列:shellcode在linux x86 64位攻击获得root权限(二)shellcode

    shellcode 是一组指令opcode, 是可以被程序运行,因为shellcode是要直接操作寄存器和函数,所以opcode 必须是十六进制的形式. 既然是攻击,那shellcode 主要的目的是 ...

最新文章

  1. python招聘笔试题_滴滴2020年春招笔试题分析(Python)
  2. Android camera开发总结
  3. JVM(六)为什么新生代有两个Survivor分区?
  4. java 假设当前时间_Java如何比较当前时间是否在两个时间范围内
  5. 《数据科学与大数据分析——数据的发现 分析 可视化与表示》一2.3 第2阶段:数据准备...
  6. 其实,人的核心职场时间是有限的,一定要和高手玩
  7. 南京:第三届软博会“外包”将唱主角
  8. Linux C++ 简单爬虫
  9. 今天,给我妈打电话聊了我爸
  10. @codeforces - 786E@ ALT
  11. STM32 F103 时钟树详解
  12. unreal ue4 虚幻 websocket Server websocket服务 插件使用及下载 非官方自己写的
  13. 用例图、功能模块图和数据库的区别
  14. 微信Windows版无法备份聊天记录
  15. 小白都能懂的设计模式 java版 抽象工厂模式 实战练习(超详细)
  16. Flowable API 瞬时变量
  17. C++的errorC2039和C2679的解决
  18. 百慕大永中为何有权继续开发集成Office?
  19. Schrodinger软件学习计算机辅助药物设计——基本操作以及分子对接
  20. android开发对Webview的应用

热门文章

  1. Java进阶资源汇总
  2. 计算机网络 多个站点共享信道的方式图
  3. 用Elman做时序预测
  4. Javascript的两种“单引号”
  5. 你不知道的JS之作用域和闭包(二)词法作用域
  6. Fabric chaincode开发调试
  7. CTFbugku--菜鸟初学
  8. thinkphp框架的优缺点
  9. Convert Sorted Array to Binary Search Tree With Minimal Height
  10. 关于IE8以上 不引人css 症状