【无标题】HIT-计算机系统-程序人生-Hello’s P2P

摘  要

本文详细介绍了hello程序在Linux操作系统上的预处理、编译、汇编、链接和运行过程。首先概述了hello程序的整个生命周期,然后逐步分析了各个阶段的操作命令及其作用,详细解析了每个阶段产生的中间结果和最终结果。通过对hello程序的深入剖析,展示了从源代码到可执行文件的整个流程,并探讨了进程管理、存储管理和IO管理等关键技术细节。本文系统地回顾了计算机系统课程中的内容。

关键词:计算机系统、hello、进程、可执行文件

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

目  录

第1章 概述................................................................................... - 4 -

1.1 Hello简介............................................................................ - 4 -

1.2 环境与工具........................................................................... - 4 -

1.3 中间结果............................................................................... - 4 -

1.4 本章小结............................................................................... - 4 -

第2章 预处理............................................................................... - 5 -

2.1 预处理的概念与作用........................................................... - 5 -

2.2在Ubuntu下预处理的命令................................................ - 5 -

2.3 Hello的预处理结果解析.................................................... - 5 -

2.4 本章小结............................................................................... - 5 -

第3章 编译................................................................................... - 6 -

3.1 编译的概念与作用............................................................... - 6 -

3.2 在Ubuntu下编译的命令.................................................... - 6 -

3.3 Hello的编译结果解析........................................................ - 6 -

3.4 本章小结............................................................................... - 6 -

第4章 汇编................................................................................... - 7 -

4.1 汇编的概念与作用............................................................... - 7 -

4.2 在Ubuntu下汇编的命令.................................................... - 7 -

4.3 可重定位目标elf格式........................................................ - 7 -

4.4 Hello.o的结果解析............................................................. - 7 -

4.5 本章小结............................................................................... - 7 -

第5章 链接................................................................................... - 8 -

5.1 链接的概念与作用............................................................... - 8 -

5.2 在Ubuntu下链接的命令.................................................... - 8 -

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

5.4 hello的虚拟地址空间......................................................... - 8 -

5.5 链接的重定位过程分析....................................................... - 8 -

5.6 hello的执行流程................................................................. - 8 -

5.7 Hello的动态链接分析........................................................ - 8 -

5.8 本章小结............................................................................... - 9 -

第6章 hello进程管理.......................................................... - 10 -

6.1 进程的概念与作用............................................................. - 10 -

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

6.3 Hello的fork进程创建过程............................................ - 10 -

6.4 Hello的execve过程........................................................ - 10 -

6.5 Hello的进程执行.............................................................. - 10 -

6.6 hello的异常与信号处理................................................... - 10 -

6.7本章小结.............................................................................. - 10 -

第7章 hello的存储管理...................................................... - 11 -

7.1 hello的存储器地址空间................................................... - 11 -

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

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

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

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

7.6 hello进程fork时的内存映射......................................... - 11 -

7.7 hello进程execve时的内存映射..................................... - 11 -

7.8 缺页故障与缺页中断处理................................................. - 11 -

7.9动态存储分配管理.............................................................. - 11 -

7.10本章小结............................................................................ - 12 -

第8章 hello的IO管理....................................................... - 13 -

8.1 Linux的IO设备管理方法................................................. - 13 -

8.2 简述Unix IO接口及其函数.............................................. - 13 -

8.3 printf的实现分析.............................................................. - 13 -

8.4 getchar的实现分析.......................................................... - 13 -

8.5本章小结.............................................................................. - 13 -

结论............................................................................................... - 14 -

附件............................................................................................... - 15 -

参考文献....................................................................................... - 16 -

第1章 概述

1.1 Hello简介

在计算机系统中,“从程序到进程”(From Program to Process)描述了一个C程序从编写到在机器上运行的转换过程。这个过程包括以下几个阶段:即hello.c源文件经过cpp预处理生成hello.i,再经过cc1编译生成hello.s汇编语言程序,之后再由as生成 hello.o二进制的可重定位目标程序,再由连接器ld链接生成二进制的可执行目标程序 hello,最后通过加载器将程序加载到内存中并运行,这样就完成了hello.c程序从program到process的转换。上述过程可用下图表示:

020过程(From Zero to Zero)描述的是一个程序从启动到结束的整个生命周期。当用户运行一个程序,例如hello.c,首先系统会检查该命令是否为内置命令。如果不是,系统通过fork函数创建一个新的子进程。这个子进程不会直接执行原有程序,而是通过execve系统调用来加载和运行可执行文件,这个过程中,子进程原有的虚拟内存被清空,并建立新的代码段、数据段、堆和栈段。

此时,操作系统将可执行文件的各个部分映射到新创建的虚拟内存空间中,随后程序开始执行。在执行过程中,通过异常控制流机制,程序所需的数据从磁盘读取到内存中。程序完成任务后,进程将终止,此时控制权返回到操作系统,它将回收子进程使用的资源,包括内存和其他系统资源,使得系统资源的使用情况再次回到了最初的“零”状态。

整个过程概括了一个程序从不存在到执行再到结束的全过程,最终资源被完全回收,系统回归到初始状态。

1.2 环境与工具

硬件环境:Intel Core i7 13700H处理器

软件环境:Windows11 64位专业版/VMware 17 Pro/Ubuntu 22.04 LTS 64位

开发与调试工具:VS code

1.3 中间结果

hello.i(hello.c预处理后的文件)

hello.s(hello.i编译后的文件)

hello.o(hello.s汇编后得到文件)

hello(hello.o经过链接获得的可执行目标文件)

hello.o.asm(hello.o反汇编得到的文件)

hello.asm(hello反汇编得到的文件)

其中,readelf查看的信息均已给出指令,并未保存到文件。

1.4 本章小结

本章主要讨论了hello.c程序的编译和执行过程,涵盖了P2P(程序到进程)和020(从无到有再到无)两个关键过程。还列出了实验过程中所依赖的环境和工具,最后列出了完成本次作业过程中所生成的中间文件。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理概念:预处理是C程序编译的第一步,它直接操作源代码文件的文本,执行如去除注释、宏替换、条件编译和头文件展开等操作。预处理不涉及代码逻辑的分析,仅对代码进行文本级的处理,并生成后缀为.i的预处理后的文件。

预处理作用:

完善代码:预处理通过展开头文件(如使用#include指令)将必要的声明和定义直接插入到源文件中,这有助于完善程序的功能。虽然这使得单个源文件可能失去了独立完整性,但却极大地便利了程序的编写和管理。

帮助编译:预处理器通过宏定义(#define)允许程序员定义编译时替换的符号或代码块,以及通过条件编译(#ifdef, #ifndef等指令)仅编译符合特定条件的代码段。这些功能简化了源代码的复杂性,使得编译过程更为高效。

代码简化:预处理器去除所有注释和多余的空白,只留下有效代码和指令,从而简化了编译器处理的文本量,提高了编译的速度和效率。

2.2在Ubuntu下预处理的命令

Ubuntu下预处理命令:gcc -E -o hello.i hello.c

2.3 Hello的预处理结果解析

2.3.1 预处理文件代码与原文件代码比较

原始的C语言程序包含18行代码,但经过预处理后,代码量扩展到了3092行。这种显著的增加主要是由于源文件中包含的头文件内容被展开。在扩展后的文件中,最后部分是源程序中main函数的代码。在main函数代码之前的部分主要由几个关键组成部分构成:包括的头文件路径、数据类型的重定义、外部函数的声明,以及枚举类型定义,这些枚举通常用于定义常量值。这样的结构展示了预处理如何通过包含头文件和展开宏定义来增加程序代码的行数,并为编译准备完整的代码结构。

2.3.2 头文件路径

头文件的引用通常位于C源文件的开头部分,确保全文可以访问所需的定义和声明。然而,根据代码的特定需求,有时也会在文件的其他部分引用头文件。在这些头文件中,重定义数据类型的段落将标准的C数据类型与在头文件内定义的类型关联起来。除此之外,大部分内容涉及到外部函数的声明,这是代码插入中最为广泛的一部分。此外,很大一部分头文件也涉及到为常用的库函数定义的常量,这些常量通常通过枚举类型进行组织和分类。

2.3.3 数据类型的重定义

数据类型重定义涉及给已存在的数据类型赋予新的名称,或调整其属性以适应特定的应用需求。

2.3.4 外部函数的声明

这种做法使得函数可以跨文件或模块使用,有助于代码的模块化和重用。

2.3.5 枚举类型定义

2.4 本章小结

本章分析了hello.i的生成过程,解释了预处理的作用和实现的功能,最后详细地分析了预处理的结果,讨论了预处理后文件各个部分的意义。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器将预处理后的代码转换成汇编语言。在这一步中,编译器也进行语法和语义分析,确保代码遵守C语言的规则,并且逻辑上也是可行的。

编译的作用:编译不仅使得以高级语言编写的程序能够在特定的计算机硬件上运行,而且还通过编译时的优化提高程序的运行效率和性能,同时在编译阶段进行的错误检查也有助于提高代码的质量和稳定性。

3.2 在Ubuntu下编译的命令

Ubuntu下编译命令:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析

本节将从数据存储、赋值操作、算术操作与比较操作、参数传递、函数调用与返回和循环操作这四个方面对hello的实现进行分析。

3.3.1 数据存储

字符串:.LC0: 这是一个 UTF-8 编码的字符串。.LC1: 格式字符串 "Hello %s %s %s\n",用于 printf 函数。

后续访问时,使用的是rip+段偏移量间接寻址。

       整数:整数主要用于局部变量存储和传递参数。

       在main函数中定义了一个未赋值的局部变量int i,储存在栈中,其生命周期与main函数自身相同,rsp向下移动了32个字节,其中就有给int i预留的空间。具体如下图所示:

       数组:argv 是一个指针数组,其存储方式如下:

数组的操作一般都是通过首地址加上偏移量得到的。movq -32(%rbp), %rax: 加载 argv 指针。addq $24, %rax: 访问数组中的第4个元素(假设每个元素占用8个字节)。movq (%rax), %rcx: 加载数组元素到寄存器。因此通过加8、加16、加24分别可以得到main()函数的第2、3、4个参数的指针。

运行时栈结构如下图所示:

3.3.2 赋值操作、算数操作与比较操作

在汇编语言下,赋值操作使用movl或者movq指令。

加法操作:加法操作在代码中多次出现,主要用于指针运算和循环计数器的增加。

       其中,在hello.c中出现了算术操作“++”,这一操作通过addl $1, -4(%rbp)来完成,在汇编代码中对应于第56行。

       关系操作:比较操作用于条件判断,以控制程序的执行流。

!=在汇编语言中,使用cmp和je的组合实现。cmp 指令进行比较并设置标志位,而 je 指令根据标志位的状态判断是否跳转。

<在汇编语言中,使用cmp和jle的组合实现。同理,通过 cmp 指令进行比较,然后通过 jle 指令根据比较结果进行相应的跳转操作,从而实现小于或等于的条件判断。

3.3.3 参数传递、函数调用与返回

在 hello.c 中,函数调用出现了 7 次:main(), printf(), exit(), printf(), sleep(), atoi(), 和 getchar()。在 x86-64 架构中,函数参数优先通过寄存器传递,当参数数量超过 6 个时,通过栈传递。本例中,各个函数的参数都是通过寄存器传递的,寄存器传递参数的顺序为:%rdi, %rsi, %rdx, %rcx, %r8, %r9(假设参数为 64 位)。

在进入main()后,参数argc和argv已经分别存入到寄存器%edi和%rsi中,之后又赋值到栈中。对于除main()之外的其它函数,都有相应的传参语句,比如sleep和atoi的传参,如下图所示:

       两次printf的调用如下所示:

 

第一次调用时,只有一个参数(字符串)通过寄存器%rdi传入,并转化成puts函数输出。第二次调用时,共有字符串、argv[0]、argv[1]共三个参数,分别通过寄存器%rdi、%rsi、%rdx传入。

       将1作为参数传给%rdi,从而调用了exit函数。

函数返回前的操作通常包括:设置返回值(如果有)。恢复栈帧,使栈指针和基指针恢复到进入函数时的状态。使用 ret 指令返回到调用函数的下一条指令。在本例中,如下图所示:

3.3.4 循环操作

基于比较操作下的比较和代码跳转进行实现。

在标签 .L2 处,将循环计数器初始化为 0。在标签 .L3 处,比较循环计数器与 9,如果小于或等于 9,则继续执行循环体。在标签 .L4 处,逐个加载 argv 参数并传递给 printf 函数,调用 printf 输出参数,调用 atoi 将参数转换为整数,再调用 sleep 函数休眠指定秒数。最后,增加循环计数器。

3.4 本章小结

本章介绍了编译的概念与作用,并给出了在Ubuntu下的编译命令。之后详细的对hello.c的汇编代码进行了分析,从从数据存储、赋值操作、算术操作与比较操作、参数传递、函数调用与返回和循环操作这四个方面对hello的实现进行了分析。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编器将hello.s翻译成机器语言指令,并将结果保存在机器可以读懂的二进制文件即目标文件hello.o中。

汇编的作用:汇编语言生成的机器代码通常比高级语言生成的代码更紧凑和高效,操作系统内核、设备驱动程序等低级系统软件常常需要用汇编语言编写,以实现对硬件的直接控制和高效操作。

4.2 在Ubuntu下汇编的命令

Ubuntu下汇编命令:as hello.s -o hello.o

4.3 可重定位目标elf格式

4.3.1 elf头

ELF头信息显示,该文件的Magic Number是0x7f 45 4c 46,即“ELF”标志。文件类型为REL(可重定位文件),它采用了ELF64格式,表示这是一个64位的文件,并且是小端序。文件的OS/ABI为UNIX - System V,表示其兼容UNIX系统。目标体系结构是Advanced Micro Devices X86-64,表明这是一个针对AMD的x86-64架构的文件。

此外,ELF头还显示入口点地址为0x0,程序头表偏移为0(即没有程序头表),节头表偏移为1088字节,节头表包含14个节,每个节头大小为64字节。最后,节头字符串表的索引为13,用于定位节头字符串表中的字符串。

4.3.2 elf的section头

    使用readelf -S --wide hello.o指令查看。

       该文件包含14个节,每一列分别表明了各个节的名称、类型、地址、偏移量大小、实际大小、标志、链接、信息和对齐。

       例如,.text节包含可执行的程序代码,类型为PROGBITS,大小为0x00b3,具有读取和执行权限。.note.gnu.property节:包含GNU特定的属性信息,类型为NOTE,大小为0x0020。.symtab节:包含符号表,类型为SYMTAB,大小为0x00b0,具有信息标志等等。

4.3.3 重定位节

       使用readelf -a hello.o指令查看以下内容。

    在.rela.text段中,有8个重定位条目,每个条目包含偏移量、信息、类型、符号值和符号名称+加数等信息。这些条目使用R_X86_64_PLT32类型,表示针对x86-64架构的PLT(Procedure Linkage Table)重定位,目标符号如.rodata、puts等,需要在程序运行时解析其实际地址。而在.rela.eh_frame段中,只有一个重定位条目,指向.text段的起始位置。

4.3.4 符号表

第一个符号是一个文件符号hello.c,表示源文件。第二个和第三个符号是段符号,分别表示.text和.rodata段。第四个符号main是一个全局函数,位于.text段。其余符号,如puts、exit、printf等,都是未定义的全局符号,需要在程序运行时解析它们的地址。

4.4 Hello.o的结果解析

使用objdump -dr hello.o > hello.o.asm指令查看。

机器语言由一系列二进制指令组成,每条指令包括操作码(opcode)和操作数(operand)。操作码表示要执行的操作,而操作数提供操作的数据或地址。

与汇编语言的映射关系如下:

机器语言的每条指令在汇编语言中都有对应的操作码和操作数。例如,push %rbp 在机器语言中对应 55,mov %rsp, %rbp 对应 48 89 e5。

内存操作在机器语言中通过操作码和内存地址表示。例如,mov %edi, -0x14(%rbp) 对应 89 7d ec,其中 89 是操作码,7d ec 是偏移地址。

条件跳转指令在机器语言中通过操作码和目标地址表示。例如,je 32 <main+0x32> 对应 74 19,其中 74 是条件跳转指令,19 是偏移地址。无条件跳转指令如 jmp 在机器语言中也有相应的操作码和偏移地址。例如,jmp 91 <main+0x91> 对应 eb 56。

函数调用指令在机器语言中通过 call 指令和目标地址表示。例如,call 28 <main+0x28> 对应 e8 00 00 00 00,其中 e8 是调用指令,00 00 00 00 是偏移地址。

立即数操作在机器语言中直接编码在指令中。例如,mov $0x1, %edi 对应 bf 01 00 00 00,其中 bf 是操作码,01 00 00 00 是立即数。

4.5 本章小结

本章简要介绍了汇编语言的概念和作用,并给出了在Ubuntu系统下使用的汇编命令。之后分析了ELF格式的结构,包括ELF头、符号表、重定位条目和个section节等。最后比较了hello.s汇编文件与hello.o文件反汇编得到的代码,给出了二者之间的不同。

(第41分)

5章 链接

5.1 链接的概念与作用

链接的概念:链接是计算机程序开发过程中将多个目标文件和库文件组合成一个单一可执行文件、共享库或静态库的关键步骤。这个过程包括符号解析、重定位和最终生成可执行文件或库文件。链接可以分为静态链接和动态链接两种方式。

链接的作用:链接将编译后的各个模块组合成一个完整的程序,从而确保程序的各部分能够正确协同工作。同时,链接也使分离编译成为可能,这样在开发大型软件中可以将源文件分解为更小、更好管理的模块,可以独立地修改和编译这些模块,从而提高开发和维护效率。

5.2 在Ubuntu下链接的命令

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的格式

5.3.1 elf头

使用readelf -h hello指令查看。

对比分析hello.o的elf头可知,hello.o作为一个可重定位文件,其结构较为简单,没有程序头表和入口点,因为它仅包含代码和数据片段,等待链接器将其整合到最终的可执行文件中。而hello文件则包含了更多的信息,如程序头表和入口点地址,使其可以在操作系统中直接运行。可执行文件包含了程序运行所需的所有指令和数据,链接器在将多个可重定位文件合并时,会处理符号解析和重定位,生成最终的可执行文件。

5.3.2 elf的section头

       发现hello共有27个节,对比分析hello.o的section节可知,因为此时已不需要进行重定位,所以已不存在记录重定位信息的‘.rela.text’和‘.rela.eh_frame’。

       并且,hello文件中出现了更多与动态链接和运行时相关的节,如.interp, .dynamic, .dynsym, .dynstr, .gnu.version, .gnu.version_r等。

5.3.3 程序头

       使用readelf -l hello指令查看以下内容。

       每个程序头记录了一个内存段的信息或用于准备程序执行的内存详情。需要注意的是,ELF文件的节和内存段之间并非一一对应关系,一个内存段可能包含一个或多个节。程序头主要对可执行文件和共享库文件有意义,而对于其他类型的目标文件,程序头的信息通常可以忽略。

5.4 hello的虚拟地址空间

5.3.1虚拟内存信息

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

在加载器执行加载操作时,会在代码段、数据段和虚拟内存之间建立映射关系。具体来说,这些映射关系包括.init段、.text段、.rodata段、.data段、.bss段以及其他动态链接部分。

       通过查看ELF文件中的节头表,可以确定各个节的起始地址。使用edb的书签功能,可以对这些地址进行查找,从而查看各个节对应的汇编代码内容。

       .interp节的起始地址为0x4002e0。

5.4.2虚拟内存中.text节

.text节的起始地址为0x4010f0。

5.4.3虚拟内存中.data节

.data节的起始地址为0x404048。

5.5 链接的重定位过程分析

       使用objdump -d hello > hello.asm指令将可执行程序hello反汇编后得到信息导出到hello.aso文件中。

链接器将目标文件中的符号解析为具体的内存地址。例如,puts、printf等函数的调用在链接后被解析为具体的地址。链接器使用重定位表将所有符号引用替换为正确的内存地址。重定位表包含了所有需要重定位的地址和对应的符号信息。

在目标文件hello.o中,外部函数调用如puts、printf等都是未解析的,需要在链接阶段通过重定位表来解析。在可执行文件hello中,链接器已经解析了这些外部函数的地址,并将代码中的符号引用替换为具体的地址。例如,call 401090 <puts@plt>表示链接器将对puts函数的调用解析为具体的地址401090。

       如上图,hello.o反汇编中的重定位条目在 hello反汇编中替换为虚拟内存地址。

5.6 hello的执行流程

使用edb将断点打在0x4010f0(_start)处,开始调试,查看程序执行的流程。

Ld-2.27.so! _dl_start

Ld-2.27.so! _dl_init

_start

_libc_start_main

_GI__cxa_atexit

_new_exitfn

_setjmp

__sigsetjmp

__sigjmp_save

_libc_csu_int

Init

Main

Puts

Printf

Atoi

Sleep

Getchar

__GI_IO_puts

__strlen_avx2

_IO_new_file_xsputn

IO_valodate_vtable

Exit

__GI_exit

__run_exit_handlers

5.7 Hello的动态链接分析

 ELF采用了一种称为延迟绑定的技术,当程序调用共享库中的函数时,只有在首次使用该函数时才会绑定。hello程序通过该技术以及PLT和GOT进行动态链接。GOT存储函数目标地址,而PLT则使用GOT中的地址跳转到目标函数。

       首先通过前面的节头部表获得got的地址,.got的位置是0x403ff0。在执行 _dl_init 之前,.got 表是全0的,在执行 _dl_init 之后,.got 表则变成了相应的值,从而使这些被调用的函数链接到动态库。

下图为_dl_init前条目:

5.8 本章小结

本章介绍了链接的概念及其作用,并提供了在 Ubuntu 下使用的链接命令。随后,对可执行文件 hello 的结构进行了详细说明,并对 hello 的虚拟地址空间进行了描述。接着,深入分析了链接的重定位过程以及 hello 的执行流程,最后对 hello 的动态链接过程进行了简要分析。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

进程的概念:进程是计算机中正在运行的程序的实例,它不仅包括可执行程序的代码,还包含程序计数器、寄存器和变量等信息。每个进程在操作系统中都是一个独立的实体,有自己的地址空间和资源。在多任务操作系统中,进程是资源分配和任务调度的基本单位,它们通过进程控制块(PCB)来管理和保存自身的状态信息。

进程的作用:进程提供了程序执行的基本环境,使得程序能够顺利运行。并且通过独立的地址空间和资源管理,确保不同程序之间的隔离性和安全性。操作系统通过调度和管理多个进程,实现多任务处理,从而提高系统的资源利用率和响应速度。此外,进程还支持并发执行,允许多个程序同时运行,为用户提供了高效的计算体验。

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

Shell-Bash的作用:将用户输入的命令翻译成操作系统能够理解和执行的操作。并且提供脚本语言的功能,使用户可以编写脚本来自动执行一系列命令,从而简化重复性任务。还能提供命令用于创建、删除、移动和修改文件和目录。也能允许用户启动和管理进程,包括前台和后台进程的控制。最后管理用户环境变量和系统设置,影响命令的执行方式和输出。

Shell-Bash的处理流程:

Bash从用户输入设备(如键盘)或脚本文件中读取命令行输入。然后将输入的命令行进行词法分析和语法分析,分解成命令、选项和参数。根据用户输入,查找命令的路径。内置命令直接执行,外部命令则通过查找路径(PATH环境变量)定位可执行文件。

执行命令:直接在Shell中执行。当为外部命令时,创建子进程并执行找到的可执行文件。之后,根据命令中的重定向符号(如>, <, |),重定向标准输入、输出和错误输出。返回执行结果和状态码。成功执行通常返回0,错误执行返回非零状态码。

在命令执行完毕后,Bash再次显示提示符,等待用户输入下一条命令。

6.3 Hello的fork进程创建过程

在shell中输入命令行./hello 2022112632 sbr 18246154537 7后,shell将命令行转存到参数列表并进行解析。

       首先判断hello不是一个内置的shell命令,然后调用fork函数创建一个子进程。fork函数的原型为pid_t fork(void); 新创建的子进程中fork函数返回的PID为0,而在父进程中fork函数返回的为子进程的PID。除此之外,其拥有和父进程完全相同的虚拟地址空间副本,相对父进程来说是独立的进程。二者有相同的代码段和数据段、堆,共享库和用户栈。

       父进程调用 fork 时,子进程继承了父进程的所有打开的文件描述符,因此子进程可以读写父进程已打开的任何文件。

6.4 Hello的execve过程

在使用fork创建新进程之后,shell调用execve函数在当前的进程上下文中加载并运行hello。总共有4步:

删除已存在的用户区域:现有的用户地址空间被清除,以确保新程序可以在一个干净的环境中运行。

映射私有区域:为 hello 程序的代码段、数据段、.bss 段和栈段创建新的区域结构,确保这些段在子进程的地址空间中正确映射。

动态链接 hello 程序:将 hello 程序需要的共享库映射到共享区域,并解析所有动态链接所需的符号,以确保程序可以正确执行。

设置程序计数器 (PC):将程序计数器设置为 _start 地址,这样当子进程开始执行时,会从 hello 程序的入口点开始运行。

       此时,argc = 5。

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

进程上下文信息:进程上下文包含了进程在CPU上执行时的所有状态信息,包括程序计数器、通用寄存器、堆栈指针、段寄存器和进程状态字等。在进程切换时,操作系统必须保存当前进程的上下文信息,以便在下次调度该进程时能够恢复其运行状态。

进程时间片:时间片是操作系统分配给每个进程在CPU上执行的时间单位。时间片调度策略是常用的一种调度算法。在这种策略下,每个进程被分配一个固定的时间片,当时间片用完时,操作系统会强制进程停止运行并调度下一个进程。

进程调度的过程:首先先确定调度点,可以是进程主动放弃CPU(例如,调用了阻塞操作或进入等待状态)。或者是进程的时间片用完。以及有更高优先级的进程需要执行。也可能时是当前进程终止。然后调度器根据调度算法(如最短作业优先、多级队列调度等)选择下一个要执行的进程。如果当前正在执行的进程没有完成,其上下文信息需要保存到进程控制块(PCB)中。在这之后,调度器从新进程的PCB中恢复其上下文信息。最后操作系统将CPU控制权交给新进程,新进程开始执行。

用户态与内核态转换:在用户态下,进程的权限受限,只能访问自己内存空间中的资源,不能直接访问硬件或进行系统级操作。在内核态下,进程具有完全的访问权限,可以执行系统调用,直接访问硬件设备,进行内存管理等操作。在系统调用、中断处理、异常处理时都会发生用户态与内核态的转换。

在执行 ./hello 2022112632 sbr 18246154537 7时,操作系统创建一个新的进程来执行 hello 程序。调度器将新创建的进程调度到CPU上执行。hello 进程在用户态下开始执行其代码,解析传递的参数,并执行相应的功能。然后会从用户态转变为内核状态并执行系统调用程序,执行结束时会检查并处理信号,然后从内核态转变为用户态,继续执行用户程序。最后,通过系统调用(如 exit)通知操作系统,该进程终止,释放资源。

6.6 hello的异常与信号处理

6.6.1出现的异常

在执行 hello 程序的过程中,可能会产生以下多种信号:比如通过键盘输入 Ctrl+C 可以向前台进程发送 SIGINT 信号。通过键盘输入 Ctrl+Z 可以向前台进程发送 SIGTSTP 信号。当一个子进程终止时,内核会向其父进程发送 SIGCHLD 信号。fg向被挂起的进程发送SIGCONT 信号。kill发送SIGKILL 信号。

6.6.2不停乱按与回车

在没有输入回车的情况下,输入的字符串被缓存到缓冲区,没有任何反应。输入回车后,回车同样被存储到缓冲区。

6.6.3 Ctrl+C

6.6.4 Ctrl+Z

6.6.4 Ctrl+Z 后 fg

    使用 fg 命令可以将最近挂起的进程或指定的后台进程恢复到前台。Ctrl+Z发送 SIGTSTP 信号后,输入 fg,fg 命令会向被挂起的进程发送 SIGCONT 信号,通知进程继续执行。

当进程收到 SIGCONT 信号后,操作系统会恢复该进程的上下文信息,将其状态从停止状态改为运行状态。进程在用户态中恢复执行,从它被暂停的地方继续运行。

6.6.5 Ctrl+Z 后 jobs

jobs 命令,显示当前终端中的所有后台和挂起的作业。

6.6.6 Ctrl+Z 后 ps

ps 命令,显示当前终端会话中的所有进程,可以查看进程的PID、TTY、执行时间和命令。尽管 hello 进程处于挂起状态,但 ps 命令仍然列出了它的信息(PID 7334),表明它仍然在系统中存在,但暂停执行。

6.6.6 Ctrl+Z 后 pstree

6.6.6 Ctrl+Z 后 kill

输入 kill -9 7334后,向进程 hello发送SIGKILL信号(信号编号9)。SIGKILL 信号的默认行为是立即终止进程,并且无法被捕捉、阻塞或忽略。

hello 进程收到 SIGKILL 信号后,操作系统立即终止该进程,释放其所有资源。hello 进程被标记为已终止,显示为“已杀死”。

6.7本章小结

本章介绍了进程的概念与作用以及shell的作用以及处理流程,然后聪慧hello程序的fork进程创建过程、execve过程、hello的进程执行做出了详细的分析,最后,针对hello的异常与信号处理,用了各种指令和信号做出测试。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:逻辑地址是程序员在源代码中看到的地址,在程序编译时,这些逻辑地址会被转换为相应的线性地址。例如,程序中的 argv 是一个指针数组,存储了命令行参数的地址。argv 是一个逻辑地址,表示在程序的地址空间中的位置。

线性地址:线性地址是逻辑地址经过段偏移转换后的地址。在x86架构中,逻辑地址由段选择子和段内偏移组成,CPU通过段寄存器和段描述符表将逻辑地址转换为线性地址。线性地址是一个扁平的32位或64位地址空间。假设 argv 指向的数据段基址是 0x400000,则 argv 的逻辑地址会被转换为线性地址。

虚拟地址:虚拟地址是操作系统提供的一个抽象层次,使每个进程都认为自己在使用整个内存空间。虚拟地址通过页表机制被映射到物理内存地址。每个进程有独立的虚拟地址空间,可以避免进程之间的相互干扰。

物理地址:物理地址是实际的内存芯片上的地址。虚拟地址通过页表映射到物理地址,从而实现程序代码和数据的访问。

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

段式管理涉及段选择子和段内偏移的使用,结合段寄存器和段描述符表(如GDT或LDT)进行地址转换。

段选择子是一个16位的值,包含三个部分。索引:指向段描述符表中的一个段描述符。TI(Table Indicator):指示使用GDT(全局描述符表)还是LDT(局部描述符表)。RPL:请求的特权级别。段内偏移是从段基址开始的偏移量,表示具体地址在段内的位置。

先从逻辑地址中提取段选择子。根据段选择子的索引部分定位到GDT或LDT中的相应段描述符。如果TI=0,段选择子指向GDT;如果TI=1,段选择子指向LDT。将段描述符中的基址和逻辑地址中的段内偏移量相加,得到线性地址。

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

虚拟内存被划分为固定大小的块,称为虚拟页。物理内存也被划分为相同大小的块,称为物理页。每个进程都有一个页表,用于存储虚拟页号到物理页号的映射关系。页表项包括虚拟页号、物理页号以及一些控制信息(如有效位、修改位等)。

先使用虚拟地址的虚拟页号 (VPN) 通过页表基址寄存器 (PTBR) 找到页表项。然后检查页表项中的有效位。如果有效位为1,则该页在内存中,可以继续转换过程;如果有效位为0,则需要从外存中加载该页。之后从页表项中读取物理页号 (PPN)。最终的物理地址由物理页号 (PPN) 和虚拟页偏移量 (VPO) 组成。

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

7.4.1 TLB加速地址翻译

TLB是一个小型的、快速的硬件缓存,存储了最近使用的虚拟页号到物理页号的映射(即页表项的副本)。当处理器需要将虚拟地址转换为物理地址时,它首先在TLB中查找。

如果TLB命中,则可以直接从TLB中获取物理页号,无需访问主存中的页表。否则,则需要访问主存中的页表来获取物理页号。此时,处理器将该映射加载到TLB中,以便未来的访问可以更快。

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

在现代计算机系统中,虚拟地址(VA)到物理地址(PA)的转换是通过页表(page table)完成的。以四级页表(four-level page table)为例,它包括四级页目录:页全局目录(PGD)、页上级目录(PUD)、页中间目录(PMD)和页表(PTE)。

当CPU访问内存时,首先检查TLB是否命中。若命中,则直接使用TLB中的映射完成地址转换;若未命中,则需要访问四级页表完成转换,并将结果存入TLB,以加速后续相同地址的访问。

访问四级页表,首先使用虚拟地址的最高位部分索引页全局目录(PGD),找到对应的页全局目录项(PDE)。然后,使用虚拟地址的次高位部分索引页上级目录(PUD),找到对应的页上级目录项(PDE)。接着,使用虚拟地址的中间位部分索引页中间目录(PMD),找到对应的页中间目录项(PMDE)。最后,使用虚拟地址的低位部分索引最终的页表项(PTE),从而找到实际的物理页框(PFN)。此时,通过将PFN与虚拟地址的页内偏移(offset)结合,得到最终的物理地址(PA)。

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

7.5.1三级Cache介绍

在现代计算机体系结构中,为了优化物理内存访问,通常采用多级缓存(Cache)架构,其中三级缓存(L1、L2、L3)是常见的设计。

L1缓存是最快速的缓存,通常分为指令缓存(L1I)和数据缓存(L1D),每个核心各自独立拥有自己的L1缓存。L1缓存的容量较小(通常在32KB到64KB之间),但其访问延迟极低,通常在几个时钟周期内。

L2缓存的容量较大(通常在256KB到512KB之间),但访问速度稍慢于L1缓存。L2缓存通常也是每个核心独立拥有,主要用于存储那些未能在L1缓存中命中的数据和指令。

L3缓存的容量最大(通常在几MB到几十MB之间),并且通常是共享的,即多个核心共用一个L3缓存。L3缓存主要用于存储L1和L2缓存未命中的数据,以减少对主存的访问频率。L3缓存的访问延迟较高(几百个时钟周期),但仍显著低于主存访问延迟。

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

在物理内存访问的过程中,当CPU需要访问某个物理地址时,先将物理地址分为 CT(标记)+CI(索引)+CO(偏移量),然后检查L1缓存是否命中。如果命中,数据会立即被读取;如果未命中,则检查L2缓存。如果L2缓存也未命中,则检查L3缓存。只有当L3缓存也未命中时,才会访问主存。找到之后则加入 cache,并返回结果。受益与局部性,通常命中的概率比较大,平均读取时间较快。

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mmstruct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序并替代了当前程序。需要以下几个步骤:

删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。映射私有区域。

为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为文件中的.text和.data区。bss区域是请求二进制零的,映射到名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。

映射共享区域。hello程序与共享对象链接,比如标准C库1ibcso,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

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

当CPU访问一个虚拟地址时,它首先通过地址转换机制(页表或TLB)将虚拟地址转换为物理地址。如果所访问的虚拟地址在当前页表中未找到对应的物理地址映射,系统就会产生一个缺页故障。这种情况可能是因为该页面从未加载到内存中,或者页面被交换到磁盘上。

       缺页故障会触发一个中断信号,通知操作系统处理这个异常。当缺页中断发生时,CPU会保存当前的上下文,操作系统根据缺页中断的地址,从页表或相关数据结构中查找对应的页面信息。如果页面不在内存中,操作系统会分配一个物理内存框架(Page Frame)来加载该页面。如果内存已满,可能需要执行页面置换算法(如LRU或FIFO)来释放一个页面。这之后,操作系统将所需页面从磁盘或其他存储设备加载到分配的物理内存框架中。并更新页表,恢复之前的上下文。

7.9动态存储分配管理

动态内存管理是计算机程序在运行时分配和释放内存空间的一种方式。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系假设堆是一个请求二进制零的区域,它紧接在未初始化的区域后开始,并向上生长。对于每个进程,内核维护着变量brk,它指向堆的顶部。

分配器有两种基本风格。两种风格都要求应用显示地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。显示分配器要求引用显示的释放任何已分配的块,malloc函数分配一个块,并调用free函数释放一个块。隐士分配器也叫垃圾收集器,自动释放已使用的分配的块。

隐式空闲链表分配中,内存块的基本结构如下:

当malloc函数分配内存时,它会返回一个指向有效载荷起始处的指针,而在这之前的头部包含了该块的元数据。头部的块大小字段包含了整个内存块的大小(包括有效载荷和填充),而分配标志则指示该内存块是否已分配。

通过这样的内存管理机制,操作系统或运行时库能够高效地管理和利用内存,确保系统运行的稳定性和性能。

7.10本章小结

本章详细探讨了“hello”程序的存储管理机制,涵盖了存储器地址空间布局、逻辑地址到线性地址的段式管理、线性地址到物理地址的页式管理、TLB加速地址翻译、四级页表支持下的VA到PA转换、三级Cache支持下的物理内存访问、进程fork和execve时的内存映射、缺页故障与中断处理,以及动态存储分配管理,全面解析了现代计算机系统的内存管理策略。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件。在Linux系统中,所有设备(包括硬件设备和虚拟设备)都被抽象为文件。这个文件可以是字符设备文件、块设备文件或网络设备文件。通过将设备抽象为文件,Linux提供了一种统一的访问和管理设备的接口,使得用户和应用程序可以通过标准的文件操作来访问和控制设备。

字符设备通常位于/dev目录下,常见的有/dev/tty、/dev/console等。块设备同样位于/dev目录下,常见的有/dev/sda、/dev/nvme0n1等。网络设备通常不直接映射为文件,但可以通过文件系统接口进行操作,例如通过/proc或/sys文件系统查看和配置网络设备。

设备管理:Unix IO接口。Linux系统采用Unix IO接口来管理设备,通过一组系统调用实现对设备的控制和数据传输。比如,ioctl():用于设备的特定控制操作,允许用户程序直接与设备驱动程序进行交互。

通过这些Unix IO接口,Linux系统提供了一种统一且灵活的设备管理机制。同时,设备驱动程序负责具体设备的操作细节,屏蔽了硬件差异,简化了上层应用的开发。

8.2 简述Unix IO接口及其函数

open():用于打开一个文件或者设备,返回一个文件描述符,用于后续的I/O操作。原型:int open(const char *pathname, int flags, mode_t mode)。参数: pathname为要打开的文件路径。flags为打开文件的方式,如只读、只写、读写等。mode为文件的权限,只有在创建新文件时才使用。

flags可以为O_RDONLY 只读,O_WRONLY 只写,O_RDWR 可读只写

close():关闭一个文件描述符,使其不再指向任何文件。原型:int close(int fd)。其中,fd为要关闭的文件描述符。

read():从文件描述符读取数据。原型:ssize_t read(int fd, void *buf, size_t count)。参数:buf为存储读取数据的缓冲区。count为要读取的字节数。

write():向文件描述符写入数据。原型:ssize_t write(int fd, const void *buf, size_t count)。

dup()和dup2():复制文件描述符。原型:int dup2(int oldfd, int newfd)。其中,oldfd为要复制的文件描述符。newfd为新的文件描述符。

8.3 printf的实现分析

观察printf代码发现调用printf时,参数会被传递给vsprintf进行处理。vsprintf 是一个变参函数,用于将格式化的字符串存储在一个缓冲区中。其实现包括解析格式字符串和参数,并将生成的字符串写入指定的缓冲区。下面为vsprintf的代码。

格式化后的字符串通过 write 系统调用写入到标准输出。write 函数通过系统调用接口进入内核态。常见的是使用 int 0x80 或 syscall 指令。终端驱动程序接收内核传递的数据,将其转换为字符,并显示在屏幕上。具体实现包括字符编码转换(如ASCII到字模),然后将字符的点阵信息写入显存(VRAM)。VRAM 是用于存储显示数据的内存,每个像素点的颜色信息(RGB值)存储在显存中。

显示芯片按照设定的刷新率从VRAM逐行读取数据,并传输到显示器。显示器接收信号,并将每个像素点的RGB信息转换为实际的光信号显示出来。

8.4 getchar的实现分析

getchar 函数是一个标准C库函数,用于从标准输入读取一个字符。若第一次调用getchar()时,输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取。下面为其原代码。

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar 函数最终调用系统的 read 函数,从键盘缓冲区中读取数据。但read 函数会阻塞,直到有数据可读(即用户按下一个键)。当用户按下回车键时,read 函数会返回读取的字符。

8.5本章小结

本章介绍了Linux的IO设备管理方法,涵盖设备分类、设备文件和驱动程序。简述了Unix IO接口及其函数,详细分析了printf和getchar的实现过程,包括从格式化字符串生成到系统调用处理的全过程。

(第81分)

结论

用计算机系统的语言,逐条总结hello所经历的过程。

       预处理:使用预处理器将hello.c中的宏展开、头文件包含处理、条件编译等预处理指令处理完成。从而得到hello.i文件。

       编译:hello.i通过编译转化为汇编指令,得到hello.s文件。

       汇编:将hello.s中的汇编代码转化为机器代码,得到hello.o文件。

       链接:hello.o在链接过程中进行符号解析和重定位,得到可执行文件hello。

       加载:操作系统加载可执行文件hello到内存,初始化进程地址空间,包括代码段、数据段、堆栈等等,并准备好进程执行环境。

       执行:通过execve系统调用执行可执行文件hello,操作系统调度器将CPU控制权交给进程,开始顺序执行代码。

IO管理:程序通过标准库函数进行输入输出操作,底层通过系统调用与内核交互,最终能够与硬件交互,从而实现了真正的IO操作。

       进程管理:操作系统负责进程的创建、调度、切换及终止,保证各个进程的独立运行及资源的合理分配。

       存储管理:虚拟内存的使用。

       通过对hello程序的逐步分析,我深刻体会到计算机系统的各个层次协同工作的重要性。每一个环节都有其不可替代的作用。操作系统通过系统调用桥接用户程序与硬件设备,驱动程序则具体实现对硬件的控制。这些机制保障了计算机系统的稳定、高效运行。

       至于创新理念与新的设计与实现方法,我认为可以异构计算资源管理,在多核及异构计算环境下,优化任务调度策略,充分利用不同处理器的特性,提高计算性能。

(结论0分,缺失 -1分,根据内容酌情加分)

附件

hello.i(hello.c预处理后的文件)

hello.s(hello.i编译后的文件)

hello.o(hello.s汇编后得到文件)

hello(hello.o经过链接获得的可执行目标文件)

hello.o.asm(hello.o反汇编得到的文件)

hello.asm(hello反汇编得到的文件)

其中,readelf查看的信息均已给出指令,并未保存到文件。

(附件0分,缺失 -1分)

参考文献

  1.  Randal E.Bryant等.深入理解计算机系统(原书第3版)[M]. 北京:机械工业出版社 2016.7:2.
  2.  X86汇编调用框架浅析与CFI简介_cfi directive-CSDN博客 X86汇编调用框架浅析与CFI简介
  3.  逻辑地址、物理地址、虚拟地址_虚拟地址 逻辑地址-CSDN博客逻辑地址、物理地址、虚拟地址
  4. 预处理、编译、汇编和链接_已知hello.h和hello.c两个文件,按所需命令写在下划线上-CSDN博客 预处理、编译、汇编和链接
  5. 段页式访存——逻辑地址到线性地址的转换_某采用段页式管理系统中,一操作数的逻辑地址为9976h,若逻辑地址格式为段号(3-CSDN博客 段页式访存——逻辑地址到线性地址的转换
  6. https://www.cnblogs.com/pianist/p/3315801.html printf函数实现的深入剖析
  7. 理解链接之链接的基本概念 - 简书 理解链接之链接的基本概念
  8. https://zhuanlan.zhihu.com/p/367223273 浅析 Linux 中的 I/O 管理

(参考文献0分,缺失 -1分)

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值