计统大作业

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业      信息安全            

学     号      2022112631          

班   级      2203202             

学       生      蒋利权            

指 导 教 师      史先俊                

计算机科学与技术学院

2024年5月

摘  要

本文主要介绍hello程序从创建到消亡的一生,将会描述hello程序如何从原始的hello.c经过预处理、编译、汇编、链接生成可执行文件的全过程,以及计算机系统是如何对hello程序进行进程管理、存储管理和I/O管理的。本文将结合作者本人对于计算机系统的理解,原书的知识以及课外的知识等来对hello程序进行分析。通过对hello程序的生命周期的探讨,有助于我们更进一步地了解计算机系统的广博的知识。

关键词:计算机系统;生命周期;hello程序;                            

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

目  录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1 字符串

3.3.2 数组及其操作

3.3.3 局部变量

3.3.4 常量

3.3.5 赋值语句

3.3.6 类型转换语句

3.3.7 算术操作

3.3.8 关系操作及控制转移

3.3.9 循环函数

3.3.10 函数操作

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

5链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

5.3 可执行目标文件hello的格式

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

6hello进程管理

6.1 进程的概念与作用

6.2 简述壳Shell-bash的作用与处理流程

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

7hello的存储管理

7.1 hello的存储器地址空间

7.2 Intel逻辑地址到线性地址的变换-段式管理

7.3 Hello的线性地址到物理地址的变换-页式管理

7.4 TLB与四级页表支持下的VA到PA的变换

7.5 三级Cache支持下的物理内存访问

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

7.8 缺页故障与缺页中断处理

7.9动态存储分配管理

7.10本章小结

8hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

P2P(From Program to Process)

    名为hello.c的Source program被GCC编译器的驱动程序读取并翻译为可执行目标文件hello,而后,操作系统外壳shell利用函数fork为hello创建进程process,可在其中加载程序。自此,hello的P2P过程已经完成,hello由一个Program变为Process,其中GCC翻译过程包含以下四个阶段:

1.预处理阶段:程序的生命周期从一个高级C语言程序开始。在预处理阶段,预处理器cpp根据以字符#开头的命令,对原始的C程序进行处理。这些命令可以包括宏定义、条件编译指令等。预处理器将根据这些命令修改原始的C程序,并生成一个新的中间文件,通常被称为hello.i。

2.编译阶段:在编译阶段,编译器cc1将文本文件hello.i作为输入,将其翻译成另一个文本文件hello.s。这个过程中,编译器将C语言代码转换成汇编语言代码。汇编语言是与具体计算机体系结构相关的低级语言。

3.汇编阶段:在汇编阶段,汇编器as将hello.s作为输入,将其翻译成机器语言指令。汇编器将汇编语言代码转换为机器可执行的指令序列。这些指令是计算机可以直接执行的低级指令。

4.链接阶段:链接阶段是将多个目标文件组合在一起以创建最终的可执行程序的过程。链接器将汇编阶段生成的目标文件hello.o与预编译好的库函数目标文件合并。链接器解决了不同目标文件之间的符号引用和重定位等问题,生成最终的可执行目标文件,通常被命名为hello。

最后,在运行时,shell(命令行解释器)会创建一个子进程,并加载可执行目标文件hello。子进程由操作系统执行,并开始执行hello程序的指令。

020(From Zero-0 to Zero)

在hello程序执行之前,它是一个存储在硬盘上的可执行目标程序。当用户选择运行该程序时,shell会创建一个子进程,并使用fork函数复制父进程的私有地址空间。接着,execve函数会清除子进程继承的私有地址空间内容,并建立虚拟地址空间与硬盘上可执行文件的映射。同时,设置子进程的指令指针(RIP)为程序入口处的地址。操作系统在调度该进程时,若出现缺页故障,会将硬盘上相应的页面加载到物理内存中,从而开始执行程序的指令。在程序执行过程中,还会进行信号处理、上下文切换等操作。操作系统会分配CPU时间片来执行hello程序的逻辑控制流。执行结束后,父进程会回收子进程,删除内核中子进程的所有内容,释放相关资源。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

软件环境:Windows7/10 64 位以上;VirtualBox/Vmware 11 以上;Ubuntu 16.04

LTS 64 位/优麒麟 64 位 以上。

开发与调试⼯具 :Visual Studio 2010 64 位 以 上 ;CodeBlocks 64 位 ;vi/vim/gedit+gcc。

1.3 中间结果

1.4 本章小结

本章介绍了 Hello 的 P2P,020 的过程,作者本⼈编写报告时的环境和开发⼯具, 分析了 hello ⽣命周期时产⽣的各个中间⽂件。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

概念:在预处理阶段,预处理器(cpp)根据以字符#开头的命令,修改原始的 C 程 序,结果得到另⼀个 C 程序,通常是以.i 作为⽂件扩展名。

作用:

1.宏定义处理。将所有的#define 删除,展开所有的宏定义,在#def 和#undef

宏定义作⽤区间内进⾏被定义量的替换。

2.⽂件包含处理。⽐如 hello.c 中第⼀⾏的#include<stdio.h>命令告诉预处理器

读取系统头⽂件 stdio.h 的内容,并把它直接插⼊到程序⽂本中。

3.处理条件编译指令,如如#ifdef,#ifndef,#else,#elif,#endif,等等。

4.特殊符号识别,预编译程序可以识别⼀些特殊的符号。例如在源程序中出现的 LINE 标识将被解释为当前⾏号(⼗进制数),FILE 则被解释为当前被编译的.C源程序的名称。预编译程序对于在源程序中出现的这些串将⽤合适的值进⾏替换。

2.2在Ubuntu下预处理的命令

cpp hello.c > hello.i

2.3 Hello的预处理结果解析

可以看到,预处理结果成为⼀个新的 c 程序,是在原来 hello.c 的基础上将系统头⽂件<stdio.h>等⽂件内容直接插⼊程序⽂本中得到的。

2.4 本章小结

本章介绍了预处理阶段的定义和作⽤,并展示了预处理命令和预处理结果。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

概念: 编译器 cc1将⽂本⽂件 hello.i 翻译成⽂本⽂件 hello.s,它包含⼀个汇编语⾔程序。

功能:将原始的⾼级语⾔程序翻译成汇编语⾔程序是编译阶段最主要的功能,除

此之外,编译器还有检查错误,对程序进⾏优化等功能。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

图3.1

3.3 Hello的编译结果解析

图3.2

图3.3

3.3.1 字符串

在hello.c中,有两个字符串存储在只读数据区,如图3.4,并以字符数组的形式表示。这些字符串可以通过它们的首地址进行访问。请参考图3.5和图3.6。在string1字符串中,汉字使用UTF-8格式进行存储。由于UTF-8编码超过了char类型所能表示的ASCII码范围,因此汉字以十六进制格式显示,呈现为一串大正数。

图3.4

    

                          图3.5

  

                          图3.6

3.3.2 数组及其操作

在hello.c中,有一个参数的char型指针数组,数组的首地址放在栈中,指针数组的每个元素的指针里面放的是每个参数字符串所在地址。

    

图3.7

              图3.8

图 3.9 为栈中-32(%rbp)处的内容,放的是指针数组⾸地址。可以看出整个指针数组也在栈⾥,

        图3.9

图3.10是顺着指针数组⾸地址找到数组所在位置,图中 00007fffb08330fa 是数组中第⼀个元素指针的内容,也就是参数字符串所在的地址,可以看出每个参数字符串其实也放在栈⾥。

         图3.10

在每次引用数组元素时,采用首地址加偏移的方式。首先,从-32(%rbp)的位置获取指针数组的首地址。然后,通过添加偏移量来找到对应的指针。接下来,将指针中参数字符串的地址传递给寄存器,并调用puts或printf函数来打印字符串。这个过程如图3.11所示。

         图3.11

3.3.3 局部变量

在for循环中,声明了一个局部变量i。在编译器进行编译时,该局部变量i会被分配在栈中的位置,即-4(%rbp)处。这意味着每次进入循环时,都会在栈中为变量i分配内存空间,用于存储其值。

         图3.12

3.3.4 常量

⽤来做⽐较运算的常量 9等以⽴即数的形式存在。

    

             图3.13

3.3.5 赋值语句

for 循环中对 i 赋初值,直接⽤数据传输指令将⽴即数 0 传送给栈中-4(%rbp) 处。

图3.14

          图3.15

3.3.6 类型转换语句

atoi(argv[3])是一个函数调用,它将输入的最后一个参数从字符串类型转换为整数类型,并将其作为sleep函数的参数。

    

          图3.16

3.3.7 算术操作

i++涉及到加法运算,⽤ addl 命令实现。

图3.17

    

          图3.18

3.3.8 关系操作及控制转移

控制转移通常与关系操作结合在一起使用,其中关系操作会设置条件码,而控制转移根据条件码来判断是否进行跳转。

   

                        图3.19

     

                        图3.20

在这段代码中,if语句通过比较和条件跳转来实现。比较操作用于判断argc是否不等于5,即判断输入参数的个数是否等于5。条件跳转根据比较结果来决定是否执行if语句。具体而言,如果输入参数的个数等于5,程序会跳转到.L2处,执行if语句后面的指令;如果输入参数的个数不等于5,则继续向下执行,跳过if语句的执行。这样的控制流机制使得程序能够根据条件来选择执行不同的代码路径,增加了程序的灵活性和逻辑控制能力。

类似的还有for循环,也被翻译成比较加跳转。

     

         图3.21

     

         图3.22

3.3.9 循环函数

hello.c中的for循环如图3.23所示

                   图3.23

如3.3.8关系操作及控制转移中已经说过,for循环经常被翻译成比较加跳转,与if跳转不同的是,条件满足时总是向之前的命令跳转,重复执行。

3.3.10 函数操作

在主函数main中,调用了一些函数,包括printf、exit、sleep、getchar和atoi。这些函数都是共享库中的函数,可以被程序直接调用。在调用这些函数时,使用的指令是直接的call指令,没有经过额外的处理。这与图3.24的示例相似。返回值通常被放置在%rax寄存器中。当调用这些函数后,它们会将返回值存储在%rax寄存器中,以便主函数可以获取并进一步处理。

     

图3.24

3.4 本章小结

本章介绍了编译阶段编译器是如何将 c 程序翻译成汇编语⾔程序的,并进⾏了编译命令的演示,以及编译器是怎么处理 c 语⾔的各个数据类型以及各类操作的。

(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

概念及作⽤:在汇编阶段,汇编器(as)将汇编语⾔程序.s ⽂件翻译成机器语⾔指令, 把这些指令打包成⼀种叫做可重定位⽬标程序的格式,并将结果保存在⽬标⽂件hello.o 中。

4.2 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o

                      图4.1

4.3 可重定位目标elf格式

将hello.o的elf格式文件存入hello_o_elf.txt中。使用指令readelf -a hello.o >hello_o_elf.txt

                          图4.2

  1. elf头

elf头描述了生成该文件的系统的字的大小和字节顺序,以及系统框架等一系列信息。

图4.3

2.节头部表

节头部表(Section Header Table)是一个包含了文件中各个节(Section)的信息的数据结构。它记录了每个节的类型、位置、大小等相关信息。在可重定位目标文件中,每个节都是从文件的起始位置开始计算的,用于进行重定位操作。因此,节头部表中的每个节的偏移量都是从文件的起始位置开始计算的。

.text节:以编译的机器代码。

.rela.text节:一个.text节中位置的列表。

.data节:已初始化的静态和全局C变量。

.bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。

.rodata节:存放只读数据,例如printf中的格式串和开关语句中的跳转表。

.comment节:包含版本控制信息。

.note节:注释节详细描述。

.eh_frame节:处理异常。

.rela.eh_frame节:一个.eh_frame节中位置的列表。

.shstrtab节:该区域包含节区名称。

.symtab节:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。

.strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。

.symtab节:本节用于存放符号表。

                            图4.4

3.重定位节

重定位节:包含.text 节中需要进⾏重定位的信息,当链接器把这个⽬标⽂件

和其他⽂件组合时,需要修改这些位置。

                            图4.5

偏移量:需要被修改的引⽤处相对 main 处的偏移。

类型:重定位的类型。

符号名称:需要重定向的符号的名称。

加数:⼀个有符号常数,⼀些重定位要使⽤它对被修改引⽤的值做偏移调整。

  1. 符号表

符号表存放在程序中定义和引用的函数和全局变量的信息。可以看到,hello程序中引用的函数名,如exit,printf,atoi等。

                      图4.6

4.4 Hello.o的结果解析

将objdump产生的结果放于1.txt中,以便后续分析。我们可以看到机器语言中指令和寄存器及数字均是由二进制数表示的,而objdump产生的文件中左侧为机器语言,而右侧为其对应的汇编语言。

                          图4.7

图4.8

区别

(1) 在hello.s文件中,操作数和偏移量都是以十进制表示,而反汇编所使用的是十六进制表示。

  1. hello.s文件中包含了.cfi指令,这些指令是为了生成调试信息而添加的,用于描述堆栈帧的信息。
  2. 在反汇编结果中,会包含二进制的机器语言指令,而在hello.s文件中,只包含了汇编语言指令。
  3.  hello.s文件中的跳转地址使用了.Lx的形式,表示为未经重定位的内存地址。而在反汇编结果中,跳转地址会使用偏移量来表示。

(5) 在hello.s文件中,函数的调用直接使用函数名进行调用。而在hello.o文件中,使用重定位条目来占位函数调用的地址。

机器语言是一种二进制语言,是CPU可以直接识别和执行的语言,由操作码和操作数组成。而汇编语言是为了方便人们阅读和编写机器语言而产生的一种文本表示形式。汇编语言可以直接翻译成机器语言,与机器语言是一一对应的关系。

4.5 本章小结

本章讲述了汇编阶段中汇编器的作用,并展示了汇编指令以及生成的.o文件中的ELF格式信息。同时,对比了汇编语言程序.s文件和反汇编文件中的内容。

(第4章1分)


5链接

5.1 链接的概念与作用

概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可以在编译时执行,即在源代码被编译成机器代码时;也可以在加载时执行,即在程序被加载器加载到内存并执行时;甚至可以在运行时执行,即由应用程序来执行。在现代系统中,链接是由叫做链接器的程序执行的。

作用:链接器的主要任务是将多个目标文件(包括编译生成的目标文件和库文件)进行组合,解决模块之间的引用关系,生成可执行文件或共享库。链接器通过符号解析和重定位来完成这些任务。

5.2 在Ubuntu下链接的命令

Linux下使用gcc编译的命令为:

ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

                                   图5.1

5.3 可执行目标文件hello的格式

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

将hello的elf格式文件存入hello_elf.txt中。

图5.2

(1)ELF Header:类型为可执⾏⽂件,共有 30 个节。

图5.3

  1. 节头部表:不同节的位置和⼤⼩是由节头部表描述的,其中⽬标⽂件中每个节都有⼀个固定⼤⼩的条⽬。地址是每个节的起始地址(虚拟空间的地址),据此可知每个节在可执⾏⽂件中所占位置和⼤⼩,后⾯四个节没有起始地址,因为这四个节不会加载到内存中,没有必要分配地址。

                            图5.4

                            图5.5

  1. 程序头:程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。

                           图5.6

(5)重定位节:链接后,重定位部分的各个符号的偏移量更加明确,类型也更加具体,符号名称也发生了改变。

                         图5.7

(6)⽐较重要的符号表,⾥⾯还有⼀些在加载时链接需要解析的符号。

                        图5.8

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

从edb中data dump一栏可以看到加载到hello的虚拟地址空间,而从hello的elf文件中的程序头一栏可以看到链接器加载是运行的内容及与动态链接相关的信息,每个项目均有对应的虚拟内存地址,可以在edb中找到。

图5.9

5.5 链接的重定位过程分析

对 hello(hello.out)进⾏反汇编,如下图所示。

                    图5.10

分析 hello 与 hello.o 的不同之处:

1.hello.out 反汇编结果中包含了更多的节。hello.out 反汇编结果从 init 部分开始,包含了许多节,如 .text、.bss 等,而 hello.o 反汇编结果中只包含了 main 函数部分。

2.hello.out 中新增了许多来自于加载时链接的共享库中的函数代码,如 exit、printf、sleep、getchar 等函数。这是因为在加载时进行了符号解析和重定位,所以反汇编结果中会包含这些函数代码。

3.hello 反汇编结果中的代码具有确定的虚拟地址,说明已经完成了重定位过程。main 函数的地址从 0x401125 开始。而 hello.o 反汇编结果中的 main 函数地址为 0,表示尚未进行可重定位过程。

图5.11

4.在调用函数和访问字符串方面,hello.out 的反汇编结果中包含了确定的 call 地址和字符串的首地址。但是 hello.o 反汇编结果中这两部分只包含了重定位条目的占位符,还没有进行全局符号和外部符号的解析和重定位。实际上,重定位条目和指令被放置在不同的节中,但 OBJDUMP 工具将它们放在一起展示。

                         

可以看出链接的过程就是符号解析和重定位。

重定位过程:

当编译器生成可重定位目标文件时,如果遇到对最终位置未知的目标引用,它会生成一个重定位条目,用于在链接过程中进行重定位。重定位条目通常存储在.rela.txt(或其他相应的节)中,并包含了需要进行重定位的代码或数据的相关信息。详细了解重定位条目可以提供更深入的了解。

图5.12

其中的重定位条⽬类型是两种最基本的形式:

R_X86_64_PC32 ,重定位⼀个使⽤ 32 位 PC 相对地址的引⽤。

R_X86_64_32,重定位⼀份使⽤ 32 位绝对地址的引⽤。

下⾯是链接重定位时通⽤的重定位算法:

                           图5.13

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

ld-2.27.so!_dl_start 0x7fce8cc38ea0

ld-2.27.so!_dl_init 0x7fce8cc47630

hello!_start 0x400500

libc-2.27.so!__libc_start_main 0x7fce8c867ab0 -libc-2.27.so!__cxa_atexit 0x7fce8c889430

-libc-2.27.so!__libc_csu_init 0x4005c0

hello!_init 0x400488

libc-2.27.so!_setjmp 0x7fce8c884c10

-libc-2.27.so!_sigsetjmp 0x7fce8c884b70 --libc-2.27.so!__sigjmp_save 0x7fce8c884bd0

hello!main 0x400532

hello!puts@plt 0x4004b0

hello!exit@plt 0x4004e0

*hello!printf@plt  --

*hello!sleep@plt --

*hello!getchar@plt  --

ld-2.27.so!_dl_runtime_resolve_xsave 0x7fce8cc4e680 -ld-2.27.so!_dl_fixup 0x7fce8cc46df0

--ld-2.27.so!_dl_lookup_symbol_x 0x7fce8cc420b0

libc-2.27.so!exit 0x7fce8c889128

5.7 Hello的动态链接分析

动态链接器使用过程链接表(PLT)和全局偏移量表(GOT)来实现函数的动态链接。在GOT中存储了函数的目标地址,而PLT使用GOT中的地址进行跳转到目标函数。在加载时,动态链接器会对GOT中的每个条目进行重定位,确保它们包含目标函数的正确绝对地址。

简单来说,PLT和GOT是动态链接器在运行时处理函数调用和链接的机制。PLT中的代码段负责跳转到GOT中存储的函数目标地址,而GOT中的条目存储着函数的地址。在加载时,动态链接器会根据需要对GOT中的地址进行重定位,使其指向正确的目标函数的地址。

                      图5.14

5.8 本章小结

本章主要介绍了链接的概念、作用和流程。我们了解到链接是将代码和数据片段组合成可执行文件的过程,可以在编译时、加载时或运行时进行。链接器负责执行链接操作。

在本章中,我们学习了链接的命令,并详细介绍了可重定位目标文件的ELF格式和各个节的含义。通过分析示例文件hello.o,我们了解了虚拟地址空间、重定位过程、执行流程和动态链接的重要性。

通过本章的学习,我们对链接的原理和实践有了更深入的了解。链接是软件开发中不可或缺的环节,它确保了程序的正确性和可执行性。

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

概念:进程是运行中程序的实例,系统中的每个程序都在某个进程的上下文中运行。

作用:进程提供了两个抽象的概念:

1.独立的逻辑控制流:每个进程都拥有自己独立的执行流程,好像它独占地使用处理器。进程以自己的速度执行指令,按照特定的顺序执行代码,从而给用户提供了一个独立运行的假象。

2.私有的地址空间:每个进程都有自己独立的地址空间,这个地址空间是进程在运行时可以使用的内存系统。进程的地址空间包含了代码、数据、堆栈等信息,并且对其他进程是不可见的。这样,每个进程可以在自己的地址空间中进行内存操作,而不会干扰其他进程的内存数据。

6.2 简述壳Shell-bash的作用与处理流程

shell 作⽤:shell 执⾏⼀系列的读/求值步骤,然后终⽌,代表⽤户运⾏其他程序。

处理流程

(1)从终端读⼊输⼊的命令。

(2)解析以空格分隔的命令⾏参数,并构造参数 argv 向量。

(3)判断是前台进程还是后台进程。

(4)判断是否内置命令,若是则⽴即执⾏,若不是则创建⼀个⼦进程,并在⼦进程中执⾏所请求的程序。

(5)如果是前台程序,shell 使⽤ waitpid 函数等待作业终⽌,开始下⼀轮迭代;如果是后台程序,shell 返回循环的顶部,等待下⼀个命令⾏。

(6)如果有运⾏过程中有异常和信号,则调⽤信号处理⼦程序或异常处理⼦程序。

6.3 Hello的fork进程创建过程

⽗进程通过调⽤ fork 函数创建⼀个新的运⾏的⼦进程,⼦进程得到与⽗进程完全相同但是独⽴的⼀个副本,包括代码段、数据段、共享库以及⽤户栈。⼦进程还获得与⽗进程任何打开⽂件描述符相同的副本,⽗进程和⼦进程最⼤的不同时他们的 PID 是不同的。⼦进程在创建后和⽗进程是并发执⾏的。

在命令⾏输⼊“./hello 2022112631 蒋利权 13828811468  1”后,终端即⽗进程⾸先创建⼀

个⼦进程,等待加载请求的程序。

6.4 Hello的execve过程

(1)exceve 函数调用 loader 加载器函数,loader 负责删除子进程现有的虚拟内存段和映射表,清空与物理内存、硬盘上可执行文件相关的映射,并将栈和堆初始化为零。

(2)将虚拟地址空间中的页映射到磁盘上所请求的可执行文件的页。在运行时,通过缺页中断将可执行文件的页加载到物理内存中,并建立虚拟地址空间与物理地址空间之间的映射关系。

(3)将 RIP(指令指针寄存器)设置为程序的起始地址,使处理器从该地址开始执行程序的指令。这样,程序的执行流程将从指定的起始地址开始执行。

6.5 Hello的进程执行

上下文信息:上下文指的是内核重新启动被抢占的进程所需的状态信息,包括通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈以及其他内核数据结构的值。

上下文切换:当内核选择一个新的进程来运行时,它会进行进程调度。在内核调度新进程运行之前,它会抢占当前进程,并使用一种称为上下文切换的机制,将控制权转移到新的进程。上下文切换涉及保存当前进程的上下文信息,包括寄存器状态和内核数据结构,并恢复新进程的上下文信息,使其能够继续执行。这个过程是操作系统进行多任务处理的基础,确保进程间的切换和调度能够顺利进行。

时间⽚:⼀个进程执⾏它的控制流的⼀部分的每⼀时间段叫做时间⽚。

在终端命令行输入"./hello 2022112631 蒋利权 13828811468 1"后,终端会fork一个子进程。子进程通过exceve函数加载请求执行的程序,并将RIP设置为程序的起始位置。此时,hello进程成为一个独立的可以被操作系统调度的程序。

当hello进程第一次被调度时,CPU开始取指令并执行打印信息。当运行到sleep系统函数时,进程进入内核模式。内核将hello进程从运行队列中移出,并加入等待队列。定时器开始计时,并进行上下文切换,将CPU控制权交给其他进程。

直到定时器时间到达,发送一个中断信号,内核调用中断处理子程序。中断处理程序将hello进程从等待队列中移出,并重新加入运行队列。此时,hello进程可以被正常调度。

再次被调度时,hello进程继续执行打印信息,然后运行到sleep系统函数,进入内核模式。重复执行此过程,直到进程终止。最后,父进程回收子进程资源。

6.6 hello的异常与信号处理

hello 可能会出现的异常及处理⽅法:

图6.1

中断:当进程运行时,可能会收到定时器发送的信号或产生其他硬件异常,这时会触发中断,并调用相应的中断处理子程序来处理。

陷阱:当进程调用sleep系统函数时,会陷入内核态,执行相应的系统调用处理程序,处理完毕后返回到下一条指令继续执行。

故障:在进程运行过程中,可能会发生缺页故障,即访问的页面不在内存中,这时会触发缺页处理子程序进行处理。

终止:进程在运行过程中可能会遇到致命的硬件错误,例如DRAM或SRAM位损坏的奇偶错误,这时会调用终止处理程序。终止处理程序将控制权返回给一个abort例程,该例程会终止当前的应用程序。

hello 可能出现的信号及处理⽅法:

                      图6.2

                            图6.3正常运行

                             图6.4ctrl+c

                             图6.5ctrl+z

                         图6.6

                         图6.7输入pstree,可以查看hello进程位置

                      图6.8输入fg

                      图6.9用kill杀死进程

                      图6.10乱按

6.7本章小结

本章主要介绍了进程的定义和作用,以及Shell的一般处理流程和作用。同时分析了通过调用fork函数创建新进程和使用execve函数加载hello程序的过程。还详细讲解了hello进程的执行过程以及异常和信号处理机制。

(第6章1分)


7hello的存储管理

7.1 hello的存储器地址空间

在hello.c程序中,逻辑地址、线性地址、虚拟地址和物理地址之间存在一定的关系和转换过程。

逻辑地址:逻辑地址指的是程序在运行过程中使用的地址,它是相对于程序自身的视角而言的。在hello.c程序中,例如变量的地址或函数的地址都可以被表示为逻辑地址。

线性地址:线性地址是指程序在运行过程中使用的地址,它是相对于整个系统的视角而言的。线性地址是在虚拟地址和物理地址之间的中间层,它可以看作是一种抽象的地址空间。在现代计算机系统中,程序使用的地址大多数是线性地址。

虚拟地址:虚拟地址指的是程序在运行过程中使用的地址,它是相对于进程自身的视角而言的。虚拟地址是通过逻辑地址经过地址转换过程得到的,它是在程序执行时访问内存的地址。在hello.c程序中,变量和函数使用的地址可以被表示为虚拟地址。

物理地址:物理地址是指内存中实际的物理存储地址,它是硬件系统所使用的地址。物理地址是虚拟地址经过地址映射和重定位等操作后得到的实际内存地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理

图 7.1

在段式内存管理中,通过段选择符从全局描述符表(Global Descriptor Table,GDT)中获取段基址(Segment Base)以及其他段描述符的信息,然后将段基址与段内偏移相加,可以得到线性地址(Linear Address)。

段式内存管理将内存划分为多个段,每个段都有一个段描述符来描述其属性和位置。通过使用段选择符作为段的索引,可以从GDT中获取相应段描述符的信息,其中包括段基址。

然而,在Linux系统中为了提高可移植性并简化内存管理,它采用了IA-32的分页机制,将所有段描述符的基址设为0。这意味着所有段的线性地址与逻辑地址基本相同,不再需要进行段基址的加法运算。因此,在Linux系统中,逻辑地址和线性地址几乎没有区别。

通过简化分段机制并仅使用分页机制,Linux系统能够更好地适应不同的硬件平台和操作系统环境,并提供更高的可移植性和灵活性。这种改变也使得地址转换和内存管理更加统一和简化。

7.3 Hello的线性地址到物理地址的变换-页式管理

                      图7.2

首先,CPU的程序计数器(PC)生成的是虚拟地址,也就是线性地址。接下来,这个线性地址需要通过内存管理单元(MMU)进行翻译,转换成物理地址。最终,CPU芯片发出的地址信号就是物理地址。在这个转换过程中,有一个重要的工具,即存放在物理内存中的页表数据结构。页表用于将虚拟页映射到物理页。每当地址翻译硬件将一个虚拟地址转换为物理地址时,它会读取页表。

页表实际上是一个页表条目(Page Table Entry,PTE)的数组。在页表中,虚拟地址空间的每一页都有一个固定的偏移量,对应一个PTE。每个PTE由一个有效位和一个n位的地址字段组成。如果有效位被设置,那么地址字段就表示相应物理页在DRAM中的起始位置。

当CPU想要读取虚拟内存中的一个字时,地址翻译硬件将虚拟地址作为一个索引,从页表中找到对应的物理页的起始位置,然后构造该字的物理地址,并传送给地址总线。

7.4 TLB与四级页表支持下的VA到PA的变换

                    图7.3

TLB(Translation Lookaside Buffer):每当CPU生成一个虚拟地址时,MMU(内存管理单元)都必须查找对应的PTE(页表条目),以将虚拟地址转换为物理地址。在最糟糕的情况下,这个过程可能需要额外的内存访问,耗费数十到数百个时钟周期。为了减少这种开销,许多系统引入了一个关于PTE的小型缓存,称为快表(TLB)。

TLB是一个小型的虚拟地址缓存,每行保存着一个PTE块。当地址翻译硬件需要访问页表时,首先会查询TLB。如果TLB中存在要访问的PTE(即命中),那么地址翻译速度会大大提高。TLB的存在可以减少对内存的访问次数,提高地址翻译的效率。

多级页表是一种压缩页表的方法,其思想十分简单,即将虚拟地址划分为k个VPN(Virtual Page Number,虚拟页号)和一个VPO(Virtual Page Offset,虚拟页偏移量)。每个VPNi都是一个索引,用于访问第i级页表。通过逐级索引,直到最后一级页表,可以找到对应的物理页的起始地址。然后,将该物理页的起始地址与VPO结合构造出物理地址。

多级页表可以有效地减少页表的大小和访问时间。与使用单一的大型页表相比,多级页表可以将地址翻译过程划分为多个层次,每个层次的页表只需存储部分的地址映射信息,从而节省了内存空间。同时,通过多级索引的方式,可以快速定位到所需的物理页,加快地址翻译的速度。

7.5 三级Cache支持下的物理内存访问

        图7.4

物理地址被划分为m位,其中包括t位标记位(Tag),s位组索引位(Set Index),以及b位块偏移(Block Offset)。

L1缓存以S=2^s个高速缓存组的形式组织成一个数组,每个组内包含E个高速缓存行,每行由一个大小为B=2^b字节的数据块构成。

下一级存储器的内容会根据其地址中的s位组索引被缓存到相应的组中。每个组中的每一行由唯一的标记位确定,标记位用于标识缓存行对应的物理地址。此外,每一行还有一个有效位,用于指示该行是否包含有意义的数据信息。

当CPU发出一个物理地址时,该地址首先被发送到L1缓存。L1缓存会根据地址中的组索引位将其定位到对应的缓存组,并使用标记位来查找该组中是否缓存了该地址所在的数据块,并对其有效性进行检查。

如果发生缓存命中,则表示所需数据已经在L1缓存中,L1缓存会直接将这个缓存数据的副本放到数据总线上,以供CPU使用。

如果发生缓存未命中,则表示所需数据不在L1缓存中,L1缓存会将该地址发送给下一级存储设备,例如L2缓存或主存,以获取包含该地址的数据块。一旦该数据块被获取并送到L1缓存中,下一次对该地址的访问就会命中缓存,并将数据发送给CPU。

如果L1缓存已满,则根据缓存的类型和替换策略,某个块可能会被替换成新的块,以为新的数据腾出空间。

这种缓存机制能够显著提高数据访问速度,因为从缓存中获取数据比从较慢的主存或其他存储设备中获取数据更快。

7.6 hello进程fork时的内存映射

当终端(父进程)调用fork函数创建子进程hello时,子进程`hello`会获得一个与父进程用户级虚拟地址空间相同但独立的副本。这意味着子进程将拥有父进程的代码、数据以及页表等信息的一份拷贝。在子进程的虚拟地址空间中,与磁盘上可执行文件的页相对应的页已经与物理内存的页建立了映射,并且与父进程完全相同。

这种机制使得子进程能够继承父进程的执行状态,包括当前指令、堆栈和寄存器的值等。因此,子进程可以在一个与父进程隔离的环境中执行,而不会对父进程的状态产生影响。同时,子进程也可以在这个独立的环境中继续执行父进程的逻辑,或者进行完全不同的操作。

7.7 hello进程execve时的内存映射

加载命令行请求的可执行文件时,execve函数调用loader函数。在loader函数中,子进程的现有映射表和物理内存会被清空,即原有的映射关系和物理内存中的内容都会被删除。此外,栈和堆会被初始化为0。

接下来,虚拟地址空间的页会与硬盘上的命令行请求的可执行文件的页建立映射关系。需要注意的是,此时硬盘上的数据还没有加载到物理内存中,即物理内存中的页和虚拟地址空间的页之间还没有建立映射。

最后,execve函数会设置当前进程上下文的程序计数器,使其指向代码区域的入口点。这样,进程就可以被调度并开始执行了。

7.8 缺页故障与缺页中断处理

缺页故障:缺页故障是指当指令引用一个相应的虚拟地址,而对应的物理页不在内存中时发生的故障,这会触发缺页异常,将控制传递给内核的缺页处理程序。

缺页异常是在访问虚拟地址时发生的一种异常情况,表示所需的页面不在物理内存中而需要从磁盘加载。下面是缺页异常的处理流程:

1. 处理器生成一个虚拟地址,并将其传送给内存管理单元(MMU)。

2. MMU生成页表条目(PTE)的地址,并向高速缓存或主存发起请求以获取该条目。

3. 高速缓存或主存将PTE返回给MMU。

4. 如果PTE中的有效位为0,表示所需的页面不在物理内存中,此时MMU会触发一次异常,将控制传递给操作系统内核中的缺页异常处理程序。

5. 缺页处理程序首先确定物理内存中的牺牲页面(如果有的话),如果该页面已被修改,则将其换出到磁盘。

6. 缺页处理程序将新的页面调入内存,并更新内存中的PTE。

7. 缺页处理程序返回到原来的进程,重新执行导致缺页的指令。CPU会重新发送导致缺页的虚拟地址给MMU。由于虚拟页面现在已经存在于物理内存中,所以会命中并继续执行。

通过缺页异常处理流程,系统可以将需要的页面从磁盘加载到物理内存中,以满足进程对虚拟地址的访问需求。

                       图7.5

7.9动态存储分配管理

动态内存管理基本方法是通过动态内存分配器来管理进程的堆。堆是进程的虚拟内存区域,在未初始化的数据区域之后开始并向下增长。内核通过一个变量brk来维护堆的顶部位置。

动态内存分配器将堆视为一组不同大小的块的集合,并对其进行维护。每个块是一段连续的虚拟内存片段,可以是已分配或空闲状态。已分配的块被明确保留供应用程序使用,而空闲块保持空闲状态,直到被应用程序显式分配。已分配的块保持分配状态,直到被释放,释放可以由应用程序显式执行,也可以由内存分配器自身隐式执行,这取决于分配器的不同实现风格。

使用动态内存分配器,应用程序可以根据需要动态地申请和释放内存块,而不需要在编译时静态分配固定大小的内存。这种灵活性使得应用程序能够根据实际需求来管理内存,并避免了内存浪费和内存不足的问题。

动态内存分配器的实现方式多种多样,其中包括基于链表、位图、伙伴系统等不同的算法和数据结构。这些分配器负责跟踪空闲块和已分配块,并根据分配请求进行合适的内存分配和释放操作。

策略:动态内存管理主要有两种策略:

(1)使用隐式空闲链表组织堆: 在这种策略下,块的边界信息和是否已分配的状态被嵌入到块本身中。一个块由一个字的头部、有效载荷和可能的填充组成。头部编码了块的大小(包括头部和所有填充)以及块的分配状态。头部的最后一位指示块的分配状态。有效载荷紧随头部,是应用程序在调用malloc时请求的数据。有效载荷后面是一片未使用的填充块,其大小可以是任意的。空闲块通过头部块的大小字段隐含地链接在一起,形成隐式空闲链表的结构。

当应用程序请求一个大小为k的块时,分配器会遍历空闲链表,查找一个足够大的空闲块来容纳请求。分配器常见的搜索策略有首次适配、下一次适配和最佳适配。一旦分配器找到一个匹配的空闲块,如果放置策略倾向于产生更好的匹配,就会选择使用整个空闲块,这可能会导致一些内存碎片。如果匹配程度不够好,分配器通常会将该空闲块分割为两部分,一部分用于分配块,剩余部分成为一个新的空闲块。

如果分配器找不到合适的空闲块,一种选择是通过合并内存中物理上相邻的空闲块来创建更大的空闲块。如果这样仍然无法生成足够大的块,则会向内核申请额外的堆内存。

这种隐式空闲链表的策略简单且易于实现,但可能会产生内存碎片和搜索开销。因此,还有其他更高级的分配策略和数据结构,如显式空闲链表、位图分配器和伙伴系统等,用于更有效地管理动态内存分配。

                        图7.6

                        图7.7

2显式空闲链表组织堆是另一种动态内存管理策略,其中空闲块被组织成一种显式的数据结构,不需要将块的边界信息和分配状态嵌入到块本身中。相反,这些信息通过指针存储在空闲块的主体中。

常见的显式空闲链表结构是双向链表。堆被组织为一个双向空闲链表,每个空闲块都包含前驱(pred)和后继(succ)指针。通过使用双向链表而不是隐式空闲链表,分配器可以将首次适配的分配时间从块总数的线性时间减少到空闲块数量的线性时间。这是因为分配器只需要遍历空闲链表来查找合适大小的块,而不需要遍历整个堆。

显式空闲链表的优点是它提供了更高效的搜索时间和更好的分配策略控制。它允许分配器更精确地选择合适大小的空闲块,并且合并和分割操作更容易实现。然而,与隐式空闲链表相比,显式空闲链表需要额外的空间来存储指针,可能会增加内存开销。

                  图7.8

7.10本章小结

本章介绍了 hello 的四种地址空间,段式管理和⻚式管理的机制及⻚式管理中 的 TLB 和多级⻚表⼯作原理,和多级 cache 的机制,分析了 hello 进程不同阶段的 内存映射和缺⻚中断处理,最后展示了动态存储分配管理的主要⽅法和策略。

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

在Linux系统中,文件被视为一个由字节序列组成的数据集,每个文件由B0、B1、B2......Bm表示,其中m表示文件的大小。为了统一管理各种IO设备(如网络、磁盘、终端),Linux采用了一种模型化的方式,即将这些设备都抽象为文件,并通过读和写操作对这些文件进行输入和输出。

这种将设备映射为文件的方式称为文件模型化。它的优势在于统一了设备和文件的操作接口,使得对设备的读写操作与对普通文件的读写操作一样,都可以使用相应的读和写函数来进行。这样简化了设备管理的方式,提供了一种统一的编程接口。

设备管理:unix io接口

为了实现设备的管理,Linux内核引入了一种简单而低级的应用接口,称为Unix I/O接口。通过Unix I/O接口,所有的输入和输出操作都可以被视为对相应文件的读和写操作。这样,Linux系统基于Unix I/O接口来实现对设备的管理。

使用Unix I/O接口,开发者可以使用一致的方式来处理设备、文件和其他I/O操作,无需关注底层设备的细节。这种模型化的设备管理方式使得Linux系统具有很好的灵活性和可扩展性,同时简化了应用程序的编写和维护。

8.2 简述Unix IO接口及其函数

Unix IO 接⼝定义如 8.1 所述,下⾯简述其函数。

1. 打开文件:int open(char* filename, int flags, mode_t mode)

   函数说明:打开一个已存在的文件或创建一个新文件。

   参数:

     filename:要打开或创建的文件名。

     flags:指定文件的打开方式和访问模式的标志位,可以使用一个或多个标志位的按位或操作。

     mode:新创建文件的访问权限位,仅在创建文件时有效。

2. 关闭文件:int close(int fd)

  函数说明:关闭一个打开的文件,关闭一个已关闭的文件会出错。

  参数:fd是要关闭的文件描述符,返回操作结果。

3. 读取文件:ssize_t read(int fd, void *buf, size_t n)

   函数说明:从描述符为fd的当前文件位置读取最多n个字节到内存位置`buf`。

   返回值:-1表示错误,0表示EOF,否则返回实际传送的字节数量。

4. 写入文件:ssize_t write(int fd, const void *buf, size_t n)

   函数说明:从内存位置`buf`复制最多`n`个字节到描述符为`fd`的当前文件位置。

5. 修改文件位置:lseek函数

   函数说明:显示地修改当前文件的位置。

这些函数提供了对文件的基本操作,包括打开、关闭、读取、写入以及修改文件位置等功能。

8.3 printf的实现分析

首先来看看printf函数的函数体

int printf(const char *fmt, ...)

{

int i;

char buf[256];

   

     va_list arg = (va_list)((char*)(&fmt) + 4);

     i = vsprintf(buf, fmt, arg);

     write(buf, i);

   

     return i;

    }

该函数定义了一个返回类型为 int 的 printf 函数,它接收一个格式字符串 fmt 和可变数量的参数(使用省略号 ... 表示)。在函数体内部,首先定义了一些局部变量。函数体的第一个步骤是将 fmt 的地址转换为字符指针类型并加上4,以此获取可变参数的地址,并将其赋值给 va_list 类型的变量 arg。这是一种处理可变参数的方法。接下来,函数使用 vsprintf 函数将格式化后的字符串写入到一个缓冲区 buf 中。vsprintf 函数接收一个格式字符串 fmt 和一个 va_list 类型的可变参数列表 arg,然后将格式化后的字符串写入 buf 中,并返回写入的字符数。这意味着变量 i 将保存写入 buf 中的字符数。write函数的实现如下:

write:

    mov eax, _NR_write    ; 将系统调用号(_NR_write)加载到eax寄存器中

    mov ebx, [esp + 4]   ; 将参数buf的地址加载到ebx寄存器中

    mov ecx, [esp + 8]   ; 将参数count的值加载到ecx寄存器中

    int INT_VECTOR_SYS_CALL  ; 触发系统调用中断,陷入内核

首先,代码将系统调用号 `_NR_write` 加载到 `eax` 寄存器中。在 x86 体系结构中,`eax` 寄存器用于存储系统调用号。接下来,代码将参数 `buf` 的地址加载到 `ebx` 寄存器中,这代表要写入的字符串的起始地址。然后,代码将参数 `count` 的值加载到 `ecx` 寄存器中,表示要写入的字节数。最后,代码使用 `int` 指令触发 `INT_VECTOR_SYS_CALL` 中断,使程序陷入内核态,进入异常处理程序,从而执行与系统调用相对应的内核操作。在这种情况下,内核会处理写操作,并将字符串从寄存器通过总线复制到显卡的显存中。字符显示驱动程序会将存储在显存中的 ASCII 码转换为相应的点阵信息,并将这些点阵信息存储在 VRAM 中。显示芯片会按照刷新频率逐行读取 VRAM,通过信号线向液晶显示器传输每个点的 RGB 分量,从而在显示器上显示相应的字符。通过这个过程,`write` 函数实现了将字符串写入输出设备的功能,完成了 `printf` 函数的最后一步操作。

8.4 getchar的实现分析

当程序调用 getchar 函数时,程序会暂停执行并等待用户按键输入。用户按下按键后,键盘接口捕获按键的扫描码,并产生中断请求。中断请求会打断当前正在执行的进程,触发键盘中断处理程序。键盘中断处理程序首先从键盘接口获取按键的扫描码,将其转换为 ASCII 码,并存储在系统的键盘缓冲区中。当用户按下回车键(回车键也存储在缓冲区中)后,getchar 调用 read 系统函数,每次从缓冲区读取一个字符,直到读取到回车键为止。getchar 函数返回用户输入的第一个字符的 ASCII 码,如果发生错误,则返回 -1,并将用户输入的字符显示在屏幕上。如果用户在按下回车键前输入了多个字符,这些字符会保留在键盘缓冲区中,等待后续的 getchar 调用。换句话说,后续的 getchar 调用将直接读取缓冲区中的字符,而不再等待用户按键,直到缓冲区中的字符全部读取完毕。然后,程序才会再次等待用户按键输入。键盘中断的处理机制如下:键盘中断处理程序接收按键的扫描码并将其转换为 ASCII 码,然后将其存储在系统的键盘缓冲区中。像 getchar 这样的函数通过系统调用 read 从缓冲区读取按键的 ASCII 码,直到读取到回车键为止。

8.5本章小结

这一章讨论了Linux的I/O设备的基本概念和管理方法,展示了Unix I/O接口及其函数,并最后分析了printf函数和getchar函数的工作过程。

(第8章1分)

结论

hello所经历的过程

1.hello.c: 编写C程序,每个字符都用ASCII编码表示。

2.hello.i: hello.c经过预处理阶段,生成hello.i。

3.hello.s: hello.i经过编译阶段,生成hello.s。

4.hello.o: hello.s经过汇编阶段,生成hello.o。

5.hello.out: hello.o经过链接阶段,生成可执行文件hello(也可以称为hello.out)。

6.加载: 进行动态链接;在shell中fork一个子进程,使用execve加载请求的进程,建立虚拟内存与硬盘上可执行文件的映射,设置RIP(指令指针寄存器)。

7.运行: 执行该进程,硬件和软件协同工作完成地址翻译和物理内存的访问,动态分配内存,处理信号和异常,进行上下文切换等操作。

8.回收: 子进程终止后,父进程将其回收。

对计算机系统设计与实现的感悟:

学完这门课程,我对设计计算机系统的那些工程师们充满了敬意。计算机系统的设计是一个巨大而复杂的工程,它涉及硬件和软件两个广泛领域的紧密结合。在硬件方面,处理器、内存和存储设备等各个组件相互协作,构建出一个高效稳定的计算基础。而在软件方面,操作系统、编程语言和各种应用程序则依赖硬件,为用户提供多样功能和便捷操作界面。

在学习的过程中,我深刻感受到了计算机系统的复杂性和精密性。每个组件的设计和实现都需要精心策划和调试,每一个细节都可能对整个系统的性能和功能产生重大影响。我意识到,计算机系统的设计不仅需要扎实的技术基础,还需要细致入微的态度和对细节的严格追求。

此外,计算机系统的设计还需要与不断变化的技术和需求同步。随着科技的不断发展,计算机系统必须不断适应新的硬件架构、新的算法和新的应用场景。这意味着设计者需要不断学习和更新自己的知识,保持对技术发展的敏感性和适应性。

(结论0分,缺失 -1分,根据内容酌情加分)


附件

hello.c

c语言源文件

hello.i

预处理后生成的文本文件

hello.s

编译后产生的汇编语言文件

hello.o

汇编后产生的二进制可重定位目标文件

hello

链接后的产生的可执行文件

hello_elf.txt

hello的elf格式文件的文本

hello_o_elf.txt

hello.o的elf格式文件的文本

1.txt

hello.o的反汇编文件的文本

(附件0分,缺失 -1分)


参考文献

[1] Randal E.Bryant.深⼊理解计算机系统[M].北京:机械⼯业出版社,2016.7

[2] https://www.cnblogs.com/diaohaiwei/p/5094959.html

(参考文献0分,缺失 -1分)

  • 27
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值