7.过程调用

过程调用的机器级表示

函数调用时如何传递参数,如何把控制转移到被调用的过程,寄存器的使用约定,递归函数的实现。

7.1

7.1.1 过程调用概述

int add(int x, int y)
{
    return x+y;
}

int main()
{
    int t1 = 125;
    int t2 = 80;
    int sum = add(t1, t2);
    return sum;
}

通过这个程序,关注的问题:

  • 这个过程调用的机器级代码是什么

  • 如何把参数传给形式参数xy

  • 如何把结果返回给caller

main为调用函数,add为被调用函数,main中有一串指令序列是存放参数的调用add,在add里肯定是有指令序列取出参数的。

CALL指令和RET指令,在硬件层次上做的事情。

**参数通过栈(stack)来传递,**那么栈在哪里呢?

栈是个存储空间,接下来要看看存储空间。

可执行文件在磁盘上,装入RAM的时候是映射到一个存储空间里的,不同的东西反倒不同的段里。

栈在一个比较高的位置,栈由上向下生长。

调用过程:

  • 1(调用者)入口参数保存

  • 2(调用者)返回地址保存

  • 3(被调用者)保存调用者的现场,并为自己的非静态局部变量分配空间

  • 4(被调用者)执行函数体

  • 5(被调用者)恢复调用者的现场,释放局部变量空间

  • 6(被调用者)取出返回地址,将控制转移到IP

12前面两个过程是CALL指令做的事情;3为准备阶段,保存现场,分配自己的空间,生成栈帧,然后就可以做具体的处理了,返回值放到合适的地方,然后5恢复现场,6执行RET指令,就回去了。

现场指的是通用寄存器的内容,因此寄存器是同一套。

关于寄存器的约定,哪些在调用过程用,哪些在被调用过程用。IA32的寄存器使用约定。

  • 调用者在调用前去保存内存的寄存器:EAX,EDX,ECX,被调用者可以直接使用不需要保护内容

  • 被调用者需要去保存的寄存器:EBX,ESI,EDI,子程序要先压栈在使用,在过程调用返回前要弹出

  • EBP和ESP是帧指针寄存器和栈指针寄存器,用来指向当前栈帧的底部和顶部

为了减少准备阶段和结束阶段的开销,Q里面可以刻意的去选择EAX,EDX,ECX,不够用在去用其他的。

7.1.2 过程调用的参数传递

入口参数的位置。

最右边的参数先进去

movl canshu3, 8(%esp)
movl canshu2, 4(%esp)
movl canshu1, (%esp)
call add    

call做了三个事情

  • R[esp] <- R[esp] - 4

  • M[R[esp]] <- 返回地址(call的下一条指令的地址)

  • R[eip] <- add函数地址

每个过程的第一条指令总是

pushl %ebp          ;ebp在main里的值
movl  %esp, %ebp    ;新的栈底是老的栈顶

所以,参数1的位置就是EBP+8,EBP+12

IA32里,char和short也是分配4个字节,因此在看反汇编程序的时候,要能看出这是在调用参数。

举个例子,交换两个数的程序,按地址传送和按值传送,观察

int add(int x, int y)
{
    return x+y;
}
int caller()
{
    int t1 = 125;
    int t2 = 80;
    int sum = add(t1, t2);
    return sum;
}
caller:
pushl   %ebp
movl    %esp, %ebp
subl    $24, %esp

movl    $125, -12(%ebp)
movl    $80, -8(%ebp)

movl    -8(%ebp), %eax
movl    %eax, 4(%esp)
movl    -12(%ebp), %eax
movl    %eax, (%esp)

call    add

movl    %eax, -4(%ebp)      
movl    -4(%ebp), %eax

leave
ret

add:
pushl   %ebp
movl    %esp, %ebp
...

经历了准备阶段,分配局部变量,准备入口参数,调用子函数。

调用嵌套也是一样的。

返回值放在 EAX 寄存器里,约定好的。

leave指令是退栈,等价于

movl    %ebp, %esp
popl    %ebp

把当前的栈切换到调用caller的函数的栈,caller的内容就释放掉了。

总结一下一个C语言过程(函数)的结构

  • 准备阶段

    • 形成栈底,push和mov,处理BS和SP的指向

    • 生成栈帧(如果需要),sub或者add

    • 保存现场(如果有被调用者保存的寄存器),mov

  • 函数体

    • 分配局部变量空间,并赋值

    • 具体逻辑处理,如果又有函数调用

      • 准备参数,实参送栈帧入口参数处

      • call,保存返回地址并转被调用函数

    • 在eax中准备返回参数

  • 结束阶段

    • 退栈,leava,pop

    • 取返回地址,ret指令

7.1.3 过程调用的参数传递

举例子,在调用子函数int fun(int a1,int a2,int a3)前,准备了3个参数

movl    parameter3, 8(%esp)
movl    parameter2, 4(%esp)
movl    parameter1, (%esp)
call    fun

参数入栈的顺序从右往左,在栈里面就是自上而下。

执行了call之后,需要4个字节存放返回地址,原来esp的值给ebp了,并且ebp压栈,这时候原来的参数地址可以用 (%ebp)+8 来找到。

IA-32 体系里,不管参数是什么类型,都是分配的4个字节。

参数传递举例子

#include <stdio.h>
void swap(int *x, int *y)
{
    int t = *x;
    *x = *y;
    *y = t;
}
void main()
{
    int a = 15, b = 22;
    printf("a=%d, b=%d\n");
    swap(&a, &b);
    printf("a=%d, b=%d\n");
}
#include <stdio.h>
void swap(int x, int y)
{
    int t = x;
    x = y;
    y = t;
}
void main()
{
    int a = 15, b = 22;
    printf("a=%d, b=%d\n");
    swap(&a, &b);
    printf("a=%d, b=%d\n");
}

编译后反汇编查看程序做了什么事情

main:
    ...
    leal    -8(%ebp), %eax
    movl    %eax, 4(%esp)
    leal    -4(%ebp), %eax
    movl    %eax, (%esp)

    call swap
    ...
    ret

swap:
    //准备阶段
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx        /* 被调用者保存 */
    //过程体
    movl    %8(ebp), %edx
    movl    (%edx), %ecx
    movl    %12(ebp), %eax
    movl    (%edx), %ebx
    movl    %ebx, (%edx)
    movl    %ecx, (%eax)
    //结束段
    popl    %ebx
    popl    %ebp
    ret

按照地址传递,用的指令是leal

main:
    ...
    movl    -8(%ebp), %eax
    movl    %eax, 4(%esp)
    movl    -4(%ebp), %eax
    movl    %eax, (%esp)

    call swap
    ...
    ret

swap:
    //准备阶段
    pushl   %ebp
    movl    %esp, %ebp

    //过程体
    movl    %8(ebp), %edx
    movl    %12(ebp), %eax
    movl    %eax, 8(%ebp)
    movl    %ecx, 12(%ebp)
    //结束段
    popl    %ebp
    ret

在看这类调用时,心里有栈,对程序和指令的行为会清晰很多。

这是c语言课里讲的参数传递的问题,现在从底层角度有了更深入的认识。

7.1.4 过程调用举例

void test(int x, int *ptr)
{
    if (x>0 && *ptr>0)
        *ptr += x;
}

void caller(int a, int y)
{
    int x = a > 0 ? a : a + 100;
    test(x, &y);
}

caller的过程为P,

7.1.6 递归过程调用

一个最简单的递归调用程序

int nn_sum(int n)
{
    int result;
    if (n<=0)
        result=0;
    else
        result = n + nn_sum(n-1);
    return result;
}
nn_sum:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    %4, $esp
    movl    8(%ebp), %ebx
    movl    $0, %eax        /* result = 0 */
    cmpl    $0, %ebx        /* at&t逆序比较,intel顺序 */
    jle     .L2
    leal    -1(%ebx), %eax
    movl    %eax, (%esp)
    call    nn_sum
    addl    %ebx, %eax
.L2
    addl    $4, %esp
    popl    %ebx
    popl    %ebp
    ret

由分析可以看出,栈一直向下生长,每递归一次,都有个新栈帧,所以递归次数太多会使得空间开销过大。

7.1.6 过程调用举例

double fun(int i)
{
    volatile double d[1] = {3.14};
    volatile long int a[2];
    a[i] = 1073741824;
    return d[0];
}

不同系统执行可能不太一样。编译器对局部变量分配方式可能不同。

fun(0) -> 3.14
fun(1) -> 3.14
fun(2) -> 3.1399998664856
fun(3) -> 2.00000061035156
fun(4) -> 3.14 然后存储保护错

有一些问题需要去考虑

  • 为何每次返回不一样

  • 为什么会存储保护错

  • 栈帧中的状态如何

fun:
    push    %ebp
    movl    %esp, %ebp
    sub     $0x10, %esp     /* 移动栈指针跨过局部变量 */
    fldl    0x8048518       /* 把此地址的数据装载到x87 ST(0)里面去 */
    fstpl   -0x8(%ebp)      /* x87栈顶存到这个位置 */
    mov     -0x8(%ebp), %eax    /* 读入口参数 */
    movl    $0x40000000, -0x10(%ebp, %eax, 4)
    fldl    -0x8(%ebp)      /* 返回值为浮点,x87栈顶 */
    leave
    ret    

i = 4时,EBP就被改变了,后面的就乱了,会有存储保护错。

7.2 选择和循环

7.2.1 选择

if-else 语句的机器级表示

if (cond_expr)
    then_statement
else
    else_statement

举例子

int get_cont(int *p1, int *p2)
{
    if (p1 > p2)
        return *p2;
    else
        return *p1;
}
get_cont:
    movl    8(%ebp), %eax
    movl    12(%ebp), %edx
    cmpl    %edx, %eax
    jbe     .L1
    movl    (%edx), %eax
    jmp     .L2
.L1:
    movl    (%eax), %eax
.L2:

机器实现基本上和 C 差不多,还是很好理解的。

switch-case 语句也是一种选择分支

int sw_test(int a, int b, int c)
{
    int result;
    switch (a)
    {
        case 15:
            c = b & 0x0f;
        case 10:
            result = c + 50;
            break;
        case 12:
        case 17:
            result = b + 50;
            break;
        case 14:
            result = b;
            break;
        default:
            result = a;
    }
    return result;
}

7.2.2 循环

最后更新于