计算机系统大作业

计算机系统大作业

题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学   号 1190202419
班   级 1903005   
指 导 教 师 史先俊

计算机科学与技术学院
2021年6月
摘 要
本文主要讲述了hello.c程序在编写完成后运行在linux中的生命历程,概述了预处理、编译、汇编、链接等各个过程在linux下实现的原理,分析了这些过程中产生的文件的相应信息和作用。并介绍了shell的内存管理、IO管理、进程管理以及异常信号处理等相关知识。
关键词:linux;预处理;编译;汇编;链接;进程;内存管理;虚拟内存;I/O;

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

目录

第1章 概述 - 4 -

1.1 Hello简介

P2P(from program to process):
使用C语言编辑器将hello的C代码编辑并保存到hello.c文件中,这就是program。
接着hello.c要经过预处理->编译->汇编->链接这几个过程最终生成hello可执行文件。这样一个process就诞生了。如下图所示。
在这里插入图片描述
020(from zero-0 to zero-0):
用户通过shell输入指令“./hello 1190202419 李洲 n(秒数)”,shell执行一系列指令来加载可执行目标程序hello,并且同时为该程序映射出虚拟内存。当该程序的进程开始运行时,系统会为其分配并且载入物理内存。在进程运行过程中,shell会调用fork()函数,生成一个子进程,并在这个子进程中调用execve()函数加载hello程序。一旦目标文件hello中的代码和数据被加载到主存,处理器就开始执行hello的main程序中的机器指令。这些指令将“Hello 1190202419 李洲\n”字符串从主存复制到寄存器,再从寄存区复制到显示设备,最终显示在屏幕上,每隔n秒输出一次,一共输出八次,然后再等待用户输入一个回车或ctrl+c结束进程(在输入回车前用户可以输入任何别的字符)。hello的进程结束时,shell会将其的内存空间回收。
在这里插入图片描述

1.2 环境与工具级目录

硬件环境:
Intel(R)Core™i7-8750H CPU @ 2.20GHz 16G RAM X64 128GSSD + 1TGHD
软件环境:
OS:windows 10 64bit;Ubuntu 20.10 LTS 64bit
开发调试工具:
GDB;EDB;OBJDUMP;READELF;vim;gcc

1.3 中间结果

hello.i 预处理之后的文本文件
hello.s 编译之后的汇编文本文件
hello.o 汇编之后的可重定位目标文件
hello 链接后的可执行目标文件
hello.elf hello的elf输出
hello.objdump hello.o的反汇编代码

1.4 本章小结

简单介绍了hello程序的从出生到死亡的基本历程。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:
预处理一般发生在程序源代码被翻译成目标代码、生成二进制代码之前,是指预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。最终得到一个以.i作为文件扩展名的C文件hello.i。
预处理的作用:
预处理需要有相应的预处理指令才能执行,C语言中,常见的有#include、#define等指令,这些预处理指令由预处理程序操作。
分类如下:
(1)文件包含:#include是C程序设计中最常用的预处理指令。作用是读取#include后面紧跟的文件名所对应的文件,并将其直接插入程序文本中。例如,hello.c文件中有#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。另外,包含文件的格式有#include <>和#include “”之分,前者的搜索路径为系统目录,后者则是当前工作目录。
(2)宏定义:包括#define和#undef。
#define可以定义无参数宏和带参数的宏定义,例如:
在这里插入图片描述
#undef表示删除已定义的宏,例如:
在这里插入图片描述
(3)条件编译:主要为了有选择地执行相应操作,防止宏替换内容的重复包含,常见命令有#if、#elif、#else、#endif、#ifdef、#ifndef。
(4)特殊控制:ANSI C还定义了ANSI C 还定义了特殊作用的预处理指令。如 #error、#pragma。
#error:使预处理器输出指定的错误信息,通常用于调试程序。
#pragma:是功能比较丰富且灵活的指令,可以有不同的参数选择,从而完成相应的特 定功能操作。调用格式为:#pragma 参数。
其中,参数可以有 message 类型、code_seg、once、warning、pack 等。通常使用如下的预处理指令来设定内存以 n 字节对齐方式。#pragma pack (n) //其中 n 称为对齐系数,取 1、2、4、8…

2.2在Ubuntu下预处理的命令

根据要求命令应为:gcc -m64 -no-pie -fno-PIC -E hello.c >hello.i
在这里插入图片描述

2.3 Hello的预处理结果解析

对比一下就可以看出,原本的hello.c文件只有23行代码。
在这里插入图片描述
而预处理后达到了3069行!
在这里插入图片描述
很显然预处理操作将原hello.c文件扩展为hello.i文件。而这些被扩展的代码就是在这里插入图片描述
这三行命令中的三个头文件。

2.4 本章小结

简单介绍了预处理的概念和作用。并实际操作生成了hello.i文件。

第3章 编译

3.1 编译的概念与作用

编译的概念:
编译是指将预处理后得到的C语言(或其他高级语言)程序文本翻译成汇编语言的过程。
编译的作用:
经过预处理得到的hello.i仍是C代码文件,而C语言等高级语言是无法被计算机底层识别并执行的。计算机只能读懂二进制机器代码,而汇编代码是机器代码的文本表示。因而需要编译器(ccl)对C代码进行词法和语法分析,在确认所有指令都符合语法规则后,再将C代码翻译成等价的汇编代码。

3.2 在Ubuntu下编译的命令

根据要求命令应为:gcc -m64 -no-pie -fno-PIC -S hello.i >hello.s
在这里插入图片描述

3.3 Hello的编译结果解析

编译完后的hello.s文件(一共80行):
在这里插入图片描述
在这里插入图片描述

3.31开头

.file指的是源文件为hello.c,.text下面是代码段,.section .rodata下面是rodata节,.align声明8字节对齐,.long、.string等声明对应的数据类型。
.globl声明全局变量main,.type用来指定是函数类型还是对象类型。这里@function指函数类型。
在这里插入图片描述

3.32数据

本程序用到的数据类型包括:整型、字符串和数组。
在这里插入图片描述
(1)整型:int i和int argc。以及其他立即数(直接以”$+数字”的形式)。
argc指的是后面参数argv指针数组的元素个数。argc作为main函数的第一个参数传入。如下图,寄存器edi存放第一个参数argc。并被存放到栈上-20(%rbp)处。
在这里插入图片描述
i是main函数中定义的局部变量,被存放在栈上-4(%rbp)位置。
在这里插入图片描述
在这里插入图片描述
(2)字符串:
在这里插入图片描述
这两个字符串的存放位置如下图所示,均在.rodate只读数据段。且其中的汉字被编码成utf-8格式。
在这里插入图片描述
(3)数组:char *argv[]指针数组,用来存放指向传入的字符串参数的指针,每个指针元素指向一个参数。寄存器rsi存放的argv[]。且被压入栈上-32(%rbp)处。argv[0]指向本程序名。argv[1]~argv[3]指向传入的三个参数,在栈中的位置如下图所示:
在这里插入图片描述

3.33赋值

mov指令结尾后缀b、w、l、q是大小指示符,分别代表1字节、2字节、4字节、8字节。
在本程序中,为i赋值为0movl表明传入4字节的值。

3.34类型转换

本程序通过调用atoi()这个库函数实现了将存在-8(%rbp)处的argv[3]由字符串转化为整型数据(即用于传给sleep函数的秒数)。如下图所示。
在这里插入图片描述

3.35算数操作

(1)加: x=x+y汇编语言是addq y,x
(2)减: x=x-y 汇编语言是subq y,x
(3)乘: x=x*y 汇编语言是imulq y,x
(4)除: z=x/y 汇编语言是
movq x, z
cqto
idivq y
(5)复合语句就是上面的组合,或者也有复合的汇编语句:
z=x+Ay+B(A,B都是立即数)的汇编语言是leaq B(x,y,A) z
本程序仅出现了i++操作:同样后缀“l”表明操作4字节数。

3.36关系操作

两个cmpl,je表示跳转的条件是小于(后者较前者),jle表实小于等于。
在这里插入图片描述在这里插入图片描述

3.37数组、指针和结构体

(1)数组:取数组起始地址加上第i位的偏移量来处理,比如数组元素大小为4字节,则取数组下标为i的元素时应为起始地址+4*i
(2)指针:若寄存器rax中存放的是指向某个数据x的指针,则访问x的值就是(%rax)
(3)结构体:通过结构体内部各元素的大小来设置偏移量访问
本程序中有数组和指针的访问:
在这里插入图片描述

3.38控制转移

本程序中有if选择和for循环。
if(argc!=4){}
在这里插入图片描述
若argc等于4,则执行for循环:
在这里插入图片描述

3.39函数操作

函数是一种过程,过程提供了一种封装代码的方式,用一组指定的参数和可选的返回值实现某种功能。P 中调用函数 Q 包含:
传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。
传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回一个值。分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。
本程序中的函数有:
(1)main函数:
main函数被call执行,call指令将下一条指令的地址压栈,之后跳转到main函数。调用main函数时向其传递的参数为argc和argv,分别用寄存器edi和rsi存放。eax存放返回值。
在这里插入图片描述在这里插入图片描述
(1)puts函数:
edi存放参数字符串“用法: Hello 学号 姓名 秒数!\n”的首地址。
在这里插入图片描述
(2)printf函数:
edi存放参数字符串“Hello %s %s\n”的首地址。rdx和rsi分别存放argv[2]和argv[1]指向的字符串。
在这里插入图片描述
(3)atoi函数:
rdi存放argv[3]指向的字符串参数,eax存放返回值。
在这里插入图片描述
(4)sleep函数:
edi存放参数(秒数)。
在这里插入图片描述
(5)getchar函数:不需要参数。
在这里插入图片描述
函数调用通过call语句实现,参数传递首要通过寄存器,如果寄存器不够则会通过栈的方式传递参数。

3.4 本章小结

简单介绍了编译的概念和作用,并对实际操作生成的hello.s文件进行了分析。

第4章 汇编

4.1 汇编的概念与作用

汇编器(as)将hello.s翻译成机器语言指令(计算机可读的二进制代码),把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。为下一步的链接做准备。

4.2 在Ubuntu下汇编的命令

根据要求应为:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
在这里插入图片描述

4.3 可重定位目标elf格式

ELF头:
在这里插入图片描述
开头16字节序列Magic是魔数,这个序列描述了生成该文件的系统的字的大小和字节顺序,确定文件的类型和格式。加载文件时,可通过魔数Magic来确认文件类型是否正确。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是有节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。
(2)节头表:记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。
在这里插入图片描述
(3)夹在ELF头和节头表之间的都是节。
.text:已编译程序的机器代码
.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表
.data:已初始化的全局变量和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
.bss:未初始化的全局和静态变量,以及所有被初始化为0的全局或静态变量(linux下)
.symtab:符号表,存放在程序中定义和引用的函数和全局变量的信息。
.rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。
.rel.data:被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果他的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
.debug:调试符号表,其条目是程序中定义得局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。
.line:原始C源程序的行号和.text节中机器指令之间的映射。
.strtab:字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部表中的节名字。
对于重定位节,本程序需要被重定位的是printf、puts、atoi、exit、sleep、getchar和.rodata中的.LC0和.LC1。.rela.eh_frame节是.eh_frame节重定位信息。
在这里插入图片描述
(5)符号表.symtab,它存放在程序中定义和引用的函数和全局变量的信息,一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
在这里插入图片描述

4.4 Hello.o的结果解析

在这里插入图片描述
在这里插入图片描述
与hello.s比较发现,hello.objdump文件中没有“.”开头的伪代码。如.file、.string等等。
hello.s文件中jmp跳转后面跟的是名字,如je .L2,而hello.objdump中跳转后跟的是确切的地址,如jle 36<main+0x36>。
hello.s文件中函数调用时直接call后面跟一个函数的名字,而hello.objdump中调用函数时call后面跟的是具体的地址,如callq 87<main+0x87>。在rela.text节中为其添加重定位条目等待链接。
全局变量访问在hello.s中,访问printf 中的字符串,使用段名称加上寄存器%rip,在反汇编代码中rodata 中数据地址也是在运行时确定,访问需要重定位,0+%rip,在汇编成为机器语言时,会将操作数设置为全 0 并在rel.data节添加重定位条目。
在这里插入图片描述

4.5 本章小结

简单介绍了汇编的概念和作用,分析了ELF文件的内容,比较了重定位前汇编程序hello.o和重定位后反汇编hello,objdump的差别,了解从汇编语言翻译成机器语言的转换处理和机器语言和汇编语言的映射关系。

第5章 链接

5.1 链接的概念与作用

链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可 被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代 码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至 于运行时,也就是由应用程序来执行。链接是由叫做链接器的程序执行的。链接 器使得分离编译成为可能。
链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

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
在这里插入图片描述

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

生成hello.elf文件
在这里插入图片描述
ELF头:
类型从原来的REL(可重定位文件)变为EXEC(可执行文件)
程序入口点地址(.text的起始位置)从原来的0x0变为0x4010f0
节头表的开始(Start of section headers)从原来的1192变为14296
程序头数量(Number of program headers)从原来的0变为12
节头表大小(Size of section headers)从原来的0变为64
节头数量(Number of section headers)从原来的14变为27
字符串表索引节头(Section header string table index)从原来的13变为26
在这里插入图片描述
(3)节头表:
重定位表.rela.text已经不见了(被链接器省略),因为已经重定位过了(.rela.data本来就不存在,因为调用的外部函数并未引用或定义全局变量)
各个节的名称、类型、大小、地址、偏移量等信息如下图所示。
在这里插入图片描述
在这里插入图片描述

5.4 hello的虚拟地址空间

程序头:
程序头表在执行的时候被使用,它告诉链接器运行时加载的内容并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息:
PHDR:保存程序头表
INTERP:指定在程序已经从可执行映射到内存之后,必须调用解释器。在这里解释器并不意味着二进制文件的内存必须由另一个程序解释。它指的是这样的一个程序:通过链接其他库,来满足未解决的引用。
LOAD:表示一个从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串),程序的目标代码等等。
DYNAMIN:保存了由动态链接器使用的信息
NOTE:指定一个段,其中可能包含专用的编译器信息。
GNU_STACK:标志栈是否可执行
GNU_RELRO:指定重定位后需被设置成只读的内存区域
在这里插入图片描述
根据程序头表的信息,打开edb加载hello,查看dump中虚拟地址空间中的各段。
下面主要展示数据段和代码段。
在这里插入图片描述
在这里插入图片描述
其余段不再赘述。

5.5 链接的重定位过程分析

输入命令:objdump -d -r hello > hello.txt
生成hello.txt反汇编文件
对比:
hello.txt与上一章生成的hello.objdump(对hello.o的反汇编)比较。
原hello.o中只有main函数,而hello中多出了很多函数和段。例如:
(1)初始化函数_init:
在这里插入图片描述
程序入口_start:
在这里插入图片描述
(4)函数调用:
got全称Global Offset Table,即全局偏移量表。它在可执行文件中是一个单独的section,位于.data section的前面。每个被目标模块引用的全局符号(函数或者变量)都对应于got中一个8字节的条目。编译器还为got中每个条目生成一个重定位记录。在加载时,动态链接器会重定位got中的每个条目,使得它包含正确的目标地址。
plt全称Procedure Linkage Table,即过程链接表。它在可执行文件中也是一个单独的section,位于.text的前面。每个被可执行程序调用的库函数都有它自己的plt条目。每个条目实际上都是一小段可执行的代码。
在这里插入图片描述
main函数调用puts、printf等外部函数时,会先跳转到.plt.sec中相应的部分,然后再读取并跳转到.got.plt中的相应条目(.got.plt的起始地址为404000)中的绝对地址(由于采用延迟解析,首次调用时并不是绝对地址,需要借助.plt调用动态链接器来赋值给相应的条目绝对地址),每个函数对应的条目在.got.plt中的位置,在重定位节表.rela.plt中可以查看。
在这里插入图片描述
_start调用main函数也同理(.got的起始地址为403ff0)
在这里插入图片描述
在这里插入图片描述
(5)对.rodata中的内容的访问,则是直接跳转到一个绝对地址,即通过绝对寻址访问。例如:
在这里插入图片描述
(6)链接的过程:
①首先链接器要进行符号解析,即将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。对于局部符号很简单(只允许每个模块中每个局部符号有一个定义),静态局部变量会有本地链接器符号(链接器要确保他们拥有唯一的名字)。
而对于全局符号则很麻烦,编译器遇到一个不是在当前模块中定义的符号(变量或函数)时,会生成一个链接器符号表条目,并把它交给链接器处理。若链接器在它输入的任何一个模块中都找不到这个符号的定义,则报错。除此之外,还要考虑多个目标文件可能会定义相同名字的全局符号,linux链接器有如下规则:(强符号指已初始化的全局符号,未初始化的是弱符号)
不允许有多个同名的强符号;
如果有一个强符号和多个弱符号同名,则选择强符号;
如果有多个弱符号同名,则任意选择一个。
在本程序中,main函数引用的只有外部库函数,而链接器则将这些函数所在的静态库做为输入,仅仅复制静态库中被引用的目标模块。而本程序还使用了动态链接共享库,比如本程序中atoi、printf等函数都在libc.so中。_start等函数在ctr1.o、ctri.o和ctrn.o库中。
②接着是重定位,首先链接器将所有相同类型的节合并成一个节,例如来 自不同模块的.data节全部被合并成一个.data节。然后链接器将内存地址赋给 新的合并后的节、输入模块定义的节和输入模块定义的每个符号。这样程序中 的每条指令和全局变量都有一个唯一的内存地址了。
对于重定位节中的符号的引用,链接器修改代码节(.text)和数据节(.data 等)中对每个符号的引用,使得它们指向正确的内存地址。这时需要汇编器在 汇编阶段生成的重定位表.rel.text和.rel.data(包含每个引用的外部定义的全局 符号的重定位条目),最基本的重定位类型有R_X86_64_PC32(相对地址引 用)和R_X86_64_32(绝对地址引用)。
而本程序中,还有一个类型 R_X86_64_PLT32(告知链接器如何修改新的 引用,正常情况本该是R_X86_64_PC32,可现在新版本gcc默认用 PLT 表, 调用函数的方式上面已经讲过)
对于重定位中的相对引用,我们举例说明main函数调用的外部函数puts:

链接器解析重定位条目时发现puts的类型为R_X86_64_PLT32,此时该函 数已经加入到了plt中,.text节与.plt节相对距离已经确定,链接器计算相对距 离,将动态链接库中puts函数的调用值改为plt中相应函数与下条指令的相对 地址,指向对应函数puts。
未重定位时:
在这里插入图片描述
在这里插入图片描述
重定位后:
在这里插入图片描述
在这里插入图片描述
具体过程如下:
已知ADDR(puts@plt) = 0x401090
如何改call后的相对引用呢:PC = ADDRA(main) + offset节偏移 + addend 引用偏移长度,其中,节偏移在汇编时就已确定为0x1e+0x1 = 0x1f,相对引用 偏移(有符号,4字节)长度为4;
则重定位后PC = 0x401125 + 0x1f + 0x4 = 0x401148。
而相对引用偏移应为ADDRA(puts@plt) - PC = 0x401090 - 0x401148 = -0xb8 = 0xffffff48。因此 call puts的二进制指令(16进制形式)从原来的e8 00 00 00 00
变为 e8 48 ff ff ff。
而对与重定位绝对引用,我们再举例.rodata中的只读字符串:
在这里插入图片描述
链接器解析对.rodata中的字符串的重定位类型为R_X86_64_32,.rodata 节与.text节之间的相对距离确定,因此链接器直接修改mov之后的值为目标 地址,指向相应的字符串。
未重定位时:
在这里插入图片描述
重定位后:
在这里插入图片描述
具体过程:
已知ADDR(该字符串) = 0x402080,则直接将mov后的地址从00 00 00 00 改为 08 20 40 00。
而got.plt中的条目的重定位,原理与上述相似。

5.6 hello的执行流程

依次是:
载入:
_init 0x401000
开始:
_start 0x4010f0
__libc_start_main 0x7fbe71ac0bc0
执行:
main 0x401125
puts@plt 0x401030 (puts绝对地址0x7fbe71b18d90)
exit@plt 0x401070 (exit绝对地址0x7fbe71adcbe0)
printf@plt 0x401040 (printf绝对地址0x7fbe71af7d90)
sleep@plt 0x401080 (sleep绝对地址0x7fbe71b767d0)
getchar@plt 0x401050 (getchar绝对地址0x7fbe71b1fe50)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

5.7 Hello的动态链接分析

在进行动态链接前,首先进行静态链接,生成部分链接的可执行目标文件hello。此时共享库中的代码和数据没有被合并到hello中。加载hello时,动态链接器对共享目标文件中的相应模块内的代码和数据进行重定位,加载共享库,生成完全链接的可执行目标文件。 动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用偏移量表GOT+过程链接表PLT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。
调用_init之前,got.plt中的条目:所有调用函数的条目都不是绝对地址。got[2]中还未存放地址。
在这里插入图片描述
调用_init之后:got[0]和got[1]条目存放动态链接器在解析函数地址时会使用的信息,而此时got[2]存放了动态链接器的入口。
在这里插入图片描述

在之后的调用函数过程中,会依次更新got中条目的内容。
例如首次调用printf函数后,got[4]中的内容发生变化,其中的内容指向printf的地址。
在这里插入图片描述

5.8 本章小结

简单介绍了链接的概念和作用,分析了hello程序的虚拟地址空间、重定位和执行过程。简述了动态链接的原理。

第6章 hello进程管理

6.1 进程的概念与作用

进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需要的状态组成。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

Shell 的作用:
Shell 是一个用 C 语言编写的程序,他是用户使用 Linux 的桥梁。Shell 是指一种应用程序,Shell 应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
处理流程:
(1)从终端读入用户输入的命令
(2)理解命令(字符串拆分)
(3)如果是内置命令则立即执行相应命令
(4)否则调用相应的程序为其分配子进程并运行
(5)shell 应该接受键盘输入信号(例如ctrl+c),并对这些信号进行相应处理

6.3 Hello的fork进程创建过程

在终端中输入命令行:./hello 1190202419李洲 1
在这里插入图片描述
shell判断出该命令不是内置调用,shell会调用fork函数创建一个新的子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这意味着子进程可以读写父进程中打开的任何文件。父进程和子进程最大的区别是它们有不同的PID。
fork():子进程返回0,父进程返回子进程的PID,如果出错则返回-1。
fork函数调用一次返回两次,且父进程和子进程是并发运行的独立进程。

6.4 Hello的execve过程

在新创建的子进程下,execve函数加载并运行程序hello,且带有参数列表argv和环境变量列表envp。只有出错时execve才会返回到调用程序,因此execve函数调用一次并从不返回。
execve加载hello后,调用启动代码,启动代码设置栈,并将控制传递给新程序的主函数:
int main(int argc, char *argv[], char *envp[]);当main函数开始执行时,用户栈的组织结构如下图所示。
在这里插入图片描述

6.5 Hello的进程执行

即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。一系列程序计数器PC的值的序列称为逻辑控制流,每个进程的执行过程都是一个逻辑流,每个进程都是在轮流使用处理器,一个进程执行它的流的一部分后,就会暂时挂起,让别的进程继续执行。
一个逻辑流的执行在时间上与另一个流重叠称为并发流,多个流并发执行的现象称为并发,一个进程执行它的控制流的一部分的每一时间段叫做时间片。
运行应用程序代码的进程初始时是在用户模式(不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据)中的。进程从用户模式变为内核模式(可以执行指令集中的任何指令,并可以访问系统中的任何内存位置)的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当发生异常时,处理器将模式从用户模式变为内核模式。异常处理程序运行在内核模式中,当它返回到应用程序时,处理器就把模式再变回用户模式。
不同进程之间交替运行靠的是称为上下文切换的异常控制流,内核保存当前进程的上下文,恢复某个先前被抢占的进程被保存的上下文,将控制传递给这个新恢复的进程。例如下图所示,进程A和B之间的上下文切换过程。
在这里插入图片描述

6.6 hello的异常与信号处理

异常就是控制流中的突变,用来响应处理器状态中的某些变化。状态变化被称为事件,事件可能与当前指令的执行直接相关。比如,发生虚拟内存缺页、算术溢出,或者一条指令试图除零。另一方面,事件也可能和当前指令的执行没有关系,比如,一个系统定时器产生的信号或者一个I/O请求完成。
在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。
当异常处理程序完成处理后,根据引起异常的事件的类型来决定是否返回,或返回到哪一条指令。共有三种情况:
(1)处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的指令;
(2)处理程序将控制返回给Inext,如果没有发生异常将会执行的下一条指令;
(3)处理程序终止被中断的程序。
在这里插入图片描述
hello执行过程中可能会出现四类异常:中断、陷阱、故障和终止。
(1)中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
(2)陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
(3)故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
(4)终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
可以产生两种信号:SIGINT和SIGTSTP
(1)从键盘键入ctrl+c,导致内核发送一个SIGINT信号到前台进程组种的每个进程,默认结果是终止前台作业。
在这里插入图片描述
(2)从键盘键入ctrl+z,会发送一个SIGTSTP信号到前台进程组中的每个进程,默认结果是停止(挂起)前台作业。
输入ps命令发现hello程序依然存在。输入jobs命令发现hello程序已停止
在这里插入图片描述
输入fg命令使hello程序继续执行。最终输入回车,程序结束,进程被回收。
在这里插入图片描述
(3)在进程执行过程中乱按键盘,并打回车,当输出8行”hello…”之后,程序结束,进程被回收。
在这里插入图片描述

6.7本章小结

简单介绍了进程的概念和作用、shell-bash的处理过程与作用,介绍了fork和execve进程以及hello进程的执行过程中的信号异常处理过程。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:CPU所生成的地址。逻辑地址是内部和编程使用的、并不唯一。例如,在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址(偏移地址),不和绝对物理地址相干。
逻辑地址的格式在实模式下为“段地址:偏移地址”,在保护模式下为“段选择符:偏移量”。
物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。在前端总线上传输的内存地址都是物理内存地址,编号从0开始一直到可用物理内存的最高端。物理地址是明确的、最终用在总线上的编号,不必转换,不必分页,也没有特权级检查。CPU访问内存的最自然的方式就是使用物理地址。
线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。
虚拟地址:保护模式下程序访问存储器所用的逻辑地址,格式为“段选择符:偏移量”。

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

段式管理的概念:
段式存储管理中以段为单位分配内存,每段分配一个连续的内存区,但各段之间不要求连续,内存的分配和回收类似于动态分区分配,由于段式存储管理系统中作业的地址空间是二维的,因此地址结构包括两个部分:段号和段内位移。
段选择符:
在这里插入图片描述
其中:
TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)
RPL=00,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态。
高13位-8k个索引用来确定当前使用的段描述符在描述符表中的位置。
段描述符:
在这里插入图片描述
逻辑地址(48位,“段选择符(16):偏移量(32)”)到线性地址的变换:
(1)首先确定要访问的段,根据逻辑地址中的段选择符(高16位)中的低3位(TI和RPL),选择相应的描述符表(GDT或LDT)和状态,然后决定使用的段寄存器。
(2)将段选择符中的索引值(高13位)乘8,然后加上GDT或LDT的首地址,就能得到对应段描述符在描述符表中的地址,并在GDT或LDT表中定位相应的段描述符,(GDT或LDT首地址则通过用户不可见的GDTR寄存器或LDTR寄存器来获得)。
(3)利用段描述符校验段的访问权限和范围,以确保该段是可以访问的并且偏移量位于段界限内。
(4)利用段描述符中取得的段基地址(32位)加上逻辑地址中的偏移量(低32位),得到一个32位线性地址。
在这里插入图片描述

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

页式管理概念:
VM系统将虚拟内存分割成若干个大小固定的虚拟页(VP),把物理内存按同样大小分割成物理页(PP),然后把页式虚拟页与物理页建立一一对应页表,并用相应的地址翻译硬件,来将虚拟地址翻译为物理地址。
页表:
在这里插入图片描述
页表是一个页表条目 (Page Table Entry, PTE)的数组,将虚拟页地址映射到物理页地址。 每个条目由一个有效位和一个n位地址字段组成,有效位表示该虚拟页是否被缓存在DRAM中。若设置了有效位,则地址字段表示DRAM中相应的物理页的起始位置。若没有设置有效位,地址字段为空表示这个虚拟页还未分配,否则地址字段指向该虚拟页在磁盘上的起始位置。
CPU想要读取虚拟内存中的某个虚拟页上的某个内容时,会出现页命中和缺页异常,处理缺页异常需要调用内核的缺页异常处理程序。另外还有分配页面操作,具体细节这里不再赘述。
地址翻译:
在这里插入图片描述
(1)首先利用虚拟地址的虚拟页号(VPN)找到页表中对应的条目。
(2)然后判断是否设置了有效位,若未设置有效位,则缺页异常,执行异常处理程序,之后再此执行到第(2)步,若已设置有效位,则直接进入第(3)步。
(3)将该条目中的物理页号(PPN)和虚拟页偏移量(VPO)分别做高低位组合成物理地址。
在这里插入图片描述

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

TLB加速地址翻译:
TLB是一个小的、虚拟寻址的缓存,每一行都保存着一个由单一的PTE组成的块。MMU直接利用VPN从TLB中取出相应的PTE。
在这里插入图片描述

多级页表:
使用层次结构的页表,为了节约内存。例如一个二级页表,其中一级页表中每个PTE指向一个页表,二级页表中每个PTE指向一页。若一级页表中某个PTE是空的,那么相应的二级页表就根本不会存在,这样节省了内存空间。
在这里插入图片描述
TLB与四级页表结合下的地址翻译:
(1)首先,CPU将虚拟地址VA传递给MMU,MMU从TLB中取出相应的PTE,若TLB未命中,则MMU从访问内存中四级页表并取出PTE(若缺页则进行缺页异常处理),新取出的PTE存放到TLB中。若命中则直接进行下一步。
(2)将PTE中的PPN与VPO结合成PA。之后就是利用PA来访问缓存了。
在这里插入图片描述

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

内存访问:根据缓存的S、E、B、m的值来划分物理地址。CO的位数b对应块大小B = 2^b。CI的位数s对应组数S = 2^s。其余位CT的位数等于m-s-b。
在这里插入图片描述
首先利用CI来选取相应的组,接着在选中的组中寻找标记位与CT相同的一行,然后根据是否设置有效位来判断是否命中(不命中则从下一级缓存中取出该行并替换),接着根据CO中的块偏移,取出块中相应位置的内容。

7.6 hello进程fork时的内存映射

运行hello程序时,shell会调用fork为hello创建一个新进程。首先,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程(shell进程)的mm_struct、区域结构和页表的原样副本。它将两个进程(新进程和shell进程)中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
在这里插入图片描述
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新的页面,因此,也就为每个进程保持了私有地址空间的概念。
在这里插入图片描述

7.7 hello进程execve时的内存映射

execve函数加载并运行hello程序需要以下几个步骤:
(1)删除已存在的用户区域:删除当前进程虚拟地址的用户部分中已存在的区域结构。(即清除这个进程曾经运行过的程序遗留下来的痕迹,为新程序初始化区域)
(2)映射私有区域:为新程序的代码、数据、bss和栈区创建新的区域结构。所有的这些区域都是私有的,写时复制的。代码和数据区被映射为hello文件中的.text和.data区。bss区是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆也是请求二进制零的,初始长度为0。
(3)映射共享区域:如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC):execve做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
在这里插入图片描述

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

MMU在试图翻译虚拟地址时。触发了一个缺页故障。(这里按照课本第八章的定义,故障处理后可能会返回当前指令,因此缺页触发的应该是故障。)这个异常导致控制转移到内核的缺页处理程序,处理程序执行以下步骤:
(1)首先判断虚拟地址是否合法,缺页处理程序搜索区域结构的链表,把虚拟地址和每个区域结构中的vm_start和vm_end做比较。如果不合法,那么缺页处理程序就触发一个段错误,从而终止这个进程。对应于下图中的标号①。
(2)接着判断内存访问是否合法,即进程是否有读、写或者执行这个区域内页面的权限。例如,试图对一个只读页面进行写操作,或者一个运行在用户模式中的进程试图从内核虚拟内存中读取字,等等。如果访问不合法,那么缺页处理程序会触发一个保护异常,从而终止这个进程。对应于下图中的标号②。
(3)处理程序知道这个缺页是对合法的虚拟地址进行合法的操作造成的,即正常缺页。那么处理程序会选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表(如果使用TLB,那么当然也要更新TLB缓存)。处理程序返回到引起故障的指令,CPU再次执行这个指令,这次MMU可以正常翻译虚拟地址了,不会产生缺页故障了。对应于下图中的标号③。
在这里插入图片描述

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为两种基本风格:显式分配器、隐式分配器。
(1)显式分配器:要求应用显式地释放任何已分配的块。
(2)隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
堆中的块主要组织为两种形式:
(1)隐式空闲链表(带边界标记):在块的首尾的四个字节分别添加header和footer,负责维护当前块的信息(大小和是否分配)。由于每个块是对齐的,所以每个块的地址低位总是0,可以用该位标注当前块是否已经分配。可以利用header和footer中存放的块大小寻找当前块两侧的邻接块,方便进行空闲块的合并操作。
所谓隐式空闲链表,对比于显式空闲链表,代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表,其中header和footer中的block大小间接起到了前驱、后继指针的作用。(下图左是正常的块的结构,右是带边界标记的块的结构)。
在这里插入图片描述在这里插入图片描述
隐式空闲链表的放置策略常见的有首次适配、下一次适配和最佳适配。首次适配每次都要从头开始搜索链表速度较慢且容易产生碎片,而下一次适配相较于首次适配速度更快但内存利用率低得多,最佳适配内存利用率比前两个都高但速度最慢。
(2)显式空闲链表:在未分配的块中添加两个指针,分别指向前一个空闲块和后一个空闲块。采用该策略,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。显示空闲链表将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针,如下图:
在这里插入图片描述
维护链表的顺序有:后进先出(LIFO)和地址顺序法。
后进先出(LIFO):将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。
地址顺序法:按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。

7.10本章小结

本章简单介绍了在计算机中的虚拟内存管理,虚拟地址、物理地址、线性地址、逻辑地址的区别以及它们之间的变换模式,以及段式、页式的管理模式,在了解了内存映射的基础上重新认识了共享对象、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) Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发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)int close(int fd),fd是需要关闭的文件的描述符,close返回操作结果。
(3)ssize_t read(int fd, void *buf, size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
(4)ssize_t wirte(int fd, const void *buf, size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。若成功则返回写的字节数,出错则返回-1。
(5)int stat(const char *filename, struct stat *buf),stat函数以一个文件名作为输入,填写stat数据结构中的各个成员。若成功则返回0,出错则返回-1。

8.3 printf的实现分析

printf函数的函数体:
在这里插入图片描述
(1)printf函数形参里有一个”…”:这是可变形参的一种写法,当传参的个数不确定时,就用这种写法。
(2)va_list是一个字符指针类型。
在这里插入图片描述
fmt是一个指针,指向第一个const参数(const char *fmt)中的第一个元素,fmt也是变量,它的位置,是在栈上分配的,它有地址为&fmt。因为C语言中参数压栈是最右边的参数先压,从右向左压,且栈是从高地址向低地址增长的,所以参数const char *fmt是在栈顶的,而紧挨着它的是”…”中的第一个参数(从左往右),它在栈顶+4的位置(指针fmt的大小为 4字节),因而arg指的是”…”中的第一个参数的地址。
(3)接下来是,vsprintf返回的是一个长度,即要打印出的字符串的长度。vsprintf的功能就是格式化,它接受确定输出格式的格式化字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。在这里插入图片描述

(4)之后就是write(buf, i)了,打印操作:
write函数:
在这里插入图片描述
其中int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call函数。
(5)再来看sys_call函数:ecx是要打印出的元素个数,ebx是要打印的buf字符数组中的第一个元素。这个函数的功能就是将字符串中的字节(例如hello中要输出的字符串“Hello 1190202419 李洲”)从寄存器中通过总线复制到显卡的显存中,显存中储存的是字符的ASCII码。
在这里插入图片描述

(6)接着,字符显示驱动子程序将通过ASCII码在字模库中找到相应的点阵信息并存储到vram(存储每一个点的RGB颜色信息)中。
(7)显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),这样一个字符串就打印到屏幕上了。

8.4 getchar的实现分析

getchar函数:
在这里插入图片描述
调用getchar函数,读取键盘缓冲区中的一个字符(读取上一次被读取的字符的下一个字符),若没有字符可读,则等待用户键入并回车后再执行下一步(回车也算一个字符,因而getchar也会读缓冲区里的回车)。
异步异常-键盘中断的处理:read函数同样通过sys_call中断来调用内核中的系统函数。键盘中断处理子程序会接受按键扫描码并将其转换为ASCII码后保存在缓冲区。然后read函数调用的系统函数可以对缓冲区ASCII码进行读取,直到接受回车键返回。

8.5本章小结

本章简单介绍了Unix是如何将I/O设备模型化文件并统一处理的,同时详细介绍了Unix I/O函数,并对标准I/O函数printf和getchar的实现进行了分析:标准I/O函数的实现都调用了系统的I/O函数,通过中断指令将程序控制权交给系统内核,进行相应的中断处理,然后对硬件进行相应操作。

结论

用计算机系统的语言,逐条总结hello所经历的过程。
(1)hello的出生:编写好的hello.c文件。
(2)预处理:删除注释,插入库函数,成为hello.i。
(3)编译:编译器对hello.i进行词法、语法、语义分析,编写成对应的汇编文本,成为hello.s。
(4)汇编:汇编器将hello.s翻译成机器语言指令(二进制),并把这些指令打包成叫做可重定位目标程序的格式,保存在hello.o中。
(5)链接:hello.o通过链接器与其他必要的可重定位目标文件和共享库链接,合并成为可执行文件hello。
(6)创建进程:hello的运行需要一个进程做为载体,shell调用fork来为hello创建新进程,之后在这个进程内调用execve函数来运行hello。
(7)内存管理:系统将hello载入内存。通过将虚拟地址提交给MMU翻译成物理地址之后访问缓存或主存。printf会调用malloc向动态内存分配器申请堆中的内存。
(8)运行:内核通过上下文切换调度hello所在进程,为hello分配时间片。
(9)异常:当hello运行出错时,内核会调用异常处理程序来解决问题。
(10)信号:hello所处进程接受到信号要做出相应的反应。
(11)结束:hello程序结束后(若是输出提示字符则直接结束,否则还需用户键入回车或相关信号),shell调用waitpid函数回收hello所处进程,内核释放内存,删除为hello创建的所有数据结构,hello消失了,没有留下一点痕迹。
然而表面上看起来简单的hello程序,却有着复杂精密的一生;人生也如此,人人追求简单,而简单生活的麻烦在于,它是快乐的,丰富的,有创意的,却一点也不简单。

附件

列出所有的中间产物的文件名,并予以说明其作用。
hello.i 预处理之后的文本文件
hello.s 编译之后的汇编文本文件
hello.o 汇编之后的可重定位目标文件
hello 链接后的可执行目标文件
hello.elf hello的elf输出
hello.objdump hello.o的反汇编代码
hello.text hello的反汇编代码

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1] 袁春风 计算机系统基础 (第二版)[M].北京:机械工业出版社,2018

[2] 麦峰强的博客CSDN

https://blog.csdn.net/weixin_36139431/article/details/89439366

[3] Randal E. Bryant, David R. O’Hallaron.深入理解计算机系统(原书第3版) [M]. 龚奕利,雷迎春译北京:机械工业出版社,2016

[4] 虚 拟 地 址 、 逻 辑 地 址 、 线 性 地 址 、 物 理 地 址

https://blog.csdn.net/rabbit_in_android/article/details/49976101

[5] printf 函 数 实 现 的 深 入 剖 析 :

https://blog.csdn.net/zhengqiju

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值