C函数调用过程


这几天在看GCC Inline Assembly,在C代码中通过asm或__asm__嵌入一些汇编代码,如进行系统调用,使用寄存器以提高性能能,需要对函数调用过程中的堆栈帧(Stack Frame)、CPU寄存器、GCC inlie assembly等了如指掌。现在看看函数调用过程吧。


1. Linux 进程虚拟地址空间

以32位操作系统为例,下面是Linux进程地址空间布局:



32位虚拟地址空间的高1GB的空间是留给操作系统内核的,栈由高地址到低地址向下增长,堆由低地址到高地址向上增长。

C中如 malloc 等分配的内存在堆中分配。初始化了的静态变量和全局变量放在Data段中。未初始化的全局变量和局部静态变量放在Bss段中,更准确的说是在Bss段为它们预留了空间。非静态局部变量是在函数调用过程中暂存在栈上的。


2. 函数的堆栈帧

栈在程序运行中具有举足轻重的地位。最重要的,栈保存了一个函数调用所需要的维护信息,被称为堆栈帧(Stack Frame),一个函数(被调函数)的堆栈帧一般包括下面几个方面的内容:

(1) 函数参数,默认调用惯例情况下从右向左的顺序依次把参数压入栈中。由函数调用方执行。

(2) 函数的返回地址,即调用方调用此函数(如call func1)的下一条指令的地址。函数调用方(call指令)执行。

(3) 保存调用方函数的EBP寄存器,即将调用方函数的EBP压入堆栈,并令EBP指向此栈中的地址:pushl %ebp; movl %esp, %ebp。由被调函数执行。

(4) 上下文:保存在函数调用过程中需要保持不变的寄存器(函数调用方的),如ebx,esi,edi等。由被调函数执行。

(5) 临时变量,如非静态局部变量。

下面是一个函数的堆栈帧结构图:


压入函数参数和返回地址的过程是由函数调用方在调用函数之前将其压入栈中,每个函数执行后首先要执行的就是把函数调用方的EBP寄存器压入栈中,之后是在栈上开辟一些空间存放局部变量,最后把要保存的寄存器压入栈中。i386标准函数进入和退出的指令序列的基本形式如下:(GCC 内联汇编描述)

pushl %ebp
movl %esp, %ebp
subl $x, %esp
[pushl reg1]
...
[pushl regn]
函数实际内容
[popl regn]
...
[popl reg1]
movl %ebp, %esp
opol %ebp
ret

3. 例子

下面通过代码分析一下吧:

/* stackFrame.c*/                                                                                                            
#include 

void func1(int a, int b);
void func2(int a, int b, int c);

int main(void )
{
    int a = 1;
    func1(a, 2);
    return 0;
}

void func1(int a, int b)
{
    int  a1 = 3;
    char b1 = '4' - '0'; //4
    func2(a1, b1, 5);
}

void func2(int a, int b, int c)
{
    char a1 = 'a';
}

汇编后的汇编代码如下,gcc -o stackFrame.s -S stackFrame.c

    .file   "funcstack.c"                                                                                                    
    .text
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $32, %esp
    movl    $1, 28(%esp)
    movl    $2, 4(%esp)
    movl    28(%esp), %eax
    movl    %eax, (%esp)
    call    func1
    movl    $0, %eax                //函数返回值送往eax
    leave
    ret
    .size   main, .-main
.globl func1
    .type   func1, @function
func1:
    pushl   %ebp                //压入函数调用方的EBP,Old EBP
    movl    %esp, %ebp          //令当前EBP指向栈顶,此时的栈顶指向Old EBP
    subl    $40, %esp           //在栈上开辟一些空间,存放局部变量等
    movl    $3, -16(%ebp)       //func1中整型变量a1的存储位置, ebp-16
    movb    $4, -9(%ebp)        //func1中字符型变量b1的存储位置,ebp-9
    movsbl  -9(%ebp), %eax      //将b1送入eax
    movl    $5, 8(%esp)         //将调用func2的函数参数5压入栈,esp+8
    movl    %eax, 4(%esp)       //将参数b1压入栈,            esp+4
    movl    -16(%ebp), %eax     //将a1送入eax
    movl    %eax, (%esp)        //将参数a1压入栈,            esp+0
    call    func2               //调用func2,下一调指令的返回地址入栈(EIP入栈),内部完成,无汇编代码
    leave
    ret                         //将返回地址送往EIP
    .size   func1, .-func1
.globl func2
    .type   func2, @function
func2:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movb    $97, -1(%ebp)
    leave
    ret
    .size   func2, .-func2
    .ident  "GCC: (GNU) 4.4.4 20100726 (Red Hat 4.4.4-13)"
    .section    .note.GNU-stack,"",@progbits 

4. 例子

代码如下:

#include  

void test1(int a, int b);
void test2(int a, int b, int c);

int main(void)
{
    test1(1, 2);
    return 0;
}

void test1(int a, int b)
{
    char *ebp = NULL;

    asm("movl %%ebp, %0\n\t"
        :"=r"(ebp));

    printf("test1: ebp = %p\n", ebp); //test1函数的ebp
    test2(3, 4, 5);
}

void test2(int a, int b, int c)
{
    int *ebp = NULL;

    asm("movl %%ebp, %0\n\t"
        :"=r"(ebp));

    printf("test2: *ebp = %p\n", (char *)(*ebp)); //test2的ebp指向的内容(old EBP),是test1的EBP
    printf("*(ebp + 4)  = %d\n", *((char *)ebp + 4));  //返回地址
    printf("*(ebp + 8)  = %d\n", *((char *)ebp + 8));  //第一个参数 3
    printf("*(ebp + 12) = %d\n", *((char *)ebp + 12)); //第二个参数 4
    printf("*(ebp + 16) = %d\n", *((char *)ebp + 16)); //第三个参数 5
}     

结果如下:



你可能感兴趣的:(技术文章)