计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能领域
学 号 2021112571
班 级 21WL026
学 生 邹轩崎
指 导 教 师 吴锐
计算机科学与技术学院
2023年5月
本文从hello.c文件入手,在linux系统上,从整体上介绍了hello程序P2P和020的过程。阐述了hello代码经过怎样的过程最终又是怎样在计算机上运行,以及hello进程创建直到回收的全部过程,通过对这些过程的分析,我们能更好的了解计算机系统的底层结构,并且进一步了解整个程序运行的的生命周期
关键词:hello world;CSAPP;程序的生命周期; P2P,O2O
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第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 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
Hello的P2P,020的整个过程是指Hello程序从一个C语言源文件到一个可执行的进程,以及从一个运行中的进程到一个结束的状态的过程。具体来说,可以分为以下几个步骤:
预处理:预处理器(cpp)根据以#开头的命令,修改原始的C程序,例如将#include指令替换为对应的头文件内容,将#define指令替换为对应的宏定义,删除注释等。预处理后生成一个修改后的C程序,通常以.i为扩展名。
编译:编译器(cc1)将预处理后的C程序翻译为汇编语言程序,进行词法分析、语法分析、语义分析和优化等操作。编译后生成一个汇编语言程序,通常以.s为扩展名。
汇编:汇编器(as)将汇编语言程序翻译为机器语言指令,并打包成可重定位目标程序,使用ELF格式存储。汇编后生成一个可重定位目标程序,通常以.o为扩展名。
链接:链接器(ld)将可重定位目标程序和其他库函数或静态库合并成一个可执行目标程序,解决符号引用和重定位问题。链接后生成一个可执行目标程序,通常以.out或无扩展名为文件名。
加载:当在shell中输入可执行目标程序的文件名时,shell会创建一个子进程,并调用execve系统调用来加载可执行目标程序到内存中,并映射到虚拟地址空间。加载后,可执行目标程序就变成了一个进程,并开始执行。
运行:进程在运行时,会通过CPU、内存、I/O等系统资源来完成其功能。CPU会按照时间片轮转的方式给进程分配执行时间,并进行取指、译码、执行等流水线操作。内存管理器会通过缓存和页表来管理进程的虚拟内存和物理内存之间的映射关系。I/O系统会根据进程的指令进行输入输出操作,例如打印字符串到屏幕上等。
结束:当进程执行完毕或遇到异常时,会向操作系统发送终止信号,并释放其占用的资源。操作系统会回收进程的内存空间和其他资源,并将其从系统中清除。这时,进程就从运行状态变成了结束状态。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk 1.2.2
软件环境
Windows10 64位; Vmware 11;Ubuntu 16.04 LTS 64位
1.2.3 开发工具
WINDOWS:clion; vi/vim/gedit+gcc
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c hello程序的源代码
hello.i 源代码经过预处理后的结果
hello.s 编译后生成的汇编程序
hello.o 汇编后生成的可重定位文件
hello 链接之后生成的可执行文件
a.out 添加相关编译参数的可执行文件
1.4 本章小结
本章根据hello自白介绍了hello的p2p,020的过程,说明了开发环境与使用的工具,以及实验中间的各种文件。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
作用:对原有的C程序进行预处理之后,原程序就不包含预处理的部分了。例如我们引用的<stdio.c>该库之中的内容直接加入到代码之中,我们使用宏定义#define定义的语句也直接被替换掉,还有根据#if决定是否处理之后的代码。
经过这样的处理,程序会变得更加方便阅读、修改、移植与调试,也有利于模块化的程序设计。
2.2在Ubuntu下预处理的命令
Ubuntu下预处理的命令是:gcc -E hello.c -o hello.i
正在上传…重新上传取消
正在上传…重新上传取消
2.3 Hello的预处理结果解析
由hello.c生成了hello.i文件
hello.i文件是预处理后生成的修改后的C程序,它包含了源程序中的所有代码,以及被#include指令替换的头文件内容。它还删除了注释,展开了宏定义,添加了一些行号和文件名信息等。
正在上传…重新上传取消正在上传…重新上传取消
2.4 本章小结
本章介绍了预处理的概念与作用,介绍了在Linux中预处理命令的使用方法,以及对hello.c预处理结果进行了解析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译是指将高级语言程序程序翻译成汇编语言文本程序,编译器把预处理后的文本文件hello.i文件进行一系列语法分析及优化后生成相应的汇编语言文件hello.s文件的过程。
编译的作用:
编译能够将不同的高级语言转化为通用的低级语言指令,即汇编语言
3.2 在Ubuntu下编译的命令
在Ubuntu下编译的命令为:gcc -S hello.i -o hello.s
正在上传…重新上传取消正在上传…重新上传取消
正在上传…重新上传取消
3.3 Hello的编译结果解析
正在上传…重新上传取消
3.3.1数据
(1)整型变量
正在上传…重新上传取消
由汇编程序的内容可知,argc作为main函数的第一个参数是一个int整型变量,在执行时先被存储在寄存器%edi中,然后又通过movl指令被存入-20(%rbp)。而根据.L2所示,函数内部的局部变量i被存储在-4(%rbp)(即用户栈)中,初始化为0,占用4B大小,它没有标识符,也不需要被声明。
(2)字符串
正在上传…重新上传取消
在hello.s中,字符串的信息都被放在了.rodata节中。如下图:LC0为默认输出,第一个字符串.LC0包含汉字,每个汉字被编码为三个字节,LC1为argc=4时的输出
(3)指针数组
正在上传…重新上传取消
argv[]数组存放我们在命令行中输入的字符串。hello.s的argv的首地址被存放在寄存器%rsi中,后来被存放在栈中,在L4中可以看到-32(%rbp)中的首地址被再次调用,如上图所示
3.3.2赋值与操作
(1)赋值
汇编程序的赋值通过mov语句实现,如下图对i赋值为零。
正在上传…重新上传取消
(2)算术计算
算术计算一般通过add,sub,imul,inc语句实现,而移位操作可以通过SAL、SAL等语句实现(在hello中没有体现),如下图执行i++,
正在上传…重新上传取消
(3)关系操作
main函数中需要判断argc是否为4,而argc作为第一个参数应该存放在%edi中,且结合前面的分析可知它又被存入-20(%rbp)中,如下图,程序判断argc是否为4,若相等,就跳转到.L2。
正在上传…重新上传取消
(4)数组/指针/结构操作
argv[0]指向输入程序的路径和名称,argv[1]和argv[2]分别指向我们从shell终端输入的字符串。如下图所示,通过语句addq $16,%rax以及addq $8,%rax分别得到argv[1]和argv[2]。
正在上传…重新上传取消
3.3.4控制转移
在main函数中存在if(argc!=4)这样的语句,而在汇编程序中它通过控制转移实现,如下图
正在上传…重新上传取消
3.3.5函数调用
函数通过跳转到特定代码执行待定函数之后再返回来实现功能。函数一般是在栈中实现的,函数调用可分为如下过程:
1,传递参数给被调用者
在64位栈结构中按照:%rdi,%rsi,%rdx,%rcx,%r8,%r9的顺序传递参数,从第七个参数开始放在调用者栈结构中。
2,调用函数
call指令会将返回地址压入栈中,并且将rip的值指向所调用函数的地址,等函数执行完之后调用ret恢复栈帧。
3,函数进行操作
函数在自己栈帧内进行操作,返回值存入rax中。
4,函数返回
函数返回时,如果有返回值,则先将返回值存在%rax中,再返回调用函数。
hello.c文件中调用的函数有:main()、printf()、exit()、sleep()、atoi()、getchar()。
(1)main函数
参数传递:第一个参数是argc(int型),第二个参数是argv[](char *型),分别存放在寄存器%rdi和%rsi中;
调用:main函数被系统函数__libc_start_main调用,call指令将main函数的地址分配给%rip,随后调用main函数。
函数操作中的栈维护:main函数使用栈指针,同时使用栈帧%rbp来记录使用情况。如图main函数在进入时先将rsp减去32形成一个栈空间结构,然后开始进行各种操作。
正在上传…重新上传取消
返回:可以看到main函数的尾部将0压入到eax中,然后调用了leav平衡栈帧,调用ret返回退出。
正在上传…重新上传取消
(2):printf函数
参数传递:call puts时只传入了字符串参数首地址;for循环中call printf时传入了 argv[1]和argc[2]的地址;
正在上传…重新上传取消
调用:第一个printf()由call puts@PLT调用,第二个printf()由call printf@PLT调用;
返回:从printf中返回。
(3):exit函数
参数传递:将1传给了%edi,完成参数传递。
正在上传…重新上传取消
调用:通过call exit@PLT函数,进行函数调用。
返回:从exit返回。
(4):sleep函数
正在上传…重新上传取消
参数传递:将atoi的返回值%eax通过%rdi传递给sleep函数
调用:调用了sleep函数,将控制传送。
返回:从sleep中返回。
(5):atoi函数
参数传递:将argv[3](字符串)通过%rdi传递给atoi函数。
调用:通过call atoi@PLT函数,进行函数调用。
返回:从atoi中返回。
正在上传…重新上传取消
(6):getchar函数
参数传递:无;
正在上传…重新上传取消
调用:call getchar@PLT调用getchar;
返回:从getchar中返回
3.4 本章小结
本章介绍了编译器如何用汇编语言实现对各种数据类型和操作的处理,以及hello中各种函数在汇编程序中的实现。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是指as将汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中。
作用:将汇编语言的程序翻译生成机器语言(二进制文件)。
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
正在上传…重新上传取消
正在上传…重新上传取消
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF格式:如图所示:
正在上传…重新上传取消
用readelf等列出其各节的基本信息:如图所示:
正在上传…重新上传取消
正在上传…重新上传取消
4.3.1:ELF头
ELF头部以一个16字节的序列开始,描述生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器分析语法和解释目标文件的信息,其中包含ELF头大小、目标文件的类型、及其类型、节头部表的文件偏移,以及节头部表中条目的大小和数量,如下图所示。
正在上传…重新上传取消
4.3.2:节头部表
节头部表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等,如下图所示:
正在上传…重新上传取消
4.3.3:重定位条目
当汇编器生成一个目标模块是,它并不知道数据和代码最终将放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行目标文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。
正在上传…重新上传取消
4.3.4:符号表
.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
正在上传…重新上传取消
4.4 Hello.o的结果解析
如图:
正在上传…重新上传取消
和hello.s对比,可以发现以下几点:
(1)操作数:
hello.s中的操作数时十进制,hello.o反汇编代码中的操作数是十六进制。
(2)分支转移:
跳转语句之后,hello.s中是.L2和.L3等段名称,而反汇编代码中跳转指令之后是相对偏移的地址。
(3)函数调用:
hello.s中,call指令之后直接是函数名称,而反汇编代码中call指令之后是函数的相对偏移地址。
(4)指令:
汇编中mov、push、sub等指令都有表示操作数大小的后缀,反汇编得到的代码中则没有。
4.5 本章小结
本章介绍了汇编的概念和作用,ELF的格式,并且比较了反汇编程序和汇编程序的异同。
(第4章1分)
第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
正在上传…重新上传取消
正在上传…重新上传取消
5.3 可执行目标文件hello的格式
使用指令readelf -a hello > hello1.elf生成hello的ELF格式文件,如下图所示
正在上传…重新上传取消
5.3.1.ELF头:
如下图所示,ELF头描述文件的总体格式,还包括程序的入口点。
正在上传…重新上传取消
5.3.2.节头:
Section Headers对hello中所有的节信息进行了声明,其中包括大小Size以及在程序中的偏移量Offset,因此根据Section Headers中的信息我们就可以用HexEdit定位各个节所占的区间(起始位置,大小)。其中Address是程序被载入到虚拟地址的起始地址。如下图所示
正在上传…重新上传取消
5.3.3.程序头:
如下图所示,Program Headers一共有8个段
(1)PHDR包含程序头表本身
(2)INTERP:只包含了一个section,在这个节中,包含了动态链接过程中所使用的解释器路径和名称。
(3)两个LOAD:第一个是代码段,第二个是数据段。在程序运行时需要映射到虚拟空间地址。
(4)DYNAMIC:保存了由动态链接器使用的信息。
(5)NOTE: 保存了辅助信息。
(6)GNU_STACK:堆栈段。
(7)GNU_RELRO:在重定位之后哪些内存区域需要设置只读。
正在上传…重新上传取消
5.3.4.段节:
正在上传…重新上传取消
5.3.5.重定位节:
正在上传…重新上传取消
5.4 hello的虚拟地址空间
使用edb加载hello,如图所示:
正在上传…重新上传取消
可根据上述的程序头来查看各段虚拟空间的映射。
5.5 链接的重定位过程分析
5.5.1hello与hello.o的反汇编代码比较:
运用指令objdump -d -r hello > hello-asm.txt得到hello的反汇编文件。
正在上传…重新上传取消
得到如下几方面的不同:
- 链接后函数数量增加。多出了puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。
- 函数调用指令call的参数发生变化。
- 跳转指令参数发生变化。
5.6 hello的执行流程
正在上传…重新上传取消
5.7 Hello的动态链接分析
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。因而,我们为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。
为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在节头部表中我们可以看到,.got.plt的起始地址为0x404000。
正在上传…重新上传取消
在edb中我们找到该位置:
正在上传…重新上传取消
可以发现在dl_init未执行时,0x404008往后的16个字节均为0,在dl_init执行后如下图:
正在上传…重新上传取消
这里多了两个地址,分别为0x7ff339ccc190和0x7ff339af5cc0,这里保存的即动态链接后,函数的最终地址。
5.8 本章小结
本章结合实验中的hello可执行程序依此介绍了链接的概念及作用以及命令,并对hello的elf格式进行了详细的分析对比。以及hello的虚拟地址空间知识,并通过反汇编hello文件,将其与hello.o反汇编文件对比,详细了解了重定位过程,遍历了整个hello的执行过程,整理了过程中的子函数,在最后对hello进行了动态链接分析,对链接有了更深的理解。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程就是一个执行中程序的实例.每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程。
作用:进程为用户提供以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个应用程序,他在操作系统中提供了一个用户与系统内核进行交互的界面。他的处理过程一般是这样的:从终端读入输入的命令,将输入字符串切分获得所有的参数,如果是内置命令则立即执行,否则调用相应的程序为其分配子进程并运行,shell应该接受键盘输入信号,并对这些信号进行相应处理。
6.3 Hello的fork进程创建过程
Shell(父进程)通过fork 函数创建一个新的运行的子进程.新的子进程几乎但不完全与父进程相同.子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈.子进程进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork 时,子进程可以读写父进程中打开的任何文件。
6.4 Hello的execve过程
execve 函数加载并运行可执行目标文件filename, 且带参数列表argv 和环境变量列表envp .只有当出现错误时,例如找不到filename, execve 才会返回到调用程序.所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。在hello中,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段.新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容.最后加载器设置PC指向_start地址,_start最终调用main函数.除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制.直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
6.5 Hello的进程执行
在hello进程执行过程中使用上下文切换的方法,如图所示
正在上传…重新上传取消
hello在用户模式下运行,当hello调用sleep后进入内核模式,内核处理休眠主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程继续进行自己的控制逻辑流。
6.6 hello的异常与信号处理
异常可分为四类分别是中断,陷阱,故障和终止
(1)中断处理:中断是异步发生的,是来自处理器外部的 I/O 设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序常常称为中断处理程序。
(2)陷阱处理:陷阱是有意的异常,是执行一条指令后的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
(3)故障处理:故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果故障处理程序能够修正这个错误,它就将控制返回给引起故障的指令,从而重新执行它,否则,处理程序返回到内核中的 abort 例程,abort 例程会终止引起故障的应用程序。
(4)终止处理:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如 DRAM 或者 SRAM 位被损坏时发生的奇偶错误。终止处理程序不会将控制返回给应用程序。
hello进程面对的各种情况:
(1)正常执行。如下图所示,为hello程序正常运行的结果,接着输入命令ps后执行,程序后台并没有hello进程正在执行了,说明进程正常结束,已经被回收了。
./hello 2021112571 邹轩崎 1
正在上传…重新上传取消
(2)不停乱按:结果是程序运行情况和前面的相同,不同之处在于shell将我们刚刚乱输入的字符除了第一个回车按下之前的字符当做getchar的输入之外,其余都当做新的shell命令,在hello进程结束被回收之后,将会在命令行中尝试解释这些命令。中间没有任何对于进程产生影响的信号被产生。
正在上传…重新上传取消
(3)运行CTRL+Z:运行中按CTRL+Z之后,将会发送一个SIGTSTP信号给shell。然后shell将转发给当前执行的前台进程组,使hello进程挂起。此时,我们输入ps命令,查看当前存在的进程,如图所示
正在上传…重新上传取消
输入jobs命令:
正在上传…重新上传取消
输入pstree命令:以树状图显示进程间的关系
正在上传…重新上传取消
使用fg指令完成剩下的执行。
正在上传…重新上传取消
使用kill命令:运行hello程序,将其挂起一次,使用kill函数杀死它。
正在上传…重新上传取消
CTRL+C命令:在hello程序运行时输入CTRL+C会导致内核发送一个SIGINT信号到前台进程组的每个进程。默认情况下,结果是终止前台作业。
正在上传…重新上传取消
6.7本章小结
本章介绍了进程的概念和作用,shell的基本原理,shell如何调用fork和execve进程hello,hello进程在执行时会遇到的各种情况。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序代码经过编译后出现在 汇编程序中地址.逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址.
线性地址&虚拟地址:逻辑地址经过段机制后转化为线性地址(虚拟地址),是逻辑地址到物理地址变换之间的中间层.在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址.是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元.线性地址通常用十六进制数字表示,值得范围从0x00000000到0xfffffff)程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址.如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址.如果没有启用分页机制,那么线性地址直接就是物理地址.
物理地址:CPU地址总线传来的地址,由硬件电路控制(现在这些硬件是可编程的了)其具体含义.物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、BIOS等).在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址.
在hello程序中,他就表示了这个程序运行时的一条确切的指令在内存地址上的具体哪一块进行执行。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符、段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。
索引号,是“段描述符(segment descriptor)”,段描述符具体地址描述了一个段。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,由8个字节组成。
Base字段:它描述了一个段的开始位置的线性地址。 Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。
当段选择符中的T1字段=0,表示用GDT;若为1,表示用LDT。 GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
(1)看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
(2)拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
(3)把Base + offset,就是要转换的线性地址了。 还是挺简单的,对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。
7.3 Hello的线性地址到物理地址的变换-页式管理
首先Linux系统有自己的虚拟内存系统,其虚拟内存组织形式如下图,Linux将虚拟内存组织成一些段的集合,段之外的虚拟内存不存在因此不需要记录。内核为hello进程维护一个段的任务结构即图中的task_struct,其中条目mm指向一个mm_struct,它描述了虚拟内存的当前状态,pgd指向第一级页表的基地址(结合一个进程一串页表),mmap指向一个vm_area_struct的链表,一个链表条目对应一个段,所以链表相连指出了hello进程虚拟内存中的所有段。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:
TLB支持下的VA到PA的变换能够利用局部性原理,加快地址翻译速度。
TLB即翻译后备缓冲器,是一个具有如下性质的缓存:
1. 分页内存管理单元中一个小的具有高相联度的集合。
2. 实现虚拟页号到物理页号的映射。
3. 页数很小的页表可以完全存放在TLB中。
如果缓存在TLB中的话,那么TLB可以直接将VPN映射到PTE,以上步骤都是在CPU内部的MMU单元完成的,因而速度较快。
四级页表:
四级页表支持下的VA到PA的变换能够利用多级页表,降低内存占用。
如果我们的页面大小4KB,48位地址空间,8字节的PTE,那么我们的页表占用的空间至少应该是512GB,这显然不现实。我们发现其实有很多页我们并没有用上,于是可以不放入PTE里面,而采用多级页表的处理思路,如下图:
正在上传…重新上传取消
由CR3寄存器指向L1的PT,然后由VPN1作为索引,进行寻找。找到PTE以后,以PTE条目里面的Base作为基址,再以VPN2作为索引,重复上述操作,直到找到L4的PT里面的PTE。以这个作为PPN,并上PPO,作为虚拟地址。
7.5 三级Cache支持下的物理内存访问
缓存的出现是为了缓解存储设备和CPU之间巨大的速度差异。处理器对内存数据的访问,一般是通过cache进行。具体过程为:
通过地址解析出缓存的索引和偏移,对缓存进行访问,匹配标记查找是否含有相关的字,如果命中,则将数据发送给CPU,如果没有命中,则访问下一级缓存,取出这个字,存入高一级缓存,返回数据给CPU。
在物理内存访问的过程中,这个过程被执行为:
对于给定的物理地址PA,将PA分割成CT CI CO。其中CI是组号,定位了应该出现的组,CO是offset定位了偏移量,CT是tag即标识的tag。
首先对本层的Cache进行寻找(从L1开始)。硬件会定位到CI的组索引,然后对组里面的每一行进行比较tag。如果tag相同且有效,那么找到了。
如果找到,就直接返回数据。如果找不到,就会到L2里面进行寻找,重复上述操作直到找到为止。
7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,也就为每个进程保持了私有地址空间的抽象概念。
正在上传…重新上传取消
7.7 hello进程execve时的内存映射
execve 函数在shell中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。.bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页:引用虚拟内存中的字,不在物理内存中,DRAM 缓存不命中。
缺页时的执行流程如下:
首先,处理器生成一个虚拟地址,并将其传送给MMU。MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到PTE。高速缓存/主存向MMU返回PTE,PTE的有效位为零, 因此 MMU 触发缺页异常。
缺页处理程序确定物理内存中的牺牲页 (若页面被修改,则换出到磁盘),缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,再次执行导致缺页的指令。
7.9动态存储分配管理
动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将堆视为一组大小不同的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是以分配的,要么是空闲的。已分配的块显式的保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格,两种风格都要求应用显式的分配快。它们的不同之处在于由哪个实体来负责释放已分配的快。
显式分配器:要求应用显式地释放任何已分配的块。例如C标准库提供一种叫malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。
隐式分配器:另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫垃圾收集器,而自动释放未使用的已分配块的过程叫做垃圾收集。
7.10本章小结
本章介绍了hello程序的存储管理,包括各类地址空间的转换,段式管理和页式管理等,介绍了VA到PA的转换,物理内存访问。以及调用fork函数和execve函数时的内存映射,发生缺页故障后的处理方法和动态储存的分配管理方法。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
文件是Linux管理的基本思想,所有的IO设备都被抽象为文件,所有的输入输出操作都作为对文件的操作。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得输入和输出都能以一种统一且一致的方式的来执行。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K 。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k 。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
1. int open(char* filename,int flags,mode_t mode),进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
2. int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
3. ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
4. ssize_t write(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
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;
}
在形参列表中的...表示传递参数个数不确定,va_list的定义为 typedef char *va_list是一个字符型指针,(char*)(&fmt) + 4) 表示的是...中的第一个参数。
此后,程序调用vsprintf函数,vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,并返回字符串长度。
之后,程序调用write函数,将缓冲区中的前i个字符输出到屏幕上,我们对write进行反汇编:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
Write将参数传如寄存器,然后进入一个陷阱-系统调用。sys_call将串里面的字节,从寄存器里面通过总线,复制到显卡显存里面,存放ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。最后我们的打印字符串就显示在了屏幕上。
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从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符显示到屏幕。如果用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的I/O设备的基本概念和管理方法,以及Unix I/O接口及其相关函数,并且分析了printf函数和getchar函数的工作过程。
(第8章1分)
结论
- hello程序是一个用C语言编写的简单程序,它的源代码是一个文本文件,包含了一条#include <stdio.h>指令和一个main函数,main函数中调用了printf函数来输出"Hello World\\n"字符串。
-为了在计算机系统上运行hello程序,它必须经过预处理、编译、汇编和链接的四个阶段,将源代码转换为可执行目标文件,即一系列的机器语言指令和相关数据。
-预处理阶段:预处理器cpp根据以#开头的指令,修改原始的C程序,将stdio.h文件中的内容插入到源代码中,生成一个新的C程序文件hello.i。
- 编译阶段:编译器ccl将hello.i文件翻译成汇编语言文件hello.s,它包含了一条条用文本格式描述的低级机器语言指令。
- 汇编阶段:汇编器as将hello.s文件翻译成机器语言指令,并打包成一种叫做可重定位目标程序的格式,保存在二进制文件hello.o中。
- 链接阶段:链接器ld将hello.o文件和printf.o文件(包含了printf函数的实现)合并在一起,解决了它们之间的引用关系,生成了一个可执行目标文件hello。
- 运行阶段:当用户在终端输入./hello命令时,外壳程序shell通过系统调用将控制权交给操作系统,操作系统创建一个新的进程来执行hello程序,并将其代码和数据从磁盘复制到主存中¹²。然后处理器开始执行hello程序的main函数中的机器语言指令,这些指令将"Hello World\\n"字符串从主存复制到寄存器文件,再从寄存器文件复制到显示设备(通过I/O总线和控制器),最终显示在屏幕上¹²。当程序执行完毕后,操作系统回收进程资源,并将控制权返回给shell²。
- 我对计算机系统的设计与实现有以下几点深切感悟:
- 计算机系统是由硬件和软件组成的复杂而有趣的系统,它们共同协作来运行各种应用程序。作为程序员,我们需要了解计算机系统的基本原理和组成部分,以及它们如何影响程序的正确性和性能。
- 计算机系统中所有的信息都是由位+上下文构成的。不同类型的数据(如整数、浮点数、字符串、指令等)都是由一串位表示的,只有根据上下文才能正确地解释它们。因此,我们需要了解不同数据类型的机器表示方式,以及它们与真实值之间可能存在的差异和限制。
- 计算机系统中有许多抽象层次,每个层次都提供了一些基本功能和服务,并隐藏了底层实现细节。这些抽象层次使得我们可以更容易地理解和设计复杂的系统,并提高了系统的可移植性和效率。例如,在编译过程中,C语言是一种高级语言抽象,它提供了比汇编语言更方便和强大的编程工具;在运行过程中,进程是一种虚拟处理器抽象,它提供了每个程序独占处理器和主存的假象。
- 我对计算机系统有以下几点创新理念:
- 利用人工智能技术来优化编译过程,使得编译器能够根据不同场景和需求自动选择最合适的优化策略和参数,提高程序的运行效率和质量。
- 利用区块链技术来保证计算机系统的安全性和可靠性,使得系统中的数据和操作都能够被加密、验证和记录,防止篡改、伪造和攻击。
- 利用云计算技术来扩展计算机系统的功能和服务,使得用户可以随时随地地访问和使用各种计算资源和应用程序,无需担心硬件配置、软件更新和数据备份等问题。
附件
hello: 链接之后的可执行目标文件
hello.c: 源文件
hello.elf: hello的ELF格式
hello.i: 预处理产生的文本文件
hello.o: 汇编产生的可重定位目标文件
hello.objdmp: hello的反汇编代码
hello.s: 编译产生的汇编文件
hello.txt: hello.o的ELF格式
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] BryantO’Hallaron, D.R.R.E.,. (2019). 深入理解计算机系统(第三版). 北京: 机械工业出版社.
[2] https://blog.csdn.net/alanwalker1/article/details/103848576
[3] 百度安全验证
[4] exit(0)在c语言中是什么意思-C#.Net教程-PHP中文网