计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术学院人工智能领域
学 号 2022110996
班 级
学 生
指 导 教 师 刘宏伟
计算机科学与技术学院
2024年6月
本文全面探讨了"hello"程序的生命周期,从编写到执行的各个阶段。全文分为七个主要章节,涵盖了编程环境的搭建、源代码的预处理、编译、汇编、链接,以及程序的执行和存储管理。第一章简要介绍了"hello"程序的基本概念和研究目的,同时概述了所使用的开发环境和工具,以及中间结果的预期。第二章深入讨论了预处理的概念、重要性,并展示了在Ubuntu系统上执行预处理的具体命令。本章还分析了"hello"程序的预处理结果,为理解后续编译过程奠定了基础。第三章阐释了编译在软件开发中的核心作用,介绍了编译命令在Ubuntu系统中的应用,并详细解析了"hello"程序编译后生成的目标文件。第四章描述了汇编过程及其在程序开发中的作用,提供了在Ubuntu环境下进行汇编的命令,并深入探讨了"hello.o"文件的格式和结构。第五章讨论了链接的概念、重要性,并演示了链接命令在Ubuntu系统中的使用方法。本章重点分析了"hello"可执行文件的格式,以及链接过程中的重定位机制和动态链接特性。第六章探讨了进程管理的基础知识,包括Shell-bash的作用和处理流程,"hello"程序的fork和execve过程,以及进程执行中的异常和信号处理。第七章详细分析了"hello"程序的存储器地址空间,包括地址转换机制、页表和TLB的使用,以及Cache和动态存储分配的管理。
本文旨在为读者提供一个清晰的视角,了解一个简单程序在计算机系统中的完整生命周期,以及各个阶段之间的相互依赖关系。
关键词:预处理、编译、汇编、链接、进程管理、存储管理
目 录
第1章 概述
1.1 Hello简介
hello程序的执行过程是计算机系统工作流程的缩影,涵盖了从源代码编写到程序终止的一系列步骤。包括源代码编写、预处理、编译、汇编、链接、加载、进程创建、内存映射、系统调用、执行、I/O操作、进程终止、资源回收。
1.2 环境与工具
1.2.1 硬件环境
12th Gen Intel(R) Core(TM) i5-12500H 2.50 GHz;16.0 GB
RAM;512GHD Disk
1.2.2 软件环境
Windows11 64 位;VMware® Workstation 17 Pro;Ubuntu 20.04 LTS 64 位
1.2.3 开发工具
Visual Studio 2022 64 位;CodeBlocks 64 位;vi/vim/gedit+gcc
1.3 中间结果
hello.c | 源文件 |
hello.i | 预处理文件 |
hello.s | 编译文件 |
hello.o | 汇编文件 |
hello | 可执行文件 |
hello.asm. | 反汇编文件 |
1.4 本章小结
本章说明了hello.c程序P2P,020的过程、本次实验所需的环境和工具以及过程中所生成的中间结果。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理是C编译过程的第一步,它是在源代码被编译之前进行的一系列操作。主要包括:包含其他头文件、宏替换、条件编译、注释删除。预处理器是一个独立的程序,它接收C源代码文件(.c)作为输入,并对其中的预处理指令进行处理。预处理指令以#开头,并告诉预处理器要执行的操作。GCC编译器中的预处理器是一个独立的模块,称为cpp。
2.1.1 预处理的作用
预处理的作用是在实际编译之前对源代码进行一些预处理操作。
包含其他头文件:使用#include指令将其他头文件的内容插入到源代码中,以便可以使用其他头文件中定义的函数、类型和宏。
宏替换:使用#define指令定义宏,然后在代码中使用宏名称来代替特定的文本。
条件编译:使用#ifdef、#ifndef、#if等指令可以根据条件编译不同的代码或定义不同的宏。
注释删除:将源代码中的注释删除,以便不会影响编译过程。
预处理的输出结果(.i)是一个经过预处理操作的源代码文件,该文件将用于后续的编译阶段。预处理操作通过扩展宏、插入头文件内容和删除注释等方式,提供了更灵活和可维护的代码编写方式。
2.2在Ubuntu下预处理的命令
使用gcc -E -o hello.i hello,c命令,生成预处理后的hello,i文件。
2.3 Hello的预处理结果解析
查看预处理生成的hello.i文件,其内容如下:
开始部分,包含了编译器的内置指令和命令行参数,以及包含的标准预定义头文件。# 1 "hello.c" 表示预处理开始了对hello.c文件的处理。#后面跟着的数字是行号,用来指示编译器在处理哪个文件的哪一行。# 1 "<built-in>" 表示编译器正在处理内置的宏定义。这些宏定义通常是由编译器提供,用于控制编译过程。# 1 "<command-line>" 表示编译器正在处理来自命令行的指令。这些指令可以包括定义宏、包含搜索路径等。# 31 "<command-line>" 表示已经处理了命令行上的一部分指令,数字31是继续的行号,表明预处理指令的延续。# 1 "/usr/include/stdc-predef.h" 1 3 4 表示编译器开始包含/usr/include/stdc-predef.h头文件。这个文件包含了标准C库的预定义宏。数字1表示这是该文件的第一行,3和4是条件编译标志,通常用来指示编译器在特定条件下包含或排除代码。(后续类似行同理,都是表示编译器开始包含不同的头文件的意思)# 32 "<command-line>" 2 表示返回到命令行指令的处理,数字32是行号,2表示这是命令行指令的第二行或者第二部分。
第二部分预处理代码是在定义一系列类型的别名。这些定义可以在程序中方便地使用更具可读性和可移植性的别名类型,而避免直接使用基本数据类型造成的不确定性。
使用 extern 可以告诉编译器所声明的标识符是其他地方定义的,因此编译器在当前文件中不需要寻找其定义,而是在链接时从其他文件中寻找。
最后是删去了全部的注释和头文件引用的相关内容后的源代码。
2.4 本章小结
预处理是在编译过程中的第一步,是对源代码进行预处理操作。通过本章内容,可以更好地理解预处理器。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译是将高级语言源代码转换为目标机器代码的过程。编译器是用于执行编译过程的工具,将人可读的高级语言源代码翻译成计算机可执行的机器代码。
3.1.1 编译的作用
1、翻译:编译器将高级语言源代码翻译成目标机器代码,使得计算机能够理解并且执行代码。
2、优化:编译器对代码进行优化,提高程序的效率和性能,例如消除冗余代码、调整代码结构等。
3、进行错误检测:编译器能够检测源代码中的语法错误和逻辑错误,并在编译过程中发出警告或错误提示。
4、提高可移植性:通过编译过程,源代码可以转换为目标机器的可执行代码,提高程序在不同平台上的可移植性。
3.2 在Ubuntu下编译的命令
使用gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -S -o hello.s hello.i命令,生成编译后的hello.s文件。其中-no-pie表示禁用PIE 。PIE是一种可执行文件格式,允许地址空间随机化。-fno-stack-protector禁用堆栈保护器-fno-PIC表示禁用位置无关代码。这个选项告诉编译器不生成位置无关代码。
3.3 Hello的编译结果解析
查看编译生成的hello.s文件,其内容如下。
3.3.1 数据
1.常量字符串
如图所示部分是涉及的部分常量字符串,其中第六行使用UTF-8编码的中文字符,是 "用法: Hello 学号 姓名 手机号 秒数!\n";第九行同样是字符串常量,用于格式化输出,格式是 "Hello %s %s %s\n",其中 %s 是字符串的占位符。
2.常量立即数
如图所示部分是涉及的部分常量立即数,使用$+数字表示,第三十行表示比较%edb寄存器内的值和立即数9;第36行表示将立即数0赋值给%eax寄存器;第42行表示%ebp寄存器的值+1。其他立即数都有类似的作用。
3.寄存器变量
如图所示的部分是寄存器变量,%rdi, %rsi, %rdx, %rcx:都是通用寄存器,通常用于传递函数参数、存储函数返回值等。
%rax: 用于存储函数的返回值。
%rbx: 用作基指针寄存器,在这段代码中,它被用来存储传入的参数数组的地址。
%rbp: 用作栈基指针寄存器,但在这段代码中,它被用作循环计数器。
%rsp: 栈指针寄存器,指向栈顶。
4.局部变量
如图所示的部分是局部变量,通过subq $8, %rsp分配了8个字节的空间在栈上用于存储局部变量。
3.3.2 赋值
1.将立即数赋值给寄存器
如图所示部分将$.LC1的内容赋给%edi寄存器,作为printf和puts函数的参数,$0赋给%eax,用作循环计数器的初始化。
- 将寄存器的值赋值到另一个寄存器
如图所示部分将%eax的值赋值给%edi,将转换后的整数移动到`%edi`寄存器,用%edi保存存放在%eax里的atoi函数的返回值。
- 将内存数据移动到寄存器
如图所示部分将%rbx寄存器里的地址作为基地址,加上32的偏移量,得到的地址存放在%rdi寄存器里,作为参数传递给atoi函数。
3.3.3 算术操作
1.加法操作
如图所示部分,使用加法操作将循环计数器增加1,对应循环体中的计数变量i。
- 减法操作
如图所示部分从栈指针%rsp中减去8。在函数的开始处进行栈空间分配的标准操作,用于保存局部变量和调用其他函数时的参数。
3.3.4 关系操作
如图所示部分是比较操作,第25行cmpl $5, %edi: 比较%edi寄存器中的值与立即数5。这个操作用于检查传递给 main 函数的参数个数是否为5;cmpl $9, %ebp: 比较%ebp寄存器中的值与立即数9。这个操作用于控制循环的退出条件,当计数器超过9时退出循环。
3.3.5 数组/指针/结构操作
1.数组
如图所示,第27行将命令行参数的指针%rsi(指向 argv[])移动到 %rbx 寄存器作为之后加载数据的基址保存寄存器。第32、33、34行分别调用数组。
2.指针
如图所示,第17行将帧指针%rbp压入栈中,以保存函数调用前的帧指针值;第56行,将栈顶元素弹出并保存到相应寄存器,这个过程隐含了将帧指针返回至函数调用前。
3.3.6控制转移
- 条件跳转操作
如图所示部分为条件跳转,第26行jne .L6表示若第25行cmpl $5, %edi操作的结果不相等,即参数个数不等于5,则跳转到标签.L6。第31行表示若第30行是%ebp大于9,则跳转到标签.L7。
- 无条件跳转操作
如图所示部分为无条件跳转。
3.3.7 函数操作
1.函数调用
如图所示为函数调用部分。
call printf: 调用C标准库中的printf函数进行格式化输出。
call atoi: 调用atoi函数将字符串转换为整数。
call sleep: 调用sleep函数使程序暂停执行指定的秒数。
call puts: 调用puts函数打印字符串到标准输出。
call getchar: 调用getchar函数读取一个字符。
call exit: 调用exit函数退出程序。
2.参数传递
如图所示,在调用函数之前,需要将参数通过寄存器或栈传递给函数。
- 返回值处理
如图所示,函数的返回值存储在%rax寄存器中,atoi函数将转换后的整数值返回到%rax,将 atoi 函数的返回值移动到%edi寄存器,作为sleep函数的参数。
3.3.8特殊指令和伪操作
如图所示为部分特殊指令和伪操作,.file为文件声明,指明了汇编代码所编译的源文件名称,供调试器使用。.text: 指示汇编器接下来的部分是代码段,即程序的指令执行区域。.section .rodata.str1.8,"aMS",@progbits,1定义了一个只读数据段,用于存储字符串常量。.LC0:和.LC1: .string "Hello %s %s %s\n": 定义了两个字符串常量,供printf和puts调用时使用。.align 8为对齐指令,确保数据按照8字节边界对齐,提高内存访问效率。.globl main: 声明main函数为全局符号,使其可以被其他代码引用。.type main, @function: 指定main的类型为函数。
.note.GNU-stack这部分定义了GNU栈属性,通常用于设置栈的权限和行为.note.gnu.property这部分定义了GNU属性。.long指令用于声明长整型的字面量。
3.4 本章小结
通过编译,从.i文件到.s汇编文件,同时分析了编译器如何处理C语言中各个数据类型和各类操作。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是将汇编语言转换为机器语言的过程。汇编器是用来执行这个过程的工具。汇编是由汇编指令组成的指令集,每个汇编指令都对应着机器指令,通过汇编器将汇编指令转化为机器指令,在计算机上执行。
4.1.2 汇编的作用
1.代码转换:汇编器将汇编语言代码转换成计算机能够直接执行的机器码。
2.符号解析:汇编器处理程序中的符号并将它们解析为内存地址或立即数。
3.指令编码:将汇编指令转换成对应的机器指令编码,这些编码是CPU能够识别和执行的二进制序列。
4.地址生成:在汇编过程中,汇编器为代码和数据生成实际的内存地址,这些地址在程序执行时用于访问内存。
4.2 在Ubuntu下汇编的命令
使用gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -c -o hello.o hello.s命令,生成汇编后的hello.o文件。
4.3 可重定位目标elf格式
ELF头部信息:
Magic: 文件开头的魔数7f 45 4c 46表明这是一个ELF文件。
类别:ELF64表明这是一个64位的ELF文件。
数据:2 补码,小端序 (little endian)表示数据是以小端序格式存储的。
Version:1 (current)是当前版本。
OS/ABI:UNIX - System V表示这是为UNIX System V ABI编译的。
ABI 版本:0
类型:REL (可重定位文件)表明这是一个可重定位的对象文件。
系统架构:Advanced Micro Devices X86-64表示这是为AMD x86-64架构编译的。
入口点地址0x0,这里没有指定入口点。
程序头起点:0,没有程序头。
节头起点:1240字节。
标志:0x0
Size of this header: 64字节。
Size of program headers: 0字节,没有程序头。
Number of program headers: 0。
Size of section headers: 64字节。
Number of section headers: 15,有15个节头。
Section header string table index: 14。
节头信息:
节头提供了文件中各个节的详细信息,包括它们的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息和对齐等。
.text 节包含可执行代码。
.data 节包含已初始化的全局和静态C变量。
.bss 节包含未初始化的静态变量,以及初始化为0的全局或静态变量。
.rodata.str1.8 和 .rodata.str1.1 节包含只读数据。
.comment 和 .note.GNU-stack 节包含文件的注释和栈信息。
.note.gnu.property 节包含 GNU 属性,这些属性提供有关文件的特定于处理器的特性信息。
.eh_frame 节包含异常处理信息。
.rela.text 和 .rela.eh_frame 节包含重定位信息,这对于链接器解析符号引用是必要的。
.symtab和 .strtab节包含程序的符号信息和字符串信息。
重定位节信息:
重定位节包含必要的信息,以便在链接时调整代码和数据的地址。
.rela.text 节包含8个重定位条目:
每个条目包含偏移量、信息、类型、符号值、符号名称和加数。
例如,R_X86_64_32和R_X86_64_PLT32是重定位类型,分别用于普通符号和过程链接表(PLT)符号。符号如printf、atoi、sleep、puts、exit、getchar都是全局符号,它们在.symtab符号表中有对应的条目。
.rela.eh_frame 节包含1个重定位条目:
条目类型为R_X86_64_PC32,这是一个PC相对的32位重定位。
符号表信息:
符号表(.symtab)包含18个条目,描述了文件中的符号及其属性。
例如,main函数是一个全局符号,类型为FUNC,表示它是一个函数。
该文件没有程序头,程序头用于可执行文件和动态库,而不是可重定位对象文件。
该文件没有动态节,动态节用于动态库和可执行文件。
该文件没有版本信息。
4.4 Hello.o的结果解析
左图为hello.s,右图为hello.o的反汇编,对比结果如下:
1.汇编指令与机器码的直接映射:
汇编代码中的 .file、.section、.align 等伪操作在机器码中没有直接对应。
movq,movl,movb等移动指令在反汇编中直接显示。cmpl指令用于比较,如cmpl $5, %edi对应于83 ff 05。jne .L6对应于75 3d
2. 函数调用
汇编中的call指令在反汇编中以callq形式出现,后面跟随的是要调用的函数的偏移量。
3. 重定位信息
反汇编中的重定位条目指示链接器需要如何调整地址:
R_X86_64_32 .rodata.str1.1和R_X86_64_PLT32 printf-0x4指明了需要重定位的符号。
- 操作数映射
汇编代码:中立即数直接写在指令中,反汇编立即数以十六进制形式表示。汇编代码常量和标签使用符号表示反汇编中常量和标签被转换为它们在内存中的地址。
- 分支转移
汇编代码中分支和跳转使用标签,如jne .L,反汇编中分支和跳转操作数是目标指令的相对偏移量,如75 3d表示如果条件满足,则跳转到后面0x3d字节的位置
4.5 本章小结
本章通过汇编,从.s文件到.o汇编文件。对ELF进行分析,并且分析了机器语言与汇编语言的映射关系。
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是编程中将多个编译后生成的目标文件(.o 文件)或代码模块组合成一个单一可执行文件的过程。链接就是将目标文件按照一定的规则合并在一起,生成一个完整的可执行文件。
5.1.2链接的作用
1.符号解析: 链接器将程序中的符号引用与其在内存中的地址关联起来。
2.地址分配: 链接器为程序中的代码和数据分配内存地址,确保它们在执行时可以被正确访问。
3.模块合并: 在大型程序中,源代码可能会被分成多个模块或文件。链接器将这些模块合并成一个单一的可执行文件。
4.库链接: 链接器将程序中引用的库与程序代码链接在一起,确保程序可以调用库中的函数。
5.重定位: 链接器处理程序中的重定位条目,将代码和数据中的地址引用调整为正确的运行时地址。
6.生成可执行文件: 链接器将编译后的目标文件(object files)和必要的库链接在一起,生成最终的可执行文件或共享库。
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.o文件。
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
节头(Sections):
每个节头都有一个特定的名称、类型、地址、偏移量、大小、全体大小、标志、链接、信息和对齐属性。
NULL: 一个空段,作为段头表的第一个条目。
.interp: 解释器段,包含程序解释器的路径。
.note.gnu.property和.note.ABI-tag: 包含特定于GNU和ABI的属性和版本信息。
.hash和.gnu.hash: 符号散列表,用于加速符号查找。
.dynsym: 动态符号表,包含动态链接用的符号。
.dynstr: 动态字符串表,包含动态符号表中的字符串。
.gnu.version: 版本符号表。
.gnu.version_r: 版本需求表,包含对库版本的依赖。
.rela.dyn和.rela.plt: 重定位表,包含动态链接时需要调整的地址。
.init: 初始化代码段。
.plt和.plt.sec: 过程链接表,包含函数调用的跳转指令。
.text: 代码段,包含程序的主要指令。
.fini: 终止代码段。
.rodata: 只读数据段。
.eh_frame: 异常处理帧信息。
.dynamic: 动态链接信息表。
.got,.got.plt 和 .data: 全局偏移表和初始化数据段。
.comment: 注释段。
.symtab和.strtab: 符号表和字符串表,包含程序的符号信息。
.shstrtab: 节头字符串表,包含节名称。
程序头(Program Headers):
程序头定义了程序的内存布局,包括代码、数据和堆栈等的加载信息。
PHDR: 程序头表。
INTERP: 解释器,指向动态链接器。
LOAD: 加载段,包含程序的多个加载区域。
DYNAMIC: 动态链接信息。
NOTE: 包含程序的额外信息,如GNU_PROPERTY和GNU_STACK。
动态节(Dynamic Section):
包含动态链接的信息,如需要的共享库、初始化和终止函数的地址、符号表和字符串表等。
重定位节(Relocation Sections):
包含.rela.dyn和.rela.plt,这些节包含动态链接时需要应用的重定位条目。
符号表(Symbol Tables):
包含程序的符号信息,如函数名、变量名等,以及它们在内存中的地址。
版本信息(Version Information):
包含程序与库的版本依赖信息。
特定段的详细信息:
入口点地址:0x4010f0
程序头起点:64字节
节头起点:14208字节
程序头大小:56字节
节头大小:64字节
程序头数量:12
节头数量:27
节头字符串表索引:26
特定段的地址和大小:
.init: 地址0x401000, 大小1b字节
.plt: 地址0x401020, 大小70字节
.text: 地址0x4010f0, 大小125字节
.rodata: 地址0x0402000, 大小48字节
.dynamic: 地址0x403e50, 大小1a0字节
.data: 地址0x404048, 大小4字节
5.4 hello的虚拟地址空间
<-init>的虚拟地址为0x401000,与5.3对照一致。
<.plt>的虚拟地址为0x401020,与5.3对照一致。
<.texi>的虚拟地址为0x4010f0,与5.3对照一致。
<.fini>的虚拟地址为0x401218,与5.3对照一致。
5.5 链接的重定位过程分析
1.hello.o与hello和区别
hello.o
类型: 可重定位的ELF对象文件,由编译器从源代码生成。
重定位: 包含重定位条目,指示链接器需要对哪些位置的代码或数据进行地址调整。
符号: 可能包含未解析的外部符号引用,需要链接器解析。
代码: 包含源代码编译后的机器指令,但尚未进行最终的地址分配。
hello
类型: 可执行的ELF文件,由链接器将一个或多个.o文件以及所需的库链接生成。
重定位: 重定位条目已经被处理,所有的地址引用都已经被解析并指向正确的内存地址。
符号: 所有外部符号引用都已被解析,不再存在未解析的外部符号。
代码: 包含最终的可执行机器指令,地址分配已完成,可直接由操作系统加载执行。
2.链接过程
编译源代码: 源代码被编译器编译成.o文件,包含可重定位的机器代码和重定位信息。
调用链接器: 链接器被用来链接一个或多个.o文件,以及它们依赖的库文件,生成最终的可执行文件。
解析重定位: 链接器根据.o文件中的重定位条目,将所有的符号引用解析为实际的内存地址。
生成程序头: 链接器为最终的可执行文件生成程序头,定义了程序的内存布局,包括代码、数据、堆栈等的加载信息。
创建动态链接信息: 如果程序使用了动态链接库,链接器会创建动态链接所需的信息。
生成可执行文件: 最终生成的可执行文件包含了所有必需的代码、数据、符号表和重定位信息,准备被操作系统执行。
3.重定位分析
在hello.o中,重定位条目如R_X86_64_PLT32指示链接器需要对程序中的某些位置进行地址调整。
在hello中,链接器创建程序链接表,用于链接库函数的调用。对于每个链接库函数的调用,链接器在PLT中生成跳转代码。程序执行时,对链接库函数的调用会通过PLT进行,最终跳转到正确的函数地址。
5.6 hello的执行流程
执行hello,逐步运行。
程序首先进入_dl_start。
随后进入_dl_catch_exception@plt()
加载_init.
跳转至_setjmp().
进入__libc_start_main
执行main函数后进入__GI_exit退出.
5.7 Hello的动态链接分析
如图,在程序法执行前查看PLT表的内容,PLT未被初始化,存在大量0x00地址,与反汇编内容一致。
如图程序执行后查看PLT表的内容,动态链接后PLT为相对应的地址。
5.8 本章小结
本章通过链接,从.o文件到可执行文件。分析hello的ELF格式,用readelf等列出其各段的基本信息。分析hello与hello.o的不同,说明链接的过程。结合hello.o的重定位项目,分析hello中对其如何重定位。使用gdb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。分析hello程序的动态链接项目。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
进程是操作系统进行资源分配和调度的一个基本单位,它是程序在特定数据集上的一次动态执行活动。每个进程拥有独立的地址空间、系统资源和执行状态,确保了程序之间的相互隔离和并发执行,从而提高了计算机系统的整体效能和响应能力。
6.1.2 进程的作用
1.并发执行:操作系统可以同时运行多个进程,使得多个程序可以同时在计算机系统上执行,提高了计算机的效率和利用率。
2.资源分配:操作系统通过进程来管理和分配计算机系统中的各种资源,如CPU时间、内存、磁盘空间等,使得资源的分配和利用更加高效、合理。
3.进程间通信:多个进程之间可以通过进程间通信机制进行数据的交换和共享,实现进程间的数据传递和协作,例如管道、信号量、消息队列、共享内存等。
4.作业控制:操作系统通过进程控制和管理,可以启动、停止、暂停、恢复和调度进程,实现对进程的控制和监控,从而更好地管理和执行各种作业。
6.2 简述壳Shell-bash的作用与处理流程
Shell是计算机操作系统提供的一种命令行界面,它允许用户通过键盘输入命令来与操作系统进行交互。Shell解释并执行用户输入的命令,并将命令的输出结果显示给用户。
Bash是Unix/Linux系统上最常见的Shell程序之一。Bash具有丰富的命令和功能,可以用于完成各种系统操作和任务。
Bash的主要作用:
- 命令行解释器:Bash接收用户在命令行上输入的命令,并解析执行这些命令。
2.系统管理和操作:通过Bash,用户可以管理和操作系统的各种功能和设置,如文件管理、进程管理、用户管理、权限管理、网络管理等。
3.脚本编写和执行:Bash支持脚本编写,用户可以使用Bash编写Shell脚本,实现自动化的任务和批处理操作。
4.环境配置和自定义:Bash可以通过配置文件或环境变量进行自定义和配置,用户可以根据自己的需求修改Bash的行为和功能。
Bash的处理流程:
1.用户在命令行输入命令。
2.Bash读取并解析用户输入的命令。
3.Bash根据命令的类型和参数执行相应的操作
4.执行结果将输出显示给用户。
这个过程是循环进行的,用户可以持续输入命令,Bash将持续解析并执行这些命令。用户可以根据需要编写Shell脚本,并通过Bash执行脚本,实现更复杂的操作和任务。
6.3 Hello的fork进程创建过程
fork() 系统调用是 Unix 和类 Unix 系统中用于创建新进程的关键机制,它通过复制调用它的现有进程(父进程)来生成一个几乎相同的副本(子进程),子进程随后可以继续执行或执行不同的操作,而父进程可以选择等待子进程的结束或继续其自身的任务,这种机制为多任务处理和并发执行提供了基础。
在shell中./hello之后,创建一个子进程来运行hello。
6.4 Hello的execve过程
execve() 系统调用是在类 Unix 系统中用于执行一个新程序的过程。它替换当前进程的映像,用新的程序映像来取而代之。execve() 的典型用途是在 fork() 创建子进程后,让子进程执行一个不同的程序,而不必启动一个新的进程。
在shell中./hello创建子进程之后,execve()系统调用执行。
6.5 Hello的进程执行
1.进程上下文
进程上下文是指进程执行时所需的所有信息,包括但不限于:
代码和数据:进程的指令和变量。
寄存器状态:包括通用寄存器、程序计数器、堆栈指针等。
内存管理信息:如页表,用于虚拟内存和物理内存的映射。
I/O状态:打开的文件描述符、I/O缓冲区等。
2.进程时间片
时间片:操作系统分配给每个进程的CPU使用时间。时间片用完后,操作系统会触发一个时钟中断,当前进程会被挂起,CPU时间被分配给其他进程。
3.进程调度过程
程序启动:用户执行hello程序,操作系统加载程序到内存中,创建进程。
上下文切换:操作系统将CPU控制权交给hello进程,进行上下文切换,加载进程的寄存器和内存状态。
执行:hello进程开始执行,使用CPU时间片进行指令执行。
时钟中断:当时间片用完,操作系统会触发中断,挂起当前进程,保存其上下文。
调度:操作系统的调度器选择另一个就绪进程运行。
上下文切换:操作系统切换到新进程的上下文,加载其寄存器和内存状态。
4.用户态与核心态转换
用户态:进程执行在用户态,可以直接访问用户程序的代码和数据。
核心态:操作系统的核心部分运行在核心态,有访问所有硬件和内存的权限。
转换场景:
系统调用:当hello程序执行如read、write或sleep等系统调用时,会触发从用户态到核心态的转换。系统调用请求操作系统执行特定的服务。
中断:如时钟中断,会触发从用户态到核心态的转换,操作系统响应中断并进行调度。
异常:程序执行非法操作时,会触发异常,操作系统会捕获异常并处理。
当hello程序开始执行时,操作系统创建进程,分配时间片,并进行上下文切换。程序执行,使用系统调用打印输出,此时从用户态转换到核心态。
时间片用完,时钟中断发生,操作系统保存hello进程的上下文,挂起进程,并调度其他进程。当hello进程再次被调度,操作系统进行上下文切换,恢复其执行状态。
6.6 hello的异常与信号处理
1.常见的异常和信号,以及它们可能的来源和处理方式:
异常和信号:
SIGINT(Ctrl-C 产生):按下 Ctrl-C 时产生的信号,通常用于中断前台进程。
SIGTERM:终止信号,可以由kill命令或系统关机时产生。
SIGHUP:当用户退出终端(关闭终端窗口)时产生。
SIGQUIT(Ctrl-\ 产生):用户按下 Ctrl-\ 时产生的信号,通常用于退出前台进程,并生成核心转储。
SIGSTOP/SIGCONT:暂停和继续信号,通常由作业控制操作产生。
SIGKILL:强制终止进程,不能被进程捕获或忽略。
SIGALRM:由计时器到期产生的信号。
SIGFPE:浮点异常,如除以零。
SIGSEGV:段错误,如访问无效的内存地址。
SIGABRT:由abort函数调用产生的信号。
信号处理:
进程可以设置信号处理函数来响应特定的信号。
某些信号默认会终止进程(如 SIGKILL 和 SIGTERM),但进程可以选择捕获它们并执行清理操作。
其他信号(如 SIGINT 和 SIGALRM)可以被进程捕获并处理,例如忽略它们或执行特定的操作。
2.键盘操作和命令:
Ctrl-C:生成 SIGINT 信号,通常用于中断正在运行的程序。
Ctrl-Z:将前台进程放到后台,并暂停它,生成 SIGSTOP 信号。
Ctrl-:生成 SIGQUIT 信号,通常用于退出并生成核心转储。
3.命令和它们的功能:
ps:显示当前系统的进程状态。
jobs:列出当前终端会话中的作业。
pstree:以树状图的形式显示进程的层次结构。
fg:将后台作业带到前台运行。
kill:发送信号到指定的进程。
killall:通过进程名发送信号到一组进程。
4运行 hello 程序
如图所示,先输入参数运行hello,输出之后程序不会停止。
在程序执行过程中,不停乱按键盘,如果是非指令则不会影响程序执行,按下回车,会默认为是一个指令存储在缓冲区,等待程序执行完后执行。
按下Ctrl-C,生成SIGINT 信号,中断hello程序。
按下 Ctrl-Z,hello 程序被放到后台,并暂停执行。
运行 ps,显示所有进程的状态的PID,TTY,TIME,CMD,可以看出hello进程的PID为2765,占用CPU为0。
运行 jobs,查看当前终端后台的作业,即暂停的 hello 程序。
运行 pstree,以树状图形式显示进程,可以看到 hello 程序的父子关系。
运行fg将 hello 程序带回前台,程序恢复执行
运行kill -SIGKILL PID,即kill -9 2781其中 2781 是 hello 程序的进程 ID,杀死进程hello。
6.7本章小结
本章内容深入探讨了hello程序在操作系统中的进程管理机制,从Shell-bash启动程序开始,依次分析了fork进程创建过程、execve过程、进程执行结以及异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1.逻辑地址是在编译时生成的地址,它们是相对于程序的代码或数据段的起始地址的偏移量。编译器在编译程序时,会为程序中的变量、数组、字符串等数据生成逻辑地址。在 hello 程序中,字符串的逻辑地址是相对于数据段起始地址的某个偏移量。
2.虚拟地址是程序在执行时实际使用的地址,它们构成了程序的虚拟地址空间。虚拟地址允许每个程序拥有自己的地址空间,而这个空间不需要直接映射到物理内存。在 hello 程序执行时,程序中的变量访问、函数调用等操作都是通过虚拟地址进行的。
3.线性地址是在段式内存管理中使用的概念,它是通过将段基地址与偏移量相加得到的。在现代操作系统中,线性地址通常等同于虚拟地址,因为现代系统通常使用分页内存管理而不是段式内存管理。
4.物理地址是实际存储在物理内存(RAM)上的地址。物理地址是内存单元在硬件上的实际位置。在 hello 程序执行过程中,当程序访问变量或调用函数时,操作系统的内存管理单元(MMU)会将虚拟地址转换为物理地址,以便 CPU 访问实际的内存单元。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段寄存器包含了段选择器,它指向 GDT 或 LDT 中的一个段描述符。段描述符包含了段的基地址、段界限和访问权限等信息。这些描述符存储在全局描述符表(GDT)或局部描述符表(LDT)中。段选择器是一个16位的值,它由一个索引和请求特权级组成。逻辑地址由段内偏移量和段选择器组成。
转换过程:
1.从指令的地址部分或隐式使用的段寄存器中提取段选择器。
2.使用段选择器中的索引来访问 GDT 或 LDT 中的相应段描述符。
3.根据当前的特权级(CPL)和段描述符中的特权级检查访问权限。
4.将段描述符中的基地址与逻辑地址中的段内偏移量相加,得到线性地址。公式为:Linear Address = Base Address + Offset
5.确保计算出的线性地址没有超出段描述符中定义的段界限。如果超出界限,将触发一个段界限违规异常。
6.如果当前的特权级低于段描述符中允许的特权级,将触发一个保护故障。
假设hello程序运行,并且需要访问数据段中的一个变量。程序会使用 DS 段寄存器中的段选择器和变量的偏移量来形成逻辑地址。处理器将使用这个逻辑地址,通过上述步骤,将其转换为线性地址,然后访问内存中的正确位置。
7.3 Hello的线性地址到物理地址的变换-页式管理
在页式管理中,线性地址到物理地址的变换主要是通过页表进行的。页表是一种数据结构,用于将线性地址映射到物理地址。
具体步骤如下:
1.根据线性地址的页目录索引,找到对应的页目录项。页目录是一级页表,它用于将线性地址的高10位映射到二级页表的物理地址。
2.根据线性地址的页表索引,找到对应的页表项。页表是二级页表,它用于将线性地址的中间10位映射到物理地址的页帧号。
3.根据线性地址的页内偏移,确定物理地址的页内偏移。
4.将页帧号与页内偏移组合,得到物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个高速缓存,用于存储最近或频繁访问的页表项(PTE)。当虚拟地址访问发生时,CPU首先检查TLB,以快速完成地址转换。
四级页表结构包括:页全局目录、页上级目录、页中间目录、页目录、页表。 首先使用虚拟地址的最高几位来索引PGD。使用PGD项指向的PUD,并使用VA的下一组位来索引PUD。使用PUD项指向的PMD,并使用VA的另一组位来索引PMD。使用PMD项指向的PD,并使用VA的另一组位来索引PD。使用PD项指向的PT,并使用VA中的中间位来索引PT。如果TLB命中,CPU直接从TLB获取物理页帧号,并与页内偏移量组合成物理地址。如果TLB未命中,CPU必须访问页表结构来找到对应的PTE,这可能涉及多次内存访问。一旦找到PTE,CPU检查其有效性和访问权限,然后使用PTE中的物理页帧号来计算物理地址。
如果页表项指示页不在物理内存中,CPU触发缺页异常,操作系统将处理此异常,将缺失的页加载到内存,并更新页表。一旦页表项被访问,其信息可能被加载到TLB中,以便将来的访问可以快速完成。
![]() |
7.5 三级Cache支持下的物理内存访问
三级缓存,即称为L1、L2和L3缓存能够极大地提高了处理器访问数据的速度其中L1 Cache是最快速的缓存,分为数据缓存(Data Cache)和指令缓存(Instruction Cache)。L2 Cache 速度次于L1,通常比L1缓存大,可能与一个或多个核心共享。L3 Cache最大的缓存,通常在多个核心之间共享。
当处理器需要数据时,首先在L1数据缓存中查找。 如果L1缓存未命中,处理器会在L2缓存中查找。如果L2缓存也未命中,处理器会在L3缓存中查找。如果L3缓存未命中,处理器将从物理内存(RAM)中加载数据。
多级缓存提供了不同级别的访问速度和容量,使得处理器能够更快地访问数据,同时保持对大量数据的访问能力。
7.6 hello进程fork时的内存映射
当运行hello程序的进程执行fork()系统调用时,操作系统会进行以下步骤来创建一个新的子进程,并设置其内存映射:
1.fork()调用会复制父进程的进程控制块(PCB),为子进程创建一个新的进程ID。
2.子进程获得父进程的地址空间的副本。这包括代码段、数据段、堆、栈等。
3.父进程和子进程的内存页在fork()后被标记为只读。如果子进程尝试写入这些页,操作系统将自动创建这些页的副本,使得父进程和子进程各自拥有独立的副本。
4.子进程获得其独立的栈副本,栈顶指针会相应调整,以确保父子进程的栈是隔离的。
5.子进程继承父进程的文件描述符和其他资源,但这些资源通常是以非共享的方式继承的,以避免父子进程间的冲突。
6.子进程继承了父进程的环境变量,这些变量存储在子进程的内存空间中。
7.7 hello进程execve时的内存映射
在hello程序执行了execve调用。execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用 hello 程序有效地替代了当前程序。加载并运行 hello 需要以下几个步骤:。1.删除已存在的用户区域。2.删除当前进程虚拟地址的用户部分中的已存在的区域结构。3.映射私有区域。4.为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区。.bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。如果hello程序与共享对象(或目标)链接,比如标准C库1ibc,so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个人口点开始执行。Linux将根据需要换入代码和数据页面。
![]() |
7.8 缺页故障与缺页中断处理
1. 缺页故障触发
当程序访问的数据或指令不在物理内存中时,触发缺页故障。这可能是因为数据从未加载到内存中,或者已经被换出到磁盘。
2. 中断处理
缺页故障导致CPU生成一个中断,操作系统的中断处理程序捕获这个中断。
3. 故障分析
中断处理程序首先分析缺页的原因。这可能包括:
访问的页是否有效(检查页表项)。
访问的权限是否适当(用户态或核心态访问)。
4. 页表检查
操作系统检查页表项(PTE)来确定缺页的原因。如果PTE指示页不在内存中,操作系统将处理缺页。
5. 物理内存管理
操作系统需要找到足够的物理内存来加载缺失的页。
6. 内存页面加载
操作系统从磁盘读取所需的数据页或指令页到物理内存中。这涉及到磁盘I/O操作。
7. 更新页表
加载页面后,操作系统更新页表项,将新加载的页的物理地址写入PTE。
8. 重新执行指令
缺页处理完成后,操作系统恢复程序的执行,重新执行导致缺页故障的指令。
![]() |
![]() |
7.9动态存储分配管理
printf函数通常不会直接调用malloc。malloc是一个C语言标准库函数,用于在运行时动态分配内存。然而,printf可能会间接导致动态内存分配,如果它在格式化字符串时需要额外的缓冲区空间。动态内存管理允许程序在运行时根据需要分配和释放内存。
1. 动态内存分配
使用malloc、calloc、realloc等函数从堆(heap)分配内存。这些函数返回一个指向分配内存块的指针。
2. 内存初始化
calloc函数不仅分配内存,而且将分配的内存初始化为零。
3. 内存重新分配
realloc函数用于调整已分配内存块的大小。如果需要更多的内存,它可以将内存块移动到新的地址并增加其大小;如果需要更少的内存,它可以缩小内存块。
4. 内存释放
使用free函数释放之前分配的内存块,使其回到可用的内存池中。
5. 内存分配策略
首次适应: 从头开始搜索,直到找到足够大的空闲内存块。
最佳适应: 搜索整个内存,找到能够满足需求的最小内存块。
最坏适应: 选择最小的内存块,这可能导致较大的内存块被分割。
7.10本章小结
本章讨论了hello的存储器地址空间,Intel逻辑地址到线性地址的变换-段式管理,Hello的线性地址到物理地址的变换-页式管理,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时的内存映射,hello进程execve时的内存映射,缺页故障与缺页中断处理,动态存储分配管理。
结论
hello程序的执行过程是计算机系统工作流程的缩影,涵盖了从源代码编写到程序终止的一系列步骤。包括源代码编写、预处理、编译、汇编、链接、加载、进程创建、内存映射、系统调用、执行、I/O操作、进程终止、资源回收。
1.源代码编写:程序员使用文本编辑器编写hello程序的源代码,这是整个软件开发过程的起点。随后源代码经过预处理。
2.编译:源代码通过编译器转换成汇编代码或直接到机器码。编译过程可能包括预处理、编译、优化等步骤。
3.汇编:汇编器将编译生成的汇编代码转换为机器可以执行的指令。
4.链接:链接器将编译后生成的目标文件与库文件链接,生成一个可执行文件。链接过程中解决程序中的符号引用。
5.加载:加载器将可执行文件加载到内存中,为执行做准备。
6.进程创建:操作系统为hello程序创建一个新进程,分配必要的资源,如内存空间、文件描述符等。
7.内存映射:操作系统为新进程设置内存映射,包括代码段、数据段、堆、栈等,为程序执行提供必要的内存布局。
8.系统调用:hello程序通过系统调用请求操作系统提供的服务,如文件操作、网络通信、进程控制等。
9.执行:CPU执行程序的指令,按照程序逻辑进行运算和控制。
10.I/O操作:程序执行输入输出操作,如将字符串输出到控制台。
11.进程终止:程序完成执行后,通过exit系统调用或从main函数返回来终止进程。
12.资源回收:操作系统回收进程使用的资源,包括内存、文件描述符等。
在模块化与层次、资源管理、性能优化、安全性、可扩展性、容错性都有了新的感悟。
我们要用人工智能技术优化系统性能,如通过机器学习预测资源需求,实现智能调度。探索量子计算在计算机系统设计中的应用,利用量子算法解决传统计算机难以解决的问题。推动绿色计算理念,优化能源使用,减少系统对环境的影响。倡导开放标准和开源软件,促进技术的共享和创新。
附件
hello.c | 源文件 |
hello.i | 预处理文件 |
hello.s | 编译文件 |
hello.o | 汇编文件 |
hello | 可执行文件 |
hello.asm. | 反汇编文件 |
参考文献
- Randal E. Bryant 深入理解计算机系统 第三版 机械工业出版社 2017.4
- 袁春风 计算机系统基础 机械工业出版社第二版 2019.12