关于C语言中函数调用和参数传递机制的探讨(一 .无参数传递)

原帖及讨论:http://bbs.bccn.net/thread-106533-1-1.html

关于C语言中函数调用和参数传递机制的探讨

  
函数,相信许多人也知道其重要性;一个文件往往由一个或者多个函数构成的。然而可能许多人还不知道函数调用的一些深层问题,所以我写的这篇文章一来是应
了一个好朋友的要求而写,二来希望一些朋友能够从我这篇文章了解函数调用的机制。但是并不是每个人都可以完全读懂这文章,想完全读懂此文,我想必须具备三
个条件:

    一、对于C语言有一定的了解,最起码有一个整体的初步了解;

    二、能够读懂UNIX/LINUX下的AT&T语法的汇编;AT&T汇编与Intel汇编的差别还是挺大的;这个条件可能一些人就不具备了,但是你通过阅读此文相信也能对函数调用机制有一个大概的了解;

    三、看到这么长的文章,一定要有耐心,用心看相信应该多少有点帮助;

    好了,不讲废话了,进入主题吧。

一、基本知识框架了解:

    这部分主要讲一些基本的东西,主要是关于堆栈的知识。只有了解了堆栈的基础内容,才可以继续往下读。

1.概念性的知识:

    所谓堆栈,其实也就是程序使用的一种内存元素;它是内存中用来存放一些数据的区域。我曾经写过一篇文章发表在这个论坛上里面也谈到了堆和栈的区别;平常经常说的堆栈,其实也是栈,而不是堆,所以这里也一样。注意这和数据结构说的栈其实还是有区别的,不要混在一起。

2.堆栈的工作方式:
    
    平常我们所说的数据是怎么存放在内存的?是从低地址开始,然后按照数据占用字节大小往高地址逐个存放的。但堆栈就不一样了。堆栈的工作方式是数据插入堆栈区域然后从堆栈区域删除数据。这是概括的说法。具体是这样的:
    
    在UNIX/LINUX
中,堆栈是从高地址向低地址衍生的。这里得说一个重要的东东,那就是堆栈指针ESP。堆栈指针是什么?它永远指向堆栈中的顶部(但如果按照地址值来说却是
底部),是不是对顶部这个词的理解感觉有点模糊?就是说,比如说你压栈,就压进一个4字节的数据元素,那么ESP就向下移动了4个字节,注意这里是向下移
动,所以ESP应该指向了更低的地址,所以说它是指向了底部。你可以把堆栈想象成一个杯子,倒进水了水平线是不是上升了(这里把杯子最底端假设成高地址,
把顶端设为低地址),倒出水了水平线是不是下降了?就和压栈和进栈的道理一样的。如果还没有
理解也没关系,自己画个图仔细比较就可以了。这里让我偷懒一下就不画图了。
    
3.压栈和进栈指令简介:

    压栈指令 :     pushx    source

    其中, 'x'可以是  'w'(表示字), 或者是'l'(表示长字);source可以是数值或者寄存器值或者内存地址;

    出栈指令 :     popx    des

    同样,'x'可以是  'w'(表示字), 或者是'l'(表示长字);des可以是寄存器值或内存值;

    关于最最基本的东西已经讲得差不多了,当然还有其他一些基本东西,留给大家去查资料了,这部分讲的都和本文有密切关系的东西。

二、函数如何通过堆栈来解决问题:

    这部分是对函数如何通过堆栈解决函数调用以及参数传递的理论性理解,相当重要,只有了解之后才可以进行实例的分析,这一大部分同样分成几个小部分:

1.通过堆栈操作实现参数的传递:

  
前面说过,堆栈的基本操作可以是压栈和出栈,而参数的传递就是通过这种方式来实现的。ESP永远指向了堆栈顶部,如果这时候压进一个int型的数据元
素,那么ESP向下移动了4个字节,这时候它还是指向了堆栈的顶部(注意了,顶部的地址比移动前的地址低,不要乱了)。假如把一个int型数据元素出栈,
那么ESP向上移动4个字节,这时候它还是指向了堆栈的顶部,只是现在地址是增加了4个字节。所以,如果一个函数需要传递参数过去那么就得在调用函数之前
先把参数压进栈,然后再调用。关于这点后面我会详细说一下,现在你如果没理解也没关系。

2.函数调用的一般汇编指令:

    函数调用的一般汇编指令都是那么几条,下面我把他们按一般顺序罗列出来:

    #Asm Code
    
    function:
            pushl    %ebp
            movl    %esp, %ebp
            subl    $8, %esp
            #...
            movl    %ebp, %esp
            popl    %ebp
            ret

    下面先简单分析这几句一般汇编指令的意思和目的。

    pushl    %ebp            #这句把寄存器%ebp压栈,目的是什么呢?看下一条指令:

    movl    %esp, %ebp      
#把寄存器%esp的值给了寄存器%ebp;想想前面说到的%esp寄存器是干什么的?用于指向堆栈的顶部,现在通过这条指令,%ebp都是指向了堆栈
的顶部了;所以看看第一条指令,其实就是为了保护原来在%ebp寄存器中的内容#那么这里为什么又要把%esp的值赋给%ebp呢?这里的巧妙就来了。在
函数的处理过程中,可能一些数据会被压进栈,那么这时候就会破坏栈里面原有的内容了,如果栈的内容被破坏了,指向栈顶的指针%esp指向的地址不准确了
(不知道能不能用“不准确”这个词来形容,可能不太合适),那么到时候要清栈就会发生更多的意外问题了。
清栈?先别管这个词,下面也会给出解释。所以第二条指令是为了保证有一个寄存器永远指向了栈顶而不必担心会
出现刚才所说的问题。现在是寄存器%ebp永远指向栈顶了,而%esp可以移动而不必害怕数据会被破坏了。

    subl    $8, %esp      
#看这条指令,为什么无故要把%esp的值减去8呢?也就是说%esp向下移动8个字节,而这8个字节的空间到底用来干什么呢?这8个字节空间其实是为
临时变量留出来的。注意,它会根据临时变量占用的字节大小而留出不同的空间大小,所以不一定是8个字节,可能是24或者36甚至更大的空间;不过临时变量
太多不是好事情,这点注意。

    movl    %ebp, %esp        #这条指令把%ebp复制到%esp了,理由是什么?让%esp重新指向栈顶,这样就可以方便函数调用完毕后的清栈了。

    ret                        #函数调用完毕的返回指令,这句指令其实同时把函数调用刚刚开始压进的IP地址弹出栈。在下面会有详细分析。

    关于函数如何通过堆栈来解决问题的基本理论大概就说到这里,假如你对上面的内容不理解也没关系,下面第3个部分通过实例来分析可以让你有
个比较深刻的理解。

三、函数调用和参数传递机制的实例分析:

    这是本文的实战分析部分了,通过例子来加深一下理解。我会先列出C代码出来,然后列出反汇编的汇编代码,结合C代码来分析汇编代码。我会尽可能对各种类型的函数调用或参数类型作一个分析,可能会显得比较累赘一点,不介意吧?准备好的话就开始吧:P

1. 函数原型:void function(void);

    // C code
    
    void function(void)
    {
        return;
    }
    
    int main(void)
    {
        function();
        
        return 0;
    }

    反汇编一下看看汇编代码,下面是Linux 下的gcc反汇编后的代码(注意:是在我的机子上的反汇编代码):

    function:
        pushl   %ebp
        movl    %esp, %ebp
        popl    %ebp
        ret

    看看,因为函数function什么也没有做,所以直接就返回了,上面的指令和第2部分的代码基本上一样,甚至更简单,参照一下前面的分析:P

    下面看看main函数的反汇编代码了,相对来说复杂一点,看好了:

    main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        andl    $-16, %esp
        movl    $0, %eax
        addl    $15, %eax
        addl    $15, %eax
        shrl    $4, %eax
        sall    $4, %eax
        subl    %eax, %esp
        call    function        #函数调用指令
        movl    $0, %eax
        leave
        ret

    看看函数调用指令 : call    function,前面居然还有那么多据指令,那些指令到底干什么用?我一句一句分析吧:P

    pushl    %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    
    这三句不分析,和前面第2部分的一样,忘记的回头看一下,其实这也反映了一件事:其实main函数也很普通,它跟其他函数其实差不多,只是地位稍微高一点而已。

    andl    $-16, %esp

    这句可能吓倒一些人了。 andl 是逻辑与指令,而-16其实补码形式是0xfffffff0。为什么要把%esp的值和-16进行逻辑与运算呢?不要小看

这条指令,它的作用不容忽视,%esp指向堆栈顶部。这条指令其实是为了强制让%esp的值是16的倍数。为什么要16的倍数?这里必须懂得一个常识:

Linux下的编译器GCC默认的堆栈是16字节对齐的,可能有些人要问为什么要对齐,对齐其实为了加快CPU的访问效率,这里你记住这点就可以了。

    movl    $0, %eax
      addl    $15, %eax
       addl    $15, %eax
    shrl    $4, %eax
    sall    $4, %eax

    看到这几句,又有更多人可能被吓到了,干嘛对%eax寄存器进行那么多的操作啊?的确,我也觉得没什么多大的必要,因为仔细看看这几条指令

无非就是为了让%eax的值是0而已。看看刚开始 %eax = 0,经过两次addl之后,%eax的值变成30了,30其实就是0x11110,再下面两条指令

是为了保证%eax最低5位的值全部为0。注意,这只是在我的机子上的反汇编指令,不同机器对此处理

可能不一样,但有一点一样就是保证%eax的值是0。看看下面这条指令:

    subl    %eax, %esp

    看,%esp值减去%eax值后把结果送到%esp,所以经过这条指令后%esp值仍然是16的倍数,这就是保证%eax值是16的倍数的原因了。    

    call    function    
    movl    $0, %eax

    这个简单了,调用函数function,最后又把%eax寄存器的值清0,结束整个main函数了。

    这就是最简单的函数调用分析了,没有涉及到参数的传递,所以非常简单,下面就要开始讲到参数的传递了,事实上有了这个例子的分析,下面的简单多了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值