> 技术文档 > HIT CSAPP 程序人生

HIT CSAPP 程序人生

计算机系统

大作业

题     目  程序人生-Hello’s P2P  

专       业 工科试验班(计算机与电子通信类)

学     号       2023112184      

班     级         23L0512        

学       生         申文圆    

指 导 教 师          史先俊       

计算机科学与技术学院

2025年5月

摘  要

本研究以hello.c初始的24行代码为出发点,深入分析该程序在linux系统下被支配管理的一生,解答初入linux编程的疑惑,从整体层面上对计算机系统做系统的阐述。本文以研究报告的形式呈现,以标准的模板顺序依次进行介绍分析,逐渐从软件层面过渡到硬件层面进行阐述,内容包括从生成可执行文件:预处理、编译、汇编、链接,到利用fork/execve创建进程等过程(P2P阶段),以及程序在从创建到退出时系统内部的相关管理:进程管理、存储管理等(020阶段)。本文的各项截图和数据都是基于linux内核的相关指令及hello.c程序和系统内部的调用,核心内容是对linux内核下hello.c程序的深入理解,这正符合这门课的标题:深入理解计算机系统。

关键词:linux系统;编译系统;进程管理;内存管理;计算机系统

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

目  录

第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简介

P2P是Program to Process的缩写,意指从程序到进程,其过程涵盖生成可执行文件和利用fork/execve创建进程等操作,其中生成可执行文件涵盖预处理、编译、汇编和链接四个过程,如图1所示。

图1.编译系统的处理过程

在编译系统中,源文件hello.c经过预处理器(cpp)的加工,产生了包含系统头文件的hello.i文件;再经过编译器的处理,产生了以汇编语言形式存储的hello.s文件;接下来汇编器将hello.s翻译成机器语言指令,并将指令打包成可重定位目标程序,形成hello.o,此时文件就已是二进制文件,难以阅读;最后链接器将hello.o文件与printf.o等被调用函数的预编译好的文件相链接,组成了可执行的二进制文件。至此,编译系统的工作就此完成,我们得到了一个hello可执行程序。

当我们在linux虚拟机终端bash中输入./hello时,系统自动使用fork创建新的进程,并使用execve加载执行可执行文件,至此可执行程序从静态的程序(Program)转变成了正在运行的进程(Process),P2P阶段至此完结,此外程序的Process阶段还包括虚拟地址空间分配、时间片分配,此处不过多赘述。

020是Zero to Zero的缩写,顾名思义是程序从无到有再到资源完全释放的完整生命周期,指的是程序从创建到终止的整个周期中的相关过程。承接P2P阶段,用户在bash输入./hell之后,进程管理、存储管理、IO管理齐上阵。进程管理部分的工作包括利用fork创建子进程,execve调用加载可执行文件,申请CPU时间片,并进行异常信号处理等;内存管理部分包括段式管理、页式管理,管理各类内存映射,VA到PA、3级Cache下的访问。系统在进程、存储、IO上的管理使得程序能从Zero开始运行,并被系统通过各类手段不断加速,变的可交互,实现了程序的从零开始。

当程序被终止时,操作系统通过信号处理终止进程,释放占用的内存、页表、文件描述符等资源,存储管理回收虚拟地址空间、页文件,清理残留数据,CPU时间片、Cache缓存等被重置,系统回归初始状态,再次回到Zero。

1.2 环境与工具

硬件环境:CPU:13th Gen Intel(R) Core(TM) i7-13650HX;2.60 GHz;RAM:16.0 GB;

软件环境:Win11+Vmware Workstation 17.6.3 + ubuntu-22.04.4-desktop-amd64

开发工具:linux bash自带的调试工具;MobaTextEditor;

图2.虚拟机相关配置

1.3 中间结果

hello.c C语言源文件

hello.i:编译系统预处理后的文件,包含系统头文件;

hello.s编译器处理后的汇编文件;

hello.o汇编器处理后的可重定位目标程序,内容为二进制机器语言;

hello 可执行文件

hello.elf hello.o的elf格式文件

1.4 本章小结

本章从P2P和020两个阶段的不同视角下概述了程序从静态到动态的处理过程,简要概括了系统内部存储管理、进程管理、IO管理在程序运行时所做的工作以及程序终结时系统的操作。本章还介绍了本研究所基于的设备和在研究过程中产生的相关文件。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

概念

预处理是编译流程的第一个阶段,发生在编译器对源代码进行语法分析和语义分析之前,由预处理器cpp完成,它的主要任务是对源代码进行文本级别的处理,遵循预定义的预处理指令,对以#开头,如#include、#define、#ifdef等进行处理,将原始代码转换为更适合编译器处理的形式。预处理的输入是高级语言源代码,输出是预处理后的中间文件,该文件会被传递给编译器进行编译处理。

作用

1.宏定义与替换:通过#define指令定义宏(符号常量或函数式宏),预处理阶段会将代码中所有宏名称替换为对应的文本,例如#define PI 3.14159会将代码中所有PI替换为3.14159。

2.文件包含:通过#include指令将指定头文件(如.h文件)的内容插入到当前源文件中,例如 #include

,cpp从编译器指定的标准库路径查找头文件。

3.条件编译:通过#ifdef、#ifndef、#if、#else、#endif等指令,根据条件选择性地包含或排除代码段。

4.删除注释与空白符:预处理会移除源代码中的所有注释,包括//和/**/,并将连续的空白符,例如空格、制表符、换行符等简化为单个空格,以减少后续阶段的处理负担。

2.2在Ubuntu下预处理的命令

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

图3.虚拟机操作过程和相关文件列表

文件夹中原本只存在hello.c文件,经过gcc预处理指令后产生了hello.i文件,其为预处理后包含头文件的程序文本文件。

2.3 Hello的预处理结果解析

图4.hello.i文件的最后部分代码截图

Hello.c程序只有24行,但预处理后的文件有3092行,其原因是cpp将相应的头文件包含的内容复制到.i文件中,对源文件进行了巨量扩展。观察源文件中包含:#include 、#include 、#include 这三个头文件,而观察.i文件,可以发现源文件相关的注释已经被删除,在第13、743、2221行分别找到下列内容,分别指明了上述头文件的起始位置,这些扩展也符合上述对预处理功能的阐述。

图5.hello.i文件中的部分指示片段

2.4 本章小结

本章从预处理的角度分析了预处理的概念和功能,并从实际出发,对hello.c文件进行预处理得到hello.i文件,并分析了预处理后的文件的结构,在文件中进一步印证了预处理的作用。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念

编译是是编译系统的核心阶段,位于预处理之后、目标代码生成之前。它的主要任务是将预处理后的源代码转换为中间表示形式或直接生成目标机器代码,需经过词法分析、语法分析、语义分析、中间代码生成、代码优化等一系列逻辑步骤。编译的输入是预处理后的纯文本代码,输出可以是汇编代码、中间代码,具体取决于编译流程的设计。

作用

1.词法分析:将代码分割为单词符号,将连续的字符流分割成词法单元,如关键字(if、int)、标识符、运算符(+、=)、常量等。代码int x = 10;会被分割为int(关键字)、x(标识符)、=(运算符)、10(常量)、;(界符)。

2.语法分析:根据编程语言的语法规则,将词法单元序列组合成语法树,同时检测语法错误,如括号不匹配、关键字遗漏等。

3.中间代码生成:将语法树转换为与机器无关的中间表示,例如抽象语法树、后缀表达式等,便于后续优化和跨平台移植。

4.验证语义正确性:确保变量和表达式的类型匹配,例如不允许将指针赋值给整数;维护符号表,记录变量、函数的作用域、类型等信息,检查未定义标识符或重复定义;报告语义错误处理,例如数组越界访问、函数参数类型不匹配等。

5.代码优化:对中间代码或目标代码进行等价变换,在不改变程序逻辑的前提下提高运行效率或减少资源占用。

3.2 在Ubuntu下编译的命令

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

图6.虚拟机编译过程和文件列表

编译后得到了hello.s文件。

3.3 Hello的编译结果解析

3.3.1汇编初始标签

这部分共四行,分别标定了hello.s文件的部分信息。

.file \"hello.c\":该文件从hello.c文件编译而来

.text:标记代码段的开始

.section .rodata:标记只读数据段的开始,存储程序中的常量数据

.align 8:数据或指令在8字节边界上对齐。

3.3.2字符串常量

LC是Local Constant 的缩写,意为本地常量,在LC0部分存储了一个字符串,其内容为UTF-8编码形式的“用法: Hello 2023112184 申文圆 18854755469 4!”;

LC1中.string \"Hello %s %s %s\\n\"是一个标准的C字符串,用于printf函数格式化输出;.globl main声明main函数是全局可见的,即它是程序的入口点;.type main, @function指定main是一个函数。

3.3.3 main函数标定和初始化

首先是函数的标定位置和安全保护等部分:main指示main函数的开始,然后.LFB6标定了此处是第六个函数的起始位置,.cfi_startproc是汇编语言中用于生成DWARF调试信息的指令,主要用于标记函数的开始并初始化帧信息,endbr64是一个安全相关的指令,用于增强对控制流劫持攻击的防御。

下面进入到函数的序言部分,对栈指针和寄存器进行规定:

pushq %rbp:保存旧的基址指针到栈中。

.cfi_def_cfa_offset 16:标定当前栈帧大小为16字节

.cfi_offset 6, -16:寄存器6保存在距CFA偏移-16的位置。

movq %rsp, %rbp:设置新的基址指针(RBP = RSP),建立新栈帧。

.cfi_def_cfa_register 6:CFA现在基于RBP寄存器。

subq $32, %rsp:分配32字节栈空间,用于局部变量和参数对齐。

这部分主要对main函数的起始位置和一些栈与寄存器的状态进行规定,对函数进行初始的初始化。

3.3.4 main函数具体功能分析

前半部分对参数进行存储与检查:

movl %edi, -20(%rbp):argc存储到栈中(%edi是第一个参数寄存器)。

movq %rsi, -32(%rbp):argv存储到栈中(%rsi是第二个参数寄存器)。

cmpl $5, -20(%rbp):检查argc是否等于5。

je .L2:若相等,跳转到.L2,否则继续往下执行。

回看这部分,存在检查argc是否为5这一操作,正好对应源程序中的if(argc!=5),若argc等于5,就跳转到.L2,执行后面的for循环,若argc不等于5,则执行printf和sleep。我们继续往下分析,查看argc不等于5的情况:

leaq .LC0(%rip), %rax:加载LC0地址到%rax,即把LC0中的地址存到%rax中。

movq %rax, %rdi:将%rax中的数据迁移到%rdi中,方便puts函数调用。

call puts@PLT:调用puts函数输出LC0中存储的常量字符串

movl $1, %edi:设置状态码1。

call exit@PLT:调用exit函数,并以状态码1退出。

总结该小节,实现了main函数中argc等于5时的跳转和argc不等于5时的打印操作和退出操作。

3.3.5循环判断和循环体

承接上阶段,我们不管汇编中的顺序安排,跳过.L4来看.L3,其循环部分如下:

跳转到.L3之后:

cmpl $9, -4(%rbp):比较i(存储在-4(%rbp)中)与9。

jle .L4:若i <=9,跳回.L4。

从这部分可知,由于.L4在.L3之前,当L3跳转到.L4之后,执行.L4中的相关指令,即循环体,单次循环结束后会再次顺序执行.L3,根据条件选择性跳转.L4,因此实现了循环,下面我们来看被循环体.L4:

可以将这部分拆成两部分来看,第一部分从35~48,第二部分49~56。

movq -32(%rbp), %rax:%rax = argv基地址,由3.3.4可知argv在-32(%rbp)中。

addq $24, %rax:%rax指向argv[3]的地址。

movq (%rax), %rcx:%rcx = argv[3]的值。

movq -32(%rbp), %rax:%rax再次指向基地址。

addq $16, %rax:%rax指向argv[2]的地址。

movq (%rax), %rdx:%rdx = argv[2]的值。

movq -32(%rbp), %rax:%rax再次指向基地址。

addq $8, %rax:%rax指向argv[1]的地址。

movq (%rax), %rax:&rax = argv[1]的值。

movq %rax, %rsi:%rsi= argv[1]的值。

leaq .LC1(%rip), %rax:同理,%rax=\"Hello %s %s %s\\n\"。

movq %rax, %rdi:%rdi =%rax=\"Hello %s %s %s\\n\"。

movl $0, %eax:清零%eax(无浮点参数)。

call printf@PLT:调用printf函数打印。

通过这部分,%rdi存储输出格式\"Hello %s %s %s\\n\",%rsi、%rdx、%rcx分别等于argv第1、2、3项,最后调用函数printf进行打印。我们继续看下一部分。

movq -32(%rbp), %rax:将%rax指向argv基地址。

addq $32, %rax:%rax指向argv[4]的地址。

movq (%rax), %rax:%rax = argv[4]的值。

movq %rax, %rdi:%rdi= argv[4],作为atoi参数。

call atoi@PLT:调用atoi函数将argv[4]转换为整数,返回值存放在在%eax。

movl %eax, %edi:%edi = 转换后的整数,作为sleep参数。

call sleep@PLT:调用sleep函数。

addl $1, -4(%rbp):实现i+1的功能。

总结这一小节,利用四个寄存器向printf传参完成打印,并实现sleep(atoi(argv[4]))的功能,还对i进行了+1,进行循环控制。

3.3.6清理与返回

call getchar@PLT:等待用户输入一个字符。

movl $0, %eax:设置 main 函数返回值为 0。

leave:恢复栈指针和基址指针,准备返回。

.cfi_def_cfa 7, 8:修正 CFA 为调用者的栈帧地址。

ret:从 main 函数返回。

.cfi_endproc:结束当前函数的 CFI 块。

.LEF6:与.LFB6相对,标定函数结束位置

经过这些步骤,main函数被终止返回,并清除相关数据和占用。

3.3.7编辑器元信息

这部分包含编译器版本、堆栈保护标记和GNU属性信息,用于链接和安全性检查。

综上所述,按照报告要求,进行另一层面的总结:常量设计LC0和LC1的两个常量字符串,数组涉及argv数组,参数有argc,局部变量有i;赋值操作包括对i赋值为0进入循环,以及各处寄存器被赋值为argv的各项;算数运算包括对i每次加一;关系运算包括对argc和5和i和9的大小判断的判断;控制转移指令和关系运算相对应,包括跳转到.L3和跳转到.L4;函数调用包括main、printf、atoi、exi、sleep、getchar函数;类型转化涉及argv[4]被atoi转化成整形作为sleep的参数。

3.4 本章小结

本章首先介绍了编译的概念与作用,然后对hello.i文件进行实际编译,得到了hello.s文件,然后对hello.s进行了详细分析:首先从hello.s的各部分展开,逐一分析各代码进行阐述,明确了hello程序在汇编层面上运行的流程和汇编代码中具体的各项具体内容,如汇编头、编译器元信息等,最后按照PPT要求进行另一层面的论述,从各项变量与操作入手,进行总结分析。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

概念

汇编是将汇编语言代码(人类可读的低级指令)转换为机器语言(计算机可执行的二进制指令)的过程。汇编器as将汇编程序翻译成机器语言指令,将这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件中。

作用

1.指令翻译:汇编的核心功能是将符号化指令转换为二进制机器码,具体包括将操作码映射为CPU可识别的二进制操作码、将变量名转换为内存地址、将十进制数转换为二进制补码形式等。

2.生成重定位信息:标记需要链接阶段处理的地址引用,生成符号表和重定位表。

4.2 在Ubuntu下汇编的命令

汇编指令:gcc -c hello.s -o hello.o

图7.产生可重定向目标文件过程

4.3 可重定位目标elf格式

输入readelf -a hello.o > hello.elf可以将hello.o的elf格式存储在hello.elf中。

图8.产生elf格式可重定向目标文件

打开hello.elf可以看到,最上方是ELF头,ELF头以一个l6字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,对应下图的Magic,此外还有类别、版本、文件类型、操作系统等一系列信息,帮助链接器完成链接。我们来关注一些重点信息:

入口点地址:由于可重定位文件不能直接执行,因此其入口地址为0;

Start of section headers: 1088:从ELF文件起始地址偏移1088个字节处是节头表的起始地址。

Size of this header: 64: ELF文件头大小为64 byte。

Size of section headers: 64:每个节头的大小为64 byte。

Number of section headers: 14:节头表中共有14项。

Number of program headers: 0:可以看出可重定位文件的program header的长度为0。这是因为program header保存的是segment信息,而segment是为了给加载器提供可执行程序在加载时所需的信息的,又因为可重定位文件本身并不能直接执行,因此在可重定位文件里不需要program header;

图9.ELF头相关信息

下面是节头,也就是secton header,正如ELF头标注的,节头共14个,记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等,例如对各个节头标注了名字:.text、.rela.text、.data等

There are no section groups in this file.:该文件没有节头群

本文件中没有程序头:与ELF头相对应

There is no dynamic section in this file.:该文件没有使用动态链接

图10.节头表相关信息

再下一部分是重定位节,.rel.text节是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。这部分标注出了代码段.rel.text和异常处理帧.rela.eh.frame的偏移地址,具有8个入口,每个入口的偏移量、信息、类型等都已给出。

图11.重定位节相关信息

再下一部分是符号表,.symtab节中包含ELF符号表,这张符号表包含一个条目的数组,存放一个程序定义和引用的全局变量和函数的信息。该符号表不包含局部变量的信息。符号表如下,包含11个入口,并标注了每个的值、类型、使用范围等。

图12.符号表相关信息

最后一部分是一些相关信息,记录了版本信息、所有者等信息。

图13.ELF尾部相关信息

4.4 Hello.o的结果解析

利用objdump -d -r hello.o反汇编后得到下列图14.图15.

图14是反汇编出的文件头,相较于hello.s中的汇编语言,反汇编得到的相关信息明显变少,只包含hello.o的文件格式,而.s中的汇编语言包含很多信息,如文件来源、对齐格式等。

图14.反汇编的得到的文件头部信息

每条汇编指令对应一个唯一的二进制操作码,每个机器码可以唯一的被翻译成汇编指令,例如mov al 5的机器码是B0 05,B0 05也被唯一的翻译成mov al 5

1.在图15可以看出,反汇编得到的汇编在最左侧有地址偏移量、中间是地址中存储的内容,即机器语言,最右侧才是汇编代码,其实中间地址存储的内容翻译过来就是右侧的汇编代码,而.s中的汇编代码只有右侧的汇编代码,不存在地址偏移和机器语言。

2.此外.s文件中的代码分成多段存储在LC3、LC4和main段中,并使用LC0和LC1专门标注出常量字符串,而反汇编代码全部存放在main函数段,并将main函数入口作为基地址计算各部分代码偏移量。

3.在表示立即数上,.s的汇编语言直接采用$+数字,而反汇编代码采用$+0x+数字,反汇编代码加上了表示16进制的符号,

4.在跳转这部分,由于.s的代码是分散在LC3、LC4、main各处的,因此跳转时往往是直接跳转到.LC3、.LC4段的起始位置,不能随意跳转到任意行,因此LC各部分需要合理安排以满足跳转需求;而反汇编得到的代码具有地址和地址偏移,可以选择性跳转到任意一行,因此更加灵活,跳转指令格式为 jn Imm,意为跳转到main函数为基址偏移量为Imm的代码行进行执行。如下图所示:

5.调用函数这部分,.s中直接call+函数+@PLT,利用函数命标识调用命令,而反汇编文件中对函数的调用与重定位条目相对应,call后面不再是函数名称,而是一条重定位条目指引的信息。如下图所示:

反汇编代码是hello.s经过汇编之后的hello.o再次反汇编来的,hello.s经过重定向和翻译成机器码之后,其结构发生了进一步的变化,因而产生了诸多不同,图15展示了反汇编部分的全部代码。

图15.反汇编代码main函数部分

4.5 本章小结

本章介绍了汇编的概念和作用,其本质是将汇编语言翻译成机器语言并修改格式,方便与其他函数相互链接,此外还逐一观察了ELF格式的hell程序具有的结构,逐一分析了反汇编代码和原汇编代码的区别,进一步明确了汇编的功能,了解了机器码与汇编的区别之处。

(第4章1分)

第5章 链接

5.1 链接的概念与作用

概念

链接是编译系统的最后阶段,是将各种代码和数据片段收集并组合成一个单一文件的过程,其核心任务是将多个目标文件(如.o、.obj)和库文件(如.lib、.a、.so)组合成一个完整的可执行程序或共享库。链接过程会解析和处理各个文件间的符号引用,将它们绑定到正确的内存地址,最终生成符合目标平台规范的可执行文件。

作用

1.符号解析与地址绑定:将代码中对外部符号的引用(如调用其他文件定义的函数)与实际定义绑定,为所有符号分配运行时内存地址(如全局变量、函数入口点)。

2.目标文件合并与重定位:将多个目标文件的代码段(.text)、数据段(.data)、符号表等合并为一个统一的内存映像,调整代码和数据的地址,使其在运行时能正确加载到内存中。

3.库文件处理:静态库以.a或.lib形式存在,链接时被完整复制到可执行文件中,无需依赖外部库,可独立运行;动态库以.so或.dll形式存在,运行时由操作系统动态加载。

5.2 在Ubuntu下链接的命令

ld的指令:

sudo ld  -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 /usr/lib/gcc/x86_64-linux-gnu/11/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/11/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello

图16.利用ld指令链接

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

输入readelf -a hello,得到hello的ELF格式。

图17.ELF头

ELF头中的Magic头、类别、数据等都未发生变化,入口点地址、程序头起点、节头部表起点、程序头大小均发生了变化,和程序起点相关的数据都从默认零变成了相应的实际数据,节头从14个扩展到30个。ELF头的大小仍为64B。

图18.节头部表

节头部表个数从14个扩展到30个,相应的位置都被填充了数据,这部分大小为64B。此外之前ELF中的“本文件中没有程序头”这几个字消失,因为该文件有了程序头,即下图。

图19.程序头部分

程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息,共12个程序头,这部分大小为56B。

图20.区段映射部分

这部分是链接器将编译后的逻辑Section组织为运行时物理Segment的过程。

图21.动态节

动态节是实现动态链接的核心组件。它存储了运行时动态链接器所需的元数据,用于在程序加载或运行时解析外部符号、加载共享库以及执行重定位操作。

图22.重定位节

起始地址为000000403ff0,终止地址为000000404040,大小为50bits。

图23.动态与静态符号表

上述两个分别是动态符号表和静态符号表,动态符号表是动态链接机制的核心组件之一,用于存储运行时所需的符号信息,共有九个入口,静态符号表有38个入口。

图24.编译器相关信息

最后一部分记录了一些gnu的版本信息和所有者等。

5.4 hello的虚拟地址空间

通过edb打开hello文件后,dump stack自动展示到0x401000地址,我们再打开memory regions可以查看不同区域的起始位置。

代码段(.text)位于r-x区域,起始地址为0x400000-0x401000,数据段(.data/.bss)位于rw-,起始位置为0x403000-0x405000,堆位于rw-,起始位置为0x7fca7e97e000-0x7fca7e82000,栈位于0x7ffccfa2d000-0x7ffccfa4e000,hello文件的只读段(包含ELF头、程序头、.rodata等)被映射到0x400000 - 0x401000 (r--p) 和 0x402000 - 0x404000 (r--p) 的区域。

图24.edb状态下的memory regions

我们打开data dump可以看到一下内容:正好对应了代码段(.text)

图25.data dump相关内容

5.5 链接的重定位过程分析

输入objdump -d -r hello得到一系列代码,截图如下

图26.objdump的结果

5.5.1分析hello与hello.o区别

1.hello反汇编后多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等由链接器创建的其他区段。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。

2.hello程序的反汇编代码的地址已经分配到实际内存,因此为绝对地址,可以直接执行,而.o文件的地址都是相对地址,将main的地址设置为0x0,其余地址都是据此偏移出的。

3.可执行文件中的外部符号已被解析为实际地址(如GOT/PLT 中的地址),而.o文件中对外部符号(如printf)的引用显示为未解析的占位符。

4.可执行文件中的重定位条目大部分重定位已完成,仅保留动态链接所需的信息,而.o包含大量重定位条目(Relocation Entries),指示链接器需要修改的地址。如下图中的.rodata使用了占位符地址和重定位条目。

5.5.2链接的过程

根据上述内容,我们总结一下连接的主要过程:首先是符号解析,链接器将多个目标文件(如hello.o、libc.a)中的符号引用与定义绑定,例如,将hello.o中对printf的引用,解析到libc.so中的实际实现;然后是地址分配,链接器为每个段(.text、.data 等)分配虚拟地址空间,比如将.text段加载到 0x401000;最后一步是重定位:链接器修改代码中的地址引用,使其指向正确的内存位置,例如将call 0x73(占位符)修改为call 0x4010a0(printf 的 PLT 地址)。

5.5.3重定位的过程

1.链接器为来自hello.o和被链接的库的所有代码和数据区段分配不重叠的虚拟内存地址。例如,hello.o 的.text区段中从相对偏移量0开始的main函数的代码 ,在可执行文件的地址空间中被放置在 0x401000处。

2.对于hello.o中引用数据且带有占位符地址的指令和重定位条目,链接器计算该数据的最终绝对地址并用此地址修补指令。

3.链接器生成.plt和.got区段,在程序加载时初始化GOT。以printf为例,首次调用printf时,PLT跳转到动态链接器,解析地址并更新GOT,后续调用直接通过GOT跳转,无需再次解析。

5.6 hello的执行流程

过程

1.当用户在shell中执行./hello 时,内核加载器首先介入读取hello可执行文件的ELF头部,根据程序头部表将文件的各个段映射到进程的虚拟地址空间。具体映射关系查看5.4节。

2.动态链接器进行自身初始化和重定位,查找hello程序使用的共享库,将共享库加载到内存中,并对hello程序和共享库进行重定位:解析外部函数符号的实际地址,填充hello程序的GOT中的条目

3.执行初始化代码,动态链接器调用所有加载模块(包括共享库和hello自身)的初始化函数,初始化完成后,动态链接器将控制权转移到 hello 程序的主入口点 _start,其地址为0x4010f0

4._start是程序在用户态执行的第一个函数,它为调用main函数做准备,运行完成之后调用C共享库中的_libc_start_main函数初始化C运行时环境,运行结束后会调用程序中的main函数

5.执行main函数,具体分析如下:

push   %rbp

mov    %rsp,%rbp

sub    $0x20,%rsp:上面三句为标准函数序言,建立栈帧。

mov    %edi,-0x14(%rbp):将argc保存到栈上。

mov    %rsi,-0x20(%rbp):将argv保存到栈上。

cmpl   $0x5,-0x14(%rbp):比较argc是否等于5。

je     0x401208 :如果等于5,则跳转到地址0x401208继续执行。如果argc不等于5则继续往下执行。

lea    0xe12(%rip),%rax        # 0x402008:将402008位置的字符串放入%rax。

mov    %rax,%rdi:把%rax的值传递给%rdi。

call   0x401090 :调用puts函数打印。

mov    $0x1,%edi:设置退出状态码为1。

call   0x4010d0 :调用exit函数退出。

movl   $0x0,-0x4(%rbp):初始化循环计数器i为0

jmp    0x401267 :跳转到循环条件判断处。

mov    -0x20(%rbp),%rax:把argv基地址给%rax。

add    $0x18,%rax:%rax加0x18指向argv[3]。

mov    (%rax),%rcx:%rcx=argv[3]。

mov    -0x20(%rbp),%rax:把argv基地址给%rax。

add    $0x10,%rax:%rax加0x18指向argv[2]。

mov    (%rax),%rdx:%rdx=argv[2]。

mov    -0x20(%rbp),%rax:把argv基地址给%rax。

add    $0x8,%rax:%rax加0x18指向argv[1]。

mov    (%rax),%rax:%rax=argv[1]。

mov    %rax,%rsi:%rsi=argv[1]。

lea    0xe00(%rip),%rax        # 0x40203c:将40203c位置的字符串

入%rax。

mov    %rax,%rdi:%rdi=%rax=字符串。

mov    $0x0,%eax:清零%eax。

call   0x4010a0 :调用printf打印。

mov    -0x20(%rbp),%rax:%rax指向argv基地址。

add    $0x20,%rax:%rax指向argv[4]。

mov    (%rax),%rax:%rax=argv[4]。

mov    %rax,%rdi:%rdi=argv[4]。

call   0x4010c0 :调用atoi函数将argv[4]转化成整数。

mov    %eax,%edi:将返回值赋给%edi。

call   0x4010e0 :调用sleep函数

addl   $0x1,-0x4(%rbp):循环计数器i加1。

cmpl   $0x9,-0x4(%rbp):比较循环计数器i是否小于等于9。

jle    0x401211 :如果i<=9,则跳转回循环体继续执行,否则继续执行下一行。

call   0x4010b0 :调用getchar函数

mov    $0x0,%eax:设置 main 函数的返回值为0。

leave  :恢复调用者栈帧

ret:从main函数返回。

6.main函数返回与程序终止:控制权从main返回到libc.so.6中的 __libc_start_main 函数,__libc_start_main调用exit函数并将main的返回值作为程序的退出状态码。在exit过程中,会执行.fini 段中的代码,做一些栈操作后返回。

7.exit函数会发起一个系统调用,通知内核终止当前进程。

各个子进程

下图为调用ebd的symbols所得到的函数进程符号表:

图27.各个进程的名称和地址

5.7 Hello的动态链接分析

我们不妨以puts函数的前后变化为例,揭示链接前后的GOT表的变化,举一反三。

首先查看未链接前的相关状态,进入gdb调试后我们在_start处设置断点,对puts函数进行反汇编,查看puts函数的GOT条目,发现地址在0x404018,查看此地址的内容得到下列结果:

图28.查看puts函数的GOT地址

图29.GOT表的内容

将在_start处的断点删除,在_libc_start_main设置断点,这样函数就完成了动态链接,此时再次查看GOT表的内容,发现发生了改变。

图30.GOT表变化后的内容

当puts函数第一次被调用并执行完毕后,GOT条目被更新:存储在0x401030处的值发生了变化,该地址指向的际上是的地址。这个地址位于已加载的libc.so.6共享库的内存空间内。

5.8 本章小结

本章从链接的基本定义和功能出发,用ld的指令将hello.o进行动态链接,并分析链接后的ELF格式,利用edb探究hello程序运行时的虚拟地址分配,进一步探究链接的连接过程和重定位原理。此外还就hello的反汇编程序进行逐步分析,探究hello执行的具体流程,并对链接前后的状态进行进一步深入分析。

(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

概念

进程是操作系统进行资源分配和调度的基本单位,作为程序独立运行的载体,保障程序的正常执行,其存在大幅提升了操作系统资源的利用率。进程是操作系统中最核心的概念之一,本质上是程序的一次动态执行实例。与静态的程序(存储在磁盘上的可执行文件)不同,进程是程序在计算机内存中的 “运行态”,包含了程序运行所需的所有资源和状态信息。操作系统通过进程管理实现多任务并发,让用户感觉多个程序可以同时运行。

作用

1.操作系统通过进程为程序分配独立的资源空间,包括内存、文件句柄、CPU 时间片等。

2.实现并发与多任务处理,通过CPU分时调度让多个进程交替使用 CPU,宏观上呈现同时运行的效果,极大提升了系统资源利用率。

3. 每个进程拥有独立的虚拟地址空间,操作系统通过内存管理单元(MMU)将进程的虚拟地址映射到物理内存,确保进程间数据互不访问。这种隔离机制防止了一个进程崩溃导致整个系统瘫痪,也避免了恶意程序篡改其他进程数据,保障了系统稳定性和安全性。

4.进行进程间通信,操作系统提供管道、消息队列、共享内存、套接字等IPC机制,允许进程安全地交换数据和同步操作。

5.进程拥有独立的生命周期,可以被创建(如fork系统调用)、调度、暂停、终止。这种特性使操作系统能灵活管理程序的运行,例如结束无响应的进程,或后台运行服务进程。

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

shell是用户和操作系统内核的一个交互型应用级程序,代表用户运行其他程序,同时也是一个命令语言解释器,拥有自己内建的shell命令集,它接收用户输入的命令,解释并传递给内核执行,再将结果返回给用户。而bash是linux系统下常用的shell。

功能

1.命令解释与执行:接收用户输入的文本命令,解析后调用系统内核接口执行相应操作;区分内置命令和外部命令。

2.用户与系统的桥梁:屏蔽内核复杂性,将底层系统调用抽象为易读的命令,支持管道、重定向、通配符等语法,简化批量操作。

3.脚本编程与自动化:允许将多条命令写入Shell脚本,按顺序执行以完成复杂任务;支持变量、条件判断、循环、函数等编程特性,提升管理效率。

4.环境配置与个性化

通过读取配置文件初始化环境变量、别名、命令补全等,定制用户工作环境。

5.进程管理

启动、监控子进程,处理进程间通信。

处理流程

1.读取从键盘输入的命令。

2.判断命令是否正确,且将命令行的参数改造为系统调用execve()内部处理所要求的形式。

3.终端进程调用fork()来创建子进程,自身则用系统调用wait()来等待子进程完成。

4.当子进程运行时,它调用execve()根据命令的名字指定的文件到目录中查找可行性文件,调入内存并执行这个命令。

5.如果命令行末尾有后台命令符号&终端进程不执行等待系统调用,而是立即发提示符,让用户输入下一条命令;如果命令末尾没有&则终端进程要一直等待。当子进程完成处理后,向父进程报告,此时终端进程被唤醒,做完必要的判别工作后,再发提示符,让用户输入新命令。

6.3 Hello的fork进程创建过程

首先shell读取命令,我们可以输入./hello,然后sell解析此命令,并识别出这是一个外部命令。因此shell需要创建子进程:首先shell调用fork()函数创建子进程,内核为shell进程创建一个几乎完全相同的副本,子进程继承父进程的内存空间(代码段、数据段、堆、栈)、文件描述符(如标准输入 / 输出 / 错误)和环境变量,这个副本就是子进程;子进程获得一个新的、唯一的进程ID;fork函数在子进程中返回0,在父进程中返回新创建子进程的PID。至此fork的工作就结束了。

6.4 Hello的execve过程

同样我们输入./hello,在fork执行完创建子进程之后,轮到execve函数了。首先系统调用陷入内核,子进程通过或syscall指令触发系统调用,传递以下参数filename:指向./hello的路径字符串、argv:参数数组、envp:环境变量数组。然后进行内核验证与准备,系统检查文件路径是否有效、调用者是否有执行权限、文件是否为可执行格式,如果符合,系统会读取读取ELF文件头部,解析程序的入口点(_start函数地址)、段信息(.text、.data等)。然后内核调用execve函数进行内存映射与加载,为hello程序创建新的虚拟地址空间:

映射.text段(代码)到只读区域(权限r-x)。

映射.data和.bss段(数据)到可读写区域(权限rw-)。

创建栈(Stack)和堆(Heap)区域。

若程序依赖共享库(如libc.so)则还需要查找并加载所需的共享库,进行动态链接。

6.5 Hello的进程执行

1.系统=进行命令解析并创建子进程:用户在终端输入./hello,Bash作为当前进程在用户态执行命令解析,按空格分词,识别./hello为可执行文件路径,并检查文件是否存在且有执行权限。

2.系统调用fork()创建子进程,此时由用户态转换到核心态。系统利用fork函数复制进程上下文,为子进程创建新的进程控制块,复制Shell的寄存器值、内存映射、文件描述符并为子进程分配唯一PID。然后系统通过iret指令返回用户态

3.系统在子进程加载hello程序:子进程执行execve,再次触发用户态到核心态的转换,在核心态下执行execve。内核验证文件./hello是否为有效ELF文件,并加载程序,清空子进程原有的内存映射,为hello程序创建新的内存区域,并设置执行上下文。结束后通过iret指令再次返回用户态执行hello,此时子进程的上下文已完全替换为hello程序的执行环境,开始执行_start函数。

4.系统进行进程调度与上下文切换:进程管理每个进程分配CPU执行时间(时间片),由内核的调度器管理。

5.系统执行hello程序,首先进行动态链接完成,动态链接器解析外部符号,更新 GOT表,系统将控制权转移到hello的main()函数,执行用户代码。main()调用printf、atoi函数等,触发系统调用,此时系统从用户态转变成核心态,调用完成后再次返回用户态。

6.程序调用exit函数退出,main()返回或显式调用exit(),触发exit_group()系统调用,内核释放进程资源,进程终止,状态变为ZOMBIE,保留PCB并退出状态。最后父进程shell通过wait()获取退出状态后,僵尸进程彻底销毁。

6.6 hello的异常与信号处理

6.6.1异常与信号处理

1.硬件异常引发的信号

(1)系统发生段错误,输出SIGSEGV信号。触发场景:访问未分配的内存或写入只读内存。系统默认处理:终止进程。

(2)系统发生算术,输出SIGFPE信号。触发场景:整数除以零、浮点数溢出。系统默认处理:终止进程。

(3)系统非法指令,输出SIGILL信号。触发场景:执行CPU不支持的指令或程序堆栈被破坏导致返回地址无效。系统默认处理:终止进程。

2.用户或系统生成的信号

(1)程序被用户中断,输出SIGINT信号。触发场景:用户按下Ctrl+C等。系统默认处理:终止进程。

(2)程序被终止进程,输出SIGTERM或SIGKILL信号。触发场景:用户通过kill命令默认发送触发SIGTERM;用户强制终止进程出发SIGKILL。系统默认处理:终止进程。

3.其他可能错误:

(1)总线错误,输出SIGBUS信号触发场景:访问未对齐的内存地址(如某些架构要求 4 字节对齐访问)。系统默认处理:终止进程。

(2)管道破裂输出SIGPIPE信号。触发场景:向已关闭的管道或套接字写入数据。默认处理:终止进程。

6.6.2用户操作与结果

首先我们正常运行该程序,程序打印十次之后,我们回车后程序正常退出。

图31.程序正常退出

输入Ctrl+c后,触发SIGINT,shell终止子进程,程序运行结束

图32.输入Ctrl+c后程序退出

输入Ctrl+z之后触发SIGSTP信号,程序被挂起,返回到shell父进程。在bash中输入ps可以查看当前的进程,输入jobs可以查看当前作业,均可以发现hello程序在其中,说明hello被挂起但并未被终止。使用fg指令后,hello程序被恢复,首先打印相关指令后程序继续执行。而输入kill+在ps中观察到的PID之后,hello程序被终止,此时再次输入fg,程序显示已终止,无法继续运行。

图33.使用Ctrl+z指令和一系列操作

乱按以及输入多次回车后,程序仍然正常执行,输出十次后正常退出。在程序执行过程中乱按所造成的输入均缓存到stdin,当getchar的时候读出一个’\\n’结尾的字串就会终结程序。

图34.不断乱按及输入回车

6.7本章小结

本章从进程出发,围绕hell程序在shell中输入后的相关处理,阐述了fork、execve等函数在创建进程时的相关作用。还从hello函数入手,详细介绍了hello程序运行时的进程处理,并分析可能出现的相关异常与信号处理,最后实际模拟相关异常,探究其内部原理。

(第6章2分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

1.逻辑地址是程序代码中直接使用的地址,由段选择子和偏移量组成。在程序编译后,所有指令和数据引用的地址都是逻辑地址,例如函数调用地址(call 0x4004d0)或变量地址(mov eax, [0x601020])。而在hello程序编译生成的汇编代码中,printf 函数的调用地址在目标文件hello.o中是一个逻辑地址(如call 0x0,待链接时修正)。

2.线性地址是逻辑地址经过分段机制转换后的结果,是一个连续的地址空间。如果未启用分页,线性地址直接对应物理地址;若启用分页,线性地址需进一步转换为物理地址。在链接后的可执行文件hello中,所有符号(如 main、printf)已被分配统一的线性地址(虚拟地址)。例如,main函数的线性地址可能是 0x4004d0,printf的地址可能是0x400520(通过PLT跳转)。

3.虚拟地址是程序视角的地址空间,通常与线性地址等价。操作系统通过页表将虚拟地址映射到物理地址,实现虚拟内存管理。当执行./hello时,操作系统为进程分配虚拟地址空间(例如 0x400000 到 0x401000 为代码段)。

4.物理地址是实际内存芯片上的硬件地址,由CPU的内存管理单元(MMU)通过页表转换得到。物理地址对程序不可见,由操作系统和硬件管理。当hello进程运行时,MMU将虚拟地址0x400520转换为物理地址(如0x12345000)物理地址可能因系统负载不同而动态变化。

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

逻辑地址到线性地址的转换是内存管理的核心机制之一,这一过程通过段式管理实现。段式管理起源于早期计算机对内存隔离和保护的需求,现代操作系统虽更多依赖分页机制,但段式管理仍作为底层基础存在。以下从机制原理、数据结构到转换流程逐步解析。

逻辑地址由16位的用于定位段描述符的段选择符和32/64位表示段内具体位置的偏移量组成。段选择符的16位结构由高13位用于在描述符表中定位段描述符索引和1位(0表示使用全局描述符表,1表示局部描述符表)的表指示位以及低2位用于表示当前代码的权限级别的请求特权级。而段描述符是内存段的元数据,是8字节的数据结构,存储在GDT或LDT中,定义了一个内存段的属性。

根据逻辑地址中的段选择子的段描述符索引,可以从GDT或LDT中找到对应的段描述符。使用段描述符中的基地址和逻辑地址中的偏移量,可以计算得到线性地址。

图35.段式管理示意图

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

页式管理是实现虚拟内存的核心机制,负责将进程视角的线性地址(虚拟地址)转换为物理地址。这一过程通过页表和硬件支持(如MMU)完成。

虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。VM系统将虚拟内存分割,称为虚拟页,类似地,物理内存也被分割成物理页。页式管理核心流程包括拆分线性地址、定位页目录、定位页表项、计算物理地址等,其中页表项(PTE)地址=页表基址+页表索引×4,物理地址=物理页帧基址+页内偏移。

图36.页式管理示意图

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

在x86-64架构中,虚拟地址到物理地址的转换通过四级页表和TLB协同完成。四级页表支持48位虚拟地址空间,而TLB作为高速缓存大幅加速地址转换过程。

CPU产生虚拟地址VA,虚拟地址VA传送给MU,MMU使用VPN高位作为TLBT和TLBI,向TLB中寻找匹配。如果命中,则得到物理地址PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成物理地址PA,添加到PLT。

图37.从VA到PA的变换

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

三级Cache(L1、L2、L3)通过层次化的存储结构优化物理内存访问效率,减少CPU等待时间。其中L1 Cache速度最快(延迟约为1-4周期),容量最小(KB级),通常分为指令Cache(L1-I)和数据Cache(L1-D)。L2 Cache速度次之(延迟约为5-12周期),容量较大(MB级),通常为统一Cache(混合指令与数据)。而L3 Cache速度较慢(延迟为20-40周期),容量最大(MB到GB级),多核共享。主存速度是最慢的,但是容量最大(GB级)。当代计算机使用的主要是组相联,平衡命中率与复杂度都比较合适。

高速缓存的结构将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位,而组相连可以将一个主存块存储到唯一的一个Cache组中任意一个行。如果将cache分成u组,每组v行,主存块存放到哪个组是固定的,至于存到该组哪一行是灵活的,即有如下函数关系:cache总行数m=u×v 组号q=j mod u。

图38.组相连映射机制

7.6 hello进程fork时的内存映射

Linux系统中,当调用fork()创建子进程时,内存映射的处理遵循写时复制机制,以优化性能和资源利用。当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_ struct、.区域结构和页表的原样副本。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任何一个。后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

当父进程或子进程尝试修改标记为COW的页时,触发页错误。内核分配新的物理页,复制原页内容到新页。更新触发进程的页表项,指向新物理页,并标记为可写,原物理页的引用计数减1,若为0则释放。进程恢复执行,写入操作在新页完成,父子进程的修改互不影响

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

1.删除已存在的用户区域:删除当前进程虚拟地址的用户部分中的已存在的区域结构。

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

3.映射共享区域:hello程序与共享对象1ibc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

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

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

当进程访问虚拟地址时,CPU会触发缺页中断由很多情况触发,例如页不存在、权限错误、写时复制等,当CPU触发缺页中断时,操作系统保存上下文并获取故障信息,以此判断故障类型。若页不存在,页表项的Present位为0,则分配物理页框:若物理内存充足,直接分配空闲页框;若内存不足,触发页面替换算法选择牺牲页;加载数据到内存,并更新页表。设置页表项的物理页框号,标记为Present和可访问权限。刷新TLB:确保后续访问使用新页表项。缺页中断处理是操作系统实现虚拟内存的核心机制,通过按需加载、页面替换和权限管理,平衡了内存使用效率与安全性。

图39.缺页中断机制

7.9动态存储分配管理

以下格式自行编排,编辑时删除

Printf会调用malloc,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)

7.10本章小结

本章围绕计算机硬件和地址,详细阐述了整个计算机中地址的变化和相关的硬件支持,例如三级cache下的物理内存访问等,此外还有软件支持,例如fork函数和execve函数等。在软硬件协同下,计算机能流畅的完成各种地址的转化和安全高效地访问内存。

(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

以下格式自行编排,编辑时删除

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

以下格式自行编排,编辑时删除

8.3 printf的实现分析

以下格式自行编排,编辑时删除

https://www.cnblogs.com/pianist/p/3315801.html

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

以下格式自行编排,编辑时删除

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

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

以下格式自行编排,编辑时删除

(第8章 选做 0分)

结论

Hello所经历的过程

1.hello.c程序首先被预处理器处理,将头文件添加到对应位置,变成hello.i。

2.hello.i被编译器转换成汇编语言,形成hello.s。

3.hello.s被汇编器转换成机器语言,形成可重定向文件hello.o。

4.hello.o被链接器与共享库中所使用的函数相链接,形成可执行的hello程序。

5.输入。/hello之后,shell用fgets读取当前的命令行

6.shell用eval函数解析命令行,判断是否为内置命令

7.shell使用fork创建子进程hello,继承了父进程shell的页表等进程控制信息,但拥有完全独立的私有虚拟地址空间。

8.shell创建新的作业job,并将hello进程加入此作业,等待前台作业终止。

9.hello子进程execve执行:调用mmap创建新的区域结构,建立文件与进程私有虚拟地址空间的映射。

10.由于hello调用了printf函数、atoi函数、sleep函数等,shell执行动态链接,从C语言标准库libc.so加载printf.o、atoi.o、sleep.o等到内存,建立printf.o与进程hello的共享虚拟地址区域的映射

11.设置PC指向代码区域的入口点_start

12.操作系统的进程调度到hello,执行_start处指令,在第一次执行指令或访问数据时,或产生缺页中断,实现虚拟内存到物理内存的映射(拷贝),然后重新执行指令。

13.hello进程执行,调用printf,进而调用write产生同步异常-陷阱,由OS完成显示输出。

14.hello执行return,进而通过exit终止进程的执行,向父进程shell发送SIGCHLD信号

15.父进程shell的sigchld信号处理子程序,通过waitpid完成对hello僵尸子进程的回收,并删除作业job。

16.父进程shell的waitfg检查job状态非前台,则结束对此进程和作业的处理。

感悟

hello这一路走来真的是太不容易了,平时我们在IDE中点一下的功夫,没想到在计算机系统的世界是如此的麻烦,计算机系统真的是妙不可言。从这次学习中也学会了很多很多,对日后编写优化代码、从底层架构上理解程序有很大的帮助。计算机远远没有我想像的那么简单,我将不断“深入学习”,在计算机的道路上追求“人生”。

(结论0分,缺失-1分)


附件

Hello.c C语言源文件

hello.i:编译系统预处理后的文件,包含系统头文件;

hello.s编译器处理后的汇编文件;

hello.o汇编器处理后的可重定位目标程序,内容为二进制机器语言;

hello 可执行文件

hello.elf hello.o的elf格式文件

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

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]  《深入理解计算机系统》

[2]  https://blog.csdn.net/xq151750111/article/details/114491731

[3]  https://zhuanlan.zhihu.com/p/511801984

[4]  https://blog.csdn.net/u014587123/article/details/115276998

[5]  https://zhuanlan.zhihu.com/p/658734980

[6]  https://www.jianshu.com/p/fd2611cc808e

[7]  【ARM-MMU】ARMv8-A 的4K页表四级转换(VA -> PA)的过程_arm mmu 粒度为什么是4k-CSDN博客

[8]https://blog.csdn.net/qq_38877888/article/details/103118068?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522175696d9a9d0ca84e8f621fabe5466f3%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=175696d9a9d0ca84e8f621fabe5466f3&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-1-103118068-null-null.142^v102^pc_search_result_base2&utm_term=%E4%B8%89%E7%BA%A7Cache%E6%94%AF%E6%8C%81%E4%B8%8B%E7%9A%84%E7%89%A9%E7%90%86%E5%86%85%E5%AD%98%E8%AE%BF%E9%97%AE&spm=1018.2226.3001.4187

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