程序人生-Hello’s P2P

									  程序人生-Hello’s P2P

摘 要
本文对hello程序的P2P和020过程进行了整体介绍,这个介绍贯穿了整学期的计算机系统知识,使得我们对庞大而精细的计算机系统有一个更深入的了解。其中首先阐述了hello如何经过多种处理后成为可执行程序,然后讲述了hello进程从创建到回收的全过程。从最开始的预处理和编译,到之后的汇编和链接;从被加载到内存,再到系统为其申请进程;从自己默默运行,到挖掘我们发送的信息对其产生的影响;从一个简简单单的字符串输出,到一个生命全过程的展现。以这些过程的分析为例,我们更好地说明了计算机的底层实现,并且更深地阐明了整个程序的生命周期。
关键词:P2P和020;计算机系统;程序生命全过程

目 录

第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 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在UBUNTU下编译的命令 - 8 -
3.3 HELLO的编译结果解析 - 8 -
3.4 本章小结 - 14 -
第4章 汇编 - 15 -
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的虚拟地址空间 - 22 -
5.5 链接的重定位过程分析 - 23 -
5.6 HELLO的执行流程 - 24 -
5.7 HELLO的动态链接分析 - 25 -
5.8 本章小结 - 26 -
第6章 HELLO进程管理 - 27 -
6.1 进程的概念与作用 - 27 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 27 -
6.3 HELLO的FORK进程创建过程 - 27 -
6.4 HELLO的EXECVE过程 - 27 -
6.5 HELLO的进程执行 - 28 -
6.6 HELLO的异常与信号处理 - 29 -
6.6.1 异常类型 - 29 -
6.6.2 处理方式 - 29 -
6.6.3 异常情况 - 31 -
6.7本章小结 - 34 -
第7章 HELLO的存储管理 - 35 -
7.1 HELLO的存储器地址空间 - 35 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 35 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 36 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 38 -
7.5 三级CACHE支持下的物理内存访问 - 38 -
7.6 HELLO进程FORK时的内存映射 - 39 -
7.7 HELLO进程EXECVE时的内存映射 - 40 -
7.8 缺页故障与缺页中断处理 - 40 -
7.9动态存储分配管理 - 41 -
7.10本章小结 - 43 -
第8章 HELLO的IO管理 - 44 -
8.1 LINUX的IO设备管理方法 - 44 -
8.2 简述UNIX IO接口及其函数 - 44 -
8.3 PRINTF的实现分析 - 45 -
8.4 GETCHAR的实现分析 - 47 -
8.5本章小结 - 48 -
结论 - 48 -
附件 - 50 -
参考文献 - 51 -

第1章 概述
1.1 Hello简介
P2P:意为 From Program to Process。在linux中,hello.c经过预处理、编译、汇编、链接四个阶段成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork产生子进程的过程。
020:意为From Zreo-O to Zero-O,shell为hello可执行程序进行执行,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello子进程。
1.2 环境与工具
软件环境:VMware Workstation Ubuntu 20.04 LTS
开发调试工具:gcc,as,ld,edb,readelf,hexedit
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c 源文件
hello.i 预处理后的文件
hello.s 编译后的文件
hello.o 可重定位文件
hello.elf可重定位文件的ELF
hello 链接后的可执行文件
hello1.elf 可执行文件的ELF
1.4 本章小结
本章主要简单介绍了hello的P2P,020过程,列出了本次实验的环境、中间结果。也列出了该篇论文完成所需要生成的一些中间文件,为后续实验提供了基本思路。

第2章 预处理
2.1 预处理的概念与作用
概念:预处理是指在进行编译的第一遍扫描(词法和语法)之前所做的工作。预处理由预处理程序负责完成。当对一个c源文件进行预处理时,系统自动引用预处理程序以解析以字符#开头的预处理命令(宏定义(#define)、文件包含(#include)、条件编译(#ifdef)),修改原始的C程序,处理完毕之后自动进入对源程序的编译。
主要作用如下:
1:删除宏定义“#define”展开并解析所定义的宏。
2:处理所有条件预编译指令,如“#if”,“#ifdef”, “#endif”等。
3:插入include后面的文件到“#include”处。
4:删除所有的注释“//”和“/* */”。
2.2在Ubuntu下预处理的命令
预处理命令:cpp hello.c > hello.i,截图如下:
在这里插入图片描述

2.3 Hello的预处理结果解析
执行上述命令后的.i文件截图:
在这里插入图片描述

在这里插入图片描述

通过打开hello.i,我们可以看到:文件有3000多行,而我们的代码在最后出现。这是由于之前#include的stdio.h,unistd.h,stdlib.h都进行了展开,并且放到了文件最前面。预处理的过程中的展开采用了循环展开的方法,以stdio.h的循环展开为例,cpp到默认的环境变量下寻找stdio.h,打开stdio.h 后发现其中依然使用了#define语句,cpp对此递归展开,所以最终.i程序中是没有#define的。
2.4 本章小结
本章主要介绍了预处理的概念与作用、并结合预处理之后的.i文件对预处理结果进行了一定的分析。预处理是将程序在编译之前进行的处理,其本质是一个替换的过程。通过宏展开和宏替换,插入头文件等操作,使得程序中的宏引用被递归地替换掉,从而从hello.c到hello.i。

第3章 编译
3.1 编译的概念与作用
编译的概念:广义的编译是指将一种语言写的程序翻译成等价的另一种语言的程序。在这里指的是把c语言程序翻译成等价的汇编语言程序:编译器ccl把预处理后的文本文件.i文件进行一系列语法分析及优化后生成相应的汇编语言文件.s文件的过程。.s文件中包含一个汇编语言程序。编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析,语法分析,语义检查和中间代码生成,代码优化,目标代码生成。
作用:编译是把高级语言程序翻译成更接近机器语言的汇编语言程序,从而通过汇编器进一步生成机器语言程序。
3.2 在Ubuntu下编译的命令
编译指令gcc -S hello.i -o hello.s
在这里插入图片描述

3.3 Hello的编译结果解析
3.3.1 文件结构
在这里插入图片描述

 .file		源文件名

.text 代码段
.section .rodata 下面是只读节
.align 对齐方式
.string 字符串
.globl 全局变量
.type 指定某个标识符是对象类型或是函数类型
3.3.2 数据
一:整形
(1)main的参数argc
在这里插入图片描述

Main的参数argc存储在edi中(通过将其move到-20(%rbp)中并将其与4进行比较的操作结合c源文件可以看出)
(2)局部整形变量i
函数内部的局部变量i会被编译器存储在寄存器或者程序栈中,它没有标识符,也不需要被声明,而是直接使用。
在这里插入图片描述

在.L2中,先把0给了-4(%rbp),之后再.L3中将其与7比较,结合c源代码可知-4(%rbp)中存储的是i。
二:字符串类型:
在这里插入图片描述

这些字符串是printf函数里面的字符串,这种字符串都会被放到只读数据区中,其中LC0是argc!=4时的输出LC1是argv=3时的输出。
三:指针数组
*argv[]是一个指针数组,存放我们在启动程序时输入的字符串,可以看出,这个指针数组的首地址在rsi后来移入-32(%rbp)中(这是为了让寄存器能够空出来方便其它操作),在调用argv[1],argv[2]时分别让栈指针加8和16。
在这里插入图片描述

3.3.3 操作
一:算数计算
在这里插入图片描述

这里是i++的实现,使用add。
二:逻辑运算
在这里插入图片描述

cmpl将内存中rbp-20的数据(实际上是argc)与4作比较,如果相等就跳转到L2。
三:赋值运算
C语言中的赋值是通过mov实现的,这里是对i赋初值0
在这里插入图片描述

四:指针操作
对参数指针数组argv的操作:
在这里插入图片描述

add $8,%rax是用来取argv[1]的地址,该数组元素是一个指针,再通过movq (%rax), %rax将argv[1]的内容给rax即把指向第二个参数的指针给rax。
3.3.4 控制转移
第一处控制转移是if(argc!=4),根据参数argc的值决定是跳转还是继续执行。汇编语言对应为语句cmpl $4,-20(%rbp)比较-20(%rbp)中的内容argc与4,并根据条件码选择跳转,如果argc==4,那么我们的程序就往下顺序执行,否则就跳转到.L2
在这里插入图片描述

第二处控制转移是for(i=0;i<8;i++),首先无条件跳转到循环体.L4之后的比较部位,cmpl将i与7进行比较,如果i小于等于7,则跳转到.L4进行循环,否则的话顺序往下执行。
在这里插入图片描述

3.3.5 函数调用
函数调用是跳转到特定代码执行待定函数之后再返回。函数调用可分为传递参数,调用函数,函数执行,函数返回四个阶段。
参数传递:在32位系统中,参数传递通过栈实现,在64位系统中,参数传递主要通过寄存器实现,当参数超过六个时,也会使用栈来传递参数。
调用函数:call指令会将返回地址压入栈中,并且将rip的值指向所调用函数的地址,等函数执行完之后调用ret弹出原来的rip并且将栈帧结构恢复。
函数执行:函数内部执行相应的操作
函数返回:函数返回时,如果有返回值,则先将返回值存在%rax中,再用leave与ret操作返回,将控制权还给调用函数。
hello.c文件中调用的函数有:main()、printf()、exit()、sleep()、atoi()、getchar()。
一:main函数
在这里插入图片描述

参数传递:第一个参数是argc(int型),第二个参数是*argv[ ](char *型),分别存放在寄存器%rdi和%rsi中;
Call调用:main函数被系统函数__libc_start_main调用,call指令将main函数的地址分配给%rip,随后调用main函数。
函数返回:main函数最后首先将0压入到eax中,然后调用了leave平衡栈帧,最后调用ret返回退出。

二:exit函数
在这里插入图片描述

参数传递:将1传给了%edi。
函数调用:通过call exit@PLT函数,进行函数调用。
函数返回:从exit返回,退出主函数。

三:printf函数
在这里插入图片描述

参数传递:call puts时传入了格式化字符串首地址;for循环中call printf前传入了 argv[1]和argc[2]的地址,可以看出,传递参数顺序是按照与参数列表中相反的顺序传递的;
函数调用:第一个printf()由call puts@PLT调用,第二个printf()由call printf@PLT调用;
函数返回:从printf中返回到主函数。

四:atoi函数
在这里插入图片描述

参数传递:将argv[3]通过%rdi传递给atoi函数。
函数调用:通过call atoi@PLT函数,进行函数调用。
函数返回:从atoi中返回到主函数。

五:sleep函数
在这里插入图片描述

参数传递:将%eax通过%rdi传递给sleep函数
Call调用:通过call sleep@PLT函数,进行函数调用。
函数返回:从sleep中返回到主函数。

六:getchar函数
在这里插入图片描述

参数传递:这个函数没有参数;
函数调用:通过call getchar@PLT调用getchar;
函数返回:从getchar中返回到主函数。

只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
本章主要阐述了编译器是如何将C语言程序转化为汇编语言程序的,并结合hello.c 程序到hello.s汇编代码之间的映射关系从数据和操作两方面来作出分析。

第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将.s汇编程序翻译成机器语言,把这些机器语言指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,这个过程就叫做汇编.
作用:将汇编语言翻译成机器语言,因为机器语言是计算机能直接识别和执行的一种语言.

4.2 在Ubuntu下汇编的命令
汇编命令gcc -c hello.s -o hello.o
在这里插入图片描述

4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
用readelf -a hello.o > hello.elf得到hello.o的elf格式。
4.3.1 ELF头
ELF头部以一个16字节的序列开始,描述生成该文件的系统的字的大小和字节顺序。剩下的部分包含ELF头的大小、目标文件的类型、机器类型,节头部表的文件偏移,以及节头部表中条目的大小和数量。
在这里插入图片描述

4.3.2节头部表
节头部表描述了不同节的偏移量和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。
在这里插入图片描述

4.3.3重定位条目
当汇编器生成.o文件时,它并不知道数据和代码最终将放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,当汇编器遇到对最终位置未知的目标引用,就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行目标文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。
在这里插入图片描述

4.3.4符号表
.symtab符号表负责存放程序中定义和引用的函数和全局变量的信息。
在这里插入图片描述

4.4 Hello.o的结果解析
使用objdump -d -r hello.o 得到hello.o的反汇编,与 hello.s进行对照分析,如下是反汇编得到的汇编语言程序。
在这里插入图片描述

与第三章对比可发现以下不同:
一:操作数进制表示:
可以看出,hello.s文件采用10进制,而反汇编的采用16进制。
在这里插入图片描述

二:函数调用
可以看出,hello.s中的函数调用是call 函数名,而反汇编得到的是call 相对地址,因为反汇编的代码已经经过了链接与重定位,知道了相对位置。
在这里插入图片描述

三:跳转分支
可以看出,hello.s文件的分支跳转是通过跳转到类似于.L2的形式来表示,而反汇编的代码使用跳转到某个相对位置来表示
在这里插入图片描述

4.5 本章小结
我们分析了hello.o的elf文件格式的信息,以及查看反汇编代码,对比函数调用,跳转分支,操作数进制表示等过程查看了其与hello.s的区别。我们看到编译之后的汇编将我们的程序向机器能识别的语言又推进了一大步。在汇编中,我们进行了更多的处理,使其变成一个可重定向文件,可以进行下一步的链接了。

第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时,加载时,甚至是运行时。
作用:可重定位目标文件中有许多符号比如全局变量和函数需要通过链接来解释并链接到一个目标上。比如当程序调用函数库中的一个函数printf,在汇编后生成的可重定位目标文件中,这个函数没有与一个具体的函数关联起来,因为printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,因此这个函数必须通过链接器将这个文件合并到hello.o程序中,结果得到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
在这里插入图片描述

此时生成了可执行文件hello
5.3 可执行目标文件hello的格式
依然使用readelf -a hello > hello1.elf命令生成ELF头文件。

5.3.1 ELF头
ELF头描述文件的总体格式。包括程程序运行时要执行的第一条指令的地址。程序头部大小和数目也确定了下来。其余的部分基本与可重定位目标文件相同。(具体数据可能会有修改)
在这里插入图片描述

5.3.2节头
节头对hello中所有的节信息进行了声明,对于节的分类也更加详细,也确定了每个节在运行时的实际起始地址以及偏移量(大小)。其余部分都与可重定位目标文件的节头相同。(具体数据可能会有修改)
在这里插入图片描述

5.3.3程序头
一共有8个段
(1)PHDR 包含程序头表本身
(2)INTERP:只包含了一个节,在这个节中,包含了动态链接过程中所使用的解释器路径和名称。
(3)两个LOAD:第一个是代码段,第二个是数据段。在程序运行时需要映射到虚拟空间地址。
(4)DYNAMIC:保存了由动态链接器使用的信息。
(5)NOTE: 保存了辅助信息。
(6)GNU_STACK:堆栈段。
(7)GNU_RELRO:在重定位之后哪些内存区域需要设置只读。
在这里插入图片描述

5.3.4重定位节
这里的重定位节已经完成了重定位,可以看出这些符号表中的符号的信息都被记录在这里。
在这里插入图片描述

5.4 hello的虚拟地址空间
在这里插入图片描述

可以看出ELF头出现在0x400000的位置上,其余各段按照前面的节头依次映射
在这里插入图片描述

比如只读数据段中就有print的格式控制字符串中的hello(旁边的乱码是中文导致的)。

在这里插入图片描述

5.5 链接的重定位过程分析
首先使用反汇编objdump -d -r hello结合hello.o的反汇编,可以得出以下几处不同。
(1)地址的访问:
hello.o中的相对地址到了hello中变成了虚拟地址。而hello.o文件中对于.rodata的访问,是类似于.LC0(%rip)的形式, 因为它们的地址是在运行时确定的,因此访问也需要重定位,这样的行事也会告诉机器在运行时进行这些重定位。
(2)函数调用:
hello中无hello.o中的重定位条目,在hello中链接时加入了在hello.o中用到的函数并在调用时给出了函数的虚拟地址,如exit、printf等函数。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
(3)增加的节:hello中增加了.init和.plt节,和一些节中定义的函数。
5.6 Hello的执行流程
1.调用start函数,地址0x4010f0
在这里插入图片描述

2.调用__libc_start_main函数,地址0x7f32e0c0ffc0
在这里插入图片描述

3.调用libc-2.27.so!__cxa_atexit ,地址0x7faf e65cef60
在这里插入图片描述

4.调用libc-2.31.so!_setjmp函数,地址0x7f2033be0e00
在这里插入图片描述

5.调用libc-2.31.so!_sigsetjmp函数,地址0x7f2033be0d30
在这里插入图片描述

6.调用main函数,地址 0x401125
在这里插入图片描述

7.调用printf函数,地址 0x4010a0
8.调用atoi函数,地址 0x4010c0
9.调用sleep函数,地址 0x4010e0
10.调用getchar函数,地址 0x4010b0
上述四个函数是main里面的函数,在edb中不显示名字。
在这里插入图片描述

11.调用libc-2.27.so!exit函数,地址0x7f18d89eebc0
在这里插入图片描述

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

调用后:
在这里插入图片描述

分析:
假设程序调用一个全局变量,由于代码段到数据段的距离是不变的,所以相对位置不变。编译器就在数据段开始的地方创建了一个全局偏移量表(GOT),并为GOT中的每一个条目生成一个重定位记录。加载时动态链接器会重定位GOT中每个条目,使其包含正确的绝对地址。
假设程序调用一个由共享库定义的函数,编译器没有办法预测函数的运行时地址,动态链接器在程序加载时再解析它.称为延迟绑定.延迟绑定通过GOT和过程链接表(PLT)实现,每个调用共享库函数的模块都有自己的GOT和PLT,其中PLT是代码段的一部分。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码,GOT存放的是PLT中函数调用指令的下一条指令地址。
在函数调用时,首先跳转到PLT执行.plt中操作,第一次访问跳转时GOT地址为下一条指令,将函数序号入栈,然后跳转到PLT[0],之后将重定位表地址入栈,访问动态链接器,在动态链接器中使用在栈里保存的函数序号和重定位表计算函数运行时的地址,重写GOT,并返回调用函数.之后如果还有对该函数的访问,就不用执行第二次跳转,直接参看GOT信息.

5.8 本章小结
本章从链接的概念与作用开始,主要介绍了hello的ELF格式以及与hello.o的区别,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
链接是一种非常高效且有用的技术。链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

第6章 hello进程管理
6.1 进程的概念与作用

概念:进程就是一个执行中程序的实例.每次用户通过向shell中输入一个可执行目标文件的名字,运行程序时, shell就会创建一个新的进程.
作用:在现代系统上运行程序时,我们会得到一个假象,我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象. 

6.2 简述壳Shell-bash的作用与处理流程
shell是一个应用程序,他在操作系统中提供了一个用户与系统内核进行交互的界面。他的处理过程一般是这样的:首先从终端读入输入的命令,并将输入字符串分割获取参数,如果是内置命令则立即执行,如果不是内置命令则调用对应的程序并运行。Shell还可以接受键盘输入的信号比如ctrl c,并对这些信号进行处理。
6.3 Hello的fork进程创建过程
父进程通过fork函数创建一个新的运行的子进程.新的子进程几乎但不完全与父进程相同.子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈.子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork 时,子进程可以读写父进程中打开的任何文件.父进程与子进程最大的区别是它们有不同的pid。
在这里插入图片描述

6.4 Hello的execve过程
Execve函数原型为:int execve(const char *filename, const char *argv[], const char *envp[]),Execve函数加载并运行可执行目标文件filename, 且带参数列表argv 和环境变量列表envp .只有当出现错误时,例如找不到filename, execve才会返回到调用程序.一般情况下, execve调用一次并从不返回.
Execve函数加载和运行文件包括以下步骤:

  1. 删除已存在的用户区域:删除当前进程虚拟地址的用户部分中的已存在的区域结构。
  2. 映射私有区域:为新程序的代码,数据,bss和栈区域创建新的区域结构。
  3. 映射共享区域:如果某些对象是动态链接到这个程序的,再将其映射到用户虚拟地址空间的共享区域内。
  4. 设置程序计数器设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
    注意:在加载过程中没有任何的数据复制。
    6.5 Hello的进程执行
    首先介绍进程上下文信息、进程时间片,逻辑控制流,用户模式与内核模式。
    (1)上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
    (2)进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
    (3)逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,这些序列对应于包含在程序的可执行目标文件中或者动态链接到程序的共享对象中的指令。进程是轮流使用处理器的,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
    (4)用户模式和内核模式:为了使操作系统内核提供一个无懈可击的进程抽象,处理器提供一种机制限制一个应用可以执行的指令以及它可以访问的地址空间范围。
    处理器通常使用控制寄存器的一个模式位提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据,用户程序必须通过系统调用接口间接地访问内核代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
    下面我们来看hello sleep函数进程调度的过程(当内核代表用户执行系统调用时,可能会发生上下文切换,sleep休眠函数会显式地请求让调用进程休眠,然后会发生上下文切换):
    当调sleep函数时,一般会进行上下文切换,上下文切换是由内核的调度器完成的,当内核准备调度新的进程运行后,它就会抢占当前进程,并进行如下操作:
    (1)保存当前进程的上下文
    (2)恢复将要恢复进程被保存的上下文
    (3)将控制传递给这个将要恢复的进程,完成上下文切换。
    过程如下图(将read函数换为sleep函数即可):hello开始运行在用户模式,在hello进程调用sleep之后进入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器计时结束后发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了
    在这里插入图片描述

6.6 hello的异常与信号处理
6.6.1 异常类型
在这里插入图片描述

6.6.2 处理方式
(1)中断:中断是异步发生的,是来自处理器外部的 I/O 设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的。硬件中断的异常处理程序常常称为中断处理程序。处理过程如下图:
在这里插入图片描述

(2)陷阱:陷阱是有意的异常,是执行一条指令后的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。处理过程如下图:
在这里插入图片描述

(3)故障:故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果故障处理程序能够修正这个错误,它就将控制返回给引起故障的指令,从而重新执行它,否则,处理程序返回到内核中的 abort 例程,abort 例程会终止引起故障的应用程序。处理过程如下图:
在这里插入图片描述

(4)终止处理:终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,终止处理程序不会将控制返回给应用程序。
在这里插入图片描述

6.6.3 异常情况
(1) 不停乱按
在这里插入图片描述

程序运行情况正常,shell将乱输入的第一个字符当做getchar的输入,其余都当做新的shell命令,在hello进程结束被回收之后,将会在命令行中尝试解释这些命令。
(2)Ctrl-Z

在这里插入图片描述

运行时按Ctrl-Z之后,将会发送一个SIGTSTP信号给shell。然后shell将转发给当前执行的前台进程组,使hello进程停止。
再输入ps查看当前进程

在这里插入图片描述

输入jobs命令
在这里插入图片描述

可以看出,刚才那个进程已停止,再输入pstree
在这里插入图片描述

这是一个树状图,显示了进程之间的关系。再输入fg
在这里插入图片描述

这是一个继续运行的命令,可以看出,刚才停止的程序继续运行,此时正在等我们输入一个字符。再运行一次这个程序,使用ctrl-z停止后再用kill杀死程序,结果如下。
在这里插入图片描述

(3)Ctrl-C

在这里插入图片描述

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本章简述了进程管理以及shell的一些信息,包括进程的概念与作用,shell的作用和处理流程,shell如何调用fork和execve运行我们的hello进程,以及hello是如何被执行的,当hello进程在执行时遇到特殊情况(比如回车,Ctrl-Z,Ctrl-C等)会如何处理。又介绍了一些常见异常和其信号处理方法。我们可以看出,一个进程的运行与信号密切相关,受到信号的调控

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序代码经过编译后出现在汇编程序中地址.逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址.

线性地址与虚拟地址:逻辑地址经过段机制后转化为线性地址(虚拟地址),是逻辑地址到物理地址变换之间的中间层.在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址.是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元.线性地址通常用十六进制数字表示,值得范围从0x00000000到0xfffffff)程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址.如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址.如果没有启用分页机制,那么线性地址直接就是物理地址.

物理地址:CPU地址总线传来的地址,由硬件电路控制(现在这些硬件是可编程的了)其具体含义.物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、BIOS等).在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址.

在hello程序中,他就表示了这个程序运行时的一条确切的指令在内存地址上的具体哪一块进行执行。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是从逻辑地址到线性地址的变换。
一个逻辑地址由两部分组成,段标识符、段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号,后面3位包含一些硬件细节,索引号,是“段描述符”,段描述符具体地址描述了一个段。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,由8个字节组成。
给定一个逻辑地址[段选择符:段内偏移地址],转换过程如下:
1、首先根据段选择符判断当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。这样就得到了一个数组。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、Base + offset就是要转换的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是从线性地址到物理地址的变换。
Linux有一个的虚拟内存系统,其虚拟内存组织形式如下图。Linux将虚拟内存组织成一些区域(称为段)的集合,段之外的虚拟内存不存在因此不需要记录。内核为hello进程维护一个单独的任务结构即图中的task_struct,其中条目 mm指向一个mm_struct,它描述了虚拟内存的当前状态,pgd指向第一级页表的基地址(结合一个进程一串页表),mmap指向一个vm_area_struct的链表,每个vm_area_struct都维护者一个区域。
在这里插入图片描述

物理内存被划分为一小块一小块的帧。分配内存时,帧是分配时的最小单位,最少也要给一帧。在虚拟内存中,与帧对应的概念就是页。线性地址的表示方式是:前部分是虚拟页号后部分是虚拟页偏移。
CPU通过将逻辑地址转换为虚拟地址来访问主存,这个虚拟地址在访问主存前必须先转换成适当的物理地址。CPU通过内存管理单元(MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址。然后CPU会通过这个物理地址来访问物理内存。
页表就是一个页表条目(PTE)数组,虚拟地址空间中的每个页在页表中的一个固定偏移量处都有一个PTE。PTE是由一个有效位和一个n个字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置。
MMU利用虚拟页号(VPN)来在虚拟页表中选择合适的PTE,当找到合适的PTE之后,PTE中的物理页号(PPN)和虚拟页偏移量(VPO)就会组合形成物理地址。其中VPO与PPO相同,因为虚拟页大小和物理页大小相同,所需要的偏移量位数也就相同。此时,物理地址就通过物理页号先找到对应的物理页,然后再根据物理页偏移找到具体的字节:
1.如果有效位是0,PPN为NULL 则代表没有在虚拟内存空间中分配该内存;
2.如果是有效位0,PPN不为NULL,则代表在虚拟内存空间中分配了但是没有被缓存到物理内存中;
3.如果有效位是1则代表该内存已经缓存在了物理内存中,可以得到其物理页号PPN,与虚拟页偏移量共同构成物理地址PA。
在这里插入图片描述

7.4 TLB与四级页表支持下的VA到PA的变换
在这里插入图片描述

虚拟地址是由VPN和VPO组成的,VPN可以作为在TLB中的索引,如上图所示,TLB可以看作是一个PTE的cache,将常用的PTE缓存到TLB中,加速虚拟地址的翻译。如果能够在TLB中找到与VPN对应的PTE,即为TLB命中,TLB直接给出PPN,然后PPO即为VPO,这样就构成了一个物理地址。
如果不能做到TLB hit就要到四级页表当中取寻址,在i7中VPN有36位,被分成了四段,从左往右的前三个九位的地址分别对应于在前三级页表当中的偏移,偏移在页表中所对应的页表条目指向某一个下一级页表,而下一个9位VPN就对应的是在这个页表中的偏移。最后一级页表中的页表条目存放的是PPN
比如VPN1在第一级页表中对应于一个页表条目,这个页表条目指向下一级页表中的某个页表,再依靠VPN2在这个页表中找到它对应的页表条目,同样,这个也表条目指向的是第三级页表中的某个页表,再依靠VPN3找到在这个页表中与之对应的页表条目,这个页表条目指向的是第四级页表中的某个页表,再依靠VPN4找出与之对应的页表条目,这个页表条目中存放的是PPN,在四级页表中最多可以存放512G的内存内容。
最后再把VPO作为PPO就能找到在对应的物理页上存放的内容了。
7.5 三级Cache支持下的物理内存访问
下图是现代计算机的高速缓存层次结构:
在这里插入图片描述

获得了物理地址VA之后,使用CI进行组索引,每组8路,对8路的块分别匹配CT如果匹配成功且块的valid标志位为1,则命中,根据数据偏移量CO取出数据返回.
如果没有匹配成功或者匹配成功但是标志位是1,则不命中,向下一级缓存中查询数据(L2 Cache->L3 Cache->主存),查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用LFU或LRU进行替换。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork函数在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当两个进程中的任一个后来进行写操作时,写时复制就会创建新页面。因次,也就为每个进程保持了私有地址空间的概念。
7.7 hello进程execve时的内存映射
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序.加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构.
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的.代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零.
3.映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内.
4.设置程序计数器, execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点.
在这里插入图片描述

7.8 缺页故障与缺页中断处理
分三步:
第一步先判断是不是一个合法的地址,即通过不断将这个地址与每个区域的vm_start和vm_end进行比对,如果不是在一个区域里的话,就给出segmentation fault,因为它引用了一个不合法的地址
第二步确认访问权限是不是正确的。比如如果这一页是只读页,但是却要做出写这个动作,那明显是非法的。如果做出了这类动作,那么处理程序就会触发一个保护异常,默认行为是结束这个进程
第三步确认了是合法地址并且是符合权限的访问,那么就用某个特定算法选出一个牺牲页,如果该页被修改了,就将此页滑出(swap out)并且swap in那个被访问的页,并将控制传递到触发缺页中断的那条指令,这条指令继续执行的时候就不会触发缺页中断,这样就可以继续执行下去。
在这里插入图片描述

7.9动态存储分配管理
7.9.1.动态内存分配的基本原理:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间实现细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk, 它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护.每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的.已分配的块显式地保留为供应用程序使用.空闲块可用来分配.空闲块保持空闲,直到它显式地被应用所分配.一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的.
显式分配器(explicit allocator):要求应用显式地释放任何已分配的块。
隐式分配器(implicit allocator):要求当分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块.隐式分配器也叫做垃圾收集器(garbage collector),而自动释放未使用的已分配的块的过程叫做垃圾收集。

7.9.2:简单的放置策略:
(1)首次适配:从头搜索,遇到第一个合适的块就停止;
(2)下次适配:从上次插入的位置搜索,遇到第一个合适的块停止;
(3)最佳适配:全部搜索,最终选择大小最合适的块。

7.9.3.三种堆的数据结构组织形式:
隐式空闲链表:
一个块是由一个字的头部,有效载荷,以及可能的一些额外的填充组成的.头部编码了这个块的大小,以及这个块是已分配的还是空闲的.如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是零.因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息.在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的.

带边界标签的隐式空闲链表:
在隐式空闲链表堆块的基础上,在每个块的结尾处添加一个脚部(footer),边界标记),其中脚部就是头部的一个副本,这样方便于进行空闲块合并操作

隐式链表的好处在于它简单,易于操作,但是它的坏处在于搜索时间与块的数量成线性关系,如果采用首次适配算法的话内存利用率会低,但如果采用最佳适配的话需要对于整个堆进行搜索。

显式空间链表
根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。
显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。
一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。

红黑树:
红黑树在教材中并没有详细讲解,然而这却是一种十分高效的方法,通过将空闲块组织成红黑树,对于需要寻找空闲块的操作比如插入操作的时间复杂度为log(空闲块的数量)。
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、linux的页式管理,TLB与四级页表支持下的VA到PA的变换、三级cache支持下物理内存访问, hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容,尤其是最后补充的红黑树方法对于以后提高c语言内存分配的性能至关重要。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有的IO设备都被模型化为文件,而所有的输入输出都被当作对相应文件的读和写来执行。
设备管理:将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,这就是Unix I/O接口。
8.2 简述Unix IO接口及其函数
Unix IO接口:
打开文件,内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。
Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。
改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k
关闭文件。当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中

Unix I/O函数:
1.open 函数:打开一个已存在的文件或者创建一个新文件
函数原型:int open(char *filename, int flags, mode_t mode);
open 函数将filename 转换为一个文件描述符,并且返回描述符数字.返回的描述符总是在进程中当前没有打开的最小描述符.flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位.返回:若成功则为新文件描述符,若出错为-1.
2.close 函数:关闭一个打开的文件.
函数原型:int close(int fd);
返回:若成功则为0, 若出错则为-1.
3.read 和write 函数:执行输入和输出
resd函数原型:ssize_t read(int fd, void *buf, size_t n);
read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf .返回值-1表示一个错误,而返回值0 表示EOF.否则,返回值表示的是实际传送的字节数量.
返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1.
write函数原型:ssize_t write(int fd, const void *buf, size_t n);
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置.图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出.返回:若成功则为写的字节数,若出错则为-1.
8.3 printf的实现分析
在这里插入图片描述

这是printf函数的函数体,参数中明显采用了可变参数的定义,可以看到*fmt是一个char类型的指针,指向字符串的起始位置。
printf接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出,fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。fmt也是个变量,它是在栈上分配的,它也有地址。
可以看出,它调用了vsprintf函数:
int vsprintf(char *buf, const char fmt, va_list args)
{
char
p;
char tmp[256];
va_list p_next_arg = args;

for (p=buf;*fmt;fmt++) { 
if (*fmt != '%') { 
*p++ = *fmt; 
continue; 
} 

fmt++; 

switch (*fmt) { 
case 'x': 
itoa(tmp, *((int*)p_next_arg)); 
strcpy(p, tmp); 
p_next_arg += 4; 
p += strlen(tmp); 
break; 
case 's': 
break; 
default: 
break; 
} 
} 

return (p - buf); 

}
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。而语句i = vsprintf(buf, fmt, arg); 获得的是一个长度,要打印出来的字符串的长度。
之后printf又调用了write函数。
在这里插入图片描述

在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。Syscall如下:
在这里插入图片描述

ecx中是要打印出的元素个数 ,ebx中的是要打印的buf字符数组中的第一个元素,这个函数的功能就是不断的打印出字符,直到遇到:’\0’ 停止.
综合分析printf函数实现过程:
(1)vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
(2)vsprintf的输出到write系统函数中。在Linux下,write通过执行syscall系统调用实现了对系统服务的调用,从而使内核执行打印操作。内核会通过字符显示子程序,根据传入的ASCII码到字模库读取字符对应的点阵,然后通过vram(显存)对字符串进行输出。
(3)显示芯片将按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终实现printf中字符串在屏幕上的输出。
8.4 getchar的实现分析
getchar函数体:
在这里插入图片描述

getchar函数通过调用read函数来读取字符。read函数有三个参数,第一个参数为文件描述符fd,fd为0表示标准输入;第二个参数为输入内容的指针;第三个参数为读入字符的个数。read函数的返回值是读入字符的个数,若出错则返回-1。
当程序调用getchar时.程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。
异步异常-键盘中断的处理:键盘中断处理子程序
首先接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。之后getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章讲述了linux下I/O设备的管理方法,了解了Unix I/O函数,深入分析了printf函数和getchar函数的实现。
与此同时,深入了解了Unix I/O 在系统中是多么重要的存在,I/O 是系统操作不可或缺的一部分.有时你除了使用Unix I/O 以外别无选择。

结论
一个简单的hello程序背后所蕴含的技术包括了计算机系统以及计算机硬件的方方面面,本文较为详细的介绍了hello的光辉的一生。
最开始,hello要经过一系列的编译:首先,在预处理器cpp中,hello.c经过预处理,与所有的外部库合体成为了hello.i;再在编译器ccl中经过编译,成为了hello.s;之后,汇编器as又将hello.s转换为可重定位的目标文件hello.o;最后,链接器ld会把hello.o进行链接,进行符号的重定位,这样,可执行的目标程序hello就诞生了!
紧接着运行程序,当我们在shell中输入“./hello”时,bash会新建一个进程,先fork一个子进程,然后调用execve清空当前进程的数据并加载hello,之后通过mmap建立虚拟内存映射,设置当前进程的上下文中的程序计数器,使之指向程序入口处,CPU分给这个进程时间片,使它得以在硬件上流水线化执行。
从函数的入口进入,开始执行。程序运行时需要调用共享库函数时,需要动态链接,即第一次调用该函数时通过PLT和GOT两个数据结构和动态链接器的配合,得到函数的运行时地址并更新GOT。之后再调用时,因为已经进行过动态链接,就可以直接通过对应的GOT条目得到运行时地址。运行中我们可能需要访问内存,这时CPU中的段式管理机制先将机器指令中的逻辑地址翻译成虚拟地址,之后CPU上的内存管理单元MMU会先将虚拟内存地址翻译成物理内存地址,然后去页表的缓存TLB里查,查不到的话再去4级页表查,如果不命中要触发缺页异常更新页表,最终页表命中,根据页表项将虚拟内存地址翻译成物理内存地址,然后访问内存。由于各种各样的原因,我们的hello可能会暂时的休息(系统调用或者计时器中断),这时我们保留当前进度,并切换上下文,让内核去处理别的进程,提高计算机内核运行程序的效率。我们还可以输入信号来终止或挂起hello进程,hello输出信息时需要调用printf和getchar函数,而printf和getchar的实现需要调用Unix I/O中的write和read函数,而它们的实现需要借助系统调用I/O,在最后结束之后bash收到exit命令,作为hello的父进程回收hello。随后,内核删除他所有的数据,hello的此次旅途也就到达终点。但它仍在磁盘中等待下一次旅途的开始。
感悟:计算机的确让人难以理解,究其原因,一是计算机是经历了许多年的发展,其中很多思想精髓都是一点一点摸索出来的,凝聚着人类的智慧。难以理解,二是我们对计算机的组成、程序的实现理解不透彻。而计算机系统确实值得我们花费时间深入探究的,即使这个过程并不容易,因为这会从底层优化计算机的运行速度。

附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c 源文件
hello.i 预处理后的文件
hello.s 编译后的文件
hello.o 可重定位文件
hello.elf可重定位文件的ELF
hello 链接后的可执行文件
hello1.elf 可执行文件的ELF

参考文献
[1] 深入了解计算机系统(第三版)2016 Bryant,R.E. 机械工业出版社
[2] https://www.cnblogs.com/mikevictor07/p/8142678.html
[3] https://blog.csdn.net/Limit_Fly/article/details/93142756
[4] https://blog.csdn.net/weixin_44551646/article/details/98076863

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值