关键词:hello程序,预处理,编译,汇编,链接,进程,存储,IO,异常和中断
本文以C语言经典入门程序hello程序为例,从预处理,编译,汇编,链接,进程,存储,IO,异常和中断多个方面,以程序员和计算机系统从局部到整体的视角,对hello的整个生命周期进行分析和解释.将hello的一生以准确的计算机的语言进行描述.通过此次学习将整个计算机系统的知识串联到一起,联系具体实际,从实践中学习计算机系统的知识.
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 6 -
3.2 在Ubuntu下编译的命令........................................................................... - 10 -
4.2 在Ubuntu下汇编的命令........................................................................... - 16 -
5.2 在Ubuntu下链接的命令........................................................................... - 24 -
5.3 可执行目标文件hello的格式.................................................................. - 24 -
5.5 链接的重定位过程分析............................................................................... - 28 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 35 -
6.3 Hello的fork进程创建过程..................................................................... - 36 -
6.6 hello的异常与信号处理............................................................................ - 38 -
第7章 hello的存储管理............................................................................... - 43 -
7.1 hello的存储器地址空间............................................................................ - 43 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................ - 43 -
7.3 Hello的线性地址到物理地址的变换-页式管理...................................... - 43 -
7.4 TLB与四级页表支持下的VA到PA的变换............................................. - 44 -
7.5 三级Cache支持下的物理内存访问.......................................................... - 45 -
7.6 hello进程fork时的内存映射.................................................................. - 46 -
7.7 hello进程execve时的内存映射.............................................................. - 46 -
7.8 缺页故障与缺页中断处理........................................................................... - 46 -
8.1 Linux的IO设备管理方法.......................................................................... - 49 -
8.2 简述Unix IO接口及其函数....................................................................... - 49 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P:
程序员使用文本编辑器(IDE内置,记事本,VIM等)创建hello.c文件,按照C语言语法,添加注释,include头文件,编写主函数main,编写逻辑,调用库函数,完成hello.c程序,然后保存文件,文件默认采用UTF-8编码,以01比特序列保存至硬盘中。至此program完成。
接下来程序员调用编译器使用指令:gcc ./hello.c -o hello,由编译器完成预处理,编译,汇编,链接的工作,生成hello程序。hello程序是编译器将hello.c的高级语言语句处理成cpu可以执行的机器语言指令。生成的hello程序以二进制文件的形式保存至磁盘中。
020:
起初内存中并不存在hello进程。在shell中,程序员输入./hello 指令,shell使用fork创建子进程,子进程调用execve函数执行hello程序,OS(操作系统)为其分配PID,mmap将文件映射到内存中,然后OS分配时间片,CPU读取内存中hello的指令开始运行,
hello内存的读取首先通过从主存中读取偏移地址,并查表获得段地址计算出线性地址,在通过TLB等4级页表查表获取物理地址,再从一级缓存,二级缓存,三级缓存,主存中获取指令和数据,由CPU处理后,再调用各种系统函数,在屏幕上显示输出。
然后程序结束,shell和OS回收进程,删除有关程序的上下文,释放内存。
1.2 环境与工具
1.2.1硬件环境:
12th Gen Intel(R) Core(TM) i7-12700H 2.30 GHz
16GRAM, 64 位操作系统, 基于 x64 的处理器
1.2.2软件环境:
Windows 11 23H2 Ubuntu 22.04
1.2.3开发环境:
gcc,vscode ,gdb
1.3 中间结果
hello:可以运行的hello程序
hello.c:hello程序的源代码
hello.i:预处理的结果
hello.s:汇编语言下的hello
hello.o:编译生成的可重定位的可执行文件
hello.obj:hello.o使用odjdump的结果
hello.obj2:hello使用odjdump的结果
1.4 本章小结
本章介绍了hello的P2P 020的详细过程和程序员的理解,以及实验的软硬件环境,使用到的工具,和实验过程中产生的文件。
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理的概念:
在C语言编程中,预处理是编译过程的第一个阶段,是C语言编译器在对源代码编译之前,对代码做的进一步的处理。这个阶段由预处理器负责,它扫描源代码并执行预处理指令指定的操作,这些指令通常以“#”开头。
2.1.2预处理的作用:
1)宏定义和替换:
宏定义:使用“#define”指令可以定义宏,可以将标识符替换为对应的文本。
参数化宏:可以定义含有参数的宏,可以实现简单的类似于函数的效果。
宏定义可以用于简化代码,和设定一些参数,同时使用宏定义也可以提高代码的可读性。
2)文件包含:
包含头文件:使用“#include”可以将一个文件的内容插入到当前文件中。
这可用于导入各种函数库,便利代码的模块化和复用。
3)条件编译:
条件性编译:使用“#if、#ifdef、#ifndef、#else、#elif和#endif”指令,可以根据条件选择性地编译代码的某些部分。
条件性编译不仅大大提高了程序编写的自由度,方便跨平台编译和调试,也可以解决库文件重复包含的问题。
4)预定义宏:
预定义宏:预处理器提供了一些内置的预定义宏,比如“__FILE__、__LINE__、__DATE__和__TIME__”,它们可以在代码中插入文件名、行号、编译日期和时间等信息。
这对调试程序和规范化编程很有用。
2.2在Ubuntu下预处理的命令
Ubuntu下预处理指令
gcc -m64 -no-pie -fno-PIC -E ./hello.c -o hello.i
指令截图:
可见生成的hello.i文件比源文件大了很多,预处理对hello.c文件添加了相当多的代码,做了很大修改。
2.3 Hello的预处理结果解析
将生成的hello.i文件通过文本编辑器打开,内容如下:
可以看到main函数之前被添加了相当大量的代码,
而hello.c的文件中的预处理指令有
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
这三条预处理指令的功能是将stdio.h, unistd.h, stdlib.h三个库文件引入到程序文件中,其中stdio.h包含了标准输入输出库的头文件。它使程序能够使用标准输入输出函数. unistd.h包含了POSIX操作系统API的头文件。它提供了对操作系统API的访问,例如文件操作、进程管理、管道、终端控制等. stdlib.h包含了标准库函数的头文件。它提供了许多通用工具函数,例如动态内存分配函数(malloc、calloc、realloc 和 free),程序控制函数(exit、atexit),以及通用的工具函数(abs、atoi、atof、rand、srand)。
预处理器读取上述三个库文件的内容,并根据读入的顺序进行内容的展开,如果文件中仍有其他预处理指令则继续处理,最终的hello.i文件中没有有宏定义,文件包含等内容,同时文件中有包含了标准库函数的头文件。它提供了许多通用工具函数,例如动态内存分配函数(malloc、calloc、realloc 和 free),程序控制函数(exit、atexit),以及通用的工具函数(abs、atoi、atof、rand、srand)。此外,hello.c文件中非预处理的内容如全局变量 main函数等均未作处理保存在hello.i中.
2.4 本章小结
在本章中,进行了对hello.c的预处理,分析预处理的概念与功能,并具体的分析了hello.c的预处理指令,并将hello.c处理的结果hello.i的文件内容进行分析,验证了预处理的部分功能.深入的学习了预处理的操作与过程.
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念:
编译是将高级编程语言编写的源代码转换为计算机可以直接执行的机器码(或中间代码、字节码等)的过程。
3.1.2编译的作用:
1)错误检测:编译器首先检测源代码中的语法错误和部分语义错误,帮助程序员在代码运行前发现和纠正错误.
2)性能优化:编译器还可以通过多种优化技术,使生成的机器码在执行时更加高效.
3)硬件独立性与可移植性: 高级编程语言提供了硬件独立性,程序员不需要关心底层硬件的具体实现细节。编译器负责将高级语言代码转换为特定硬件架构的机器码。
3.2 在Ubuntu下编译的命令
编译命令:
gcc -m64 -no-pie -fno-PIC -S ./hello.i -o hello.s
3.3 Hello的编译结果解析
数据:
3.3.1:字符串常量:
字符串常量一般位于.text节,在汇编代码中,字符串"用法: Hello 学号 姓名 手机号 秒数!\n", "Hello %s %s %s\n"均位于.text节.
3.3.2:main参数:
由第24行可知-20(%rbp)保存argc 参数,易知argc由edi传入, argv 由rsi传入并分别保存至-20(%rbp)和-32(%rbp)中,这两个都是由下面语句申请的32字节的堆栈空间.
3.3.3:局部变量:
由这句可知局部变量i保存至堆栈中,为-4(%rbp)
语句和操作
3.3.4:赋值语句:
对局部变量的赋值:
参数赋值:
返回值赋值:
几乎都是使用mov直接赋值,
将字符串的地址赋值到rdi中:
先将.lc1符号解析到的(使用相对寻址)地址使用lea指令直接赋值到rax中,再由rax寄存器mov到rdi寄存器中.
3.3.5比较语句:
相等判断:
先使用cmp做一次不保存结果的减法,再通过标志位z判断结果是否为0,即是否相等,使用je判断,相等则跳转.
i小于10:
使用-4(%rbp)减去立即数9,小于等于0,则跳转至L4.
3.3.6分支语句:
先使用cmp作减法,后使用je跳转.
3.3.7循环语句:
L4是循环体,循环体每运行一次后,执行L3,先判断后跳转,这里保存循环的条件,总循环10次,循环结束后,继续执行L3后的语句.
3.3.8数组操作:
数组的元素通过-32(%rbp)+24获得argv[1]地址, -32(%rbp)+16获得argv[2]地址
-32(%rbp)+8获得argv[3]地址.数组的内容保存在栈中,可以通过rbp和rax的配合完成对数组的访问.
3.3.9函数调用:
先从数组中取得地址,并解引用保存到rcx rdx rsi rdi等传参寄存器,后将rax置0用于保存传回的参数.然后使用call调用函数.
3.3.10:main函数返回:
将返回值0mov到eax中,然后使用leave指令将栈指针指向帧指针,pop原帧指针到%rbp,ret返回到函数调用的地方.
3.4 本章小结
1)本章对编译器生成的汇编代码做了深入的解读,了解了各种c语言的操作在汇编中的实现方式.
2)本章了解了全局变量,字符串常量,文件名.临时变量,函数参数的保存,赋值等操作和分支语句,循环语句,比较操作,赋值操作,变量和数组的使用,函数的调用,参数传递与返回等操作.
第4章 汇编
4.1 汇编的概念与作用
4.1.1汇编的概念:
汇编器(AS)将hello.s翻译成机器语言指令,并把这些指令打包成可重定位目标程序,将结果保存至hello.0文件中,hello.o文件是以二进制数据保存的,是机器可以识别的机器指令.
4.1.2汇编的作用:
将汇编语言的文件翻译成可重定位的二进制目标程序文件.
4.2 在Ubuntu下汇编的命令
汇编指令:
gcc -m64 -no-pie -fno-PIC -c ./hello.s -o hello.o
4.3 可重定位目标elf格式
ELF(Executable and Linkable Format,可执行与可链接格式)是一种用于可执行文件、目标代码和共享库文件的标准文件格式,广泛应用于类Unix操作系统(如Linux、BSD)中。ELF格式具有高度的灵活性和可扩展性,是现代操作系统中最常用的可执行文件格式之一。
4.3.1文件头
文件头中包含有关文件结构和内容的基本信息.包括ELF标识,目标体系结构,节表偏移,程序头表偏移等.
4.3.2程序头:
这个程序没有程序头表.
4.3.3节表:
节头表包含了文件中所有节(Section)的信息,每个节存储不同类型的数据,如代码、数据、符号表等。
4.4.4:节:
不同的节在 ELF 文件中存储不同类型的数据,每个节都有其特定的作用。下面是一些常见的节及其可能包含的内容:
.text:存储程序的代码。
.data:存储已初始化的全局和静态变量。
.bss:存储未初始化的全局和静态变量。
.rodata:存储只读数据,如字符串常量。
.comment:存储编译器或汇编器生成的注释。
.symtab:符号表,存储程序中定义和引用的符号信息。
.strtab:字符串表,存储符号表中的字符串。
.rela.text:存储代码段的重定位信息。
.rela.data:存储数据段的重定位信息。
.dynsym:动态符号表,存储动态链接所需的符号信息。
.dynstr:动态字符串表,存储动态链接所需的字符串。
.plt:过程链接表,存储延迟绑定函数的地址。
.got:全局偏移表,存储全局偏移地址。
.dynamic:动态段,存储动态链接器的信息。
.eh_frame:存储异常处理的帧信息。
.init_array:存储构造函数(C++)或初始化函数(C)的地址。
.fini_array:存储析构函数(C++)或终止函数(C)的地址。
.eh_frame_hdr:存储异常处理的帧信息头。
.gnu_debuglink:存储调试信息的链接。
.gnu.version:存储符号版本信息。
.gnu.version_r:存储符号版本依赖信息。
.shstrtab:节头字符串表,存储节名的字符串。
.gnu.hash:GNU hash 表,用于快速符号查找。
这些节中的内容在链接、加载和执行过程中起着不同的作用,例如帮助链接器解析符号、实现动态链接、进行重定位等。理解这些节及其内容有助于深入理解 ELF 文件的结构和功能。
下面是hello.o中各节的内容:
4.4 Hello.o的结果解析
比较分析:
从main开始指令部分基本相同,每行的代码相同,每条指令前由16进制的指令,hello.s是汇编代码,相比于机器指令,仍有一定的可读性,能支持一些方便的语法,比如汇编指令可以使用符号表示全局变量和跳转位置,而机制指令则只能使用偏移量或地址寻址或跳转.
具体来说,hello.s使用10进制,hello.o使用二进制或16进制.
分支转移中汇编使用标记和符号,obj翻译的指令使用偏移量,而机器指令与obj翻译的指令也不完全相同.
函数调用汇编使用函数名进行调用,而obj翻译的代码中使用偏移.同时原机器指令中置0.
以puts的调用为例, 1f: R_X86_64_PLT32:这是一个重定位项,说明链接器在链接时需要对这个地址进行重定位。R_X86_64_PLT32 是一种重定位类型,表示跳转目标的地址在全局偏移表(PLT)中,需要对其进行32位的重定位。puts-0x4:这个部分表示需要对PLT表中的puts函数地址进行重定位,减去0x4是因为call是相对于下一条指令的所以需要减4字节调整位置.
4.5 本章小结
汇编这一步完成了程序由源代码向机器指令的转变,至此程序才成为了计算机可以识别的而至今程序.在这一章中深入的分析hello.o文件的内容,了解了编译器在其中完成的工作,同时了解重定位文件的含义为接下来的链接提供基础.
第5章 链接
5.1 链接的概念与作用
5.1.1链接的概念:
C语言中的链接(Linking)是将多个编译后的目标文件(Object Files)或者库文件(Library Files)合并成一个可执行文件的过程。编译器会生成目标文件,而链接器则将这些目标文件组合在一起,解析它们之间的符号引用关系,并将它们映射到最终的可执行文件中。
5.1.2链接的作用:
1)符号解析: 链接器解析目标文件中的符号引用,确定每个符号的实际地址或偏移量。
2)地址重定位: 将符号引用替换为实际的内存地址或偏移量。
3)库链接: 将外部库文件链接到可执行文件中,以便程序可以调用库中的函数和使用库中的变量。
4)符号表生成: 生成最终可执行文件中的符号表,供动态链接器或调试器使用。
5)节合并: 生成最终可执行文件中的符号表,供动态链接器或调试器使用。
6)优化: 链接器可以进行一些优化,例如删除未使用的符号、合并相同的代码等,以减小最终可执行文件的大小。
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.3 可执行目标文件hello的格式
hello的elf头
hello的节头
程序头:
5.4 hello的虚拟地址空间
5.4.1edb打开:
5.4.2
.init节:
从401000开始
.text节:
从401000开始到401fff
.rodata节
从0x402000开始.
5.5 链接的重定位过程分析
5.5.1
hello文件中添加了程序中用到的函数. 包括程序加载后执行main前的一些准备工作,以及hello需要用到的一些库函数的定义等;
5.5.2
包括程序加载后执行main前的一些准备工作,以及hello需要用到的一些库函数的定义等;
5.5.3链接的过程:
1)库解析: 如果目标文件中引用了库中的符号,链接器会搜索和链接适当的库文件。
2)符号解析: 将每个目标文件中的符号(如变量和函数)解析为它们的实际地址。
3)重定位: 调整代码和数据的地址以反映它们在最终可执行文件中的实际位置。
5.6 hello的执行流程
1)程序地址:
2)执行过程:
__libc_start_main
_init
_dl_runtime_resolve_xsavec
_dl_fixup
_dl_lookup_symbol_x
_dl_audit_preinit@plt
__libc_start_call_main
_setjmp
__sigsetjmp
__sigjmp_save
main
printf@plt
atoi@plt
sleep@plt
getchar@plt
_IO_switch_to_get_mode
_IO_file_underflow
exit
gdb调试例子:
5.7 Hello的动态链接分析
动态链接的过程: 程序调用一个有共享库定义的函数。编译器无法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法时为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU通过一种叫做延迟绑定的技术来将地址的绑定推迟到第一次调用该过程。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
got plt地址:
初始
运行后:
5.8 本章小结
链接时编译的关键步骤,它将多个目标文件和库文件合成一个最终的可执行文件,或库文件. 链接器的出现,使得模块化的编程成为常态,分块编程,各自编译,最后链接是一个很自然的想法。同时,我们程序员也可以更好的使用库函数了。这得益于动态链接的出现,我们不用多次在文件中保存相同的代码.
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念:
进程是计算机科学中的基本概念,指的是一个正在执行的程序的实例。每个进程都是一个独立的实体,它拥有自己的地址空间、执行状态和系统资源。是对CPU,内存,IO设备的抽象.
6.1.2进程的作用
1)资源分配与保护: 进程是资源分配的基本单位。操作系统通过进程来分配CPU时间、内存、I/O设备等资源,并保护进程之间的资源不被非法访问。
2)并发执行: 通过多进程并发执行,提高了系统的利用率和吞吐量,使得多个任务可以同时进行,增强了系统的响应能力。
3)隔离和独立性: 每个进程有自己的地址空间和数据,互相独立,增强了系统的稳定性和安全性。如果一个进程崩溃,不会影响到其他进程。
4)简化设计: 通过将复杂的任务分解为多个独立的进程,简化了系统设计和编程。每个进程可以专注于完成特定的任务。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell的作用:
命令解释和执行:Shell读取用户输入的命令并将其传递给操作系统执行,显示执行结果。
脚本编程:Shell脚本可以自动执行一系列命令,实现批处理和任务自动化。
进程管理:Shell可以启动、控制和终止进程,管理进程的运行状态。
文件系统导航:Shell提供命令来浏览和操作文件系统,如创建、删除、移动文件和目录。
环境管理:Shell可以设置和管理环境变量,这些变量影响Shell和其他程序的行为。
输入/输出重定向和管道:Shell允许重定向命令的输入和输出,并通过管道将一个命令的输出作为另一个命令的输入。
6.2.2 Shell的处理流程:
1)启动Shell
用户启动终端并看到提示符 $。
2)读取输入
用户输入命令并按下回车键:
3)解析输入
Shell将输入解析为命令和参数,以及输出重定向操作
4)命令查找
Shell查找命令 echo,这是一个内建命令。
5)执行命令
Shell执行内建命令 echo,并处理重定向符号 >。
6)输入/输出重定向
Shell将命令的标准输出重定向到文件 output.txt。
7)等待命令完成
echo 命令执行完成,Shell将 Hello, World! 写入文件 output.txt。
8)处理命令结果
Shell更新返回状态变量$?,命令成功执行返回状态为 0。
9)返回提示符
Shell返回提示符,等待用户的下一个输入。
6.3 Hello的fork进程创建过程
终端输入./hello 2022113360 冯国浩 15130928963 1后开始程序运行
首先终端先检测是否是内置命令,如果是执行,不是则在磁盘上搜索,找到后执行fork函数,创建一个子进程, 此时,该子进程拥有和父进程完全相同的虚拟地址空间副本,即是相对于父进程是独立的。如果能够在fork函数在父进程和子进程中返回后立即暂停这两个进程,我们会看到两个进程的地址空间都是相同的。每个进程有相同的用户栈、相同的本地变量值、相同的堆、相同的全局变量值,以及相同的代码。同时父进程和子进程是共享文件的。父进程和子进程的区别在于他们的进程号不同,即PID不同。
同时由于fork函数调用一次,却会返回两次:一次是在父进程中,一次是在新创建的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回0。,根据返回值不同,程序判断为父进程还是子进程, 父进程和子进程在各自的虚拟地址空间内独立并发的向前推进。子进程中的程序开始准备执行hello程序.
6.4 Hello的execve过程
execve() 是一个系统调用,用于在一个新的进程中执行一个新的程序。它在Unix和类Unix系统中广泛使用。在Bash中,当你运行一个程序时,Bash的fork函数的子进程会调用 execve() 来加载并执行该程序。
函数的原型是
int execve(const char *filename, char *const argv[], char *const envp[]);
filename 参数是一个指向包含要执行程序的路径的字符串的指针。
argv 参数是一个指向参数列表的指针数组。数组的第一个元素通常是程序的名称,随后是任何传递给程序的参数。最后一个指针必须是NULL,以表示参数列表的结束。
envp 参数是一个指向环境变量的指针数组。这个数组中的每个元素都是一个字符串,包含一个环境变量的键值对。最后一个指针必须是NULL,以表示环境变量列表的结束。
execve函数会首先调用启动代码,启动代码设置栈,并将控制传递给新程序的主函数,当main开始执行时,用户栈的组织结构从栈底往栈顶依次是这样的:首先是参数和环境字符串,栈往上紧随其后的是以null结尾的指针数组,其中每个指针都指向栈中的一个环境变量字符串,全局变量environ指向这些指针中的第一个envp[0]。紧随环境变量数组之后的是以null结尾的argv[]数组,其中每个元素都指向栈中的一个参数字符串。在栈的顶部是系统启动函数libc_start_main的栈帧,接下来就是main函数执行后的即未来的栈帧了。
6.5 Hello的进程执行
在程序运行时,Shell为hello通过fork函数创建了一个子进程,这个子进程有其独立的进程.hello运行时.如果hello的进程不被抢占,则正常执行,若被抢占.则进入内核模式,进行上下文切换,然后转入用户模式调度其他进程,直到hello调用到sleep函数,为了最大化利用处理器资源,sleep函数会向内核发送请求将hello挂起,并进行上下文切换,进入内核模式切换到其他进程,切换回用户模式运行抢占的的进程.与此同时,将hello进程从运行队列加入等待队列,由用户模式变成内核模式,并开始计时.当计时结束时,sleep函数返回,触发一个中断,使得hello进程重新被调度,将其从等待队列中移除,并从内核模式转为用户模式.此时hello进程就可以继续执行其逻辑控制流.
进程调度的相关概念:
上下文信息: 上下文就是内核重新启动一个被抢占的进程所需要的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
进程时间片的概念:时间片(timeslice)又称为“量子(quantum)”或“处理器片(processor
slice)”是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间(在抢占内核中是:从进程开始运行直到被抢占的时间)。
6.6 hello的异常与信号处理
会产生的异常:
中断,陷阱(即系统调用)
会产生的信号:
SIGINT、SIGTSTP、SIGCONT、SIGCHLD
正常运行:
Ctrl-C:
按下后立即停止,Shell收到 SIGINT信号没有后台进程.
随意乱按:
程序正常运行,但getchar直接结束.
Ctrl-Z: 进程收到SIGSTP信号.Shell显示提示信息,进程挂起.
PS和jobs结果:ps:进程确实未被回收,job代号为1状态Stopped
pstree所有进程以树状图显示.
fg:继续运行.首先打印程序运行命令,hello并继续执行,完成后再回收.
kill:挂起后状态时stopped,kill发送信号后,进程被回收.
6.7本章小结
本章介绍了进程的概念和作用,以及shell的工作方式和原理.对于hello进程的具体创建方式,其fork和execve的具体工作有了一定的了解,并了解了hello在各种异常与信号下的处理结果.
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1逻辑地址:
编译器生成的汇编代码中与段相关的偏移地址,逻辑地址由选择符和偏移量两部分组成.hello.s中的相对偏移地址是相对偏移地址.
7.1.2线性地址:
逻辑地址与左移2位后的段地址相加即为线性地址.对于hello程序,线性地址,描述hello的程序与数据在哪些数据块上运行.
7.1.3虚拟地址:
对于现代的CPU来说,一般虚拟地址与线性地址相同, 拟地址空间是由操作系统管理的,每个进程有自己的虚拟地址空间。
7.1.4物理地址:
是内存芯片中的实际地址。CPU最终通过内存管理单元(MMU)将线性地址或虚拟地址映射到物理地址。这是实际存在于硬件中的地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在intel X86架构下逻辑地址到线性地址的转换是通过段式管理机制实现的。在这一机制中,逻辑地址是由段选择器和段内偏移组成的,通过段寄存器和段描述符进行转换。
逻辑地址(有时称为虚拟地址)由两个部分组成:
段选择器: 用于选择当前使用的段描述符
段内偏移: 表示在段内的具体地址。
CPU使用保存于段寄存器的段选择器从GDT或LDT中查找对应的段描述符,然后从段描述符中提取段基地址, 将段基地址与段内偏移相加,得到线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址到物理地址的转换是通过页式管理机制实现的。页式管理提供了一种将线性地址映射到物理地址的方式,通常包括页目录、页表和页帧。
页式管理将内存划分为固定大小的块,称为页(通常为4KB)。页式管理的主要组件包括:
页目录: 存储页表的物理地址,可能存在多级页目录.
页表: 存储页帧的物理地址。
页帧: 实际的物理内存块。
在进行虚拟地址转换成物理地址是首相将虚拟地址解析为页目录索引、页表索引和页内偏移,然后通过页目录索引找到页目录项,获取页表的物理地址,然后在从页表索引找到页表项,获取页帧的物理地址。最后通过页帧地址和页内偏移计算物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
对于虚拟地址(VA)到物理地址(PA)的转换是通过多级页表和转换后备缓冲区(TLB,Translation Lookaside Buffer)实现的。
TLB是一种高速缓存,用于存储最近使用的虚拟地址到物理地址的映射。它加速了地址转换过程,因为查找TLB比查找页表要快得多。如果TLB命中(即找到相应的映射),则可以直接获得物理地址;如果TLB未命中,则需要查找页表来进行转换。
Core i7C CPU的基本参数如下:
虚拟地址48位,
物理地址52位
TLB四路16组相连
页大小4kB 12位,四级页表可以计算的,VPO PPO 12位,VPN36位 PPN40位,每个页目录表和页表有512条条目.对于TLB缓存,16组则组号4位,标志32位,按照缓存的方式存取.
CPU将VA送给MMU,MMU使用前36位在TLB中进行匹配,命中则将得到的PPN与VPO组合得到PA,若TLB没有命中,则用CR3确定内存中以及页表的起始地址,用VPN1(9位)查的二级页表的地址,如此反复直至查询到PPB40位为止,再与PPO计算出物理地址.如过查询时发现不在物理内存中则引发缺页中断,从硬盘中读入.
7.5 三级Cache支持下的物理内存访问
缓存(Cache)用于加速对内存数据的访问。缓存通常分为三级:一级缓存(L1),二级缓存(L2),和三级缓存(L3)。每一级缓存的容量和速度都不同,L1缓存最快但容量最小,L3缓存最慢但容量最大。
当CPU需要访问物理内存中的数据时,首先会尝试在缓存层次结构中找到所需的数据。这一过程称为缓存查找或缓存命中检测。
CPU L1cache参数如下:
52位物理地址
8路64组,组相联
块大小64字节
块大小64位可知块偏移6位,组号6位,标记40位,
在上一节我们已经得到了物理地址,首先使用组号进行组索引,然后将8个路中的标记进行匹配,如果匹配成功且标志位是1则根据块偏移取出数据,如果其他情况,则向L2级缓存中请求数据,成功取回,失败下一级,依次是 L1 L2 L3主存.
取得数据后,需要对数据进行读入,一种常见的放置策略是吗,如果组内有空块则直接放入,如果没有则产生冲突,采用LFU策略进行驱逐替换.
7.6 hello进程fork时的内存映射
当fork函数被终端调用后,内核为新进程创建各种数据结构,并分配PID,为其创建虚拟内存,mm_struct 区域结构和页表的原样副本,将两个进程的每个页面都标记为只读,并将两个进程的每个区域结构都标记为私有的写时复制.
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当两个进程中的任一一个写数据时,写时复制机制,会申请新的物理页并将数据复制到这个页中,并在页表重新映射到这个页上,这样就维护了每个进程虚拟内存的独立.
7.7 hello进程execve时的内存映射
exevce函数加载并运行hello程序需要一下几个步骤
1)删除已存在的用于区域
删除当前进程虚拟地址中的用户部分中已存在的区域结构,
2)映射私有区域
为新程序的代码,数据,bss和栈区域创建新的区域结构,其中代码和数据分别映射到hello文件中的.text和.data节,bss是置0的,其大小用hello文件的中的信息决定,栈的初始长度为0.
3)映射共享区域
若hello程序与共享对象或目标链接,将这些对象动态链接到hello程序,再链接到进程的虚拟内存中的共享区域,
4)设置程序计数器
最后,execve设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点.
7.8 缺页故障与缺页中断处理
缺页故障(Page Fault)是操作系统在虚拟内存管理中处理的一种常见情况。它发生在进程试图访问的虚拟地址在内存中没有对应的物理页时。操作系统会通过缺页中断(Page Fault Interrupt)处理这一情况,将缺失的页加载到内存中,然后重新执行导致缺页故障的指令。
当进程试图访问某个虚拟地址时,CPU通过页表查找相应的物理地址。如果该虚拟地址没有对应的物理页(即页表项中不存在或标记为不在内存中),则会产生缺页故障。缺页故障触发缺页中断,控制权转交给操作系统的内存管理模块。CPU保存当前的执行状态和进程上下文,以便中断处理完成后能够继续执行。
操作系统的缺页中断处理程序开始执行, 操作系统检查缺页故障的原因.如果是页未加载到内存中操作系统从虚拟内存管理结构中查找缺失页在二级存储(如磁盘)中的位置,并在物理内存中分配一个空闲的页框,将缺失页从磁盘加载到分配的物理页框中, 果没有可用的空闲页框,则需要使用页替换算法选择一个页框,将其内容换出到磁盘,然后分配给缺失页。将虚拟地址与新加载的物理页框建立映射关系并设置相应的访问权限,最后操作系统恢复进程的执行状态,并重新执行导致缺页故障的指令。进程从操作系统转交给hello程序.
7.9动态存储分配管理
动态内存管理的基本方法与策略涉及在程序运行时有效地分配、使用和释放内存。
7.9.1动态内存管理的基本方法:
1)显示分配器:
要求应用显式地释放任何已分配的块。例如,c标准库提供一种叫做malloc程序包的显式分配器。c程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。c++中的new和delete操作符与c中的malloc和free相当。
2)隐式分配器:
要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集,例如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
7.9.2动态内存管理的策略
1)带边界标签的隐式空闲链表
带边界标签的隐式空闲链表使用边界标签(boundary tags)来管理内存块,内存块之间没有显式的指针链接。每个内存块包含头部和尾部的边界标签,这些标签存储块的大小和状态(分配或空闲)。
2)显示空间链表
显式空闲链表使用链表来维护所有空闲块,链表中的每个节点都包含指向下一个和上一个空闲块的指针。这种方式提供了更高效的空闲块管理。
3)分配策略:
首次适应(First Fit):选择第一个能够满足需求的空闲内存块。
最佳适应(Best Fit):选择最小且能够满足需求的空闲内存块。
最差适应(Worst Fit):选择最大的能够满足需求的空闲内存块。
4) 释放策略:
立即释放:释放不再使用的内存,使其能够被重新分配。
延迟释放:在一定条件下,将不再使用的内存保留一段时间后再释放,以提高性能。
5)碎片处理:
内部碎片(Internal Fragmentation):已分配的内存空间中有一部分未被使用,可以通过合并或重新分配来减少。
外部碎片(External Fragmentation):分配的内存块之间有大量未被使用的空间,可以通过紧缩内存空间或者使用内存池等方法来减少。
6)内存合并于分割:
合并(Coalescing):当有相邻的多个空闲内存块时,将它们合并成一个更大的内存块,以减少碎片。
分割(Splitting):当分配请求的内存小于某个空闲内存块时,将该块分割成两部分,一部分分配给请求,另一部分保留为新的空闲内存块。
7.10本章小结
在本章详细的分析了hello程序的各级存储机制,从hello的寻址方式,如虚拟内存,逻辑内存,物理内存的映射关系,TLB和4级页表,三级缓存的访问,缺页故障,以及fork和execve的详细存储机制.
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
在Linux中,所有的IO设备(网络、磁盘、终端等)都被模型化为文件,所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
8.2.1 Unix接口:
1) 打开文件:
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件;Linux shell创建的每个进程开始时都有三个打开的文件,标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2);
2) 改变当前的文件位置:
对于每个打开的文件,内核保持着一个文件位置k,初始为0;
3) 读写文件:
一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n;类似地,写操作就是从内存复制n>0个字节到文件,从当前文件位置k开始,然后更新k;
4) 关闭文件:
当应用完成了对文件的访问之后,它就通知内核关闭这个文件。
8.2.2 Unix IO函数:
1)open():
原型:int open(const char *pathname, int flags);
描述:打开由pathname指定的文件,并返回一个文件描述符。如果成功,它将返回一个非负整数表示文件描述符。flags参数指定文件应该以何种模式打开,如只读、只写或读写,并且可以包括额外的选项,如在文件不存在时创建文件。
2) close():
原型:int close(int fd);
描述:关闭与文件描述符fd相关联的文件。关闭后,文件描述符将不再有效用于进一步的操作。
3) read():
原型:ssize_t read(int fd, void *buf, size_t count);
描述:从与文件描述符fd相关联的文件中读取数据到由buf指向的缓冲区。它最多读取count字节的数据,并返回读取的字节数。在文件末尾返回0,在错误时返回-1。
4) write():
原型:ssize_t write(int fd, const void *buf, size_t count);
描述:从由buf指向的缓冲区向与文件描述符fd相关联的文件中写入数据。它最多写入count字节的数据,并返回写入的字节数。在错误时返回-1。
5) lseek():
原型:off_t lseek(int fd, off_t offset, int whence);
描述:根据指示whence,将文件描述符fd相关联的打开文件的文件偏移量重新定位到参数offset。它返回从文件开头以字节为单位的结果文件偏移量。whence可以取三个值之一:SEEK_SET(文件开头)、SEEK_CUR(当前文件偏移量)或SEEK_END(文件末尾)。
8.3 printf的实现分析
8.3.1printf()的实现:
第一句va_list 是 char* arg arg 保存的是printf第一个参数的地址,
第二句vprintf()
printf中 汇编代码中 vprintf()的调用
vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。其中buf用于保存生成好的输出信息,fmt是格式化字符串,args是printf中第一个参数的地址(即向格式化输出中传递值的变量的地址), vsprintf生成了一个可以直接显示的字符串.
第三行write(buf, i):吧io
是系统调用io输出的函数,其汇编代码是
这里使用eax ebx ecx 传递参数然后使用int 使用陷阱-系统调用,让系统输出已经处理好的字符串. 内核的显示驱动程序会通过这些字符串及其字体生成要显示的像素数据,将它们传到屏幕上对应区域的显示vram中。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
8.4.1:getchar的实现:
首先函数中申请一定空间,用于保存读入的字符,bufp保存buf的地址. 则调用read向输入缓冲区中读入一行字符串。而read会通过syscall陷阱跳到内核,内核会使得调用方不断等待。当按下键盘后,键盘中断处理程序执行,向输入缓冲区中放入由键盘端口读入的扫描码转换成的字符,直到按下回车后调用方不再等待。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回.
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
8.5本章小结
在本章从系统级IO学习了printf和getchar等函数的实现,深入了解了hello如何在屏幕上打印信息,和接受键盘输入,对中断,异常,和系统IO有了一定了解.
结论
用计算机系统的语言,逐条总结hello所经历的过程。
1)使用C语法编写hello.c文件.
2)使用预处理器,将hello.c文件的预处理指令进行处理.得到hello.i文件
3)使用汇编器进行汇编,将高级语言翻译成汇编语言程序.hello.s
4)编译hello.s文件得到可重定位的可执行文件hello.o
5)采用静态链接将hello.o进行重定位和补全引用的函数.
6)运行时,bash首先使用fork创建一个新的进程,然后使用execve函数将hello程序加载到内存中.并完成动态链接
7)经过4级页表,和3级缓冲,开始执行hello程序.
8)根据异常和中断完成进程的调用,使用信号完成进程间的通信.
9)调用系统级IO函数完成字符串的输入和键盘的输入.
感想:
hello时C学习的第一个程序,也是最简单的程序,然而hello的运行也是计算机全体软硬件密切配合的结果,需要整个计算机系统的调动与协调,最终才能够在屏幕上显示.在以后的工作中也要重视整个系统的配合和对各部分工作的优化与整理.
附件
hello:可以运行的hello程序
hello.c:hello程序的源代码
hello.i:预处理的结果
hello.s:汇编语言下的hello
hello.o:编译生成的可重定位的可执行文件
hello.obj:hello.o使用odjdump的结果
hello.obj2:hello使用odjdump的结果
参考文献
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
[7] ChatGPT
[8] Push, Pop, call, leave 和 Ret 指令图解_push等价-CSDN博客
[9] gdb调试--汇编指令处断点_gdb 对cpu指令断点-CSDN博客
[10] read和write系统调用以及getchar的实现_getchar 和read-CSDN博客
[11] C语言#include的用法详解(文件包含命令) (biancheng.net)
[12] [转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com)
[13] 微机原理——指令系统——算数运算指令(ADD、ADC、SUB、SBB、INC、DEC、NEG、CMP、MUL、IMUL、DIV、IDIV、CBW、CWD、BCD调整)_微机原理cmp指令-CSDN博客