2022 HIT-ICS大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业          计算学部       

指 导 教 师            史先俊           

计算机科学与技术学院

2022年5月

摘  要

    本文通过一个简单的hello.c 的程序来分析一个计算机对一段代码的详细处理过程,包括预处理、编译、汇编、链接阶段,进入到内存,各级cache,最后在I/O中输出,最后被回收的过程描述。通过本次大作业,可以将课堂所学内容全部进行实践,同时可以进一步理解计算机系统。

关键词:hello.c 计算机系统,预处理,编译,汇编,链接,shell-base,存储空间,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 本章小结............................................................................... - 6 -

第3章 编译................................................................................... - 7 -

3.1 编译的概念与作用............................................................... - 7 -

3.2 在Ubuntu下编译的命令.................................................... - 7 -

3.3 Hello的编译结果解析........................................................ - 7 -

3.4 本章小结............................................................................. - 11 -

第4章 汇编................................................................................. - 12 -

4.1 汇编的概念与作用............................................................. - 12 -

4.2 在Ubuntu下汇编的命令.................................................. - 12 -

4.3 可重定位目标elf格式...................................................... - 12 -

4.4 Hello.o的结果解析........................................................... - 15 -

4.5 本章小结............................................................................. - 16 -

第5章 链接................................................................................. - 17 -

5.1 链接的概念与作用............................................................. - 17 -

5.2 在Ubuntu下链接的命令.................................................. - 17 -

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

5.4 hello的虚拟地址空间....................................................... - 20 -

5.5 链接的重定位过程分析..................................................... - 20 -

5.6 hello的执行流程............................................................... - 22 -

5.7 Hello的动态链接分析...................................................... - 23 -

5.8 本章小结............................................................................. - 24 -

第6章 hello进程管理.......................................................... - 25 -

6.1 进程的概念与作用............................................................. - 25 -

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

6.3 Hello的fork进程创建过程............................................ - 25 -

6.4 Hello的execve过程........................................................ - 26 -

6.5 Hello的进程执行.............................................................. - 26 -

6.6 hello的异常与信号处理................................................... - 26 -

6.7本章小结.............................................................................. - 30 -

第7章 hello的存储管理...................................................... - 31 -

7.1 hello的存储器地址空间................................................... - 31 -

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

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

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

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

7.6 hello进程fork时的内存映射......................................... - 32 -

7.7 hello进程execve时的内存映射..................................... - 32 -

7.8 缺页故障与缺页中断处理................................................. - 32 -

7.9动态存储分配管理.............................................................. - 33 -

7.10本章小结............................................................................ - 33 -

第8章 hello的IO管理....................................................... - 35 -

8.1 Linux的IO设备管理方法................................................. - 35 -

8.2 简述Unix IO接口及其函数.............................................. - 35 -

8.3 printf的实现分析.............................................................. - 36 -

8.4 getchar的实现分析.......................................................... - 37 -

8.5本章小结.............................................................................. - 37 -

结论............................................................................................... - 38 -

附件............................................................................................... - 39 -

参考文献....................................................................................... - 40 -

第1章 概述

1.1 Hello简介

Hello的P2P过程指的是从hello.c从一段代码开始,经过预处理,编译,汇编,链接一系列操作后生成一个可执行文件的全部过程。

Hello的020过程From Zero-0 to Zero-0,指的是Process在内存中从0到0,先使用fork产生一个子进程,然后shell通过execve加载并执行hello,映射虚拟内存,然后程序载入物理内存,执行主函数的代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程回收僵死进程,内核删除相关数据结构。

1.2 环境与工具

硬件环境:AMD X64 CPU 3.00GHZ; 16G RAM; 512 G SSD

软件环境:Windows10 64位; Vmware 16 PRO; Ubuntu 20.04 LTS 64位

开发工具:Visual Studio 2022;CodeBlocks 64位;EDB;GDB;OBJDUMP;GCC等。

1.3 中间结果

hello.i

hello.s

hello.o

hello

hello_o.elf

hello.elf

helloo_obj.txt

hello_obj.txt

1.4 本章小结

本章简述了本次大作业的主要目的,开发环境以及工具,总结了所有的中间结果。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。编译器在编译开始之前调用预处理器来执行以#开头的命令,包括#include的头文件、#define的宏定义,#if、#ifdef、#endif等条件编译,最终生成以.i结尾的文本文件。

作用:扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏。

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

       通过gedit打开hello.c源程序:

       通过gedit打开hello.i文件,报告中只对main函数部分进行了截图:

 hello.i文件中插入了所有用#include命令指定的文件,包括stdlib.h、stdio.h、unistd.h三个文件。其次,删除了注释等信息。

2.4 本章小结

本章主要讲述执行源程序的第一个步骤——预处理。将一个.c文件转化为.i文件,实现了预处理的作用——扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏,为下一步的编译做好提前工作。

第3章 编译

3.1 编译的概念与作用

       概念:编译器首先检查代码是否有错误,确定代码接下来要做的工作,检查无误后,编译器将hello.i翻译成hello.s。该程序包含函数main的定义,以.s这种文本格式输出源程序编译后产生的汇编代码。

       作用:为源程序产生相应的汇编语言,为不同高级语言的不同编译器提供了通用的输出语言。

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1数据

   1.int型数据:i,argc。

       i为局部变量,存放在cpu的栈中。

       argc也是main函数中的一个int型变量,存放在栈-20(%rsp)中

       2.字符串

   字符串有两个,都是全局变量,存放在.rodata段。

    第一个字符串为“用法: Hello 学号 姓名 秒数!\n”汉字在utf-8中占3个字节,所以第一个字符串如图:

       第二个字符串与输入有关,是“Hello %s %s\n”。中间的两个字符串存放在argv[1]和argv[2]中。第二个字符串如图:

       3.数组

   argv[]为一个字符串的指针数组,存放函数执行的命令行参数存放在栈中。

    4.常数

   源代码中直接出现的数字,均通过立即数的形式给出

3.3.2操作

    1.赋值操作:将前面的值赋给后面的寄存器或者栈中

       2.加法操作:将后面寄存器或者栈中的数加上前面的立即数或者寄存器或者栈中的数,将结果保存在后面的寄存器或者栈中。本程序中用于通过指针找数组中的元素,和对于循环变量的每次加一操作。

       3.减法操作:将后面寄存器或者栈中的数减去前面的立即数或者寄存器或者栈中的数,将结果保存在后面的寄存器或者栈中。本程序中用于栈顶减去一定的大小来为数据开辟空间存放在栈中。

       4.加载有效地址:将.LC0.LC1的有效地址传送给%rdi

 

       5.比较操作:用于比较argc与4的大小,判断程序是否退出;

       同样用于循环变量与8的大小比较,判断循环是否结束。

       6.数组/指针操作:源代码中只有一个数组char *argv[]。所以在hello.s程序中,argv[]作为唯一一个数组,被存储在栈中,取用其中信息时利用%rbp加偏移量的方法来进行。

       7.控制转移操作:

控制转移分为以下几类

源程序中提到的控制转移有:

       比较argc与4的大小,相等跳转到.L2

       在.L2中循环变量i的值初始化为0后,直接跳转到.L3

       在.L3中将i与7进行比较,小于等于则跳转到.L4

      8.函数操作

       Main函数:

(1) 传递控制:main函数被系统函数_libc_start_main调用 执行(call main)。

(2) 传递数据:main函数的两个参数分别是argc和argv, 分别使用%rdi和%rsi来存储。函数的返回值用%eax来存储。

(3) 分配和释放内存:在调用main函数的时候,用%rbp记录栈帧起始位置,函数在栈中分配的空间在%rbp之上。 当程序调用结束时,用leave指令  ,leave指令将栈空间恢复为初始状态,之后ret返回。

puts函数:

(1) 传递数据:一个参数,一个整型返回值

(2) 传递控制:call puts@PLT

用来在argc不为4时提示正确用法

exit函数:

(1) 传递数据:一个参数存放在%edi;正确返回0,错误返回非0.

(2) 传递控制:call exit@PLT

用来在argc不为4时退出程序

printf函数:

(1) 传递数据:printf设置%rdi 为“Hello %s %s\n”的首地址,设置%rsi为 argv[1],%rdx为argv[2]。

(2) 传递控制:call printf@PLT

用来打印输入的字符串

sleep函数:

(1) 传递数据:一个存放在%edi的参数。

(2) 传递控制:call sleep@PLT

用于让程序进入休眠,使其在一段时间内处于非活动状态。

atoi函数:

(1) 传递数据:一个存放在%edi的参数,返回一个整型值。

(2) 控制传递:call atoi@PLT

对进程执行挂起一段时间。

getchar函数:

(1) 传递数据:无输入参数;返回用户输入的第一个字符的 ASCII码,出错返回-1。

(2) 传递控制:call getchar@PLT

读入一个输入的字符

3.4 本章小结

本章具体分析了汇编代码中的数据和操作,复习了汇编部分的相关内容,对hello.s进行了详细的解析。

第4章 汇编

4.1 汇编的概念与作用

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

汇编的作用: 将hello.s中保存的汇编代码翻译成可供机器执行的二进制代码。

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

首先,用readelf -a hello.o > hello_o.elf命令生成可重定位目标文件hello.oELF格式文件hello_o.elf,然后查看该文件。

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

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

.rel.text节:

一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。

这个elf中的重定位节中有8条重定位信息,分别为对第一个字符串内容(.L0)、puts函数、exit函数、第二个字符串内容(.L1)、printf函数、atoi函数、sleep函数、getchar函数的重定位声明。

symtab节:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。每个可重定位目标文件在.symtab中都有一张符号表。

4.4 Hello.o的结果解析

通过objdump -d -r hello.o > helloo_obj.txt命令来获取hello.o的反汇编代码,与之前的hello.s进行比较,可以看出总体没有什么差别,只是在一些细节上有一点不同。

  1. 操作数

hello.s中操作数为十进制

而helloo_obj中操作数为十六进制

  1. 分支转移

在hello.s中,分支跳转是用.L2/.L3/.L4等来标识符来标记。

而在helloo_obj中,跳转地址是用主函数加偏移量来进行书写。

因为标记符是为了便于编写而引入的,在汇编成机器识别的二进制语言后不存在了,只有确定的地址。

  1. 函数调用

在hello.s中,函数调用是函数名加@PLT

而在helloo_obj中,函数调用则是主函数加偏移量

这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器,以确定函数的运行时执行地址。当汇编变成机器语言时,对于这些地址不确定的函数调用,将其调用指令后的相对地址设置为全零(目标地址正好是下一条指令),然后在.rela.text部分为其添加一个重定位条目,等待静态链接的进一步确定。

  1. 全局变量访问

在hello.s中,对于全局变量的访问是通过.LC0和.LC1

而在helloo_obj中,对于全局变量的访问也是通过栈指针,不过此时未进行重定位,全局变量全都初始化为0+%rip

4.5 本章小结

本章查看了可重定位目标文件ELF格式,特别是重定位条目的分析,然后看了hello.o反汇编后与hello.s的不同,了解了机器语言与汇编语言的不同。

5章 链接

5.1 链接的概念与作用

链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器(loader)加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。在现代系统中,链接是由叫做链接器(linker)的程序自动执行的。链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。

5.2 在Ubuntu下链接的命令

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

   ELF可执行文件的常见文件格式:

ELF头

节头部表

.init

.text

.rodata

.data

.bss

.symtab

.debug

.line

.strtab

节头部表

我们先用readelf -a hello > hello.elf命令生成可执行目标文件hello的ELF格式文件hello.elf。之后查看该文件。

ELF头:可执行目标文件hello的格式类似于可重定位目标文件的格式,ELF头描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text、.rodata和.data节与可重定位目标文件中的节是类似的,除了这些节已经被重定位到它们最终的运行时的内存地址外。.init节定义了一个小函数_init,程序初始化代码会调用它。因为可执行文件时完全连接的(已被重定位),所以无.rel节。

节头部表:节头部表描述了不同节的大小和数量,它对hello中所有节的信息进行了说明,包括名称、大小、类型、在程序中的偏移量等等。由于hello为完全链接的程序,因此根据给出的信息即可确定程序实际加载到虚拟地址的地址位置。

5.4 hello的虚拟地址空间

      

       与5.3比较之后,我们可以发现,二者有一一对应的关系。

   

5.5 链接的重定位过程分析

       我们使用命令objdump -d -r hello > hello_obj.txt来对hello进行反汇编,与hello.o的反汇编文件进行比较。

  1. 节数:hello与hello.o比较增加了很多节
  2. 函数个数:hello同样也增加了很多从外部库链接进来的函数

链接分为两个主要任务,一个是符号解析,另一个是重定位。重定位时,链接器首先添加hello的代码和数据,其中包括程序初始化函数.init,以及外部调用函数.plt和.plt.got部分,还有.text中的程序入口点_start,然后将这些符号分配到相应的运行地址。用readelf查看文件信息,hello的反汇编代码在重定位部分后有libc后缀,因为可执行文件是完全串联的,它将程序入口点添加到程序运行时要指向的第一个指令的地址。

3.函数调用:hello中已经对函数完成了重定位,函数跳转目的地址已经明确,而hello.o中函数调用的目标地址不明确,对于这些地址不确定的函数调用,将其调用指令后的相对地址设置为全零(目标地址正好是下一条指令),然后在.rela.text部分为其添加一个重定位条目,等待静态链接的进一步确定。

  1. 虚拟内存地址:hello每行代码前的地址为虚拟内存地址,而hello.o中的都是相对偏移地址。

5.6 hello的执行流程

      

5.7 Hello的动态链接分析

动态链接是将程序按模块划分为相对独立的部分,并在程序运行时将它们链接在一起形成一个完整的程序。虽然动态链接将链接过程推迟到程序运行,但在形成可执行文件时仍需要使用动态链接库。例如,当我们形成一个可执行程序时,我们发现引用了一个外部函数。此时,我们将检查动态链接库,发现函数名是一个动态链接符号。此时,可执行程序不会重新定位符号,而是将进程保留到加载。

hello程序的动态链接项目:global_offset表 。在elf文件中查询:

在edb中观察该地址中的变化:

dl_init操作是给程序赋上当前执行的内存地址偏移量 。

  

5.8 本章小结

本章介绍了hello.o文件链接生成可执行目标文件hello的过程。着重分析了可执行文件的结构,执行流程与动态链接。

6章 hello进程管理

6.1 进程的概念与作用

进程(process)的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

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

Shell概念:

Linux本质上是一个操作系统内核。一般用户不能直接使用内核,而是通过shell与内核进行通信。Shell(命令行解释器)是一个交互式应用程序级程序。它代表用户运行其他程序,是用户与系统内核之间的桥梁。用户可以通过shell向操作系统发送请求,操作系统选择执行该命令。通常,我们使用的shell是bash。在解释命令时,bash不会直接参与解释,而是创建一个新进程来解释命令。Bash只需等待结果,就可以确保Bash进程的安全。

Shell作用:

可以在DOS下使用doskey功能。可以使用方向键快速输入和修改命令。

通过自动搜索匹配项,可以发出以字符串开头的命令。

包括自己的帮助功能。您可以通过在提示符处键入help获得相关帮助。

Shell处理流程:

shell从终端读取用户输入的命令。

根据空间将输入字符串划分为一个小字符串,以获取所有参数

Shell判断:如果用户输入内置命令,将立即执行

如果是外部命令,shell将调用相应的程序,为其分配子进程并运行它

在程序执行期间,外壳应接受键盘输入信号并相应地处理这些信号

6.3 Hello的fork进程创建过程

shell首先会调用 fork 函数创建一个新的运行的子进程,这个子进程就是当前shell的一个副本。新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的 PID。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行新程序。它覆盖当前进程的地址空间,但不创建新进程。新程序仍然具有相同的PID,并继承调用execve函数时打开的所有文件描述符。execve函数加载并运行带有参数列表argv和环境变量list ENVP的可执行对象文件hello。只有在发生错误(例如,找不到Hello)时,Execve才会返回调用方。

6.5 Hello的进程执行

操作系统内核使用一种称为上下文切换的更高级形式的异常控制流来实现多任务。内核为每个进程维护一个上下文。它由一些对象的值组成,包括通用寄存器、浮点寄存器、程序计数器、用户堆栈、状态寄存器、内核堆栈和各种内核数据结构。在进程执行的某个时候,内核可以决定抢占当前进程并重新启动以前抢占的进程。这个决定称为调度。处理器以内核模式运行。当它返回到应用程序代码时,处理器将模式从内核模式更改为用户模式。

6.6 hello的异常与信号处理

1.程序正常执行:

2.程序执行中按下Ctrl-C:

此时我们可以看到Ctr+C终止了hello,并且回收了进程

3.程序执行中按下Ctrl-Z:

通过ps和jobs指令都可以看到后台存在被挂起的hello程序,可以通过fg命令将其调成前台继续执行,说明crtl+z是停止(挂起)前台作业。

4.kill杀死hello进程:

5.pstree运行:

6.程序运行中不停乱按:

可以看出在程序执行过程中乱按实际上是将屏幕输入缓存到stdin,当getchar()读到\n字符时,前面的字符当做命令输入。

6.7本章小结

本章分析了进程管理,理解了fork函数以及execve函数在进程执行过程中的作用。同时通过运行进程时输入的各种命令——ps、pstree、crtl+c、crtl+z、kill、fg等等了解了进程管理。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序生成的与段相关的偏移地址与当前过程数据段的地址相关,与绝对物理地址无关。只有在Intel实模式下,逻辑地址才等于物理地址(因为实模式下没有分段或分页机制,CPU不执行自动地址转换)。应用程序程序员只需要处理逻辑地址,而分段和分页机制仅由系统程序员参与。

线性地址:地址空间是一组有序的非负整数地址。如果地址空间中的整数是连续的,我们称之为线性地址空间。是hello中的虚拟内存地址。

虚拟地址:虚拟地址是系统模拟的不存在的地址,以便于内存管理。对应于磁盘的虚拟页,每个程序的代码段总是从0x400000开始,这也便于程序员对程序进行代码分析。线性地址=逻辑地址+段基址*可以由逻辑地址表示的最大地址数。Linux系统中内核数据段的线性地址从0x0开始,段基址为0,因此Linux系统中的逻辑地址和线性地址相等。

物理地址:它用于内存芯片级单元寻址,与处理器和CPU之间连接的地址总线相对应。计算机系统的主存被组织成一个由m个连续字节大小的单元组成的数组。每个字节都有一个唯一的物理地址。是与运行时hello的虚拟内存地址相对应的物理地址。

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

段式管理:段式管理是指将程序划分为多个段进行存储。每个段都是程序员需要知道和使用的逻辑实体。它的生成直接关系到程序的模块化。段式管理通过段表进行,段表包括段号或段名称、段起点、加载位、段长度等。此外,还需要主存占用面积表和主存可用面积表。

段是程序逻辑的一部分。一组具有完整逻辑意义的程序被划分为一段,因此该段的长度是不确定的。

从逻辑地址到线性地址的转换:

线性地址=逻辑地址+基址

其中,基址=段寄存器中的值×(最大逻辑地址+1)

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

页式管理:将虚拟内存空间和物理内存空间划分为相同大小的页面,如4KB、8KB和16kb。该页被视为内存空间的最小分配单元。由于DRAM缓存是全相联的,因此任何物理页都可以包含任何虚拟页,这可以避免内存浪费。虚拟地址由虚拟页码(VPN)和虚拟页偏移量(VPO)组成。首先,根据VPN找到内存中虚拟地址对应的物理地址的PPN,并与VPO一起形成虚拟地址的物理地址。如果内存中没有相应的PPN,将触发缺页中断。处理完异常处理程序后,从内存中取出PPN并读取物理地址。

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

1、CPU产生一个虚拟地址,并把它传送给MMU。

2、MMU生成PTE地址,并从告诉缓存/主存请求得到它。

3、高速缓存/主存向MMU返回PTE。

4、MMU构造物理地址,并把它传送给高速缓存/主存。

5、高速缓存/主存返回所请求的数据字给处理器。

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

如果物理地址命中L1,则直接读出数据;否则,它将依次从L2、L3和主存中读取。如果没有命中,则从磁盘读取数据并将数据缓存在三级缓存和主存中

7.6 hello进程fork时的内存映射

当fork函数被shell调用时,内核会为hello进程创建各种数据结构并分配给hello唯一的PID。为了给hello创建虚拟内存,内核创建了当前进程的mm_struct、区域结构和样表的原样副本,并将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为写时复制。从上述过程可以看出,在调用fork后和调用execve之前,hello所在进程的内存空间情况和父进程完全一致

7.7 hello进程execve时的内存映射

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

2、映射私有区域。为新程序的代码、数据、bss和栈区创建新的区域结构

3、映射共享区域。

4、设置程序计数器,使之指向代码区域的入口点。

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

缺页故障:当指令引用虚拟地址,虚拟地址对应的物理页不在内存中且必须从磁盘中取出时,将触发缺页故障。

缺页中断处理:1、搜索area structure的链表,将虚拟地址与每个area structure_Start和VM_End中的VM进行比较。如果此指令非法,缺页处理程序将触发段错误并终止进程。

2、确定进程是否具有读取、写入或执行此区域中页面的权限。如果尝试的访问是非法的,缺页处理程序将触发保护异常以终止进程。

3、在这一步,内核知道缺页是由对合法虚拟地址的合法操作引起的。此时,丢失的页面处理程序选择一个牺牲页面。如果修改了牺牲页面,它将交换它,用新页面替换它,并更新页表。当缺页处理程序返回时,CPU重新启动导致缺页的指令,这将再次向MMU发送虚拟地址。

7.9动态存储分配管理

动态存储分配管理由动态内存分配器完成。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区后开始,并向更高的地址。分配器将堆视为一组大小不同的块的集合,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配块显示地保留为供应用程序使用,空闲块可以用来分配。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放。释放可以由应用显示执行,或者由内存分配器隐式执行。

隐式空闲链表:

头部有四个字节。前三个字节存储块的大小。最后一个字节存储当前块是空闲块还是已分配块。0表示空闲块,1表示分配的块。中间有效载荷用于在分配的块中存储信息。最后一个填充部分用于地址对齐和其他要求。隐式链表的结构按照地址由小到大进行连接。每个元素表示一个空闲块或一个分配块。由于空闲块将被合并,链表中的元素必须通过空闲块的分配块交替连接。

显示空闲链表:

显示结构在空闲块中增加了8个字节,分别保存当前空闲块的前驱空闲块的地址和后继空闲块的地址。形成一个双向空闲链表。这样使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。而维护方式有两种:一种方法是用后进先出( LIFO ) 的顺序维护链表, 将新释放的块放置在链表的开始处。

使用LIFO 的顺序和首次适配的放置策略, 分配器会最先检查最近使用过的块。在这种情况下, 释放一个块可以在常数时间内完成。如果使用了边界标记,那么合并也可以在常数时间内完成。另一种方法是按照地址顺序来维护链表, 其中链表中每个块的地址都小于它后继的地址。在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序的首次适配比LIFO 排序的首次适配有更高的内存利用率, 接近最佳适配的利用率。一般而言,显式链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。这就导致了更大的最小块大小,也潜在地提高了内部碎片的程度。

7.10本章小结

本章我们讨论了intel的段式管理、页式管理,介绍了VAPA的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

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

8.2 简述Unix IO接口及其函数

Unix I/O接口统一操作:

  1. 打开文件。一个应用程序通过要求内核打开相应的文件, 来宣告它想要访问一个I/O 设备。内核返回一个小的非负整数, 叫做描述符, 它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  2. Linuxshell 创建的每个进程开始时都有三个打开的文件;标准输入(描述符为0 )、标准输出(描述符为1)和标准错误(描述符为2)。头文件< unistd.h> 定义了常量STDIN_FILENO 、STCOUT_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. ssize_t read(int fd,void *buf,size_t n):read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF, 否则返回值表示的是实际传送的字节数量。
  3. ssize_t wirte(int fd,const void *buf,size_t n):write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
  4. int close(int fd):fd是需要关闭的文件的描述符,close返回操作结果。

8.3 printf的实现分析

Printf函数:

  1. int printf(const char *fmt, ...)  
  2. {  
  3. int i;  
  4. char buf[256];  
  5.      
  6.      va_list arg = (va_list)((char*)(&fmt) + 4);  
  7.      i = vsprintf(buf, fmt, arg);  
  8.      write(buf, i);  
  9.      
  10.      return i;  
  11.     }  

printf函数功能:接受字符串指针数组fmt,然后将匹配到的参数按照fmt格式输出。

vsprintf函数:

  1. int vsprintf(char *buf, const char *fmt, va_list args)   
  2.    {   
  3.     char* p;   
  4.     char tmp[256];   
  5.     va_list p_next_arg = args;   
  6.      
  7.     for (p=buf;*fmt;fmt++) {   
  8.     if (*fmt != '%') {   
  9.     *p++ = *fmt;   
  10.     continue;   
  11.     }   
  12.      
  13.     fmt++;   
  14.      
  15.     switch (*fmt) {   
  16.     case 'x':   
  17.     itoa(tmp, *((int*)p_next_arg));   
  18.     strcpy(p, tmp);   
  19.     p_next_arg += 4;   
  20.     p += strlen(tmp);   
  21.     break;   
  22.     case 's':   
  23.     break;   
  24.     default:   
  25.     break;   
  26.     }   
  27.     }   
  28.      
  29.     return (p - buf);   
  30.    }   

vsprintf的作用是格式化。接受确定输出格式的格式字符串fnt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,并返回要打印的字符串的长度。

write函数中,先给寄存器传了几个参数,然后通过系统调用sys_call。

syscall函数将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。

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

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

8.4 getchar的实现分析

  1. int getchar(void)  
  2.   
  3. {  
  4.   
  5.     static char buf[BUFSIZ];  
  6.   
  7.     static char* bb = buf;  
  8.   
  9.     static int n = 0;  
  10.   
  11.     if(n == 0) {  
  12.   
  13.         n = read(0,buf,BUFSIZ);  
  14.   
  15.         bb= buf;  
  16.   
  17.     }  
  18.   
  19.     return (--n >= 0) ? (unsigned char)*bb++ : EOF;  
  20.   

getchar函数调用read函数,将整个缓冲区都读到buf里,并将缓冲区的长度赋值给n。返回时返回buf的第一个元素,除非n<0。

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

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

8.5本章小结

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

结论

Hello从几行c语言代码到执行完毕一共有一下几个过程:

1)编辑:在编辑器中输入hello的c代码,保存为hello.c文件。

2)预处理:在预处理器中,进行展开头文件、去掉注释等一系列操作,得到hello.i文件。

3)编译:在编译器中,对hello进行逐条语句分析,将hello.c变成了汇编语言文件hello.s。

4)汇编:在汇编器中,hello.s被翻译为机器语言指令,并被打包为可重定位目标文件hello.o。

5)链接: hello调用了标准C库的一些函数,链接器将这些函数与hello.o程链接到一起,结果就得到了hello的可执行程序。

6)进程创建:可以调用fork或者exceve创建hello进程。

7)进程切换:hello在运行时会调用sleep函数,这时CPU会切换到其他进程,直到sleep结束,hello发出信号,又再经过上下文切换,回到hello程序,继续执行sleep的下一条语句。

8)虚拟地址转化为物理地址:hello在执行过程中,利用TLB、页表等,将虚拟地址转化为物理地址,读出物理页的内容。

9) 发出/接收信号:如果运行中键入Ctrl + C或Ctrl + Z,则调用shell的信号处理函数分别停止、挂起。

10) 结束进程:exit,hello的父进程回收hello,内核也删除它的所有数据。

通过计算机系统这门课,我学习到了一段代码是如何被执行的,详细学习了执行过程中的各个流程。计算机系统是一个严密的系统,对于处理程序的每一步都有着严格的规定,程序就在这个系统中有条不紊的执行下去。

附件

hello.c:hello源程序

hello.i:修改了的源程序

hello.s:汇编程序

hello.o:可重定位目标程序

hello:可执行目标程序

hello_o.elf:可重定位目标文件hello.o的ELF格式文件

hello.elf:可执行目标文件hello的ELF格式文件

helloo_obj.txt:hello.o程序的反汇编文件

hello_obj.txt:hello程序的反汇编文件

参考文献

[1]  Bryant,R.E.   Computer Systems:A Programmer's Perspective  2016

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

[3]计算机系统课程ppt实验ppt等课程资料

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值