摘  要

通过分析hello.c的从程序到进程的过程,展示本学期计算机系统课所学的部分内容:程序的编译、链接,进程,内存,I/O。hello.c经过预处理、编译、汇编、链接成为一个可执行程序,shell进程创建子进程,在子进程中运行hello程序,对hello进程进行管理,在hello进程终止后回收它。hello程序运行时,Linux系统的内存系统和I/O也在发挥作用。

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

第1章 概述

1.1 Hello简介

源程序文本文件hello.c经预处理、编译、汇编、链接变为可执行目标文件hello。在bash内,执行 ./hello <学号> <姓名> 命令后,shell先使用fork创建子进程,然后用execve在子进程的上下文中加载并运行可执行目标文件hello。这是hello的P2P(from Program to Process)。

hello进程终止后,shell会回收它。这是hello的020(from zero to zero)。

1.2 环境与工具

硬件环境:X64 CPU;2.3GHz;8G RAM;256GHD Disk

软件环境:macOS 11.2.3;VMware Fusion 12.1.2;Ubuntu 20.04.2

开发与调试工具:gcc;objdump;readelf;as;ld;Sublime;gedit;edb;gdb

1.3 中间结果

hello.i 是hello.c的预处理结果

hello.s 是使用gcc将hello.i编译得到的

hello.o 是将hello.s进行汇编得到的

hello 是将hello.o与库链接得到的

hello_dump_dr.txt 对hello用objdump -dr输出重定位得到的

helloo_dump_dr.txt 对hello.o用objdump -dr输出重定位得到的

1.4 本章小结

本章简要介绍了hello的P2P、020,列出了完成本次大作业所使用的环境与工具以及产生的中间结果。

第2章 预处理

2.1 预处理的概念与作用

预处理将C的源程序.c翻译成一个ASCII码的中间文件.i。预处理中会展开以#起始的行,试图将其解释为预处理指令,包括ISO C/C++要求支持的#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。

预处理的作用:预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。

2.2在Ubuntu下预处理的命令

预处理命令:

cpp [options] file…

图1 预处理结果

2.3 Hello的预处理结果解析

# include 的头文件内容被复制到了hello.i中,其中包含大量函数声明以及类型定义(typedef),除此之外,还用#标明了程序涉及到的许多.h文件的所在目录。hello.c内的文本内容除了注释和#include命令被去掉外没有发生变化。

图2 预处理解析

图3 预处理解析

图4 预处理解析

图5 预处理解析

2.4 本章小结

本章介绍了预处理的概念和作用,对hello.c的预处理结果进行了解析。

第3章 编译

3.1 编译的概念与作用

编译是预处理后的.i文件翻译成ASCII汇编语言.s文件。编译主要进行的是词法分析和语法分析,又称为源程序分析。若分析过程中发现有语法错误,则会给出提示信息。

编译的作用是将C语言源代码(经预处理的)翻译为汇编语言文件

3.2 在Ubuntu下编译的命令

命令:

gcc -S <文件名>

图6 编译命令

图7 编译结果

3.3 Hello的编译结果解析

图8 hello.c代码

3.3.1 数据

hello.c中出现的常量有printf中的格式串"Usage: Hello 学号 姓名!\n"和"Hello %s %s\n"。

在hello.s中表示为:

图9 格式串的汇编表示

格式串中的每个中文字符根据UTF-8的编码规则被编码为3个字节的二进制码。

hello.c中出现的变量有全局变量sleepsecs,main的命令行参数argc、argv以及局部变量i。argc存在%edi中,

图10 汇编指令

argv存在%rsi中,用偏移的方式引用argv[1]和argv[2],

图11 汇编指令

图12 汇编指令

局部循环变量i存在%ebx中,被初始化为0。

图13 for循环的汇编表示

3.3.2 赋值

hello.c中发生了两次赋值,分别是sleepsecs和i的初始化。sleepsecs被赋值2,在汇编代码中直接用它的变量名引用它,

图14 sleepsecs的汇编表示

图15 sleepsecs的汇编引用

变量i用movl指令进行初始化。

图16 汇编指令

3.3.3 类型转换

对int类型的sleepsecs赋值时发生了隐式类型转换,浮点数2.5被隐式转换为2。

图17 sleepsecs赋值时的隐式转换

3.3.4 算数操作

自增运算i++被翻译为addl指令。

图18 汇编指令

3.3.5 关系操作

argc != 3 被翻译为cmpl指令,

图19 汇编指令

i < 10 也被翻译为cmpl指令。

图20 汇编指令

3.3.6 数组操作

用偏移的方式引用argv[1]和argv[2]。一开始指针argv存在%rsi中,它先被movl到%rbp中,然后通过%rbp加上偏移量引用argv[1]和argv[2]。

图21 汇编指令

图22 汇编指令

3.3.7 控制转移

图23 C代码

图24 汇编指令

图25 汇编指令

用比较和跳转指令实现if语句,当argc不等于3时,控制会跳转到标号.L6处。

图26 hello.c中的for循环

图27 for循环的汇编表示

for循环被翻译为guarded-do形式。首先使用条件分支,若初始条件不成立就跳过循环。

3.3.8 函数操作

用call指令进行函数调用。hello.c中的两个printf函数中的第一个被翻译为puts。movl指令将字符串的地址放到%rdi,作为puts的第一个参数。用call指令将控制转移给puts。

图28 汇编指令

图29 格式串的汇编表示

格式串的地址、argv+1、argv+2分别被放在%rdi、%rsi、%rdx中作为printf的三个参数。用call指令将控制转移给printf。

图30 汇编指令

图31 汇编指令

用call指令调用getchar。

图32 汇编指令

3.4 本章小结

本章介绍了编译的概念和作用,将hello.i编译为hello.s后对编译结果进行了分析。

第4章 汇编

4.1 汇编的概念与作用

汇编是指将编译后的.s文件翻译为机器语言二进制可重定位目标.o文件。

4.2 在Ubuntu下汇编的命令

gcc -c -m64 -Og -no-pie -fno-PIC hello.s -o hello.o

图33 汇编命令

4.3 可重定位目标elf格式

一个典型的ELF可重定位目标文件包含下面几个节:

ELF头

.init               定义了_init函数,程序初始化代码会调用它。

.text                     已编译程序的机器代码。

.rodata          只读数据,比如printf语句中的格式串和开关语句的跳转表

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

.bss               未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。

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

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

节头部表     描述目标文件的节。

ELF头:从一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助连接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

图34 hello.o的ELF头

节头部表:描述了不同节的位置和大小。目标文件中每个节都有一个固定大小的条目。

图35 hello.o的节头部表

重定位信息如下:

图36 hello.o的重定位信息

Offset是引用的节偏移,表示引用发生的位置;Type告知链接器如何修改新的引用;Addend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。我们只关心两种最基本的重定位类型:R_X86_64_32和R_X86_64_PC32。R_X86_64_32是重定位一个使用32位绝对地址的引用,R_X86_64_PC32是重定位一个使用32位PC相对地址的引用。

这是调用第一个printf函数时对"Usage: Hello 学号 姓名!\n"的引用:

图37 格式串的重定位信息

这是调用第二个printf时对"Hello %s %s\n"的引用:

图38 格式串的重定位信息

这是调用sleep函数时对sleepsecs的引用:

图39 sleepsecs的重定位信息

4.4 Hello.o的结果解析

机器语言是由二进制码组成的,每条机器指令的字节数不等,由操作码字段和操作数字段组成。

图40 反汇编指令

图41 反汇编结果

在.o文件中,原本.s文件中的跳转指令用到的语句标号被换成了.text节的偏移量,比如

图42 汇编指令

被替换为

图43反汇编结果

movl语句中用于引用.rodata和.data段中的标号被换为了数字0,表示等待链接器根据重定位条目进行重定位,比如

图44 汇编指令

被替换为

图45 反汇编结果

调用函数的地方也都是0,表示等待重定位,比如

图46 汇编指令

被替换为

图47 反汇编结果

此外,所有在.s文件中用十进制表示的数在.o文件中都用十六进制表示。

4.5 本章小结

本章介绍了汇编的概念与作用,介绍了ELF可重定位目标文件的格式,并分析了hello.s的汇编结果。

第5章 链接

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可执行于编译时,加载时,运行时。在现代系统中,链接是由链接器自动执行的。为了构造可执行文件,链接器主要要完成两个任务:符号解析、重定位。

链接的作用:把可重定位目标文件和命令行参数作为输入,产生一个完全链接的,可以加载运行的可执行目标文件。链接器在软件开发中扮演一个重要的角色,它使得分离编译成为可能。

5.2 在Ubuntu下链接的命令

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

图47 链接命令

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

一个典型的ELF可执行目标文件包含下面这些节:

ELF头

段头部表     描述了如何将文件的片映射到运行时的内存段。

.init               定义了_init函数,程序初始化代码会调用它。

.text                     已编译程序的机器代码。

.rodata          只读数据,比如printf语句中的格式串和开关语句的跳转表

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

.bss               未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。

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

.debug          一个调试符号表,其条目时程序中定义的全局变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。

.line                     原始C源程序的行号和.text节中机器指令之间的映射

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

节头部表     描述目标文件的节。

ELF头:从一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助连接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(可执行)、机器类型(AMD X86-64)、节头部表的文件偏移(14200),节头部表中条目的大小和数量,段头部表的文件偏移(64),节头部表中条目的大小和数量。

图48 hello的ELF头

段头部表描述了文件中各段的位置与其在内存中运行时位置的映射关系:

图49 hello的段头部表

节头部表描述了各节的大小、位置信息:

图50 hello的节头表

图51 续图50

此时还存在重定位节说明重定位还没有完成,程序加载/运行时会完成最后的链接。

图52 hello的重定位信息

5.4 hello的虚拟地址空间

地址最低的地方是用户的代码段和数据段,

图53 hello的虚拟地址空间

共享库的内存映射区域在地址更高一点的地方,

图54 续图53

用户栈在地址更高的地方,用户栈之上还有虚拟动态共享库(virtual dynamic shared library, vdso)和虚拟系统调用(virtual system call, vsyscall)的相关区域。

图55 续图54

下面以hello的代码段为例看一下虚拟内存里的具体内容:

代码段从0x401000开始,

图56 hello段头部表的部分内容

从0x401000到0x40101a的内容是_init,

图57 反汇编结果

图58 内存中实际的内容

从0x401020到0x4010cf是.plt节的内容,

图59 hello的.plt节的内容

(中间省略)

图60 续图59

图61 内存中的实际内容

.text节中,main在0x401105到0x40116f,

图62 内存中的实际内容

5.5 链接的重定位过程分析

图63 反汇编命令

经过链接后,hello的代码段比hello.o长了很多,这是因为链接器将hello.o和库中的许多目标文件进行了合并。.init节定义了一个小函数_init,程序的初始化会调用它。

图64 反汇编结果

在.text节中,多了一个函数       _start。加载程序时,加载器会跳转到_start的地址,它会调用main函数。

图65 反汇编结果

此外,还多了这些函数:

图66 反汇编结果

图67 反汇编结果

图68 反汇编结果

图69 反汇编结果

通过.plt中的数据结构与.got的合作,程序运行时可以调用动态链接器与printf、puts等库函数链接。

图70 反汇编结果

图71 反汇编结果

在hello中,.text节的main中的重定位已经完成了。main的起始位置变为0x401105,而不是hello.o中的0。

图72 main的地址

两次对.rodata节中的字符串的32位绝对地址引用已经生成了。

图73 对字符串的重定位信息

图74 对字符串的重定位结果

图75 对字符串的重定位信息

图76 对字符串的重定位结果

对sleepsecs的32位相对地址引用也已经生成了。

图77 重定位信息

图78 重定位结果

对printf、sleep、puts、exit、getchar的调用被重定位到.plt节。

图79 重定位信息

图80 重定位结果

图81 重定位信息

图82 重定位结果

图83 重定位信息

图84 重定位结果

图85 重定位信息

图86 重定位结果

图87 重定位信息

图88 重定位结果

通过.plt可以跳到.got。

图89 hello的.plt.sec

此时.got中还是空的,程序运行时,动态链接器会将调用的库函数的运行时地址写入.got。

图90 hello的.got的内容

5.6 hello的执行流程

图91 启动edb

_start

__libc_start_main

__libc_csu_init

_init

_setjmp

main调用了puts、printf、sleep、getchar

main返回到__libc_start_main中

__run_exit_handlers调用了很多函数,它执行完之后程序结束。

5.7 Hello的动态链接分析

图92 hello的.got的内容

图93 hello的.got的的运行时实际内容

在dl_init之后,全局偏移量表GOT的内容发生了变化,填入了动态链接器在解析函数地址时会使用的信息(查询memory region发现此时填入的信息是共享库的数据段的地址,这个信息会在运行时不断变化),其他条目没有变化,仍然是到plt中的条目的跳转,

图94 hello的.got的的运行时实际内容

程序运行过程中,会在got中填上共享库中函数的地址,在下图的时刻got中已经有了printf和sleep的地址:0x404020处是printf的运行时地址0x7f9396885e10;0x404038处是sleep的运行时地址0x7f9396906f40。

图95 hello的.got的的运行时实际内容

5.8 本章小结

本章介绍了链接的概念与作用,分析了hello的链接与执行过程。

第6章 hello进程管理

6.1 进程的概念与作用

进程是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行的状态组成的。

作用:进程给应用程序提供两个关键抽象:一个独立的逻辑控制流,一个私有的存储空间。在操作系统上运行一个程序时,就好像程序是系统中当前运行的唯一的程序,好像是在独占处理器和内存。

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

shell是一种交互型应用程序,它代表用户运行其他程序。shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行;求值步骤解析命令行,并代表用户运行程序。shell还应该能对前台/后台的进程进行管理(比如回收僵死进程),并能对用户发送的信号做出合理的反应。

处理流程:shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行;求值步骤解析命令行,并代表用户运行程序。

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程得到与父进程的用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程有与父进程不同的PID。fork函数在子进程中返回0,这就提供了一个明确的辨别程序是在父进程还是子进程中执行的方法。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新的程序。它会覆盖当前进程的地址空间。但并没有创建一个新的进程。新的程序仍然有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。

shell创建的子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的代码和数据段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件中的片,新的代码和数据段初始化为可执行文件的内容。最后,加载器跳转到程序的入口点,也就是_start地址。这个函数是在系统目标文件ctrl.o中定义的。_start函数调用系统启动函数_libc_start_main,该函数在libc.so中定义。它初始化执行环境,调用用户层的main函数,处理main函数返回值,并且在需要的时候把控制返回给内核。

6.5 Hello的进程执行

内核为每个进程维持一个上下文。上下文是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种用户数据结构(包括页表、进程表、文件表等)。

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

在内核执行的某些时刻,内核可以决定抢占当前进程,并通过上下文切换重新开始一个先前被抢占的进程,这就是调度

处理器用某个控制寄存器中的一个模式位来描述进程当前享有享有的权限。设置了模式位时,进程处在内核模式,否则处在用户模式

下面分析调用sleep函数时hello的进程执行:

hello进程执行sleep系统调用陷入到内核,sleep系统调用显时地请求让调用进程(hello进程)休眠,此时内核在内核模式中上下文切换到别的进程(先保存hello进程的上下文,然后回复某个先前被抢占的进程的上下文,再将控制传递给这个新恢复的进程),在hello进程休眠的时候执行其他进程。定时器中断时,内核判断当前进程已经运行了足够长的时间,切换到另一个进程(比如由回到了hello)。

6.6 hello的异常与信号处理

不停乱按(包括回车):在乱按过程中,shell会把输入的字符存到缓冲区。当hello运行结束后,shell会把hello执行时用户输入的字符当作命令(以回车为间隔,每个回车一个命令)。

图96 hello运行时乱按

ctrl+c:内核会发送一个SIGINT信号给前台进程组的每个进程,用ps命令发现hello进程已被回收。

图97 hello运行时时按ctrl+c

ctrl+z:内核发送一个SIGSTOP信号给前台进程组的每个进程,将hello进程挂起,

图98 hello运行时按ctrl+z

使用fg命令后,hello进程继续运行。

图99 ctrl+z后用fg命令

6.7本章小结

本章介绍了进程的概念和作用,简述了shell的作用和处理流程,介绍了hello的进程执行,hello的异常与信号处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:在有地址变换功能的计算机中,访内指令给出的地址(操作数)叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。存储单元的地址就可以用段基址(段地址)和段内偏移量(偏移地址)来表示,段基址确定它所在的段居于整个存储空间的位置,偏移量确定它在段内的位置,这种地址表示方式称为逻辑地址,通常表示为段地址:偏移地址的形式。

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

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

物理地址:是指出现CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

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

分段内存管理模在实地址模式下,处理器使用20位的地址总线,可以访问 1MB(0~FFFFF)内存。块把逻辑地址转换为线性地址。逻辑地址由段地址和段内偏移量组成。8086的模式,只有16位的地址线,不能直接表示20位的地 址,采用内存分段的解决方法。段将内存空间划分为64KB的段。CS用于存放16位的代码段的段地址/基地址,DS用于存放16位的数据段的段地址/基地址,SS用于存放16位的堆栈段的段地址/基地址,ES用于存放16位的附加段的段地址/基地址。

20位线性地址是这样计算的,08F1: 0100 = 08F1H*10H + 0100H = 09010H。

图100 逻辑地址到线性地址到变换

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

内存管理单元(MMU)利用页表来实现地址翻译,即从虚拟地址空间到物理地址空间的映射(线性地址就是虚拟地址,VA)。CPU中的一个控制寄存器,页表基址寄存器(PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号(VPN)。MMU利用VPN选择合适的页表条目(PTE)。将PTE中的物理页号(PPN)和虚拟地址中的VPO串联起来,就得到相应的物理地址。

图101 线性地址到物理地址的变换

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

MMU中有一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB),通过这个缓存,MMU可以加速对PTE的查询,从而加速地址翻译。

Core i7使用四级列表(L1页表、L2页表、L3页表、L4页表)来将虚拟地址翻译为物理地址。36位的VPN被划分为四个9位的片(VPN1、VPN2、VPN3、VPN4),每个片被用作到一个页表的偏移量。CR3(i7中的PTBR)寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含一个L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。在L4页表的PTE中存了VPN对应的40位PPN,将这个PPN与VPO合并,就得到了VA对应的PA。

图102 Core i7的四级页表

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

地址翻译发生在高速缓存查找之前。PTE可以缓存,就像其他数据字一样。CPU产生一个虚拟地址后,MMU从TLB中取出相应的PTE,当PTE不命中时,MMU必须从L1缓存取出相应的PTE,新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。PTE命中后,MMU将之前CPU产生的虚拟地址翻译为一个物理地址,并将该物理地址发送到高速缓存/主存,高速缓存/主存将所请求的数据字返回给CPU。

图103 Cache支持下的物理内存访问

7.6 hello进程fork时的内存映射

fork函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了当前进程的mm_struct、vm_area_struct和页表的原样副本。它将这两个进程中的每个页面都标记为只读,并将这两个进程中的每个vm_area_struct都标记为私有的写时复制。

当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

图104 进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

1. 删除hello进程虚拟地址空间的用户部分中的已存在的vm_area_struct;

2. 为新程序的代码、数据、bss、和栈区域创建新的vm_area_struct,其中bss区域、栈和堆是请求二进制零的;

3. 当hello程序与共享目标链接时,这些共享目标动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域中;

4. 设置PC,使它指向代码区域的入口点。

内核下一次调度hello进程时,hello程序会从入口点开始执行。内核将根据需要换入代码和数据页面。

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

DRAM缓存不命中称为缺页。在下图的时刻,CPU引用了VP3中的一个字,VP3并未缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,并且触发了一个缺页异常。

图105 某一时刻的内存状态

缺页异常处理程序会选择一个牺牲页,比如存放在PP3中的VP4。如果VP4已经被修改了,那么内核会将它复制回磁盘。内核会修改VP4的磁盘条目,反映出VP4不再缓存在主存中。接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,然后返回。

图106 另一时刻的内存状态

当异常处理程序返回时,内核会重新执行导致缺页的指令。现在页命中。

7.9动态存储分配管理

动态内存分配器维护这一个进程的称为堆的虚拟内存区域。对每个进程,内核维护这一个指向堆的顶部的变量brk。分配器将堆视为一组不同大小的块(block)的集合。每个块是一片连续的虚拟内存,要么是已分配的,要么是空闲的。已分配的块被释放后变为空闲块。根据这种释放是否是由应用程序显示执行的,内存分配器被分为显示分配器隐式分配器

显示分配器要求应用显示地释放任何已分配的块。C标准库提供的malloc程序包就是显示分配器。

隐式分配器会检测到一个已分配块不再被程序所使用,就会释放这个块。隐式分配器也叫垃圾收集器,自动释放未使用的已分配块的过程叫垃圾收集。

带边界标签的隐式空闲链表分配器

边界标签技术是对普通的带头部的隐式空闲链表的优化。

边界标签就是在每个块的结尾处添加一个脚部(footer,边界标签),脚部是头部的一个副本。因为每个块都包含一个脚部,所以分配器可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字(4字节)的位置。边界标签使得对前面块的合并只需要常数时间,而不需要在调用free时从头开始遍历整个堆。

显示空闲链表

可以将空闲块组织成一种显示数据结构。程序不需要一个空闲块的主体(有效载荷),所以可以在空闲块的有效载荷内存放指向前一个空闲块和后一个空闲块的指针。这样,堆就被组织成了一个双向空闲链表。使用双向链表而不是隐式空闲链表,首次适配的分配时间就从块总数的线性时间减少到了空闲块数量的线性时间。

空闲链表中块的排序策略决定了释放一个块的时间是线性的还是常数。

可以用后进先出(LIFO)的顺序维护链表,将新释放的块放在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块。使用这种策略,释放一个块可以在常数时间内完成。

还可以按照地址顺序来维护链表,即链表中每个块的地址都小于它后继的地址。使用这种策略,释放一个块需要线性时间的搜索来定位合适的前驱。使用按照地址排序的首次适配比LIFO排序的首次适配有更高的内存利用率。

分离的空闲链表

一个使用单向空闲链表的分配器需要与空闲块数量呈线性关系的时间来分配块。使用分离存储技术可以减少分配的时间,即维护多个空闲链表,其中每个链表中块有大致相等的大小。一般讲所有可能大小的块大小分成一些等价类(大小类)。简单分离存储分离适配是两种基本的分离存储的方法。伙伴系统是分离适配的一种特例。

7.10本章小结

本章介绍了存储空间地址的几种形式,介绍了逻辑地址到线性地址的变换,从线性地址到物理地址的变换,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时的内存映射,缺页故障与缺页中断处理,动态存储分配管理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

一个Linux文件就是一个m个字节序列,所有的I/O设备都被模型化为文件,所有的输入和输出都被当作相应文件的读和写来执行。

Linux文件的类型有普通文件(regular file)、目录(directory)、套接字(socket)、命名通道(named pipe)、符号链接(symbolic link)、字符(character)、块设备(block device)。

普通文件包含任意数据。文本文件是只含有ASCII和Unicode字符的普通文件;二进制文件是所有其他的文件。

目录是包含一组链接的文件。其中每个链接都将一个文件名映射到一个文件。

Linux内核将所有文件都组织成一个目录层次结构(directory hierarchy)。

设备管理:Unix I/O接口

将设备映射为文件的方式,允许Linux内核引出一个简单低级的Unix I/O应用接口,这使得所有的输入和输出都能以一种统一的方式来执行。

8.2 简述Unix IO接口及其函数

打开文件

进程通过调用open函数来打开一个已存在的文件或创建一个新文件。

int open(char *filename, int flags, mode_t mode)

将filename转换为一个文件描述符(是一个小的非负整数),并返回这个描述符数字。返回的描述符总是在该进程中当前没有打开的最小描述符。

flags参数指明了进程打算如何访问这个文件。

mode参数指定了新文件的访问权限位。

关闭文件

进程通过调用close函数关闭一个文件。

int close(int fd)

读文件

ssize_t read(int fd, void *buf, size_t n)

从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回-1表示错误,返回0表示当前文件位置是EOF,否则返回实际传送的字节数量。

size_t被定义为unsigned long,ssize_t被定义为long。

写文件

ssize_t write(int fd, const void *buf, size_t n)

从内存位置bug复制至多n个字节到描述符fd的当前文件位置。

8.3 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;}

typedef char *va_list说明它是一个字符指针

(char*)(&fmt) + 4) 表示的是...中的第一个参数的地址

int vsprintf(char *buf, const char *fmt, va_list args){char* p;char tmp[256];va_list p_next_arg = args;for (p = buf; *fmt; fmt++) {if (*fmt != '%') {*p++ = *fmt;continue;}fmt++;switch (*fmt) {case 'x':itoa(tmp, *((int*)p_next_arg));strcpy(p, tmp);p_next_arg += 4;p += strlen(tmp);break;case 's':break;default:break;}}return (p - buf);}

vsprintf根据格式串fmt产生格式化输出。

返回值是字符串的长度,在vsprintf后write把buf中的i个元素的值输出。

write会调用syscall(陷阱)。

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 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时,程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。

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

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

8.5本章小结

本章介绍了Linux的I/O,分析了printf和getchar的基本原理。

结论

hello所经历的过程

  1. 源程序文本文件hello.c经预处理、编译、汇编、链接变为可执行目标文件hello。
  2. 在bash内,执行 ./hello <学号> <姓名> 命令后,shell先使用fork创建子进程,然后用execve在子进程的上下文中加载并运行可执行目标文件hello。
  3. hello进程终止后,shell会回收它。

计算机系统是一个复杂系统,硬件与软件的深入协作是任何一个程序能运行的基础。计算机系统是复杂的,同时也是精巧的。计算机系统的每一部分都经历了从基础理论开始的漫长研发过程,凝聚着许多人的心血与智慧。

考虑到机器学习技术在现代计算机系统的无处不在的应用,计算机系统也许应该在底层、在处理器的层面加强对机器学习技术的支持。另外,现代计算机系统中,为不同任务特化的硬件越来越多(GPU、TPU、处理器中的机器学习核心等等),内存系统有待重新设计以满足各种硬件对虚拟/物理内存的同时访问,而不是为每个硬件都提供不同的缓存。

附件

hello.i 是hello.c的预处理结果

hello.s 是使用gcc将hello.i编译得到的

hello.o 是将hello.s进行汇编得到的

hello 是将hello.o与库链接得到的

hello_dump_dr.txt 对hello用objdump -dr输出重定位得到的

helloo_dump_dr.txt 对hello.o用objdump -dr输出重定位得到的

参考文献

[1] CS: APP

[2] https://baike.baidu.com/item/预处理/7833652?fr=aladdin

[3] https://baike.baidu.com/item/编译/1258343?fr=aladdin

[4] https://blog.csdn.net/u012927281/article/details/51649477

[5] https://baike.baidu.com/item/逻辑地址/3283849?fr=aladdin

[6] https://baike.baidu.com/item/线性地址/9013682?fr=aladdin

[7] https://baike.baidu.com/item/虚拟地址/1329947?fr=aladdin

[8] https://www.cnblogs.com/pianist/p/3315801.html

[9] https://baike.baidu.com/item/getchar/919709?fr=aladdin

HIT计算机系统 大作业 程序人生相关推荐

  1. HIT计算机系统大作业-程序人生-Hello’s P2P

    计算机系统大作业 ** 由于采用静态部署,需要看图片详细分析的小伙伴请移步个人博客网站:** 个人博客 题目:程序人生-Hello's P2P 学号: 姓名:熊峰 摘要: hello程序作为最简单的. ...

  2. HIT 计算机系统 大作业 程序人生-Hello’s P2P

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机类 学 号 1180300223 班 级 1803002 计算机科学与技术学院 2019年12月 摘 要 本文主要介绍了一个 ...

  3. HIT计算机系统大作业程序人生

    目 录 第1章 概述 - 4 - 1.1 HELLO简介 - 4 - 1.2 环境与工具 - 4 - 1.3 中间结果 - 4 - 1.4 本章小结 - 4 - 第2章 预处理 - 5 - 2.1 预 ...

  4. HIT 深入理解计算机系统 大作业 程序人生-Hello’s P2P

    HIT 深入理解计算机系统 大作业 程序人生-Hello's P2P 本论文旨在研究 hello 在 linux 系统下的整个生命周期.结合 CSAPP 课本, 通过 gcc 等工具进行实验,从而将课 ...

  5. 【2022】哈工大计算机系统大作业——程序人生Hello’s P2P

    2022哈工大计算机系统大作业--程序人生Hello's P2P 摘要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概 ...

  6. 2022计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学    号 120L021716 班    级 2003005 学       生 蔡泽栋 指 导 ...

  7. 哈工大2022计算机系统大作业---程序人生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学    号 120L021923 班    级 2003006 学       生 甄自镜 指 导 ...

  8. 哈工大计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题          目程序人生-Hello's P2P 专          业 计算机科学与技术 学       号120L022401 班          级 200300 ...

  9. 哈工大2022年春季学期计算机系统大作业——程序人生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 人工智能(未来技术) 学   号 7203610716 班   级 20WJ102 学       生 孙铭蔚 ...

最新文章

  1. vue ajax highcharts,在vue项目中引入highcharts图表的方法(详解)
  2. 平面设计师如何掌握色彩心理学(实用技巧)
  3. 安卓application_阿里面试官刁钻连问:安卓 UID的分配、查看及相关知识
  4. struts1.x心得1--struts入门介绍
  5. Python学习笔记_读Excel去重
  6. MaxCompute作业日常监控与运维实践
  7. ASP.NET操作Word的IIS权限设置
  8. java 屏幕键盘io
  9. 有哪些朋友圈励志说说短句?
  10. 日期对象Date的计算
  11. ldap 统一认证 java_LDAP统一用户认证
  12. 数据分析师,数据挖掘工程师和数据研发工程师有什么区别?
  13. 行人重识别实验笔记3-JDAI fast-reid项目配置
  14. android app应用签名生成工具,Android APK生成证书并签名方法
  15. [Maven实战-许晓斌]-[第二章]-2.7-2.8 Mave安装的最优建议和安装小结
  16. 超级实用的分时图指标 有了本分时图你根本不用看K线了
  17. buu crypto 变异凯撒
  18. 计算机word知识试题及答案,计算机二级考试word试题及答案
  19. 2组语法,1个函数,教你学会用Python做数据分析!
  20. 温度传感器的c语言程序,DS18B20数字温度传感器C语言程序实例

热门文章

  1. 无线蓝牙耳机哪个牌子的好?无线蓝牙耳机分享
  2. 天才神童V神与以太坊
  3. [转]一路且行且思考
  4. 史上最好用的安卓应用市场导航
  5. Windows老电脑的系统哪个更好
  6. nginx安装相关依赖
  7. 【Hadoop】HA高可用搭建保姆级教程(大二学长的万字笔记)
  8. SQL Server 计划作业
  9. 人脸颜值打分相关文章总结
  10. 均方误差越大越好_什么是峰值信噪比(PSNR)及均方误差(MSE)