ICS大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业   ********************   

学     号        202111****        

班     级         210*****         

学       生           ***         

指 导 教 师            ***            

计算机科学与技术学院

2023年4月

摘  要

本文结合计算机系统课程及《深入理解计算机系统》教材,以hello程序为依托,经过并记录hello程序的一生,即从程序员创建该程序开始,到在系统上运行,到输出信息,到终止的全部过程,其中包括预处理、编译、汇编、链接等阶段,并对相应的进程、存储、I/O进行管理,处理可能的异常情况。通过本文的撰写,可以进一步加深在Linux系统上程序是怎样运行的,以及相关的指令系统、内存存储体系结构、操作系统硬件管理、进程线程与虚拟内存等架构之间的相互联系。

关键词:预处理;编译;汇编;链接;进程管理;存储管理;I/O管理;                            

目  录

第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的P2P(From Program to Process):

Hello的源程序是hello.c,通过预处理器cpp得到修改后的源程序hello.i,通过编译器cc1得到汇编文件hello.s,通过汇编器as得到可重定位目标文件hello.o,最后,通过链接器ld得到可执行目标文件hello。执行程序 “hello”,shell就会通过fork()创建一个子进程,子进程执行execve()在子进程的上下文中加载并执行hello。

Hello的O2O(From Zero-0 to Zero-0):

shell在调用完execve()执行hello程序之后,系统内核就会为hello进程映射一份虚拟内存。在hello进入程序入口之后,hello的相关数据就会被系统内核加载到物理内存中,至此hello便开始了他的时间——hello正式被系统开始运行。为了保证正常执行,系统内核还要为hello做一系列工作,比如分配时间片、分配逻辑控制流等。hello运行结束后,它便成为了zombie进程,之后shell会负责回收它,最后,shell删除了一切和hello相关的内容。

1.2 环境与工具

使用的软件环境:VMware16虚拟机     Ubuntu22.04.01 LTS

硬件环境:AMD Ryzen7 5800H处理器   16 G RAM    512 G HD Disk

开发工具与调试工具:gcc,edb,gdb

1.3 中间结果

hello.c——Hello的源文件

hello.i——Hello的修改了的源程序

hello.s——Hello的汇编文件

hello.o——Hello的可重定位目标文件

hello——Hello的可执行目标文件

elfheader.txt——hello.o的ELF头

secheader.txt——hello.o的节头部表

relahello.txt——hello.o的重定位节

hellosymbol.txt——hello.o的符号表

disassembly.txt——hello.o的反汇编

helloelfhead.txt——hello的ELF 头

hellosection.txt——hello的节头部表

hellodiss.txt——hello的反汇编

hellorela.txt——hello的重定位节

hellosym.txt——hello的符号表

1.4 本章小结

       这一章介绍了hello的P2P和O2O的过程,说明了hello从源程序hello.c变为hello的全过程,展示了进行实验的软硬件环境和开发与调试工具,并列出了本文撰写过程中生成了一系列文件名及其对应功能。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

预处理器(cpp)操作hello.c,根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。

预处理的作用:

预处理能够让编写的程序更便于阅读、修改、移植和调回,同时还有利于模块化程序设计。比如hello.c变成hello.i的过程,cpp把#include<stdio.h>、#include <unistd.h> 、#include <stdlib.h>这三个头文件插入到了程序的文本文件之中,方便编译器cc1进行下一步的操作。

2.2在Ubuntu下预处理的命令

gcc预处理的命令  Linux>gcc hello.c -E -o hello.i

图2.1 利用gcc对hello.c进行预处理

2.3 Hello的预处理结果解析

hello.i的图标

2.2 预处理产生的hello.i

      hello.i的内容

图2.3 hello.i中的部分内容1

图2.4 hello.i中的部分内容2

预处理后生成的hello.i文件与hello.c文件相比,内容多了很多,hello.c中的代码内容在hello.i文件的最后完整保留。C预处理器扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏。

2.4 本章小结

本章介绍了C程序预处理的概念和作用,展现了Linux下利用gcc预处理hello.c的过程,提供了预处理后生成的hello.i文件的部分内容,并分析了.c.i文件内容上的异同。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译的概念:

编译器(cc1)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。该程序包括函数main的定义,如下所示:

1 main:

2    subq       $8, %rsp

3    movl      $.LCO, %edi

4    call        puts

5    movl      $0, %eax

6    addq      $8, %rsp

7    ret

定义中2~7行的每条语句都已一种文本格式描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。例如,C汇编器和Fortran编译器产生的输出文件用的都是一样的汇编语言。

编译的作用:

编译可以将源程序(高级语言C语言)翻译成为优化后的汇编代码(低级语言),在这个阶段还可以对源程序进行语法检查、调试、修改、覆盖、优化等操作,以及一些嵌入式汇编的处理。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

gcc编译的命令  Linux>gcc -S hello.i -o hello.s

图3.1 利用gcc对hello.i进行编译

3.3 Hello的编译结果解析

图3.2 hello.s的内容1

图3.3 hello.s的内容2

3.3.1 汇编指令

       .file(第1行):声明源文件

       .text(第2行):代码节

       .section(第3行):

       .align(第4行):数据或者指令的地址对其方式

       .string(第6行):声明一个字符串(.LC0,.LC1)

       .global(第10行):声明全局变量(main)

       .type(第11行):声明一个符号是数据类型还是函数类型

3.3.2 数据

       本程序用到了字符串常量和变量

       字符串常量:

变量:i 、argc 、argv共三个

(1)i:在循环中用到,为局部变量,保存在堆栈中。

(2)argc:main()函数的参数, 为输入参数个数,为外部参数,保存在寄存器当中。

(3)argv: main()函数的参数,存放各参数,其中argv[0]为该文件路径,为外部参数,保存在寄存器当中。

       其中-4(%rbp)位置位置int i的局部变量,-20(%rbp)位置位置main函数的参数argc,-32(%rbp)位置位置参数argv。

3.3.3 赋值

       对局部变量i的赋值,通过mov指令实现。

3.3.4 算术运算

       局部变量i++,通过addl指令实现。

3.3.5 类型转换

       本文件涉及到的类型转换是atoi,将字符串型转化为整数型。

3.3.6 比较判断

(1)if条件判断,通过cmpl指令实现

(2)for循环的条件判断,通过cmpl指令实现

3.3.7 控制转移

       if条件分支引起的跳转以及for循环分支引起的跳转,通过cmpl指令设置条件码后通过jmp指令跳转。

3.3.8 函数调用

       设置寄存器值后通过call指令调用。

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。

3.4 本章小结

本章主要介绍了编译的概念和作用、在Linux下的命令,以及hello.s文件中各种数据类型和各类操作的处理说明,解释了修改后的源文件hello.i转化成二进制文件hello.s的过程, 并总结了C语言和汇编代码之间指令的对应关系。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

       汇编的概念:

       汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。文件hello.o是一个二进制文件,它包含的17个字节是函数main的指令编码。如果我们在文本编辑器中打开hello.o文件,将看到一推乱码。

       汇编的作用:

       汇编阶段可以生成可重定位目标文件,为下一步与其他程序、动态库、静态库相链接做好准备。

       注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

gcc汇编的命令  Linux>gcc hello.s -c -o hello.o

图4.1 利用gcc汇编生成hello.o

生成的hello.o文件

图4.2 生成的hello.o文件

4.3 可重定位目标elf格式

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

1.ELF header(ELF头)的相关信息

       查看指令 Linux> readelf -h hello.o > elfheader.txt

4.3 ELF头查看指令

相关内容:

图4.4 hello.o文件的ELF头

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。

2.Section Header(节头部表)的相关信息

查看指令 Linux> readelf -S hello.o > secheader.txt

图4.5  节头部表查看指令

相关内容:

图4.6 hello.o的节头部表

节头部表包含了hello.o文件中各个节的名称、类型、地址、偏移量、大小等信息。

3.rela.txt (重定位节)的相关信息

       查看指令 Linux> readelf -r hello.o > relahello.txt

图4.7 重定位节的查看指令

相关内容:

图4.8 hello.o的重定位节

       重定位是指,编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得他们它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。

       我们从中可以得到hello需要重定位的信息有:函数puts,exit,printf,atoi,sleep,getchar和rodata节中的两个字符串

4.symtab (符号表)的相关信息

       查看指令 Linux> readelf -s hello.o > hellosymbol.txt

图4.9 符号表的查看指令

相关内容:

图4.10 hello.o的符号表

       符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。.symtab节中包含ELF符号表,这张表包含一个条目的数组,name是字符串表中的字节偏移,指向符号的以null结尾的字符串名字;value是符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行的地址。size是目标的大小(以字节为单位)。type通常要么是数据,要么是函数。符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目。所以这些目标的类型也有所不同。binding字段表示符号是本地的还是全局的。

4.4 Hello.o的结果解析

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

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

查看指令 Linux> objdump -d -r hello.o > disassembly.txt

图4.11 反汇编的查看指令

相关内容:

图4.12 反汇编内容

机器语言与汇编语言的对应关系:

(1)机器语言是由一串串01代码构成的,并由16进制数来描述。

(2)每一个机器指令序列都包含操作码、操作数等信息,一一对应每一种汇编指令。

反汇编代码和汇编代码在指令格式上非常相似,但在以下几个方面存在着不同:

(1)立即数的引用

       在反汇编中立即数是十六进制的,而汇编代码则是十进制。

(2)子程序的调用

       反汇编代码中子程序的调用是通过对主函数地址的相对偏移进行的,而在汇编代码中则是通过call直接加上函数名的方法进行的。

(3)分支跳转

在反汇编代码中,分支转移是通过跳转到以主函数地址为基址的一个偏移地址中,而在汇编代码中则是通过.L4、.L3这样分块的方式来跳转的。

反汇编代码与汇编代码在指令上是一一对应的关系,只有在一些特殊的指令需要有引用的转化,其他地方几乎完全一致。

4.5 本章小结

本章通过汇编操作,将hello.s转换成hello.o,并分析可重定位目标文件的ELF内的相关内容。同时通过对hello.o的反汇编,分析汇编语言和机器语言之间的对应关系,明确hello.o中的汇编语言和hello.s中的汇编语言的异同点。

(第41分)

5章 链接

5.1 链接的概念与作用

链接的概念:

       链接就是指把各种代码和数据片段收集并组合成为一个文件的过程,这个文件就叫做可执行目标文件,它可以直接被加载到内存中执行。

       请注意,hello程序调用了printf函数,它是每个C编译器都提供的标准C库中的一个函数。函数printf存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。链接器(ld)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。

链接的作用:

       可以使分块编程成为可能,有了链接,程序员不用去编写巨大的程序,同时也方便了程序的维护,更新和优化。

注意:这儿的链接是指从 hello.o 到hello生成过程。

5.2 在Ubuntu下链接的命令

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

链接的指令 Linux> 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 利用gcc链接

图5.2 生成的可执行目标文件的图标

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

5.3.1 可执行目标文件的ELF Header

查看指令 Linux> readelf -a hello > helloelfhead.txt

图5.3 生成ELF头

具体内容:

5.4 ELF头的内容

与hello.o的ELF头相比,可执行目标文件的文件类型改变了,从REL变成了EXEC,入口点地址从0x0变成了0x400550,节头数量从13变成了25。

5.3.2 可执行目标文件的Section Headers

查看指令  Linux> readelf -S hello > hellosection.txt

图5.5 节头部表的查看指令

具体内容:

图5.6节头部表的内容

这里和hello.o不同的地方是,每个节的地址不是0x0,而是变成了根据节自身的大小和对齐规则计算得出的偏移量。

5.3.3 可执行目标文件的重定位节.rela.txt

查看指令 Linux> readelf -r hello > hellorela.txt

图5.7 重定位节的查看指令

具体内容:

图5.8 重定位节的内容

可以看到,相比于hello.o的重定位节,hello的重定位节被分成了两部分,包含动态链接的重定位内容,同时每一个部分都有了准确的偏移量,因而所有的加数都变成了0。

5.3.4 可执行目标文件的符号表

       利用指令 Linux> readelf -s hello > hellosym.txt

图5.9 符号表的查看指令

具体内容:

图5.10 符号表的内容

       这部分相比于hello.o多出了动态链接解析出来的符号,同时其他的符号也多了不少。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。  

使用edb来查看一下虚拟空间的位置:

5.11 虚拟地址空间的位置

可以看到,hello的虚拟是从0x400000开始的,根据hello的节头部表,可以得知.interp的偏移量是0x000200,所以它的位置应该就在0x400200,通过edb查看一下:

图5.12 .interp节的位置

同样的,还可以找到.rodata节的位置在0x400690。

图5.13 .rodata节的位置

5.5 链接的重定位过程分析

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

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

首先对hello进行反汇编

指令 Linux> objdump -d -r hello > hellodiss.txt

图5.14 hello反汇编的查看指令

具体内容:

图5.15 hello反汇编的内容

分析:

1.hello的反汇编文件中可以看出,相比于hello.o,里面所有的不确定的地址值都变得确定了,这说明链接器完成了对于hello的重定位工作。

2.hello中增加了很多hello.o中没有,但是定义在了动态库中,并被hello直接使用的函数。在链接过程中,链接器ld完成了对它们的符号解析和重定位。

3.相比于hello.o程序,hello的反汇编文件大量使用了间接跳转。

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

利用edb执行hello

图5.7 利用edb执行hello

图5.8 调用的子进程名和程序地址

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

动态链接采用了延迟加载的策略,只有在调用函数的时候才会进行符号的映射。动态链接通过使用偏移量表GOT和过程链表PLT的协同工作来实现。

GOT表中存放着函数的目标地址,PLT表则使用GOT中的地址来跳转到目标函数,在程序执行的过程中,dl_init负责修改PLT和GOT。

通过节头部表找到GOT起始位置0x601000。

图5.9 节头部表GOT部分内容

       调用edb查看调用dl_init前后的变化。

图5.10调用dl_init前的地址

图5.11调用dl_init后的地址

5.8 本章小结

本章结合实验中生成的可执行程序hello介绍了链接的概念及作用以及命令,并对hello的elf格式进行了详细的分析对比。通过反汇编hello文件,将hello.o与hello.o的反汇编文件对比,描述重定位过程,整理过程中的子函数。最后,对hello进行了动态链接分析,加深对链接的理解。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

进程的概念:

进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。在大多数系统中,需要运行的进程数是多于可以运行它们的CPU个数的。传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能够执行多个程序。无论是在单核还是多核系统中,一个CPU看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。操作系统实现这种交错执行的机制称为上下文切换。

进程的作用:

像hello这样的程序在现代系统上运行时,操作系统会提供一种假象,就好像系统上只有这个程序在运行。程序看上去是独占地使用处理器、主存和I/O设备。处理器看上去就像在不间断地一条接一条地执行程序中的指令,即该程序的代码和数据是系统内存中唯一的对象。这些假象是通过进程的概念来实现的,进程是计算机科学中最重要和最成功的概念之一。

图6.1逻辑控制流

图6.2 进程的上下文切换

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

作用:

Shell为用户提供命令行界面,使用户可以在这个界面中输入shell命令,然后shell执行一系列的读/求值步骤,读步骤读取用户的输入的命令行,求值步骤则解析命令行,并运行程序。完成后重复上述步骤,直到用户退出shell。从而完成用户与计算机的交互来操作计算机。

处理流程:

Shell打印一个命令行提示符,等待用户输入指令。在用户输入指令后,从终端读取该命令并进行解析,若该命令为shell的内置命令,则立即执行该命令;若不是内置命令,是一个可执行目标文件,则shell创建会通过fork创建一个子进程,并通过execve加载并运行该可执行目标文件,用waitpid命令等待执行结束后对其进行回收,从内核中将其删除;若将该文件转到后台运行,则shell返回到循环的顶部,等待下一个命令行。完成上述过程后,shell重复上述过程,直到用户退出shell。

6.3 Hello的fork进程创建过程

父进程通过fork函数创建一个新的运行的子进程。

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

fork函数是有趣的(也常常令人迷惑),因为它只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。

图6.3嵌套fork的进程图

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序。

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

图6.4一个新程序开始时,用户栈的典型组织结构

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

1.上下文信息

       内核为每个进程维持一个上下文(context)。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

图6.5进程上下文切换的剖析

2.进程的时间片

一个进程执行它的控制流的一部分的每一时间段叫做时间片。

3.用户模式和内核模式
       为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。

       处理器通常是用某个控制寄存器中的一个模式位(mode bit)来提供这种功能的,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中(有时叫做超级用户模式)。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。

       没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令(privileged instruction),比如停止处理器、改变模式位,或者发起一个1/O操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。任何这样的尝试都会导致致命的保护故障。反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。

       运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把机式从内核模式改回到用户模式。

       Linux提供了一种聪明的机制,叫做/proc文件系统,它允许用户模式进程访问内核数据结构的内容。/proc文件系统将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。比如,你可以使用/proc文件系统找出一般的系统属性,比如CPU类型(/proc/cpuinfo),或者某个特殊的进程使用的内存段(/proc/<process-id>/maps)。2.6版本的Linux内核引入/sys文件系统,它输出关于系统总线和设备的额外的低层信息。

6.6 hello的异常与信号处理

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

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

L

图6.6inux的信号

图6.7hello的正常执行结果图

图6.8hello运行时不停乱按键盘结果图

       可以看到,虽然是乱按,但没有影响程序的运行,只是会在程序运行结束后对shell发送许多无效指令。不过要注意,因为hello程序最后有一个getchar函数,因此第一个乱按的指令被getchar读走,不会成为发送给shell的无效指令。因此,我们乱按的内容是被放入缓冲区,等待程序执行结束后被shell当作命令读走。

图6.9hello运行时多次按回车结果图

图6.10hello运行时按ctrl+c结果图

      可以看到,按ctrl+c后,程序直接结束运行,回到shell等待输入下一条指令。

图6.11hello运行时按ctrl+z结果图

      可以看到,ctrl+z后程序被放入后台并暂停运行。

图6.12hello运行按ctrl+z后ps命令结果图

      可以看到,输入ps,可以查看当前所有进程的相关信息,可以发现此时hello程序仍然存在。

图6.13hello运行按ctrl+z后jobs命令结果图

      输入jobs可以查看前台作业号。

图6.14hello运行按ctrl+z后pstree命令结果图

      输入pstree命令,以树形结构显示了程序和进程间的关系。

图6.15hello运行按ctrl+z后fg命令结果图

6.7本章小结

本章首先介绍了进程的概念与作用,其次介绍了壳Shell-bash的作用与处理流程,接着解释了过调用fork()函数与execve()来实现的hello的fork进程创建过程与execve过程。然后,结合进程上下文信息、进程时间片、用户态与核心态转换等内容,介绍hello是如何在shell中作为一个子进程执行的,并分析在hello执行过程中按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等出现的异常、产生的信号,在Ctrl-z后运行ps、jobs、pstree、fg、kill等命令,查看这些指令对应的内容,进程的状态等,说明异常与信号的处理。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

逻辑地址:

是指由程序产生的和段相关的偏移地址部分,程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

线性地址:

线性地址是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。

虚拟地址:

虚拟地址就是线性地址。每个进程都有独立且结构相同的虚拟地址空间,从0x400000开始。

物理地址:

CPU通过地址总线的寻址,找到真实的物理内存对应地址。 CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。

图7.1地址空间描述

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

被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址。

图7.2Intel逻辑地址到线性地址的转换——段式管理

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

线性地址空间划分:4GB=1K个子空间 * 1K个页面/子空间 * 4KB/页。

页目录项和页表项格式一样,都有32位(4B)。

图7.3IA32线性地址向物理地址转换

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

从CPU产生虚拟地址的时刻一直到来自内存的数据字到达CPU,每个进程有它自己私有的页表层次结构,当一个Linux进程在运行时,已分配了的页相关联的页表都是驻留在内存中的。CR3控制寄存器指向第一级页表(L1)的起始位置。CR3的值是每个进程上下文的一部分,每次上下文切换时,CR3的值都会被恢复。

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

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

在虚拟地址翻译成物理地址之后,CPU向内存传递物理地址,物理地址分成标记CT、组索引CI和块偏移CO。先到L1,按照CI、CT、CO的顺序搜索,命中就返回数据,不命中就在下一级缓存L2中寻找,取出CPU要寻找的块。以此类推到L3。

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

7.6 hello进程fork时的内存映射

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

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

7.7 hello进程execve时的内存映射

假设运行在当前进程中的程序执行了如下的execve调用:

execve("a.out", NULL, NULL);

正如在第8章中学到的,execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out需要以下几个步骤:

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

·映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。       所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为a.out文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的,初始长度为零。图9-31概括了私有区域的不同映射。

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

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

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

图7.6加载器是如何映射用户地址空间的区域的

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

在虚拟内存的习惯说法中,DRAM缓存不命中称为缺页。

图7.7Linux缺页处理

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

基本方法:

1.隐式空闲链表

2.放置已分配的块

3.分割空闲块

4.获取额外的堆内存

5.合并空闲块

策略:

Placement policy:放置策略

首次适配, 下一次适配, 最佳适配, 等等;减少碎片以提高吞吐量;有趣的观察:近似于最佳适配算法,独立的空闲链表不需要搜索整个空闲链表

Splitting policy 分割策略:

我们什么时候开始分割空闲块?我们能够容忍多少内部碎片?

Coalescing policy合并策略:

立即合并 (Immediate coalescing):每次释放都合并

延迟合并 (Deferred coalescing):尝试通过延迟合并,即直到需要才合并来提高释放的性能。例如:为 malloc扫描空闲链表时可以合并;外部碎片达到阈值时可以合并。

7.10本章小结

本章简述了hello的存储器地址空间,即说明了说明逻辑地址、线性地址、虚拟地址、物理地址的概念。解释了Intel逻辑地址到线性地址的变换-段式管理、Hello的线性地址到物理地址的变换-页式管理、TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问、hello进程fork时的内存映射、hello进程execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。通过本章的撰写,对虚拟内存及存储系统管理有了更深的认识。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

在Linux系统中,是通过使用由内核提供的系统级Unix I/O函数来实现这些较高级别的IO函数的。

8.2 简述Unix IO接口及其函数

一个Linux文件就是一个m个字节的序列:

B0,B1,…,Bk,…,Bm-1

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为UnixI/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:

·打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个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.3 printf的实现分析

https://www.cnblogs.com/pianist/p/3315801.html

图8.1printf函数的函数体

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。

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

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

8.4 getchar的实现分析

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

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

8.5本章小结

本章介绍了Linux的IO设备管理方法、Unix IO接口及其函数的概念,分析了printf的实现、getchar的实现。

(第81分)

结论

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

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

hello程序的一生,即从程序员创建该程序开始,到在系统上运行,到输出信息,到终止的全部过程,其中包括预处理、编译、汇编、链接等阶段,并对相应的进程、存储、I/O进行管理,处理可能的异常情况。通过本文的撰写,可以进一步加深在Linux系统上程序是怎样运行的,以及相关的指令系统、内存存储体系结构、操作系统硬件管理、进程线程与虚拟内存等架构之间的相互联系。

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

附件

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

hello.c——Hello的源文件

hello.i——Hello的修改了的源程序

hello.s——Hello的汇编文件

hello.o——Hello的可重定位目标文件

hello——Hello的可执行目标文件

elfheader.txt——hello.o的ELF头

secheader.txt——hello.o的节头部表

relahello.txt——hello.o的重定位节

hellosymbol.txt——hello.o的符号表

disassembly.txt——hello.o的反汇编

helloelfhead.txt——hello的ELF 头

hellosection.txt——hello的节头部表

hellodiss.txt——hello的反汇编

hellorela.txt——hello的重定位节

hellosym.txt——hello的符号表

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

参考文献

[1]  兰德尔E.布莱恩特,大卫R.奥哈拉伦. 深入理解计算机系统. 北京:机械工业出版社,2016年11月第1版.

[2]  [转]printf 函数实现的深入剖析 - Pianistx - 博客园

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

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值