2024哈工大计算机系统课程大作业:程序人生-Hello’s P2P ...

小小小幸运  金牌会员 | 2024-6-26 18:34:19 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 504|帖子 504|积分 1512

摘  要

这篇论文深入探讨了在 Linux 系统下,一个简朴的 C 语言文件hello.c的完备生命周期。从最初的原始程序开始,逐步深入研究了编译、链接、加载、运行、终止以及接纳的各个阶段,以此相识hello.c文件的整个演变过程。论文以 hello.c文件为紧张研究对象,结合了csapp课程的相关内容,在 Ubuntu 系统下对程序的生命周期举行了系统研究。对程序的深入分析能乐成将计算机系统的各个构成部分串联起来,真正实现了理论与实践的结合,达到了全面理解与应用的目的。

关键词:程序的生命周期;计算机系统;计算机体系结构;


第1章 概述

1.1 Hello简介

Hello的P2P是指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。hello.c 文件经过 cpp 的预处理惩罚、 ccl 的编译、 as 的汇编、 ld 的链接终极成为可执目标程序 hello。 在 shell 中输入下令./hello后,shell 通过fork产生子进程,hello 便从可执行程序(Program)变成为进程(Process)。
Hello的020是指hello.c文件“From 0 to 0”,初始时内存中并无hello文件的相关内容。shell 为 hello 进程 execve,映射假造内存,进入程序入口后程序开始载入物理内存,然后执行相关代码,程序运行结束后shell 父进程负责接纳 hello 进程,内核删除相关数据结构。
1.2 环境与工具

硬件:AMD Ryzen 7 6800H with Radeon Graphics  3.20 GHz
         16GB RAM
软件:Windows11 64位
Ubuntu 20.04.6 LTS 64位
开发与调试工具:VScode,gcc,as,ld,vim,edb,readelf,objdump
1.3 中间结果

文件的作用

文件名

预处理惩罚后的文件

hello.i

编译之后的汇编文件

hello.s

汇编之后的可重定位目标文件

hello.o

链接之后的可执行目标文件

Hello

Hello.o 的 ELF 格式

Elf.txt

Hello.o 的反汇编代码

Disas_hello.s

hello的ELF 格式

hello1.elf

hello 的反汇编代码

hello1_objdump.s


1.4 本章小结

本节内容概述了hello的一生,并详细介绍了P2P、020的整个流程,包括本计算机的硬件环境、软件环境、开发工具,以及用于编写论文的中间文件名称及其功能。
第2章 预处理惩罚

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

程序计划领域中,预处理惩罚是指程序开始运行时,预处理惩罚器(cpp,C Pre-Processor,C预处理惩罚器)根据以字符#开头的下令,修改原始的C程序的,生成二进制代码之前的过程。典型地,由预处理惩罚器对程序源代码文本举行处理惩罚,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码举行解析,但它把源代码分割或处理惩罚成为特定的单位——(用C/C++的术语来说是)预处理惩罚记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。预处理惩罚指令一样平常被用来使源代码在不同的执行环境中被方便的修改大概编译。
预处理惩罚器(cpp)根据以字符#开头的下令,修改原始的C程序。比如hello.c中第1行的#include <stdio.h>下令告诉预处理惩罚器读取系统头文件stdio.h的内容,把它直接插人程序文本中,并拓展全部用#define声明指定的宏。结果就得到了另一个C程序,通常是以.i作为文件扩展名,生成的hello.i文件仍旧是文本文件。
2.2在Ubuntu下预处理惩罚的下令

在Ubuntu系统下,输入以下下令举行预处理惩罚:
cpp hello.c > hello.i
运行结果如下:

2.3 Hello的预处理惩罚结果解析

打开hello.i文件,可以发现hello.i程序已经拓展为3110行,行数比起hello.c文件大幅增加。其中, hello.c中的main函数相关代码在hello.i程序中对应着3047行到3061行。

预处理惩罚器执行宏替换、条件编译以及包罗指定的文件。
#include指令:用于在编译期间把置顶文件的内容包罗进当前文件中;
#define指令:用恣意字符序列替换一个标记;
在main函数内代码出现之前是大段的头文件 stdio.h unistd.h stdlib.h 的依次睁开。睁开的详细流程概述如下(以stdio.h为例):CPP先删除指令#include <stdio.h>,并到Ubuntu系统的默认的环境变量中寻找 stdio.h,终极打开路径/usr/include/stdio.h下的stdio.h文件。
若stdio.h文件中使用了#define语句,则按照上述流程继续递归地睁开,直到全部#define语句都被表明替换掉为止。除此之外,CPP还会举行删除程序中的解释和多余的空缺字符等利用,并对一些值举行替换。

2.4 本章小结

本章紧张介绍了预处理惩罚(包括头文件的睁开、宏替换)的概念和应用功能,以及Ubuntu下预处理惩罚的指令,同时详细到我们的hello.c文件的预处理惩罚结果hello.i文本文件解析,详细相识了预处理惩罚的内涵。
第3章 编译

3.1 编译的概念与作用

编译器(ccl)将名为hello.i的文本文件转换为名为hello.s的文本文件,其中包罗一个汇编语言程序。这个程序用一种尺度的文本格式准确描述了每条低级机器语言指令。汇编语言的代价在于它为不同高级语言的编译器提供了通用的输出语言。举例来说,无论是C编译器照旧Fortran编译器产生的输出文件,它们都接纳相同的汇编语言格式。
3.2 在Ubuntu下编译的下令

编译下令如下:
gcc -m64 -Og -S -no-pie -fno-PIC hello.c -o hello.s

3.3 Hello的编译结果解析

3.3.1文件分析

对hello.s文件整体结构分析如下:
内容

含义

.file

源文件

.text

代码段

.global

全局变量

.data

存放已经初始化的全局和静态C 变量

.section  .rodata

存放只读变量

.align

对齐方式

.type

表示是函数范例/对象范例

.size

表示大小

.long  .string

表示是long范例/string范例


3.3.2数据

C语言中的数据有:常量、变量、表达式等。
常量大多是以立刻数的形式出如今汇编代码hello.s中,如:exit(1)中的1

变量分为全局变量、静态变量、局部变量。
全局变量:
初始化的全局变量储存在.data节,它的初始化不必要汇编语句,而是直接完成的。
局部变量:
局部变量存储在寄存器或栈中。程序中的局部变量i定义
在我们的 hello.c 程序中,没有全局变量和静态变量,因此在 hello.s 文件的开头的伪指令中,我们不会找到 .data 和 .bss 节。但是,我们会看到 .rodata 只读数据节的存在,由于这里存放着 printf 的格式串。比方,对于 printf("用法: Hello 学号 姓名 秒数!\n") 这一语句,其汇编代码为 movl $.LC0, %edi 和 call puts。我们可以看到 .LC0 中的字符串正是我们要输出的字符串,其中的汉字已经转换成了相应的编码。

for(i=0;i<10;i++)中的i是一个局部变量,编译器将它放在了寄存器%ebp中。首先将它初始化为0,cmpl和addl分别实现比力和加一利用。

在hello.c程序中一共有3个表达式,分别为赋值表达式i=0,两个关系表达式argc!=5和i<10,分别用cmpl     $5, %edi和cmpl $9, %ebp实现。

3.3.3赋值

int i
对局部变量的赋值在汇编代码中通过mov指令完成。详细使用哪条mov指令由数据的大小决定,如图所示:
后缀

b

w

l

q

大小(字节)

1

2

3

4

由hello.s可以瞥见,i=0是使用movl指令
3.3.4算术利用

整数利用指令如下表所示:(图片来源于课本)

在hello.s中,我们可以看到第45行就是加法利用,用于i++


3.3.5关系利用

常用的关系利用指令有cmp和test,前者用于比力,后者用于测试。
程序第14行中判断传入参数argc是否等于5,源代码为

汇编代码为

jne用于判断cmpl产生的条件码,若两个利用数的值不相称则跳转到指定地址.L6


3.3.6数组/指针/结构利用

程序中涉及的数组为char *argv[],即函数的第二个参数。在hello.s中,其首地址生存在栈中。访问时,通过寄存器寻址的方式访问。
argv,这是一个指针数组,每个数组元素是一个指向参数字符串的指针。末了程序调用argv的一个数组元素。printf("Hello %s %s %s\n",argv[1],argv[2],argv[3])的汇编代码为:


3.3.7控制转移

控制转移紧张涉及跳转利用,在汇编代码中紧张是je, jg, jne等形式存在。
在本程序中控制转移的详细表现有两处:

  • if(argc!=5):
当argc不等于5时,执行函数体内部的代码。若不相称,则跳转至.L6,不执行后续部分内容.


  • for(i=0;i<10;i++):
当i < 10时举行循环,每次循环i++。在hello.s中,使用cmpl $9,%ebp,比力 i与9是否相称,在i<9时继续循环,进入.L7,i>=9时跳出循环。



3.3.8函数利用

C语言中,调用函数时举行的利用如下:
转达控制:
举行过程 Q 的时间,程序计数器必须设置为 Q 的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。
转达数据:
P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回一个值。
分配和释放内存:
在开始时,Q 可能必要为局部变量分配空间,而在返回前,又必须释放这些空间。
hello.c 中间也有众多函数,比方printf函数(上文中已有截图片断)sleep(atoi(argv[4]));等函数,puts,printf,sleep,exit,getchar 函数紧张是通过call指令举行调用,比如在hello.s文件38-44行,程序三次调用了call指令


3.4 本章小结

本章讨论了编译以及相关利用。编译是将hello.i转换为汇编文本文件hello.s的过程。汇编代码是机器代码的文本形式,它展示了程序中的每一条指令。理解和分析汇编代码是一项紧张的技能,通过这种理解,我们可以评估编译器的优化结果,并分析代码中可能存在的低效。

第4章 汇编

4.1 汇编的概念与作用

概念
驱动程序运行汇编器as,将汇编语言(这里是hello.s)翻译成机器语言(hello.o)的过程称为汇编,同时这个机器语言文件也是可重定位目标文件。
作用
汇编是将高级语言转换为机器可直接执行的代码文件的过程。汇编器负责将.s汇编程序翻译成机器语言指令,并将这些指令打包成可重定位的目标程序格式,然后生存在.o目标文件中。.o文件是一个二进制文件,其中包罗了程序指令的编码。
4.2 在Ubuntu下汇编的下令

在Ubuntu下汇编的下令为:
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

4.3 可重定位目标elf格式

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


下面我们分析结构:

  • ELF头
其内容如下所示:

ELF头以一个16字节的Magic序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包罗帮助链接器语法分析和表明目标文件的信息。其中包括ELF头的大小,目标文件的范例(可重定位、可执行大概是共享的)、机器范例(如x86-64)、节头部表的文件偏移、以及节头部表中条目的大小和数目。

2.节头:紧张包罗文件中出现的各个节的意义,包括节的范例、地址大小等信息。


3.重定位节.rela.text
表述了各个段引用的外部符号等,在链接时,必要通过重定位节对这些位置的地址举行修改。链接器会通过重定位条目的范例判断该使用什么养的方法计算正确的地址值,通过偏移量等信息计算出正确的地址。
本程序必要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号。
4.重定位节.rela.eh_frame
反映了eh_frame 节的重定位信息,如下所示


5.符号表

用来存放程序中定义和引用的函数和全局变量的信息。注意,符号表不包罗局部变量的条目。

4.4 Hello.o的结果解析

以下格式自行编排,编辑时删除
使用objdump -d -r hello.o 指令分析hello.o的反汇编,将其输出到hello.asm便于分析,接下来我们将其与第3章的 hello.s举行对照分析。
得到的结果如下(部分截图)



1.分析机器语言

x86-64的指令长度从1到15个字节不等。而且hello.s中的利用数时十进制,hello.o反汇编代码中的利用数是十六进制。常用的指令以及利用数较少的指令所需的字节数少,而那些不太常用或利用数较多的指令所需字节数较多;

计划指令格式的方式是,从某个给定位置开始,可以将字节唯一地解码成机器指令。如果指令中有地址大概常数,要按小端存储顺序依次存放。


2.分支转移

跳转语句之后,hello.s中是.L2和.L3等段名称,而反汇编代码中跳转指令之后是相对偏移的地址,即目标指令地址与当前指令下一条指令的地址之差。


3.函数调用
在hello.s文件中,call之后直接跟着函数名称,而在反汇编得到的hello.asm中,call 的目标地址是当前指令的下一条指令。比如下图call后紧跟的是3c <main+0x3c>,并且还有一个重定位条目,用于链接时重定位。

我们可以知道,hello.c 中调用的函数都是共享库中的函数,终极必要通过动态链接器作用才能确定函数的运行时执行地址,机器在.rela.text 节中为其添加重定位条目,等待静态链接进一步确定。这是他们的紧张区别。


4.5 本章小结

这一部分详细探讨了汇编的根本原理和执行过程。在这个过程中,汇编器将汇编语言转换为机器语言指令,并将这些指令打包成可重定位的目标程序,生存在名为hello.o的文件中。hello.o文件是一个二进制文件,其中包罗了函数main的指令编码。在链接阶段,可以将多个可重定位目标文件合并成一个可执行的目标文件。



第5章 链接

5.1 链接的概念与作用

链接是将各种代码和数据片断网络并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。链接可以在编译时、加载时或运行时执行。比方,当hello程序调用printf函数时,它是尺度C库中的一个函数,存在于一个名为printf.o的单独的预编译目标文件中。这个目标文件必须以某种方式合并到我们的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

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

下令:readelf -a hello > hello1.elf
打开hello1.elf,分析hello的ELF格式如下:
1.ELF 头
hello1.elf和hello.elf中的ELF头大致包罗相似的信息,以一个16字节序列 Magic 开始,描述生成该文件的系统的字节大小和字节顺序。别的部分则包罗有助于链接器分析和表明目标文件的信息。相较之下,hello1.elf的根本信息(如Magic和种别)未发生变化,但范例已经改变,同时程序头大小和节头数目增加,并添加了入口地址。

2.节头
hello2.elf中的节头包罗了文件中出现的各个节的语义,包括节的范例、位置、偏移量和大小等信息。与hello.elf相比,其在链接之后的内容更加丰富详细。


3.程序头

程序头部表描述了可执行文件的连续的片被映射到连续的内存段的映射关系。每一个表项提供了各段在假造地址空间大小和物理地址,标志,访问权限和对齐方式。我们可以以此读出各段的起始地址。


4.动态段

动态段(Dynamic section)是ELF文件中的一个部分,它包罗了一些与运行时链接和执行时特性相关的信息。这些信息包括共享对象的动态符号表、重定位项、共享对象的名称等。动态段使得程序在加载和执行时能够动态地连接到所需的库,并举行必要的重定位以确保正确的内存映射和地址解析。


5.符号表

符号表(Symbol Table)是可执行文件或共享目标文件中的一种数据结构,用于存储程序中使用的符号及其相关信息。这些符号可以是变量、函数、类、结构体或其他命名实体的名称。符号表通常由编译器在编译源代码时生成,并由链接器在链接过程中使用。


5.4 hello的假造地址空间

使用edb加载hello, Data dump 窗口可以检察加载到假造地址中的 hello 程序。检察 ELF 格式文件中的 Program Headers,它告诉链接器运行时加载的内容,并提供动态链接的信息。每一个表项提供了各段在假造地址空间和物理地址空间的各方面的信息。如下图所示。 

根据计算机系统的规定,程序被加载到地址范围0x401000~0x402000中。在这个范围内,每个节的地址与前一个节中相对应的地址相同。根据使用edb检察的结果,在地址空间0x401000~0x401fff中存放着与地址空间0x401000~0x402000相同的程序内容,而在0x401fff之后则存放着.dynamic到.shstrtab节的内容。
5.5 链接的重定位过程分析

使用下令objdump -d -r hello > hello1.asm生成反汇编文件hello1.asm,与第四章中生成的hello.o.asm文件举行比力,其不同之处如下:
1.链接增加新的函数:
在hello中链接加入了在hello.c中用到的库函数,如exit、printf、sleep、getchar等函数。
2.增加了节:
hello中增加了.init和.plt节,和一些节中定义的函数。


3.函数调用:
hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了假造内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
4.地址访问:
hello.o中的相对偏移地址转换为hello中的假造内存地址。但是,在hello.o文件中,对于某些地址的定位是不明确的,这些地址在运行时才会确定。因此,在访问这些地址时必要举行重定位。在汇编成机器语言时,会将这些利用数全部置为0,并添加重定位条目。
5.6 hello的执行流程
使用edb单步调试程序,观察函数调用。在调用main之前,举行了初始化工作并调用了_init函数。在_init之后,动态链接的重定位工作完成。随后调用了一系列库函数(如printf、exit、atoi等),它们在代码段中并不占用现实空间,只是占位符号,现实内容存储在共享区(高地址)。接着调用_start作为起始地址,预备执行main内容。在main执行后,还会执行__libc_csu_init、__libc_csu_fini、_fini等函数,终极程序结束。
下面列出其调用与跳转的各个子程序名或程序地址。
401000 <_init>

401020 <.plt>

401030 <puts@plt>

401040 <printf@plt>

401050 <getchar@plt>

401060 <atoi@plt>

401070 <exit@plt>

401080 <sleep@plt>

401090 <_start>

4010c0 <_dl_relocate_static_pie>

4010c1 <main>

401150 <__libc_csu_init>

4011b0 <__libc_csu_fini>

4011b4 <_fini>


5.7 Hello的动态链接分析

对于动态共享链接库中的PIC函数,编译器无法猜测函数的运行时地址,因此必要添加重定位记录,等待动态链接器处理惩罚。GNU编译系统接纳耽误绑定技能,将过程地址的绑定推迟到第一次调用该过程时。这种做法的动机是,对于像libc.so如许的共享库输出的成百上千个函数,一个典型的应用程序只会使用其中很少的一部分。将函数地址解析推迟到现实调用时,能够避免动态链接器在加载时举行大量不必要的重定位。虽然第一次调用过程的运行时开销较大,但之后的每次调用只需花费一条指令和一个间接的内存引用。耽误绑定通过GOT(全局偏移量表)和PLT(过程链接表)两个数据结构的交互实现。如果目标模块调用共享库中的函数,则其拥有自己的GOT和PLT。GOT是数据段的一部分,PLT是代码段的一部分。在这种组合中,GOT[0]和GOT[1]包罗动态链接器解析函数地址所需的信息,而GOT[2]是动态链接器在ld-linux.so模块中的入口点。链接器接纳耽误绑定策略,以避免在运行时修改调用模块的代码段。动态链接器使用PLT和GOT实现函数的动态链接,其中GOT存放函数目标地址,PLT使用GOT中的地址跳转到目标函数。在加载时,动态链接器会重定位GOT中的每个条目,使其包罗目标的正确绝对地址。
接下来观察 dl_init 前后动态链接项目的变化。.got.plt 节的起始地址是 0x601000,在 DataDump 中找到该位置,
使用 edb 执行至 dl_init,按 F8,发现地址 0x601000 后发生了变化:

可以看到 dl_init 后出现了两个地址0x7f27f6a3el70和0x7f27f682c750,这便是 GOT[1]和 GOT[2]。


5.8 本章小结
本章紧张展示在linux中链接的过程。通过检察hello的假造地址空间,并且对比hello与hello.o的反汇编代码,加深了对重定位与动态链接的理解。链接远不止本章所涉及的这么简朴,就像是hello会在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中。

第6章 hello进程管理

6.1 进程的概念与作用

概念
进程是一个运行中的程序实例也是系统举行资源分配和调理的根本单位。通常,它包括文本地区、数据地区和堆栈。文本地区存储着处理惩罚器执行的代码,数据地区存储变量和进程执行期间动态分配的内存,而堆栈地区则存储着活动过程调用的指令和本地变量。
作用
给应用程序提供两个关键抽象:一个独立的逻辑流,它提供一个假象,好像我们的程序独占地使用处理惩罚器;一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理惩罚流程

Shell 的作用:
Shell是一个使用C语言编写的交互式应用程序,其作用是代表用户来运行其他程序。Shell应用程序提供了一个界面,用户可以通过这个界面举行系统的根本利用,并访问利用系统内核的服务。
Shell的处理惩罚流程大致如下:

  • 从Shell终端读入输入的下令。
  • 切分输入字符串,获得并辨认全部的参数
  • 若输入参数为内置下令,则立刻执行
  • 若输入参数并非内置下令,则调用相应的程序为其分配子进程并运行
  • 若输入参数非法,则返回错误信息
  • 处理惩罚完当前参数后继续处理惩罚下一参数,直到处理惩罚完毕
6.3 Hello的fork进程创建过程

首先运行hello 程序,必要在shell 输入./hello 2023140004 yfx 15662786165 0
接下来 shell 会分析这一串下令:
 1. 先判断./hello 是否是内置下令,结果是它不是内置下令
2. 然后 shell 调用 fork()函数,创建一个子进程,这个子进程与父进程几乎没有差别,子进程的假造地址空间均与父进程的映射关系一致,是父进程假造地址空间的一份副本,包括代码和数据段、堆、共享库以及用户栈。同时,子进程还获得与父进程任何打开文件描述符相同的副本,故此时子进程可以读写父进程打开的任何文件。子进程与父进程的最大差别在于它们有不同的 PID。
3. 接下来 hello将加载到fork 创建的子进程中,然后执行。

6.4 Hello的execve过程

调用fork函数创建新的子进程后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。execve函数从不返回,它会扫除该进程的代码和地址空间内的内容,然后初始化并运行该程序,通过跳转到程序的第一条指令或入口点。它会映射私有地区,如打开的文件、代码和数据段,然后映射公共地区。加载器随后跳转到程序的入口点,即设置PC指向_start地址。_start函数终极调用hello中的main函数,从而完成了子进程的加载过程。
6.5 Hello的进程执行 

execve函数负责加载并运行可执行目标文件Hello,并吸收参数列表argv和环境变量列表envp。它的功能是在当前进程的上下文中启动一个新的程序。与fork一次调用两次返回的情况不同,只有在发生错误时,比如找不到Hello时,execve才会返回到调用程序。
一旦execve加载了Hello,它会调用启动代码。启动代码设置栈,并将控制转达给新程序的主函数,其原型如下:
int \ main(int \ argc, \ char \ **argv, \ char \ *envp)

结合假造内存和内存映射过程,可以更详细地说明execve函数现实上是如何加载和执行程序Hello:

1. 删除已存在的用户地区(自父进程独立)。
2. 映射私有区:为Hello的代码、数据、.bss和栈地区创建新的地区结构,全部这些地区都是私有的、写时复制的。
3. 映射共享区:比如Hello程序与尺度C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户假造地址空间中的共享地区内。
4. 设置PC:execve做的末了一件事就是设置当前进程的上下文中的程序计数器,使之指向代码地区的入口点。

6.6 hello的异常与信号处理惩罚

hello 执行过程中可能出现四类异常:中断、陷阱、故障和终止。
1. 中断是由I/O设备发送的信号,异步发生,中断处理惩罚程序对其举行处理惩罚后会返回到调用前待执行的下一条代码,就好像没有发生过中断一样。

2. 陷阱是有意的异常,是执行一条指令的结果。调用陷阱后会返回到下一条指令,用来调用内核的服务举行利用,帮助程序从用户模式切换到内核模式。

3. 故障是由错误情况引起的,可能能够被故障处理惩罚程序修正。如果修正乐成,则将控制返回到引起故障的指令,否则将终止程序。

4. 终止是不可规复的致命错误造成的结果,通常是一些硬件错误。处理惩罚程序会将控制返回给一个abort例程,该例程会终止这个应用程序。

接下来分析hello执行过程中对各种异常和信号的处理惩罚
1)正常运行
程序正常执行,总共循环10次每次输出提示信息之后等待我们从下令行输入的秒数,末了必要输入一个字符回车结束程序。

2)中途按下ctrl-Z
内核向前台进程发送一个SIGSTP信号,前台进程被挂起,直到关照它继续的信号到来,继续执行。

当按下fg 1 后,输出下令行后,被挂起的进程从停息处,继续执行。

3)中途按下ctrl-C
内核向前台进程发送一个SIGINT信号,前台进程终止,内核再向父进程发送一个SIGCHLD信号,关照父进程接纳子进程,此时子进程不再存在

4)不停乱按
在程序执行过程中乱按所造成的输入均缓存到stdin,当getchar的时间读出一个’\n’末了的字串(作为一次输入),hello结束后,stdin中的其他字串会当做Shell的下令行输入。

6)kill下令
挂起的进程被终止,在ps中无法查到到其PID。输入kill -9 %1就可以删除进程。

6.7本章小结

本章介绍了hello进程的执行过程,紧张涉及其创建、加载和终止,以及与键盘输入的交互。程序是对指令、数据及其构造形式的描述,而进程则是程序的实体,现实运行的程序。在hello的运行过程中,内核会根据情况对其举行管理,包括何时举行上下文切换等利用。同时,在hello的执行过程中,当吸收到不同的异常信号时,异常处理惩罚程序将会对这些信号做出相应的处理惩罚,执行相应的代码。不同的信号拥有不同的处理惩罚机制,因此针对不同的异常信号,hello会产生不同的处理惩罚结果。通过对hello执行过程中信号的产生和处理惩罚过程的探究,我们对于使用Linux调试和运行程序有了更深入的理解和认识。

第7章 hello的存储管理

7.1 hello的存储器地址空间

1.逻辑地址
逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分构成。详细而言,其为hello.asm中的相对偏移地址。
2.线性地址
逻辑地址经过段机制转化后为线性地址,其为处理惩罚器可寻址空间的地址,用于描述程序分页信息的地址。详细以hello而言,线性地址标志着 hello 应在内存上哪些详细数据块上运行。
3.线性地址
线性地址是逻辑地址到物理地址变换之间的中间层。代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。
4.假造地址
假造内存为每个程序提供了一个大的、一致的和私有的地址空间。其每个字节对应的地址成为假造地址。假造地址包括 VPO(假造页面偏移量)、VPN(假造页号)、TLBI(TLB 索引)、TLBT(TLB 标记)

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

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

其包罗三部分:索引,TI,RPL
索引:用来确定当前使用的段描述符在描述符表中的位置;
TI:根据TI的值判断选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);
RPL:判断紧张品级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;

段描述符是一种数据结构,等价于段表项,分为两类。一类是用户的代码段和数据段描述符,一类是系统控制段描述符。通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,假造地址。
描述符表:现实上为段表,由段描述符(段表项构成)分为三种范例:
全局描述符表 GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,比方,内核代码段、内核数据段、用户代码段、用户数据段以及 TSS(任务状态段)等都属于 GDT 中描述的段
局部描述符表 LDT:存放某任务(即用户进程)专用的描述符
中断描述符表 IDT:包罗 256 个中断门、陷阱门和任务门描述符

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

页式管理是一种内存空间管理技能,将内存划分成大小相同的页,并将每个进程的假造地址空间也分割成相同大小的页。通过创建假造地址到物理地址的映射关系,页式管理将内存空间划分为多个页面(page frame),并使用页表来管理这些页面的映射关系。硬件地址转换机制负责将假造地址转换为物理地址,从而办理了离散地址转换的问题。页式管理通过请求调页或预调页技能,实现了对内存和外存的同一管理。
7.4 TLB与四级页表支持下的VA到PA的变换

每当CPU生成一个假造地址时,内存管理单位(MMU)都必须检索相应的页表条目(PTE),以便将假造地址转换为物理地址。在最糟糕的情况下,这个过程可能必要额外访问内存一次,耗时几十到几百个周期。如果幸运的话,PTE可能已经缓存在一级缓存(L1)中,如许可以降低1到2个周期的开销。然而,许多系统都在MMU中引入了一个小型的PTE缓存,称为翻译后备缓存(TLB),以实验消除即使是如许的耽误。
多级页表是一种优化方案,将假造地址的假造页号(VPN)分成多个相称大小的部分,每个部分用于查找由上一级确定的页表基址对应的页表条目。
解析VA,利用前m位vpn1寻找一级页表位置,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得PA
7.5 三级Cache支持下的物理内存访问

CPU发送一个假造地址后,MMU执行地址转换,得到物理地址(PA)。PA根据缓存大小和组数要求被分为标记位(CT)、组号(CS)、偏移量(CO)。根据组号CS,查找正确的缓存组,然后比力每个缓存行的标记位是否有效以及标记位是否与CT相匹配。如果命中,则直接返回所需数据;如果未命中,则依次访问更高级别的缓存(如L2、L3),直到主存。如果在任一级别命中,数据将传输给CPU,并更新相应级别的缓存行(如果缓存已满,则使用换入换出策略)。若三级缓存均未命中,则需访问主存,从中取出数据并放入缓存。
7.6 hello进程fork时的内存映射

当前进程调用fork函数时,内核会为新进程创建各种数据结构,并为其分配一个唯一的PID。为了创建新进程的假造内存,内核会复制当前进程的mm_struct、地区结构和页表。如许做的结果是,新进程和调用fork时的进程具有相同的假造内存。内核将两个进程中的页面标记为只读,并将每个地区结构标记为私有的写时复制。因此,当这两个进程中的恣意一个执行写利用时,写时复制机制会创建新的页面,从而维持了每个进程的私有地址空间。
7.7 hello进程execve时的内存映射

execve函数调用内核中的启动加载器代码,该代码加载并运行可执行目标文件hello,有效地替代了当前进程。执行hello程序必要以下步骤:
1. 扫除已存在的用户地区,在当前进程的假造地址空间中删除已存在的地区结构。
2. 创建私有地区,为新程序的代码、数据、bss和栈地区创建新的地区结构。全部这些新地区都是私有的,并接纳写时复制。代码和数据地区映射到hello文件中的.text和.data区,bss地区映射到请求的二进制零大小的匿名文件。栈和堆地址也映射到请求的二进制零,初始长度为零。
3. 映射共享地区,hello程序链接到共享对象libc.so。libc.so是动态链接到该程序中的,然后映射到用户假造地址空间中的共享地区。
4. 设置程序计数器(PC),execve的末了一步是设置当前进程上下文的程序计数器,使其指向代码地区的入口点。
7.8 缺页故障与缺页中断处理惩罚

当发生缺页异常时,控制权会转移到内核的缺页处理惩罚程序。首先,该程序会验证假造地址的合法性。如果地址无效,则会产生段错误,并终止进程。如果地址有效,则处理惩罚程序会在物理内存中选择一个捐躯页。如果该捐躯页已被修改,则将其换出到磁盘,并换入新的页面,并更新页表。处理惩罚程序返回后,CPU会重新执行引发缺页的指令,重新发送假造地址给MMU。由于假造页面如今缓存在物理内存中,所以会命中,主存将所请求的字返回给处理惩罚器。
7.9动态存储分配管理

动态存储分配管理使用动态内存分配器来实现。该分配器维护一个进程的假造内存地区,称为堆,将其视为一组不同大小的块的聚集。每个块都是连续的假造内存片断,可以是已分配的或空闲的。已分配的块明确生存供应用程序使用,而空闲块可用于分配。空闲块保持空闲状态,直到被应用程序显式分配。已分配的块保持已分配状态,直到被应用程序显式释放或内存分配器隐式释放。

动态内存分配紧张有两种根本方法和策略:
1. 带界限标签的隐式空闲链表分配器管理:
在这种管理方式下,每个块由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部构成。
隐式空闲链表通过头部中的大小字段隐含地连接空闲块。分配器可以通过遍历堆中全部块间接遍历整个空闲块聚集。终止头部将作为特殊标记的结束块,其大小字段设置为零。
当应用程序请求一个k字节的块时,分配器搜索空闲链表以找到富足大的空闲块。分配器有三种放置策略:首次适配、下一次适配和最佳适配。分配后,可以分割空闲块以减少内部碎片。释放已分配块时,分配器可以合并空闲块,利用隐式空闲链表的界限标记举行合并。
2. 显式空闲链表管理:
显式空闲链表将空闲块构造为显式数据结构的形式。由于程序不必要空闲块的主体,因此实现该数据结构的指针可以存放在空闲块的主体中。
比方,堆可以构造为双向链表,在每个空闲块中包罗前驱和后继指针。
显式空闲链表可以按后进先出的顺序维护链表,最新释放的块放置在链表的开头。也可以按地址顺序维护链表,确保链表中每个块的地址小于其后继地址。在这种情况下,释放一个块必要线性时间搜索以定位符合的前驱。

7.10本章小结

本章介绍了hello的存储管理机制。讨论了假造地址、线性地址、物理地址,介绍了段式管理与页式管理、VA 到 PA 的变换、物理内存访问,以及 hello 进程 fork 、execve 时的内存映射、缺页故障与缺页中断处理惩罚、动态存储分配管理等。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

在Linux系统中,每个文件都可以看作是一个包罗了m个字节序列(B0,B1,…,Bm-1)的数据结构。这里的“文件”不但仅指磁盘上的文件,还包括了全部的I/O设备,由于在Linux中,统统I/O设备都被抽象为文件。
文件在Linux系统中有多种范例:
1.平凡文件:平凡文件可以包罗恣意数据,它们又分为两种范例:
  文本文件:只包罗ASCII码或Unicode字符的文件。
  二进制文件:包罗任何其他范例的数据,如图像、音频或视频等。
2.目录:目录是一种特殊的文件,它包罗了一组链接的文件。每个链接将一个文件名映射到一个详细的文件。
3.套接字:套接字文件用于在不同进程之间举行跨网络通讯。
在Linux中,全部的输入和输出利用都被视为对相应文件的读取和写入。这种将设备抽象为文件的优雅计划使得Linux内核能够提供一个简朴而低级的应用接口,即Unix I/O。这个接口使得应用程序能够以一种同一且一致的方式来执行全部的输入和输出利用。
8.2 简述Unix IO接口及其函数

Unix I/O 接口:
Unix I/O 接口是利用文件的基础。每当一个文件被打开时,内核会返回一个非负整数的文件描述符,通过该描述符可以对文件执行各种利用。Linux shell创建的每个进程在启动时都会自动打开三个文件:尺度输入(文件描述符为0)、尺度输出(描述符为1)和尺度错误输出(描述符为2)。为了更方便地使用这些描述符,头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可以代替显式的描述符值。
通过 Unix I/O 接口,应用程序可以改变当前文件位置。文件的偏移量被视为当前位置,应用程序可以通过 seek 利用将文件的当前位置设置为恣意位置 k。
在举行读写利用时,读取利用会从文件的当前位置 k 开始将 n 个字节复制到内存中,并将当前位置 k 增加到 k+n;写入利用会从内存中复制 n 个字节到文件中,并更新当前文件位置为 k。
末了,在应用程序完成对文件的访问后,必要关照内核关闭该文件。内核会释放文件打开时创建的相关数据结构,并将文件描述符规复到描述符池中,以便其他程序使用。
Unix IO函数:
1.函数原型:int open(char *filename, int flags, mode_t mode);
open 函数将文件名 filename 转换为一个文件描述符,并返回该描述符。返回的描述符总是当前进程中最小的未打开描述符,flags 参数指定进程对文件的访问方式,而 mode 参数指定新文件的访问权限。
2.函数原型:int close(int fd);
close 函数用于关闭一个已打开的文件。
3.函数原型:ssize_t read(int fd, void *buf, size_t n);
read 函数从文件描述符 fd 的当前位置读取最多 n 个字节到内存位置 buf。返回值为-1 表示出错,0 表示已到达文件末尾(EOF),否则返回值表示现实传输的字节数目。
4.函数原型:ssize_t write(int fd, const void *buf, size_t n);
write 函数将内存位置 buf 中的最多 n 个字节写入到文件描述符 fd 的当前位置

8.3 printf的实现分析

系统下的printf函数如下:
int printf(const char *fmt, ...)
{
    int i;
    va_list arg = (va_list)((char *)(&fmt) + 4);
    i = vsprintf(buf, fmt, arg);
    write(buf, i);
    return i;
}
所引用的vsprintf函数如下:
int vsprintf(char *buf, const char *fmt, va_list args)
{
    char *p;
    chartmp[256];
    va_listp_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 函数的功能是按照指定的格式将 printf 的参数解析,并将生成的字符串存储在 buf 中,末了返回生成字符串的长度。
接下来是 write 系统函数的介绍。在 Linux 下,write 函数的第一个参数是 fd,即文件描述符,其中尺度输出的描述符为 1。通过检察 write 函数的汇编实现,可以观察到它首先将参数转达给寄存器,然后执行 int INT_VECTOR_SYS_CALL,这表示通过系统调用 syscall。在 syscall 的过程中,寄存器中的数据通过总线传输到显卡的显存中。显示芯片会按照刷新频率逐行读取显存中的数据,并通过信号线将每个像素的 RGB 分量传输到液晶显示器。如许,write 函数能够将格式化后的字符串显示在屏幕上。
字符显示驱动子程序负责从 ASCII 码到字模库再到显示显存(其中存储了每个像素的 RGB 颜色信息)的转换和管理。显示芯片则负责按照指定的刷新频率从显存中读取数据,并将每个像素的颜色信息传输到液晶显示器。


8.4 getchar的实现分析

getchar的代码如下:
int getchar(void)
{
static char buf[BUFSIZ];
static char* bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return(--n>=0)?(unsigned char)*bb++:EOF;
}
getchar 函数返回一个整型值。当程序调用 getchar 时,程序会等待用户按键。用户输入的字符会被存放在键盘缓冲区中,直到用户按下回车键(回车字符也会放在缓冲区中)。
一旦用户按下回车键,getchar 函数才会开始从尺度输入流中每次读取一个字符。getchar 函数的返回值是用户输入的第一个字符的 ASCII 码,如果出错则返回 -1,并且将用户输入的字符回显到屏幕上。如果用户在按下回车键之前输入了多个字符,那么其他字符会生存在键盘缓冲区中,等待后续的 getchar 调用读取。
换句话说,后续的 getchar 调用不会等待用户再次按键,而是直接读取缓冲区中的字符,直到缓冲区中的字符全部被读取完毕,才会再次等待用户按键输入。
异步异常-键盘中断的处理惩罚:键盘中断处理惩罚子程序。担当按键扫描码转成ascii码,生存到系统的键盘缓冲区。

8.5本章小结

本章紧张介绍了linux的IO设备管理方法和及其接口和函数,对printf函数和getchar函数的底层实现有了根本相识。

结论

hello程序的生命周期可以概括如下:


1. 预处理惩罚:将 hello.c 中包罗的全部外部头文件内容直接插入程序文本中,完成字符串的替换,以便后续处理惩罚。


2. 编译:通过词法分析和语法分析,将合法指令翻译成等价的汇编代码。编译器将 hello.i 翻译成汇编语言文件 hello.s。


3. 汇编:将 hello.s 汇编程序翻译成机器语言指令,并将这些指令打包成可重定位目标程序格式,最闭幕果生存在 hello.o 目标文件中。


4. 链接:通过链接器,将 hello 程序的代码与动态链接库等整合成一个单一文件,生成完全链接的可执行目标文件 hello。


5. 加载运行:在Shell中键入下令,终端为其fork新建进程,并通过execve将代码和数据加载入假造内存空间,程序开始执行。


6. 执行指令:当该进程被调理时,CPU为 hello 分配时间片,在一个时间片中,hello 获得CPU的全部资源。PC寄存器逐步更新,CPU不停取指,顺序执行自身的控制逻辑流程。


7. 访存:内存管理单位(MMU)将逻辑地址逐步映射成物理地址,通过三级高速缓存系统访问物理内存/磁盘中的数据。


8. 动态申请内存:printf 调用 malloc 向动态内存分配器申请堆中的内存。


9. 信号处理惩罚:进程时刻等待信号,如果在运行过程中键入 ctrl-c 或 ctrl-z,则调用Shell的信号处理惩罚函数执行克制或挂起等利用;对于其他信号也有相应的利用。


10. 终止并被接纳:Shell父进程等待并接纳 hello 子进程,内核删除为 hello 进程创建的全部数据结构。


计算机系统是无数人聪明的结晶,也是现代科技不停创新的来源。在这门课我学到了很多东西,感谢一学期以来老师的耐心讲解和辛劳付出。计算机系统的内容很巨大,一个学期的功夫是远远不够的,希望以后偶然间能够借此为引导,不停深入的去相识学科的本质内容,做一名优秀的工程师。


附件

hello.c
hello源代码
hello.i
预处理惩罚之后的文本文件
hello.s
hello的汇编代码
hello.o
hello的可重定位文件
hello
hello的可执行文件
hello.elf
hello的elf文件
hello1.elf
hello.o的elf文件
hello.asm
反汇编hello.o得到的反汇编文件
hello1.asm
反汇编hello得到的反汇编文件

参考文献

[1]  大卫R.奥哈拉伦,兰德尔E.布莱恩特. 深入理解计算机系统[M]. 机械工业出书社,2016.
[2]  空想之家xiao_chen.ELF文件头更详细结构[EB/OL].2017[2021-6-10].
https://blog.csdn.net/qq_32014215/article/details/76618649.




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

使用道具 举报

0 个回复

倒序浏览

快速回复

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

本版积分规则

小小小幸运

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

标签云

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