HIT-CSAPP程序人生大作业

 

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业          数据科学与大数据技术              

学     号           2021111662             

班   级            2103501            

学       生            盖嘉怡      

指 导 教 师             史先俊        

计算机科学与技术学院

20234

摘  要

本文基于《深入了解计算机系统》一书,详细阐述了hello程序的完整生命周期。第一到五章介绍了hello程序在Linux环境下从源文件到预处理、编译、汇编、链接到最后执行的过程,第六到九章详细介绍了程序执行过程中的进程管理、内存空间管理和I/O与CPU信息交互,以上各章内容有助于加深对计算机系统的理解。

关键词:计算机系统;程序的生命周期;进程管理;内存和空间管理;I/O与计算机文件交互                            

目  录

第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的整个过程。

1.1.1Hello的P2P

P2P是指from program to process,即从程序到进程。Hello程序经预处理、编译、汇编、链接生成hello.out可执行文件。在shell中输入./hello ,shell会将其当做一个可执行文件去执行。之后会调用fork()函数,在当前进程中创建一个子进程,子通过调用函数execve( )在当前子进程的上下文中运行并加载hello程序,至此完成了从.c源程序到进程的转换。

1.1.2Hello的020

020是指from zero_0 to zero_0,即刚开始程序并不存在于内存上,在运行结束后,占用的相应内存空间会被释放,也不会在内存空间保留,因此是从0到0的过程,具体过程如下:

创建好子进程后,子进程通过调用execve()函数,调用启动加载器。由于该子进程和父进程共享相同的虚拟地址空间,在子进程的上下文中运行新的hello程序需要先删除子进程的虚拟内存段,创建一个新的代码段、数据段、栈和堆段,并完成从可执行文件到虚拟内存空间的映射。在此期间,I/O设备与主存进行信息交配合程序的执行。程序运行结束后,子进程终止,由父进程回收,释放程序在运行过程中占用的内存空间。

1.2 环境与工具

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

硬件环境:

CPU:AMD Ahlon Silver 3050U with Radeon Graphics

X64 CPU;2.30GHz;8G RAM;512GHD Disk

软件环境:Windows 10 64位

调试工具:Codeblocks 64位;vi/vim/gedit+gcc

          VMware Workstation 17 Player

1.3 中间结果

文件名

作用

main.c

源程序

main.i

预处理文件

main.s

汇编文件

main.o

可重定位的目标程序

main

可执行程序

main_o.elf

用readelf读取main.o文件的ELF格式信息

main.asm

main.o反汇编文件

main.txt

main文件反汇编

main2.elf

用readelf读取main文件的ELF格式信息

1.4 本章小结

    本章从宏观上介绍了hello程序从预处理、编译、汇编、连接到最后执行结束的全部过程。并列出了程序执行过程中的软硬件环境以及用到的相关调试工具。


第2章 预处理

2.1 预处理的概念与作用

2.2.1 预处理的概念

预处理是指在源文件编译前由预处理器完成的预备工作,主要是对预处理指令进行解析,预处理后会得到一个同名的.i文件。

2.2.2 预处理的作用

(1)宏定义(宏展开):

 将程序中所有用宏定义的数据更换为原始常量数据,如#define num 10,预处理时会将所有程序中出现的num用10代替。

(2)文件包含:

将所有的头文件对应的源程序插入到程序文本中,如#include<stdio.h>。

(3)条件编译:

给编译制定一个编译条件,当满足此条件时,源代码部分才会被编译,若不满足,则会将相应的代码从源程序文本中移除。例如:#ifdef COMPILETIME,只有在对COMPLETIME进行宏定义时才会对下面的程序进行编译,可运用于库打桩技术中。

2.2在Ubuntu下预处理的命令

 (图2.3.1)

2.3 Hello的预处理结果解析

预处理后得到的.i文件比原来的文件长度增加很多,主要由以下几部分构成:

  1. 包含的文件信息(如图2.3.1):

 

(图2.3.1)

  1. 一些变量类型的定义(如图2.3.2):

 

(图2.3.2)

  1. 用到的头文件和函数的定义(如图2.3.3):

 

(图2.3.3)

  1. 源程序部分(如图2.3.4):

 

(图2.3.4)

2.4 本章小结

本章主要介绍对源程序的预处理操作,并对预处理文件的内容做了较为详细的介绍。


第3章 编译

3.1 编译的概念与作用

3.1.1编译的概念

编译是指将预处理文件在编译器的处理下生成相应的汇编语言程序的过程。

3.1.2编译的作用

(1)进行基本的语法检查,如果存在语法错误,编译器会给出错误提示。

(2)将C语言代码翻译成相应的汇编代码,根据设定的编译器等级对程序进  行初步的优化来提升程序性能。

3.2 在Ubuntu下编译的命令

 

3.3 Hello的编译结果解析

(1)函数堆栈开辟(如图3.3-1)

首先将基址指针%rbp压栈,之后通过移动%rsp指针为函数开辟存储空间,对应汇编代码为:subq $32,%rsp,代表参数分配了50byte(32为16进制数)的空间。

(2)参数传递(如图3.3-1)

%edi存放函数的第一个参数,将其保存在首地址为-20(%rbp)的栈空间 中。%rsi存放函数第二个参数,将其保存在首地址为-32(%rbp)的栈空间中。

  1. 指针(如图3.3-1)

其中-20(%rbp),-32(%rbp)分别代表比基址指针%rbp所在地址小0x20byte和小0x32byte的地址,类似于C语言中的指针变量。函数中参数一般都用%rbp加上相应偏移量访问。

 

(图3.3-1)

  1. 跳转指令(如图3.3-2)

将第一个参数与4比较,若相等会跳转到.L2部分,否则会顺序执行。其中je代表相等则跳转。

  1. 取地址指令(如图3.3-2)

“leaq .LC0(%rip),%rax”是取地址指令,表示将.LC0(%rip)的地址存到%rax中。

(4)赋值指令(如图3.3-2)

“movq %rax,%rdi” 是赋值操作,将%rax中的内容存到%rdi寄存器中。

(5)Call指令负责调用函数(如图3.3-2)

程序中调用了以下函数:

call puts@PLT

call exit@PLT

call printf@PLT

call sleep@PLT

call getchar@PLT

在函数调用过程中,会将返回地址压栈,之后将PC赋值为跳转到的函数的地址,在执行完调用的函数即函数return后,如果有返回值,会将返回值保存在%rax中,然后将返回地址pop出,将其赋值给PC,从而实现回到调用函数前程序执行的位置。

 

(图3.3-2)

  1. 加减法指令(如图3.3-3)

subq $32,%rsp指令代表将指针%rsp代表的地址值减50(0x32),即实现了指针的移动。

addq $8,%rax指令代表将寄存器%rax中的数据加8并保存在%rax中。

 

(图3.3-3)

  1. 数据类型

%edx代表32位数据,%rax代表64位数据,一般由寄存器存储的是局部变量

$0,$1等由“$”加数字构成的代表常数,addq 后面的q代表对64位数据进行操作,addl l代表对32位数据操作。

   而一些常量数据存储在只读数据区,如字符串“Hello %s %s\n”(如图3.3-4).

 

(图3.3-4)

  1. 文件构成(如图3.3-5)

汇编文件由以下几部分构成:

1.源文件(.file)

2.代码区(.text)

3.只读变量区(.section  .rodata)

4.对齐方式(.align)

 

(图3.3-5)

3.4 本章小结

   本章实现了将预处理文件生成汇编文件,分析了汇编文件中的内容,对于main.c中涉及到的汇编指令进行了详细的解释。


第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

汇编是指汇编语言文件转化成可重定位的二进制目标文件的过程。

4.1.2汇编的作用

   将main.s文件中的汇编指令翻译成机器可以“理解”的二进制指令,并将这些指令打包生成main.o的可执行文件。

4.2 在Ubuntu下汇编的命令

Ubuntu下汇编的命令:gcc -m64 -no-pie -fno-PIC -c main.s -o main.o

 

(图4.2)

4.3 可重定位目标elf格式

生成elf格式文件并打开的指令:

(图4.3)

4.3.1 elf头

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

      

 

 

(图4.3.1)

4.3.2 节头部表

节头部表用于描述目标文件中节的信息:名称、类型、地址、偏移量等(如图4.3.2所示)。

 

 

(图4.3.2)

4.3.3重定位节

在链接器完成了符号解析之后会将相同类型的节进行合并,并对其重新分配内存空间,本程序中重定位的节有.rodata,puts,exit,atoi,sleep,getchar。

并且重定位有两种类型,一种为R_X86_64_PC32,使用32位PC相对地址的引用。在本程序中.rodata采用这种重定位方式。另一种是R_X86_64_PLT32,使用32位绝对地址进行重定位,本例中puts,exit,atoi,sleep,getchar采用这种重定位方式(如图4.3.3所示)。

 

(图4.3.3)

4.3.4符号表

记录程序中引用的符号的信息,程序中共有两种符号,全局符号,局部符号。全局符号包括非静态函数和全局变量,全局符号中由其他函数定义的符号被称作外部符号。局部符号为前有static修饰的函数和变量,main.o中共定义了11种符号,如图4.3.4所示。

        

 

(图4.3.4)

4.4 Hello.o的结果解析

Objdump反汇编运行结果如图4.4所示:

 

(图4.4)

4.4.1机器语言的构成

机器语言由01二进制指令构成,可以被机器直接识别,无需翻译,可以对计算机内部硬件直接控制并完成相应操作。机器语言一般由操作码和地址码构成,操作码控制完成何种运算,地址码给出对哪个存储单元的数据进行运算。

4.4.2机器语言与汇编语言的映射关系

一条机器语言对应一条汇编语言程序,汇编程序是机器程序的符号化表示,可读性更好,而机器语言则是汇编语言与机器交互的一种形式。

4.4.3 objdump反汇编文件与汇编文件比对

(1)文件包含的内容:

objdump文件中只包含.text段(图4.4.3_1),而main.s文件中除了代码段还 含有.file,.rodata,.section,.align部分的数据(图4.4.3_2)。

       

 

(图4.4.3_1)

    

 

(图4.4.3_2)

  1. 跳转指令:

在main.s文件中跳转指令后加的地址是段地址,而objdump反汇编.asm文件中跟的是相应函数的地址。

 

(图4.4.3_3  .asm文件中跳转指令)

 

(图4.4.3_3  .s文件中跳转指令)

  1. 函数调用:

在.asm文件调用的函数均给出了函数的地址,.s文件中则是给出了函数的名字,如图4.4.3_4所示。

 

(图4.4.3_4)

  1. 是否有重定位标记:

  .asm文件中给出了需要重定位的符号的信息:偏移量、符号名称、解析类型(若符号为函数名还会有PC跳转的偏移),而.s文件中没有,如图4.4.3_5。

 

(图4.4.3_5)

4.5 本章小结

本章利用汇编器将汇编文件转化成可重定位的目标文件,利用readelf指令查看了可重定位目标文件格式,熟悉了可重定位目标文件各个部分记录的信息。通过将main.o文件反汇编生成的.asm文件与.s文件比对,更清楚地知道了汇编器对.s文件进行了哪些处理。

5链接

5.1 链接的概念与作用

5.1.1链接的概念

链接是指链接器(ld)将各种代码和数据片断收集并组合成一个单一可执行目标文件的过程。

5.1.2链接的作用

  1. 链接使得分离编译成为可能,程序员可以将大的程序拆分成小的模块,独立地修改编译小的模块,然后由链接器进行链接,方便程序的修改调试。
  2. 链接还可以实现共享库,减少程序的代码量。

5.2 在Ubuntu下链接的命令

在Ubuntu下链接的命令:ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.omain.o/usr/lib/x86_64-linux-gnu/crti.o/usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -o main。

 

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

5.3.1 ELF头

ELF头以一个16字节的Magic序列开始,它描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、字节顺序、目标文件的类型(可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表的文件偏移、入口点地址、程序头起点,以及节头部表中条目的大小和数量,如图5.3.1所示。

 

(图5.3.1)

5.3.2 节头

节头表用于描述目标文件中节的信息:名称、类型、地址、偏移量等(如图5.3.2所示)

       

 

(图5.3.2)

5.3.3 程序头表

只有可执行程序才有程序头表,包含页面大小、虚拟地址内存段、段大小等信息(图5.3.3)

 

(图5.3.3)

5.3.4 段节和Dynamic section

 

(图5.3.4)

5.3.5 重定位节

 

(图5.3.5)

5.3.6 符号表

 

(图5.3.6)

5.4 hello的虚拟地址空间

使用edb加载hello,发现虚拟内存从0x400000开始。ELF文件存储在低地址部分,如图5.4.1_1所示。

  

 

(图5.4.1_1)

通过查看节头表可以知道各段在虚拟内存的起始地址,.text在0x4010f0处,.rodata在0x402000处,.data在0x404080处,由于此程序中没有需要解析的符号,.symtable的偏移量为0(代码段如图5.4.1_2所示,数据段如图5.4.1_3所示)。

 

(图5.4.1_2)

 

 

(图5.4.1_3)

5.5 链接的重定位过程分析

objdump -d -r main.o > main.asm

objdump -d -r main > main.txt

main.o的反汇编文件存储在main.asm中,main的反汇编文件存在main.txt中。

通过对比main.asm与main.txt文件,发现以下不同之处:

  1. 文件长度不同:

main.asm中含有代码段的反汇编程序,而main.txt文件中含有其他段的代码数据:Disassembly of section .init、Disassembly of section .plt、Disassembly of section .plt.sec、Disassembly of section .text、Disassembly of section .fini(如图5.5所示)。

  1. 调用函数和跳转指令的地址所给形式不同:

在main.txt文件中每次函数的调用和跳转指令所给的地址都是直接给出的虚拟内存的地址或者是相对于PC的偏移量,而在main.asm中给出的是标记过的待重定位的地址或者是相对main函数的偏移地址(如图5.5所示)。

 

(图5.5)

通过分析以上不同之处,我们可以得出结论,链接器主要完成两项任务:符号解析和重定位。待所有符号完成解析后,链接器将所有可以合并的节合并,并将每个带解析的符号和合并后的节与一个虚拟内存地址对应。最后将程序中所有的符号应用修改为重新分配的虚拟地址,并存储在rel.data节中。

5.6 hello的执行流程

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

执行过程:

  1. 开始执行:

加载器跳到_start,之后调用系统程序_libc_start_main准备执行main函数。

  1. 执行main函数:

在main函数中会依次执行以下函数:_printf,_exit,_sleep,_getchar。

  1. 退出程序:

exit。

调用的函数及其对应地址如表5.6所示:

函数名称

函数入口地址

_start

0x4010f0

_init

0x401000

puts@plt

0x401090

printf@plt

0x4010a0

getchar@plt

0x4010b0

atoi@plt

0x4010c0

exit@plt

0x4010d0

sleep@plt

0x4010e0

.plt

0x401020

_fini

0x4011c0

(表5.6)

5.7 Hello的动态链接分析

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

动态链接使我们在调用一个共享库定义的函数可以在运行时找到函数地址。由于定义共享库的共享模块可以在运行时加载到任何一个位置,编译器无法对其位置进行预测,需要借助GNU编译系统的延迟捆绑技术解决,将过程地址的绑定推迟到第一次调用该过程中。

延迟捆绑技术通过GOT和PLT实现:PLT是一个数组,每个数组项有16字节代码构成。PLT[0]项很特殊,可以跳转到动态链接器,其余条目都负责一个库函数的调用。GOT也是一个数组,但每个数组项对应的是一个8字节的地址。其中GOT[0],GOT[1]包含动态链接器在解析函数会用到的地址信息,GOT[2]记录了动态链接器在ld_linux.so中的函数入口点。

通过查询节头表发现GOT存储在0x404000处,在调用dl_init前,此位置的内存映像如图5.7.1所示:

 

(图5.7.1)

在调用dl_init之后,内存映像如图5.7.2所示:

 

(图5.7.2)

通过上图对比可以发现,在调用dl_init函数后,GOT表中的内容发生了改变。在运行dl_init前,GOT中存放的是PLT条目的第二条指令,而调用函数后,完成了动态链接,GOT中存放的是调用的函数的地址。动态链接正式通过在GOT条目与PLT条目之间不断跳转完成的。

5.8 本章小结

本章对可重定位的目标文件进行链接生成了可执行程序main,通过查看ELF文件,清楚了可执行文件的格式。通过对比main.o文件和main文件的反汇编代码,分析出链接器在链接时完成了符号解析和重定位。使用edb执行程序,直观地展现了helloworld程序在执行过程中调用了哪些函数,以及如何通过GOT和PLT协作完成动态链接。


6hello进程管理

6.1 进程的概念与作用

6.1.1进程的概念

进程是指程序执行过程中的一个实例,系统中每个程序都运行在一个进程的上下文中。

6.1.2进程的作用

进程可以给程序提供两个关键的抽象,逻辑控制流和私有的地址空间,好像每个程序都在独占CPU和内存系统,便于对内存的管理。

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

(1)作用:

shell是命令行解释器,是一个交互型的应用级程序,代表用户运行其他程序。主要有以下几个作用:

1)信号处理

2)进程创建

3)前后台控制

4)作业调用

5)信号发送与管理

(2)处理流程:

用户在shell里面输入一个命令,如果此命令是shell内置的命令,则直接执行,如果不是,如./hello,会将其视作一个可执行程序执行。首先会调用fork()函数,在当前父进程中创建一个子进程作为该程序的执行实例,调用execve()函数为该程序建立好与虚拟内存之间的映射。之后由操作系统的页面调度机制将磁盘上的程序调到内存中执行,程序执行完毕会由父进程回收该子进程。如果命令行的最后一个参数是&,会在后台进行执行,shell会返回循环顶部,等待用户发出下一个命令,不会等待它执行完成;若没有&标记,则会在前台执行该程序,直至执行完毕才能接收下一个命令。

在执行过程中,shell还会随时接收来自键盘的输入信号,并及时对这些信号进行应答处理。

6.3 Hello的fork进程创建过程

bash接收到一个非内置命令的命令行,如./main 2021111662 gaijiayi 1时,解析指令时会发现它不在内置命令集合中,会把它当做一个可执行目标文件来处理。首先调用fork()函数在bash父进程里创建一个子进程,这个子进程会共享与父进程相同但独立的虚拟地址空间,并继承父进程打开的所有文件,但是子进程的PID(进程组编号)与父进程不同。创建好了子进程,接下来就准备在子进程中执行main程序。

6.4 Hello的execve过程

bash里面创建的子进程与父进程依然共享相同单独立地虚拟地址空间,因此需要调用execve()函数进行虚拟内存与程序的重新映射。调用execve函数后首先会调用加载器,加载器会删除子进程现有的虚拟内存段,并组建一组新的代码、数据、堆、栈段。新的堆栈段会初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页的大小的片,新的代码和数据段就会被初始化为可执行文件中的内容。之后加载器会跳到程序的入口点执行_start函数,在_start函数中调用系统级函数_libc_start_main初始化执行环境,最后才开始执行main文件中的main函数。在执行过程中,程序从磁盘调入内存由操作系统的页面调度机制负责,而在加载过程中只是完成虚拟内存空间的分配。

此程序中main函数有两个参数,argc和argv,argc给出了argv中指针不为空的数目,若在bash中输入指令./hello 2021111662 gaijiayi 3,此时argv数组中指针有四个不为空(argv[0]默认存储文件名不为空),会执行循环语句,运行结果如图6.4所示:

 

(图6.4)

6.5 Hello的进程执行

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

  1. 进程上下文信息:

  进程上下文信息是指内核重新启动一个被抢占的进程所需的状态。进程上下文通常包含以下内容:通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构(页表、进程表、文件表)。

  1. 进程时间片:

  进程时间片是指一个进程执行它的控制流的一部分的每一时间段,当一个进程与其他进程轮流执行时叫做多任务,也叫时间分片。

  1. 进程调度过程:

 进程调度是由操作系统中的内核进行控制的。在调度过程中内核可以决定抢占当前进程并恢复之前被抢占的进程,并使用上下文转换机制来控制转移这个重新被开始的进程。上下文转换过程中需要完成三件事:1)保存当前进程的上下文,2)恢复此进程的上下文,3)将控制转移给这个新恢复进程。

  1. 用户态和和核心态的转换

 为了限制一个程序可以执行的指令,处理器提供了用户模式和内核模式转化的机制来保证程序访问合法的指令和虚拟内存空间。

控制寄存器中会设置一个模式位,若该模式位没有设置,则代表进程运行在用户态下,反之,运行在内核态下。用户态向内核态转换只能通过异常来实现。比如当一个进程A因为缺页而中断时,会由用户态转移到内核态,执行缺页中断处理子程序将缺的页从磁盘调到内存。由于访存过程的时间过长,此时不会一直等待而是进行上下文转换切换到另一个进程B,在进程B运行的时间片到了之后会再次进行上下文转换将控制权转回到之前的进程A。由此可见由于异常导致的上下文转换中可能会存在用户模式和内核模式的切换。

6.6 hello的异常与信号处理

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

异常可以被分为两大类:同步异常和异步异常,这里的同步与异步是指异常是否由当前执行的指令发出(异步不是由当前指令导致的异常)。

  1. 异步异常:

是指由外部设备发出中断请求信号导致的异常,当CPU完成当前处理的指令的执行阶段后会统一发出对中断异常的查询信号,当查询过程中若是发现终端引脚的电压升高,就会从数据总线上读取相应的异常服务号,并调用相应中断处理子程序处理异常,结束后会返回原指令的下一条指令处。

  1. 同步异常:

同步异常主要有三类分别为:陷阱、故障和终止。

1)陷阱:

是指有意的异常,是执行当前指令的结果,处理后会返回到下一条指令。陷阱为用户程序和内核之间提供了一个像过程一样的接口,实现了系统调用,可由syscall指令实现。

2)故障:

不是有意的,有的可以被恢复。常见的故障:缺页异常(可被恢复并返回当前指令)、保护故障(不可恢复)、浮点异常。不可修复的故障会导致程序的终止。

3)终止:

不是有意导致的,由不可恢复的致命错误导致。常见的终止异常有:非法指令、奇偶校验错误、机器检查等,会立刻终止当前程序。

可能产生的信号:SIGINT(键盘中断信号)、SIGKILL(杀死程序)

当内核向进程发出信号后,如果被进程接收并调用了信号处理程序,会根据信号的名称调用相应的处理程序进行处理,处理完毕后会下一条指令处继续正常执行。

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

  1. 程序执行过程中乱按键盘,并不会影响程序运行,如图6.6_1所示

 

(图6.6_1)

  1. 程序执行过程中按Ctrl+C会挂起当前进程,如图6.6_2所示。

 

(图6.6_2)

  1. 程序执行过程中按Ctrl+Z后运行ps指令,Ctrl+Z会使当前进程被挂 起,ps可以显示当前系统中的所有进程的信息,如图6.6_3所示:

 

(图6.6_3)

  1. 程序执行过程中按Ctrl+Z后运行jobs指令,终止后显示shell环境中已启动的状态,如图6.6_4所示。

 

(图6.6_4)

  1. 程序执行过程中按Ctrl+Z后运行pstree指令,将进程关系以树状图形式展现出来,如图6.6_5所示。

 

(图6.6_5)

  1. 程序执行过程中按Ctrl+Z后运行kill指令,杀死当前进程,如图6.6_6所示。

 

(图6.6_6)

6.7本章小结

本章介绍了进程的概念与作用,详细地介绍了进程的创建、程序的加载执行过程,以及执行过程中可能出现的异常即相应的信号处理。完整展现了程序从加载、执行、到最后终止、子进程被父进程回收的全过程。


7hello的存储管理

7.1 hello的存储器地址空间

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

逻辑地址:包含在机器语言中,用来指定一条指令或者一个操作数的地址,由段地址加偏移地址构成。

线性地址:逻辑地址到物理地址之间变换的中间层。程序产生的段偏移地址加上相应段的基地址就会构成一个线性地址。

物理地址:CPU可以直接访问的地址形式,对应某个存储单元。

虚拟地址:是操作系统对存储器和I/O设备的抽象,程序中生成的地址单元都是虚拟地址,需要经过MMU找到与物理地址的对应关系。

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

逻辑地址的高位部分为段号,低位部分为段内偏移量。16位的系统中段寄存器中直接保存段的基地址,而32为系统段寄存器中存有段选择符。32位系统寻址时先到相应的段寄存器中找到相应的段选择符,根据段选择符中的索引和类型到全局描述符表或者局部描述符表中获得段描述符。再将段描述符送往描述符cache中,通过映射关系找到相应段的基地址,基地址加上偏移量可以得到对应的32位线性地址。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          

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

页式管理是将物理空间与虚拟地址空间均划分为大小相同的页,由页表记录虚拟地址中的页与物理地址中的页的映射关系,页地址加上页内偏移地址就会得到与虚拟地址对应的物理地址。

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

(1)TLB:

页表存储在主存中,每次访存时间效率不高,因此增加一个特殊的cache 用于存放那些经常被使用的页表项。TLB采用全相联的映射方法,对于所给的 VA会先到TLB中进行比对,如果未命中才会去主存访问页表。

(2)四级页表:

由于一个程序所需要的页表项可能会很多,会占用大量的存储空间,现采 取多级页表的方法来解决这个问题。在多级页表中高一级的页表存储着指向低 一级页表的指针,以此类推,直到最后一级页表才会指向物理地址。之前的单 级页表中可能会有大量的空页表项占用存储空间,而采用多级页表结构只需将 上一级页表项中的指针置为空,无须保存这些空页表项,在一定程度上减少了 存储空间的浪费。

(3)四级页表下的VA与PA的转换:

在VA与PA转化时,首先在TLB中比对。若未命中,会在页表基址寄存器中获得一级页表首地址,然后根据虚地址中高位段的VAPN1找到对应的一级页表项,根据这个一级页表项中保存的指针找到第二级页表首地址,同样的,用VAPN2找到在二级页表中的页表项获得三级页表首地址,以此类推,直至找到对应的物理地址。

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

获得物理地址后,传统的计算机会直接用这个物理地址去访问存储器获得所需的数据。由于访存速度过慢,会严重影响计算机效率,因此现在的计算机大多采用三级cache来存储那些被经常访问的数据,以减少访存次数提升性能。通过之前的多级页表机制获得物理地址后,会先用这个物理地址到一级cache中查找,通常采用组相连的映射方式。先根据PA中的组号找到对应的组,再比对tag位查看所需数据是否在这个组中。如果未命中,则到二级cache中继续比对,以此类推,如果三级cache中均未命中才会访存。对于cache中保留哪些数据会有相应算法进行约束,尽可能提高cache的命中率,切实减少访存次数,提升效率。

7.6 hello进程fork时的内存映射

Shell中执行hello程序时会先调用fork()函数为程序创建一个进程,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct,区域结构和页表的原样副本。为了保证与父进程共享相同但独立地虚地址空间,采用一种私有的写时复制策略:当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任意一个后来进行写操作的时候,写时复制机制就会创建新页面,并更改页表项中虚地址与实地址的对应关系,从而hello程序获得了属于自己的代码段、数据段……

7.7 hello进程execve时的内存映射

Execve函数调用加载器完成以下操作:

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

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

(3)映射共享区域。如果hello的可执行文件与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

(4)设置程序计数器PC。在下一次调度这个进程的时候,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

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

由于在程序加载时程序并未从磁盘上加载到内存,而CPU只能对内存中的程序进行执行。在程序执行时在页表项中不会查到该虚地址对应的实地址,此时会产生缺页中断信号。在CPU查询到该信号时会进行上下文切换,转到内核态,调用缺页中断子程序将需要的程序块从磁盘加载到内存中,并在页表项中增添这个映射关系,然后再转回之前的进程继续执行程序。

7.9动态存储分配管理

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

  1. 动态内存管理的基本方法:动态内存分配器。

动态内存分配器维护者一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块都是一个连续的虚拟内存片,有的是已经分配的,还有一些是未分配的。已分配的块会被相应程序使用,未分配的等待被分配使用。

  1. 动态内存管理的策略:

动态内存分配器的两种风格:显示空闲链表和隐式空闲链表。

隐式空闲链表通过头部中的大小字段隐含地连接。分配器通过遍历堆中的所有块间接地遍历整个空闲块的集合。放置已分配的块时要从链表头开始搜索空闲块,但在分配过程中很容易产生碎块,这时就需要采取合适的合并策略将空闲块进行合并。

由于隐式空闲链表块分配与堆块的总是成线性关系,对于一般的分配器不适合,故还有一种显示空闲链表的方式。显示空闲链表有两种维护方式: 一种是后进先出的顺序维护。将新释放的块放在链表的开始处,分配器会最先检查最近使用过的块,使得释放一个块可以在常数时间内完成。另一种是按照地址顺序维护。链表中每个块的地址都小于它的后继地址。

空闲块的分配方式也有很多种方法:首次适配、下一次适配、最佳适配和分离适配,不同适配方案的吞吐率和内存利用率各有不同。

7.10本章小结

本章介绍了hello程序的存储管理——虚拟内存。介绍了常见的两种虚拟内存管理方式:段式管理和页式管理。并详细说明了目前的计算机在页式管理结构下如何利用TLB、多级页表、三级cache完成从虚地址到实地址的转换并且获得相应存储单元的数据。最后,结合hello程序分析了程序在加载、执行过程中的存储管理。

8hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

一个Linux文件就是一个m字节的序列,所有的I/O设备(例如网络、磁盘和终端)都被模型化成文件,而所有的输入和输出都被当作对应文件的读和写来执行。使用这种将设备优雅地映射为文件的方式,允许Linux内核引用出一个简单、低级的应用接口,称作Unix IO。

8.2 简述Unix IO接口及其函数

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

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

(2)Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符为0)、标准输出(文件描述符为1)和标准错误(文件描述符为2)。

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

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

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

包含的函数:

int open(const cahr *pathname,int flags,int perms)

Int close(unistd.h)

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

ssize_t write (int fd,void *buf,size_t count)

8.3 printf的实现分析

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

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

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

具体过程如下:

printf负责获取格式字符串和可变形参列表,并将其作为参数传递给函数vsprintf。vsprintf负责实现格式化,它接受确定输出格式的格式字符串fmt,并用格式字符串对个数变化的参数进行格式化,即构建出将要打印在屏幕上的字符串。之后vsprintf会调用write函数,在write函数的最后会调用syscall函数。syscall函数调用save函数,完成字符串的打印,直到遇到’\0’结束符。最后通过字符显示驱动子程序将字符的ASCII码存到显示vram(存储每一个点的RGB颜色信息)中,显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。在屏幕上就显示出了想要打印的字符串。

8.4 getchar的实现分析

Getchar中的read函数将缓冲区都读入buf数组中,返回缓冲区的长度。当buf数组为空,调用read函数,如果不空就返回buf中的第一个元素。

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

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

8.5本章小结

本章介绍了Linux下IO设备的管理方法,了解了IO接口的作用以及接口中可以调用的一些函数,最后分析了printf和getchar函数的实现过程。

结论

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

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

Hello 的一生经过了以下过程:

  1. 预处理生成预处理文件。
  2. 编译生成汇编文件。
  3. 汇编生成可重定位的二进制文件。
  4. 链接生成目标文件。
  5. 在shell中输入”./main 2021111662 gaijayi 3”会为其创建进程去执行。
  6. 在一个时间片中顺序执行程序。
  7. 缺页时会利用页面调度机制将程序从磁盘调到内存。
  8. 运行中可以发出各种信号控制。
  9. 运行结束会清空内存中为其分配的堆栈。
  10. shell父进程回收已经终止的main子进程。

通过对以上过程的逐条实践,我对于程序执行过程有了更深刻的了解,并对于计算机系统产生了浓厚的兴趣。通过本门课程的学习,我在编程时也会更加注意程序的效率,尽量编写对编译器对cache有好的程序,在实践中不断践行和应用学到的知识,提升自己的能力。


附件

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

文件名

作用

main.c

源程序

main.i

预处理文件

main.s

汇编文件

main.o

可重定位的目标程序

main

可执行程序

main_o.elf

用readelf读取main.o文件的ELF格式信息

main.asm

main.o反汇编文件

main.txt

main文件反汇编

main2.elf

用readelf读取main文件的ELF格式信息


参考文献

  1.  Randal E.Bryant,David R.O’Hallaron. 深入理解计算机系统
  2. [转]printf的深入剖析:https://www.cnblogs.com/pianist/p/3315801.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值