关于C语言中函数调用和参数传递机制的探讨(三 .传递多个参数等)

时间:2021-07-04 01:04:27

3. 函数原型:int function(int i, int j);


    现在参数是两个,不是一个了,两个到底该怎么处理呢?同样看C程序和相应的汇编代码:


    // C code


    int function(int i, int j)

    {

        return (i + j);

    }

   

    int main(void)

    {

        function(1, 2);

    }


    下面列出主要的汇编代码,没有全部列出来,因为一些和前面一样的代码已经分析过,不再罗嗦了。


main:

              # ... ...

        movl    $2, 4(%esp)

        movl    $1, (%esp)

        call    function


    看到没有?先把2送进栈里,再把1压栈,我们看看函数调用的C代码:function(1, 2); 2在右边,而1在左边,所以,当存在多参数的时候参数压栈其实是按从右向左的顺序压栈的。当参数都压栈后,就调用函数了。


    function:

        pushl   %ebp

        movl    %esp, %ebp

        movl    12(%ebp), %eax

        addl    8(%ebp), %eax

        popl    %ebp

        ret

   

    看函数的汇编代码:movl    12(%ebp), %eax ; 知道12是哪里来的吗?自己画图看看,结合前面的分析!一调用函数先压IP进栈,再压%ebp进栈,那么%esp值就被减去8了,再把%esp值复制到%ebp,就是这样而已!


4.函数原型:char *function(char *s);


    作为字符串函数其实道理也差不多,而且我觉得反而更简单,怎么个简单法,看代码吧:


    // C code


    char *function(char *s)

    {

        return s;

    }


    int main(void)

    {

        char *p = function("abcd");

    }

   

    列出并简单分析一下汇编代码:


    main:

              # ... ...

        movl    $.LC0, (%esp)

        call    function


    这里你可能会问 $.LC0 是什么。看看下面的定义:


    .LC0:

        .string "abcd"

   

    .LC0只是一个标志,就是字符串"abcd";所以说白了其实非常简单。movl    $.LC0, (%esp) ;
就是把字符串送到地址为%esp的空间里。

   

    下面看调用的函数的汇编代码了:


    function:

              #... ...

        movl    8(%ebp), %eax

          #.. ...


    8(%ebp)其实就是字符串了,依然建议你们自己画图看看,只要图一画,自然变得十分清晰。


5.函数原型:struct text function(int n);


    现在函数返回值类型换成是结构体了,差别就来了,不过其实道理还是那个样,本质的东西还是一样的。到结构体这里我就简单说几句而已。


    //C code


    struct text {

        int a;

    };


    struct text function(int n)

    {   

        struct text s;

        s.a = n;

        return s;

    }


    int main(void)

    {

        struct text t = function(10);

        return 0;

    }


    这里举的例子都非常简单,我们的目的只是为了分析不同函数调用和参数传递方式是怎样进行的。看汇编代码:


    main:

              # ... ...

        leal    -20(%ebp), %eax

        movl    $10, 4(%esp)

        movl    %eax, (%esp)

        call    function

   

    到这个例子就只分析最重要的指令了,其他的大家可以尝试分析看看。在main函数开头,我的机子上GCC居然留出了40个字节给临时结构体变量!


开头有这句:subl    $40, %esp ; 有点惊讶,我也不知道为什么留这么大的空间。但这些数据我们先不管了,毕竟不是最重要的。


    leal    -20(%ebp), %eax


    看看上面这句,这条指令非常重要。把(%ebp - 20)的地址复制给%eax,%eax 的值其实是结构体中成员的地址。自己分析后面的代码证实一


下。


    下面是function函数的汇编代码:


    function:

        pushl   %ebp

        movl    %esp, %ebp

        subl    $16, %esp

        movl    8(%ebp), %eax

        movl    12(%ebp), %edx

        movl    %edx, -4(%ebp)

        movl    -4(%ebp), %edx

        movl    %edx, (%eax)


    subl    $16, %esp        #为函数中临时结构体变量保留空间.


    在上面和条指令后面那几条指令都是按变量所在的地址赋给结构体变量成员了,有兴趣的朋友自己分析下,让我偷个懒吧。


5. 其他说明


    其实还有很多的类型都没讲,其中就有返回值为浮点数或者参数是浮点数,浮点数不讲了,因为这就比较复杂了,为了支持浮点数以及其他


功能(比如说数学运算功能)就需要附加的指令和寄存器了,这些东西全部结合就称作“浮点单元”(Floating-point unit ---> FPU)。看看


浮点数以及运算有自己的指令和寄存器,这些东西说起来就得一篇文章了。


    递归就不也用讲了,道理和前面一样,只是重复调用自己而已,就按照栈的顺序衍生过去而已然后最后一层层清栈了。


    另外还有可变参数函数,其实有了上面的基础来分析可变参数函数挺简单的,入栈顺序还是那样从右向左,只是你需要了解一下可变参数函数


的内部到底怎么实现的,这里存在很强的技巧性,有兴趣的朋友可以去看看在头文件<stdarg.h>里面关于可变参数的几个宏,技巧性超强!


四、后记


 
写到这里我的头都有点涨了,花了好几个钟头写的。本来后面还想再把结构体啊可变参数函数啊之类的说一下,但没什么激情了。抱歉一下。也不想重审文章了。
因为写得匆忙,文章肯定有不少地方表达不清楚甚至观点错误,希望大家指正。最后希望我写的这文章能够帮助一些朋友理解函数调用和参数传递的机制。


五、参考文献:


《Professional Assembly Language》  Author:Richard Blum(US)   


                  ID:lj_860603(键键)

               2006.11.30