哈工大计算机系统大作业-步伐人生-Hello’s P2P

打印 上一主题 下一主题

主题 550|帖子 550|积分 1650

 
 
 
计算机系统
 
大作业
 
 
题     目  步伐人生-Hellos P2P  
专       业   人工智能模块                     
学     号   2022112531                     
班     级   22WL028                     
学       生   陈宇轩                  
指 导 教 师   郑贵滨                    
 
 
 
 
 
 
计算机科学与技术学院
20245
摘  要
本文以hello.c步伐为例,深入剖析了Ubuntu环境下C语言步伐的编译、链接、执行过程,以及操作系统的内存与IO设备管理方法。编译过程包罗预处置惩罚、编译和汇编三个阶段,每个阶段都有特定的下令和效果,对步伐的结构和功能产生深远影响。链接阶段则将各个目标文件合并成可执行文件,涉及地址空间的重定位等复杂操作。
    步伐执行时,操作系统负责历程创建、调理和资源分配。历程在执行过程中会经历用户态与核心态的切换,特别是在进行系统调用时。别的,操作系统通过内存映射和动态存储分配管理内存资源,确保步伐能够高效、安全地运行。当发生缺页故障时,操作系统会及时加载页面并更新页表,以包管步伐的连续执行。
    在IO设备管理方面,Linux系统提供了丰富的接口和策略,使得步伐能够方便地与外部设备进行交互。比方,通过Unix IO接口,步伐可以实现数据的输入和输出操作。
    综上所述,本文详细剖析了C语言步伐的编译和执行过程,深入探讨了操作系统的内存和IO设备管理方法,深入剖析了hello的P2P、020过程。
 
关键词:步伐;预处置惩罚;编译;汇编;链接;历程管理;存储管理; IO管理                        
 
 
 
 
 
 
 
 
目  录
 
第1章 概述
1.1 Hello简介
1.2 环境与工具
1.3 中间效果
1.4 本章小结
第2章 预处置惩罚
2.1 预处置惩罚的概念与作用
2.2在Ubuntu下预处置惩罚的下令
2.3 Hello的预处置惩罚效果剖析
2.4 本章小结
第3章 编译
3.1 编译的概念与作用
3.2 在Ubuntu下编译的下令
3.3 Hello的编译效果剖析
3.4 本章小结
第4章 汇编
4.1 汇编的概念与作用
4.2 在Ubuntu下汇编的下令
4.3 可重定位目标elf格式
4.4 Hello.o的效果剖析
4.5 本章小结
第5章 链接
5.1 链接的概念与作用
5.2 在Ubuntu下链接的下令
5.3 可执行目标文件hello的格式
5.4 hello的虚拟地址空间
5.5 链接的重定位过程分析
5.6 hello的执行流程
5.7 Hello的动态链接分析
5.8 本章小结
第6章 hello历程管理
6.1 历程的概念与作用
6.2 简述壳Shell-bash的作用与处置惩罚流程
6.3 Hello的fork历程创建过程
6.4 Hello的execve过程
6.5 Hello的历程执行
6.6 hello的异常与信号处置惩罚
6.7本章小结
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.3 Hello的线性地址到物理地址的变换-页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
7.6 hello历程fork时的内存映射
7.7 hello历程execve时的内存映射
7.8 缺页故障与缺页中断处置惩罚
7.9动态存储分配管理
7.10本章小结
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.2 简述Unix IO接口及其函数
8.3 printf的实现分析
8.4 getchar的实现分析
8.5本章小结
结论
附件
参考文献
 
 
第1章 概述

1.1 Hello简介

根据Hello的自白,可以概括P2P过程如下:步伐员编写好C语言步伐后,颠末以下四个步骤将其转化为可执行步伐:预处置惩罚、编译、汇编、链接。首先颠末预处置惩罚器cpp天生hello.i文件,然后颠末编译器ccl天生hello.s汇编步伐,接下来颠末汇编器as天生hello.o文件,最后颠末链接器ld天生可执行文件hello。然后shell系统调用execve等函数创建新历程而且把步伐内容加载,从而实现步伐到历程的转化,这就是P2P过程。
020可概括如下:一开始可以将步伐视为“0”。步伐运行前,shell会调用execve函数加载hello步伐,将步伐内容载入物理内存;在步伐运行结束后,shell父历程回收历程,释放虚拟内存空间,删除相关数据,又回到了“0”的状态,这就是020过程。
1.2 环境与工具

硬件环境:X64 CPU;3.2GHz;16G RAM
软件环境:Windows11 64位;VMware Workstation Pro 17;Ubuntu 20.04.4
开发和调试工具:gcc;objdump;edb;CodeBlocks20.03
1.3 中间效果

hello.i:hello.c预处置惩罚后的文件,可用于编译。
hello.s:hello.i编译后的文件,可用于汇编。
hello.o:hello.s汇编后的文件,可用于链接。
hello:hello.o链接后的文件,可用于执行。
1.4 本章小结

    本章报告了hello.c的P2P和020的过程,介绍了完成这次作业所使用的环境和工具,列出了产生的中间效果。
 
第2章 预处置惩罚

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

步伐预处置惩罚是指在编译或执行步伐之前,对源代码或数据进行的一系列处置惩罚和转化的过程。步伐预处置惩罚的目的是为了使源代码或数据顺应特定的编译器或运行环境,提高步伐的可读性和可维护性。
步伐预处置惩罚的重要作用包罗:
1.条件编译:根据特定的条件,选择性地编译或忽略某些代码块,实今世码的可设置和可扩展。
2.宏更换:将某个名称或表达式更换为另一个名称或表达式,简化代码和提高可读性。
3.文件包含:将其他文件的代码合并到当前文件中,实今世码的重用和模块化。
4.范例转换:将一种数据范例转换为另一种数据范例,使数据顺应特定的编译器或运行环境。
5.错误处置惩罚:捕捉和处置惩罚步伐运行时的错误和异常,提高步伐的可靠性和稳定性。
2.2在Ubuntu下预处置惩罚的下令

使用下令gcc -E hello.c -o hello.i 天生hello.i文件,如下图所示。
 
图2.2.1 Ubuntu下的预处置惩罚下令
2.3 Hello的预处置惩罚效果剖析

由上图可以看出,预处置惩罚后天生了hello.i文件,共3061行。第一张图显示的是步伐用到的头文件,第二张图显示了使用typedef界说变量名的别名,第三张图是hello.i的最后部分,可以看出与hello.c完全相同。可以注意到预处置惩罚后所有的注释已经消失了。
 
图2.3.1 步伐用到的头文件
 
图2.3.2 变量名别名界说
 
图2.3.3 hello.i的结尾与hello.c完全相同的部分
2.4 本章小结

本章介绍了预处置惩罚的概念与作用,给出了在Ubuntu下进行预处置惩罚的下令,剖析了预处置惩罚的效果。
第3章 编译

3.1 编译的概念与作用

编译是指将源代码(此处为hello.i)转换为目标代码(此处为hello.s)的过程。编译的作用在于将人类易于理解和编写的源代码转换为计算机可以执行的目标代码。编译后的汇编语言步伐可以被汇编器进一步转换为呆板语言步伐,最终由计算机的CPU执行。
3.2 在Ubuntu下编译的下令

    使用gcc -S hello.i -o hello.s下令进行编译,得到hello.s文件,如下图所示。
 
图3.2.1 Ubuntu下的编译下令
3.3 Hello的编译效果剖析

3.3.1 常量
3.3.1.1 字符串常量
.LC0 和 .LC1 界说了字符串常量。比方,.LC0 存储了一个中文字符串,用于输出特定的消息。
 
图3.3.1 hello.s中的字符串常量
3.3.1.2 数值型常量
以立刻数的情势展现,
 
图3.3.2 hello.s中的数值型常量
3.3.2 变量
hello.s中只出现了局部变量,为int i,存储在栈中,如 -4(%rbp) 存储了一个局部变量。
 
 
图3.3.3 局部变量的存储
3.3.3 赋值操作
在汇编代码 hello.s 中,有几个赋值操作的实例。下面是对这些赋值操作的分析:
3.3.3.1函数参数的赋值:
movl %edi, -20(%rbp):将寄存器 %edi 中的值(通常是 main 函数的第一个参数,即 argc)存储到栈上的 -20(%rbp) 位置。
movq %rsi, -32(%rbp):将寄存器 %rsi 中的值(通常是 main 函数的第二个参数,即 argv)存储到栈上的 -32(%rbp) 位置。
3.3.3.2局部变量的赋值:
movl $0, -4(%rbp):将立刻数 0 存储到栈上的 -4(%rbp) 位置,这通常用于初始化一个局部变量。
3.3.3.3循环计数器的赋值:
addl $1, -4(%rbp):将栈上的 -4(%rbp) 位置的值增加 1,用于循环计数器的增量。
3.3.3.4函数返回值的赋值:
movl $0, %eax:将立刻数 0 存储到寄存器 %eax 中,这是 main 函数的返回值。
3.3.3.5是否赋初值
关于是否赋了初值,我们可以看到:
-20(%rbp) 和 -32(%rbp) 存储的是 main 函数的参数,这些参数在函数调用时已经由调用者设置,因此它们不是由 main 函数内部赋初值的。
-4(%rbp) 在 main 函数内部被赋了初值 0,这是通过 movl $0, -4(%rbp) 指令实现的。总结来说,汇编代码中的赋值操作包罗函数参数的存储、局部变量的初始化和增量操作,以及函数返回值的设置。其中,局部变量 -4(%rbp) 被明确地赋了初值 0。
 
3.3.4 范例转换(隐式或显式)
    hello.s中存在范例转换,通过atoi@PLT来实现,将字符串转化为整数。
 
图3.3.4 范例转换
3.3.5 算术操作
    hello.s中涉及了多种算术操作:
加法操作:
addq 24, %rax:将 %rax 寄存器的内容加上 24。
addq 8, %rax:将 %rax 寄存器的内容加上 8。
addq 32, %rax:将 %rax 寄存器的内容加上 32。
减法操作:
subq $32, %rax:这个指令执行减法操作,从寄存器 %rax 的值中减去 32。
比较操作:
cmpl 9, -4(%rbp):比较 -4(%rbp) 内存位置的内容和 9 的大小。
自增操作:
addl 1, -4(%rbp):等同于自增操作,将 -4(%rbp) 内存位置的内容加 1。
 
图3.3.5 算术操作
3.3.6 关系操作
hello.s中涉及了关系操作,虽然没有直接使用关系操作符,但是通过cmpl和其他跳转指令来实现。
等于:
 
图3.3.6 等于操作
小于等于:
 
图3.3.7 小于等于操作
3.3.7 数组操作
    hello.s中涉及了数组操作,通过movq和addq指令来实现。对数组的操作是先找到数组的首地址,然后加上偏移量。在main中,调用了argv[1],argv[2],argv[3],在汇编代码中,每次将%rbp-32的的值即数组首地址传%rax,然后将%rax分别加上偏移量24,16,8,得到argv[1]、argv[2]、argv[3],分别存入对应的寄存器%rcx、%rdx、%rsi作为第一个参数、第二个参数和第三个参数,之后调用printf函数时使用。调用完printf后,在偏移量为32时,取出一个参数在调用函数atoi使用。
 
 
图3.3.8 数组操作
3.3.8 控制转移
hello.s中涉及了控制转移操作,涉及了if和for循环,if体现在比较5和-20(%rbp)的大小,即检查argc是否为5,如果相称则会跳转到.L2位置。for循环体现在每次循环结束使用cmpl指令判断循环条件,满足则继续,否则退出循环。
 
图3.3.9 if的使用
 
图3.3.10 for的使用
 
3.3.9 函数操作
    hello.s涉及的函数操作如下:
1.参数传递(地址/值):
在.L4标签处,有movq -32(%rbp),%rax、addq $24,%rax和movq (%rax),%rcx这几条指令,这表明它从内存地址-32(%rbp)+24中加载了一个值到%rcx寄存器中。这个值可能是一个参数传递给背面的printf@PLT函数。
在.L4标签处,有movq -32(%rbp),%rax、addq $16,%rax和movq (%rax),%rdx这几条指令,这表明它从内存地址-32(%rbp)+16中加载了一个值到%rdx寄存器中。这个值可能是一个参数传递给背面的printf@PLT函数。
在.L4标签处,有movq -32(%rbp),%rax和addq $8,%rax这几条指令,这表明它从内存地址-32(%rbp)+8中加载了一个值到%rax寄存器中。这个值可能是一个参数传递给背面的printf@PLT函数。
2.函数调用:
在.L4标签处,有leaq .LC1(%rip),%rdi、movl $0,%eax和call printf@PLT这几条指令,这表明它调用了printf@PLT函数。
在.L4标签处,有movq -32(%rbp),%rax和call atoi@PLT这几条指令,这表明它调用了atoi@PLT函数。
在.L4标签处,有movq -32(%rbp),%rax和call sleep@PLT这几条指令,这表明它调用了sleep@PLT函数。
在main函数的末端,有call getchar@PLT这条指令,这表明它调用了getchar@PLT函数。
3.局部变量:
在main函数的开头,有subq $32,%rsp这条指令,这表明它在内存中分配了32字节的空间作为局部变量。
在.L4标签处,有movl $0,-4(%rbp)这条指令,这表明它在内存地址-4(%rbp)中存储了一个值0,这是一个局部变量。
4.函数返回:
在main函数的末端,有movl $0,%eax这条指令,这表明它将返回值存储在%eax寄存器中。
在main函数的末端,有leave和ret这两条指令,这表明它已经结束了函数并返回了。
 
图3.3.11 函数操作
3.4 本章小结

本章报告了编译的概念和作用,给出了Ubuntu下的编译下令,并对编译效果进行了详细的剖析。


第4章 汇编
4.1 汇编的概念与作用

汇编是把汇编步伐翻译为呆板语言的过程,便于计算机的执行,在此处是指将hello.s转化为hello.o的过程。汇编的作用为将汇编代码转酿成可以执行的指令,天生目标文件,方便呆板直接分析。
4.2 在Ubuntu下汇编的下令

在Ubuntu下使用gcc -C hello.s -o hello.o下令使用hello.s转化为hello.o文件,实现汇编。
 
图4.2.1 Ubuntu下的汇编过程
4.3 可重定位目标elf格式

hello.o的elf格式如下:
 
 
ELF头
.text
.rodata
.data
.bss
.symtab
.rel.text
.rel.data
.debug
.line
.strlab
节头部表
使用readelf -h hello.o下令查看ELF头如下:
 
图4.3.1 ELF头
使用readelf -S hello.o下令查看节头表如下:
 
图4.3.2 节头表
 
图4.3.3 节头表
 
图4.3.4 节头表
 
图4.3.5 节头表
使用readelf -s hello.o下令查看符号表如下:
 
图4.3.6 符号表
 
图4.3.7 符号表
 
 
图4.3.8 符号表
 
图4.3.9 符号表
 
图4.3.10 符号表
使用readelf -r hello.o下令查看重定位节如下:
 
图4.3.11 重定位节
 
图4.3.12 重定位节
4.4 Hello.o的效果剖析

使用objdump -d -r hello.o 下令对hello.o进行反汇编,如下图所示。
 
图4.4.1 hello.o反汇编
 
图4.4.2 hello.o反汇编
 
图4.4.3 hello.o反汇编
 
图4.4.4 hello.o反汇编
 
图4.4.5 hello.o反汇编
 
图4.4.6 hello.o反汇编
 
图4.4.7 hello.o反汇编
 
图4.4.8 hello.o反汇编
 
图4.4.9 hello.o反汇编
汇编语言中每个呆板码与操作数对应一个01序列,呆板语言由01构成,操作数在汇编语言中为十进制,在呆板语言中为十六进制0x。
hello.o反汇编文件与hello.s汇编代码部分相同,但反汇编文件多了呆板代码,每一条呆板代码对应一条呆板指令。
分支转移:汇编语言使用跳转指令(如je .L2)决定跳转,呆板语言直接使用对应地址实现跳转。
函数调用:汇编语言直接使用函数名,呆板语言中call目标地址为当前指令的下一条指令地址。对于不确定地址的调用,呆板语言会先将下一条指令的相对地址设置为0,在链接时确定地址。
4.5 本章小结

本章首先介绍了汇编的概念和作用,接着对hello.s文件进行汇编天生hello.o,接着使用readelf工具查看了hello.o的ELF头、节头表、符号表和重定位节。在反汇编后,将反汇编效果与hello.s比较,分析了差别之处和映射关系。
5链接

5.1 链接的概念与作用

链接的概念:链接器将多个可重定位目标文件、外部库文件等各种代码和数据的片断组合成一个可执行文件或库文件的过程。
链接的作用:
1.链接使得分离编译成为可能,这意味着我们可以将一个大的步伐分解为多个小的模块进行独立编译,从而提高了编译的效率和便利性。当我们需要修改某个模块时,只需要重新编译该模块,而无需重新编译整个工程。
2.链接有利于共享库的构建。共享库是一种可重用的代码库,它可以被多个步伐共享使用,从而减少了代码的重复开发和存储。链接器可以将步伐链接到相应的共享库,从而实今世码的共享和重用。
3.链接可以办理目标文件之间的符号引用,为外部函数和变量分配实际的内存地址,使得步伐能够正确地执行。
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.2.1 Ubuntu下的链接下令
5.3 可执行目标文件hello的格式

hello的ELF格式如下所示:
ELF头
步伐头表
.init
.text
.rodata
.data
.bss
.symtab
.debug
.line
.strtab
节头部表
   使用readelf -h hello下令查看ELF头如下:
 
图5.3.1 ELF头
使用readelf -S hello下令查看节头表如下:
 
图5.3.2 节头表
 
图5.3.3 节头表
使用readelf -s hello下令查看符号表如下:
 
图5.3.4 符号表
 
图5.3.5 符号表
使用readelf -r hello下令查看重定位节如下:
 
图5.3.6 重定位节
    各段的起始地址,大小等信息如上图所示。
5.4 hello的虚拟地址空间

使用edb加载hello查看本历程的虚拟地址空间各段信息如下图所示,从0x0000000000401000开始,到0x0000000000402000结束。
 
图5.4.1 hello的虚拟地址空间各段信息
 
图5.4.2 .init的信息
 
图5.4.3 .rodata的信息
    .init的起始地址是0x0000000000401000,与hello的起始地址相同,.rodata的起始地址是0x0000000000402000,与hello的结束地址相同。
5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的差别,说明链接的过程。
团结hello.o的重定位项目,分析hello中对其怎么重定位的。
 
图5.5.1 hello反汇编
 
图5.5.2 hello反汇编
 
图5.5.3 hello反汇编
 
图5.5.4 hello反汇编
 
图5.5.5 hello反汇编
可以看出,在hello.o中跳转指令和call指令后为绝对地址,而在hello中是重定位之后的虚拟地址。hello中还多了很多重定位之后的函数,如多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt,atoi@plt等,hello.o在.text段之后只有main函数。
链接的过程如下:

  • 符号剖析:在步伐中,界说和引用的符号(如函数和全局变量)存储在符号表(.symtab 节)中,它是一个结构数组。编译器将符号的引用存储在重定位节(.rel.text 和 .rel.data 节)中。链接器则将每个符号引用与确定的符号界说关联起来,实现符号剖析。
 

  • 重定位:链接器将多个代码段和数据段分别合并为一个完整的代码段和数据段。此时,计算每个界说的符号在虚拟地址空间的绝对地址,而不是相对偏移量。接着,链接器将可执行文件中的符号引用处修改为重定位后的地址信息,完成重定位过程。
接下来以atoi函数为例介绍hello的重定位过程。
 
图5.5.6 atoi函数
    由图可知,当前PC的值为callq指令的下一条地址,即0x4011a5,atoi函数的地址为0x4010c0,两者相差0xe5,因此PC需要减去0xe5,即加上0xff ff ff 1b,因为是小端序,所以重定位目标处应填上1b ff ff ff,与图中0x4011a0行的效果一致。
5.6 hello的执行流程

hello调用与跳转的各个子步伐名及步伐地址如下表所示:
子步伐名
hello!_start
libc-2.31.so!__libc_start_main
hello!_init
hello!main
hello!printf@plt
hello!atoi@plt
hello!sleep@plt
hello!getchar@plt
hello!exit@plt
hello!__libc_csu_init
hello!_fini
 
5.7 Hello的动态链接分析

根据.got的地址,在edb中进行查看。
 
图5.7.1 .got的地址
 
图5.7.2 dl_init前的内容
    
图5.7.3 dl_init后的内容
    可以看到,在dl_init后,.got地址处的内容发生了改变,这是因为动态链接器通过过程链接表和全局偏移量表协作。全局偏移量表存储函数的绝对目标地址,过程链接表使用这些地址进行跳转。在加载时,动态链接器会修正全局偏移量表,将每个条目的地址变为指向实际目标函数的绝对地址,实现动态链接。
 
5.8 本章小结

本章介绍了链接的概念和作用,以hello为例,分析了可执行文件的ELF格式、虚拟地址空间,将hello的反汇编文件与hello.o的反汇编文件进行比较,并分析了重定位的过程与动态链接的过程。
 
6hello历程管理

6.1 历程的概念与作用

历程的概念:是计算机系统中执行步伐的基本单位,它包罗步伐代码、数据和历程控制信息。
历程的作用:历程可以并发执行,通过操作系统管理和调理,实现资源的有用使用和任务的并行处置惩罚。历程间通过通讯机制互换信息,协作完成复杂任务,提高系统效率和相应速率。
6.2 简述壳Shell-bash的作用与处置惩罚流程

Shell-bash 是一种下令行解释器,重要用于与操作系统内核通讯,允许用户输入下令并执行。它的作用包罗:
1.下令解释:接收用户输入的下令,解释并执行。
2.脚本执行:可以运行包含一系列下令的脚本文件。
3.环境设置:提供设置文件来设置用户环境变量和别名。
4.步伐执行管理:管理后台历程、作业控制等。
处置惩罚流程大致如下:
1.启动:用户通过终端启动bash。
2.读取下令:bash等待用户输入下令。
3.剖析下令:分析下令行,确定下令和参数。
4.执行下令:查找并执行下令,可能涉及系统调用。
5.效果输出:将下令执行效果输出到终端。
6.循环处置惩罚:重复上述步骤,直到用户退出bash。
6.3 Hello的fork历程创建过程

在shell中,当输入./hello下令时,shell下令行解释器首先剖析这个下令,将其构建成一个环境变量数组envp(环境变量列表)和一个下令行参数数组argv(下令参数列表)。接着,它调用内核中的fork()系统调用。
fork()函数在父历程中执行,创建了一个新的子历程。这个过程是复制式的,子历程会从父历程获取一个险些完全相同的虚拟地址空间副本,包罗代码段(代码执行地区)、数据段(静态数据和全局变量)、堆(动态分配的内存)以及用户栈。共享库(如动态链接的库)也会被复制,但每个历程有自己的独立副本,以制止并发访问时的辩说。
子历程和父历程之间的关键区别在于,它们虽然共享相同的可执行代码,但数据和堆栈是独立的,这意味着它们可以拥有各自独立的状态。别的,fork()函数在父历程中返回的是子历程的PID(历程标识符),而子历程返回的是0。这个PID值使得我们可以区分是父历程还是子历程,因为它们的返回值差别。
总结来说,父历程通过fork()创建了一个新的子历程,它们共享代码,但拥有独立的数据和堆栈,以及各自的PID。如许,每个历程都有自己的生命周期,互不影响,但可以协同工作。父历程和子历程在执行过程中,通过PID来确认自己的身份和相互之间的关系。
6.4 Hello的execve过程

可执行步伐hello的execve过程如下:
1.父历程调用fork()创建一个子历程。
2.子历程调用execve()函数,传入可执行文件hello的路径名作为第一个参数,一个指向参数列表的指针作为第二个参数,以及一个指向环境变量列表的指针作为第三个参数。
3.execve()函数调用内核中的加载器,加载器负责将hello步伐加载到子历程的虚拟地址空间中。
4.加载器首先删除子历程现有的虚拟内存段,然后创建一组新的段,包罗代码段、数据段、BSS段、堆和栈等。
5.加载器将hello步伐的代码和数据段加载到子历程的虚拟地址空间中,并将BSS段初始化为0。
6.加载器将虚拟地址空间中的页映射到可执行文件的页大小的片。
7.加载器跳转到hello步伐的开头_start,开始执行hello步伐。
8.如果execve()函数执行乐成,它将不会返回,因为子历程已经开始执行hello步伐。如果execve()函数执行失败,它将返回-1,并将错误代码存储在errno变量中。
总之,execve过程是将一个可执行文件加载到子历程的虚拟地址空间中,并开始执行该步伐的过程。在这个过程中,加载器负责将步伐的代码和数据加载到内存中,并将虚拟地址空间映射到可执行文件的内存中。
6.5 Hello的历程执行

可执行步伐hello的历程执行过程如下:
1.父历程调用fork()创建一个子历程,并将hello步伐的路径名传递给子历程。
2.子历程调用execve()函数,将hello步伐加载到虚拟地址空间中,并开始执行hello步伐。
3.历程上下文信息包罗历程的步伐计数器(PC)、寄存器、内存映射和其他状态信息。在执行hello步伐时,子历程的历程上下文信息会被加载到CPU寄存器中,以便于执行。
4.历程调理是指操作系统在多个历程中分配CPU时间片的过程。当hello历程使用完CPU时间片后,操作系统会将其停息并切换到另一个历程。
5.在用户态和核心态之间转换是指历程在执行用户代码和系统调用时所经历的状态转换。当历程执行系统调用时,它会从用户态切换到核心态,并在完成系统调用后再次返回到用户态。
6.在执行hello步伐时,历程可能会进行多次用户态和核心态的转换,比方在进行文件IO操作时会进行系统调用,从而进入核心态。
7.当hello历程执行完毕后,它会释放所有资源并退出,父历程可以选择等待子历程退出并获取其结束状态。
总之,可执行步伐hello的历程执行过程包罗历程上下文信息、历程调理、用户态和核心态之间的转换等等。在执行过程中,历程会被操作系统调理并分配CPU时间片,并在用户态和核心态之间转换以执行系统调用。当hello历程执行完毕后,它会释放所有资源并退出。
6.6 hello的异常与信号处置惩罚

hello的运行过程中可能出现中断、陷阱、故障、终止四种异常。
hello的运行过程中可能产生的信号如下表所示:
 
 
 
 
 
 
 
 
 
 
 
 
 
编号
信号名称
缺省动作
说明
1
SIGHUP
终止
终止控制终端或历程
2
SIGINT
终止
键盘产生的中断(Ctrl-C)
3
SIGQUIT
dump
键盘产生的退出
4
SIGILL
dump
非法指令
5
SIGTRAP
dump
debug中断
6
SIGABRT/SIGIOT
dump
异常中止
7
SIGBUS/SIGEMT
dump
总线异常/EMT指令
8
SIGFPE
dump
浮点运算溢出
9
SIGKILL
终止
逼迫历程终止
10
SIGUSR1
终止
用户信号,历程可自界说用途
11
SIGSEGV
dump
非法内存地址引用
12
SIGUSR2
终止
用户信号,历程可自界说用途
13
SIGPIPE
终止
向某个没有读取的管道中写入数据
14
SIGALRM
终止
时钟中断(闹钟)
15
SIGTERM
终止
历程终止
16
SIGSTKFLT
终止
协处置惩罚器栈错误
17
SIGCHLD
忽略
子历程退出或中断
18
SIGCONT
继续
如历程克制状态则开始运行
19
SIGSTOP
克制
克制历程运行
20
SIGSTP
克制
键盘产生的克制
21
SIGTTIN
克制
后台历程哀求输入
22
SIGTTOU
克制
后台历程哀求输出
23
SIGURG
忽略
socket发生告急情况
24
SIGXCPU
dump
CPU时间限制被打破
25
SIGXFSZ
dump
文件大小限制被打破
26
SIGVTALRM
终止
虚拟定时时钟
27
SIGPROF
终止
profile timer clock
28
SIGWINCH
忽略
窗口尺寸调整
29
SIGIO/SIGPOLL
终止
I/O可用
30
SIGPWR
终止
电源异常
31
SIGSYS/SYSUNUSED
dump
系统调用异常
 
 
图6.6.1 hello执行过程(乱按+回车)
由图可知,在hello的执行过程中乱按和回车对其没有任何影响。
 
图6.6.2 hello执行过程(Ctrl-C)
由图可知,在hello的执行过程中按下Ctrl-C使历程直接克制。
 
图6.6.3 hello执行过程(Ctrl-Z+ps+jobs)
由图可知,在hello的执行过程中按下Ctrl-Z历程也会克制。
 
图6.6.4 hello执行过程(pstree)
由图可知,按下Ctrl-Z后,继续输入ps,jobs,pstree下令后可以实现对应功能。
 
图6.6.5 hello执行过程(pstree)
 
图6.6.6 hello执行过程(pstree)
 
图6.6.7 hello执行过程(fg)
由图可知,按下Ctrl-Z后,继续输入fg下令可以使步伐恢复执行。
 
图6.6.8 hello执行过程(kill)
由图可知,输入kill下令杀死指定历程后,该历程被乐成杀死。
6.7本章小结

本章介绍了历程的概念和作用,Shell-bash的作用,以及fork历程创建过程、execve过程,描述了历程的执行和hello的异常和信号处置惩罚。
7hello的存储管理

7.1 hello的存储器地址空间

在讨论hello步伐的上下文中,逻辑地址是指CPU天生的、步伐中使用的地址,如hello.o文件中的偏移量。这些地址需要通过寻址计算才能转换为内存中的实际有用地址。
线性地址是逻辑地址颠末分段和分页处置惩罚后的效果,它是处置惩罚器可寻址的内存空间(线性地址空间)中的地址。在hello步伐中,逻辑地址加上段基址形成线性地址。若启用分页机制,线性地址进一步转换为物理地址;否则,线性地址自己就是物理地址。
虚拟地址是虚拟存储技术中使用的地址,它也是步伐中使用的地址,但在物理内存中没有直接对应的位置。操作系统将虚拟地址映射到物理地址,以优化内存使用。在分段机制中,逻辑地址加上基地址得到线性地址,通常是一个32位无符号整数,可以表示4GB的地址空间。线性地址通常以十六进制表示。hello步伐的反汇编文本文件中的地址即为虚拟地址,也是线性地址。
物理地址是CPU通过地址总线访问内存时使用的实际存在的地址。操作系统将虚拟地址映射到物理地址,以实现内存的有用管理。物理地址重要用于访问内存条中的内存,但也可能映射到其他存储器,如显存、BIOS等。在hello步伐中,反汇编得到的文本文件中的地址反映了这些地址层次结构的信息。
7.2 Intel逻辑地址到线性地址的变换-段式管理

Intel的段式管理将逻辑地址分为两部分:段基址和段内偏移地址。段基址是由操作系统分配的,用于定位内存中的段起始地址。段内偏移地址是指段内的偏移地址,用于定位段内的数据。在段式管理中,逻辑地址转换为线性地址的过程如下:
1.取出段基址,将段基址与段内偏移地址相加,得到段内的线性地址。
2.检查该线性地址是否超出当前段的长度。如果超出,则产生越界错误。
3.如果没有超出段的长度,则将段内的线性地址作为最终的线性地址。
在Intel的x86架构中,段基址和段内偏移地址都是16位的,最大可表示64KB的内存空间。因此,如果需要访问凌驾64KB的内存空间,则需要使用多个段,并通过分段机制将它们连接起来。
7.3 Hello的线性地址到物理地址的变换-页式管理

在hello的页式管理中,内存被划分为固定大小的页框,每个页框有一个唯一的物理地址。步伐的逻辑地址空间也被划分为相同大小的页,每个页对应一个线性地址。页式管理通过将线性地址映射到物理地址来实现内存的访问。
线性地址到物理地址的变换过程如下:
1.分页单位将线性地址分为两个部分:页号和页内偏移。页号用于索引页表,页内偏移用于定位页内的数据。
2.页表中存储了每个页对应的物理页框号。页表通常由操作系统维护,存储在内存中。
3.分页单位通过页号在页表中查找对应的物理页框号。
4.将物理页框号与页内偏移相加,得到最终的物理地址。
在页式管理中,每个页都有一个页表项,PTE 中存储了页的物理页框号、页的权限、页的状态等信息。操作系统可以通过修改 PTE 来改变页的属性。
页式管理的优点是可以提高内存使用率,因为可以将内存分成多个固定大小的页框,每个页框可以独立管理,制止了内存碎片的产生。同时,页式管理也可以提高系统的安全性,因为可以通过设置页的权限来控制对内存的访问。
但是,页式管理也存在一些缺点,比方页表需要占用大量的内存空间,因此需要使用一些技术来减少页表的存储空间,比方多级页表、页表压缩等。别的,页式管理也需要更多的硬件和软件支持,因此系统开销较大。
7.4 TLB与四级页表支持下的VA到PA的变换

TLB (Translation Lookaside Buffer) 和四级页表是Intel处置惩罚器中用于实现虚拟地址(VA)到物理地址(PA)变换的两个紧张机制。当CPU需要访问内存时,它会先查看TLB中是否存在对应的VA的映射关系。如果存在,TLB中的映射关系会被直接使用,从而加快了内存访问速率。如果不存在,CPU会进入页表查找VA对应的PA。在四级页表中,VA被分成了四个部分,通过索引页目录项和页表项获取页的物理页框号,再加上页内偏移得到PA。如果TLB中没有对应的VA的映射关系,CPU会将其加入TLB中,以便下次使用。这两个机制共同工作,使得Intel处置惩罚器可以加快VA到PA的变换,提高内存访问速率。
7.5 三级Cache支持下的物理内存访问

在三级 Cache(L1、L2、L3)支持下的物理内存访问过程如下:

  • 本地缓存(L1):L1 Cache是最靠近处置惩罚器核心的缓存,速率最快。当CPU访问一个数据后,首先在L1Cache中查找。如果掷中(数据在缓存中),则险些瞬时返回,无需访问内存,从而提高了效率。
  • L2 Cache:如果L1 Cache未找到,数据会流向L2 Cache。L2 Cache的容量比L1大,但速率稍慢。如果L2 Cache掷中,会比L1 Cache访问慢一些,但仍旧比内存快得多。
  • L3 Cache:如果L2 Cache也未找到,数据才会流向L3 Cache。L3 Cache是整个系统中最大的缓存,它的存在是为了进一步减少访问内存的频率。如果L3 Cache掷中,CPU性能将进一步提升,因为L3 Cache的数据传输速率靠近于内存速率。
  • 内存访问:如果L3 Cache也未找到数据,CPU才会正式访问物理内存即主存。内存访问速率较慢,因为它位于CPU之外,通过总线连接。由于内存访问时间较长,所以整个系统会停息其他操作,直到数据返回。  
  • 数据返回:一旦数据从内存返回,L3 Cache会先接收,然后根据需要,L2和L1 Cache也会更新,以减少未来访问内存的次数。
通过这种逐级查找的机制,三级Cache显着减少了内存访问延迟,提高了计算机的整体性能。当数据频仍重复访问时,缓存掷中率高,性能提升更为显着。
7.6 hello历程fork时的内存映射

hello历程在调用fork时的内存映射过程如下:
1.创建新历程的数据结构:当hello历程调用fork时,内核会为新创建的子历程创建各种数据结构,包罗历程控制块(PCB)、历程描述符等,并为子历程分配一个唯一的历程标识符(PID)。
2.复制内存管理结构:内核会为子历程创建一个与父历程相同的内存描述符(mm_struct)、地区结构(vm_area_struct)和页表。这些结构描述了历程的虚拟内存布局和映射关系。
3.标志页面为只读和私有写时复制:内核将父子历程中的每个页面都标志为只读,并将每个地区结构标志为私有的写时复制。这意味着,如果任何一方实验写入这些页面,内核将通过写时复制机制来处置惩罚。
4.写时复制机制:在初始阶段,父子历程共享相同的物理内存页。如果任何一个历程实验写入这些共享的只读页面,内核会捕捉这个写操作,并为该历程创建一个新的物理内存页副本,然后将该页的访问权限更改为可写。这个过程对历程是透明的。
5.子历程的虚拟内存布局:当fork在新创建的子历程中返回时,子历程的虚拟内存布局与父历程在调用fork时的虚拟内存布局完全相同。这意味着子历程继承了父历程的所有内存映射,包罗代码、数据、堆栈以及任何共享库的映射。
6.后续的写操作:如果父子历程中的任何一个在fork之后进行写操作,写时复制机制将确保只有实际被修改的页面才会被复制,从而制止了不必要的内存复制,提高了效率。
综上所述,hello历程在调用fork时,子历程会继承父历程的虚拟内存布局,并通过写时复制机制共享物理内存页。只有当历程实验写入共享页面时,内核才会为该历程创建新的内存页副本,确保历程间的内存隔离。这种机制使得fork操作在创建新历程时非常高效,尤其是在历程不需要修改共享内存的情况下。
7.7 hello历程execve时的内存映射

当hello历程调用execve函数时的步骤如下:
1.终止当进步程:execve调用之前,hello历程会终止其当前的执行上下文,包罗扫除栈、关闭文件描述符等,这会释放掉大部分内存映射。
2.加载新步伐:execve会加载新的步伐(通常是步伐的可执行文件)到内存中。内核会为新的步伐创建一个新的内存映射,包罗代码段(text)、数据段(data)、堆(heap)和栈(stack)等。这个新的内存映射通常不会与父历程共享,而是为新步伐独立分配。
3.重置内存映射:执行新步伐时,内核会重新设置历程的内存映射,确保新步伐的代码、数据和动态分配的内存地区(如堆和栈)与新加载的步伐文件对应。这可能包罗加载新的页表,重新初始化地区结构。
4.权限和共享:如果execve加载的是一个共享库(比方动态链接的步伐),内核会创建一个私有的映射,使得每个历程都有自己的私有副本,即使它们共享相同的库。如许可以确保线程安全,并在需要时进行写时复制。
5.扫除旧映射:由于execve更换的是整个历程的执行环境,所以之进步程的内存映射(除了内核空间和一些特定的系统地区)通常会被扫除,以制止安全和资源题目。
6.初始化新历程状态:执行新步伐后,内核会根据新步伐的入口点初始化历程状态,包罗设置堆栈指针、步伐计数器等。
    
 
综上所述,hello历程在调用execve时,会终止当前的执行环境,更换为新步伐的内存映射,创建独立的代码、数据和堆栈地区,确保新步伐的执行环境与旧历程隔离。这个过程允许历程在执行差别的步伐时,有用地管理和隔离内存。
7.8 缺页故障与缺页中断处置惩罚

缺页故障是操作系统中的一种异常,发生在步伐访问的内存页不在物理内存中时。当发生缺页故障时,操作系统会通过缺页中断来处置惩罚这种情况。缺页故障与缺页中断处置惩罚的介绍如下:
缺页故障
1.触发条件:当步伐实验访问一个内存地址,而这个地址对应的页不在物理内存中(即不在RAM中),而是在磁盘上的互换区或文件系统中时,就会触发缺页故障。
2.异常处置惩罚:缺页故障被视为一种异常,需要操作系统的异常处置惩罚机制来处置惩罚。
缺页中断处置惩罚的几种方式如下:
1.中断相应:当CPU检测到缺页故障时,会立刻中断当前的执行流程,跳转到预设的缺页中断处置惩罚步伐。  
2.查找页表:中断处置惩罚步伐首先检查页表,确认发生故障的页是否真的不在物理内存中。如果页表项标志为无效,那么确实是缺页故障。
3.加载页面:如果确认是缺页故障,操作系统会从磁盘上的互换区或文件系统中读取相应的页面到物理内存中。这通常涉及到磁盘I/O操作,是处置惩罚过程中最耗时的部分。
4.更新页表:页面加载到物理内存后,操作系统会更新页表,将该页的状态标志为有用,并记录其对应的物理内存地址。
5.重新执行指令:页面加载并更新页表后,操作系统会重新执行导致缺页故障的那条指令,因为此时所需的页面已经在物理内存中了。
6.内存管理:在加载新页面到物理内存时,如果物理内存已满,操作系统可能需要执行页面置换算法(如LRU、FIFO等),选择一个或多个页面从物理内存中移除,以便为新页面腾出空间。
7.9动态存储分配管理

printf函数自己不会直接调用malloc来分配内存,因为printf重要用于格式化输出,并不涉及动态内存分配。然而,在某些情况下,如果printf的参数需要动态分配内存,那么可能会间接调用malloc。
动态内存管理的基本方法与策略如下:
基本方法
1.内存分配:
使用malloc等函数来分配内存,通常会从堆(heap)中分配一块连续的内存空间,并返回一个指向该空间起始地址的指针。malloc 函数接受一个参数,即所需分配的内存大小(以字节为单位),并返回一个指向分配内存起始地址的指针。如果分配乐成,返回的指针指向一块连续的内存空间;如果分配失败,返回 NULL。
2.内存释放:
使用free函数来释放之前通过malloc系列函数分配的内存。释放内存后,该内存块可以被重新分配给其他部分使用。
3.内存管理:
操作系统或运行时库负责管理堆内存,包罗跟踪哪些内存块已被分配,哪些是空闲的,以及如何高效地分配和回收内存。
策略
1.内存分配策略:
首次顺应(First-fit):从空闲链表中找到第一个充足大的空闲块,并分配所需内存。
最佳顺应(Best-fit):找到最小的充足大的空闲块,以减少内存碎片。
最坏顺应(Worst-fit):分配最大的空闲块,以期望剩余的空闲块仍旧充足大,可以被其他分配哀求使用。
伙伴系统(BuddySystem):将内存分割成对称的块,通过合并相邻的空闲块来减少碎片。
2.内存回收策略:
立刻回收:当内存被释放时,立刻合并相邻的空闲块。
延迟回收:将释放的内存块保留在空闲链表中,直到需要时再进行合并。
3.内存碎片管理:
内部碎片:由于分配的内存块大小通常是固定大小的整数倍,可能导致分配的内存块内部有未使用的空间。
外部碎片:随着内存的分配和释放,可能会导致空闲内存被分割成许多小块,难以满足大块内存的分配哀求。
4.内存分配器优化:
池分配(PoolAllocation):预先分配一组固定大小的内存块,用于快速分配和回收相同大小的对象。
垃圾回收(GarbageCollection):主动跟踪和管理不再使用的内存,无需步伐员显式调用free。
动态内存管理是编程中的一个复杂主题,需要细致考虑内存分配和释放的机遇,以制止内存泄漏和悬挂指针等题目。精良的内存管理策略可以提高步伐的性能和稳定性。
7.10本章小结

本章介绍了hello的存储器地址空间、Intel逻辑地址到线性地址的变换-段式管理、Hello的线性地址到物理地址的变换-页式管理、LB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问、hello历程fork时的内存映射、hello历程execve时的内存映射、缺页故障与缺页中断处置惩罚、动态存储分配管理的相关内容。
8hello的IO管理

8.1 Linux的IO设备管理方法

“设备的模子化:文件”是指所有的I/O设备(比方网络、磁盘和终端)都被模子化为文件,而所有的输入和输出都被当尴尬刁难相应文件的读和写来执行。
“设备管理:unix io接口”是指这种将设备映射为文件的方式允许Linux内核引出一个简单、低级的应用接口,这使得所有的输入和输出都能以一种同一且一致的方式来执行。
8.2 简述Unix IO接口及其函数

    Unix I/O接口是Linux内核提供的一套标准输入输出函数,用于处置惩罚文件和设备的操作。接口函数重要包罗:
open():打开文件或设备,返回一个文件描述符。
read():从文件或设备读取数据。
write():向文件或设备写入数据。
close():关闭文件或设备,释放文件描述符。
lseek():移动文件读写指针到指定位置。
ioctl():用于设备特定的控制操作。
8.3 printf的实现分析

printf的函数体如下:

  • intprintf(const char *fmt, ...)
  • {
  • int i;
  • char buf[256];

  • va_list arg = (va_list)((char*)(&fmt) + 4);
  • i = vsprintf(buf, fmt, arg);
  • write(buf, i);

  • return i;
  • }
vsprintf的函数体如下:

  • intvsprintf(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);
  • }
在write函数执行时,它会将参数加载到寄存器中,然后通过int 21h指令触发系统调用。系统调用负责将字符串中的字节从寄存器经由总线传输到显卡的显存中,显存内存储着字符的ASCII码。字符显示驱动步伐根据这些ASCII码在字模库中查找对应的点阵信息,并将这些点阵信息写入到vram中。显示芯片随后以设定的革新频率逐行扫描vram,通过信号线将每个像素的RGB颜色信息传输给液晶显示器。最终,hello步伐的输出内容便在屏幕上显示出来。
8.4 getchar的实现分析

在键盘中断处置惩罚子步伐中,步伐接受按键扫描码并转换为 ASCII 码,保存到系统的键盘缓冲区。getchar() 等调用 read() 系统函数时,通过系统调用读取按键 ASCII 码,直到检测到回车键才返回。当步伐调用 getchar() 函数时,步伐会等待用户按键,用户输入的字符被存放在键盘缓冲区,直到用户按回车为止(回车也在缓冲区中)。getchar() 函数每次从 stdio 流中读取一个字符,返回用户输入的第一个字符的 ASCII 码,如出错返回 -1。在用户按回车之前输入的其他字符将保留在键盘缓冲区中,等待后续 getchar() 调用读取。在用户按键时触发键盘终端,操作系统转移控制到键盘中断处置惩罚子步伐,中断处置惩罚步伐执行,接受按键扫描码转成 ASCII 码,保存到系统的键盘缓冲区,并显示在用户输入的终端内。中断处置惩罚步伐执行完毕后,返回到下一条指令运行。
8.5本章小结

本章介绍了Linux的IO设备管理方法,简述了简述Unix IO接口及其函数,分析了printf和getchar的实现。
结论

当hello.c被步伐员写出来后,还不是一个可执行步伐,需要颠末以下步骤来赋予它“生命”:
hello.c预处置惩罚得到hello.i文件;
hello.i编译得到hello.s文件;
hello.s汇编得到二进制可重定位目标文件hello.o;
hello.o通过链接天生可执行步伐hello;
Shell通过fork函数天生子历程;
execve函数加载并运行步伐hello;
hello的运行与存储管理息息相关,也与IO管理紧密相连;
hello最终被shell父历程回收,内核会收回为其创建的所有信息,hello的一生到此结束。
通过这次大作业,我深刻体会了一个步伐从编写到执行的各个环节,深刻感受到了执行一个步伐需要的步骤之多,我对计算机系统的理解也大大加深。通过在Ubuntu下进行各个下令的执行,我实现了对hello的各方面的剖析,还认识了许多原来不那么认识的函数,好比我以atoi函数为例介绍了hello的重定位过程。hello的一生虽然短暂,但却凝结着人类的许多智慧,让我获益颇多。
附件

文件名
作用
hello.i
hello.c预处置惩罚后的文件,可用于编译
hello.s
hello.i编译后的文件,可用于汇编
hello.o
hello.s汇编后的文件,可用于链接
hello
hello.o链接后的文件,可用于执行
 
参考文献

[1]  Randal E. Bryant,David O’Hallaron,《深入理解计算机系统》[M](原书第三版),北京:呆板工业出版社,2021:10-1.
[2]  https://www.cnblogs.com/pianist/p/3315801.html
 

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

使用道具 举报

0 个回复

倒序浏览

快速回复

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

本版积分规则

滴水恩情

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

标签云

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