P2P hello程序人生

计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算学部
学   号 120L02xxxx
班   级 03007
学 生   
指 导 教 师 吴x

计算机科学与技术学院
2021年5月

摘 要
本文通过对hello程序一生的回顾,复习了hello是如何从代码源文件一步步变成可执行文件;这个生成的可执行文件又是如何在计算机上运行的。
通过在Linux下的操作,我们学习了一些底层的知识,加深了对计算机系统的理解。
关键词:hello;程序;预处理;编译;汇编;链接;进程;信号;异常;处理;内存;地址;I/O;计算机系统

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第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:From Program to Process,即表Hello从编写代码到程序运行的过程。Hello需要经过4个大步骤:预处理、编译、汇编、链接共4个过程,得到一个可在内存中被直接执行的可执行文件。然后在执行的过程中,shell新建进程对Hello进行执行,在这个过程中,操作系统会调用fork产生子进程Hello成为进程的开始。然后通过execve将其加载,不断进行访存、内存申请等操作。最后,在程序结束返回后,由父进程或祖先进程进行回收,程序结束。
020 :From Zero-0 to Zero-0,即表示在hello的整个运行的过程中,由开始的操作系统进行的存储管理,然后由内存管理单元将虚拟地址翻译成物理地址,同时进行内存访问,通过页面的调度进入内存,开始了进程的执行。最后以父进程或者祖先进程的回收作为终点,生也操作系统,死也操作系统,这是hello进程的一生的执行。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.3 中间结果
中间结果文件 文件作用 使用时期
hello.i 预处理得到的文件
ASCII码的中间文件 预处理
hello.s ASCII汇编语言文件 编译
hello.o 可重定位目标文件 汇编
hello 可执行目标文件 链接
hello_elf hello的elf文件 链接
hello_asm hello.o的反汇编文件 汇编
hello_dump.d hello的反汇编文件 链接
1.4 本章小结
本章简要介绍了hello的一生,p2p,020 过程,列举了hello程序从c程序hello.c到可执行目标文件hello需要经过的阶段、每一个阶段得到的文件,并给出了进行实验的软硬件环境。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
2.1.1概念:
预处理是指在进行正式编译(此法分析,代码生成,优化等)之前所做的工作。预处理是C语言的一个重要的功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理命令部分做处理,处理完毕自动进入对源程序的编译。任何C语言程序都有一个预处理程序。
使用 gcc -E hello.c -o hello.i 生成.i文件,因为引入头文件,执行过后我们的代码会多上几百行左右。

2.1.2作用:
①展开头文件
在写有#include 或#include "filename"的文件中,将文件filename展开,通俗来说就是将fiename文件中的代码写入到当前文件中
②宏替换
③去掉注释,添加行号和文本标识
④条件编译
即对#ifndef #define #endif进行判断检查,也正是在这一步,#ifndef #define #endif的作用体现出来,即防止头文件被多次重复引用

2.2在Ubuntu下预处理的命令
以#号开头的命令称为预处理命令。例如#include <stdio.h>等。
图 1 hello.c中预处理命令图 1 hello.c中预处理命令
2.3 Hello的预处理结果解析
使用 gcc -E hello.c -o hello.i 指令生成.i文件,使用gedit工具打开.i文件,对比原文件和.i文件:
图 2 生成.i文件图 2 生成.i文件
在这里插入图片描述在这里插入图片描述
图 3 .i文件头 图 4 .i文件尾,共3060行
在这里插入图片描述
图 5 c源代码仅有23行
结果分析:
预处理将#号开头的命令进行解析, 在hello…c中依次读取stdio.h unistd.h stdlib.h三个头文件的内容写入.i文件中。然后宏替换、去掉注释,添加行号和文本标识、条件编译。最终的hello.i文件中没有宏定义、文件包含及条件解析等内容。预处理后的文件大小大于源文件大小。
2.4 本章小结
在本章中,我们先复习了预处理的概念和作用,知道预处理最大的作用是将#号引入的文件与源代码合并。在ubuntu下使用gcc -E指令能从c文件生成.i文件,分析预处理后的 hello.i文件,我们发现文件大小有明显的变化,#号引入的文件被展开写入。

第3章 编译
3.1 编译的概念与作用
3.1.1概念:
编译就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码。使用gcc进行编译时,默认情况下,不输出该文件,生成的汇编文件是 .s 文件。例如使用gcc -S hello.i -o hello.s指令,将生成一个文本文件hello.s,它包含一个汇编语言程序。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。

3.1.2作用:
把预处理完的文件进行一些列的词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。
1.词法分析:扫描器(Scanner)将源代的字符序列分割成一系列的记号(Token)。lex工具可实现词法扫描。
2.语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)。yacc工具可实现语法分析(yacc: Yet Another Compiler Compiler)。
3.语义分析:静态语义(在编译器可以确定的语义)、动态语义(只能在运行期才能确定的语义)。
4.源代码优化:源代码优化器(Source Code Optimizer),将整个语法书转化为中间代码(Intermediate Code)(中间代码是与目标机器和运行环境无关的)。中间代码使得编译器被分为前端和后端。编译器前端负责产生机器无关的中间代码;编译器后端将中间代码转化为目标机器代码。
5.目标代码生成:代码生成器(Code Generator).
6.目标代码优化:目标代码优化器(Target Code Optimizer)
3.2 在Ubuntu下编译的命令
在这里插入图片描述图 6 使用gcc -S hello.i -o hello.s指令生成编译后汇编文件并查看
在这里插入图片描述图 7 查看生成的. s汇编文件
3.3 Hello的编译结果解析
3.3.1常量处理:
c语言中常量有整型、浮点型、字符串型、字符型等,分别讨论编译过程对其处理方式:
整型:
在这里插入图片描述
图 8 hello中整型以数字的形式出现
在这里插入图片描述图 9 整型数写入到汇编文本中的形式

字符串型:
在c源码中,共有两处用到字符串:
在这里插入图片描述在这里插入图片描述
图11 hello中的字符串使用
经过编译后,汇编文件中的字符串没有中文,而是三个数字对应一个汉字的位置,则知晓这是汉字经过某种编码后的呈现。同时中文感叹号也对应着三个数字。
在这里插入图片描述图12 编译后的字符串常量汉字被三个数字代替

此外没有浮点和字符型常量。

3.3.2变量(全局/局部/静态):
Hello么有全局变量和静态变量,只有局部变量int i。

在这里插入图片描述
图13 hello中的局部整型变量i
在这里插入图片描述在这里插入图片描述
图14 运行时 i被分配在栈空间上

3.3.3赋值/不赋初值
Hello只有局部变量int i,在for循环for(i=0;i<8;i++)中有一次赋值i=0,汇编操作如下。
且i是不赋初值的情况,所以不会对该内存区域有任何操作。

3.3.4逗号操作符
在这里插入图片描述
图15 源代码逗号操作符存在的位置
在这里插入图片描述
在这里插入图片描述
图 16逗号操作符存在的位置有两个参数
在汇编实现里,先对待输出字符串中的字符串型格式符与寄存器中数据相匹配,hello的逗号操作符存在的位置有两个参数,所以会从rdx开始顺着rsi等找两个寄存器中的内容,若寄存器中都是字符串,则正确匹配。经过LC1正确匹配后,call printf@PLT调用系统函数进行输出打印。

3.3.5算数操作
Hello中涉及到算数的只有在for循环中计数器i 的递增。
在这里插入图片描述
图17 源代码i++操作
在这里插入图片描述
图18 汇编实现

3.3.6关系操作
在这里插入图片描述
图19 源代码两处关系比较
在这里插入图片描述
图20 汇编中的实现(cmpl + jle或je组合)

3.3.7数组/指针/结构操作
在main函数中传递的参数出现了指针数组
在这里插入图片描述
图21 main参数指针数组

数组的元素是char*类型的指针,可以指向一个字符串。因为有argc的大小为4的判断,而argv[0]指向文件名,所以应该有另外三个参数argv[1] argv[2] argv[3]分别依次指向命令行输入的三个参数,在汇编代码中这个指针数组的首地址被存放在rsi中,也就是存放main函数的第二个参数的寄存器。
在这里插入图片描述
图22 main的参数识别
对于指针数组argv,其每一个元素的大小都是8字节,而且数组中的元素应是连续存放的,因此地址偏移量也应该是8的倍数。

3.3.8控制转移
Hello中出现了if和for两种控制转移方式,在汇编的实现主要通过cmpl + jle或cmpl + je的组合,根据条件判断转移到的代码块。
在这里插入图片描述
图23 if和for两种控制转移方式
在这里插入图片描述
图24 控制转移的汇编实现

3.3.9函数操作
Hello中用到的函数有printf()、exit()、sleep()、atoi()、getchar()。

Printf()函数:
1.printf(“用法: Hello 学号 姓名 秒数!\n”);
在这里插入图片描述
图 25第一次printf的汇编实现
由于没有参数,在编译优化过程中printf被puts函数替代,同等的完成将字符串“用法: Hello 学号 姓名 秒数!\n”打印到屏幕的效果。
2.printf(“Hello %s %s\n”,argv[1],argv[2]);在这里插入图片描述
图 26第二次printf的汇编实现
第二次使用printf时,待打印字符串中有两个字符串型格式符%s占位,需要提前检验所给参数是否合法,通过寄存器中地址找到替换的字符串的首地址,然后进行替换,若成功,最后调用printf进行打印。
Exit()函数:
汇编中直接调用系统函数即可,edi中存放退出状态的提示信息,edi中保存1与exit(1)中的1相对应。
在这里插入图片描述
图27 Exit()函数汇编实现
Sleep()函数与atoi()函数:
源代码中出现在sleep(atoi(argv[3]));
在这里插入图片描述
图28 Sleep()与atoi()的汇编实现
在调用atoi之前,rdi中保存了argv[3]的值;调用atoi之后,rdi中字符串转为整型数并保存到eax。eax的值再赋值到edi中,edi作为sleep函数的参数,调用sleep函数进行休眠。
Getchar()函数:
Getchar没有参数,直接调用可以接收来自键盘的一个无符号字符。其实现是以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。
在这里插入图片描述

图29 getchar()的汇编实现

3.4 本章小结
本章我们对预处理后的.i文件编译生成汇编文本的.s文件,了解了编译的概念和作用,其最重要的功能是把预处理完的文件进行一些列的词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件;在对编译结果的分析中,我们对C语言的数据与操作在汇编中的变化更明白,知道了控制转移在汇编中的主要实现方法,了解了函数是如何进行调用的,调用另一个函数的时候如何进行参数的传递,也分析了栈帧和寄存器在对变量和参数的管理。

第4章 汇编
4.1 汇编的概念与作用
4.1.1汇编的概念:
汇编就是将汇编代码转换成可以执行的机器指令(二进制指令)。绝大部分汇编语句对应一条机器指令,有的汇编语句对应多条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可。对hello.c来说,汇编过程就是将hello.s文件翻译成hello.o文件。

4.1.2汇编的作用:
汇编将高级语言转化为机器可直接识别执行的代码文件。
绝大部分汇编语句对应一条机器指令,这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中。目标文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
输入gcc -c hello.s -o hello.o指令实现汇编文件生成可重定向目标文件(是二进制文件)。
在这里插入图片描述
图30 gcc指令生成可重定向目标文件
用odjdump反汇编功能验证.o文件为二进制文件。
在这里插入图片描述
图31 用odjdump对hello.o反汇编

4.3 可重定位目标elf格式
4.3.1ELF Header详细信息:
readelf -h hello.o指令读取elf头信息
在这里插入图片描述
Magic含义:
在这里插入图片描述
图32 header中第一行Magic的解读

hello.o的文件类型为REL (Relocatable file)可重定位文件,此外还有两种文件类型为可执行文件和共享文件。
关注到Size of this header为64字节,则section区域在elf中的起始位置为x40。同时,Start of section headers的位置为第1240字节,section headers大小为64字节,hello.o共有14个section,则section header table的大小为14*64=896个字节,计算得该文件总共2136字节。与使用wc指令的结果一致。
在这里插入图片描述
图33 用wc指令查看hello.o文件大小

4.3.2section头表
在这里插入图片描述
图34 section头表内容
根据section头表我们知道section按照.text–>.data–>.bss等等的顺序存放,offset代表section的起始位置,size表示section大小。

4.3.3重定位节
重定位节包含了许多需要在连接中重定位的项目,包含了符号名称,重定位类型等等。
在这里插入图片描述
图35 重定位节

4.3.5 符号表
符号表中显示了各个符号的名称、值、类型、大小等等。
在这里插入图片描述
图36 符号表
4.4 Hello.o的结果解析
4.4.1获得objdump 反汇编结果
使用objdump -d -r hello.o > hello.asm指令生成反汇编后的 hello.asm文件,并与第3章的 hello.s进行对照分析。
在这里插入图片描述
图37 hello.asm文件内容
4.4.2 分析Hello.o的结果
此小节分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
1.分支转移
hello.s中分支转移是使用段名称进行跳转的,而hello.o 中分支 转移是通过地址进行跳转的。表现为hello.s中函数被分为多个块,而反汇编语言中没有进行分块,所有的代码是一整段。
2.函数调用
hello.asm对函数调用call后直接call一个数字,即main 函数的相对偏移地址,而hello.s中,直接通过call函数名来调用函数。函数只有在链接之后才能确定运行执行的地址,因 此在.rela.text节中为其添加了重定位条目。
3.全局变量
hello.asm通过pc相对寻址,通过rip+x的值进行访问,因为.rodata节中的数据是在运行时确定的,未重定位,故用0占位。在hello.s中,通过.LC0+rip进行访问。
在这里插入图片描述
图 38hello.asm 中寻址
在这里插入图片描述
图39 hello.s 用 .LC0+rip访存
4.5 本章小结
本章简要介绍了汇编的概念和作用,介绍了如何获得.o文件的elf格式的可重定位目标文件以及反汇编文件.asm。分析了可重定位目标文件的内容及其用途,将.o文件与.asm文件的内容进行了对比,明晰了机器语言和汇编语言的区别,加深了对汇编过程的理解。

第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。
5.1.2 链接的作用
链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器的程序自动执行的。
5.2 在Ubuntu下链接的命令
链接ld指令: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
在这里插入图片描述
图40 使用ld指令链接后运行可执行程序
5.3 可执行目标文件hello的格式
readelf -a hello > hello.elf
在这里插入图片描述
图 41 典型的 ELF 可执行目标文件
5.3.1ELF Header
ELF 头描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。
在这里插入图片描述
图42 hello的elf头

Magic 16字节序列
DATA Little endian
TYPE EXEC(可执行文件)
ENTRY POINT ADDRESS 程序从0x4010f0开始运行
ELF大小 64
节头大小 64
节头数目 27

5.3.2头节表Section Headers
对 hello中所有的节信息进行了声明,其 中包括大小 Size 以及在程序中的偏移量 Offset,因此根据 Section Headers 中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。
在这里插入图片描述
在这里插入图片描述

图43,44 头节表内容

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
查看edb,可以看出hello的虚拟地址空间开始于0x400000,结束于0x40ff0
在这里插入图片描述

图45 hello的虚拟地址空间开始于0x400000
在这里插入图片描述

图46 hello的虚拟地址空间结束于0x40ff0

打开edb的symbols窗口,发现这里的地址与hello.elf中的ProgramHeaders地址是一一对应的。
在这里插入图片描述

图47 symbols窗口
通过节头部表,找到edb的各个节的信息,如.text,虚拟地址开始于0x4010f0,字节偏移为0x10f0
在这里插入图片描述

图48 .text段的虚拟地址开始于0x4010f0,字节偏移为0x10f0
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
1.得到反汇编文件
输入指令objdump -d -r hello > hello_dump.d得到hello的反汇编文件hello_dump.d,将其与链接前的hello.o的反汇编文件进行对比。
2.分析 hello_dump.d和hello.o的区别
(1)hello中增加了一些节,如.init、.plt、.plt.sec、.fini等等,而链接之前的hello.o中只有.text节的信息。而阅读.plt.sec节中的信息可以发现,程序所使用的共享库函数如puts、printf等的跳转指令均位于这个节中。
在这里插入图片描述
在这里插入图片描述

图49 hello_dump.d中.init、.plt、.plt.sec、.fini等节 图50 hello.o中只有.text节
(2)在hello中,链接器加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
(3)hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
5.6 hello的执行流程
将生成的hello可执行文件放到edb所在的目录下,输入以下命令行:
./edb --run ./hello 120L022131 pbc 2
通过edb的调试,一步一步地记录下call命令进入的函数。
在这里插入图片描述

图 51 访问的子程序
5.7 Hello的动态链接分析
1.对于变量而言,我们利用代码段和数据段的相对位置不变的原则计算正确地址。
2.对于库函数而言,我们需要利用PLT表(获取数据段存放函数地址的代码)和GOT表(存放函数地址的数据段)。在动态链接时,程序只会链接被调用的代码段,需要使用PLT表和GOT表得到真正的地址。

在这里插入图片描述

图 52执行dl_init之前
在这里插入图片描述

图 53 执行dl_init之后

5.8 本章小结
了解了链接的概念和作用、复习了链接的命令。
学习了可执行目标文件的ELF格式。
通过查看hello的虚拟地址空间,并且对比hello与hello.o的反汇编代码,更好地掌握了链接与之中重定位的过程。
通过分析hello的动态链接的过程,更好地掌握了相关知识。

第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程的经典定义就是一个执行中程序的实例。进程曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的UNIX,Linux 2.4及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。
6.1.2 进程的作用
程序本身只是指令、数据及其组织形式的描述,相当于一个名词,进程才是程序(那些指令和数据)的真正运行实例,可以想像说是现在进行式。若干进程有可能与同一个程序相关系,且每个进程皆可以同步(循序)或异步(平行)的方式独立运行。现代计算机系统可在同一段时间内以进程的形式将多个程序加载到存储器中,并借由时间共享(或称时分复用),以在一个处理器上表现出同时(平行性)运行的感觉。同样的,使用多线程技术(多线程即每一个线程都代表一个进程内的一个独立执行上下文)的操作系统或计算机体系结构,同样程序的平行线程,可在多CPU主机或网络上真正同时运行(在不同的CPU上)。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell-bash的作用
解释命令,连接用户和操作系统以及内核。
6.2.2 Shell-bash的处理流程
1.从终端读入命令
2.将输入的字符串切分,以满足各种参数的需要
3.如果是内置命令立即执行;如果不是内置命令就认为输入的字符是一个路径,然后调用fork函数的execve,为该路径上的程序分配子进程去执行。
4.接收输入的各种信号,比如从键盘的输入。
5.执行命令
6.3 Hello的fork进程创建过程
执行中的进程调用fork()函数,就创建了一个子进程。
6.3.1fork作用
fork()调用一次,返回两次:若成功,子进程返回0,父进程返回子进程ID;否则,出错返回-1。

6.3.2hello进程
1.首先对于hello进程。我们终端的输入“./hello 120L022131 pbc 2”被判断为非内置命令,然后shell试图在硬盘上查找该命令(即hello可执行程序),并将其调入内存,然后shell将其解释为系统功能调用并转交给内核执行。
2.shell执行fork函数,创建一个子进程。这时候我们的hello程序就开始运行了。值得注意的是,hello子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。但是子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。并且子进程有不同于父进程的PID。
6.4 Hello的execve过程
6.4.1execve函数作用
execve函数在当前进程的上下文中加载并运行新程序hello。如果成功,则不返回;如果错误,则返回-1。

6.4.2execve过程
在execve加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:int main(int argc , char **argv , char *envp)。执行将:
1.删除已存在的用户区域(自父进程独立)。
2.映射私有区:为hello的.text、.data、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
3.映射共享区:比如hello程序与标准C库libc.so链接,这些对象都是动态链接到hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
6.5 Hello的进程执行
6.5.1 进程上下文信息
系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由 程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和 数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件 描述符的集合。
每次用户通过向 shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行 目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它 们自己的代码或其他应用程序。
6.5.2 进程时间片
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
6.5.3 进程调度
当内核选择一个新的进程运行时,我们说内核调度了这个进程。
在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为 上下文切换的机制来将控制转移到新的进程,上下文切换 1)保存当前进程的 上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这 个新恢复的进程。
6.5.4 用户态与核心态转换
1.内核态—>用户态:设置程序状态字PSW
2.用户态—>内核态:唯一途径是通过中断、异常、陷入机制(访管指令)
通常来说,以下三种情况会导致用户态到内核态的切换:
(1)系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统 调用申请使用操作系统提供的服务程序完成工作。

(2)异常
当CPU执行运行在用户态下的程序时,发生了某些事先不可知的异常,这 时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内 核态,比如缺页异常。

(3)外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
6.6.1 异常的分类
在这里插入图片描述
图 54 异常的分类
6.6.2 异常的处理方式
在这里插入图片描述

图 55 中断处理:中断处理程序将控制返回给应用程序控制流中的下一条指令
在这里插入图片描述

图 56 陷阱处理:陷阱处理程序将控制返回给应用程序控制流中的下一条指令
在这里插入图片描述

图 57 故障处理:根据故障是否能够被修复,故障处理程序要么重新执行引起故障的指令,要么终止

在这里插入图片描述

图 58 终止处理:终止处理程序将控制传递给一个内核 abort 例程,该例程会终止这个应用程序

6.6.3 异常与信号的处理

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
1.按很多空格
在这里插入图片描述

图59 运行时按多个空格
按空格尚在运行时会出现换行效果,程序运行在结束后,会出现所按空格数量的linux命令行空指令换行效果。
2.随便乱按
在这里插入图片描述

图60 运行时随便乱按
运行时只是将屏幕的输入缓存到缓冲区,程序输出的内容会跟在乱输入的字符后面,乱码被认为是命令。

3.Ctrl-Z
在这里插入图片描述

图61 Ctrl-Z后ps、jobs指令检查状态,fg恢复运行
在这里插入图片描述

图62 Ctrl-Z后pstree查看系统进程
进程收到 SIGSTP 信号,hello 进程挂起。控制台输入:ps,查看其进程PID,是11522,控制台输入:jobs,查看其后台job号,是1。调用 fg 将其调回前台。pstree显示进程的关系。

4.Ctrl-C
在这里插入图片描述

图63 按下Ctrl-C
效果:进程收到 SIGINT 信号,hello进程结束。

5.Ctrl-Z后kill hello进程
在这里插入图片描述

图64 kill命令
挂起的进程被终止,在ps中无法查到到其PID。

6.7本章小结
本章主要介绍了使用fork函数创建子进程以及使用execve函数加载并运行程序的详细过程,并阐述了shell在工作时的处理流程。然后,我们还探究了上下文切换、时间片、进程调度以及内核的用户态和核心态的概念。最后,在hello程序的实际运行过程中,直观地观察了进程的运行和切换规则以及一些异常和信号的处理过程。

第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1逻辑地址
逻辑地址(Logical Address)是指由程序hello产生的与段相关的偏移地址 部分(hello.o)。
7.1.2线性地址
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程 序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址, 它加上相应段的基地址就生成了一个线性地址。
7.1.3虚拟地址
有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似, 逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
7.1.4物理地址
物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物 理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么 hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没 有启用分页机制,那么hello的线性地址就直接成为物理地址了。
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.2.1 逻辑地址
(1)逻辑地址实际是由 48 位组成的,前 16 位包括「段选择符」,后 32 位「段内偏移量」。

在这里插入图片描述

图 65 逻辑地址的前16位
(1)索引:「描述符表」的索引(Index)
(2)TI:如果 TI 是 0。「描述符表」是「全局描述符表(GDT)」,如果 TI 是 1。「描述符表」是「局部描述表(LDT)」
(3)RPL:段的级别。为 0,位于最高级别的内核态。为 11,位于最低级别 的用户态。在 linux 中也仅有这两种级别。
7.2.2转化过程
被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址。
在这里插入图片描述

图 66 转化过程
7.3 Hello的线性地址到物理地址的变换-页式管理
7.3.1 线性地址
在这里插入图片描述

图 67 IA-32的页目录项和页表项
7.3.2 转化过程
给定一个逻辑地址和页面大小,如何计算物理地址?
(1)根据页面大小可计算出页内地址的位数
(2)页内地址位数结合逻辑地址计算出页内地址(块内地址)和页号
(3)页号结合页表,即可得出块号
(4)块号&块内地址即可得出物理地址
7.4 TLB与四级页表支持下的VA到PA的变换
7.4.1 TLB
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个 PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这 会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1 中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样 的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓 存器(TLB)。
7.4.2 多级页表
将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由 上一级确定的页表基址对应的页表条目。
在这里插入图片描述

图 68 使用页表的地址翻译
在这里插入图片描述

图 69 虚拟地址中用以访问 TLB 的组成部分
在这里插入图片描述

图 70 使用k级页表的地址翻译
7.5 三级Cache支持下的物理内存访问
以Core i7为例。高速缓存L1、L2和L3是物理寻址的,块大小为64字节。L1和L2是8路组相联的,而L3是16路组相联的。在得到52位的物理地址后,先在L1中寻址。L1共有64组。将PA的低6位作为高速缓存的块偏移(CO),再取出6位作为其组索引(CI),剩余的40位作为标记位(CT)。使用CI找到L1中对应的组,在这组中,若有一行的标记位与CT相同,且有效位为1,则缓存命中,使用CO取出块中对应的字节即可;否则缓存不命中,需要依次到L2、L3甚至是主存中继续查找。
在这里插入图片描述

图71 3级Cache
7.6 hello进程fork时的内存映射
当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制
7.7 hello进程execve时的内存映射
hello调用execve后,execve在当前进程中加载并运行包含在可执行目标文件hello.out中的程序,用hello.out程序有效地替代了当前程序。加载并运行hello.out需要以下几个步骤:
1.删除已存在的用户区域(自父进程独立)。
2.映射私有区:为hello的.text、.data、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
3.映射共享区:比如hello程序与标准C库libc.so链接,这些对象都是动态链接到hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
当要使用的虚拟地址对应的PTE的有效位为0时,说明该页面没有缓存到主存中,就发生了缺页异常,于是调用缺页异常处理程序。
缺页处理程序会选择一个牺牲页,若其内容发生改变,将其复制到磁盘中,更新PTE,将牺牲页的PTE的地址字段更新为牺牲页在磁盘中的起始地址,有效位置为0,将缺少的页的PTE的地址字段更新为缓存页的起始位置,也就是对应的PPN,将有效位置为1,缺页处理程序返回。
返回到导致缺页异常的指令再次运行,重新进行地址翻译。
在这里插入图片描述
图 72缺页处理
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块(blocks)的集合来维护,每个块就是一个连续的虚拟内存片,每个块要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格:显式分配器和隐式分配器。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
·显式分配器: 要求应用显式地释放任何已分配的快
例如,C语言中的 malloc 和 free
·隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块
比如Java,ML和Lisp等高级语言中的垃圾收集 (garbage collection)。

对于空闲块的组织形式,有如下两种策略:
1.隐式空闲链表
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。
隐式空闲链表:在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
在这里插入图片描述

图73 隐式空闲链表
2.显式空闲链表
显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
显式空闲链表:在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
在这里插入图片描述

图74 双向空闲链表的堆块
7.10本章小结
本章主要介绍了hello的存储器地址空间,Intel逻辑地址到线性地址的变换-段式管理,Hello的线性地址到物理地址的变换-页式管理,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时的内存映射,hello进程execve时的内存映射,缺页故障与缺页中断处理,动态存储分配管理。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行。
8.2 简述Unix IO接口及其函数
8.2.1 简述Unix I/O 接口
这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、 低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2.2 Unix I/O 函数
大多数 UNIX 文件 I/O 只需用到6个函数: open、create、read、write、lseek 以及 close 。
1.open函数
open 函数用于打开和创建文件。他定义在文件 <fcntl.h> 中。
函数原型:int open(char *filename, int flags, mode_t mode);
open 函数将filename 转换为一个文件描述符,并且返回描述符数字.返回的描述符总是在进程中当前没有打开的最小描述符.flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位.返回:若成功则为新文件描述符,若出错为-1。
2.create函数
creat 函数用于创建新文件。
函数原形为:int creat(const char *pathname, mode_t mode);
返回值:文件描述符(成功)或者 -1(出错)。creat 函数等同于 open 函数的部分用法,现在基本都用 open 函数替代它了。
3.read函数
用 read 函数从打开文件中读数据。定义在文件 <unistd.h> 中。
函数原形为:ssize_t read(int filedes, void *buf, size_t nbytes);
read 函数从 filedes 指定的已打开文件中读取 nbytes 字节到 buf 中。
4.write函数
用write函数向打开文件写数据。定义在 中,函数原形为:ssize_t write(int filedes, const void *buf, size_t nbytes);
write函数将缓冲区中的数据写入到指定文件。若写入成功,返回写入的字节数;若写入失败,返回-1。
5.lseek函数
可以调用lseek显式地定位一个打开文件。lseek 函数定义于文件 中,函数原形为:off_t lseek(int filedes, off_t offset, int whence);
每个打开文件都有一个与其相关联的“当前文件位移量”。它是一个非负整数,用以度量从文件开始处计算的字节数。通常,读、写操作都从当前文件位移量处开始,并使位移量增加所读或写的字节数。
6.close 函数
close 函数用于关闭打开的文件,函数定义在文件 <unistd.h> 中,函数原形为:int close( int filedes );
当一个进程终止时,它所有的打开文件都由内核自动关闭。很多程序都使用这一功能而不显式地用close关闭打开的文件。
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
https://www.cnblogs.com/pianist/p/3315801.html
在这里插入图片描述

观察形参,发现存在“…”,这表示当传递参数的个数不确定时,就可以用这种方式来表示。接着,命令va_list arg = (va_list)((char*)(&fmt) + 4);中,将指针arg指向了…中的第一个参数。然后调用了另一个函数vsprintf:
函数vsprintf
在这里插入图片描述

该函数根据调用prinf时规定的各种格式fmt(如%x、%s等)用格式字符串对参数进行格式化,将要输出的数据保存在缓冲区buf中,返回buf中的字节数。
返回到printf中后,又调用了write函数,对buf中的数据进行写操作。
命令write(buf , i);把buf中的i个元素的值写到终端。
最后write通过执行syscall指令实现了对系统服务的调用,从而使内核执行打印操作。内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram(显存)对字符串进行输出。显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终实现printf中字符串在屏幕上的输出。
8.4 getchar的实现分析
getchar函数
在这里插入图片描述

1.读取
getchar函数通过调用read函数来读取字符。read函数由三个参数,第一个参数为文件描述符fd,fd为0表示标准输入;第二个参数为输入内容的指针;第三个参数为读入字符的个数。read函数的返回值是读入字符的个数,若出错则返回-1。
2.输入缓冲判断回车
当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。
3.异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
4.getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux下的I/O设备管理方法、Unix I/O接口以及相应的I/O函数。本章还剖析了printf函数和getchar函数的底层实现方法,对其进行了较为细致的分析之后,我们对其中的细节性原理有了更深刻的认识。

结论
hello所经历的过程:
1.预处理:将源代码文件hello.c进行预处理,生成hello.i文件;
2.编译:将hello.i编译为汇编文件hello.s;
3.汇编:将hello.s汇编为机器语言的二进制可重定位文件hello.o;
4.链接:将hello.o与使用的动态链接库链接,生成可执行文件hello;
5.shell输入指令运行:在shell中输入运行hello程序的指令,shell进程为hello创建一个子进程,然后加载并运行hello;
6.加载执行:运行过程中,内核在不同进程之间进行上下文切换,并发运行包括hello在内的诸多进程;
7.信号处理:当按下Ctrl-C和Ctrl-Z时,发送信号给内核,内核调用信号处理函数处理这些信号;
8.回收:hello进程结束,被shell父进程回收。
9.I/O管理:访存时,利用四级页表将运行时的虚拟地址映射为实际的物理地址,然后利用三级高速缓存和主存取出想要的数据;使用I/O接口,调用一些I/O函数以实现printf功能;
感悟:计算机系统庞大而精妙绝伦,里面每一个功能都是充分配合协调实现,环环相扣。从程序代码到屏幕输出, hello的一生贯穿了众多计算机核心技术与概念,在揭开面纱的过程中,我们得以体会计算机系统的底层机制,感悟众多工程师科学家积累的经验与思维,这是继承和革新的智慧结晶。

附件
列出所有的中间产物的文件名,并予以说明起作用。
中间结果文件 文件作用 使用时期
hello.i 预处理得到的文件、ASCII码的中间文件 预处理
hello.s ASCII汇编语言文件 编译
hello.o 可重定位目标文件 汇编
hello 可执行目标文件 链接
hello_elf hello的elf文件 链接
hello_asm hello.o的反汇编文件 汇编
hello_dump.d hello的反汇编文件 链接

参考文献
[1] https://blog.csdn.net/dlutbrucezhang/article/details/8753765 C语言中的预处理详解
[2] https://baike.baidu.com/item/%E7%BC%96%E8%AF%91 百度百科 编译
[3] https://www.cnblogs.com/snail-micheal/p/4189632.html (转)汇编.section和.text解释
[4] https://zhuanlan.zhihu.com/p/130271689 深入理解GOT表和PLT表
[5] https://www.cnblogs.com/Allen-rg/p/7171105.htm 用户态和核心态的区别
[6] https://hansimov.gitbook.io/csapp/ 《深入理解计算机系统》中文电子版(原书第 3 版)
[7] https://www.jianshu.com/p/fd2611cc808e 段页式访存——逻辑地址到线性地址的转换
[8]https://baike.baidu.com/item/getchar/919709 getchar (计算机语言函数)
[9]https://www.cnblogs.com/pianist/p/3315801.html [转]printf 函数实现的深入剖析
[10]http://blog.chinaunix.net/uid-23709303-id-2388882.html Unix 文件 I/O 函数

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值