【CSAPP】计算机系统漫游

本文从hello程序出发,深入探讨计算机系统的工作原理,涵盖编译过程的预处理、编译、汇编和链接阶段,以及处理器如何读取指令。通过讲解系统硬件组成,包括总线、I/O设备、主存和处理器,揭示了程序运行的硬件基础。文章还讨论了操作系统在管理硬件、进程、线程和虚拟内存等方面的作用,强调了理解和掌握这些基础知识对于优化程序性能、避免安全漏洞的重要性。
摘要由CSDN通过智能技术生成


计算机系统是硬件和系统软件互相交织的集合体

hello程序

hello.c

#include <stdio.h>

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

我们通过跟踪hello程序的生命周期来开始对系统的学习:从它被程序员创建开始,到在系统上运行,输出简单的消息,然后终止。

信息就是位 + 上下文

hello程序的生命周期是从源程序源文件)开始的,即程序员通过编辑器创建并保存的文本文件hello.c。源程序实际上就是一个由01组成的比特bit)序列。8个位被组织成一组,称为字节byte)。

ASCII标准用一个唯一单字节大小的整数值表示一个字符,如字符#对应的数值是35,字符i对应的数值是105hello.cASCII文本表示如下图所示:
在这里插入图片描述
hello.c这样可以通过标准字符集(如ASCII)解析的文件称为文本文件,所有其他文件都称为二进制文件

系统中的所有信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文。比如在不同的上下文中,一串相同的字节序列可能表示一个整数、浮点数、字符串或机器指令。

通常情况下,机器表示的数值是实际数值的有限近似值

程序被其他程序翻译成不同的格式

hello.c是能够被人读懂的文本文件,但为了在系统上运行它,每条C语句都必须被其他程序转化为一系列的低级机器语言指令(能被机器读懂)。然后这些指令按照一种称为可执行目标程序的格式打好包,并以二进制文件可执行目标文件)的形式存放在磁盘。

Unix系统上,从源文件目标文件的转化是由编译器驱动程序(简称编译器)完成的:
在这里插入图片描述
在这里,gcc编译器读取hello.c并生成hello的过程可分为四个阶段:预处理编译汇编链接。与此相关的预处理器编译器汇编器链接器一起构成了编译系统
在这里插入图片描述

预处理阶段

预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如#include <stdio.h>命令告诉cpp读取系统头文件stdio.h里的内容,并把它插入程序文本中相应的位置。结果就得到了另一个以.i作为文件扩展名的C程序。

编译阶段

编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。该程序包含函数main的定义:

main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$.LC0, %edi
	call	puts
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc

上述每条汇编语句都文本的形式描述了一条低级机器语言指令。不同高级语言不同编译器针对某个确定的CPU架构输出统一的汇编语言。

汇编阶段

汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o中包含的是函数main的指令编码,如果在文本编辑器中打开该二进制文件,会看到一堆乱码。

链接阶段

hello程序调用了printf函数,它是每个C编译器都提供的标准C库中的一个函数。printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,这个文件必须以某种方式合并hello.o中,否则main函数将无法调用printf函数。

链接器(ld)就负责处理这种合并,结果就得到hello文件,它是一个可执行目标文件(简称可执行文件),平时存储在磁盘上,运行时被加载到内存,由系统执行。

了解编译系统如何工作是大有益处的

优化程序性能

了解编译器将不同的C语句转化为机器代码的方式,有助于我们写出更高效的代码。如:

  1. 一个switch语句是否总比一系列的if-else高效?
  2. 一个函数调用的开销有多大?
  3. while循环比for循环更有效吗?
  4. 指针引用比数组索引更有效吗?
  5. 为什么将循环求和的结果放到一个本地变量中,会比将其放到一个通过引用传递过来的参数中,运行起来快很多呢?
  6. 为什么我们只是简单地重新排列一下算术表达式中的括号就能让函数运行得更快?

理解链接时出现的错误

一些最令人困扰的程序错误都和链接有关,尤其是构建大型软件系统的时候。如:

  1. 链接器报告说它无法解析一个引用,这是什么意思?
  2. 静态变量和全局变量的区别是什么?
  3. 在不同的C文件中定义了名字相同的两个全局变量会发生什么?
  4. 静态库和动态库的区别是什么?
  5. 命令行上排列库的顺序有什么影响?
  6. 为什么有些链接错误直到运行时才会出现?

避免安全漏洞

缓冲区溢出是造成软件安全漏洞的主要原因,学习安全编程的第一步就是理解数据控制信息存储在程序栈上的方式(这种方式由编译器决定)及其会引起的后果。

处理器读并解释储存在内存中的指令

现在,hello.c源程序已经被编译系统翻译成了可执行目标文件hello,并被存放在磁盘上。要想在Unix系统上运行该可执行文件,我们将它的文件名输入到称为shell的应用程序中。
在这里插入图片描述
shell是一个命令行解释器,它输出一个提示符,等待输入一个命令行,然后执行这个命令。
在此例中,shell加载并运行hello程序,然后等待程序终止,随后再输出一个提示符,等待输入下一个命令行。

系统的硬件组成

要理解hello程序运行时发生了什么,我们需要了解一下典型系统的硬件组织。
在这里插入图片描述

总线

贯穿整个系统的是一组电子管道,称作总线bus),它携带信息字节并负责在各个部件间传递。通常总线被设计成传送定长的字节块,也就是word)。字的字节数(即字长)是一个基本的系统参数,各系统不尽相同,如32位系统的字长是4字节,64位系统的字长是8字节。

I/O设备

I/O输入/输出)设备是系统与外部世界的联系通道。图中包括4I/O设备:作为输入设备的鼠标键盘,作为输出设备的显示器,即能输入又能输出、长期存储数据和程序的磁盘驱动器(简称磁盘)。hello程序就存放在磁盘上。

每个I/O设备都通过一个控制器适配器I/O总线相连。控制器是I/O设备本身或者系统的主印制电路板(即主板)上的芯片组,适配器则是一块插在主板插槽上的卡(可插拔),它们的功能都是在I/O总线和I/O设备之间传递信息。

主存

主存(即内存)是一个临时存储设备,在处理器执行程序时,用来存放程序程序处理的数据。每条程序指令所占用的字节数不尽相同,每个数据变量所占用的字节数也不尽相同。

物理上来说,主存是由一组动态随机访问存储器DRAM)芯片组成的。逻辑上来说,主存是一个线性的字节数组,每个字节都有其唯一的地址(数组索引),地址从0开始。

处理器

中央处理单元CPU),简称处理器,是执行存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(寄存器),称为程序计数器PC),PC始终指向CPU执行的当前指令或下一条指令。从系统通电开始,直到系统断电,处理器一直在不断地执行PC指向的指令,再更新PC,执行下一条指令。

处理器是按照一个简单的指令执行模型工作的,这个模型由其指令集架构(如x86_64armv8)决定。
执行指令时,处理器从PC指向的内存处读取指令解释指令中的位执行该指令指示的简单操作,然后更新PC

这样的简单操作围绕着主存寄存器文件算术逻辑单元ALU)进行。寄存器文件是一个小的存储设备,由一些单字长的寄存器组成,每个寄存器都有唯一的名字。ALU计算新的数据和地址。CPU在执行一条指令时,可能会做这些简单操作:

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值