计算机系统原理
大作业
题 目 ++++程序人生-Hello++++ ++++'++++ ++++s P2P++++
专 业 ++++计算学部++++
学 号 ++++2024112676++++
班 级 ++++24Q0303++++
学 生 ++++文宇博++++
指 导 教 师 ++++史先俊++++
计算学部
202 5 年9月
摘 要
本文探究了一个程序hello.c从预处理、编译、汇编、链接生成可执行文件,到输入参数执行后经历进程创建、内存加载、存储管理、运行、外部I/O调用,最终运行完成、子进程回收、一切归零的过程。通过一个程序将本课程应用到的计算机系统知识串联起来,探究了这些计算机管理方式,演示了这些内容的操作方法,理清了计算机系统运行程序的底层工作原理,做到"深入理解计算机系统"。
****关键词:****预处理;编译;汇编;链接;进程管理;存储管理;I/O管理。
目 录
[第1章 概述](#第1章 概述)
[1.1 Hello简介](#1.1 Hello简介)
[1.2 环境与工具](#1.2 环境与工具)
[1.3 中间结果](#1.3 中间结果)
[1.4 本章小结](#1.4 本章小结)
[第2章 预处理](#第2章 预处理)
[2.1 预处理的概念与作用](#2.1 预处理的概念与作用)
[2.3 Hello的预处理结果解析](#2.3 Hello的预处理结果解析)
[2.4 本章小结](#2.4 本章小结)
[第3章 编译](#第3章 编译)
[3.1 编译的概念与作用](#3.1 编译的概念与作用)
[3.2 在Ubuntu下编译的命令](#3.2 在Ubuntu下编译的命令)
[3.3 Hello的编译结果解析](#3.3 Hello的编译结果解析)
[3.3.1 数据类型](#3.3.1 数据类型)
[3.3.2 赋值操作](#3.3.2 赋值操作)
[3.3.3 类型转换操作](#3.3.3 类型转换操作)
[3.3.4 算数操作](#3.3.4 算数操作)
[3.3.5 关系操作](#3.3.5 关系操作)
[3.3.6 指针操作](#3.3.6 指针操作)
[3.3.7 控制转移操作](#3.3.7 控制转移操作)
[3.3.8 函数调用](#3.3.8 函数调用)
[3.4 本章小结](#3.4 本章小结)
[第4章 汇编](#第4章 汇编)
[4.1 汇编的概念与作用](#4.1 汇编的概念与作用)
[4.2 在Ubuntu下汇编的命令](#4.2 在Ubuntu下汇编的命令)
[4.3 可重定位目标elf格式](#4.3 可重定位目标elf格式)
[4.4 Hello.o的结果解析](#4.4 Hello.o的结果解析)
[4.5 本章小结](#4.5 本章小结)
[第 5 章 链接](#第5章 链接)
[5.1 链接的概念与作用](#5.1 链接的概念与作用)
[5.2 在Ubuntu下链接的命令](#5.2 在Ubuntu下链接的命令)
[5.3 可执行目标文件hello的格式](#5.3 可执行目标文件hello的格式)
[5.4 hello的虚拟地址空间](#5.4 hello的虚拟地址空间)
[5.5 链接的重定位过程分析](#5.5 链接的重定位过程分析)
[5.6 hello的执行流程](#5.6 hello的执行流程)
[5.7 Hello的动态链接分析](#5.7 Hello的动态链接分析)
[5.8 本章小结](#5.8 本章小结)
[第 6 章 hello进程管理](#第6章 hello进程管理)
[6.1 进程的概念与作用](#6.1 进程的概念与作用)
[6.2 简述壳Shell-bash的作用与处理流程](#6.2 简述壳Shell-bash的作用与处理流程)
[6.3 Hello的fork进程创建过程](#6.3 Hello的fork进程创建过程)
[6.4 Hello的execve过程](#6.4 Hello的execve过程)
[6.5 Hello的进程执行](#6.5 Hello的进程执行)
[6.6 hello的异常与信号处理](#6.6 hello的异常与信号处理)
[第 7 章 hello的存储管理](#第7章 hello的存储管理)
[7.1 hello的存储器地址空间](#7.1 hello的存储器地址空间)
[7.2 Intel逻辑地址到线性地址的变换-段式管理](#7.2 Intel逻辑地址到线性地址的变换-段式管理)
[7.3 Hello的线性地址到物理地址的变换-页式管理](#7.3 Hello的线性地址到物理地址的变换-页式管理)
[7.4 TLB与四级页表支持下的VA到PA的变换](#7.4 TLB与四级页表支持下的VA到PA的变换)
[7.5 三级Cache支持下的物理内存访问](#7.5 三级Cache支持下的物理内存访问)
[7.6 hello进程fork时的内存映射](#7.6 hello进程fork时的内存映射)
[7.7 hello进程execve时的内存映射](#7.7 hello进程execve时的内存映射)
[7.8 缺页故障与缺页中断处理](#7.8 缺页故障与缺页中断处理)
[第 8 章 hello的IO管理](#第8章 hello的IO管理)
[8.1 Linux的IO设备管理方法](#8.1 Linux的IO设备管理方法)
[8.2 简述Unix IO接口及其函数](#8.2 简述Unix IO接口及其函数)
[8.3 printf的实现分析](#8.3 printf的实现分析)
[8.4 getchar的实现分析](#8.4 getchar的实现分析)
第1章 概述
1.1 Hello简介
P2P为从hello.c程序到进程的过程。对原代码进用给定的编译链接方式,进行预处理、编译、汇编、链接,最终输出可执行文件hello。对于可执行文件,运行命令"./hello 姓名 学号 手机号 秒数",此时shell调用fork创建子进程,子进程加载、动态链接后从入口开始最终实现main函数。
020为从进程初始化的0内存状态到进程结束的0内存状态。创建进程时,内存空间为空。用execve加载函数将虚拟内存对应到物理内存,寄存器初始化为特定的值。运行过程中不断调用。进程中止后,调用exit释放内存资源,向父进程发送SIGCHLD信号,最终变为僵死进程,等待父进程回收资源。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
处理器:AMD Ryzen 7 7840H with Radeon 780M Graphics (3.80 GHz)。
系统:64 位操作系统, 基于 x64 的处理器。
软件环境:Windows 11,VMware-Ubuntu 22.04。
开发工具:Codeblocks,VisualStudio。
1.3 中间结果
|--------------|--------------|
| hello.c | 提供程序 |
| hello.i | 预处理的文本文件 |
| hello.s | 编译文件 |
| hello.o | 汇编得到的可重定位文件 |
| hello | 链接得到的可执行文件 |
| hello.asm | 可执行文件的反汇编文件 |
| hello(o).asm | 可重定位文件的反汇编文件 |
| hello.elf | 可执行文件的ELF信息 |
| hello(o).asm | 可重定位文件的ELF信息 |
1.4 本章小结
本章介绍了程序hello.c的P2P、020流程,梳理了程序从的编译链接到进程中执行,最终结束清空的过程。并且介绍了编辑程序用到的软硬件设备和各种生成文件。
第2章 预处理
2.1 预处理的概念与作用
- 预处理的概念
预处理是C/C++编译体系中的前置解析阶段,由预处理器(通常为cpp)在语法分析前对源代码执行基于指令的文本转换。预处理通常包括将#为前缀的编译指示转换为实际代码内容、对源代码进行结构重组、删去注释等多余的字符,最终生成符合编译器输入规范的翻译单元。
- 预处理的作用
(1)文本级宏展开:将标识符宏定义映射到预定义文本序列,实现编译期常量计算和内联代码生成。
(2)头文件包含:处理#内容,通过文件内容逐字插入构建完整的声明上下文。
(3)条件编译:基于预定义标识符的代码路径选择是否编译。
(4)元信息注入:自动注入源码定位元数据。
(5)其他各种预处理作用,如注释的删除,......
2.2在Ubuntu下预处理的命令
Linux系统终端下运行命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
观察预处理后的文件hello.i,文件扩展到3092行,在main函数上的3000多行代码源于对三个头文件<stdio.h>、<unistd.h>和<stdlib.h>的头文件包含处理,通过路径寻找这些头文件的位置,将其复制到源文件。同时对头文件中的宏定义做出处理。

定位发现hello.c中main函数的文字存在于最底部,如图所示。同时hello.c中所有的文字注释被删除。

由此可知,预处理后的文件做出了2.1所述的改动。
2.4 本章小结
本章介绍了C语言程序的预处理操作的概念、方法和作用。通过对给定程序hello.c的处理,分析预处理文件hello.i,得出预处理文件通过头文件包含<stdio.h>、<unistd.h>,宏展开其中的定义,删除注释代码等方式,实现了C语言文件的预处理。
第3章 编译
3.1 编译的概念与作用
- 编译的概念
编译是指编译器将经过预处理的C语言源代码(.i文件)进行分析优化,并最终转换为针对不同处理器架构的汇编语言代码(.s文件)的过程。这个过程重在理清代码的逻辑结构,并将其翻译成等效的CPU指令序列。
- 编译的作用
计算机编译可分解成词法分析、语法分析、语义分析、中间代码生成与优化和目标代码生成几个步骤,这个过程联系了计算机编译器和开发工具,将高级C语言转变为平台相关的低级汇编语言,完成优化和错误检查,提高代码可用性。
3.2 在Ubuntu下编译的命令
Linux系统终端下运行命令:gcc -S hello.i -o hello.s

3.3 Hello的编译结果解析
3.3.1 数据类型
- 字符串
程序中的两个字符串存放在只读数据段.rodata节,中文字符转换成8字节序列。

- 整型常量
程序中的整型包括argc!=5、i<10、i++,它们分别表示为:



作为立即数的形式,其中i<10优化为i<=9。
- 局部变量
局部变量有未初始化的int i、参数int argc、指针数组char *argv[]。它们在汇编代码中以分配栈空间的形式表示:

其中argc通过寄存器edi传递,指针argv通过寄存器rsi传递。

.L2中存储i,初始化为0。
3.3.2 赋值操作
3.3.1整型常量中立即数int i,进行赋值操作。

参数int argc、指针数组char *argv[]赋值到栈。
3.3.3 类型转换操作
函数调用sleep(atoi(argv[4]))中,sleep函数需要unsigned int类型,atoi函数返回int类型,两个函数调用间做类型转换。

3.3.4 算数操作
for循环后的每次i++操作,实质上是复合赋值操作,每一次循环完毕后,计数器加1,使用addl实现。

3.3.5 关系操作
- 不等于
条件判断中关系argc!=5,使用cmp比较argc与5的大小,相等则跳转.L2,不相等执行下面的语句。

- 小于等于
for循环结束后判断i是否<=9,若不,则跳转到.L4。

3.3.6 指针操作
对指针数组argv[1]到argv[4],每一次赋值都要从基址%rax中取8位,然后对%rax解引用。

3.3.7 控制转移操作
- if-else
判断argc是否不等于5,使用cmp比较argc与5的大小,相等则跳转.L2,不相等执行下面的语句。

- for
for循环中首先初始化i为0,然后直接跳转到条件检查。

每一次为i添加增量后,检查i是否<=9,若不,则跳转到.L4。

3.3.8 函数调用
- call
函数puts、exit,设置地址,设置edi为1后调用函数。

外部函数printf,在指针操作每次从基址中取出字符串,设置.LC1起始地址将其传递,调用函数打印到屏幕。

外部函数atoi、sleep,将参数从rax传递到rdi后,进行atoi函数调用,得到结果从eax传递到edi后再进行sleep函数调用。

外部函数getchar,直接调用。

- 函数返回
针对hello.c程序的return 0,汇编代码中恢复栈帧后返回。

3.4 本章小结
本章介绍了C语言程序的编译操作的概念和作用。通过对给定预处理文件hello.i的处理,分析编译文件hello.s,研究编译文件在数据类型,赋值、算数、类型转换、关系、指针、控制转移、函数调用上的操作,比较了其对源程序文件的处理。
第4章 汇编
4.1 汇编的概念与作用
- 汇编的概念
汇编是指将汇编语言源文件(.s)翻译成机器语言目标文件(.o)的过程。.o文件是二进制程序。这是编译过程的第二阶段,位于预处理和编译之后、链接之前。
- 汇编的作用
(1)指令翻译
将符号指令转换为机器指令。包括将汇编指令转换为对应的二进制机器码;将寄存器名、立即数、内存地址等转换为二进制编码。
(2)符号解析,重定位
解析局部符号和变量后生成重定位信息。包括记录需要链接时确定的地址;创建重定位表标记需要调整的位置。
(3)数据段组织:将.data、.rodata、.text分段处理。
(4)生成目标文件格式:Linux系统为ELF
4.2 在Ubuntu下汇编的命令
Linux系统终端下运行命令:gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -c hello.s -o hello.o

4.3 可重定位目标elf格式
Linux系统终端下运行命令:readelf -a hello.o > hello(o).elf。得到hello.o文件的elf格式,列出各节的基本信息。

- ELF头:

文件为小端序,其中,"类型: REL"表明这是可重定位文件,不是可执行文件;"入口点地址:0x0"表示可重定位文件没有入口点;"Size of program headers:0(bytes)"表示可重定位文件没有程序头表。
- 节头
记录代码段、初始化数据段、未初始化数据段、只读数据段以及符号表等其他各段的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等信息。

- 重定位节
.rela.text节(代码重定位节)包含8个重定位项,链接器将这个文件和其他文件组合时需要修改这些项的位置。这一节包含重定位项的类型,目标符号名称和指向位置等信息。例如,第一行的偏移量0x1c,指向.rodata节,代表某个指令的操作数指向这个偏移位置。

.rela.eh_frame节为异常处理帧重定位节,用于异常处理时定位代码起始位置。指向.text节的起始位置。

- 符号表
包含源文件、节符号、函数符号(main)、未定义的外部函数符号的信息。

4.4 Hello.o的结果解析
Linux系统终端下运行命令:objdump -d -r hello.o > hello.asm,得到反汇编信息。

对比hello.asm与hello.s分析有:
- 每一个指令从汇编源码转换为带机器码的反汇编源码。例如,汇编语言第一条指令:

转换为反汇编,55为单字节指令

- 操作数保存环节,立即数在汇编语言保存为十进制,在反汇编中表示为十六进制。


- 条件判断环节,跳转偏移从汇编语言的段名称地址(.L2)转换为主函数加行偏移量地址(0x32)。对于其他需要地址跳转的操作,也做了同样的处理。


- 函数调用环节,反汇编代码相较于汇编代码增加了地址占位符(00 00 00 00,链接时填充)、重定位信息(1c:R_X86_64_PC32 .rodata-0x4为.rodata节地址修正,24:R_X86_64_PLT32 puts-0x4为puts的PLT地址修正)、相对寻址(lea 0x0(%rip),%rax)。


由此可知,反汇编语言相较于汇编语言,控制流指令在机器语言中使用相对偏移;外部引用在目标文件中使用占位符;地址计算在链接时完成。
4.5 本章小结
本章介绍了C语言程序的汇编操作的概念和作用。通过对给定编译文件hello.s的处理,分析汇编文件hello.o。首先研究汇编文件的elf格式,分析hello.elf的每节内容,特别注重对重定位节的分析;然后分析了反汇编代码,与hello.s对照,指出了他们的异同之处。
第 5 章 链接
5.1 链接的概念与作用
- 链接的概念
链接是编译过程的最后阶段,由链接器完成。链接就是将所有目标文件合并,计息其中的 各种符号引用,分配内存地址并链接库文件的过程。链接可以执行于编译时、加载时和运行时。
- 链接的作用
(1)符号解析:链接器查找所有目标文件和库,将每个符号引用关联到唯一的符号定义。
(2)重定位:链接器将所有目标文件的代码、数据段合并,并为其分配运行时的内存地址。
(3)合并代码与数据:将多个目标文件的代码段、数据段等合并,生成可执行文件的对应段。
(4)链接库文件
5.2 在Ubuntu下链接的命令
Linux系统终端下运行命令: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的格式
将hello.o的elf文件重命名。Linux系统终端下运行命令:readelf -a hello > hello.elf。得到hello文件的elf格式,列出各节的基本信息。

- ELF头

对比hello.o的elf文件,文件为小端序,"类型: EXEC"表明这是可执行文件;"入口点地址:0x4010f0"表示可执行文件有入口点,为_start的位置;"Size of program headers:56(bytes)"表示可执行文件有程序头表。
- 节头表
节头表记录了各节的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等信息。其中序号1到11为动态链接相关节,12到16为代码节,17到22为数据节,23到26为调试信息节。


- 程序头
程序头表描述如何将文件映射到进程的虚拟地址空间。

- 段节和动态段
对节头表中段信息的解释


- 重定位信息
包含重定位项的类型,目标符号名称和指向位置等信息。

- 符号表
包含动态和静态符号表。


5.4 hello的虚拟地址空间
程序头中LOAD处记录了可加载的地址,从0x400000开始,到0x403e50结束。

edb加载hello,从Data Dump处可以查看虚拟地址信息。可以看到从0x400000到0x405000的所有数据
这是0x400000到0x401000的数据节选:

数据中的信息可以与节头表中的地址信息对应,例如,节头表第一项.interp就对应图中004002e0行的内容。
其他的数据段信息可以在memory regions中选择查看。

5.5 链接的重定位过程分析
将hello.o的asm文件重命名。Linux系统终端下运行命令:objdump -d -r hello > hello.asm。得到反汇编信息。

链接后的hello多出来<_init>、<.plt>、<puts@plt>、<printf@plt>、<getchar@plt>、<atoi@plt>、<exit@plt>、<sleep@plt>、<_start>、<_dl_relocate_static_pie>、<_fini>的反汇编,说明动态链接器将用到的函数加载进可执行文件中。
链接后的字符串引用使用重定位。例如,hello.o的反汇编中,直接列出需要定位的类型,符号和偏移量。而在hello的反汇编中,重定位需要通过计算定位到链接的库中


链接后的函数调用使用重定位。例如,hello.o的反汇编中对exit函数的调用直接列出定位的类型和符号。而在hello的反汇编中,直接跳转到链接地址。


5.6 hello的执行流程
从加载hello到_start,程序调用_start、_lib_start_main等函数;从_start到call main,程序加载printf、sleep、getchar、atoi、main、exit等函数;到结束,程序加载_fini等函数。
edb查看所有加载的函数的子程序名和程序地址:


5.7 Hello的动态链接分析
hello程序调用由共享库定义的函数时,编译器无法在编译阶段确定该函数在内存中的准确地址,因为共享库在运行时可能被加载到内存的任意位置。现代编译系统采用延迟绑定,将函数地址的解析推迟到程序第一次调用该函数的时刻进行。
延迟绑定是通过GOT和PLT实现的,PLT是跳转表、GOT是存储函数实际地址的数组。过程如下:第一次函数调用时,程序执行call xxx@plt,调用函数在PLT中的跳转桩,进入PLT跳转桩,第一条指令就是跳转到GOT表中函数对应的槽,初始时,这个槽指向桩内的第二段代码。压入函数的重定位索引,跳转到PLT[0],从GOT[1]获取动态链接器数据结构,跳转到GOT[2](动态链接器的解析函数入口),此时动态链接器介入,根据重定位索引找到函数的符号信息,在已加载的共享库中搜索函数的真实地址,将找到的地址写回GOT中函数对应的槽,动态链接器跳转到函数的真实实现,程序正常执行函数。第二次及后续调用,程序执行call,进入PLT跳转桩,跳转到GOT表中函数对应的槽,此时槽中已存储真实地址,直接跳转到函数的真实实现。
下面展示了函数调用前后PLT的变化,调用前指向虚拟地址:

执行第一次函数调用后,这些函数的地址指向真实地址。

5.8 本章小结
本章介绍了C语言程序的链接操作的概念和作用。通过对给定汇编文件hello.o的处理,分析可执行文件hello.o。首先研究可执行文件的elf格式,分析hello.elf的每节内容;然后分析了可执行文件的虚拟地址空间;最后以hello为例分析了可执行文件的重定位、执行和动态链接流程。
第 6 章 hello进程管理
6.1 进程的概念与作用
- 进程的概念
进程是正在执行中程序的实例。进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
- 进程的作用
进程为程序提供了一种假象,程序好像是独占的使用处理器和内存,处理器好像是无间断地一条接一条地执行我们程序中的指令。进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中。
进程可以实现并发,在不同核心上并行执行;可以提供资源抽象与隔离;可以实现CPU资源的有效管理和共享。
6.2 简述壳Shell-bash的作用与处理流程
- 作用
Shell是一个交互级命令解释程序,将用户输入的文本命令解析并转换为系统调用,请求内核执行真正的操作。Shell不仅解释单条命令,还能执行由多条命令、控制结构、函数等编写的脚本文件。Shell允许用户在前后台运行、暂停(Ctrl+Z)、恢复(fg/bg)、终止(Ctrl+C)进程,并管理多个并发作业。同时,Shell也负责维护和传递用户的运行环境,包括环境变量、工作目录、文件描述符等。每个Shell进程都有一份独立的环境,子进程会继承父Shell的环境。
- 处理流程
(1)读取并处理命令
从输入或脚本文件中读取一行命令。Bash会将输入行按分隔符划分命令,解析成计算机可以执行的形式。
(2)重定向处理
Bash识别并设置好命令中指定的所有输入/输出重定向,提前打开或创建必要的文件并准备好文件描述符。
(3)确定命令与执行
取扩展后的第一个单词作为命令名。如果是内置命令,由Bash自身内部的函数直接执行,不创建新进程。如果是外部命令(如 /bin/ls, python),则Bash首先通过系统调用fork()创建一个自身的子进程,在子进程中通过系统调用execve()将命令程序文件加载并替换掉当前的子进程,开始执行该命令。
(4)等待
如果是前台命令,父进程(Bash)会调用wait()暂停自己直到子进程结束;如果是后台命令,父进程(Bash)不等待,立即返回提示符。
(5)收集状态与报告
命令执行完毕后,Bash会获取其退出状态码,并据此决定是否执行条件语句中的后续命令,然后显示新的提示符,等待下一条命令。
6.3 Hello的fork进程创建过程
-
命令行输入"./hello 2024112676 文宇博 13091433771 1",Bash确定是一个外部可执行程序(非内置命令)。
-
父进程调用fork()系统调用操作系统复制当前Bash进程,创建一个几乎完全相同的子进程。子进程获得父进程(Bash)相同的代码段、相同的数据段、堆、栈、相同的文件描述符表、相同的环境变量。唯一区别是,两者是独立的进程,地址空间不同。
-
fork()在父进程中返回子进程的PID,在子进程中返回0。
6.4 Hello的execve过程
-
子进程检查fork()返回值是0,知道自己是子进程,调用execve()函数。execve()函数声明:int execve(const char *filename, const char *argv[], const char *envp[])。替换过来就是execve("./hello", argv, environ)。
-
操作系统加载hello可执行文件,替换子进程的地址空间,舍弃复制的Bash代码和数据,加载hello程序的代码段,为hello程序创建新的数据段、堆、栈,保持相同的PID(1001),继承文件描述符表。
6.5 Hello的进程执行
-
hello进程被放入运行队列,等待调度器选择。当CPU空闲或当前进程时间片用完时,调度器就从就绪队列选择Hello进程。选择依据包括虚拟运行时间、优先级、等待时间等。
-
上下文切换过程:首先保存当前进程上下文如寄存器状态,更新当前进程的PC为下一条指令地址;然后切换内存空间,切换执行上下文;最后返回用户空间。
-
时间片内的计算:hello进程获得CPU后,在分配的时间片内依次执行_start 、参数检查、for循环和内容打印
-
用户态与内核态转换:
当执行到printf时,调用外部函数,发生用户态到内核态的转换。转换点发生在触发syscall指令时。此时内核保存用户态上下文并加载内核栈,执行系统调用。同样,hello进程执行到sleep()时也发生态转换。
综上,hello进程的执行过程展示了操作系统的上下文管理、时间片分配和用户/内核态切换机制。
6.6 hello的异常与信号处理
- 可能出现的异常
|----|------------|--------|------------|
| 类别 | 原因 | 异步/ 同步 | 返回行为 |
| 中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
| 陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
| 故障 | 潜在可恢复的错误 | 同步 | 可能返回到当前指令 |
| 终止 | 不可恢复的错误 | 同步 | 不会返回 |
- 异常的处理方式
参阅教材,中断的处理方式为;

陷阱的处理方式为:

故障的处理方式为:

终止的处理方式为:

- 产生的信号及处理方式:
(1)回车

回车使输出换行,不影响输出个数,这是程序对正常输入的处理,表示循环进行完成后的结束。
(2)Ctrl-C

按Ctrl-C后,终端驱动发送SIGINT信号给前台进程组。hello进程收到信号,调用_exit()系统调用终止进程。Shell收到SIGCHLD信号清理子进程。
(3)Ctrl-Z

按Ctrl-Z后,终端驱动发送SIGTSTP信号给前台进程组。hello进程收到信号,进程状态变为TASK_STOPPED,同时进程从运行队列移除。Shell显示作业信息[1]+ 已停止。
运行ps命令

表明:进程被挂起,不占用CPU时间;进程仍在内存中,可以恢复。
运行jobs命令

Job代号为1。
运行pstree命令,看到进程树:




(4)fg命令

输入fg命令后进程再次调度执行,接着停止时运行完。
(5)kill命令,-9可杀死进程。

6.7本章小结
本章结合进程的概念和作用,详细描述了父进程的fork创建hello子进程,execve加载进程内容,进程运行过程,对hello程序的进程创建、加载和执行进行了系统分析。最后,以hello为例对进程可能产生的异常和信号处理进行分析,展示了信号处理过程。
第 7 章 hello的存储管理
7.1 hello的存储器地址空间
- 逻辑地址
逻辑地址又称段内偏移量,是程序中使用的地址。在Intel架构中,形式为段标识符:偏移量。编译器和汇编器产生的地址都是逻辑地址。Hello.asm中每一行反汇编码中代码、数据的相对偏移就代表了逻辑地址,编译器在生成目标文件时就确定了这些相对地址。
- 线性地址
线性地址是逻辑地址通过分段机制转换后的32位地址,是虚拟地址的一种表现形式,通常等于偏移量。hello的代码在分段部件中逻辑地址是段中的偏移地址,加上基地址就是线性地址。
- 虚拟地址
虚拟地址是每个进程独有的地址空间(一般为0~4GB),Linux中线性地址就是虚拟地址,程序看到和使用的所有地址都是虚拟地址。如Hello.asm中的0x40xxxx等。
- 物理地址
物理地址是实际内存芯片上的物理位置,是虚拟地址通过MMU的页表转换得到,输出至地址总线的电信号编码。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理中所有内存访问都必须通过一个段。通过段式管理将逻辑地址转换后得到线性地址,它是一个单一连续的无符号整数,在32位保护模式下是32位,范围0x00000000 ~ 0xFFFFFFFF。
通过段式管理将逻辑地址转换的操作是:
(1)获取段选择子和偏移量
逻辑地址由两部分组成:一个 16位的段标识符和一个 32位的偏移量。段标识符的前3位为请求特权级和表指示器,后13位为描述符索引号,在GDT或LDT中定位具体段描述符的位置。
(2)查找段描述符
处理器内部有GDTR和LDTR寄存器,分别指向全局描述符表和当前任务的局部描述符表在内存中的起始位置。根据TI位选择GDT或LDT,再根据Index计算偏移,从内存中读取对应的段描述符。
(3)解析段描述符包含的关于这个段的信息。
(4)计算线性地址
所有检查完毕后,CPU将段描述符中取出的32位段基址与逻辑地址中的32位偏移量直接相加,计算结果就是一个32位的线性地址。
如图展示了这个操作过程。

7.3 Hello的线性地址到物理地址的变换-页式管理
经过段式管理转换的线性地址还要通过页式管理转换为物理地址。页式管理将线性地址空间和物理地址空间都划分为固定大小的页(通常4KB)。操作系统通过维护页表来记录每个线性页到物理页框的对应关系。
页式管理的流程为:地址分割,将线性地址划分为页号和页内偏移两部分,页号是高位部分,用于在页表中查找;页内偏移是低位部分,在物理页内定位。查表转换:查表得到线性地址的物理页框基址,加上页内偏移就得到物理地址。
页式管理的流程如图所示。

7.4 TLB与四级页表支持下的VA到PA的变换
Hello的页式管理利用四级页表。x86-64架构使用四级页表来高效管理内存。TLB是地址转换的硬件缓存,当要转换的线性地址在TLB中有缓存时,直接获得物理地址,无需访问内存中的页表。当TLB中没有所需的地址映射时,需要遍历四级页表,并将结果存入TLB。利用TLB访问可以减少内存,提高了地址转换速度。
hello中线性地址转换为物理地址的流程为:

将一个线性地址如0x40xxxx分解,首先从十六进制转换为二进制,其中倒数11-0位为偏移量,其余每8位一组为各级页表的索引位。CPU首先在TLB中查找此线性地址对应的物理地址,假设TLB未命中,开始四级页表遍历。从CR3获取当前进程PTE表物理地址,用各级页表的索引位获取每级页表基址。物理地址即物理地址加各级页表基址。最后更新TLB。
7.5 三级Cache支持下的物理内存访问
现代计算机系统通常采用三级缓存结构,如图展示,平衡CPU与主存之间的速度差距。Cache基于时间局部性和空间局部性原理工作。所需数据在Cache中找到称缓存命中;所需数据不在Cache中,需要从下级存储加载,称缓存不命中。

Cache有三种映射方式:直接映射是每个内存块只能映射到Cache中的一个固定位置;全相联映射是内存块可以映射到Cache的任何位置;组相联映射是将Cache分为多个组,内存块映射到特定组的任意行。
Hello程序中访问一个指令或参数,首先获取它的物理地址,将物理地址分解为标签、组索引、块偏移,根据缓存命中与否分别处理。
7.6 hello进程fork时的内存映射
通过fork创建子进程时并不立即复制所有内存,而是使用写时复制。
当进程调用fork()时,fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的页面标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
fork后,系统的物理内存使用量几乎不增加,因为只复制了页表。子进程获得完整的独立虚拟地址空间。每个进程页表占用约几十KB到几百KB内存。

7.7 hello进程execve时的内存映射
execve加载hello时,会将当前进程的映像完全替换为目标可执行文件及其相关环境所定义的新内存映射。此时发生虚拟内存区域的销毁与重新构建。
-
解构现有内存映射。包括释放原进程的代码段、数据段、堆栈等所有区域。接触与之关联的所有匿名内存页、文件映射页以及共享内存段的映射,保证旧地址空间的彻底隔离与资源的回收。进程标识符(PID)等被保留。
-
系统加载器构建符合目标可执行文件格式(如ELF)定义的新的地址空间布局,通过建立一系列独立的内存区域映射来完成:
文本段:将hello可执行文件的代码段(.text)以只读权限映射到虚拟地址空间中由链接器预先定义的固定地址或随机地址。该映射通常标记为共享,使得同一可执行文件的多个实例可以物理共享代码页,并支持写时复制的代码更新。
数据段:将hello已初始化的全局/静态数据(.data)映射到相邻的虚拟地址区域,权限为可读写但不可执行。该映射为私有写时复制,确保进程对数据的修改不会影响磁盘文件或其他进程。
BSS段:为未初始化的全局或静态数据建立一个匿名的、全零填充的私有可读写映射。其大小由ELF程序头精确指定,实现了高效的内存零初始化。
堆空间初始化:创建一个初始大小(可能为零)的堆区域,用于程序运行时动态内存分配(如malloc)。
栈空间建立:为用户态栈创建一个匿名私有映射。通常位于地址空间的高端并向下增长,并伴随一个不可访问的保护页以防止栈溢出。权限为可读写。
其他如动态链接器映射。
整个映射建立过程遵循延迟加载和写时复制原则。文本与数据页最初仅建立虚拟地址到文件偏移的映射关系,并不立即加载物理页。当首次访问触发缺页异常时,才从磁盘读入物理页。对于私有写时复制映射,真正的物理页复制仅在进程尝试写入时发生。
7.8 缺页故障与缺页中断处理
- 缺页故障的触发原因与类型
CPU访问虚拟地址时,MMU会通过查找页表将其转换为物理地址。转换过程中遇到以下情况,MMU将自动触发一个缺页故障,并将控制权移交到操作系统内核预定义的缺页中断处理程序。
(1)由于未映射或未加载,目标虚拟地址所在的PTE不存在。
(2)访问类型违反页表项中规定的权限。
- 缺页中断处理
缺页中断处理程序在内核态执行,处理流程为:
(1)捕获与现场保存
CPU硬件在检测到故障的瞬间,会将错误原因和故障虚拟地址存入特定寄存器,然后切换至内核模式,跳转到预设的中断向量。内核保存被中断进程的完整上下文以便后续恢复。
(2)确定故障
内核分析故障原因和地址,如果虚拟地址不合法则触发段错误终止进程;如果进程没有读写或执行权限则触发保护异常使程序终止。
(3)解决
对于延迟加载的文件映射,内核从磁盘交换区或原始文件读入对应页的数据到一个新分配的物理页框中,并在页表项中建立映射,标记为有效。
对于因写入私有映射而触发的写保护故障,内核会为新进程分配一个全新的物理页框,将原页内容复制到新页,更新当前进程的页表项指向新页,并设置为可写。原页的引用计数减一。
对于首次访问的堆或栈空间(匿名映射),内核直接分配一个用零填充的物理页框,并建立映射。
7.9动态存储分配管理
此节课堂没有讲授。
7.10本章小结
本章探讨了hello中的地址管理,详细介绍了两种地址管理方式及地址转化的流程,尤其是页式管理中采用的TLB和多级页表方法。介绍了hello的内存访问过程,尤其是创建子进程并加载hello的过程。最后介绍了缺页的处理。
第 8 章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux系统将所有I/O设备都模型化为文件,通过统一的文件接口进行访问。
Linux中主要有三种设备文件类型。对于字符设备,以字符流形式顺序访问,无缓存,包括键盘、鼠标、串口;对于块设备,以数据块形式随机访问,有缓存,包括硬盘等;对于网络设备,采用网络通信接口特殊处理,包括网卡、套接字接口等。
8.2 简述Unix IO接口及其函数
Unix I/O接口将所有设备都被抽象为文件,并使用数字标识打开;设备被视为字节序列,支持顺序或随机访问;通过内核提供的系统调用进行I/O操作。主要的函数有:
open:打开或创建文件/设备。
close:关闭文件描述符。
read:从文件描述符读取数据。
write:向文件描述符写入数据。
lseek:设置文件偏移量。
ioctl:执行设备特定的控制操作。
fcntl:对打开的文件描述符执行各种控制操作。
select/poll:I/O多路复用,监视多个文件描述符的就绪状态。
mmap:内存映射文件,将文件映射到进程的地址空间。
等等。
8.3 printf的实现分析
执行printf,首先需要格式式化字符串处理,printf函数调用vprintf,再调用vsprintf,遍历格式字符串"Hello %s %s %s\n",处理转义字符,依次将三个%s替换为argv[1]、argv[2]、argv[3]的实际值,并在栈或堆上分配缓冲区,构建完整的输出字符串。
printf检查输出目标是否为终端,根据是否使用行缓冲、全缓冲或无缓冲决定立即输出或缓存。
printf最终调用write()函数,将文件描述符(1)、缓冲区地址、字符串长度放入寄存器。系统调用指令执行,传统方式为int 0x80指令,将CPU切换到内核模式,跳转到中断向量表0x80对应的处理程序;现代方式采用syscall指令,直接切换到内核预设的入口点。
打印的每个字符对应字模库中的索引,驱动根据当前字体设置、字号获取对应字模数据。根据当前光标位置计算VRAM中的写入地址。遍历字模的每个像素,对每个"1"写入前景色RGB值、每个"0"写入背景色RGB值或保留原值。
8.4 getchar的实现分析
hello运行完毕,按下键盘任意键时,键盘将物理按键转换为对应的扫描码。随后键盘控制器通过中断请求线向CPU发送硬件中断信号,CPU暂停当前执行的任务,保存现场状态,并跳转到预设的键盘中断处理程序。中断处理程序首先从键盘控制器的I/O端口读取扫描码,然后根据当前键盘状态将扫描码转换为对应的ASCII字符码,转换后的字符被存入内核维护的环形键盘缓冲区中。
调用getchar函数首先要检查其内部输入缓冲区,缓冲区为空则调用read系统函数,向操作系统发起读取请求。read系统调用从文件描述符0(标准输入)读取数据,此时内核的终端驱动程序检查键盘缓冲区。按下回车键后,终端驱动程序唤醒等待的进程,将整行数据从内核缓冲区复制到用户空间缓冲区。
复制后系统调用返回实际读取的字符数。getchar函数从用户空间缓冲区提取第一个字符返回给调用者,同时调整缓冲区指针,为后续读取做好准备。如果读取成功,getchar返回字符的ASCII码;如果遇到文件结束符(如Ctrl+D)或读取错误,则返回EOF。
8.5本章小结
本章介绍了Linux系统的I/O管理,Linux系统正是将所有设备抽象为文件,通过I/O函数统一调度管理的。并结合两个函数具体分析了I/O管理过程。
结论
对一个hello.c的程序文件,真正让它运行要经历很多处理环节。
文件处理过程有
-
预处理:调用的外部库文件合并展开,生成较长的hello.i文件。
-
编译:hello.i翻译成汇编语言文件hello.s,便于机器理解。
-
汇编:hello.s翻译成可重定位目标文件hello.o。
-
链接:将hello.o与动态链接库链接,合成可执行文件hello。
-
运行:终端输入。
内核处理过程有
-
创建进程:调用fork函数创建hello子进程。
-
加载:调用execve函数将新进程的地址空间替换成hello的映像。
-
执行:虚拟地址被加载并重定位至物理内存,程序调用函数与Linux的I/O系统交互。
-
信号管理:运行成功后输入回车接受信号处理,运行终端有特殊的信号处理。
-
结束:内核安排父进程回收子进程,删除这个进程的所有数据结构。
感悟:
之前的我是只会编程并执行,做浮于表面的东西。计算机系统运行的每个过程都是清楚,明白的,这对于我对计算机运行程序的系统认识大有帮助。
附件
|--------------|--------------|
| hello.c | 提供程序 |
| hello.i | 预处理的文本文件 |
| hello.s | 编译文件 |
| hello.o | 汇编得到的可重定位文件 |
| hello | 链接得到的可执行文件 |
| hello.asm | 可执行文件的反汇编文件 |
| hello(o).asm | 可重定位文件的反汇编文件 |
| hello.elf | 可执行文件的ELF信息 |
| hello(o).asm | 可重定位文件的ELF信息 |
参考文献
- Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社.2016.
- 课上ppt.