深入了解计算机系统简介

深入了解计算机系统简介

从helloworld入手

首先我们来理解一下简单的hello world 程序是怎么被电脑执行的

1
2
3
4
5
6
7
#include <stdio.h>

int main()
{
printf("hello, world\n");
return 0;
}

这个hello.c 文件在被电脑执行之前需要比其他程序转化为一系列的低级机器语言指令。然后这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。目标程序也成为可执行目标文件。

整个过程如下所示。

  • 首先是preprocessor 阶段,预处理器(cpp) 会以# 开头的命令修改原始的C程序。比如 hello.c中的第一行#include <stdio.h> 命令告诉预处理器读取系统头文件 stdio.h 的内容,并把它直接插入到程序文本当中,结果就得到了另一个C程序,通常是以.i作为文件扩展名

我们在命令行中敲入

1
gcc -E hello.c -o hello.i

预处理器就会读取头文件的内容然后直接插入到程序文本当中去。一共添加了了600多行,这里截取了最后一段。上面所有的内容都是头文件stdio.h 中包含的

  • 第二个阶段是编译阶段。编译器(ccl) 会将文本文件 hello.i 翻译成 文本文件 hello.s 它包含一个汇编语言程序。该程序包含函数main的定义我们敲入命令行
1
gcc -S hello.i -o hello.s

随后文件夹下会多出一个hello.s 的文件,文件中的语言是汇编语言,是一种低级机器语言指令。文件中包含一个main函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
   .file  "hello.c"
.def __main; .scl 2; .type 32; .endef
.section .rdata,"dr"
.LC0:
.ascii "hello, world\0"
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
subq $32, %rsp
.seh_stackalloc 32
.seh_endprologue
call __main
leaq .LC0(%rip), %rcx
call puts
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (tdm64-1) 4.9.2"
.def puts; .scl 2; .type 32; .endef

汇编语言是非常有用的。它为不同的高级语言的不同编译器提供了通用的输出语言

  • 汇编阶段 是汇编器(as) 将hello.s 翻译成机器语言指令,把这些指令打包成一种可叫做 relocatable object program 的格式,并将结果保存在目标文件hello.o 当中。hello.o文件是一个二进制文件,它包含的17 个字节是函数main的指令编码。如果我们在文本编辑器中打开hello.o文件,将看到一堆乱码。

在命令行中输入,文件夹下多出一个 hello.o 的文件 。

1
gcc -c hello.s -o hello.o

  • 最后一个是链接阶段。hello程序调用了printf函数,他是每个C编译器都提供的标准C库中的一个函数。printf函数存在于一个名为printf.o 的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o 程序中。链接器(ld)就负责处理这种合并. 结果就得到了hello文件,它是一个可执行目标文件,可以加载到内存中并被系统调用

计算机是怎么运行hello程序的呢?

下面是一个冯诺依曼架构的计算机模型。里面分为好几个部分:主线,输入/输出,主存,处理器等。初始时,shell程序执行它的指令,等待我们输入一个命令当我们在键盘上输人字符串“./hello”后,shell 程序将字符逐一读入寄存器,再把它存放到内存中

当我们按下回车键的时候,shell会执行一系列指令来加载可执行的hello文件,这些指令将hello目标文件中的代码和数据从磁盘复制到主存。数据包括最终会被输出的字符串 “hello,world\n”

一旦目标文件hello 中的代码和数据被加载到主存,处理器就开始执行hello 程序的main 程序中的机器语言指令。这些指令将“hello, world\n 字符串中的字节从主存复制到寄存器文件(Register file),再从寄存器文件中复制到显示设备,最终显示在屏幕上.

存储设备形成层次结构

每个计算机系统中的存储设备都被组织成了一个存储器层次结构。在这个层次结构中,从上至下,设备的访
问速度越来越慢、容量越来越大,并且每字节的造价也越来越便宜。寄存器文件在层次结构中位于最顶部,也就是第0级或记为$L_0$ 这里我们展示的是三层高速缓存L1 到L3,占据存储器层次结构的第1层到第3层。主存在第4 层, 以此类推。

存储器层次结构的主要思想是上一层的存储器作为低一层存储器的高速缓存。因此,寄存器文件就是L1 的高速缓存,L1 是L2 的高速缓存,L2 是L3 的高速缓存,L3 是主存 的高速缓存,而主存又是磁盘的高速缓存。在某些具有分布式文件系统的网络系统中,本地磁盘就是存储在其他系统中磁盘上的数据的高速缓存。

程序员可以利用对整个存储器层次结构的理解来提高程序性能。

作业

  1. 测试自己的编程环境下,C语言是否允许void main()?如果出错,报什么类型的错误。
    如果文件为.cpp 的话,void main()会出现报错’::main’ must return ‘int’ ,

但是如果是.c文件的话,void main() 是可以通过编译并且运行的。

这是因为.cpp文件和.c文件的编译器是不同的。.c 文件的编译器是gcc,而.cpp文件的编译器是g++.

  1. 尝试使用一个int型的4096x4096的二维数组,如果遇到错误,了解不同错误消息的具体含义。

如果把这个二维数组定义在main() 函数当中, 那么是无法定义的。会遇到错误 ”Process finished with exit code -1073741571 (0xC00000FD)”,报错的提示是程序意外终止。查阅资料得到原因 StackOverflow也就是栈区溢出。Windows下栈区默认的大小为1024kb,但是4096x4096xsizeof(int) /1024/1024 =16 MB,所以导致了栈区溢出

但是如果把二维数组设置为静态变量或者全局变量,那么编译器并不会给报错。这是因为它们存储在大小比规定栈的大小更大的静态储存区,因此能申请到更大的数据。

-------------本文结束,感谢您的阅读-------------