【无标题】

 

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业     计算机科学与技术     

学     号    2021113296            

班     级    21W0312              

学       生    王鹏博             

指 导 教 师    史先俊                

计算机科学与技术学院

2022年5月

摘  要

本文分析程序hello从编译到执行的过程的整个生命周期,包括了使用toolchain进行预处理、编译、汇编、连接的过程,以及程序执行过程中系统对hello程序的管理过程

关键词:编译;Linux;计算机系统;系统调用;                         

目  录

第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简介

P2P:Program to Progress。

普遍来讲,父进程会通过fork建立新的进程,通过exec函数族,进行相关系统调用,载入新的程序,并分配时间片。

020:Zero to Zero

本来没有程序,而是通过程序员编写的源代码出发,通过工具链预处理、编译、汇编、链接,形成可执行elf。之后通过父进程执行相关系统调用执行,结束后,通过父进程回收,内核删除相关信息,什么也不会留下。

1.2 环境与工具

硬件环境:AMD Ryzen 7 5800H  (Family 19h)

操作系统:Gentoo Linux with Linux kernel 5.19.7

Compiler:

gcc version 11.3.0 (Gentoo 11.3.0 p7) 

                    LLVM/clang version 15.0.3

开发工具:

emacs 28.1

                    GNU objdump (Gentoo 2.38 p4) 2.38等

1.3 中间结果

    hello.i:hello预处理后的文件

hello.s:hello.i编译后的文件

hello.o:hello.s汇编后的文件

hello:hello.o链接后的文件

hello.o.elf:hello.o的ELF输出文件

hello.o.asm:hello.o的反汇编文件

hello.elf:hello的ELF文件

hello.asm:hello的反汇编文件

1.4 本章小结

简述P2P 020概念,环境以及中间产物。

第2章 预处理

2.1 预处理的概念与作用

预处理是源文件编译前所做的预备工作,预处理是要借助预处理程序,进行预处理指令的解析。

其可以提供宏定义、include引入文件、条件编译等

2.2预处理的命令

gcc -E hello.c -o hello.i

 

2.3 Hello的预处理结果解析

 

   按照include等预处理指令进行预处理,忽略原本文件的注释,插入原文件include进来的各种文件

       我们可以看到,经过预处理后的文件已经达到3000+行,这说明#include过程中会引入大量头文件。

2.4 本章小结

       本章介绍对于预处理的概念与作用,并对得到的程序进行简单分析。

cpp能够为c语言程序提供对于include define等预处理指令的动作。

第3章 编译

3.1 编译的概念与作用

编译的过程是将.i中的 c语言(以及控制部分)转换为汇编语言的过程。

这里能够将代码初步转换为汇编语言,但仍然存在

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

3.2编译的命令

 

gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

3.3.1 常量

       对于普通的int float等非字符串常量,一般的,我们认为,常量会存在于指令中的立即数中。

 

 

比如此处 argc !=4中的4就对应着汇编中的$4

但是 我们认为,并非所有的常量在c与asm中都是一一对应的。

譬如下图中,i<9实际上被编译成为i<=8

 

包括数组下标,都会出现asm中无对应常量的情况。

3.3.2 字符串

普遍的,对于常量字符串,会被放到rodata段中

3.3.3 变量

对于C语言中的变量,很难在汇编语言中找到明显的固定的符号来表示变量。

(我们此处的变量以及下文的变量包括局部变量、全局变量、以及传参过程中的变量)

本程序(hello.c)中存在的正确定义的局部变量为i

根据上图循环过程中,我们可以看出,i被认为是栈空间上-4(%rbp)这一部分。

3.3.4 算数运算

同样见上图循环中的i的操作

对于i++,编译器将其处理成为

addl    $1, -4(%rbp)

3.3.5 赋值

在程序执行过程中,包括了显性的对变量赋值,同时也有隐性的对参数变量赋值,如下:

 

此处为对i进行赋初值0

 

此处为对printf的各参数进行赋值,准备接下来的函数调用。

3.3.6 数组/指针

见上图,使用某一地址加上偏移量代表所需元素的地址,之后通过间接寻址进行访问。

在其他程序中,我们可能会遇到直接进行间接寻址的操作。

3.3.7 控制转移语句

 

 

对于for循环,通过对变量与8的比较判断是否转移。

cmpl会写标志寄存器,jle会读出标志寄存器来判断是否进行跳转。

3.3.8 函数调用

此程序我们可以看到多处函数调用

 

对于函数调用,在asm中可以显著分为传参、调用call、取返回值三个部分。

传参在上赋值部分已经说明。

call在此处只是对相关函数名进行调用

 

对于返回值,其存储在寄存器%eax上,会发生相关取值操作

3.4 本章小结

本章主要涵盖了对于汇编语言的概念与作用,并分析了c语言与汇编语言的对应关系。

ccl提供对编译为汇编语言的实现

第4章 汇编

4.1 汇编的概念与作用

       汇编是指从汇编语言翻译到机器语言/二进制文件的过程。此处linux下是将汇编翻译为可重定位的linux elf文件。

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

4.2汇编的命令

 

4.3 可重定位目标elf格式

 

使用readelf -h hello.o命令,得到ELF Header。

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

 

使用readelf -S hello.o,得出Section headers

Section Headers:描述不同节的位置和大小,包括节名称,节的类型,节的属性(读写权限),节在ELF文件中所占的长度,节的对齐方式以及节的偏移量。

 

使用readelf -s hello.o, 得到符号表

符号表.symtab:存放在程序中定义和引用的函数和全局变量的信息。

4.4 Hello.o的结果解析

 

objdump -d -r hello.o 

我们可以看到,反汇编得到的与hello.s并不完全相同

首先,反汇编中没有段名称,而是使用相对地址进行跳转。

其次,对于函数调用,汇编语言中使用的是函数的名称,而此处使用的是重定位条目,等待确定地址。

并且,对于反汇编得到的指令,操作数为16进制而不是10进制

4.5 本章小结

本章介绍了汇编的概念以及作用,分析了重定位elf的各个部分以及作用,分析了反汇编得到的asm与s文件中的asm的区别。

5章 链接

5.1 链接的概念与作用

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

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

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

5.2链接的命令

ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib64/crt1.o /usr/lib64/crti.o hello.o /usr/lib64/libc.so /usr/lib64/crtn.o

 

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

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

分别使用readelf -a 读出hello于hello.o文件的elf格式文件,并进行对比

 

我们可以发现 区别如下

Elf格式不同 可执行的和可重定位的

Entry point 不同 程序入口正确被确定

Header大小不同 正确被加入可执行elf的程序头

Section headers 数量不同 实际上引入了更多的节

5.4 hello的虚拟地址空间

      

 

我们可以根据上文的结果判断程序加载的位置为0x401000

同样的,我们可以通过上节内容在此找出各节的位置。

5.5 链接的重定位过程分析

 

我们可以看到 两者的main函数几乎是相同的,除了地址的相对偏移改为了绝对地址。

头部插入了其他函数,所有重定位部分已经被具体的地址替代。

5.6 hello的执行流程

子程序名如下:

ld-2.27.so!_dl_start

ld-2.27.so!_dl_init

hello!_start

libc-2.27.so!__libc_start_main

libc-2.27.so!__cxa_atexit

libc-2.27.so!__libc_csu_init

libc-2.27.so!_setjmp

hello!main

hello!puts@plt

hello!exit@plt

hello!printf@plt

hello!sleep@plt

hello!getchar@plt

ld-2.27.so!_dl_runtime_resolve_xsave

ld-2.27.so!_dl_fixup

ld-2.27.so!_dl_lookup_symbol_x

libc-2.27.so!exit

5.7 Hello的动态链接分析

根据ELF格式中.rela.plt的内存位置查看需要动态链接的函数的地址:

 

其在dl_init前后的内容发生了突变,其中一个地址指向的内容是重定位表,另一个指向的是动态链接器运行的地址。重定位表可用来确定调用函数的地址,动态链接器则是进行动态链接。

5.8 本章小结

了解了hello.o变成hello可执行程序时的链接,熟悉了链接、反汇编的指令,并且对链接、运行分析。

6章 hello进程管理

6.1 进程的概念与作用

进程就是一个执行中程序的实例。系统中每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态所组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

进程提供给应用程序的关键抽象:一个独立的逻辑控制流,好像程序独占处理器;一个私有的地址空间,好像程序独占内存系统。

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

shell是一个交互型的应用级程序,它代表用户运行其他程序,解释命令

shell处理中包括了:

  1. 读入命令:读取用户输入的命令
  2. 语法分析:对用户的指令进分析
  3. 内部指令执行:判断是否为内部指令,若是则在内部进行操作,执行后跳回1
  4. 外部程序调用:使用fork与exec家族函数进行系统调用,把执行程序调入内存。
  5. 接受signal:通过signal决定对程序执行的操作,如放到前台 杀死进程(同样通过signal实现)
  6. 使用wait家族函数等待程序执行结束

6.3 Hello的fork进程创建过程

进程通过调用fork函数创建一个新的运行的子进程。父进程判断输入命令执行hello不是内部指令,创造一个子进程子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的 逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。

6.4 Hello的execve过程

   execve函数在当前进程的上下文中加载并运行hello程序,且带参数列表argv和环境变量envp。在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,其中包含以下几个步骤:

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

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

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

(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

6.5 Hello的进程执行

进程提供给应用程序的抽象:

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

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

1、逻辑控制流:如果想用调试器单步执行程序,我们会看到一系列的程序计数器的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流。进程是轮流 使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占 (暂时挂起),然后轮到其他进程。

2、并发流:一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。多个流并发地执行的一般现象被称为并发。

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

4、私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。

5、用户模式和内核模式::处理器通常是用某个控制寄存器中的一个模式位来提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

6、上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。

7、上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:

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

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

3)将控制传递给这 个新恢复的进程 ,来完成上下文切换。

6.6 hello的异常与信号处理

       hello执行过程中可能出现三类异常:陷阱、故障和终止。

陷阱是执行指令的结果,例如打开文件的open()函数或者产生子进程的fork()函数。故障是非有意的结果,但是可以被修复,比如对于数组的越界访问。而终止则是非有意发生的致命错误,常常不可修复。

会产生的信号有许多许多种,他们都以SIG开头,代表着不同的含义,比如:SIGINT是来自键盘的终止信号,SIGKILL是杀死程序的终止信号 ,SIGSEGV是由于段错误原因终止程序的信号,SIGALRM是由于计时器终止程序的信号,SIGCHLD 是子进程停止发出的一个信号,一般被忽略。

1. Ctrl-Z

 

程序会收到SIGSTP信号,从而暂停运行

 

Ps中,我们仍然能够看到此程序,说明程序没有终止。

我们执行(fg) %./hello

 

程序会在前台继续执行

如果在程序suspend时使用pstree, 我们可以看到此程序为我们当前zsh的子进程

 

使用jobs可以看到在后台的程序

 

此时 我们也可以使用kill杀死程序。

 

2. Ctrl-C

 

程序执行时,按下Ctrl-C 发送SIGINT信号,程序终止。

3.执行时输入字符

 

执行时输入的字符将会被放入缓冲区,如果存在\n,使得getchar结束执行,剩下的字符将会直接进入shell。

6.7本章小结

通过本章,我们了解执行程序过程中的进程机制、exec函数族的机制、shell的功能、异常信号机制,初步理解了程序执行在用户态与内核态发生的表层操作。

7章 hello的存储管理

7.1 hello的存储器地址空间

物理地址:内储存器之中实际有效的地址。

逻辑地址:指令给出的地址,实际上是一个相对地址,需要通过寻址方式的计算和变化才能得到实际的物理地址。

线性地址:线性地址是逻辑地址到物理地址的中间层。由于逻辑地址相当于一个偏移量,线性地址就是基址加上这个偏移量。但是由于有分页方式的存在,线性地址还要经过一个变换才能变为物理地址。

虚拟地址:虚拟地址是当CPU启动保护模式之后,物理内存映射到磁盘的一个虚拟空间,这个映射由一套硬件、软件的完整系统来实现。

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

从逻辑地址转化到线性地址是通过分段机制来进行的。程序通过分段划分为多个模块,如代码段、数据段、共享段:可以分别编写和编译;可以针对不同类型的段采取不同的保护;可以按段为单位来进行共享,包括通过动态链接进行代码共享。这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。

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

线性地址到物理地址的变换是通过分页模式来执行的。线性地址分为三个字段:页目录表项指针、页表项指针和页内偏移量。控制寄存器(CR3)保存了页目录的起始地址。

n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。

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

翻译后备缓冲器(TLB):每次CPU产生一个虚拟地址,MMU就必须查阅一个PIE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会要求从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就下降到1个或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器。

多级页表:虚拟地址被划分成k个VPN和1个VPO。每个VPNi都是一个到第i级页表的索引。

TLB与四级页表支持下的VA到PA的变换:开始时,MMU从虚拟地址中抽取出VPN,并检查TLB,看它是否因为前面的某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLBT和TLBI,进行匹配,如果命中,将缓存的PPN返回给MMU,得到PPN和VPO组成的PA;如果不命中,TLB 中没有命中,MMU 向页表中查询确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存 中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。

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

Cache1和Cache2、Cache3工作方式相同,故以Cache1为例:

因为每个页面是2^6 = 64字节,所以L1 Cache的低6位作为PPO,高6位作为PPN。这样L1 Cache每个块中都是4个字节,所以块的低2位作为块偏移(CO),接下来4位来表示组索引(CI),剩下的作为标记(CT)。

MMU发送物理地址给L1 Cache,L1 Cache从物理地址中取出CO、CI、CT。如果标记与CT相匹配,为命中,读出CO处的数据字节,把它返回给MMU,MMU把它传递回CPU。如果不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的 放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。

7.6 hello进程fork时的内存映射

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

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

7.7 hello进程execve时的内存映射

当创建了子进程了之后,execve函数在当前进程的上下文中加载并运行hello程序:

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

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

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

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

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

缺页故障:当指令引用了一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生故障。

缺页中断处理:缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成了。具体来说,就是若操作合法,则缺页处理程序从物理内存中确定一个牺牲页,若该牺牲页被修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处理程序返回时,CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存将所请求字返回给处理器。

7.9动态存储分配管理

当进程运行时需要额外虚拟内存时,用动态内存分配器更方便、可移植性更好。

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

分配器有两种基本风格:

显式分配器:要求应用显式地释放任何已分配的块,比如说C标准库提供一种叫做malloc程序包的显示分配器。

隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的过程叫做垃圾收集。

显式分配器的要求和目标:

1.处理任意请求序列;2.立即响应请求;3.只使用堆;4.对齐块;5.不修改已分配的块。

目标1:最大化吞吐率;目标2:最大内存利用率。

隐式空闲链表:

假设块的格式如下图所示,我们可以将堆组织为一个连续的已分配块和空闲块的序列,这种结构被称为隐式空闲链表:

(1)当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求块的空闲块。分配器执行这种搜索方式是由放置策略决定的。

(2)分割空闲块:一旦分配器找到一个匹配的空闲块,它就必须做另一个策略决定,那就是分配这个空闲块中多少空间。

(3)获取额外的堆内存:如果分配器不能为请求块找到合适的空闲块时,一个选择是通过合并那些在内存中物理上相邻的空闲块来创建一些更大的空闲块;如果上一个选择行不通,分配器就会通过调用sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化成一个大的空闲块,将这个块插入到空闲链表中,然后将被请求的块放置在这个新的空闲块中。

(4)合并空闲块:当分配器释放一个已分配块时,可能会引起一种现象,叫做假碎片,就是许多可用的空闲块被切割成为小的、无法使用的空闲块。为了解决这个问题,任何实际的分配器都必须合并相邻的空闲块,这个过程称为合并。利用边界标记,允许在常数时间内进行对前面块的合并。

显式空闲链表:显示空闲链表是将空闲块组织为某种形式的显示数据结构。堆可以组织成一个双向空闲链表,每个空闲块中,都包含一个pred(前驱)指针和succ(后继)指针。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线 性时间减少到了空闲块数量的线性时间。一种方法使用后进先出的顺序维护链表,将新释放的块在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要 线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比 LIFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。一般而言,显式链表的缺点是空闲块必须足够大,这就导致了更大的最小块。

7.10本章小结

本章主要介绍hello的存储器地址空间、四种地址,阐述了Intel逻辑地址到线性地址的变换-段式管理、Hello的线性地址到物理地址的变换-页式管理、TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问、hello进程fork时的内存映射、hello进程execve时的内存映射、缺页故障与缺页中断处理以及重点介绍了动态存储分配管理。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

B0,B1,B2……Bm

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

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix I/O 接口:

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

2.Shell 创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。头文件< unistd.h> 定义了常量STDIN_FILENO、STDOUT_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. int close(fd),fd是需要关闭的文件的描述符。
  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的当前文件位置。

8.3 printf的实现分析

从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函数的实现。

结论

hello所经历的过程:

  1. 编程:通过编程软件编写出hello.c;
  2. 预处理:预处理hello.c得到hello.i;
  3. 编译:编译hello.i得到hello.s;
  4. 汇编:汇编hello.s得到hello.o;
  5. 链接:链接hello.o与可重定位文件、动态链接库得到hello;
  6. 加载运行:通过Shell输入./hello 7203610202 熊杨逸磊 1,内核调用fork函数为hello创建一个子进程,调用execve函数把代码和数据载入虚拟空间,开始执行;
  7. 上下文切换:hello调用sleep函数后进入休眠状态,内核通过上下文切换将控制权给其他进程,hello到达设定的时间量后,内核再通过上下文切换将控制权还给hello;
  8. 动态内存申请:hello调用printf函数之后,malloc函数会向动态内存分配器申请堆中的内存;
  9. 接收信号:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
  10. 终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有 数据结构。

附件

hello.i:hello预处理后的文件

hello.s:hello.i编译后的文件

hello.o:hello.s汇编后的文件

hello:hello.o链接后的文件

hello.o.elf:hello.o的ELF输出文件

hello.o.asm:hello.o的反汇编文件

hello.elf:hello的ELF文件

hello.asm:hello的反汇编文件

参考文献

[1]readelf命令使用说明:readelf命令使用说明_木虫下的博客-CSDN博客_readelf

[2]重定位节:重定位节 - 链接程序和库指南

[3]Shell流程详解:Shell执行流程详细解释 - 百度文库

[4]逻辑地址:逻辑地址_百度百科

[5]线性地址:线性地址_百度百科

[6]物理地址:物理地址(CPU中相关术语)_百度百科

[7] Randal E.Bryant, David R.O'Hallaron 深入理解计算机系统[M]. 北京:机械工业出版社,1992:25-42.

[8] 代码交流

[9] 存储管理-段式管理 - 梦小冷 - 博客园 存储管理-段式管理

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值