• 简单C程序在IA-32 CPU上运行过程的分析


    本文将通过编译器生成的汇编代码分析C程序在IA-32体系PC上的运行流程

    实验环境: gcc 4.8.2

    C语言程序的内存结构

    C代码如下

    int g(int x)
    {
        return x + 1;
    }
    
    int f(int x)
    {
        return g(x);
    }
    
    int main(void)
    {
        return f(2) + 3;
    }

    使用编译命令gcc -S -O0 -o main.s main.c -m32编译出汇编文件,如下

    g:
        pushl   %ebp
        movl    %esp, %ebp
        movl    8(%ebp), %eax
        addl    $1, %eax
        popl    %ebp
        ret
    f:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $4, %esp
        movl    8(%ebp), %eax
        movl    %eax, (%esp)
        call    g
        leave
    main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $4, %esp
        movl    $2, (%esp)
        call    f
        addl    $3, %eax
        leave
    

    入口点位于main,由main开始分析

    • pushl %ebp
      • subl $4, %esp
      • movl %ebp, (%esp)
      • 这是进入main函数后的第一个操作,保存了原来的ebp,即main开始执行之前的栈现场
    • movl %esp, %ebp
      • esp的值进入ebp,这儿将栈底设置为进入main之前的栈顶,即此处开始,ebp后面新增的栈都是main函数中的操作造成的,每次ebp的变化都意味着当前所处的函数的变化
    • subl $4, %esp
    • movl $2, (%esp)
      • 以上两句实际上是执行了pushl $2, %esp,将立即数2入栈,
      • 这句执行完成以后,使用gdb查看eip的值为0x8048418<main+13>,即下一行call f
    • call f
      • pushl %eip
      • jmp f
      • 调用f函数,上面两句入栈的立即数2就是函数f的参数,并将eip指向f的入口地址
    • addl $3, %eax
      • eax中存储的是函数f调用返回的结果,这条语句在eax上加上了立即数3
    • leave
      • movl %ebp, %esp
      • popl %ebp
      • 这两句将ebp还原到了调用main之前的现场,main函数调用到此为止
    • ret
      • popl %eip
      • 将eip指回调用main之前的地址,退出函数main

    进入main以后的第一个函数调用f(x)

    • pushl %ebp
    • movl %esp, %ebp
      • 这儿的作用和main中一样,不重复说明
    • subl $4, %esp
      • 为参数留出栈空间
    • movl 8(%ebp), %eax
      • 栈地址是向下增长的,这儿ebp+8是获取了压入eip和参数前的地址,这条语句就是将参数存入eax
    • movl %eax, (%esp)
      • eax入栈,即参数入栈,c代码中函数g的参数就是函数f接收的参数,所以直接调用g函数
    • call g
      • pushl %eip
      • jmp g
    • leave
      • 以上两行和main一致,不再重复
    • ret
      • 和main中作用一致

    最后一个函数调用g(x)

    • pushl %ebp
    • movl %esp, %ebp
      • 和前面描述一致,不赘述
    • movl 8(%ebp), %eax
      • 依然和f中的功能一致
    • addl $1, %eax
      • 函数g的功能,即在参数上+1
    • popl %ebp
      • movl (%esp), %ebp
      • addl $4, %esp
      • 恢复调用g之前的栈
    • ret
      • popl %eip
      • 将eip指回调用函数g之前的地址,即回到函数f中

    C语言程序在IA-32机器上的运行过程中,最重要的几条指令即push/pop,call/ret,其中push/pop实现了栈的基本操作,call/ret维护了函数的调用栈,确保了函数调用流程的连续

    使用ddd抓了两张call前后的eip变化,可以很清晰的看出eip在call时的非线性改变

    执行call之前的eip

    执行call之后的eip

    最后再看一下编译器优化可以做到的效果,以gcc -S -O3 -o test.s test.c -m32编译上述代码,得到的汇编如下

    g:
        movl    4(%esp), %eax
        addl    $1, %eax
        ret
    f:
        movl    4(%esp), %eax
        addl    $1, %eax
        ret
    main:
        movl    $6, %eax
        ret
    

    可以看见编译器直接把main函数的结果算出来当做立即数返回了。。还是很强大的。。f和g这种简单函数也没有重新设置栈底,而是直接采用了变址寻址,提升了运行效率

    云课堂作业

    吴韬,原创作品转载请注明出处,《Linux内核分析》MOOC课程

  • 相关阅读:
    视频输入 范例
    视频输出 范例
    开启VI视频输入设备 范例
    初始化MMP系统 范例
    Git 的使用
    DVS/DVR/NVR/XVR
    shell命令中 && 和 || 的区别
    码流 / 码率 / 比特率 / 帧速率 / 分辨率 / 高清
    DNS与DSN
    ob_start()失效与phpunit的非正常结束
  • 原文地址:https://www.cnblogs.com/current/p/4321525.html
Copyright © 2020-2023  润新知