csapp大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业     计算学部                  

学     号       1190200720                

班     级         1903008              

学       生        牛瀚尘             

指 导 教 师        吴锐               

计算机科学与技术学院

2021年5月

摘  要

    本文从计算机系统底层的角度出发,运用本学期学习的计算机系统知识,以程序hello.c为对象描述了计算机程序的从c文件代码到执行最后结束的过程,及From Program to Process和From Zero-0 to Zero-0的过程。详细解释了过程中的关键概念,系统的总结了计算机系统的知识内容。

关键词:预处理,编译,汇编,链接,虚拟内存,进程管理,I/O管理                              

(摘要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 -

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

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

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

第1章 概述

1.1 Hello简介

       hello.c经过预处理后插入相关内容后生成hello.i,hello.i经过编译生成相应的汇编程序hello.s,hello.s经过汇编被翻译为机器语言,生成一个可重定位的目标程序hello.o,然后对hello.o进行链接,生成一个可执行目标文件hello。在shell中运行hello.out,通过fork创建它的进程,exceve加载上下文开始运行hello程序,为它分配相应的内存资源,包括CPU的使用权限和虚拟内存等,在这个过程中运用到了虚拟内存这一重要的重要概念,运行中还要对信号进行响应。在hello进程中,还要通过I/O链接外设,最终显示相应内容。

当进程执行结束后,操作系统进行进程回收,实现所谓的O2O:From Zero-0 to Zero-0。

1.2 环境与工具

1.2.1 硬件环境

X64 CPU;2.8GHz;16RAM; 256Gssd

1.2.2 软件环境

Windows10 64位;vm virtualbox;ubuntu 16.04;

1.2.3 开发工具

vscode;CodeBlocks;vi/vim/gpedit+gcc;edb-debugger

1.3 中间结果

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

文件名称

文件作用

hello.i

预处理之后文本文件

hello.s

编译之后的汇编文件

hello.o

汇编之后的可重定位目标执行

hello

链接之后的可执行目标文件

hello1.elf

hello.o的ELF格式

hello.elf

hello的ELF格式

1.4 本章小结

hello程序从c代码到运行,中间经历预处理,编译,汇编,链接,进程创建等等.内容,这些内容是计算机系统的核心知识,研究明白hello从诞生到结束的整个过程是对计算机系统的一次小型实践,更能加深对其的理解。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:

预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作,预处理器(cpp) 根据以字符#开头的命令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等,修改原始的C 程序,得到另一个c程序,通常以.i为文件扩展名。

作用:

做些代码文本的替换工作。处理#开头的指令,预编译过程主要处理那些源代码中以#开始的预编译指令,主要处理规则如下:

1)将所有的#define删除,并且展开所有的宏定义;

2)处理所有条件编译指令,如#if,#ifdef等;

3)处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。该过程递归进行,及被包含的文件可能还包含其他文件。

4)删除所有的注释//和 /**/;

5)添加行号和文件标识,如#2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号信息;

6)保留所有的#pragma编译器指令,因为编译器须要使用它们;

2.2在Ubuntu下预处理的命令

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

2.3 Hello的预处理结果解析

预处理后,删除了所有的备注信息,读取了系统头文件,并将它们插入了程序文本。

总行数大大增加,但代码段并没有被更改。

2.4 本章小结

预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的。程序员在程序中用预处理命令来调用这些功能。文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。掌握预处理相关知识对程序员编写程序有帮助。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

定义:

     将某一种程序设计语言写的程序翻译成等价的另一种语言的程序的程序, 称之为编译程序(compiler) .这里是指编译器(ccl)将预处理后的.i文件翻译为.s汇编语言程序的过程。

作用:

      把预处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件。通过编译,把高级语言文件翻译为机器级的文件表示,使之在后续过程中最终生成机器码,最终生成可被计算机执行的文件。

3.2 在Ubuntu下编译的命令

编译命令:gcc –S hello.c –o hello.s 

3.3 Hello的编译结果解析

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析

3.3.1数据

       1)常量:常量4在编译文件中以立即数形式出现。

       2)变量:变量i存放于-4(%rbp)中。

       3)参数:main函数参数argv由寄存器%edi存放于栈-20(%rbp)中;argc数组的头指针由寄存器%rsi存放于栈中,即存放-32(%rbp)。

3.3.2 字符串

字符串有两个:

1)“用法: Hello 学号 姓名 秒数!\n”编码如下

2)另一个字符串“"Hello %s %s\n”存放如下:

3.3.3赋值

if语句i赋值i=0,使用mov指令,由于i为int类型,因此使用的是movl, i存放于-4(%rbp)中。

3.3.4类型转换

sleep(atoi(argv[3]));处将char*类型arg[3]强制转化为了int型。

3.3.5算术操作

for循环过程中每次循环的i++,使用addl实现。

 

3.3.6关系操作

1)条件语句中argv!=4的比较。使用了cmpl(int型)指令和je指令(相等则跳转)。

 

2)循环内每次都有i<8的比较。使用cmpl和jle(小于等于则跳转),并把i<8变成了i<=7。

 

3.3.7数组操作

对数组argv中元素的引用。

由于char*类型即指针类型的大小是8字节(64位环境下),所以每个位置为头地址+8即可。argv数组头指针头指针存储于-32(%rsp)。

3.3.8控制转移

1)if语句。用cmpl指令比较argc与4,若不等,je指令会使得程序跳转到.L2处,否则继续进行。

2)for循环语句。首先mov指令给i赋初值0,然后跳转到.L3部分。L3部分先比较7与i,通过jle语句可知,若i<=7,程序跳转到.L4继续,否则调用函数getchar。

3.3.9函数操作

1)Sleep函数与atoi函数

atoi函数的参数为argv[3], argv[3]值开始存储于%rax中,寄存器%rdi为函数的第一个参数,将%rax赋值给%rdi,sleep函数参数为atoi函数的返回值,返回值由%rax存储,当atoi返回时,要将%rax再次赋值给%rdi,再用call指令调用sleep函数。

 

2)main函数

传入参数为argv和argc,它们在传入时分别存储与寄存器%rdi和%rsi中

 

最后的返回值为0,所以在最后使用mov指令将0赋给了%eax,然后使用leave,ret指令退出main函数。

 

3)printf函数

输出"用法: Hello 学号 姓名 秒数!\n"使用的是puts,传入参数为字符串的首地址。

输出printf("Hello %s %s\n",argv[1],argv[2]);时,传入参数有argv[1],argv[2]及字符串"Hello %s %s\n"。

4)exit函数

传入参数为1,使用call指令调用。

3.4 本章小结

通过编译,编译器(ccl)将.i文件翻译成.s汇编程序,作为一种低级的机器语言,汇编语言是非常有用的,因为它为不同的高级语言,不同的编译器提供了通用的输出语言。汇编语言本身是面向寄存器的操作,较高级语言来说编写和理解都较困难,且作为一种机器及语言,汇编语言直接与机器相关,通过编译,为程序员提供了方便,让高级语言代码具有可移植性。编译时,编译器会根据定义变量的类型对数据的定义赋值和存放,对循环和分支语句进行合理的划分,对程序分段以便进行跳转。进行一定的优化提高程序执行的效率。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫可做重定位目标程序(relocatable object program)的文件格式,并将结果保存在目标文件hello.o中

作用:汇编语言的指令与机器语言的指令大体上保持一一对应的关系,汇编算法采用的基本策略通常采用两遍扫描源程序的算法。第一遍扫描源程序根据符号的定义和使用,收集符号的有关信息到符号表中;第二遍利用第一遍收集的符号信息,将源程序中的符号化指令逐条翻译为相应的机器指令。

4.2 在Ubuntu下汇编的命令

gcc hello.s –c –o hello.o

4.3 可重定位目标elf格式

Elf头

节表头

重定位节

符号表

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

通过-S参数,可以看到节头部表的相关信息,节头部表中描述了该ELF可重定位文件各节的信息。

通过-r参数可以看到重定位节的信息,包括偏移量,信息,类型符号值和符号名称等信息。

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

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o

机器级语言的构成:机器语言是一种指令集体系结构或指令集架构,是电脑真正可解读的语言,机器指令由操作码和操作数组成。

汇编代码的表示非常接近于机器代码,与机器代码的二进制格式相比,汇编代码具有更好的可读性。

机器语言与汇编语言大体上保持一一对应的映射关系,一条机器语言程序对应一条汇编语言语句,与硬件有关。

在hello.s中,操作数都以十进制的格式表示,而在hello.o中,操作数都是十六进制的。hello.o反汇编中循环分支的跳转变为了相对地址偏移,函数调用也变成了相对地址偏移。

4.5 本章小结

汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫可做重定位目标程序(relocatable object program)的文件格式,并将结果保存在目标文件hello.o中。

ELF可重定位目标文件包含了ELF头,节头部表和多个节,其中包含了重定位需要的信息。汇编语与指令与机器指令一一对应,相比二进制机器指令具有更好的可读性。机器指令与硬件相关,不同的硬件指令集体系架构可能是不同的

(第41分)

第5章 链接

5.1 链接的概念与作用

概念:链接(linking)是将各种代码和数据片段收集并组合成为单一文件的过程,这个文件可被加载到内存执行。

作用:为了生成可执行文件链接器主要完成符号解析和重定位两个任务。符号解析将每个符号引用和一个符号的定义关联起来。重定位通过把每个符号的定义与一个内存位置关联起来从而重定位这些节,然后修改对这些符号的引用,使得它们指向这个内存位置。

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 > hello.elf 命令生成hello程序的ELF格式文件。在ELF格式文件中,Section Headers对hello中所有的节信息进行了声明,其中包括大小Size以及在程序中的偏移量Offset,因此根据其中的信息我们就可以找到各个节所占的区间(起始位置,大小)。

节头声明了包括大小以及在程序中的偏移量,根据这些信息我们就可以用 HexEdit 定位各个节所占的区间。其中地址是程序被载入到虚拟地址的起始地址。

程序头里包含了虚拟地址

5.4 hello的虚拟地址空间

使用edb的内置插件SymbolViewer查看各个段的位置。对照5.3,不难发现与5.3中各个段的Address一致。

 

5.5 链接的重定位过程分析

以下格式自行编排,编辑时删除

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

hello相对于hello.o的不同:

  • hello.o中是相对偏移地址,hello中是虚拟内存地址

  • hello中相对hello.o增加外部链接的函数

如puts

  • hello相对hello.o多了很多的节如.init,.plt

  • hello.o中跳转以及函数调用的地址在hello中都被更换成了虚拟内存地址。

链接器重定位过程:

①  增加函数:在使用 ld 命令链接的时候,指定了动态链接器为 64 的 /lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o 中主要定义了程序入口_start、初始化函数_init,_start 程序调用 hello.c 中的 main 函数,libc.so 是动态链接共享库, 其中定义了 hello.c 中用到的 printf、sleep、getchar、exit 函数和_start 中调用的 __libc_csu_init,__libc_csu_fini,__libc_start_main。链接器将上述函数加入。 

②  函数调用:链接器解析重定条目时发现对外部函数调用的类型为 R_X86_64_PLT32 的重定位,此时动态链接库中的函数已经加入到了 PLT 中,.text 与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为 PLT 中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt 与.got.plt。 

③  .rodata 引用:链接器解析重定条目时发现两个类型为 R_X86_64_PC32 的 对.rodata 的重定位(printf 中的两个字符串),.rodata 与.text 节之间的相对距离确定,因此链接器直接修改 call 之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。

5.6 hello的执行流程

ld-2.31.so!_dl_start

ld-2.31.so!_dl_init

hello!_start

libc-2.31.so!__libc_start_main

libc-2.31.so!__cxa_atexit

libc-2.31.so!__libc_csu_init

libc-2.31.so!_setjmp

hello!main

hello!puts@plt

hello!exit@plt

hello!printf@plt

hello!sleep@plt

hello!getchar@plt

ld-2.31.so!_dl_runtime_resolve_xsave

ld-2.31.so!_dl_fixup

ld-2.31.so!_dl_lookup_symbol_x

libc-2.31.so!exit

5.7 Hello的动态链接分析

在调用共享库内的函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块可以加载到任何位置。GNU采取延迟绑定的方法,将过程地址的绑定推迟到第一次调用该程序。其主要依据的原理代码段中指令和数据段中任何变量距离都已是一个运行时常量,与代码段和数据段的绝对内存位置无关。

延迟绑定通过两个数据结构 PLT和GOT来实现的。

PLT是一个数组,每个条目16字节代码,PLT[0]是一个特殊的条目,挑战到动态链接器中。PLT[1]调用系统启动函数,PLT[2]开始调用用户代码调用的函数。

GOT:是一个数组,每个条目8个字节地址,和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会用的信息。GOT[2]时动态链接器在ld-linux.so模块中入口点。其余对应一个被调用函数。初始时,每个GOT指向对应的PLT条目第二条指令。

GOT和PLT如何协同工作,在addvec被第一次调用时,延迟解析它的运行时地址:
.第1步。不直接调用addvec, 程序调用进人PLT[2], 这是addvec的PLT条目。

.第2步。第一条PLT指令通过GOT[4]进行间接跳转。因为每个GOT条目初始时都指向它对应的PLT条目的第二条指令,这个间接跳转只是简单地把控制传送回PLT[2]中的下一条指令。
.第3步。在把addvec的ID(0x1)压人栈中之后,PLT[2]跳转到PLT[O].
.第4步。PLT(O]通过GOT(1)间接地把动态链接器的一个参数压人栈中,然后通GOT(2)间接跳转进动态链接器中。动态链接器使用两个栈条目来确定addvec的运行时位置,用这个地址重写GOT14],再把控制传递给addvec.

后续再调用addvec时的控制流:

.第1步,和前面一样,控制传递到PLT[21。
.第2步。不过这次通过GOT(4]的间接跳转会将控制直接转移到addvec.

通过hello.elf可以看到.got地址0x6008c0

图5.7.1

查看此处的信息看到全为0

图 5.7.2

图5.7.3 没有调用dl_init之前的全局偏移量表.got.plt

图 5.7.4  调用dl_init

在调用dl_init之后

图5.7.5 调用dl_init之后的全局偏移量表.got.plt

看到got[2]有了内容,进入这个地址

图 5.7.6

发现是动态链接库的入口

5.8 本章小结

本章讨论了链接过程中对程序的处理。Linux系统使用可执行可链接格式,即ELF,具有.text,.rodata等节,并且通过特定的结构组织。

经过链接,ELF可重定位的目标文件变成可执行的目标文件,链接器会将静态库代码写入程序中,以及动态库调用的相关信息,并且将地址进行重定位,从而保证寻址的正确进行。静态库直接写入代码即可,而动态链接过程相对复杂一些,涉及共享库的寻址。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:

      一个执行中程序的经典实例。

进程的作用:

1.一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器;

2.一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统。

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

作用:

在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器)。它接收用户命令,然后调用相应的应用程序

处理流程:

首先是调用parseline函数,这个函数解析了以空格分隔的命令行参数,并构造最终会传递给execve的argv向量。第一个参数被假设为要么是一个内置的shell命令名,马上就会解释这个命令,要么是一个可执行目标文件,会在一个新的子进程的上下文中加载并运行这个文件。

如果最后一个参数是一个“&”字符,那么parseline返回1.表示应该在后台执行该程序(shell不会等待它完成)。否则,它返回0,表示应该在前台执行这个程序(shell会等待它完成)。

在解析了命令行之后,eval 函数调用builtin _command函数,该函数检查第-个命令行参数是否是个内置的 shell命令。如果是,它就立即解释这个命令,并返回值1.否则返回0。简单的shell只有以个内置命令  -quit命令,该命令会终止shell. 实际使用的shell有大量命令。

简单概括为:

1.读取用户输入的命令

2.解析命令,将命令行输入内容按照空格等处理为若干参数

3.判断是否为内置命令,是则执行,否则会在一个新的子进程的上下文中加载并运行这个文件

6.3 Hello的fork进程创建过程

Shell通过调用fork函数创建hello进程,作为shell的一个子进程,hello几乎但不完全与父进程相同。子进程得到与父进程用户级店拟地址空间相同的(但是独立的)份副本, 包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID.

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件filename(这里应当是hello),且带参数列表argv和环境变量列表envp.只有当出现错误时,例如找不到filename, execve 才会返回到调用程序。所以,与fork一次调用返回两次不同,execve 调用一次并从不返回。

在exeve加载了filename之后,它调用7.9节中描述的启动代码。启动代码设栈,并将控制传递给新程序的主函数。

6.5 Hello的进程执行

Linux 系统中的每个程序都运行在一个进程上下文中,有自己的虚拟地址空间。当hello运行时,分时操作系统分配给每个正在运行的进程微观上的一段CPU时间,也包括hello,只有在hello的时间片中时,hello是在使用CPU的。在调用 sleep 之前,如果 hello 程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换。

上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行 :

1)保存以前进程的上下文 ;

2)恢复新恢复进程被保存的上下文;

3)将控制传递给这个新恢复的进程 ;

来完成上下文切换。

 

hello 初始运行在用户模式,由于sleep是系统函数,在 hello 进程调用 sleep 之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将 hello 进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时发送一个中断信号,此时进入内核状态执行中断处理,将 hello 进程从等待队列中移出重新加入到运行队列,成为就绪状态,等重新分配到CPU时间片,hello 进程就可以继续进行自己的控制逻辑流了。

6.6 hello的异常与信号处理

以下格式自行编排,编辑时删除

 hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

1.回车

回车键并不影响程序的运行。

 

2.Ctrl-C

程序接收到SIGSTOP信号,进程终止。

 

3.Ctrl-Z

程序收到SIGINT信号,进程暂停。

 

4.Ctrl-z后运行ps

使用ps可以查看进程和对应的PID。

 

如图,hello的pid为7687

5.Ctrl-z后运行jobs 

使用jobs可以查看到被停止的hello进程。

 

6.Ctrl-z后运行pstree 

hello在其中的一个分支上: systemd-gnome-terminal-bash-hello

 

 

7.Ctrl-z后运行fg 

运行fg后hello再次开始运行。

 

8.Ctrl-z后运行kill

运行kill 7687即hello被杀死。

 

6.7本章小结

       本章主要内容是异常控制流和信号。介绍了进程的定义与作用,Shell 的一般处理流程,调用 fork 创建新进程,调用 execve 执行 hello,hello 的进程执行,实践了hello 的异常与信号处理,增加了对异常处理函数的理解,对进程有了更深的认识。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:

      逻辑地址(LogicalAddress)是指由程序产生的与段相关的偏移地址部分。在hello中体现为当前进程数据段的地址(偏移地址)

线性地址:

      地址空间(address space) 是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间(linear address space) 。

虚拟地址:

      在一个带虚拟内存的系统中,CPU从一个有N=2n个地址的地址空间中生成虚拟地址。对hello而言它有自己的一个独立的虚拟地址空间。

物理地址:

      计算机系统的主存被组织为一个有M个连续的字节大小的单元组成的数组。每个字节都有唯一的物理地址(Physical Address).在hello中就是运行时具体操作的主存的地址。。

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

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

 

索引号,可以理解为数组的下标——而它将会对应一个数组,它又是什么的索引呢?这就是“段描述符(segment descriptor)”,段描述符具体地址描述了一个段(对于“段”这个字眼的理解:我们可以理解为把虚拟内存分为一个一个的段。比如一个存储器有1024个字节,可以把它分成4段,每段有256个字节)。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。

GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

概括起来说转换步骤为:

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

2、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。

3、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

4、把Base + offset,就是要转换的线性地址了。

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

系统将每个段分割为被称为虚拟页(VP)的大小固定的块来作为进行数据传 输的单元,在 linux 下每个虚拟页大小为 4KB,类似地,物理内存也被分割为物理页(PP/页帧),虚拟内存系统中 MMU 负责地址翻译,MMU 使用存放在物理内存中的被称为页表的数据结构将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。

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

CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36(4*9bit) 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。 

如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。

如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

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

得到物理地址 VA后,如图 7.8,使用 CI进行组索引,每组 8 路,对 8 路的块分别匹配 CT如果匹配成功且块的 valid 标志位为 1,则命中,根据数据偏移量 CO取出数据并返回。

 如果没有匹配成功或者匹配成功但是标志位是 0,则不命中,向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,根据放置策略进行放置或替换。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:

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

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

3) 映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

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

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

以下格式自行编排,编辑时删除

缺页故障遵循故障处理程序

缺页中断是常见的故障,如下图

缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表(写回策略)。当缺页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令再次发送 VA 到 MMU,这次 MMU 就能正常翻译 VA 了。

7.9动态存储分配管理

   动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为 一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已 分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

策略:

  • 隐式适配:每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。
  • 显式适配:将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
  • 分离适配:与上法基本一致,只是将链表分成了大小不同的组,寻找空闲块的时间大大降低。

方法:

  • 首次适配:第一次遇到的空闲块就直接分配,快速。
  • 最佳适配:找到大小最佳的空闲块才开始分配(够用且最小)。

7.10本章小结

现带计算机为了平衡速度和造价,采用了多级的存储结构,速度越快的存储模块容量往往越小,造价越高。通过多级缓存的想法提高整体的运行效率。虚拟内存现代计算一个重要的概念,它是核心的,强大的也是危险的,它是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个私有的地址空间。内核为每一个进程维护一个页表,虚拟内存和内存映射在将程序加载到内存的过程中也扮演着重要的角色,了解exceve函数如何加载和执行程序很有必要。最后,缺页故障处理需要计算多个部分协作完成,理解好缺页处理对理解计算机系统很重要。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

所有的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 参数指定了新文件的访问权限位。

返回:若成功则为新文件描述符,若出错为-1。

2. int close(int fd);

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

返回:若成功则为0, 若出错则为-1。

3. ssize_t read(int fd, void *buf, size_t n); ssize_t write(int fd, const void *buf, size_t n);

应用程序是通过分别调用read 和write 函数来执行输入和输出的。

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

返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1。

write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出。

返回:若成功则为写的字节数,若出错则为-1。

8.3 printf的实现分析

Printf函数

参数采用了可变参数的定义,可以看到*fmt是一个char 类型的指针,指向字符串的起始位置。

vsprintf函数:

write函数

syscall函数

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

我们的字符串就显示在了屏幕上。

8.4 getchar的实现分析

getchar 函数的实现是调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串。

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。

8.5本章小结

通过I/O,hello走完了最后一关,显示在了屏幕上。通过了解UnixI/O将帮助我们理解其他的系统概念,I/O是系统操作不可或缺的一部分,我们经常遇到I/O和其他系统概念之间的循环以来。学习UnixI/O为之后的学习奠定了基础。

(第81分)

结论

hello从c代码到执行,本文将其划分为预处理、编译、汇编、链接进程管理、储存管理、I/O管理七大阶段。

1.预处理,将hello.c调用的所有外部的库展开合并到一个hello.i文件中

2.编译,将c语言文件转换为汇编语言文件,将hello.i编译成为汇编文件hello.s

3.汇编,将hello.s翻译成机器语言指令,打包成可重定位目标程序格式

4.链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello

5.进程管理,运行hello,创建hello的进程,并相应收到的信号。

6.储存管理,运用虚拟内存机制,给hello独立的地址空间,通过逻辑地址,虚拟地址,物理地址之间的转换实现对内存的访问与管理。

7.I/O管理七大阶段,hello程序要最终在屏幕上显示相应信息还需要通过最后一关I/O与外设交互。

走到这里,hello也完成了使命,帮助一各成长中的程序员学习了计算机系统。麻雀虽小,五脏俱全,hello.c用自己的一生帮助我理解了计算机系统的运行,这种牺牲自我的精神值得我学习,我也不能辜负hello.c的付出,重铸计算机系统荣光,吾辈义不容辞!

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

附件

文件名称

文件作用

hello.i

预处理之后文本文件

hello.s

编译之后的汇编文件

hello.o

汇编之后的可重定位目标执行

hello

链接之后的可执行目标文件

hello1.elf

hello.o的ELF格式

hello.elf

hello的ELF格式

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

参考文献

[1]  printf函数的实现[转]https://www.cnblogs.com/pianist/p/3315801.html

[2]  深入理解计算机系统(原书第三版)/(美)兰德尔.布莱尔特.北京:机械工业出版社,2016.7.

[3] LINUX 逻辑地址、线性地址、物理地址和虚拟地址 转:https://www.cnblogs.com/zengkefu/p/5452792.html

[4]  ELF 构造:https://www.cs.stevens.edu/~jschauma/631/elf.html

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值