HIT【2024春】csapp大作业——程序人生-Hello’s P2P

打印 上一主题 下一主题

主题 533|帖子 533|积分 1599


计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 网络空间安全
学 号 2022111009
班 级 2203901
学 生 周天宇
指 导 教 师 史先俊
计算机科学与技能学院
2024年5月
摘 要
本文探讨了一个简单的 C 语言程序 hello.c 的整个生命周期。从最初的源代码编写开始,履历了编译、汇编和链接的过程,将 .c 文件转化为可执行文件。文章还深入解释了程序在运行时系统中的状态,包括内存分配、进程创建以及代码执行的过程,全面展示了程序从诞生到执行的精华。
**关键词:**P2P;020;I/O管理。
**
**
目 录
第1章 概述 - 4 -
1.1 Hello简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 5 -
第2章 预处置惩罚 - 6 -
2.1 预处置惩罚的概念与作用 - 6 -
2.2在Ubuntu下预处置惩罚的命令 - 6 -
2.3 Hello的预处置惩罚结果解析 - 6 -
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进程创建过程 - 28 -
6.4 Hello的execve过程 - 29 -
6.5 Hello的进程执行 - 29 -
6.6 hello的异常与信号处置惩罚 - 30 -
6.7本章小结 - 33 -
第7章 hello的存储管理 - 34 -
7.1 hello的存储器所在空间 - 34 -
7.2 Intel逻辑所在到线性所在的变换-段式管理 - 34 -
7.3 Hello的线性所在到物理所在的变换-页式管理 - 35 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 35 -
7.5 三级Cache支持下的物理内存访问 - 36 -
7.6 hello进程fork时的内存映射 - 36 -
7.7 hello进程execve时的内存映射 - 37 -
7.8 缺页故障与缺页停止处置惩罚 - 37 -
7.9动态存储分配管理 - 37 -
7.10本章小结 - 39 -
第8章 hello的IO管理 - 40 -
8.1 Linux的IO设备管理方法 - 40 -
8.2 简述Unix IO接口及其函数 - 40 -
8.3 printf的实现分析 - 41 -
8.4 getchar的实现分析 - 43 -
8.5本章小结 - 44 -
结论 - 45 -
附件 - 46 -
参考文献 - 47 -
第1章 概述

1.1 Hello简介

P2P
编译器的驱动程序负责将源文件转换为目标文件。你已经利用高级语言编写了一个名为hello.c的文件。GCC编译器驱动程序接下来读取这个hello.c源文件,并将其转换为一个名为hello的可执行目标文件。这个编译过程可以分为四个主要阶段。首先,预处置惩罚器(cpp)根据以字符#开头的命令修改原始的C程序,生成一个名为hello.i的中间文件。接着,编译器(ccl)将hello.i翻译成汇编语言程序的文本文件hello.s。然后,汇编器(as)将hello.s翻译成机器语言指令,并将结果生存在目标文件hello.o中,这个目标文件是可重定位的。末了,由于hello程序调用了尺度C库的printf函数,因此必要利用链接器(ld)将包罗printf函数定义的printf.o文件与hello.o程序归并,最终得到名为hello的可执行文件。在Linux系统中,通过内置的命令行解释器shell加载运行hello程序。hello程序会fork一个新的进程,完成了hello.c中定义的进程间通信(P2P)过程。
020
在shell中,fork产生了一个子进程,通过execve加载了hello。首先,它清除了当前虚拟所在中已存在的用户部门数据结构,然后为hello的代码段、数据、bss以及栈地域创建了新的地域结构。接着,举行虚拟内存映射,并设置程序计数器,将其指向代码地域的入口点,从而进入程序入口。一旦程序开始载入物理内存并进入main函数,CPU就为hello分配时间片并执行其逻辑控制流。hello利用Unix I/O管理来控制输出。执行完成后,shell的父进程将回收hello进程,并且内核会从系统中清除hello的所有痕迹。至此,hello完成了O2O的过程。
1.2 环境与工具

硬件环境:CPU:12th Intel Core i5 12500H
软件环境:Windows 11, VMware,Ubuntu-22.04.3-desktop-amd64
工具:gcc, gdb,edb,vscode,readelf
1.3 中间结果


图1-1 初始文件及中间结果
1.4 本章小结

本章主要先容了hello的P2P,020过程,并列出了本次实验信息、环境、中间结果,和生成的一些中间文件。
第2章 预处置惩罚

2.1 预处置惩罚的概念与作用

概念:预处置惩罚器(cpp)在编译器处置惩罚源代码之前对其举行处置惩罚,即根据以字符#开头的命令(主要包括文件包罗、宏定义和替换、条件编译等特定操纵),修改原始的c程序。
作用:提高代码的可读性、可维护性和编译服从。
2.2在Ubuntu下预处置惩罚的命令

gcc -E hello.c -o hello.i :

图2-1 预处置惩罚命令
2.3 Hello的预处置惩罚结果解析

查看预处置惩罚文件内容:

图2-2 hello.i文件
有以下变化:
1)文件长度行数增加了,这是由于头部的3个包罗头文件stdio.h、unist.h和stdlib.h都被替换成了完整的内容。
2)注释内容被删除。
3)每一行’#’后面的数字表现行号, 表现编译器原始代码的位置,便于调试和错误报告。
别的,预处置惩罚器会将所有宏定义替换为其定义的内容,而此程序没有宏定义。
2.4 本章小结

先容并展示了linux的预处置惩罚命令,查看了预处置惩罚文件并比较其与源文件的差异。
第3章 编译

3.1 编译的概念与作用

概念:编译器(cc1)将高级编程语言(颠末预处置惩罚后的C语言代码)转换为汇编语言代码的过程。该程序包罗函数 main 的定义。
作用:将高级语言的抽象表达(如数据类型和函数调用)翻译为低级汇编语言表现,使其更接近机器语言。同时编译器可以对中间代码举行各种优化,提高生成的汇编代码的运行服从。
3.2 在Ubuntu下编译的命令

gcc -S hello.c -o hello.s

图3-1 编译命令
3.3 Hello的编译结果解析

编译后的.s文件:


图3-2 hello.s文件
3.3.1数据
1)常量
数字常量:源代码中利用的数字常量一般储存在.text段中,而该程序的 if语句比较时利用的数字变量5是作为立刻数直接嵌入在cmpl指令中的。

图3-3 数字常量存储
字符串常量:在 printf 等函数中利用的字符串常量存储在 .rodata 段中。

图3-4 字符串常量
2)变量
局部变量:可以发现局部变量是储存在栈中的某一个位置的或是做为立刻数储存在寄存器中的。我们可以对源代码中的每一个局部变量逐一分析。源程序中的局部变量共有三个,一个是循环变量i,以及int型argc和数组argv[],
1)对于int型i,我们发现它储存在栈中所在为-4(%rbp)的位置,对于i的操纵如下:

图3-5 局部变量i的存储
2)局部变量argc代表程序运行时输入的变量的个数,可以发现它储存在栈中所在为-20(%rbp)的位置,对于它的操纵主要是与立刻数5比较之后确定有一部门代码是否执行,如果等于5,则跳转至.L2。具体汇编代码如下:

图3-6 局部变量argc的存储
3)对于局部变量argv[],它是一个生存着输入变量的数组,观察发现它储存在栈中,具体汇编代码段如下:

图3-7 局部变量argv[]的存储
3.3.2赋值
变量赋值的操纵在程序中出现了两次,一次是对于argv[] 元素的赋值,一次是循环变量i的在循环中的赋值,我们分别举行分析。
1)对于数组argv[]元素的赋值,正如图3-7所示,movq -32(%rbp), %rax表现从栈帧中的偏移量 -32 处读取一个 64 位的值,并存入寄存器 rax。addq $24, %rax表现将寄存器 rax 的值增加 24,这相当于 argv[3] 的所在。在这之后rax的值加16、加8,分别代表argv[2]、argv[1]的所在。比较特别的是,将 rax 的值增加 32,相当于 argv[4] 的所在,然后从 rax 所指向的内存所在(即 argv[4] 的所在)读取 64 位值,存入寄存器 rdi,作为函数的参数。

图3-8 argv[4]的赋值
2)对于局部变量i,每次循环结束的时间都对齐举行+1操纵,具体的操纵汇编代码如下:

图3-9 i的赋值及更新
3.3.3算数操纵
此程序中只有循环变量i在每一轮的循环中举行加1操纵,对于这个局部变量的算术操纵的汇编代码如下:

图3-10 i的加1操纵
3.3.4类型转换
函数atoi将输入的字符串转换为int型作为sleep函数的输入:

图3-11 string转换为int
3.3.5关系操纵
源代码中一共出现了两处关系操纵:

  • if语句中5和变量argc的比较,对应的汇编代码如下:

图3-12 比较操纵1

  • for循环中循环变量i和9的比较,当循环变量i大于等于9的时间将举行条件跳转:

图3-13 比较操纵2
3.3.6结构操纵
此程序中出现的结构操纵只有数组操纵,argv数组的值均被存储在栈中,利用寄存器rax作为数组的索引。

图3-14 数组寻址
3.3.7控制转移
1)if语句有条件转移,argc不是5则跳转,是5则执行if中的语句。

图3-15 if语句
2)for循环也有条件转移,当循环变量i大于等于9的时间将举行条件跳转,如图3-7。
3.3.8函数
1)main函数:
参数:argc和argv[],此中argc储存在%rdi中,argv[]储存在栈中。
返回值:源代码中返回语句是return 0,因此在汇编代码中末了是将%eax设置为0并返回这一寄存器。

图3-16 main函数
2)printf函数:
参数:第一次调用无传参;for循环中调用的时间传入了argv[1]、argc[2]和argc[3]的所在。
调用:第一次是满足if条件时调用,第二次是在for循环条件满足时调用。

图3-17 printf函数
3)atoi函数:
参数:argc[4]
返回值:整型

图3-18 atoi函数
4)sleep函数:
参数:转换成int型的argc[4]
返回值:无

图3-19 sleep函数
3.4 本章小结

本章主要先容了在将修改后的源程序文件转换为汇编程序的过程中,主要发生的变化以及汇编代码文件中的主要构成部门。还探讨了源代码中的一些主要操纵在汇编代码中的表现形式。总的来说,编译器在举行词法分析和语法分析之后,确认源代码符合语法要求,然后将其转换为汇编代码。
第4章 汇编

4.1 汇编的概念与作用

概念:汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果生存在目标文件 hello.o 中,该文件是二进制文件。
作用:将汇编代码根据特定的转换规则转换为二进制代码,也就是机器代码,机器代码也是计算机真正能够明确的代码格式。
4.2 在Ubuntu下汇编的命令

gcc –c hello.s –o hello.o

图4-1 hello.o文件
4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
利用readelf命令,将elf结果生存到elf.txt中:

图4-2 生成elf格式
1)ELF头
ELF头以一个16字节的序列开始,这个序列描述了文件生成系统的字大小及其他相关信息。ELF头的其余部门包罗资助链接器语法分析和解释目标文件的各种信息,包括:ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中每个条目标大小和数量。具体ELF头的代码如下:

图4-3 ELF头
2)节头表:描述了.o文件中每一个节出现的位置,大小,目标文件中的每一个节都有一个固定大小的条目。

图4-4节头表
3)重定位节
重定位节包罗了在代码中利用的一些外部变量等信息,在链接过程中,必要根据重定位节的信息对某些变量符号举行修改。链接器会根据重定位节的信息,决定如何计算外部变量符号的正确所在,比方通过偏移量等信息举行计算。
本程序必要重定位的信息有:.rodata中的模式串,puts,exit,printf,atoi, sleep,getchar。

图4-5重定位节
4)符号表
.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。比方本程序中的getchar、puts、atoi等函数名都必要在这一部门表现,具体信息如下图所示:

图4-6符号表
4.4 Hello.o的结果解析

利用objdump -d -r hello.o > fan_hello.s反汇编,并生存在fan_hello.s中

图4-7生成反汇编文件

图4-8反汇编文件
与第3章的 hello.s举行对照分析,得出以下几处不同:
1)进制不同:hello.s的数字用十进制表现,而hello.o反汇编之后数字的表现是十六进制的。


图4-9 (a)反汇编代码 图4-9 (b)汇编代码
2)条件跳转格式不同:hello.s中给出的是段的名字,比方.L2等来表现跳转的所在,而hello.o的反汇编代码由于已经是可重定位文件,对于每一行都已经分配了相应的所在,因此跳转命令后跟着的是必要跳转部门的目标所在。
3)全局变量访问不同:在 hello.s 中,直接通过段名称加 %rip 寄存器访问 .rodata 段。然而在 hello.asm 中,初始阶段是不知道 .rodata 段的数据所在的,以是只能先写成 0(%rip) 举行访问。而在重定位和链接后,链接器会更新确定的所在以正确访问 .rodata 段中的数据。
4.5 本章小结

本章对汇编过程举行了一个简明而完整的叙述。汇编器处置惩罚后,生成了一个可重定位文件,为下一步的链接做好了预备。通过与 hello.s 的反汇编代码比较,更深入地明确了汇编过程中发生的变化,这些变化都是为链接阶段做预备的。
第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-1 链接
5.3 可执行目标文件hello的格式


图5-2 ELF可执行目标文件的格式
1)ELF头

图5-3 ELF头
2)节头: 描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的雷同段归并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的所在。

图5-4 节头表
3)程序头部表
参数寄义:
Offset:目标文件中的偏移;
VirtAttr:内存所在;
FileSiz:目标文件中段的大小;
MemSiz:内存中的段大小
Flags:运行时访问权限

图5-5 头部表
5.4 hello的虚拟所在空间

利用edb加载hello可执行文件,在Data Dump窗口可以看到虚拟所在空间分配情况:

图5-6 edb的Data Dump窗口
发现该程序是从所在0x401000开始的,并且该处有ELF的标识。可以判断这是从可执行文件加载的信息。
接下来分析.elf中程序头的部门:此中PHDR生存的是程序头表;INTERP生存了程序执行前必要调用的解释器;LOAD记载程序目标代码和常量信息;DYNAMIC储存了动态链接器所利用的信息;NOTE记载的是一些辅助信息;GNU_EH_FRAME生存异常信息;GNU_STACK利用系统栈所必要的权限信息;GNU_RELRO生存在重定位之后只读信息的位置。
5.5 链接的重定位过程分析

运行命令:objdump -d -r hello > objdump_hello.s,查看反汇编代码:

图5-7 objdump的反汇编
链接的重定位过程主要包括两个步调:符号解析和重定位。
符号解析:链接器网络符号定义和引用(每个目标文件都包罗符号表),然后将每个符号引用与相应的符号定义匹配起来。如果找到多个定义,链接器会报错(符号冲突)。
重定位:链接器将所有目标文件的代码和数据段分配到最终的内存所在空间中,然后修改目标文件中的代码和数据,使得所有符号引用都指向其最终的内存所在。
hello与hello.o的不同:
在链接过程中,hello中加入了代码中调用的一些库函数,如getchar、sleep等,同时每一个函数都有了相应的虚拟所在。比方函数sleep、getchar都有明确的虚拟所在。

图5-8 sleep、getchar函数虚拟所在展示
5.6 hello的执行流程


图5-9 hello程序执行时函数的所在
比方,下图是main函数调用puts函数的过程:

图5-10 main函数调用puts函数
5.7 Hello的动态链接分析

当程序调用由共享库定义的函数时,编译器无法预先确定该函数的所在。为了解决这个问题,编译系统提供了延迟绑定的方法,将函数所在的绑定推迟到第一次调用该函数时完成。这个过程通过全局偏移表(GOT)和过程链接表(PLT)的协作来实现。在加载时,动态链接器会重定位GOT中的每个条目,使其包罗正确的绝对所在,而PLT中的每个函数负责跳转到不同的共享库函数。通过观察edb,可以发现执行dl_init之后.got.plt节的变化。
先观察hello.elf中.got.plt节的所在,为0x404000:

图5-11 got.plt的所在
执行init前:

图5-12执行init前 got.plt的所在
执行init后:

图5-13执行init后 got.plt的所在
5.8 本章小结

在链接过程中,各种代码和数据片断被网络并组合成一个单一的可执行文件。通过利用链接器,实现了分离编译,使得我们不必将整个应用程序组织成一个巨大的源文件,而是可以将其分解为多个可独立管理的模块。这样,在必要时只需将这些模块链接在一起即可完成整个应用程序的构建。颠末链接后,我们已经生成了一个可执行文件,接下来只需在shell中运行相应的命令,即可为该文件创建进程并执行它。
第6章 hello进程管理

6.1 进程的概念与作用

概念:进程是计算机中正在运行的一个程序的实例。它是一个动态的实体,包罗了程序代码以及程序执行时的当前活动,包括程序计数器、寄存器内容和变
量。进程是操纵系统举行资源分配和任务调度的基本单位。
作用:
1)在资源管理上,每个进程都有自己的所在空间、内存、文件描述符等资源。操纵系统通过进程管理来分配和回收这些资源,确保多个程序可以在系统中有效运行。
2)通过进程,操纵系统可以实现并发执行,即多个进程可以同时在系统中运行。这利用了多核处置惩罚器的优势,提高了系统的整体服从和性能。
6.2 简述壳Shell-bash的作用与处置惩罚流程

Shell是用户与操纵系统之间的接口,它允许用户输入命令来控制操纵系统的举动。Bash Shell处置惩罚用户命令的一般流程如下:
1)显示提示符:Shell显示提示符,等待用户输入命令。通常是$或#。
2)读取命令:用户输入命令后,Shell读取整行输入。这一步通常利用read系统调用。
3)解析命令:Shell将用户输入的命令解析成单独的词(tokens),如命令名称、选项、参数等。它会考虑引号、转义字符和管道符号。
4)命令睁开:路径名睁开:将文件名模式(如*.txt)睁开为匹配的文件名。变量替换:将变量(如$HOME)替换为其值。命令替换:将反引号括起来的命令(如`date`)替换为其输出。算术扩展:计算包罗在$(( ))中的算术表达式。
5)执行前处置惩罚:
管道处置惩罚:识别并设置管道(|),将一个命令的输出作为下一个命令的输入。重定向:处置惩罚输入输出重定向(>、<、>>等),调解文件描述符。背景执行:识别背景执行符号(&),将命令放入背景执行。
6)查找命令:
内建命令:检查是否是内建命令(如cd、echo)。外部命令:在系统的PATH环境变量指定的目次中查找可执行文件。
7)创建进程:利用fork系统调用创建子进程。如果是外部命令,子进程调用exec系列系统调用加载并执行程序。
8)等待命令完成:如果是前台进程,Shell等待子进程完成,返回其退出状态。如果是背景进程,Shell不等待其完成。
9)返回提示符:命令执行完毕后,Shell显示新的提示符,等待下一次用户输入。

图6-1shell-bash的处置惩罚流程
6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个与调用进程几乎完全雷同的新进程。操纵系统会将父进程的所在空间(代码段、数据段、堆和栈)复制到子进程。子进程获得父进程的一个副本,包括打开的文件描述符、环境变量和其他资源。这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。子进程获得一个唯一的进程ID(PID),这与父进程的PID不同。fork被调用一次,却返回两次,子进程返回0,父进程返回子进程的PID。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
6.4 Hello的execve过程

execv函数在当前进程的上下文中加载并运行一个新程序。它会加载指定的可执行文件,并接受参数列表和环境变量列表。execve只有在出现错误时才会返回到调用程序。与fork函数一次调用会返回两次不同,execve只会调用一次并且从不返回。
一旦可执行文件被加载,execve会启动启动代码。启动代码负责设置栈,将可执行文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的入口点或第一条指令来执行该程序,从而将控制权交给新程序的主函数。
6.5 Hello的进程执行

相关概念:
1)进程上下文:内核在重新启动被抢占的进程时所需的状态信息。它包括通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈,以及各种内核数据结构的值。
2)上下文切换:内核为每个进程维持一个上下文,上下文就是在进程执行的某些时候,内核可以决定枪战当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度。系统调用和停止也可能引发上下文切换。
3)逻辑控制流:尽管系统中通常有许多其他程序在运行,每个进程都可以获得一种假象,仿佛它在独占地利用处置惩罚器。如果利用调试器单步执行程序,我们会看到一系列的程序计数器(PC)值,这些值唯一地对应于程序的可执行目标文件中的指令,或者是运行时动态链接到程序的共享对象中的指令。这个 PC 值的序列称为逻辑控制流,简称逻辑流。
Hello进程的执行过程:
shell为hello fork了一个子进程,这个子进程和shell进程有独立的逻辑控制流,它们是并发举行的,但是若hello是以前台任务举行的,那么shell将会挂起等待hello运行结束,否则它们将会“同时运行”。
内核调度hello的进程开始举行,输出Hello 2022111009周天宇 178****5218 5后执行sleep(atoi(argv[4]))函数,这个函数是系统调用,显式地请求让调用进程休眠。内核转而执行其他进程,这时就会发生一个上下文转换。5s后又会发生一次进程转换,恢复hello进程的上下文,继续执行hello进程。其余9次sleep的执行雷同。
循环结束后,后面执行到getchar函数,getchar函数是通过read函数实现的。这时hello进程将会由于执行系统调用函数read而陷入内核,以是这里又会发生一个上下文切换转而执行其他进程。当数据被读取到缓存区后,将会发生一个停止,使内核发生上下文切换,重新执行hello进程,与调用sleep时的进程执行情况雷同。

图6-2 上下文切换
6.6 hello的异常与信号处置惩罚

异常类型:

图6-3 异常类型
异常处置惩罚流程:

图6-4 停止异常处置惩罚

图6-5 陷阱异常处置惩罚

图6-6 故障异常处置惩罚

图6-7 中止异常处置惩罚
程序正常执行:

图6-8 正常执行
随机按字符,无影响:

图6-9 运行时输字符
运行时按Ctrl+C, 会发送SIGINT信号,向子进程发送SIGKILL信号使进程终止并回收:

图6-10 运行时输Ctrl+C
运行时按下Ctrl+Z,会发送SIGTSTP信号,子进程被挂起:

图6-11 运行时输Ctrl+Z
输入ps会显示所有的进程及其状态,可以发现hello是被挂起的状态,PID为9190:

图6-12输入ps
输入jobs可以显示暂停的进程:

图6-13输入jobs
输入pstree可以显示进程树,显示所有进程的情况:

图6-14输入pstree
输入fg可以使第一个背景作业变成前台作业,由于hello是第一个背景作业,以是它又变为前台执行:

图6-15输入fg
kill指令,先输入ps查看hello的PID,输入kill -9 9190,杀死hello进程:

图6-16输入kill
6.7本章小结

本章主要解说了 hello 可执行文件的执行过程,涵盖了进程的创建、加载和终止,以及通过键盘输入等环节。从创建进程到回收进程的整个过程中,涉及到各种异常和停止信息。程序的高效运行离不开异常、信号和进程等概念,正是这些机制保障了 hello 程序在计算机上的顺利运行。
第7章 hello的存储管理

7.1 hello的存储器所在空间

以下格式自行编排,编辑时删除
联合hello阐明逻辑所在、线性所在、虚拟所在、物理所在的概念。
1)逻辑所在:包罗在机器语言指令中用来指定一个操纵数或一条指令的所在。它促使程序员把程序分成若干段。每一个逻辑所在都由一个段(segment)和偏移量(offset)构成,偏移量指明了从段开始的地方到实际所在之间的距离。对应于hello.o中的相对偏移所在。
2)线性所在:逻辑所在到物理所在变换之间的中间层。程序代码会产生逻辑所在,或说是段中的偏移所在,加上相应段的基所在就生成了一个线性所在。如果启用了分页机制,那么线性所在能再经变换以产生一个物理所在。若没有启用分页机制,那么线性所在直接就是物理所在。
3)虚拟所在:指的是在操纵系统虚拟内存中的所在,是逻辑所在颠末计算得到的结果,不能直接用于访问存储,必要通过MMU举行翻译才能得到物理所在。在hello的反汇编代码中,颠末计算后得到的就是虚拟所在。
4)物理所在:计算机主存中连续字节单元的唯一所在。每个字节都有其独特的物理所在。虚拟所在颠末MMU翻译后才能得到物理所在。在hello中,颠末翻译得到的物理所在用于获取所需的数据。
7.2 Intel逻辑所在到线性所在的变换-段式管理

在Intel处置惩罚器上,逻辑所在到线性所在的转换是通过段式管理实现的。逻辑所在由两个部门构成:段选择子和偏移量。段选择子指示一个段描述符,该描述符包罗段的基所在和长度信息,而偏移量则是相对于该段基所在的所在偏移量。
首先,根据逻辑所在中的段选择子找到对应的段描述符。段描述符位于全局描述符表(GDT)或局部描述符表(LDT)中,它包罗段的基所在及其他控制信息。从段描述符中获取段的基所在,并将其与逻辑所在中的偏移量相加,得到线性所在。这个线性所在是一个32位所在,位于操纵系统的虚拟所在空间中。
在生成线性所在后,处置惩罚器会检察访问权限,比方是否有足够的权限读写该段,是否在合法的内存范围内等。如果开启了分页机制,处置惩罚器会继续举行所在翻译,将线性所在转换为物理所在。这个过程包括查找页目次、页表项等。
7.3 Hello的线性所在到物理所在的变换-页式管理

页式管理是一种计算机内存管理方式,它将主存储器分割成大小固定的页,并将进程的所在空间也划分为雷同大小的页,从而实现物理内存和逻辑所在之间的灵活映射。其要点如下:
1.分页机制:操纵系统将内存划分成固定大小的页,通常为4KB或更大。处置惩罚器通过页表来管理这些页,页表将线性所在映射到物理所在。
2.页目次:页目次是特别的页表,存储着指向其他页表的指针。在分页机制中,处置惩罚器首先利用线性所在的高位来索引页目次,根据这些位找到对应的页目次项。
3.页表:页表是存储在内存中的数据结构,用于将线性所在映射到物理所在。每个进程都有自己的页表。处置惩罚器利用线性所在的中间位来索引页表,根据这些位找到对应的页表项。
4.偏移量:线性所在的低位部门是偏移量,用于在页内定位具体的所在。
5.所在翻译:处置惩罚器利用线性所在的高位部门找到对应的页目次项,然后利用中间位找到对应的页表项,末了加上偏移量得到物理所在。
6.TLB缓存:为提高所在翻译速率,处置惩罚器利用TLB缓存已经翻译过的页表项。如果在TLB中找到了对应的项,就能直接获取物理所在,否则必要访问内存获取页表项。 TLB(Translation Lookaside Buffer)是一个高速缓存,存储了迩来利用的一些页表项,以加速所在转换过程。
7.通过这些机制,页式管理实现了高效的所在映射和内存管理,为多任务操纵系统提供了良好的支持。
7.4 TLB与四级页表支持下的VA到PA的变换

TLB(Translation Lookaside Buffer)是一个硬件缓存,用于存储迩来的一些虚拟所在到物理所在的映射。在四级页表结构中,虚拟所在通常分为四个部门:索引1、索引2、索引3和偏移量。这四个部门一起构成了一个层级结构,用于访问页表。
当CPU必要转换虚拟所在到物理所在时,首先会查询TLB来查找映射。如果TLB中找到了对应的映射,这被称为TLB命中,CPU可以直接利用TLB中存储的物理所在。如果TLB中没有找到虚拟所在到物理所在的映射,这被称为TLB未命中。在这种情况下,CPU必须通过多级页表来查找所在映射。CPU首先利用索引1找到第一级页表项,再利用索引2找到第二级页表项,以此类推,直到找到最终的页表项,并从中获取物理所在。
TLB的作用是加速所在翻译的速率,由于TLB存储了迩来利用过的虚拟所在到物理所在的映射,减少了对页表的频仍访问,从而提高了内存访问的服从。
7.5 三级Cache支持下的物理内存访问

一旦获取了物理所在,处置惩罚器会开始访问三级缓存以及内存以获取物理内存中的内容。
L1缓存:L1缓存是距离处置惩罚器核心迩来的缓存层,速率最快。当处置惩罚器必要访问内存中的数据时,首先会检查L1缓存。如果数据在L1缓存中找到了对应的副本,则可以立刻访问这个数据,这被称为L1缓存命中。
L2缓存:如果在L1缓存中未找到必要的数据,处置惩罚器会继续检查L2缓存。L2缓存通常比L1缓存更大,但速率稍慢一些。如果数据在L2缓存中找到了对应的副本,则处置惩罚器会从L2缓存中读取这个数据,这称为L2缓存命中。
L3缓存:如果在L2缓存中也未找到必要的数据,处置惩罚器会继续检查L3缓存。L3缓存通常更大,但速率可能比L2缓存稍慢。如果数据在L3缓存中找到了对应的副本,则处置惩罚器会从L3缓存中读取这个数据,这称为L3缓存命中。
主存储器:如果在处置惩罚器的各级缓存中都未找到必要的数据,则处置惩罚器必要从主存储器中读取这个数据。这将导致一个内存访问周期,处置惩罚器从主存中读取数据并将其加载到适当的缓存层中。
这些缓存层级的存在可以极大地提高内存访问速率,由于较高级别的缓存通常速率更快,而且更接近处置惩罚器核心。只有在缓存中未命中时,才会导致对较慢的主存储器的访问。

图7-1 一个存储器层次结构的示例
7.6 hello进程fork时的内存映射

在fork()系统调用时,新的子进程被创建。最初,子进程会拥有与父进程雷同的内存映射。然而,这些内存映射并不是实际共享的,而是采用了写时复制(copy-on-write)的机制。具体来说,当fork()调用发生时,操纵系统会为子进程创建一个与父进程雷同的虚拟所在空间副本,但在物理内存上并不会真正存在雷同的副本。相反,它们会共享雷同的物理页面。直到父进程或子进程中有一个尝试修改这些共享内存页面时,才会发生实际的复制。当有一方尝试修改共享内存地域时,操纵系统会将该内存页面复制到一个新的物理内存页面,并且这个新页面只属于修改它的进程。这样就实现了进程间的内存分离,制止了不必要的内存复制,提高了服从。
因此,在fork()之后,父子进程共享雷同的内存映射,但在任何一个进程中对这些映射的修改都不会影响到其他进程,直到有进程修改了这些共享的页面,触发了实际的复制操纵。
7.7 hello进程execve时的内存映射

在一个进程调用execve()时,该进程的虚拟所在空间会被一个新的程序所取代。execve()系统调用会完成以下步调:
1. 清除原所在空间映射:
*移除旧程序的代码、数据和堆栈等内容。
*清空原先程序利用的内存地域。
2. 加载新程序的可执行文件:
*从磁盘上的可执行文件中加载新程序的代码和数据到内存。
*构建新程序的堆和栈。
3. 更新程序计数器:
*将程序计数器设置到新程序的入口点,使得程序重新的代码段开始执行。
4. 创建新的内存映射:
*根据新程序的需求,在虚拟所在空间中创建新的内存映射,包括代码段、数据段、堆和栈。
这些步调确保了进程在调用execve()后能够正确加载并执行新的程序,同时清除了旧程序的内存映射,为新程序的执行做好了预备。
7.8 缺页故障与缺页停止处置惩罚

在虚拟内存的习惯说法中,DRAM 缓存不命中称为缺页。这时会触发缺页停止,导致操纵系统参与并举行相应的处置惩罚。处置惩罚流程如下:

  • 触发停止:CPU访问虚拟所在对应的页面,但该页面不在物理内存中,触发缺页停止。
  • 操纵系统参与:CPU转交控制权给操纵系统内核,执行缺页停止处置惩罚程序。
  • 检查缺页原因:操纵系统分析引起缺页的原因,可能是页面未分配、页面被交换到磁盘上或者是页面错误等。
  • 处置惩罚缺页:

    • 未分配页面:如果所在对应的页面尚未分配,则分配一页物理内存并更新页表。
    • 页面被交换到磁盘:如果页面在磁盘上,则执行页面置换算法将一个页面从磁盘换入到物理内存中。
    • 页面错误:如果是其他类型的页面错误,比方权限错误或无效访问,则相应地处置惩罚。

  • 更新页表:操纵系统将正确的物理所在映射到相关的页表项中。
  • 恢复进程执行:停止处置惩罚完成后,CPU恢复用户进程的执行。通常,进程不会察觉到缺页停止,由于停止处置惩罚后,所在对应的页面已经在物理内存中,进程可以继续正常执行。

    图7-2 缺页停止处置惩罚
7.9动态存储分配管理

动态内存分配器负责管理堆的分配和释放,维护对堆内存的跟踪,并提供接口供程序员举行内存的动态管理。
堆是进程运行时用于动态分配内存的一块地域,通常位于进程所在空间的底部,由动态分配器举行管理。程序在运行时可以请求从堆中分配内存空间,并在利用完后释放。堆内存的大小通常在程序启动时确定,但在运行时可以动态增长或缩减。
在程序中,利用动态内存分配器接口(比方malloc)来请求和释放内存时,实际上是向动态内存分配器发出请求,而动态内存分配器会在堆中举行相应的操纵。
动态内存分配器可以分为两种类型:显式分配器和隐式分配器。
1)显式分配器:程序员显式地分配和释放内存。在这种情况下,程序员负责跟踪内存的分配和释放,并通过特定的API手动请求分配内存和释放不再利用的内存。
2)隐式分配器:由编程语言或者运行时环境自动处置惩罚内存的分配和释放。在这种情况下,内存的分配和释放是由语言或者环境的机制隐式完成的,程序员无需手动举行内存管理。比方,在一些高级语言中,有自动的垃圾回收机制,负责自动释放不再利用的内存。
不论是显式还是隐式的动态内存分配器,其目标都是为了方便程序员管理内存,提高内存的利用率和程序的性能。
7.10本章小结

本章深入探讨了hello程序的存储器所在空间和在Intel处置惩罚器上的内存管理机制,包括段式管理和页式管理。具体讨论了在Intel环境下的虚拟所在到物理所在的转换过程,以及物理内存的访问方式。同时,解释了TLB、四级页表和三级缓存在所在转换和内存访问中的作用和支持。
第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件
设备管理:unix io接口
Linux 中的 I/O 设备管理是通过文件系统来举行的,I/O 设备以文件的形式呈现,这些文件都位于 /dev 目次下。Linux 贯彻"一切皆文件"的理念,提供了统一的 I/O 设备访问模型,因此对于每个设备,用户可以通过文件 I/O 系统调用(如 open、read、write、close 等)来举行访问和控制。
8.2 简述Unix IO接口及其函数

Unix I/O接口是Unix和类Unix操纵系统中用于举行输入输出操纵的一组接口和函数。这些函数包括了文件操纵、套接字操纵、管道操纵等,提供了统一的接口来处置惩罚各种不同类型的I/O操纵。以下是Unix I/O接口中常见的函数:
Unix I/O函数按照功能和参数可以分为以下几类:
1.文件操纵函数:
int open(const char *pathname, int flags, mode_t mode):打开文件或创建文件。
int close(int fd):关闭文件。
ssize_t read(int fd, void *buf, size_t count):从文件中读取数据到缓冲区。
ssize_t write(int fd, const void *buf, size_t count):将数据从缓冲区写入文件。
off_t lseek(int fd, off_t offset, int whence):在文件中定位文件指针的位置。
int fcntl(int fd, int cmd, …):对文件描述符举行控制操纵。
int rename(const char *oldpath, const char *newpath):重定名文件。
int unlink(const char *pathname):删除文件名。
2.文件描述符操纵函数:
int dup(int oldfd) / int dup2(int oldfd, int newfd):复制文件描述符。
3.设备操纵函数:
int ioctl(int fd, unsigned long request, …):举行设备控制操纵,对特定设备举行设置和查询。
4.套接字操纵函数:
int socket(int domain, int type, int protocol):创建套接字。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen):绑定所在或创建毗连。
int listen(int sockfd, int backlog):监听套接字上的毗连请求。
5.多路复用函数:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout):用于多路复用I/O操纵,检查多个文件描述符是否停当。
6.管道操纵函数:
int pipe(int pipefd[2]):创建管道。
8.3 printf的实现分析

printf函数吸收一个格式字符串fmt和参数,按照fmt的格式输出匹配到的参数。在底层实现中,printf调用了两个外部函数:vsprintf和write。vsprintf负责将格式化的字符串写入缓冲区中,而write系统调用则将缓冲区中的内容写入到输出设备。这通常涉及到一个陷阱-系统调用,如int 0x80或syscall,将控制权从用户态切换到内核态。
printf()函数原型:

图8-1 printf函数示意图
此中, (char*)(&fmt) + 4) 表现的是…中的第一个参数。fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。
Vsprintf()函数原型:

图8-2 Vsprintf函数示意图
vsprintf的作用就是格式化。它接受确定输特别式的格式字符串fmt。用格式字符串对个数变化的参数举行格式化,产生格式化输出。
write()函数原型:

图8-3 write函数示意图
这里是给几个寄存器转达了几个参数,然后通过系统来调用sys_call这个函数。
sys_call函数:

图8-4 sys_call函数示意图
这个函数的功能就是不绝的打印出字符,直到遇到:‘\0’; [gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串。
8.4 getchar的实现分析

在系统中,键盘输入会触发异步停止。当用户按下键盘上的键时,硬件将该动作转换为扫描码并发送给计算机。键盘停止处置惩罚程序吸收按键的扫描码,并将其转换为相应的ASCII码,然后将ASCII码存储到系统的键盘缓冲区中。这个缓冲区通常是一个队列,用于存储键盘输入的字符序列。
在C语言中,getchar() 函数通常调用底层的系统调用,通常是 read()。在键盘输入的背景下,getchar()尝试从尺度输入读取字符。它调用底层的 read() 系统调用,该调用会壅闭程序直到有字符输入或者发生错误。在键盘输入的情况下,read() 会等待按键输入,并当吸收到字符后,将其读取为ASCII码。一旦吸收到回车键的按下,getchar() 会从键盘缓冲区中读取字符,并返回相应的ASCII码值。这样,getchar()实现了一次键盘输入的处置惩罚,等待用户输入并在输入完成后返回。
因此,getchar()在键盘输入的实现中会通过系统调用 read() 从键盘读取字符,但会等待用户输入完成(按下回车键)后才返回。
8.5本章小结

本章重点先容了Linux的I/O设备管理以及Unix的I/O接口函数。在此根本上,我们深入分析了printf和getchar函数的工作原理,突出了I/O在系统中的关键性作用,并探讨了Unix I/O接口的告急性。Linux操纵系统将系统中的I/O设备抽象为文件,这一概念的引入大大简化了对I/O操纵的处置惩罚。通过对文件的操纵,包括打开、位置更改、读写和关闭等,实现了对各种I/O设备的统一管理。这种设计使得编程者可以用雷同的方式处置惩罚不同类型的I/O设备,提高了代码的可读性和可维护性。
结论

Hello的一生是一个看似简单实则十分复杂的过程,当中蕴含着每一个c语言程序执行前的必经之路:
首先是可执行文件的形成过程:

  • 编写:利用高级语言编写.c文件。
  • 预处置惩罚:将.c文件转化为.i文件,归并外部库调用。
  • 编译:将.i文件转化为.s汇编文件。
  • 汇编:将.s文件翻译为机器语言指令,生成可重定位目标程序hello.o。
  • 链接:将.o文件和动态链接库链接为可执行目标程序hello。
然后是在系统和硬件的精密协作下的运行过程:
1. 在Shell中输入命令,子进程被创建,并调用execve函数加载并运行hello。
2. CPU为进程分配时间片,加载器设置程序入口点,hello开始执行逻辑控制流。
3. 通过MMU映射虚拟内存所在至物理内存所在,CPU访问内存。
4. 动态内存分配根据必要申请内存。
5. 信号处置惩罚函数处置惩罚程序的异常和用户请求。
6. 执行完成后,父进程回收子进程,内核删除为该进程创建的数据结构,到此hello运行结束,完成了它短暂的“演出”。
感想:现代计算机是云云的精密,从硬件到软件,环环相扣又紧密配合,包罗着无数巧妙的设计思想,让学习计算机的我们既“头疼”又深深被此中的奥秘吸引。盼望今后能继续深入熟悉这个“朋友”,让它成为我们改造世界的利器。
附件


参考文献

[1] bash处置惩罚的12个步调流程图_linux bash流程详解-CSDN博客
[2] 本电子书信息 - 深入明确计算机系统(CSAPP) (gitbook.io)
[3] [转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com)
[4] 读CSAPP(4) - 虚拟内存 – heisenbug blog (heisenbergv.github.io)
[5] C语言编译过程详解 - kinfe - 博客园 (cnblogs.com)

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

干翻全岛蛙蛙

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表