深入理解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内核第三版

  

 

posted @ 2013-05-14 22:20  20082663  阅读(591)  评论(2编辑  收藏  举报