Hello‘s P2P—一个普通程序出生入死的一生

在这里插入图片描述

计算机系统

大作业

题 目 程序人生-Hello’s P2P

专 业 人工智能

学 号 2021120020

班 级 21E0361

学 生 吕佳琦

指 导 教 师 史先俊

计算机科学与技术学院

2022年5月

摘 要

本文将深入计算机系统中去探究hello的生命周期,看看它是如何从一个程序员手里的C语言程序一步步经过预处理、编译、汇编、链接的一步步“成长”,最后成为一个真正的可执行文件,以及这个可执行文件又是如何在操作系统的一步步推动下完成自己的使命最终被回收整个的过程。

关键词: 计算机系统;编译;进程管理

目 录

第1章 概述 - 4 -

1.1 Hello简介 - 4 -

1.2 环境与工具 - 4 -

1.3 中间结果 - 5 -

1.4 本章小结 - 5 -

第2章 预处理 - 6 -

2.1 预处理的概念与作用 - 6 -

2.2在Ubuntu下预处理的命令 - 6 -

2.3 Hello的预处理结果解析 - 7 -

2.4 本章小结 - 7 -

3.1 编译的概念与作用 - 8 -

3.2 在Ubuntu下编译的命令 - 8 -

3.3 Hello的编译结果解析 - 9 -

3.3.1 函数执行前 - 9 -

3.3.2 函数执行过程 - 9 -

3.3.3 常量及局部变量存储 - 10 -

3.3.4 赋值及算术操作 - 11 -

3.3.5 选择及循环控制语句(控制转移操作) - 11 -

3.3.5 数组操作 - 12 -

3.3.6 函数操作 - 12 -

3.4 本章小结 - 14 -

4.1 汇编的概念与作用 - 15 -

4.2 在Ubuntu下汇编的命令 - 15 -

4.3 可重定位目标elf格式 - 15 -

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

4.5 本章小结 - 19 -

第5章 链接 - 20 -

5.1 链接的概念与作用 - 20 -

5.2 在Ubuntu下链接的命令 - 20 -

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

5.4 hello的虚拟地址空间 - 23 -

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

5.6 hello的执行流程 - 25 -

5.7 Hello的动态链接分析 - 26 -

5.8 本章小结 - 26 -

第6章 hello进程管理 - 27 -

6.1 进程的概念与作用 - 27 -

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

6.3 Hello的fork进程创建过程 - 27 -

6.4 Hello的execve过程 - 28 -

6.5 Hello的进程执行 - 28 -

6.6 hello的异常与信号处理 - 29 -

6.7本章小结 - 30 -

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时的内存映射 - 33 -

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

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

7.9动态存储分配管理 - 34 -

7.10本章小结 - 35 -

第8章 hello的IO管理 - 36 -

8.1 Linux的IO设备管理方法 - 36 -

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

8.3 printf的实现分析 - 37 -

8.4 getchar的实现分析 - 38 -

8.5本章小结 - 38 -

结论 - 39 -

参考文献 - 41 -

第1章 概述

1.1 Hello简介

hello的生命周期从一个高级C语言程序开始,而为了能在系统上运行hello.c程序,系统需要一步步的努力将其转化为对应的低级机器语言指令,这些指令最终会打包为可执行文件。具体操作为预处理、编译、汇编、链接四个阶段。

在可执行文件继续执行的时候,shell的execve过程会先删除当前虚拟地址的数据结构,为hello创建新的区域结构,然后映射共享区域,设置程序计数器,映射虚拟内存,最后加载物理内存。随后进入main函数执行目标代码。CPU为他分配时间片执行逻辑控制流。hello执行结束后,shell父进程负责回收该子进程,删除其存在痕迹,一切归零。

1.2 环境与工具

硬件环境:

处理器:Intel® Core™ i5-6200U CPU @ 2.30GHz 2.40 GHz

机带:RAM 8.00 GB (7.90 GB 可用)

系统类型:64位操作系统, 基于x64 的处理器

软件环境:

操作系统:Win10 64位

虚拟机:VMware Workstation Pro 12.5.5

Linux系统:Ubuntu 20.04.4 LTS

开发与调试工具:

codeblocks、gcc、dgb、edb、vim等

1.3 中间结果

表格 1 中间结果

文件名功能
hello.i预处理后得到的文本文件
hello.s编译后得到的汇编语言文件
hello.o汇编后得到的可重定位目标文件
hello.elf用readelf读取hello.o得到的ELF格式信息
hello.asm反汇编hello.o得到的反汇编文件
hello_.elf由hello可执行文件生成的.elf文件
hello_.s由可执行文件编译后得到的汇编语言文件
hello2.asm反汇编hello可执行文件得到的反汇编文件

1.4 本章小结

本章是简单介绍了hello从C语言整个生命周期的大致过程,以及对中间结果和具体软硬件环境做出了相关介绍。

第2章 预处理

2.1 预处理的概念与作用

预处理器(cpp C Pre-Processor)根据以字符#开头的命令,修改原始的C程序。C语言的预处理主要有三个方面的内容:宏定义,文件包含以及条件编译。比如hello.c中第1行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插人程序文本中。类似的#define指令定义了一个宏—用来代表其他东西的一个命令,通常是某一个类型的常量。预处理会通过将宏的名字和它的定义存储在一起来响应#define指令,预处理的结果是我们会得到了另一个C程序,通常是以i作为文件扩展名。

预处理是C语言的一个重要功能, 它由预处理程序负责完成。当对一个源文件进行编译时, 系统把自动引用预处理程序对源程序中的预处理部分作处理, 处理完毕自动进入对源程序的编译。在嵌入式系统编程中不管是内核的驱动程序还是应用程序的编写, 涉及大量的预处理与条件编译, 这样做的好处主要体现在代码的移植性强以及代码的修改方便等方面。因此引入了预处理与条件编译的概念[1]。

2.2在Ubuntu下预处理的命令

Ubuntu下我们可以使用cpp hello.c > hello.i来将hello.c预处理
在这里插入图片描述

图1 预处理过程展示

2.3 Hello的预处理结果解析

在ubuntu下打开文件,可以看到预处理之后的文件变成了3060行,最下方存储者我们所书写的原始代码,而我们的#include <stdio.h>、#include <unistd.h>、#include <stdlib.h>三个头文件已经消失不见,以#include<stdio.h>举例,这条指令指示预处理器打开一个名字为stdio.h的文件, 并将它的内容加到当前的程序中。hello.i文件中多出的便是对应的三条#include指令集的展开,除此之外,CPP处理宏定义命令(#define)、条件编译语句以及进行删除程序中的注释和多余的空白字符等操作,并对一些值进行替换。

在这里插入图片描述

图 2 预处理结果部分展示

2.4 本章小结

本章主要介绍了预处理的概念及作用、并在Ubuntu系统下利用指令将hello.c转换成hello.i文件并在Ubuntu下打开实际预处理之后得到的hello.i程序,对预处理结果进行了解析。

第3章 编译

3.1 编译的概念与作用

编译器(ccl)将文本文件hello.i翻译咸文本文件hello.s,它包含一个汇编语言程序。该程序包含函数main的定义,每条语旬都以一种文本格式描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。

编译过程各阶段和作用可以细分如下词法分析阶段:读入源程序:对构成源程序的字符流进行扫描和分解,识别出单词、语法分析阶段:机器通过词法分析,将单词序列分解成不同的语法短语,确定整个输入串能够构成语法上正确的程序。语义分析阶段:检查源程序上有没有语义错误,在代码生成阶段收集类型信息、中间代码生成阶段:在进行了上述的语法分析和语义分析阶段的工作之后,有的编译程序将源程序变成一种内部表示形式、代码优化:这一阶段的任务是对前一阶段产生的中间代码进行变换或进行改造,目的是使生成的目标代码更为高效,即省时间和省空间、目标代码生成:这一阶段的任务是把中间代码变换成特定机器上的绝对指令代码或可重定位的指令代码或汇编指令代码。

3.2 在Ubuntu下编译的命令

可以使用gcc -S hello.c -o hello.s

在这里插入图片描述

图3 编译过程

3.3 Hello的编译结果解析

3.3.1 函数执行前

在这里插入图片描述

图4 部分编译结果(main函数执行前)

可以看到函数执行前hello.s的相关操作如下:

(1).fire 表明读入的原文件为hello.c

(2).LC意为Local Constant,上图记录了其保存的对应变量,如对中文进行转码后的字符串变量.string,全局变量.globl(main函数)等

(3).type表示对应的是函数类型或对象类型,显然main是@function(全局)类型变量

3.3.2 函数执行过程

在这里插入图片描述\

图4 函数执行编译结果

编译后的main函数内容如上图所示,.LFB(Local Function Beginning)标识了函数的开始,.LFE(Local Function Ending)标识了函数的结束,中间过程为main函数执行过程,接下来对其一一解析。

3.3.3 常量及局部变量存储

常量:在编译后.section .rodata存放C语言文件中的只读变量,我们可以看到对应的位置存放了,printf函数的传入的参数“用法:Hello 学号 姓名 秒数!\n”和“Hello %s %s\n”(均为字符串常量)

在这里插入图片描述

图5 字符串常量的存储

局部变量:分析原C语言代码可以看到代码中书写的局部变量有int argc、char *argv[]、int i三个,局部变量被存储在栈中

在这里插入图片描述

图6 局部变量的存储

mov指令是传送指令。代表着表示把一个字节、字或双字的操作数从源位置传送到目的位置,不同的后缀代表不同的变量位数,movb(8位)、movw(16位)、movl(32位)、movq(64位)。分析代码可知argc传送到了(rbp-20)地址中,i被传送到了(rbp-4)地址中,argv被传送到了(rbp-32)地址中。

3.3.4 赋值及算术操作

在hello.s中所涉及到的各类赋值算术语句如下

在这里插入图片描述

图7 赋值算术语句

其中add s,d表示d=d+s、sub s,d表示d=d-s, subq

而在对应的汇编代码中 $32, %rsp表示开辟栈帧、addq $16, %rax用于修改地址偏移量、addl $1, -4(%rbp)为实现对应的i++的操作

3.3.5 选择及循环控制语句(控制转移操作)

C语言中存在判断语句if(arcg!=4)和循环语句for(i=0;i<8,i++),我们可以找到对应的汇编代码中的对应表示

在这里插入图片描述

图8 选择跳转语句

首先对相关指令进行介绍,CMP为比较指令(两操作数作减法,仅修改标志位,不回送结果)、JE等于转移指令,所以hello.s对应代码块的含义为,将4与rbp-20地址中元素进行比较(argc变量),如果相等,则跳到代码块.L2执行,JMP为无条件转移指令,执行完.L2代码块后将继续跳转至.L3执行
在这里插入图片描述

图9 循环部分语句

其中(%rbp-4)地址中存放的是局部变量i,每次将i加1,在L3中将i和7进行比较,如果小于等于7就跳转回L4,继续执行循环,如果大于7就跳出循环,继续往下执行。

3.3.5 数组操作

在这里插入图片描述

图10 数组操作部分

汇编代码中使用首地址+偏移量的方式来访问数组元素,数组首地址存储在%rbp-32的位置,通过将首地址加8获得argv[1]的地址,将首地址加16获得argv[2]的地址。

3.3.6 函数操作

在这里插入图片描述

图11 hello.c main函数

我们逐个分析hello.c文件中的各个函数是如何被加载执行返回的,首先关注需要了解的编译代码:.cfi_startproc 定义函数开始,.cfi_endproc 定义函数结束,lea装入有效地址,如LEA DX,string代表把偏移地址存到DX、call表示过程调用,ret代表过程返回。

大部分的参数传递通过寄存器实现,通过寄存器最多传递6个参数,按照顺序依次为%rdi、%rsi、%rdx、%rcx、%r8、%r9。多余的参数通过栈来传递。所有的函数调用都转换成了指令call,后面跟着调用函数的名字。

在这里插入图片描述

图12 参数传递与函数调用

图12展示了参数传递与函数调用的整个过程,蓝框部分为参数传递,红框部分为函数调用,例如printf函数的执行过程即为使用寄存器%edi、%rsi、%rdx传递参数,在通过call进行调用。其中%rdi保存的是“Hello %s %s\n”的首地址,%rsi保存的是argv[1],%rdx保存的是argv[2]

最后是函数的返回,编译时,在函数的最后使用leave来退出、添加指令ret来实现函数的返回。我们可以从汇编代码中看到main函数的返回。
在这里插入图片描述

图13 main函数返回

3.4 本章小结

本章介绍了编译的概念与作用,编译是将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备的过程。本章以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作,包括常量及局部变量存储、赋值及算术操作、选择及循环控制语句(控制转移操作)、数组操作、函数操作等部分的相关介绍

第4章 汇编

4.1 汇编的概念与作用

在编译结束后,汇编器(as)会将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的17个字节是函数main 的指令编码。

汇编的作用是将汇编指令根据汇编指令和机器指令的对照表一一翻译转换成机器可以直接读取分析的机器指令,使之在链接后能够被计算机直接执行

4.2 在Ubuntu下汇编的命令

可使用gcc -c hello.s -o hello.o指令生成汇编文件

在这里插入图片描述

图14 汇编文件的生成

4.3 可重定位目标elf格式

我们在终端中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:

在这里插入图片描述

图15 hello.o的ELF头

图中Magic所示,这个序列描述了生成该文件的系统的字的大小和字节顺序。16字节序列为7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00,描述了系统的字的大小为8字节,字节顺序为小端序。

ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。ELF头中包含了ELF头的大小为64字节;目标文件的类型为REL(可重定位文件);机器类型为Advanced Micro Devices X86-64;节头部表的文件偏移为1240 bytes;节头部表中条目的数量为13。

在这里插入图片描述

图15 hello.o的ELF节头部表

节头部表描述不同节的位置和大小,目标文件中的每个节都有一个固定大小的节头部表条目。包含了各节的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、对齐等信息。

在这里插入图片描述

图16 重定位节

.rela.text是一个.text节中位置的列表,代表了连接器把hello.o和其他文件组合时,需要修改这些位置。具体信息如下

偏移量:代表需要进行重定向的代码在.text或.data节中的偏移位置;信息:symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型;类型:重定位到的目标的类型;加数:计算重定位位置的辅助信息

从图16中可以看出,hello.o中的.rela.text节包含了puts、exit、printf、atoi、sleep、getchar函数的重定位位置。

在这里插入图片描述

图17 符号表

符号表存放程序中定义和引用的函数和全局变量的信息,每个符号表是一个条目的数组,每个条目包括value:距定义目标的节的起始位置的偏移;size:目标的大小;type:指明数据还是函数;bind:表示符号是本地还是全局属性。

4.4 Hello.o的结果解析

使用命令objdump -d -r hello.o对hello.o进行反汇编,得到结果如图

在这里插入图片描述

图18 hello.o的反汇编

反汇编代码helloo.objdump和hello.o之间主要差别如下:

1、反汇编代码跳转指令的操作数使用的不是段名称如.L2,而在汇编成机器语言之后使用的是确定的地址。

在这里插入图片描述

图19 分支转移
  1. hello.s中的函数调用直接在call指令后面加上要调用的函数名。在机器语言中,调用的函数都是库函数,需要在动态链接后才能确定被调函数的确切位置,因此call指令后的二进制码为全0,同时需要在重定位节中添加重定位条目,在链接时确定最终的相对地址。

在这里插入图片描述

图20 函数调用

3、全局变量

在.s文件中,访问.rodata,使用段名称+%rip,在反汇编代码中0+%rip,因为.rodata中数据地址也是在运行时确定,故访问也需要重定位。
在这里插入图片描述

图21 全局变量

4.5 本章小结

本章讨论了从hello.s到hello.o的汇编过程,通过readelf命令查看了可重定位目标elf格式。使用objdump工具得到了hello.o的反汇编代码,通过hello.s进行比较,找到了两者之间关于指令转移、分支转移、全局变量上两者的区别。

第5章 链接

5.1 链接的概念与作用

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

链接能够将一个大型的应用程序分解成为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

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指令文件进行链接

链接过程及结果如图所示:
在这里插入图片描述

图22 链接hello.s

其中,ld-linux-x86_64.so.2是链接器ld本身所依赖的库,crt1.o、crti.o和crtn.o是C运行时所依赖的环境,libc.so中包含了printf、getchar、exit等函数的定义。

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

在这里插入图片描述

图22 反编译hello

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

在这里插入图片描述

图23 ELF头

图中Magic所示,这个序列描述了生成该文件的系统的字的大小和字节顺序。16字节序列为7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00,描述了系统的字的大小为8字节,字节顺序为小端序。

ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。ELF头中包含了ELF头的大小为64字节;目标文件的类型为REL(可重定位文件);机器类型为Advanced Micro Devices X86-64;节头部表的文件偏移为14208 bytes;节头部表中条目的数量为12。

在这里插入图片描述

图24 节头部表

节头部表

节头部表描述不同节的位置和大小,目标文件中的每个节都有一个固定大小的节头部表条目。包含了各节的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、对齐等信息。与hello.o相比,hello的节头部表一共描述了26个不同节的位置、大小等信息,比hello.o多出13个节。

在这里插入图片描述

图25 程序头

相比hello.o文件,hello多了程序头,包括偏移量、虚拟地址、物理地址、段的大小等信息。

此外符号表、重定位节等信息类似与hello.o的反编译文件,这里不再过多叙述。

5.4 hello的虚拟地址空间

在这里插入图片描述

图26 使用edb打开hello文件

通过 data dump 查看加载到虚拟地址的程序代码,可以看到程序起始地址为0x401000
在这里插入图片描述

图26 查看加载到虚拟地址的程序代码

我们可以通过5.3中的节头部表可以获得各个节的偏移量信息,从而得知各节在虚拟地址空间中的地址。

类似地,对于.rodata节,节头部表中给出了它的偏移量为0x2000,大小为0x3b字节。因此它的虚拟地址空间就从0x401200开始,在edb中查看该虚拟内存地址,可以看出,这个位置一个存放了程序中的两个字符串常量。其余代码块子啊虚拟地址上的存放位置同理可得。

在这里插入图片描述

图27 虚拟地址的程序代码位置查找

5.5 链接的重定位过程分析

使用objdump -d -r hello > hello_.s生成反汇编文件

在这里插入图片描述

图28 hello汇编文件生成

在这里插入图片描述

图29 汇编文件对比

将两个汇编文件对比,如图5-7所示。我们可以发现,hello.o反汇编的结果只有.text段的内容,但是经过链接后生成的hello文件反汇编的结果有.init,.plt,.text等段的内容,并且为puts、printf、sleep等函数分配了在虚拟内存中具体的地址。动态链接器将共享库中hello.c用到的函数加入可执行文件中

在这里插入图片描述

图30 汇编文件的main函数中call函数对比

上图可以看出,函数调用指令call的参数发生了变化。在链接后,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码。
在这里插入图片描述

图30 汇编文件的main函数中跳转指令对比

类似的,跳转指令参数也发生了变化

5.6 hello的执行流程

表 2 程序名称与程序地址

程序名称程序地址
ld-2.31.so!_dl_start0x7f7fb63c3108
libc-2.27.so!__libc_start_main0x7f7fb63c3135
libc-2.27.so!_setjmp0x7f7d2567a144
libc-2.27.so!exit0x7ffcb84d7350
<_init>0x401000
<puts@plt>0x401090
<printf@plt>0x4010a0
<getchar@plt>0x4010b0
<atoi@plt>0x4010c0
<exit@plt>0x4010d0
<sleep@plt>0x4010e0
<_start>0x4010f0
<_dl_relocate_static_pie>0x401120
<main>0x401125
<__libc_csu_init>0x4011c0
<__libc_csu_fini>0x401230

5.7 Hello的动态链接分析在这里插入图片描述

图31 动态链接前后

之前重定位等一系列工作中,用到的地址都是虚拟地址,由于编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,每次从PLT表中查看数据的时候,会首先根据PLT表访问GOT表,得到了真实地址之后再进行操作。我们可以看到对应着全局偏移量表GOT[1]和GOT[2]的内容发生了变化。

5.8 本章小结

本章中介绍了链接的概念与作用、并得到了链接后的hello可执行文件的ELF格式文本hello2.elf,据此分析了hello2.elf与hello.elf的异同;并解释分析了hello虚拟地址空间和重定位过程。探讨了hello的动态链接的相关内容。

第6章 hello进程管理

6.1 进程的概念与作用

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

每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

进程提供给应用程序两个关键抽象:一个独立的逻辑控制流;一个私有的地址空间。

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

Shell 是一个用C语言编写的交互型应用程序,代表用户运行其他程序。Shell 应用程序提供了一个界面,用户可以通过这个界面进行系统的基本操作,访问操作系统内核的服务。

Shell的处理流程大致如下:

  1. 从Shell终端读入输入的命令。
  2. 切分输入字符串,获得并识别所有的参数
  3. 若输入参数为内置命令,则立即执行
  4. 若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行
  5. 若输入参数非法,则返回错误信息
  6. 在运行期间,监控shell界面内是否有键盘输入的命令,如果有需要作出相应的反应
  7. 处理完当前参数后继续处理下一参数,直到处理完毕

6.3 Hello的fork进程创建过程

当检测到用户输入./hello后,shell首先判断它不是内置命令,于是shell查找当前目录下的可执行文件hello,并将其调入内存,shell将它解释为系统功能函数并交给内核执行。Shell通过pid_t fork(void)函数创建一个子进程,子进程会获得与父进程虚拟地址空间相同的一段数据结构的副本。子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。

6.4 Hello的execve过程

shell-bash调用完fork函数生成子进程后,在子进程中调用execve函数,执行可执行目标文件hello。execve函数将删除该进程的代码和地址空间内的内容并将其初始化,新的代码和数据段被初始化为可执行文件中的内容。只有一些调用程序头部的信息才可能会在加载的过程中被从可执行磁盘复制到对应的可执行区域的内存。

6.5 Hello的进程执行

hello执行时,操作系统为其分配时间片与内存空间,程序开始执行,当执行到某些函数时,内核会代表用户执行系统调用,发生上下文切换。其中上下文是指内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件信息的文件表。

调度是指在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就成为调度,是由内核中成为调度器的代码处理的。

在调用进程发送sleep之前,hello在当前的用户内核模式下进程继续运行,在内核中进程再次调用当前的sleep之后进程转入用户内核等待休眠模式,内核中所有正在处理等待休眠请求的应用程序主动请求释放当前正在发送处理sleep休眠请求的进程,将当前调用hello的进程自动加入正在执行等待的队列,移除或退出正在内核中执行的进程等待队列。

休眠的时间等于定时器设置的时间,当定时器时间到时,发送一个中断信号。内核收到中断信号进行终端处理,hello被重新加入运行队列,等待执行,这时候hello就可以运行在自己的逻辑控制流。

6.6 hello的异常与信号处理

处理hello程序使其变为每1s输出一次

在这里插入图片描述

图32 在运行时乱按

乱按的内容会在屏幕上显示,但是不影响程序的继续执行。

在这里插入图片描述

图33 CTRL+C

按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。
在这里插入图片描述

图34 运行时不断回车

不断回车,内容会在屏幕上显示,但是也不影响程序的继续执行。

在这里插入图片描述

图35 CTRL+Z

按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。

在这里插入图片描述

图36 ps指令

在这里插入图片描述

图37 jobs指令

对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收。

在这里插入图片描述

图38 fg指令

输入fg 1则命令将hello进程再次调到前台执行,hello再从挂起处继续运行,打印剩下的语句。

在这里插入图片描述

图39 kill指令

输入kill命令,则可以杀死指定(进程组的)进程
在这里插入图片描述

图40 pstree指令

在Shell中输入pstree命令,可以将所有进程以树状图显示

6.7本章小结

本章介绍了可执行文件hello是如何被加载进内存执行的,其进程如何被执行的,并介绍了异常控制流,以及信号控制相关概念及作用等。最后以hello程序为例,探索了hello进程的fork、execve过程及执行等过程。并给出了hello带参执行情况下各种异常与信号处理的结果。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。即hello.asm中的相对偏移地址。

逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。标志着 hello 应在内存上哪些具体数据块上运行。

物理地址CPU通过地址总线的寻址,找到真实的物理内存对应地址。

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

Intel处理器从逻辑地址到线性地址的变换通过段式管理的方式实现。每个程序在系统中都保存着一个段表,段表保存着该程序各段装入主存的状况信息,包括段号或段名、段起点、装入位、段的长度、主存占用区域表、主存可用区域表等,从而方便进行段式管理。在段寄存器中,存放着段选择符,可以通过段选择符来得到对应段首地址。

在这里插入图片描述

图41 段描述符

通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。

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

同缓存一样,虚拟内存系统必须有某种方法来判定一个虚拟页是否缓存在DRAM中的某个地方。如果是,系统还必须确定这个虚拟页存放在哪个物理页中。如果不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到DRAM中,替换这个牺牲页。

这些功能是由软硬件联合提供的,包括操作系统软件、MMU(内存管理单元)中的地址翻译硬件和一一个存放在物理内存中叫做页表(page table) 的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将-一个虚拟地址转换为物理地址时,都会读取页表。操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。

在这里插入图片描述

图42 页表

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

现代CPU为了节省页表所耗费的内存空间,采用了一种叫多级页表的技术,core i7cpu采用了4级页表,具体做法为,将虚拟页号分为4部分,第i个部分存储着第i+1级页表的基址,然后第i+1个部分是第i+1级页表的表内便宜,最后一级页表中存储的就是最后真实的物理地址。

在这里插入图片描述

图43 使用k级页表的地址翻译

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

在这里插入图片描述

图44 TLB命中和不命中的操作图

三级Cache支持下的物理内存访问的步骤如下:

第1步:CPU产生一个虚拟地址。

第2步和第3步:MMU从 TLB中取出相应的PTE。

第4步:MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。

第5步:高速缓存/主存将所请求的数据字返回给CPU。

7.6 hello进程fork时的内存映射

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

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

7.7 hello进程execve时的内存映射

调用ececve后加载并运行hello主要有以下几个步骤:.

1.删除已存在的用户区域,即删除当前fork函数执行后子进程中存在的所有shell的用户信息。

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

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

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

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

当对应的虚拟页面没有被加载进物理内存,而CPU又要访问这个页面的内容时,就会触发一个缺页故障,缺页即DRAM缓存不命中。缺页故障会引发异常,这时系统就会调用缺页异常处理子程序来对其进行处理。处理程序从物理内存中确定一个牺牲页,若该牺牲页被修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处理程序返回时,CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存将所请求字返回给处理器。

7.9动态存储分配管理

在这里插入图片描述

图45 一个简单的堆块模式

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

分配器有显式分配器和隐式分配器两种风格。

显式分配器要求应用显式地释放任何已分配的块,比如在C和C++中就必须通过free和delete函数释放动态分配的内存,不然就会发生内存泄漏;隐式分配器会检测每个已分配块何时不再被程序所使用,如果不再使用就释放这个块。隐式分配器也叫做垃圾收集器,自动释放未使用的已分配的块的过程叫做垃圾收集,Lisp、ML以及Java等语言有这样的特性。

分配器会进行合并空闲块的操作,避免可用的空闲块被分割成很多小的、无法使用的空闲块,提高内存的利用率。

分配器可以选择立即合并或者推迟合并,立即合并就是在每次一个块被释放时,就合并所有的相邻块,这个操作可以在常数时间内完成,但是对于某种请求方式,可能会产生抖动;推迟合并就是等到某个稍晚的时候再合并空闲块,例如,分配器可以选择直到某个分配请求失败,然后扫描整个堆,合并所有的空闲块。现在快速的分配器通常会选择某种形式的推迟合并。

7.10本章小结

本章主要介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理的相关概念。

第8章 hello的IO管理

8.1 Linux的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、STDOUT_FILENO和STDERR_ FILENO,它们可用来代替显式的描述符值。

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

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

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

Unix I/O 函数:

1. open:进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。open将filename转换为一个文件描述符,并且放回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。

2. close:进程通过调用close函数关闭一个打开的文件。关闭一个已关闭的描述符会出错。

3. read:应用程序通过调用read函数来执行输入。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误;返回值0表示EOF;否则,返回值表示的是实际传扫的字节数量。

4. write:应用程序通过调用write函数来执行输出。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

5. lseek:应用程序通过调用lseek函数,能够显示地修改当前文件的位置。

8.3 printf的实现分析

找到关于printf.c的源代码

在这里插入图片描述

图46 printf.c

我们可以看到在printf函数中传入了一个变长char作为参数并调用了vsprintf函数和write函数

查看vsprintf函数定义:
在这里插入图片描述

图47 vsprintf函数

vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

函数整体的调用过程时printf -> write -> show on screen,在这种模式下,所以write内部就是调用BIOS调用或者LCD驱动显示数据[3]。

应用程序调用write函数,首先进入uclibc,uclibc中会将write的系统调用号及参数保存在r7,及r0-r6中,然后触发软中断,保存在软中断的处理流程前先进性地址空间的转换及堆栈的切换,然后进行中断处理,中断处理中读取中断号及参数,然后找到中断服务例程并执行,退出中断后进行堆栈切换,返回用户态,继续执行用户程序[4]。

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,而后调用字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

getchar调用系统函数read,发送一个中断信号,内核抢占这个进程,用户输入字符串,键入回车后(字符串和回车都保存在缓冲区内),再次发送信号,内核重新调度这个进程,getchar从缓冲区读入字符。

8.5本章小结

本章主要涉及文件IO的操作。介绍了IO设备管理方法、IO函数,并且分析了printf函数和getchar函数的实现。

结论

行文至此,我们就正式陪伴者hello程序经历了它这完整的一生经历了如下过程:我们看着它经过预处理、编译、汇编、链接、最终编程一个可执行程序,在指令访存、动态申请、内存信号处理的一系列计算机帮助下完成了他的使命并最终终止并被回收的整个过程。

计算机系统这一庞大的体系并不是可以随随便便速成的,接下来还要用心的深入了解。

附件

文件名功能
hello.i预处理后得到的文本文件
hello.s编译后得到的汇编语言文件
hello.o汇编后得到的可重定位目标文件
hello.elf用readelf读取hello.o得到的ELF格式信息
hello.asm反汇编hello.o得到的反汇编文件
hello_.elf由hello可执行文件生成的.elf文件
hello_.s由可执行文件编译后得到的汇编语言文件
hello2.asm反汇编hello可执行文件得到的反汇编文件

参考文献

[1] 曾祥宇,王璐瑶,张珂鹭.浅谈C语言中预处理[J].艺术科技,2016,2909:426.

[2] 王秀芳,孙承爱,路燕.C语言中编译预处理命令的解读与应用[J].电脑编程技巧与维护,2010(22):22-24.DOI:10.16184/j.cnki.comprg.2010.22.019.

[3]write系统调用的实现 https://blog.csdn.net/cxsjabcabc/article/details/107116821

[4]printf_系统调用过程分析_write() putc() 函数实现

https://blog.csdn.net/edonlii/article/details/20135059?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_utm_term~default-0-20135059-blog-62043431.pc_relevant_paycolumn_v3&spm=1001.2101.3001.4242.1&utm_relevant_index=1
预处理后得到的文本文件 |
| hello.s | 编译后得到的汇编语言文件 |
| hello.o | 汇编后得到的可重定位目标文件 |
| hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
| hello.asm | 反汇编hello.o得到的反汇编文件 |
| hello_.elf | 由hello可执行文件生成的.elf文件 |
| hello_.s | 由可执行文件编译后得到的汇编语言文件 |
| hello2.asm | 反汇编hello可执行文件得到的反汇编文件 |

参考文献

[1] 曾祥宇,王璐瑶,张珂鹭.浅谈C语言中预处理[J].艺术科技,2016,2909:426.

[2] 王秀芳,孙承爱,路燕.C语言中编译预处理命令的解读与应用[J].电脑编程技巧与维护,2010(22):22-24.DOI:10.16184/j.cnki.comprg.2010.22.019.

[3]write系统调用的实现 https://blog.csdn.net/cxsjabcabc/article/details/107116821

[4]printf_系统调用过程分析_write() putc() 函数实现

https://blog.csdn.net/edonlii/article/details/20135059?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_utm_term~default-0-20135059-blog-62043431.pc_relevant_paycolumn_v3&spm=1001.2101.3001.4242.1&utm_relevant_index=1

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值