HIT-CSAPP大作业——程序人生-Hello‘s P2P

计算机系统

大作业

题       目   程序人生-Hellos P2P 

专       业         信息安全              

学  号        2022******             

班       级          22*****                 

学       生             **然                   

指 导 教 师           史先俊             

计算机科学与技术学院

20245

摘  要

HelloWorld是几乎每个程序员的第一个项目,我们慢慢输入几行代码,运行后惊喜地看到输出中的“Hello,World!”然而,从代码编辑器中输入代码到运行程序的这个简单过程,实际上反映了计算机科学的核心概念。我们需要理解语法规则、编译原理和操作系统的运行机制。当我们点击运行按钮时,代码被编译成机器可执行的指令,然后由操作系统加载到内存中,并在CPU上执行。在这个过程中,涉及到诸多底层细节,如内存管理、进程调度和指令执行等。

本文通过跟踪hello的一生,介绍了hello从代码编辑器到运行最后结束的过程,对计算机底层进行了较深的分析。

关键词:计算机系统,Linux,C语言                           

目  录

第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.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 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

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本章小结

第8章 hello的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:在文本编辑器中将hello的代码输入,并保存为.c格式的文件,形成hello.c文件。这就是程序(Program),这是hello程序的生命周期的开始。在OS(例如Linux)中,通过交互式应用程序Shell,输入命令实现对hello.c从源文件到目标文件的转化。源程序通过cpp(预处理器)预处理,ccl(编译器)编译,as(汇编器)汇编,最后通过ld(链接器)链接生成hello可执行目标程序并将其保存到磁盘中。

020:在Shell运行该hello程序时,Shell调用fork函数创建子进程,并通过execve函数将hello程序载入并创建运行环境,比如分配虚拟内存,运行完成后,Shell回收该进程,释放内存空间。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:Intel(R) Core(TM) i9-14900HX   2.20 GHz

软件环境:Windows 11    VMware   Ubuntu-22.04.4 

开发调试工具:GDB,EDB,Visual Studio Code,Vim,gcc

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

  1. hello.c:源代码文件。
  2. hello.i:预处理后的文本文件。
  3. hello.s:编译后的汇编文件。
  4. hello.o:汇编后的可重定位文件。
  5. hello:链接后的可执行文件。
  6. disa_hello.s:反汇编hello.o的汇编代码。
  7. disa_hello_2.s:反汇编hello的汇编代码。

1.4 本章小结

本章对hello程序运行的P2P和020过程进行了简单的介绍,列出了此次大作业所使用的相关工具和软硬件环境,最后介绍了文中所用到的文件和作用。


第2章 预处理

2.1 预处理的概念与作用

概念:在预处理时,预处理器需要根据以字符开头的命令,修改原始的C程序。其实,就是在对源程序做文本替换的操作。比如hello.c第一行的“include<stdio.h>” 预处理时预处理器会读取stdio.h并把它插入到Hello程序中。

作用:在代码编写的过程是为了方便程序员而设计这些开头的命令,而为了后续编译器的方便,需要对代码进行替换。

2.2在Ubuntu下预处理的命令

预处理命令:gcc -E hello.c -o hello.i

图2.1 预处理命令

图2.2 文件列表

2.3 Hello的预处理结果解析

打开hello.i查看文件。

开头时引入外部库.h文件。

图2.3 hello.i文本文件 1

接下来是typedef进行数据类型名称替换。

图2.4 hello.i文本文件 2

引入外部函数:

图2.5 hello.i文本文件 3

最后,是我们编写的main函数代码:

图2.6 hello.i文本文件 4

2.4 本章小结

本章介绍了hello.c的预处理过程,大致分析了预处理后形成的hello.i文件。可以知道,仅23行的.c文件预处理后的文件竟有3000行。如果编写一个hello程序需要3000行,这样的效率是极其低下的。这也就是预处理的意义:能让我们轻松写出可读性高,方便修改,利于调试的代码。


第3章 编译

3.1 编译的概念与作用

概念:编译是将预处理后的文本文件.i翻译为汇编语言的文本文件.s。

编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析,语法分析,语义检查和中间代码生成,代码优化,目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。

作用:将高级程序语言翻译为统一的,接近机器语言,对机器友好的汇编语言。

3.2 在Ubuntu下编译的命令

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

图3.1 编译命令

图3.2 文件列表

3.3 Hello的编译结果解析

编译生成了hello.s文件,为汇编代码,该小节将针对hello程序具体介绍编译器怎么处理C语言中各个数据类型以及各类操作的。

1.常量:

字符串常量:

"用法: Hello 学号 姓名 秒数!\n"

"Hello %s %s\n"。

汇编文件在开头为.LC0和.LC1中存放这两个字符串,其中一个汉字对应一个\xxx。这些常量存放在.rodata节,意为只读数据

图3.3 hello.s文件 1

立即数常量:

如for循环中10这个常量:

图3.4 hello.s文件 2

2.变量

局部变量:“int i”

局部变量存放在寄存器或栈中。

查看汇编代码:

图3.5 hello.s文件 3

可以知道,i存放在栈中。

3.算术操作

“i++”:

图3.6 hello.s文件 4

如上图,addl指令对i进行+1。

4.赋值:

“i = 0”:

图3.7 hello.s文件 5

通过movl指令赋初值。

5.比较语句:

比较通过cmp等指令来实现,根据两个操作数的差来设置条件码。

6.数组/指针操作

“sleep(atoi(argv[3]))”:

图3.8 hello.s文件 6

可以知道%rax先保存了argv[3]的地址。

7.函数调用及返回:

如上图中调用的atoi函数,%rdi为参数,存放着字符串的首地址,%eax为返回值,为一个整数。

3.4 本章小结

本节主要介绍编译器通过编译由.i文件生成汇编语言的.s文件的过程,并分析了变量,赋值,循环等各类C语言的基本语句的汇编表示。汇编语言和高级语言很不同,即使是高级语言中一个简单的条件语句或者循环语句在汇编语言中都需要涉及到更多步骤来实现。学习汇编语言与编译,使我们能够真正的理解计算机底层的一些执行方法,有利于我们以后对程序的优化或调试。


第4章 汇编

4.1 汇编的概念与作用

概念:把汇编语言翻译成机器语言的过程称为汇编,汇编器同时将汇编程序(.s文件)打包成可重定位目标程序(.o文件)。这里的.o是二进制文件,而.s仍然是文本文件。

作用:通过汇编,汇编代码转化为了计算机能够完全理解的机器代码。

4.2 在Ubuntu下汇编的命令

汇编命令:gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o

图4.1 汇编命令

图4.2 文件列表

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

分析:

  1. ELF头

命令:readelf -h hello.o

ELF头首先以一个16字节的序列开始,这个序列描述了生成该文件的系统的 字的大小和字节顺序。剩下部分就如下图所示,列出了包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小(64字节),目标文件的类型(REL可重定位文件),机器类型(AMD X86-64),节头部表的文件偏移,以及节头部表中条目的大小和数量。

图4.3 ELF头

hello.o的ELF以一个16进制序列:

              7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

作为ELF头的开头。这个序列描述了生成该文件的系统的字的大小为8字节和字节顺序为小端序

ELF头的大小为64字节,目标文件的类型为REL(可重定位文件)、机器类型为Advanced Micro Devices x86-64、节头部表的文件偏移为0,以及节头部表中条目的大小,其数量为13。

    2.节头部表

命令:readelf -S hello.o

图4.4 节头部表

如图4.4所示,节头部表描述了13个节的相关信息,由表可以看出:

.text节:以编译的机器代码,类型为PROGBITS,意为程序数据,旗标为AX,

即权限为分配内存、可执行。

.rela.text节:一个.text借中的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

.data节:已初始化的静态和全局C变量。类型为PROGBITS,意为程序数据,旗标为WA,及权限为可分配可写。

.bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。类型为NOBITS,意为暂时没有存储空间,旗标为WA,即权限为可分配可写。

.rodata节:存放只读数据,例如printf中的格式串和开关语句中的跳转表。类型为PROGBITS,意为程序数据,旗标为A,即权限为可分配。

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

.note.GNU_stack节:用来标记executable stack(可执行堆栈)。

  .eh_frame节:处理异常。

.rela.eh_frame节:.eh_frame的重定位信息。

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

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

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

    3.符号表(.symtab)

命令:readelf -s hello.o

图4.5 符号表

可以看到.c文件中用到的main,atoi,exit,sleep函数。

    4.重定位条目

命令:readelf -r hello.o

图4.6 重定位条目

当汇编器生成 hello.o 后,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目。重定位条目在链接时告诉链接器目标文件合并时如何修改应用。  

重定位条目的类型这里有两种,R_X86_64_32意思是重定位时使用一个32位的绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。R_X86_64_PC32意思是重定位时使用一个32位PC相对地址的引用。一个PC相对地址就是据程序计数器的当前运行值的偏移量。

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o > disa_hello.s

生成hello.o的反汇编文件。

分析hello.o的反汇编,并与第3章的 hello.s进行对照分析。

图4.7 disa_hello.s文件内容(部分)

不同点:

1.跳转命令:hello.s的跳转目的是.L1,.L2这样的符号来实现的,而disa_hello.s中,是直接跳转到指令的地址。而这个地址在<>中注明了关于整个函数首地址的偏移量,这个偏移量是根据PC值和距跳转目的的偏移量算出来的。

2.格式上:hello.s前没有一串二进制数,即相应的机器码,而反汇编代码前面有与之对应的机器码。

3.函数调用:hello.s函数调用后是函数名,而disa_hello.s中call后有函数名,也有关于这个函数地址的信息,如重定位条目的类型。

4.重定位条目:汇编代码仍然采用直接声明的方式,即通过助记符;而反汇编代码采用重定向的方式进行跳转,机器代码在此处留下一些地址以供链接时重定向,链接时根据标识、重定位条目自动填充地址。

4.5 本章小结

从汇编代码变为机器代码后,这个程序就可以真正被计算机理解。我们也可以利用反汇编工具查看一些二进制机器程序的汇编代码从而去破解或翻译,虽然反汇编后的代码与原本的汇编代码有所不同。


5链接

5.1 链接的概念与作用

概念:链接是将各种代码和数据片段收集并组合成为了一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也可以执行于加载时,分别对应静态链接和动态链接。

作用:链接在软件开发中扮演着重要角色,因为它使得分离编译成为可能。我们可以将软件进行模块化设计,然后模块化编程,这样分组工作高效。而且,需要修改或者调试时,只需要修改某个模块,然后简单地重新编译它,并重新链接,而不是全部重新编译链接。

5.2 在Ubuntu下链接的命令

链接命令:

sudo 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的格式

1.ELF头

命令:readelf -h hello

图5.2 ELF头

hello.o的ELF以一个16进制序列:

              7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

作为ELF头的开头。这个序列描述了生成该文件的系统的字的大小为8字节和字节顺序为小端序。

ELF头的大小为64字节,目标文件的类型为REL(可重定位文件)、机器类型为Advanced Micro Devices X86-64即AMD X86-64、节头部表的文件偏移为0,以及节头部表中条目的大小,其数量为25。

2.节头部表:

命令:readelf -S hello

图5.3 节头部表-1

图5.4 节头部表-2

如图5.3、图5.4所示,节头部表描述了25个节的相关信息,与hello.o的节头部表相比,多出来的部分:

.interp段:动态链接器在操作系统中的位置不是由系统配置决定,也不是由环境参数指定,而是由ELF文件中的.interp段指定。该段里保存的是一个字符串,这个字符串就是可执行文件所需要的动态链接器的位置,常位于/lib/ld-linux.so.2。(通常是软链接)

.dynamic段:该段中保存了动态链接器所需要的基本信息,是一个结构数组,可以看做动态链接下ELF文件的“文件头”。存储了动态链接会用到的各个表的位置等信息。

.dynsym段:该段与“.symtab”段类似,但只保存了与动态链接相关的符号,很多时候,ELF文件同时拥有.symtab与.synsym段,其中.symtab将包含.synsym中的符号。该符号表中记录了动态链接符号在动态符号字符串表中的偏移,与.symtab中记录对应。

.dynstr段:该段是.dynsym的辅助段,.dynstr与.dynsym的关系,类似于.symtab与.strtab的hash段:在动态链接下,需要在程序运行时查找符号,为了加快符号查找过程,增加了辅助的符号哈希表,功能与.dynstr类似。

.rel.dyn段:对数据引用的修正,其所修正的位置位于“.got”以及数据段(类似重定位段“.rel.data”)

3.符号表

图5.5 hello符号表

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

 如图5.5所示,hello程序的符号表包含编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段。

4.程序头表

图5.6 程序头表

5.Section to Segment Mapping

图5.7 Section to Segment Mapping

6.Dynamic Section

图5.8(1) Dynamic Section(1)

图5.8(2) Dynamic Section(2)

7. 重定位节(动态链接库)

图5.9(1) 重定位节(1)

图5.9(2) 重定位节(2)

8.版本信息

图5.10 版本信息

    

5.4 hello的虚拟地址空间

hello的虚拟地址从0x401000开始。

图5.11 EDB查看虚拟地址空间

比较readelf的段信息和edb的虚拟内存空间:

查看偏移0x3048的.data段

图5.12 EDB查看.data

5.5 链接的重定位过程分析

objdump -d -r hello > disa_hello_2.s分析与disa_hello.s的不同

图5.13 查看disa_hello_2.s文件内容(部分)

1.新增加的函数:

    新增加了很多函数和它的实现汇编代码,如我们源代码中使用的printf,atoi,exit等。还有_init      初始化函数。

2.新增加的节(Section):

    新增加了.init节,.plt节,.plt.sec。

3.调用函数:

    调用函数不再有重定位类型和偏移量,而是直接为函数的绝对地址和它的函数名。

4.链接:

    链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累      积在一起。从.o提供的重定位条目将函数调用和控制流跳转的地址填写为最终的地址。

5.6 hello的执行流程

 函数调用如下所示:

图5.14 hello调用的函数

5.7 Hello的动态链接分析

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

例如:查看.plt段

图5.15 EDB查看.plt段-1

运行后:

图5.16 EDB查看.plt段-2

5.8 本章小结

本章主要介绍了链接器如何将hello.o可重定向文件与动态库函数链接起来,分析了可重定位文件与可执行文件ELF的差异,并分析了重定位的过程。 


6hello进程管理

6.1 进程的概念与作用

概念:进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

作用:通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。

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

在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面的软件(命令解析器)”。它类似于DOS下的command.com和后来的cmd.exe。Shell接收用户命令,然后调用相应的应用程序。同时,Shell也是一种程序设计语言。作为命令语言,它交互式地解释和执行用户输入的命令,或者自动地解释和执行预先设定好的一连串命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。

Bash是一个命令处理器,通常运行于文本窗口中,并能执行用户直接输入的命令。Bash还能从文件中读取命令,这样的文件称为脚本。和其他Unix Shell一样,它支持文件名替换(通配符匹配)、管道、here文档、命令替换、变量,以及条件判断和循环遍历的结构控制语句。包括关键字、语法在内的基本特性全部是从Shell借鉴过来的。其他特性,例如历史命令,是从CSH和KSH借鉴而来。总的来说,Bash虽然是一个满足POSIX规范的Shell,但有很多扩展。

处理流程:

1. 用户输入命令。

2. Shell对用户输入命令进行解析,判断是否为内置命令。

3. 若为内置命令,调用内置命令处理函数,否则调用execve函数创建一个子进程进行运行。

4. 判断是否为前台运行程序:

     - 如果是,则调用等待函数等待前台作业结束。

     - 否则,将程序转入后台,直接开始下一次用户输入命令。

5. Shell接受键盘输入信号,并对这些信号进行相应处理。

6.3 Hello的fork进程创建过程

我们在Shell上输入./hello,这个不是一个内置的Shell命令,所以Shell会认为hello是一个可执行目标文件,通过条用某个驻留在存储器中被称为加载器的操作系统代码来运行它。当Shell运行一个程序时,父进程通过fork函数生成这个程序的进程。这个子进程几乎与父进程相同,子进程得到与父进程相同的虚拟地址空间(独立)的一个副本,包括代码,数据段,堆,共享库以及用户栈,并且和父进程共享文件。它们之间最大的不同是PID不同。

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件,且带参数列表argv和环境变量列表envp。在exevce加载了后,它调用启动代码,启动代码会设置栈,并将控制传递给新程序的主函数。

  1. 删除已存在的用户区域
  2. 映射私有区:为 hello 的代码、数据、.bss 和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制。
  3. 映射共享区:比如 hello 程序与共享库 libc.so 链接。
  4. 设置 PC:exceve() 做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点

图6.1 进程的虚拟内存

6.5 Hello的进程执行

多个流并发地执行的一般现象被称为并发。一个进程和其他进程轮流运行的概念被称为多任务。一个进程执行其控制流的一部分的每一时间段叫做时间片。因此,多任务也叫作时间分片。

操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文。上下文就是内核重启一个被抢占的进程所需的状态。在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个决策称为调度。

hello程序与操作系统的其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。因此,hello在sleep时就会进行这样的切换。程序在进行一些操作时会发生内核与用户状态的不断切换,这是为了保持在适当的时候有足够的权限并减少出现安全问题的可能性。

简单来看hello sleep进程调度的过程如下:

1. 调用sleep之前,如果hello程序不被抢占,则顺序执行。

2. 假如发生被抢占的情况,则进行上下文切换。上下文切换由内核中的调度器完成。当内核调度新的进程运行后,它就会抢占当前进程,并运行:

   (1). 保存以前进程的上下文。

   (2). 恢复新恢复进程被保存的上下文。

   (3). 将控制传递给这个新恢复的进程,来完成上下文切换。

图6.2 进程上下文的剖析

6.6 hello的异常与信号处理

1.不停乱按,包括回车:

图6.3 乱按的现象

Shell会将回车前输出的字符串当作命令。

由于这里设置的秒数为零,所以所有的hello语句全部给出,没有进行sleep。

2.Ctrl + C

图6.4 Ctrl + C的现象

会立即终止进程,通过ps命令发现hello进程被回收。

3.Ctrl + Z

图6.5 Ctrl + Z的现象

会在后台停止,fg放到前台运行时,会输出剩下的7个字符串。

6.7本章小结

本章了解了hello进程的执行过程。在hello运行过程中,内核对其调度,异常处理程序为其将处理各种异常。每种信号都有不同的处理机制,对不同的shell命令,hello也有不同的响应结果


7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址(也称为相对地址)是在有地址变换功能的计算机中,访问指令给出的地址(操作数)。逻辑地址经过寻址方式的计算或变换才能得到内存储器中的物理地址。

线性地址:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中,逻辑地址是段中的偏移地址,加上基地址后得到线性地址。

虚拟地址:虚拟地址是CPU在启动保护模式后,程序运行在虚拟地址空间中使用的地址。注意,并不是所有的程序都运行在虚拟地址中。CPU在启动时运行在实模式,Bootloader以及内核在初始化页表之前使用的是物理地址,而非虚拟地址。

物理地址:物理地址(Physical Address)是内存储器中以字节为单位存储信息的地址。每个字节单元都有一个唯一的存储器地址,以正确地存放或获取信息。物理地址也叫实际地址或绝对地址。

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

在计算机系统中,一个逻辑地址由两部分组成:段标识符和段内偏移量。段标识符由一个16位长的字段组成,称为段选择符,其中前13位是一个索引号,后3位包含一些硬件细节。

段选择符

  1. 段选择符:16位长的字段。
    1. 前13位:索引号,可直接理解为数组下标。
    2. 后3位:包含一些硬件细节。

索引号对应的是“段描述符表”中的一个条目。段描述符具体描述了一个段的地址,多个段描述符组成段描述符表。通过段选择符的前13位,可以在段描述符表中找到对应的段描述符。段描述符中的Base字段描述了段的开始位置的线性地址。

段描述符表

Intel的设计是将全局段描述符放在“全局段描述符表(GDT)”中,而将局部段描述符(例如每个进程的段描述符)放在“局部段描述符表(LDT)”中。

  1. GDT:其地址和大小存放在CPU的gdtr控制寄存器中。
  2. LDT:其地址和大小存放在ldtr寄存器中。

逻辑地址转换为线性地址的过程

1.给定一个完整的逻辑地址:[段选择符:段内偏移地址]。

2.检查段选择符的T1位:

  1. T1=0:使用GDT中的段。
  2. T1=1:使用LDT中的段。

3.获取段描述符表的地址和大小:

  1. 根据相应寄存器(gdtr或ldtr),得到段描述符表的地址和大小。

4.查找段描述符:

  1. 使用段选择符的前13位作为索引,在段描述符表中找到对应段描述符。

5.获取段基地址:

  1. 段描述符中的Base字段表示段的开始位置的线性地址。

6.计算线性地址:

  1. 线性地址 = 段基地址(Base) + 段内偏移地址(Offset)。

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

计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。

图7.1 虚拟地址到物理地址

线性地址即虚拟地址,用VA来表示。如图7.1所示,VA被分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基址寄存器(PTBR)来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。

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

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,成为翻译后备缓存器(TLB)。

TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单一PTE组成的块。TLB通常有高的相联度,从虚拟地址中的页号提取出组选择和行匹配的索引和标记字段。因为所有的地址翻译都是在芯片上的MMU进行的,因此非常快。

多级页表:将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。如下图,VPN被分为k个部分,第一级VPN结合基址寄存器得到一个页表条目,其中存放下一级页表的基址,再结合VPN2,得到第三级页表基址,继续寻找,以此类推,知道最后确定对应的物理页号,与VPO结合,由图7.2,得到由PPN与PPO结合成的物理地址,用于物理地址寻址。

图7.2 多级页表

如果是二级页表,第一级页表的每个PTE负责一个4MB的块,每个块由1024个连续的页面组成。二级页表每一个PTE负责一个4KB的虚拟地址页面。这样的好处在于,如果一级页表中有一个PTE是空,那么二级页表就不会存在,这样会有巨大的潜在节约,因为4GB的地址空间大部分都是未分配的。

现在的64位计算机采用4级页表,36位的VPN被封为4个9位的片,每个片被用作一个页面的偏移,CR3寄存器包含L1页表的物理地址。VPN1提供一个到L1PET的偏移量,这个PTE包含L2页表的基地址,VPN2提供一个到L2PTE的偏移量,以此类推。

Core i7是四级页表进行的虚拟地址转物理地址。48位的虚拟地址的前36位被分为四级VPN区。结合存放在CR3的基址寄存器,由前面多级页表的知识,可以确定最终的PPN,与VPO结合得到物理地址。

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

对于一个虚拟地址请求,首先将去TLB寻找,看是否已经在TLB中缓存。如果命中的话就直接MMU获取,没有命中的话就再结合多级页表,得到物理地址,去Cache中寻找,到L1中后,寻找物理地址又要检测是否命中,不命中则紧接着寻找下一集Cache L2,接着L3。这就是使用CPU的高速缓存机制,一级一级往下找,知道找到对应的内容。

7.6 hello进程fork时的内存映射

当fork函数被Shell调用时,内核对hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

图7.3 一个私有的写时复制对象

7.7 hello进程execve时的内存映射

execve函数在Shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地代替了当前程序。加载并运行hello需要以下几个步骤:

①删除已存在的用户区域。删除当前进程(Shell)虚拟地址的用户部分中的已存在的区域结构。

②映射私有区域。为hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data段。.bss段是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。

③映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

④设置程序计数器(PC)。execve做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

下次调度hello进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

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

在虚拟内存中,DRAM缓存不命中称为缺页。CPU需要引用VP3中的一个字,通过读取PTE3,发现有效位为0,说明不在内存里,这时就发生了缺页异常。缺页异常发生时,通常会调用内核里的缺页异常处理程序,该程序会选择一个牺牲页,这里是存放在PP3的VP4,如果VP4已经被修改,内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在内存里。接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令。

缺页处理程序不是直接就替换,它会经过一系列的步骤:

①虚拟地址是合法的吗?若不合法,它就会触发一个段错误。

②试图进行的内存访问是否合法?意思就是进程是否有读、写或执行这个区域的权限。经过上述判断,这时才能确定这是个合法的虚拟地址,然后才会执行上述的替换。

图7.4 VM缺页(之前) 对VP3中的字的引用会不命中,从而触发了缺页

图7.5 进入缺页异常处理程序

7.9动态存储分配管理

动态内存管理涉及在程序运行时分配和释放内存,以有效地利用可用内存资源。以下是动态内存管理的基本方法与策略:

 基本方法:

1. 分配内存:

   - `malloc`: 分配指定字节数的内存,返回指向已分配内存的指针。

   - `calloc`: 分配内存并初始化为零,适合需要初始化的动态数组。

   - `realloc`: 调整已分配内存块的大小,可以扩展或缩小内存块。

   - `free`: 释放之前分配的内存,避免内存泄漏。

2. 内存池:

   - 通过预分配一大块内存,并从中划分出小块内存用于分配,提高分配和释放内存的效率。

 动态内存管理策略:

1. 首次适配(First Fit):

   - 从头开始搜索,找到第一个大小合适的空闲块进行分配。

   - 优点:简单,快速找到合适的块。

   - 缺点:可能产生许多小碎片。

2. 最佳适配(Best Fit):

   - 搜索整个空闲链表,找到最接近所需大小的空闲块。

   - 优点:减少浪费空间。

   - 缺点:可能会更慢,因为需要搜索整个链表。

3. 最差适配(Worst Fit):

   - 搜索整个空闲链表,找到最大块进行分配。

   - 优点:有助于防止大块内存碎片化。

   - 缺点:可能会造成更多的小碎片。

4. 下一适配(Next Fit):

   - 类似于首次适配,但从上次结束的地方继续搜索。

   - 优点:可能比首次适配更均匀地分布内存使用。

   - 缺点:不一定能减少碎片化。

5. 快速适配(Quick Fit):

   - 维护多个大小类别的空闲块列表,快速找到合适的块进行分配。

   - 优点:快速分配和释放。

   - 缺点:需要复杂的管理和维护多个空闲列表。

 内存回收策略:

1. 引用计数:

   - 每个对象都有一个计数器,记录被引用的次数。当计数器为零时,释放内存。

   - 优点:及时释放无用内存。

   - 缺点:无法处理循环引用。

2. 标记-清除(Mark and Sweep):

   - 分为标记阶段和清除阶段。首先标记所有可达对象,然后清除未标记的对象。

   - 优点:能处理循环引用。

   - 缺点:标记和清除过程可能导致性能下降。

3. 复制回收(Copying Collection):

   - 将存活对象从一个区域复制到另一个区域,然后清空原区域。

   - 优点:能有效整理内存,减少碎片。

   - 缺点:需要额外的内存空间。

4. 分代回收(Generational Collection):

   - 将堆分为几代(如年轻代和老年代),不同代采用不同回收策略。

   - 优点:优化回收效率,年轻对象回收更频繁。

   - 缺点:复杂性增加。

这些方法和策略共同作用,确保动态内存管理的高效性和可靠性,使得程序能够灵活地使用内存资源,避免内存泄漏和碎片化。

7.10本章小结

本章主要介绍了存储器地址空间、Intel逻辑地址到线性地址的变换-段式管理、线性地址到物理地址的变换-页式管理、TLB与四级页表支持下的VA到VP的变换、三级Cache支持下的物理内存访问,进程fork和execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容。有助于理解程序的存储管理的各种机制,以及有关的存储方式与管理策略。


8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件。所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对对应文件的读和写来执行。

设备管理:unix io接口。将设备映射为文件的方式,使得Linux内核引出一个简单、低级的应用接口,称为I/O接口,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

8.2.1Unix IO接口

Unix IO使得所有的输入和输出都能以一种统一且一致的方式来执行:

①打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符

②Linux Shell创建的每个进程开始时都有三个打开的对应文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。

③改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始时为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。

④读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

⑤关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

8.2.2Unix IO函数

①打开文件:int open(char *filename, int flag, mode_t mode);

进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件,也可以是一个或者更多位掩码的或,为写提供给一些额外的指示。mode参数指定了新文件的访问权限位。

②关闭文件:int close(int fd);

进程通过调用close函数关闭一个打开的文件。关闭一个已关闭的描述符会出错。

③读文件:ssize_t read(int fd, void *buf, size_t n);

应用程序通过调用read函数执行输入。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。

④写文件:ssize_t write(int fd, const void *buf, size_t n);

应用程序通过调用write函数执行输出。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

8.3 printf的实现分析

printf函数体:

  1. int printf(const char *fmt, ...)
  2. {
  3.  
  4.     int i;
  5.  
  6.     char buf[256];
  7.  
  8.     va_list arg = (va_list)((char*)(&fmt) + 4);
  9.     i = vsprintf(buf, fmt, arg);
  10.     write(buf, i);
  11.     return i;
  12.  
  13. }

printf函数调用vsprintf函数进行格式化,接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出;调用write函数把buf中的i个元素的值写到终端。syscall函数不断地打印出字符,直到遇到:'\0'。

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

Getchar函数体:

  1. int getchar(void)
  2. {
  3.       static char buf[BUFSIZE];
  4.       static char *b=buf;
  5.       static int n=0;
  6.  
  7.       if(n==0)
  8.       {
  9.              read(0,buf,BUFSIZE);
  10.              b=buf;
  11.       }
  12.  
  13.       return ((--n)>0) ? (unsigned char) *b++ : EOF;

异步异常-键盘中断的处理:当用户按键时触发键盘终端,操作系统将控制转移到键盘中断处理子程序,中断处理程序执行,接受按键扫描码转成ascii码,保存到系统的键盘缓冲区,显示在用户输入的终端内。当中断处理程序执行完毕后,返回到下一条指令运行。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

8.5本章小结

本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,同时对printf和getchar的实现进行分析。通过对熟悉函数的分析深入了解函数实现与IO的关系,有助于理解IO管理的概念和相关函数。

结论

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

本文围绕hello所经历的过程展开,其中hello所经历的重要结点包括:

①编写源程序(文本)hello.c;

②hello.c经过预处理器(cpp)的预处理得到修改了的源程序(文本)hello.i;

③hello.i经过编译器(ccl)的编译得到汇编程序(文本)hello.s;

④hello.s经过汇编器(as)的汇编得到可重定位目标程序(二进制)hello.o;

⑤hello.o经过链接器(ld)将其与其它目标文件合并得到可执行目标文件hello;

⑥shell调用fork函数创建子进程;

⑦shell调用execve函数加载hello程序,映射到对应的虚拟内存;

⑧hello程序执行过程中通过进程管理实现异常与信号的处理,存储管理实现内存访问,同时相应的IO设备配合hello程序实现输入输出等功能;

⑨程序结束,父进程对其进行回收,内核将其从系统中清除。

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

计算机系统的设计与实现首先需要深刻理解计算机系统的各种概念,包括运行机制、原理等,了解程序执行过程中的基本流程,包括预处理、编译、汇编、链接等阶段以及相应的细节。在此基础上,通过对进程管理、存储管理、IO管理等加深对整体框架结构的认识。同时,依据计算机系统的相关原理,可以在安全性、高效性等方面对程序做进一步优化;针对各种可能存在的安全风险进行有效防范。因此,熟练掌握计算机系统相关知识无论是对于加深对计算机的理解,还是编写更加优秀的程序都至关重要。


附件

  1. hello.c:源代码文件。
  2. hello.i:预处理后的文本文件。
  3. hello.s:编译后的汇编文件。
  4. hello.o:汇编后的可重定位文件。
  5. hello:链接后的可执行文件。
  6. disa_hello.s:反汇编hello.o的汇编代码。
  7. disa_hello_2.s:反汇编hello的汇编代码。


参考文献

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

[2]  https://blog.csdn.net/qq_36314864/article/details/121250743

[3]  https://blog.csdn.net/hzp020/article/details/83765267

[4]  https://www.runoob.com/w3cnote/gcc-parameter-detail.html

[5]  https://www.cnblogs.com/skywang12345/archive/2013/05/30/3106570.html

  • 32
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值