C语言函数参数入栈的汇编理解

原创 2013年12月04日 22:11:51

先来看这样一段程序:

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

void print1(int a,int b,int c)
{
    printf("%p\n",&a);
    printf("%p\n",&b);
    printf("%p\n",&c);
}

int main(void)
{
   print1(1,2,3);
   exit(0);
}

它的输出是:

0022FF40
0022FF44
0022FF48
发现a,b,c的地址是逐渐增大的,差值是4个字节。这和我所知道的:C函数参数入栈的顺序是从右到左 是相匹配的,而且地址的增大值也

与变量所占的字节数相匹配。

不过当把程序稍微做一下修改,如下:

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

void print2(char a,char b,char c)
{
   printf("%p\n",&a);
   printf("%p\n",&b);
   printf("%p\n",&c);
}

int main(void)
{
   print2(1,2,3);
   exit(0);
}
再观察一下它的输出:

0022FF2C
0022FF28
0022FF24
怎么和上面的效果是相反的!虽然我知道这肯定编译器的一个技巧,不过 参数入栈的顺序是从右到左的 概念却动摇了。

为了弄清楚其中的道理,必须观察程序生成的中间.s文件,为此,我执行了以下一条命令:

gcc -S test.c(当前C文件中保存的程序是文章一开始的那个) 在当前目录下生成test.s文件
使用vim打开test.s文件(只截取主要内容了):

esp是指向栈顶的指针,ebp是用来备份这个指针的。栈的形状如下:

esp

ebp

|____________________________________________________

栈的最大值    栈的最小值

每压入一个参数入栈,就执行  esp = esp - sizoeof(参数)。不过在esp值变之前,先备份一下 ebp = esp,这样不管最后esp指到哪里去了,函数结束时就用这个ebp就能顺利回到调用者了。

print1:
   pushl %ebp//6.先把ebp压栈,保存这个指针
   movl  %esp, %ebp//7.使ebp这个指针保存着esp这个指针指向的地址值
   subl  $8, %esp//8.使esp - 8,也就是说空下8个字节以便实现某个功能
   leal   8(%ebp), %eax//9.把(ebp + 8)的地址给eax 这个地方为什么要+8 因为这个函数在经历第5,6步的时候存在着压了两个4字节入栈的操作。此时+8就指向了实参1
   movl  %eax, 4(%esp)//10.这个时候就用到第8步空下来的8个字节中的4个了,原来是保存值,原理就是用C语言写两个数交换值时的那个第三个变量,即缓冲区
   movl  $.LC0, (%esp)	//11.把字符串“%p\n”压栈     从第10,11步来看,两个参数的入栈顺序,其实不管顺序了,两个参数,最右边的在高地址,最左边的在低地址
   call  printf//12.调用函数printf,又是压栈出栈的操作了	到此可以得到8个字节的缓冲区全部用完了
   leal  12(%ebp), %eax//13.同第9步,此时获取的是实参2的地址
   movl  %eax, 4(%esp)//14.我想说同上
   movl  $.LC0, (%esp)	//15.我想说同上
   call  printf//16.我想说同上
   leal  16(%ebp), %eax//17.同第9步,此时获取的是实参3的地址
   movl  %eax, 4(%esp)//18.我想说同上
   movl  $.LC0, (%esp)	//19.我想说同上
   call  printf//20.我想说同上。到了此处我们就知道,printf打印参数的地址,这个地址是在main函数中压栈时分配的,是什么就是什么,符合参数入栈的顺序是从右到左这个说法。
   leave
   ret 
   .size print1, .-print1
.globl main
   .type main, @function
main:
   leal  4(%esp), %ecx
   andl  $-16, %esp
   pushl -4(%ecx)
   pushl %ebp
   movl  %esp, %ebp
   pushl %ecx
   subl  $20, %esp//1.先把栈预留20个字节,这其中的原因(为什么是20)估计与什么算法有关
   movl  $3, 8(%esp)//2.看!,先把3放入esp + 8
   movl  $2, 4(%esp)//3.再看!,把2放入esp + 4
   movl  $1, (%esp)//4.最后把1放入esp
   call  print1//5.调用函数print1。至此可以看到参数1,2,3是从右往左压入栈的,3在最高最地址(相对于1的保存地址来说),而1就在最低地址了(相对于3的保存地址来说)
   movl  $0, (%esp)
   call  exit
   .size main, .-main

好的,这个程序分析完了,再来看有疑问的程序吧:

print2:
   pushl %ebp//5.我想说同上
   movl  %esp, %ebp//6.我想说同上
   subl  $24, %esp//7.这个就不同上了,比上面那个esp - 8大很多吗,不过要记住,这24个字节是个缓冲区
   movl  8(%ebp), %eax//8.把实参1放入eax
   movl  12(%ebp), %edx//9.把实参2放入edx
   movl  16(%ebp), %ecx//10.把实参3放入ecx
   movb  %al, -4(%ebp)//11.把eax的低字节放入ebp - 4
   movb  %dl, -8(%ebp)//12.把edx的低字节放入ebp - 8
   movb  %cl, -12(%ebp)//13.把ecx的低字节放入ebp -12。从这个地方就可以发现问题的出现原因了,此时的实参1所存放的地址高于存放实参3的地址。到此,24字节的缓冲区已经使用了12
   leal  -4(%ebp), %eax//14.把实参1存放的地址放入eax
   movl  %eax, 4(%esp)//15.把实参1放入esp + 4
   movl  $.LC0, (%esp)	//16.把字符串“%p\n”放入esp
   call  printf//17.调用函数printf。从此依然可以看出函数参数的入栈地址是最右边的在高地址,最左边的在低地址。到此24字节的缓冲区使用了20个,还余下4个没有用
   leal  -8(%ebp), %eax//18.我想说同上
   movl  %eax, 4(%esp)//19.我想说同上
   movl  $.LC0, (%esp)	//20.我想说同上
   call  printf//21.我想说同上
   leal  -12(%ebp), %eax//22.我想说同上
   movl  %eax, 4(%esp)//23.我想说同上
   movl  $.LC0, (%esp)	//24.我想说同上
   call  printf//25.我想说同上
   leave
   ret
   .size print2, .-print2
.globl main
   .type main, @function
main:
   leal  4(%esp), %ecx
   andl  $-16, %esp
   pushl -4(%ecx)
   pushl %ebp
   movl  %esp, %ebp
   pushl %ecx
   subl  $20, %esp
   movl $3, 8(%esp)//1.和上面那个程序一样的压栈操作
   movl $2, 4(%esp)//2.我想说同上
   movl $1, (%esp)//3.我想说同上
   call  print2//4.我想说同上
   movl  $0, (%esp)
   call  exit
   .size main, .-main


结束了,知道了原因了。这计算机执行函数的时候不停的压栈出栈,执行这些精细的操作真是太牛了,不过计算机没有情感,它不会评估这个复杂度,只要一条条执行就行了,就像我们抄作文似的,作文

最后好不好不是我们能决定的,而是作文的作者。我暴露了-_-|||。

谢谢观赏!

此处的汇编语言是AT&T汇编,相关学习资料地址:点击打开链接

本文参考文章地址:点击打开链接





C函数与汇编函数之间参数及返回值传递方法

AAPCS对ARM结构的一些标准做了定义,在这里我们只重点介绍函数调用部分,如图8所示,AAPCS为ARM的R0~R15寄存器做了定义,明确了它们在函数中的职责: 图 8 AAPCS关于ARM寄存...
  • zuotian5212008
  • zuotian5212008
  • 2015年07月22日 16:06
  • 3983

汇编学习第五课之函数参数传递,函数返回值

最近在看《C++反汇编和逆向分析》这本书,还是收获不少. 自己也做实验来验证下心里的想法.函数参数是通过栈结构进行传递,在C++代码中,默认函数调用约定下(有三种函数调用约定,区别在于函数参数入栈顺...
  • haolipengzhanshen
  • haolipengzhanshen
  • 2016年05月13日 11:58
  • 989

arm汇编程序调用C函数之参数传递

对于ARM体系来说,不同语言撰写的函数之间相互调用(mix calls)遵循的是 ATPCS(ARM-Thumb Procedure Call Standard),ATPCS主要是定义了函数呼叫时参数...
  • sinat_24088685
  • sinat_24088685
  • 2016年06月23日 09:51
  • 750

x86 和 x64 汇编调用C 函数参数传递规则(GCC)

在ubuntu12.04 AMD64 位系统下
  • liliang365
  • liliang365
  • 2014年04月11日 15:05
  • 3188

汇编向函数传参数

今天师兄讲了下汇编如何向函数传参数,为了怕以后忘记,先记在这里。在老内核里面,汇编往函数传递参数是第一个参数是栈顶,依次往后。但是得在函数定义的时候用asmlinkage 前缀,是函数强制从汇编里面取...
  • lvqqrainbow
  • lvqqrainbow
  • 2010年05月12日 10:46
  • 1403

C函数与汇编函数之间参数及返回值传递方法

AAPCS对ARM结构的一些标准做了定义,在这里我们只重点介绍函数调用部分,如图8所示,AAPCS为ARM的R0~R15寄存器做了定义,明确了它们在函数中的职责:                  ...
  • hcx25909
  • hcx25909
  • 2013年01月14日 19:41
  • 8918

C语言中函数参数压栈方式为什么是从右到左的?

从论坛回答中摘出3个比较有说服力的观点: 一、 先通过一个小程序来看一看: #include void foo(int x, int y, int z) { printf("x = %...
  • own_ss
  • own_ss
  • 2016年06月01日 22:45
  • 2062

C语言中函数参数的入栈顺序

C语言中函数参数的入栈顺序 http://www.cnblogs.com/xkfz007/archive/2012/03/27/...
  • u014181676
  • u014181676
  • 2014年03月25日 21:51
  • 1380

汇编 压栈出栈

一 PS:EBP是当前函数的存取指针,即存储或者读取数时的指针基地址;ESP就是当前函数的栈顶指针。每一次发生函数的调用(主函数调用子函数)时,在被调用函数初始时,都会把当前函数(主函数)的EB...
  • hit_shaoqi
  • hit_shaoqi
  • 2016年03月20日 20:54
  • 2339

函数参数的压栈过程

1 _cdecl    按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于“C”函数或者变量,修饰名是在函数名前加下划线。对于“C++”函数,有所不同。如函数void test(void)的修饰名...
  • xyw_Eliot
  • xyw_Eliot
  • 2014年02月26日 20:59
  • 1266
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:C语言函数参数入栈的汇编理解
举报原因:
原因补充:

(最多只允许输入30个字)