CSAPP大作业程序人生

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业   数据科学与大数据技术   

学     号   2021110564            

班     级   2103501               

学       生   崔志阳                

指 导 教 师    史先俊                   

计算机科学与技术学院

2023年4月

摘  要

    本文主要讲述了hello.c程序从编写完到执行完的历程,从预处理,编译,汇编,链接,再到进程管理,存储管理,IO管理。并对每一过程都加以分析。

关键词:关键词1hello;关键词2预处理;关键词3编译;                           

目  录

第1章 概述................................................................................................................ - 4 -

1.1 Hello简介......................................................................................................... - 4 -

1.2 环境与工具........................................................................................................ - 4 -

1.3 中间结果............................................................................................................ - 4 -

1.4 本章小结............................................................................................................ - 4 -

第2章 预处理............................................................................................................ - 5 -

2.1 预处理的概念与作用........................................................................................ - 5 -

2.2在Ubuntu下预处理的命令............................................................................. - 5 -

2.3 Hello的预处理结果解析................................................................................. - 5 -

2.4 本章小结............................................................................................................ - 5 -

第3章 编译................................................................................................................ - 6 -

3.1 编译的概念与作用............................................................................................ - 6 -

3.2 在Ubuntu下编译的命令................................................................................ - 6 -

3.3 Hello的编译结果解析..................................................................................... - 6 -

3.4 本章小结............................................................................................................ - 6 -

第4章 汇编................................................................................................................ - 7 -

4.1 汇编的概念与作用............................................................................................ - 7 -

4.2 在Ubuntu下汇编的命令................................................................................ - 7 -

4.3 可重定位目标elf格式.................................................................................... - 7 -

4.4 Hello.o的结果解析.......................................................................................... - 7 -

4.5 本章小结............................................................................................................ - 7 -

第5章 链接................................................................................................................ - 8 -

5.1 链接的概念与作用............................................................................................ - 8 -

5.2 在Ubuntu下链接的命令................................................................................ - 8 -

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

5.4 hello的虚拟地址空间..................................................................................... - 8 -

5.5 链接的重定位过程分析.................................................................................... - 8 -

5.6 hello的执行流程............................................................................................. - 8 -

5.7 Hello的动态链接分析..................................................................................... - 8 -

5.8 本章小结............................................................................................................ - 9 -

第6章 hello进程管理....................................................................................... - 10 -

6.1 进程的概念与作用.......................................................................................... - 10 -

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

6.3 Hello的fork进程创建过程......................................................................... - 10 -

6.4 Hello的execve过程..................................................................................... - 10 -

6.5 Hello的进程执行........................................................................................... - 10 -

6.6 hello的异常与信号处理............................................................................... - 10 -

6.7本章小结.......................................................................................................... - 10 -

第7章 hello的存储管理................................................................................... - 11 -

7.1 hello的存储器地址空间................................................................................ - 11 -

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

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

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

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

7.6 hello进程fork时的内存映射..................................................................... - 11 -

7.7 hello进程execve时的内存映射................................................................. - 11 -

7.8 缺页故障与缺页中断处理.............................................................................. - 11 -

7.9动态存储分配管理........................................................................................... - 11 -

7.10本章小结........................................................................................................ - 12 -

第8章 hello的IO管理.................................................................................... - 13 -

8.1 Linux的IO设备管理方法............................................................................. - 13 -

8.2 简述Unix IO接口及其函数.......................................................................... - 13 -

8.3 printf的实现分析........................................................................................... - 13 -

8.4 getchar的实现分析....................................................................................... - 13 -

8.5本章小结.......................................................................................................... - 13 -

结论............................................................................................................................ - 14 -

附件............................................................................................................................ - 15 -

参考文献.................................................................................................................... - 16 -

第1章 概述

1.1 Hello简介

Hello程序起源是用c语言编写的hello.c源文件开始的。在Linux系统下,将hello.c进行预处理,生成hello.i文件,然后将hello.i文件翻译成汇编语言文件hello.s。然后再将hello.s生成重定位文件hello.o。最后链接完成生成可执行文件hello。下面为示意图(来自CSAPP)。

图1-1

操作系统调用execve后映射虚拟内存,先删除当前虚拟地址的数据结构并为hello创建新的区域结构,进入程序入口后载入物理内存,再进入main函数执行代码。代码完成后,父进程回收hello进程,内核删除相关数据结构。

1.2 环境与工具

1)硬件环境:X64 CPU;2GHz;4GRAM;256Disk

(2)软件环境:Windows10 64位;Vmware 10;Ubuntu 16.04 LTS 64位

(3)使用工具:Codeblocks;Objdump;Gdb;Hexedit

1.3 中间结果

hello.c:程序源代码文件。

hello.i:预处理器对hello.c进行宏替换和头文件展开后生成的中间文件。

hello.s:编译器对hello.i进行编译后生成的汇编代码文件。

hello.o:汇编器将hello.s汇编成目标文件hello.o,其中包含了机器码和可重定位信息。

hello:可执行文件,由链接器将目标文件和系统库链接生成,包含了程序的机器码和可执行信息。

helloo.elf:hello.o的ELF格式。

hello.elf:hello的ELF格式,分析重定位过程。

1.4 本章小结

本章主要介绍Hello程序P2P,020的整个过程,并且列出了实验环境与工具。

第2章 预处理

2.1 预处理的概念与作用

概念:

程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。

作用:

最常见的预处理是C语言和C++语言。ISO C和ISO C++都规定程序由源代码被翻译分为若干有序的阶段(phase) ,通常前几个阶段由预处理器实现。预处理中会展开以#起始的行,试图解释为预处理指令 (preprocessing directive) ,其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。

2.2在Ubuntu下预处理的命令

通过gcc -E -o hello.i hello.c命令实现对hello.c的预处理,并且生成hello.i。

 

 

图2-

2.3 Hello的预处理结果解析

       可以看到hello.i拓展到了三千多行,这是因为将hello.c上面的头文件给全部展开写入到hello.i当中。

 

图2-2

2.4 本章小结

在本章实现了对hello.c的预处理,初步了解hello.i文件中的内容。

第3章 编译

3.1 编译的概念与作用

编译是编译器将预处理文件hello.i翻译汇编语言文件hello.s。

作用首先是检查程序是否有语法错误,例如warning,error。其次是将其翻译成汇编语言。编译器还能对程序进行优化。

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s

 

图3-1

产生了hello.s汇编语言文件。

3.3 Hello的编译结果解析

3.3.1:程序前面的初始部分:

     

                                                图3-2

     

.file    指出源文件名为hello.c

.text    表示代码节

.rodata  表示只读的数据

.align   表示对齐的方式,这里表示8字节对齐

.string   声明字符串

.globle  声明全局变量

.type     声明符号类型

3.3.2 数据部分

 

 

图3-3

这两个字符串放在只读数据中。

 

图3-4

这里是将main函数的两个参数argc和字符指针argv存入rdi,rsi中并且将其压入栈中。

3.3.3赋值操作:

 

图3-5

将循环控制变量i赋值为0。通过movl指令,因为int为32位,所以位l。

3.3.4关系操作:

 

图3-6

此处对应if语句中argc与4的比较操作,通过cmpl对这两个数进行比较,如果相等就跳转。

 

图3-7

此处应该对于for循环中对i与4的比较,如果小于等于就跳转到.L4。

        3.3.5算术操作:

 

图3-8

       此处是对循环控制变量i的加一操作,每次循环过后对应对i++。

       3.3.6转移控制:

 

     

图3-9

此处对应if语句中argc与4的比较操作,通过cmpl对这两个数进行比较,如果相等就跳转到.L2。

图3-10

此处应该对于for循环中对i与4的比较,如果小于等于就跳转到.L4。

3.3.7函数操作:

(1)main函数:

参数传递:main函数的参数是argc和argv字符指针,通过压入栈中进行参数传递。

函数调用:通过使用call内部指令进行函数调用。

局部变量:局部变量i进行循环控制。

(2)printf函数:

参数传递:调用参数argv[1],argv[2]。

函数调用:调用了两次,通过rdx,rdi传入参数。

(3)exit函数:

 

 

图3-11

因为程序中是exit(1),通过将rdi设为1传参调用。

(4)atoi函数:

 

图3-12

通过rdi传承调用。

(5)sleep函数:

 

图3-13

调用方式同atoi函数。

(6)getchar函数:

 

图3-14

无参数通过call调用。

3.3.8类型转换:

atoi函数将字符型argv[3]转换为整型数。

3.4 本章小结

本章主要讲述了编译阶段编译器如何处理各种数据和操作,并通过具体实例hello.s文件解析这个过程,了解各个C语言中的数据和操作在汇编语言中的操作。通过理解这种机制,我们也可以将汇编语言翻译为C语言进行反汇编工作,理解高级语言和汇编语言之间的转化关系。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编过程将把编译得到的汇编程序指令逐条翻译为机器语言指令,然后把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在一个二进制文件中。

作用:汇编将汇编指令转换成一条条机器可以直接读取分析的机器指令,计算机只有通过机器指令才能实现各运算过程。

4.2 在Ubuntu下汇编的命令

gcc-c hello.s -o hello.o

图4-1

通过以上命令产生了hello.o文件。

4.3 可重定位目标elf格式

首先通过readelf -a hello.o >helloo.elf生成helloo.elf。

 

 

图4-2

下面是CSAPP中提供的elf文件内容。

 

图4-3

1.ELF头:

ELF文件的头部包含了文件类型、目标平台、程序头表偏移量、节头表偏移量等信息。具体来说,ELF头部包括以下字段:

Magic Number:ELF文件的魔数,用于标识文件类型和版本。

文件类型(File Type):ELF文件的类型,如可执行文件(ET_EXEC)、共享库文件(ET_DYN)等。

目标平台(Machine):ELF文件所要运行的目标平台,如x86、ARM、MIPS等。

版本(Version):ELF文件的版本号。

入口地址(Entry Point):程序执行的入口地址。

程序头表偏移量(Program Header Offset):程序头表在文件中的偏移量。

节头表偏移量(Section Header Offset):节头表在文件中的偏移量。

标志(Flags):ELF文件的标志信息,如是否需要动态链接等。

头部大小(Header Size):ELF头部的大小。

程序头表项大小(Program Header Entry Size):程序头表项的大小。

程序头表项数量(Program Header Entry Count):程序头表项的数量。

节头表项大小(Section Header Entry Size):节头表项的大小。

节头表项数量(Section Header Entry Count):节头表项的数量。

节名称字符串表偏移量(Section Name String Table Offset):存储节名称的字符串表在文件中的偏移量。

 

2.节头部表

节头表(Section Header Table)是ELF文件中的一个数据结构,用于描述不同节(Section)的信息,例如节的名称、大小、偏移量、属性等

3.重定位节

用于描述程序的重定位信息。在编译和链接过程中,不同模块的代码和数据可能被编译成不同的目标文件,其中每个目标文件都包含了一些重定位节,用于描述该文件中需要被动态链接器修改的符号引用

图4-6

 

4.符号表

用来存放程序中定义和引用的函数或全局变量的信息。每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号的信息。符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。

 

4.4 Hello.o的结果解析

       通过objdump -d -r hello.o指令生成了hello.o的反汇编:

 

       观察到其与hello.s基本相同,有几个地方不同:在分支转移的时候没用.L3,而是具体的地址,访问字符串也是这样。在调用函数时.s用call加函数名,这里时call加地址。

4.5 本章小结

本章介绍了hello 从hello.s 到hello.o 的汇编过程,分析了可重定位文件的结构和各个组成部分,以及它们的内容和功能;还查看了hello.o 的elf 格式,并使用objdump 得到反汇编代码与hello.s 进行比较。在这个过程中,生成了重定位条目,为之后的链接中的重定位过程奠定了基础。

5章 链接

5.1 链接的概念与作用

链接的概念:链接(Linking)是将多个目标文件(Object File)组合成一个可执行文件(Executable File)或共享库文件(Shared Library)的过程。链接器(Linker)是执行链接操作的程序。

链接的作用:解决符号引用:在程序中,函数和变量通常是在不同的源文件中定义和引用的。链接器的一个主要作用是解决这些符号引用。它会查找每个符号的定义,并将符号引用修改为符号定义的地址或位置,以便程序正确执行。

减少重复代码和数据:在多个源文件中可能会包含相同的代码和数据。链接器可以将这些相同的代码和数据合并,减少最终可执行文件的大小,提高可执行文件的运行效率。

生成可执行文件或共享库文件:链接器将多个目标文件组合成一个可执行文件或共享库文件,使得软件系统具备可执行性和可扩展性。可执行文件可以直接在操作系统中运行,共享库文件可以被多个程序共享使用。

加载共享库:在程序运行时,动态链接器会加载共享库文件,并将共享库文件中的符号引用解析为实际的函数地址。这样,程序可以调用共享库中的函数,实现代码的复用和共享。

5.2 在Ubuntu下链接的命令

命令:ld -o hello -dynamic-linkker /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.3 可执行目标文件hello的格式

生成hello的elf的命令格式:readelf -a hello >hello.elf

  1. ELF头:

区别:相较于 hello.o 文件,hello 文件的 ELF 头区别在以下几点:

文件类型不同:hello 文件的文件类型是 EXEC,而 hello.o 文件的文件类型是 REL。

程序的入口点不同:在 hello 文件中,程序的入口点是 main 函数所在的地址,而不是在地址 0x0 处。

节头数量发生变化:由于 hello 文件需要包含可执行代码和数据,所以在其 ELF 头中会有更多的节头信息,而 hello.o 文件则只需要包含与目标文件相关的节头信息。

换句话说,hello 文件是可执行文件,可以直接运行,而 hello.o 文件只是一个目标文件,需要进一步链接才能生成可执行文件。

 

  1. 节头:

节头部表(Section Header Table)记录了可执行文件或目标文件中所有节的信息,包括节的名称、大小、偏移量、内存对齐、类型等。通过读取节头部表中的信息,我们可以获得每个节所占据的空间范围,进而可以使用 HexEdit 工具定位这些节所在的位置。

 

  1. 重定位:

             

  1. 符号表:

 

5.4 hello的虚拟地址空间

       通过edb加载hello程序,打开Data Dump查看hello加载到虚拟地址的状况,并查看各段信息。

 

5.5 链接的重定位过程分析

有.init,.plt,.text三个节,而且每个节中有许多的函数。库函数的代码都已经链接到了程序中,程序各个节变的更加完整,跳转的地址也具有参考性。

hello比hello.o多出的节头表。

.interp:保存ld.so的路径

.note.ABI-tag

.note.gnu.build-i:编译信息表

.gnu.hash:gnu的扩展符号hash表

.dynsym:动态符号表

.dynstr:动态符号表中的符号名称

.gnu.version:符号版本

.gnu.version_r:符号引用版本

.rela.dyn:动态重定位表

.rela.plt:.plt节的重定位条目

.init:程序初始化

.plt:动态链接表

.fini:程序终止时需要的执行的指令

.eh_frame:程序执行错误时的指令

.dynamic:存放被ld.so使用的动态链接信息

.got:存放程序中变量全局偏移量

.got.plt:存放程序中函数的全局偏移量

.data:初始化过的全局变量或者声明过的函数

5.6 hello的执行流程

(1) 载入:_dl_start、_dl_init

(2)开始执行:_start、_libc_start_main

(3)执行main:_main、_printf、_exit、_sleep、

_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x

(4)退出:exit

程序名称 地址

ld-2.27.so!_dl_start 0x7fb85a93aea0

ld-2.27.so!_dl_init 0x7f9612138630

hello!_start 0x400582

lib-2.27.so!__libc_start_main 0x7f9611d58ab0

hello!puts@plt 0x4004f0

hello!exit@plt 0x400530

5.7 Hello的动态链接分析

在进行动态链接之前,需要先进行静态链接,以生成部分链接的可执行目标文件hello。这时,共享库中的代码和数据并没有被合并到hello文件中。在加载hello文件时,动态链接器会对共享目标文件中的相应模块内的代码和数据进行重定位,并加载共享库以生成完全链接的可执行目标文件。

 

动态链接使用了延迟加载的方法,即在调用函数时才进行符号的映射。为了实现函数的动态链接,采用了偏移量表GOT和过程链接表PLT。在GOT中存储了函数的目标地址,并为每个全局函数创建一个副本函数。然后将对函数的调用转换成对副本函数的调用。

 

5.8 本章小结

链接是将一个或多个目标文件合并成一个可执行文件或动态链接库的过程。在链接过程中,目标文件中的符号(Symbol)会被解析并转换为对应的地址或者重定位信息,同时,对于不同的符号定义可能存在冲突或者未定义情况,这些问题需要被解决以确保最终生成的可执行文件或动态链接库是正确的。

Ubuntu下的 gcc 编译器提供了预处理、编译、汇编和链接等功能。在预处理、编译和汇编阶段,编译器会将源代码转换为可执行目标文件,但是这些目标文件还不能被直接执行,需要进行链接操作,将不同目标文件之间的符号引用和定义进行匹配和解析,最终生成可执行文件或动态链接库。在本例中,我们通过将 hello.o 进行链接操作,生成了可执行文件 hello。

在进行链接操作的过程中,首先需要对目标文件进行重定位,将目标文件中的符号解析为实际地址,并将符号引用和定义之间的关联关系建立起来。对于动态链接库的情况,链接过程将延迟到运行时进行,需要使用动态链接器(如 ld.so)动态加载库,并进行符号解析和重定位操作。

在重定位的过程中,我们可以观察到目标文件和可执行文件中的一些变化。例如,符号地址和节偏移量等信息发生了变化,动态链接库中的代码和数据可能会被共享等。这些变化会影响最终可执行文件或动态链接库的行为和性能,因此需要仔细进行调试和测试,以确保链接结果正确。

6章 hello进程管理

6.1 进程的概念与作用

进程是计算机中的一种基本概念,它指正在运行的程序在操作系统中的实例。在操作系统中,每个进程都有独立的内存空间、状态信息和执行上下文,可以被操作系统调度执行。

进程在计算机系统中具有重要的作用。首先,进程是计算机中程序执行的基本单位。每个进程都有独立的内存空间,可以保证程序之间的隔离性,同时也可以最大限度地利用计算机的多核处理器,实现多任务并行执行。

其次,进程也是实现多用户操作的重要手段。在多用户操作系统中,每个用户都可以通过创建自己的进程来进行独立的计算和操作,从而实现多用户共享计算资源的目的。在操作系统中,进程之间也可以通过进程间通信(IPC)的方式来实现数据和资源共享。

此外,进程还是操作系统中实现进程调度、资源分配和管理的基本单元。操作系统通过对进程的调度和管理,实现了计算机资源的有效利用和管理,提高了计算机系统的运行效率和可靠性。

总之,进程是计算机中的基本概念,是实现多任务、多用户、资源管理和调度等功能的重要手段,是计算机操作系统中的核心组成部分之一。

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

Shell是计算机操作系统中的一个用户界面,它提供了一种通过命令行交互的方式来操作计算机系统的方法。而bash则是一种常见的Shell程序,它是Bourne-Again SHell的缩写,是Linux和其他类Unix系统中广泛使用的一种Shell程序。

bash的作用是提供一个交互式的命令行界面,允许用户通过输入命令来操作计算机系统。它可以执行各种命令和程序,管理文件和目录,编辑文本文件等等。

在Linux系统中,bash的处理流程通常如下:

用户输入命令:用户在命令行界面输入要执行的命令,例如ls或cd等。

Shell解析命令:bash会解析用户输入的命令,并将其转化为操作系统可以理解的形式。

程序执行:Shell会调用对应的程序或脚本来执行用户输入的命令,例如运行ls程序来列出当前目录中的文件列表。

输出结果:程序执行完毕后,bash将执行结果输出到命令行界面上,供用户查看。

循环执行:如果用户输入了多个命令,bash会按照顺序循环执行它们,直到所有命令执行完毕或用户终止程序。

需要注意的是,bash除了可以解析和执行命令外,还具备了一定的编程能力。用户可以使用bash来编写Shell脚本,通过脚本的方式批量执行命令、处理文件、管理系统等操作。因此,bash不仅是Linux系统中必不可少的一个组件,也是Linux用户和系统管理员日常工作中的重要工具之一。

6.3 Hello的fork进程创建过程

       当运行 Hello 程序时,操作系统会为该程序创建一个进程。在该进程中,程序会执行 main 函数中的代码。当程序执行到 fork() 函数时,会创建一个子进程,然后该子进程会拷贝父进程的地址空间,包括代码、数据、堆栈等,但是子进程会拥有自己独立的地址空间。

在 Hello 程序中,调用 fork() 函数会创建一个子进程,子进程会从 fork() 函数返回并返回值为 0。而父进程会从 fork() 函数返回并返回值为子进程的进程 ID。在子进程中,将执行与父进程相同的代码,也就是从 fork() 函数后面的代码开始执行。在父进程中,可以通过子进程的进程 ID 来识别子进程,并执行父进程的代码。

因此,在 Hello 程序中,当 fork() 函数被调用时,会创建一个子进程并返回子进程的进程 ID。子进程会从 fork() 函数返回并开始执行循环体内的代码,而父进程会继续执行循环体外的代码,直到程序结束。由于子进程是从 fork() 函数后面的代码开始执行的,因此它会在循环体的第一次迭代中输出一次“Hello 学号 姓名”并暂停指定秒数,总共输出5次。而父进程会在循环体外的代码中等待用户输入任意字符,直到用户输入后才结束程序。

6.4 Hello的execve过程

在 Hello 程序中,execve() 函数被用来执行另一个程序。在 execve() 函数被调用时,它会替换当前进程的映像,并开始执行指定的程序。

execve() 函数需要传递三个参数:第一个参数是要执行的程序的路径,第二个参数是一个字符串数组,用来传递给程序的命令行参数,第三个参数是一个字符串数组,用来设置环境变量。在 Hello 程序中,调用 execve() 函数时,第一个参数是要执行的程序的路径,第二个参数是一个字符串数组,用来传递给程序的命令行参数,第三个参数是一个空的环境变量数组。

在 execve() 函数被调用后,操作系统会将当前进程的映像替换为指定的程序的映像,并开始执行该程序的代码。因此,当 execve() 函数被调用时,Hello 程序的代码就不会再执行了。

6.5 Hello的进程执行

       Hello 程序的进程执行过程中,涉及到了进程上下文信息、进程时间片、进程调度、用户态与核心态转换等概念。

进程上下文信息:进程的上下文信息包括了进程的程序计数器、寄存器、内存管理信息、文件描述符等状态信息。当操作系统从一个进程切换到另一个进程时,它需要保存当前进程的上下文信息,并将下一个进程的上下文信息加载到系统中,这个过程称为进程切换。

进程时间片:操作系统对进程进行调度时,为每个进程分配一定的时间片。当进程的时间片用完之后,操作系统会停止当前进程的执行,并将 CPU 时间分配给下一个等待执行的进程。时间片的大小可以根据系统的负载情况和优先级进行调整,以确保系统的效率和稳定性。

进程调度:进程调度是操作系统中一个重要的组成部分。它的主要作用是将 CPU 时间分配给等待运行的进程,并根据进程的优先级和调度算法来确定哪个进程应该获得 CPU 时间片。常用的调度算法包括轮询调度、优先级调度、时间片轮转调度等。

用户态与核心态转换:在 Linux 操作系统中,进程可以运行在用户态或核心态。在用户态中,进程只能访问自己的用户空间,不能访问操作系统的核心空间。而在核心态中,进程可以访问所有的系统资源,包括硬件设备、内核数据结构等。当进程需要访问核心态资源时,需要通过系统调用将进程从用户态转换到核心态,以便访问这些资源。在 Hello 程序中,当调用 execve() 系统调用时,操作系统会将进程从用户态转换到核心态,并将程序加载到内存中运行。

综合上述概念,Hello 程序的进程执行过程中,操作系统会为程序创建一个新的进程,并将其上下文信息加载到系统中。随后,操作系统会将进程切换到 CPU 上执行,通过进程调度算法分配 CPU 时间片,控制进程的执行。当程序需要访问核心态资源时,操作系统会将进程从用户态转换到核心态,以便访问这些资源。当程序运行结束后,操作系统会将进程的上下文信息保存到系统中,然后将 CPU 时间分配给下一个等待执行的进程,完成进程的切换。

6.6 hello的异常与信号处理

执行过程可能出现的异常一共有四种:中断、陷阱、故障、终止。

中断:来自I/O设备的信号,异步发生,总是返回到下一条指令。

陷阱:有意的异常,同步发生,总是返回到下一条指令。

故障:潜在可恢复的错误,同步发生,可能返回到当前指令或终止。

终止:不可恢复的错误,同步发生,不会返回。

正常执行:

输入ctrl z:

进程被挂起,并没有被回收,

执行ps:

输入ctrl c:

进程被停止。

乱输入:

执行jobs:

执行pstree:

执行fg:

6.7本章小结

创建进程:通过调用fork函数创建子进程,子进程会复制父进程的数据段、代码段、堆栈等信息,从而得到一个新的进程控制块(PCB)。子进程的PCB会被内核标记为就绪态,等待被调度执行。

进程调度:操作系统根据进程调度算法,在就绪态进程中选择一个进程分配CPU时间片,进行运行。在Hello程序中,进程调度的过程由操作系统内核自动完成。

用户态与核心态转换:当进程需要执行一些特权操作时,需要切换到核心态。比如在Hello程序中,当进程需要调用系统调用函数时(如sleep、printf等),就需要由用户态切换到核心态。

异常与信号处理:在Hello程序执行过程中,如果出现异常或接收到信号,操作系统会根据事先定义的处理方式进行处理。比如,当接收到SIGINT信号时,会触发进程终止的操作。而当用户输入Ctrl-Z时,会将进程挂起,并将进程移动到后台等待恢复。

进程终止:当进程执行完毕或出现异常时,操作系统会释放进程占用的资源,并将进程控制块(PCB)从进程表中删除,结束进程的生命周期。

综上所述,进程管理是操作系统中的一个重要组成部分,通过对进程的创建、调度、资源分配、异常处理等方面的管理,实现了对计算机资源的有效利用和保护。在Hello程序中,我们可以通过对进程管理的分析,更好地理解操作系统中进程管理的具体实现和作用。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址是指程序在执行时所使用的地址,是相对于程序自身的地址,通常是指令中的操作数或地址。逻辑地址是与具体硬件平台无关的,不同的进程可以有相同的逻辑地址。

线性地址是指逻辑地址通过分段、分页等机制转换为的地址,也就是在内存中实际存放的地址。操作系统在管理内存时,需要把程序的逻辑地址转换为线性地址,以便在物理内存中定位所需的数据。

虚拟地址是指在进程中使用的地址,它不是实际存在的物理地址,而是通过操作系统和硬件的组合转换机制转换为物理地址。在操作系统中,每个进程都有自己独立的虚拟地址空间,进程间的虚拟地址是互不干扰的。

物理地址是指实际存在的物理内存地址,是操作系统将虚拟地址转换为物理地址后,真正的内存地址。

在hello程序的执行过程中,当操作系统加载程序时,将程序的二进制文件映射到进程的虚拟地址空间中。在程序执行时,程序中的逻辑地址需要通过操作系统的内存管理机制转换为虚拟地址、线性地址和最终的物理地址,才能访问内存中的数据。例如,程序中的变量和函数都需要通过地址访问内存中的数据,这些地址在程序中是逻辑地址,但是需要通过操作系统的内存管理机制转换为物理地址,才能真正地访问内存中的数据。

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

Intel x86架构的内存管理采用了段式管理和分页式管理相结合的方法。在段式管理中,程序的逻辑地址会经过以下的变换过程,最终得到线性地址:

逻辑地址由段选择符(Segment Selector)和段内偏移量(Offset)组成。

段选择符指向描述符表(Descriptor Table),描述符表中存放了每个段的基地址、大小、访问权限等信息。

根据段选择符在描述符表中找到对应的段描述符(Segment Descriptor)。

从段描述符中获取该段的基地址和大小等信息,将段内偏移量与基地址相加,得到线性地址。

线性地址是虚拟地址的一种,它是由操作系统管理的地址空间中的地址,而不是直接映射到物理地址。线性地址是在分页式管理中进一步映射到物理地址的中间层。

物理地址是实际存在于计算机物理内存中的地址。在操作系统中,物理地址通常是通过内存管理单元(MMU)进行映射,将线性地址映射到物理地址。这种映射可以通过页表实现。由于物理地址是由硬件生成的,因此用户程序无法直接访问物理地址。

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

在页式管理下,物理内存被划分为大小相等的页帧,而逻辑地址空间被划分为大小相等的页。线性地址由32位组成,其中高20位为页目录项的索引,中间10位为页表项的索引,低12位为页内偏移量。

在Linux系统中,进程的每个线性地址空间都有一个对应的页目录表和多个页表。进程通过使用页目录表和页表来将线性地址翻译为物理地址。

在Hello程序运行时,操作系统将Hello进程的代码和数据加载到进程的地址空间中,同时为该进程创建页目录表和页表。当进程访问某个地址时,处理器会先将线性地址分解成页目录项和页表项的索引,然后访问相应的页目录表和页表,将其翻译成对应的物理地址。这个过程在硬件上完成,处理器内部有相应的地址转换缓冲区来加速地址翻译。

页式管理使得进程的地址空间可以离散地分配在物理内存中,从而避免了内存碎片的问题。此外,页式管理也使得内存管理变得更加灵活,例如可以为不同的进程分配不同的页表,从而隔离它们的地址空间,保护它们的数据不被其他进程访问。

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

Core i7采用四级页表的层次结构。CPU产生VA,VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI向TLB中寻找匹配。如果命中,则得到PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成PA,添加到PLT。

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

三级Cache通常包括L1 Cache、L2 Cache和L3 Cache。L1 Cache位于处理器核心内部,速度最快,容量最小,一般用于存储指令和数据,每个核心都有一个L1 Cache;L2 Cache位于处理器核心与主存之间,速度较快,容量比L1 Cache大,多个核心共享一个L2 Cache;L3 Cache位于处理器与内存控制器之间,速度较慢,容量最大,多个核心共享一个L3 Cache。

在物理内存访问时,当CPU需要访问某个地址时,会先检查L1 Cache是否存在该数据。如果L1 Cache中存在该数据,则直接返回给CPU,不需要访问内存;否则,L1 Cache会将数据请求发送到L2 Cache,如果L2 Cache中存在该数据,则返回给L1 Cache,L1 Cache再返回给CPU,不需要访问内存;如果L2 Cache中也不存在该数据,则L2 Cache会将数据请求发送到L3 Cache,同样地,如果L3 Cache中存在该数据,则返回给L2 Cache,L2 Cache再返回给L1 Cache,L1 Cache最终返回给CPU,不需要访问内存;如果L3 Cache中也不存在该数据,则L3 Cache会将数据请求发送到主存,主存将数据返回给L3 Cache,L3 Cache再返回给L2 Cache,L2 Cache再返回给L1 Cache,L1 Cache最终返回给CPU,完成内存访问。

TLB(Translation Lookaside Buffer)是一种高速缓存,用于存储虚拟地址到物理地址的转换。在使用页式管理时,每个进程有自己的页表,用于将虚拟地址映射为物理地址。每次访问内存时,CPU会将虚拟地址发送到TLB,TLB会先检查是否存在该虚拟地址对应的物理地址,如果存在,则直接返回物理地址;否则,TLB会将虚拟地址发送到主存,主存将对应的物理地址返回给TLB,同时,TLB会将该虚拟地址和物理地址的映射关系存储到TLB中,以便下次访问时可以直接返回物理地址,提高访问速度。

7.6 hello进程fork时的内存映射

在Hello进程调用fork()创建子进程时,子进程将会拥有一个新的、独立的进程地址空间,其中包括与父进程相同的代码和数据,但是各自拥有独立的栈空间和堆空间。

具体地,fork()系统调用在子进程中返回0,在父进程中返回子进程的进程ID。当子进程被创建时,其进程地址空间会被操作系统中的MMU(内存管理单元)重新映射,以反映新的进程地址空间,即:

子进程复制父进程的页表,并将父进程页表项中的写标志位设置为只读。这是为了确保子进程不能修改父进程的地址空间。

子进程对于那些已经被父进程修改的页,将会使对这些页的写操作产生页错误(page fault),并分配一个新的物理页来存储该页的副本。这被称为写时复制(copy-on-write)。

子进程根据需要分配新的内存,比如堆空间,使用系统调用brk()或mmap()来分配一块新的内存。

子进程根据需要加载新的可执行代码,使用系统调用execve()来执行一个新的可执行文件。

值得注意的是,由于写时复制技术的使用,父进程和子进程之间的内存共享只有在它们之间没有任何写操作时才是有效的。因此,当一个进程需要在子进程中修改内存时,必须使用进程间通信(IPC)机制来传递数据。

7.7 hello进程execve时的内存映射

当 hello 进程调用 execve 函数时,它会首先清空自己的虚拟地址空间,然后载入可执行文件中的代码和数据段。这个过程会重新映射新的虚拟地址到物理内存中。

具体来说,进程在执行 execve 函数后,内核会执行以下步骤:

将 ELF 文件的程序头表读入内存,并解析出可执行文件各个段的信息。

分配新的页表,并建立新的虚拟地址空间。

从可执行文件中读取代码段、数据段等信息,并将它们映射到新的虚拟地址空间中。

将程序的入口地址设置为代码段的起始地址。

具体来说,hello 进程执行 execve 函数后,内核会在其虚拟地址空间中建立代码段、数据段、堆和栈,它们的大小和位置如下:

代码段:包含可执行代码,大小为 4KB,起始地址为 0x400000。

数据段:包含全局变量和静态变量,大小为 4KB,起始地址为 0x601000。

堆:动态分配的内存区域,大小不固定,起始地址为 0x8d9010。

栈:保存局部变量和函数调用信息,大小为 8KB,起始地址为 0x7fffffffdef0。

在这个过程中,内核会根据物理内存的使用情况来选择合适的物理页面来存储进程的各个段。它还会根据 CPU 的 TLB 缓存来加速虚拟地址到物理地址的转换过程。

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

在计算机系统中,程序访问某些虚拟内存时,如果对应的物理内存没有被加载到主存中,就会发生缺页故障(page fault)。此时CPU会产生一个中断信号,称为缺页中断(page fault interrupt),操作系统会捕获该中断信号并进行处理,将缺失的页加载到物理内存中,然后重新执行产生缺页中断的指令。

缺页故障的处理过程一般如下:

CPU在访问某个虚拟地址时发现对应的页表项(或页目录项)无效或没有加载到TLB中,此时就会触发缺页异常,也称为缺页中断。

操作系统的中断处理例程捕获到缺页异常信号,保存当前进程的上下文,根据缺失的页号查找磁盘上的对应页,将该页调入内存。

将该页放入空闲物理页框中,更新页表项(或页目录项)的状态,指向该物理页框,然后更新TLB中对应的页表项。

恢复进程上下文,将控制权返回到引起缺页异常的指令处,重新执行该指令。

缺页故障的处理会增加一定的开销,因为需要从磁盘中读取数据到内存中,而磁盘的访问速度远慢于内存。为了减少缺页故障的次数,可以使用一些优化策略,如预先加载(预取)页面、使用局部性原理(尽量访问靠近上一次访问的地址)等。同时,CPU和操作系统中都会有缓存来加速内存访问,如CPU中的缓存和操作系统中的页缓存等。

7.9动态存储分配管理

       动态存储分配管理是指在程序运行过程中,根据程序的实际需要进行内存的分配与释放的管理方式。常见的动态存储分配方式有两种:堆和栈。

堆是由程序员手动申请和释放的内存空间,由malloc函数进行申请,由free函数进行释放。堆的分配方式是基于内存池的,当申请一块内存时,系统会在内存池中找到一块足够大的空闲内存,并返回给程序员。当释放一块内存时,这块内存会被放回内存池中以供下次使用。

栈则是由系统自动分配和回收的内存空间,用于存储函数的参数、局部变量等。栈的分配方式是按照“先进后出”的原则进行的,当一个函数被调用时,系统会为该函数在栈上分配一段内存,该函数的参数和局部变量将被存储在这段内存中。当函数执行完毕时,系统会自动将这段内存释放。

在动态存储分配管理中,常常会出现内存泄漏和内存溢出的问题。内存泄漏指的是在程序运行过程中,由于程序员未能正确地释放内存,导致内存得不到回收,最终导致系统内存耗尽。内存溢出则是指程序申请的内存超出了系统所能提供的内存大小。

为了避免这些问题,需要程序员在进行动态存储分配时,仔细管理内存的申请和释放。常用的方法包括使用RAII技术和智能指针等。RAII技术是指在对象的构造函数中申请内存,在析构函数中释放内存,从而保证内存的正确释放。智能指针则是一种封装了指针的类,能够自动管理指针的内存申请和释

7.10本章小结

本章以hello为例子,简述了在计算机中的虚拟内存管理,虚拟地址、物理地址、线性地址、逻辑地址的区别以及它们之间的变换模式,以及段式、页式的管理模式。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux的IO设备管理方法主要涉及三个方面:设备文件、设备驱动程序和设备控制器。

设备文件:在Linux系统中,每个设备都会被视为一个文件,对设备的操作都通过打开、读、写、关闭文件来完成。设备文件的命名规则一般是/dev/xxx,其中xxx表示设备的名称。

设备驱动程序:设备驱动程序是连接应用程序和设备控制器的重要桥梁,它提供了访问设备硬件的接口,可以将设备的操作(如读、写、控制等)转换为硬件的控制信号。

设备控制器:设备控制器是真正执行操作的硬件部分,它可以控制设备的输入、输出、状态等操作。

在Linux中,IO设备管理主要是通过驱动程序来实现的。驱动程序可以分为两种:字符设备驱动和块设备驱动。其中,字符设备驱动主要管理像串口、终端、鼠标等字符设备,而块设备驱动主要管理像硬盘、U盘、闪存等块设备。

Linux中的IO设备管理方法还包括了缓冲机制、中断机制、DMA(直接内存访问)等技术,以提高IO设备的读写性能和系统响应速度。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix提供了一套IO接口,包括文件IO和网络IO。其中文件IO的函数主要在<stdio.h>和<fcntl.h>头文件中定义,网络IO的函数主要在<sys/socket.h>和<netinet/in.h>头文件中定义。

常见的文件IO函数有:

fopen():打开文件

fclose():关闭文件

fread():从文件中读取数据

fwrite():向文件中写入数据

fseek():移动文件指针

ftell():获取当前文件指针位置

rewind():将文件指针移到文件开头

feof():判断是否已到达文件结尾

ferror():判断是否发生错误

常见的网络IO函数有:

socket():创建套接字

bind():绑定套接字到指定的地址和端口号

listen():将套接字设置为监听状态,等待客户端连接

accept():接受客户端的连接请求,创建新的套接字

connect():向服务器发起连接请求

read():从套接字中读取数据

write():向套接字中写入数据

close():关闭套接字

这些函数基本涵盖了Unix IO接口的常用操作,我们可以根据具体需求选择合适的函数进行文件IO或网络IO操作。

8.3 printf的实现分析

printf是C语言标准库中的一个函数,用于将格式化的数据输出到标准输出流(stdout)中。下面是printf的实现分析过程:

格式化字符串处理

printf函数首先会将传入的格式化字符串进行处理,根据格式化字符串中的占位符,将变量转换成指定格式并保存在缓存中。

vsprintf函数处理

接下来,printf函数将处理好的字符串传递给vsprintf函数,vsprintf函数将格式化字符串中的占位符替换成对应的值,并将生成的字符串写入到一个字符数组中。

write系统调用

printf函数将处理好的字符串传递给write系统调用,write函数会将字符串写入到标准输出流(stdout)中。

系统调用陷阱

在Linux系统中,用户程序无法直接访问内核空间,需要通过系统调用来请求内核执行一些操作。printf函数通过int 0x80或syscall等指令发起一个陷阱,将控制权转移到内核中的中断服务例程中。

内核态处理

进入内核态后,内核将从系统调用号中确定调用的函数,并根据系统调用参数,执行相应的操作。

文件描述符处理

在内核中,标准输出流(stdout)是通过文件描述符表示的,printf函数调用write系统调用时,需要传递一个文件描述符参数。在内核中,文件描述符表示为一个整数,内核根据该整数找到对应的文件句柄,并将数据写入到该句柄对应的文件中。

中断返回

当系统调用完成后,控制权返回到用户态,printf函数执行完毕,程序继续向下执行。

8.4 getchar的实现分析

getchar 的源代码为:

int getchar(void)

{

static char buf[BUFSIZ];

static char *bb = buf;

static int n = 0;

if(n == 0)

{

n = read(0, buf, BUFSIZ);

bb = buf;

}

    return(--n >= 0)?(unsigned char) *bb++ : EOF;

}

       当程序调用getchar函数时,程序会先判断标准输入流stdin是否还有缓冲区中的字符,如果有,则直接返回该字符;如果没有,则会调用read函数从标准输入流读入一定数量的字符到缓冲区中。

如果缓冲区中有字符,则直接返回第一个字符,并将缓冲区指针后移一位,否则将会调用read函数从标准输入流读入一定数量的字符到缓冲区中。

在调用read函数时,内核会将标准输入流中的字符读入到内核空间中的缓冲区,然后再通过内核空间和用户空间之间的数据传输,将字符拷贝到用户空间的缓冲区中。

当缓冲区中的字符全部被读取后,read函数将会再次从标准输入流中读取一定数量的字符到缓冲区中,并重复第2步的操作。

如果在读取过程中遇到了文件结束标志(EOF),则会将EOF返回给程序,同时也会将EOF置为缓冲区的第一个字符,这样下一次调用getchar函数时就可以直接返回EOF,而无需再次从标准输入流中读取字符。

如果在读取过程中遇到了错误(比如文件被删除或者权限不足等),则会将错误码返回给程序。

程序可以通过在标准输入流中插入EOF来手动结束输入。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

第八章讲述了Linux中IO设备的管理方法,IO接口及其函数,最后分析了printf和getchar函数的实现方法

结论

用计算机系统的语言,逐条总结hello所经历的过程。

1.编写源代码

Hello程序的源代码是经过编写的,包括使用的头文件、定义的变量、执行的代码等。

2.预处理

在编译之前,对源代码进行预处理,主要包括处理头文件、宏替换等操作。预处理生成的结果被编译器使用。

3.编译

编译器将预处理生成的结果转换成汇编代码,然后将汇编代码转换成可执行文件的目标代码。

4.链接

将目标代码和所需的库文件链接起来,生成可执行文件。在链接的过程中,动态链接库被加载到内存中,函数调用通过动态链接库进行。

5.加载

将可执行文件加载到内存中运行,此时操作系统将创建一个新的进程,将可执行文件的代码、数据、堆栈等加载到进程的虚拟地址空间中。

6.进程执行

进程开始执行,执行过程中,操作系统会对进程进行调度,分配时间片,处理IO请求等操作。进程通过系统调用访问操作系统提供的服务,例如打印输出、读取输入、创建进程等。

7.内存管理

在进程执行过程中,操作系统通过分页机制将进程虚拟地址映射到物理地址。进程需要分配内存时,操作系统会在进程的虚拟地址空间中分配一块可用的内存空间,并将其映射到物理地址空间中。

8.输入输出

在进程执行过程中,操作系统负责管理输入输出设备。当进程需要读取用户输入时,操作系统会从标准输入设备中读取数据;当进程需要输出数据时,操作系统会将数据写入标准输出设备中。

9.进程终止

当进程执行完成或出现异常情况时,操作系统会将进程终止。在终止进程时,操作系统会回收进程所占用的内存空间、关闭进程所打开的文件等操作。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

作为一台计算机系统,其设计与实现的过程中需要考虑到各种不同的因素和需求,包括性能、功耗、安全性、可靠性、可维护性等等。在设计与实现的过程中,应该注重实用性和可持续性,而不是盲目地追求性能和规模。

附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.c:程序源代码文件。

hello.i:预处理器对hello.c进行宏替换和头文件展开后生成的中间文件。

hello.s:编译器对hello.i进行编译后生成的汇编代码文件。

hello.o:汇编器将hello.s汇编成目标文件hello.o,其中包含了机器码和可重定位信息。

hello:可执行文件,由链接器将目标文件和系统库链接生成,包含了程序的机器码和可执行信息。

helloo.elf:hello.o的ELF格式。

hello.elf:hello的ELF格式,分析重定位过程。

参考文献

[1]    Randal E.Bryant David R.O Hallaron. 深入理解计算机系统(第三版),机械工业出版社,2016

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值