• 深入理解Linux之计算机是怎样工作的?


    SA12226242 施健  信息安全

    导读

      在深入理解Linux之前,我们需要了解计算机是如何工作的。使用Example的c代码分别生成.cpp,.s,.o和ELF可执行文件,并加载运行,分析.s汇编代码在CPU上的执行过程。

    一、C语言的编译过程

    1.1 C语言的编译过程

      由于是单文件的程序,因此链接的过程省略。详细参考《程序员的自我修养》第2.1节 被隐藏了的过程[1]

    1.2 源文件example.c

     1 // example.c
     2 
     3 int g(int x)
     4 {
     5     return x + 3;
     6 }
     7  
     8 int f(int x)
     9 {
    10     return g(x);
    11 }
    12 
    13 int main()
    14 {
    15     return f(8) + 1;
    16 }

    1.3 预处理

    预处理主要是处理宏指令和#include指令。使用命令 gcc -E -o example.cpp example.c。

     1 # 1 "example.c"
     2 # 1 "<built-in>"
     3 # 1 "<command-line>"
     4 # 1 "example.c"
     5 
     6 
     7 int g(int x)
     8 {
     9     return x + 3;
    10 }
    11 
    12 int f(int x)
    13 {
    14     return g(x);
    15 }
    16 
    17 int main()
    18 {
    19     return f(8) + 1;
    20 }

    1.4 编译成汇编代码

    编译的过程使用一系列的词法分析,语法分析,语义分析和优化后生成相应的汇编代码。使用命令 gcc -x cpp-output -S -o example.s example.cpp

     1     .file    "example.c"
     2     .text
     3 .globl g
     4     .type    g, @function
     5 g:
     6     pushl    %ebp
     7     movl    %esp, %ebp
     8     movl    8(%ebp), %eax
     9     addl    $3, %eax
    10     popl    %ebp
    11     ret
    12     .size    g, .-g
    13 .globl f
    14     .type    f, @function
    15 f:
    16     pushl    %ebp
    17     movl    %esp, %ebp
    18     subl    $4, %esp
    19     movl    8(%ebp), %eax
    20     movl    %eax, (%esp)
    21     call    g
    22     leave
    23     ret
    24     .size    f, .-f
    25 .globl main
    26     .type    main, @function
    27 main:
    28     leal    4(%esp), %ecx
    29     andl    $-16, %esp
    30     pushl    -4(%ecx)
    31     pushl    %ebp
    32     movl    %esp, %ebp
    33     pushl    %ecx
    34     subl    $4, %esp
    35     movl    $8, (%esp)
    36     call    f
    37     addl    $1, %eax
    38     addl    $4, %esp
    39     popl    %ecx
    40     popl    %ebp
    41     leal    -4(%ecx), %esp
    42     ret
    43     .size    main, .-main
    44     .ident    "GCC: (Ubuntu 4.3.3-5ubuntu4) 4.3.3"
    45     .section    .note.GNU-stack,"",@progbits

    1.5 汇编成目标代码

      汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了,“汇编”这个名字也来源于此。[1]

      使用命令 gcc -x assembler -c example.s -o example.o

    1.6 链接  

      详见《程序员的自我修养》2.1.4 [1]

    二、C程序的运行

       现在我们观察汇编的代码,模拟C语言运行过程中栈的变化情况,来深入了解C程序的运行过程。

      首先,C程序从main函数入口进入执行

      28 到30

        27 main:
    ->  28     leal    4(%esp), %ecx
        29     andl    $-16, %esp
        30     pushl    -4(%ecx)

      这三条指令是:将esp按照16字节对齐,然后再push esp。
      31到32行:

    ->  31     pushl    %ebp
        32     movl    %esp, %ebp
    

      31行将ebp压栈,

      32行将esp中的地址赋值给ebp,为了区别,我们使用ebp1表示。对应的栈示意图如下:

    高地址  +---------------+
           |               |
           +---------------+
    ebp1-> |     ebp      | <- esp
           +---------------+
           |               |
    低地址  +---------------+

      此时,就搭建好了main函数运行的框架。

      33行:

      继续看第33行代码

    ->  33     pushl    %ecx

      将ecx压栈,具体原因暂时不详,栈示意图。

    高地址  +---------------+
           |               |
           +---------------+
    ebp1-> |     ebp       | 
           +---------------+
           |     ecx       | <- esp
           +---------------+
           |               | 
    低地址  +---------------+

      34到36行

    ->  34     subl    $4, %esp
        35     movl    $8, (%esp)
        36     call    f

      34 行将esp向下减4个字节的大小,等价与分配了4个字节的空间。

      35行将立即数8放入到esp指向的内存中,这其实是在压入参数。栈示意图

    高地址  +---------------+
           |               |
           +---------------+
    ebp1-> |     ebp       | 
           +---------------+
           |     ecx       | 
           +---------------+
           |      8        | <- esp
           +---------------+
           |               | 
    低地址  +---------------+

      36是宏指令call,其作用等价于将当前cs : eip的值压入栈顶,cs : eip指向被调用函数的入口地址。[2]

      栈示意图:

    高地址  +---------------+
           |               |
           +---------------+
    ebp1-> |     ebp       | 
           +---------------+
           |     ecx       | 
           +---------------+
           |      8        | 
           +---------------+
           |    cs:eip     | <- esp
           +---------------+
           |               | 
    低地址  +---------------+

      接下来执行的是函数f,我们从15行开始继续看

      15到17行

    ->  15 f:
        16     pushl    %ebp
        17     movl    %esp, %ebp

      16行将ebp压栈

      17行将esp的内容赋值给ebp,为了区别,我们将其命名为ebp2,栈示意图:

    高地址  +---------------+
           |               |
           +---------------+
           |     ebp       | 
           +---------------+
           |     ecx       | 
           +---------------+
           |      8        | 
           +---------------+
           |    cs:eip     | 
           +---------------+
    ebp2-> |     ebp1      | <- esp
           +---------------+
           |               | 
    低地址  +---------------+

      现在,f函数的执行框架搭好了。

      18到20行:

    ->  18     subl    $4, %esp
        19     movl    8(%ebp), %eax
        20     movl    %eax, (%esp)

      18行申请了一个4字节大小的栈空间

      19行将ebp+8位置处的内容放入到eax寄存器中。ebp+8的位置,就是第一个参数的位置。所以,这句话其实是传参数。

      20行将eax寄存器的内容放入到栈顶。对应的栈示意图:

    高地址  +---------------+
           |               |
           +---------------+
           |     ebp       | 
           +---------------+
           |     ecx       | 
           +---------------+
           |      8        | 
           +---------------+
           |    cs:eip     | 
           +---------------+
    ebp2-> |     ebp1      | 
           +---------------+
           |       8       | <- esp
           +---------------+
           |               | 
    低地址  +---------------+

      21行:

    ->  21     call    g

      21行调用函数g。call是宏指令,栈示意图:

    高地址  +---------------+
           |               |
           +---------------+
           |     ebp       | 
           +---------------+
           |     ecx       | 
           +---------------+
           |      8        | 
           +---------------+
           |    cs:eip     | 
           +---------------+
    ebp2-> |     ebp1      | 
           +---------------+
           |       8       | 
           +---------------+
           |    cs:eip     | <- esp
           +---------------+
           |               | 
    低地址  +---------------+

      现在观察函数g,汇编对应的是5到10行:

      5到7行:

     ->  5 g:
         6     pushl    %ebp
         7     movl    %esp, %ebp

      6行,入栈ebp

      7行,将esp赋值给ebp,为示区别,我们使用ebp3。

      栈示意图:

    高地址  +---------------+
           |               |
           +---------------+
           |     ebp       | 
           +---------------+
           |     ecx       | 
           +---------------+
           |      8        | 
           +---------------+
           |    cs:eip     | 
           +---------------+
           |     ebp1      | 
           +---------------+
           |       8       | 
           +---------------+
           |    cs:eip     | 
           +---------------+
    ebp3-> |      ebp2     | <- esp
           +---------------+
           |               | 
    低地址  +---------------+

      函数g的执行环境搭建好了。

      8到9行:

    ->   8     movl    8(%ebp), %eax
         9     addl    $3, %eax

      8行,从ebp+8的位置出取参数,放入到eax寄存器中。

      9行,将eax中的内容增加3。

      此时,栈无任何变化。

      10-11行:

    ->  10     popl    %ebp
        11     ret

      20行,从栈顶取出数据,放入到ebp中。对应的栈变化如下:

    高地址  +---------------+
           |               |
           +---------------+
           |     ebp       | 
           +---------------+
           |     ecx       | 
           +---------------+
           |      8        | 
           +---------------+
           |    cs:eip     | 
           +---------------+
    ebp2-> |     ebp1      | 
           +---------------+
           |       8       | 
           +---------------+
           |    cs:eip     | <- esp
           +---------------+
           |               | 
    低地址  +---------------+

      11行ret是个宏指令,其功能是从栈顶弹出原来保存在这里的cs : eip的值,放入cs : eip中。[2]

      对应的栈变化示意图:

    高地址  +---------------+
           |               |
           +---------------+
           |     ebp       | 
           +---------------+
           |     ecx       | 
           +---------------+
           |      8        | 
           +---------------+
           |    cs:eip     | 
           +---------------+
    ebp2-> |     ebp1      | 
           +---------------+
           |       8       | <- esp
           +---------------+
           |               | 
    低地址  +---------------+

      此时,我们回到了f函数中call指令的下一条指令,即第22行

      22行到23行:

    ->  22     leave
        23     ret

      22行,leave是宏指令,其相当于 movl %ebp, %esp和popl %ebp

      运行后,栈示意图如下:

    高地址  +---------------+
           |               |
           +---------------+
    ebp1-> |     ebp       | 
           +---------------+
           |     ecx       | 
           +---------------+
           |      8        | 
           +---------------+
           |    cs:eip     | <- esp
           +---------------+
           |               | 
    低地址  +---------------+

      23行,ret恢复eip到main函数中call的下一条指令。

      栈示意图:

    高地址  +---------------+
           |               |
           +---------------+
    ebp1-> |     ebp       | 
           +---------------+
           |     ecx       | 
           +---------------+
           |      8        | <- esp
           +---------------+
           |               | 
    低地址  +---------------+

      此时,我们回到main函数的37行继续执行:

      37到39行:

    ->  37     addl    $1, %eax
        38     addl    $4, %esp
        39     popl    %ecx

      37行:将eax中的内容加1,注意eax通常用来做为返回值,所以,eax存储的是调用f后的返回值。

      38行:将esp增加4,这是销毁了调用f的参数栈。

      39行:将ecx寄存器恢复。还是不清楚,这是在做什么。

      栈示意图:

    高地址  +---------------+
           |               |
           +---------------+
    ebp1-> |     ebp       | <- esp
           +---------------+
           |               | 
    低地址  +---------------+

      40到42行:

    ->  40     popl    %ebp
        41     leal    -4(%ecx), %esp
        42     ret

      40行:将栈顶内容出栈到ebp寄存器。

      41行:将ecx-4中的内容出栈到esp,即将之前保存的esp内容出栈。

      42行:将eip恢复到某个地方继续执行。结束。

      现在,我们对C语言的程序执行调用用了大概的了解了。那么,计算机是怎样工作的呢。

    三. 计算机是如何工作的

      通过上面的分析,我们知道计算机的工作过程实际上就是取指令->执行指令的工程。其基本模型如下:

    for (;;) {
      read_next_intruction();
      execute_intruction(); }

      在计算机中,又不单纯的是线性的执行下去。还有跳转指令,跳转指令是通过修改eip寄存器实现的,因为cpu每次是从eip指向的内存位置取下一条指令的。

      单任务中,这个模型没什么问题。如果多任务怎么办呢?

      多任务中,引入了中断的概念了。中断信号提供了一种特殊的方式,使得处理器转而去运行正常控制流之外的代码。当一个中断信号到达时,CPU必须停止它当前正在做的事情,并且切换到一个新的活动。[3]

      这样,上面的模型就可以改成下面这样。

    for (;;) {
        read_next_intruction();
        execute_intruction();
        detect_interrupt();   
    }

      中断的概念在计算机系统中非常重要,如:IO中断,时间片中断,系统调用中断等。

      这样,我们就很基础的理解了计算机是怎样工作的了。

      下一次,我会尝试自己编译Linux内核,欢迎持续关注。

    参考资料:

      [1] 程序员的自我修养

      [2] Linux操作系统分析所需的相关基础知识.ppt by 孟宁

        [3] 深入理解Linux内核第三版

      

  • 相关阅读:
    ubuntu下如何关闭某个端口?
    linux如何将某个用户加入到其它组?
    linux如何离线加载docker镜像?
    linux下如何查看当前内核的配置?
    linux下如何单独编译设备树?
    在编译内核之前到底应该使用make mrproper,make distclean,make clean中的哪个命令呢?
    dts是如何来描述iommu与PCI(e)之间的关系?
    iommu是干什么的呢?
    ubuntu下如何使用apt-get安装arm64的交叉编译工具链?
    oracle 10g函数大全--日期型函数
  • 原文地址:https://www.cnblogs.com/sj20082663/p/3078813.html
Copyright © 2020-2023  润新知