计算机原理-程序编译及运行(五)

前面一篇我们已经知道CPU所需的指令和数据都在内存中的基本工作原理,现在再介绍下程序运行在内存的分布情况。

我们知道对于计算机系统来说,最底层的是硬件,硬件之上是操作系统,而我们的程序都是基于操作系统来运行的,而不是基于硬件,这样操作系统为我们提供了一层抽象,所以对于程序员来说,不需要特别的关注计算机硬件。所以正常来说介绍完硬件后,应该来介绍操作系统。但是我们知道在计算机在不断的发展,硬件,操作系统都在不断发展。而无论怎么发展,程序运行都离不开内存,所以我决定以内存和出发点,来看硬件,操作系统以及应用程序的发展过程。

一.操作系统历史

1.手工系统:独占处理,程序运行独占所有CPU,内存资源。

2.单道批处理系统:程序独占资源,自动化操作

3.多道批处理操作系统:程序独占CPU,共享内存(同时加载到内存)

4.分时操作系统:CPU时间分片,通过调度按权限让程序轮流执行。

5.现在操作系统:多CPU,多进程,多线程,虚拟内存等等,进化史基本就是为了更好的执行效率,更好的用户体验,基本都是为了克服硬件的IO问题。

二.程序编译和链接

一里面我们知道了操作系统如何调度让程序在机器上更加高效的运行起来,现在我们来看看程序是如何从一行行代码变成可运行的可执行程序的。

编译过程:

(源代码)–预处理–编译—汇编—链接–(可执行文件)。在Linux下一般使用GCC来编译C语言程序, 而VS中使用cl.exe。下图就是上面的代码在GCC中编译的过程。我们后面讨论的都以C语言为例。编译器

 

2.1.1 预处理

预处理是程序编译的第一步,以C语言为例, 预编译会把源文件预编译成一个 .I 文件。而C++则是编译成 .ii。 GCC中预编译命令如下

view plain copy

print?在CODE上查看代码片派生到我的代码片

  1. #gcc -E hello.c -o hello. I 

当我们打开hello.i 文件是会发现这个文件变的好大,因为其中包含的<stdio.h> 文件被插入到了hello.i 文件中,一下是截取的一部分内容

view plain copy

print?在CODE上查看代码片派生到我的代码片

  1. # 1 “hello.c”
  2. # 1 “<built-in>”
  3. # 1 “<命令行>”
  4. # 1 “hello.c”
  5. # 1 “/usr/include/stdio.h” 1 3 4
  6. # 28 “/usr/include/stdio.h” 3 4
  7. # 1 “/usr/include/features.h” 1 3 4
  8. # 324 “/usr/include/features.h” 3 4
  9. # 1 “/usr/include/i386-linux-gnu/bits/predefs.h” 1 3 4
  10. # 325 “/usr/include/features.h” 2 3 4
  11. # 357 “/usr/include/features.h” 3 4
  12. # 1 “/usr/include/i386-linux-gnu/sys/cdefs.h” 1 3 4
  13. # 378 “/usr/include/i386-linux-gnu/sys/cdefs.h” 3 4
  14. # 1 “/usr/include/i386-linux-gnu/bits/wordsize.h” 1 3 4
  15. # 379 “/usr/include/i386-linux-gnu/sys/cdefs.h” 2 3 4
  16. # 358 “/usr/include/features.h” 2 3 4
  17. # 389 “/usr/include/features.h” 3 4
  18. # 1 “/usr/include/i386-linux-gnu/gnu/stubs.h” 1 3 4# 940 “/usr/include/stdio.h” 3 4
  19. # 2 “hello.c” 2
  20. int main()  
  21. {  
  22.     printf(“Hello, world.\n”);  
  23. return 0;  

总结下来预处理有一下作用:

  • 所有的#define删除,并且展开所有的宏定义
  • 处理所有的条件预编译指令,比如我们经常使用#if #ifdef #elif #else #endif等来控制程序
  • 处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。这也就是为什么我们要防止头文件被多次包含。
  • 删除所有注释 “//”和”/* */”.
  • 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。比如上面的 # 2 “hello.c” 2
  • 保留所有的#pragma编译器指令,因为编译器需要使用它们。
2.1.2 编译

编译是一个比较复杂的过程。编译后产生的是汇编文件,其中经过了词法分析、语法分析、语义分析、中间代码生成、目标代码生成、目标代码优化等六个步骤。如要要详细深入研究,就要去看下《编译原理》。当我们语法有错误、变量没有定义等问题是,就会出现编译错误。

view plain copy

print?在CODE上查看代码片派生到我的代码片

  1. #gcc -S hello.i -o hello.s 

通过上面的命令,可以从预编译文件生成汇编文件,当然也可以之际从源文件编译成汇编文件。实际上是通过一个叫做ccl的编译程序来完成的。

view plain copy

print?在CODE上查看代码片派生到我的代码片

  1.     .file   “hello.c”  
  2.     .section    .rodata  
  3. .LC0:  
  4.     .string “Hello, world.”  
  5.     .text  
  6.     .globl  main  
  7.     .type   main, @function  
  8. main:  
  9. .LFB0:  
  10.     .cfi_startproc  
  11.     pushl   %ebp  
  12.     .cfi_def_cfa_offset 8  
  13.     .cfi_offset 5, -8  
  14.     movl    %esp, %ebp  
  15.     .cfi_def_cfa_register 5  
  16.     andl    $-16, %esp  
  17.     subl    $16, %esp  
  18.     movl    $.LC0, (%esp)  
  19.     call    puts  
  20.     movl    $0, %eax  
  21.     leave  
  22.     .cfi_restore 5  
  23.     .cfi_def_cfa 4, 4  
  24.     ret  
  25.     .cfi_endproc  
  26. .LFE0:  
  27.     .size   main, .-main  
  28.     .ident  “GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3″  
  29.     .section    .note.GNU-stack,””,@progbits 

上面就是生成的汇编文件。我们看出其中分了好几个部分。我们只需要关注,LFB0这个段中保存的就是C语言的代码对于的汇编代码.。

2.1.3 汇编

汇编的过程比较简单,就是把汇编代码转换为机器可执行的机器码,每一个汇编语句机会都对应一条机器指令。它只需要根据汇编指令和机器指令的对照表进行翻译就可以了。汇编实际是通过汇编器as来完成的,gcc只不过是这些命令的包装。

view plain copy

print?在CODE上查看代码片派生到我的代码片

  1. #gcc -c hello.s -o hello.o  
  2. //或者  
  3. #as hello.s -o hello.o 

汇编之后生成的文件是二进制文件,所以用文本打开是无法查看准确的内容的,用二进制文件查看器打开里面也全是二进制,我们可以用objdump工具来查看:

view plain copy

print?在CODE上查看代码片派生到我的代码片

  1. cc@cheng_chao-nb-vm:~$ objdump -S hello.o  
  2. hello.o:     file format elf32-i386  
  3. Disassembly of section .text:  
  4. 00000000 <main>:  
  5.    0:   55                      push   %ebp  
  6.    1:   89 e5                   mov    %esp,%ebp  
  7.    3:   83 e4 f0                and    $0xfffffff0,%esp  
  8.    6:   83 ec 10                sub    $0x10,%esp  
  9.    9:   c7 04 24 00 00 00 00    movl   $0x0,(%esp)  
  10.   10:   e8 fc ff ff ff          call   11 <main+0x11>  
  11.   15:   b8 00 00 00 00          mov    $0x0,%eax  
  12.   1a:   c9                      leave    
  13.   1b:   c3                      ret     

上面我们看到了Main函数汇编代码和机器码对应的关系。关于objdump工具后面会介绍。这里生成的.o文件我们一般称为目标文件,此时它已经和目标机器相关了。

2.1.4 链接

链接是一个比较复杂的过程,其实链接的存在是因为库文件的存在。我们知道为了代码的复用,我们可以把一些常用的代码放到一个库文件中提供给其他人使用。而我们在使用C,C++等高级语言编程时,这些高级语言也提供了一些列这样的功能库,比如我们这里调用的printf 函数就是C标准库提供的。 为了让我们程序正常运行,我们就需要把我们的程序和库文件链接起来,这样在运行时就知道printf函数到底要执行什么样的机器码。

view plain copy

print?在CODE上查看代码片派生到我的代码片

  1. #ld -static crt1.o crti.o crtbeginT.o hello.o -start-group -lgcc -lgcc_eh -lc-end-group crtend.o crtn.o 

我们看到我们使用了链接器ld程序来操作,但是为了得到最终的a.out可执行文件(默认生成a.out),我们加入了很多目标文件,而这些就是一个printf正常运行所需要依赖的库文件。

2.1.5 托管代码的编译过程

对于C#和Java这种运行在虚拟机上的语言,编译过程有所不同。 对于C,C++的程序,生成的可执行文件,可以在兼容的计算机上直接运行。但是C#和JAVA这些语言则不同。他们编译过程是相似的,但是他们最终生成的并不是机器码,而是中间代码,对于C#而言叫IL代码,对于JAVA是字节码。所以C#,JAVA编译出来的文件并不能被执行。

我们在使用.NET或JAVA时都需要安装.NET CLR或者JAVA虚拟机,以.NET为例,CLR实际是一个COM组件,当你点击一个.NET的EXE文件时,它和C++等不一样,不能直接被执行,而是有一个垫片程序来启动一个进程,并且初始化CLR组件。当CLR运行后,一个叫做JIT的编译器会吧EXE中的IL代码编译成对应平台的机器码,然后如同其他C++程序一样被执行。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>