计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 数据科学与大数据技术
学 号 2022112863
班 级 2203501
学 生
指 导 教 师
计算机科学与技术学院
2024年5月
本文探讨了Hello程序在计算机系统中的整个生命周期,从预处理、编译、汇编、链接到进程管理、存储管理、IO管理等方面逐一进行了详细的分析以及解析,深入探讨了Hello程序在计算机系统中的运行机理。深入浅出地介绍了在UBUNTU操作系统下各个阶段的命令操作和结果解析,展现了程序在计算机系统中的历程。通过对预处理、编译、汇编、链接等过程的解析,可以清晰地了解程序从源码到可执行文件的转化过程,以及各个阶段所起到的作用。通过对进程管理、存储管理和IO管理等方面的分析,揭示了程序在计算机系统中的运行机制和各种管理方法,并对其执行过程进行了详细描述和分析。对每个阶段的概念、作用和具体实现的详细分析,针对每个阶段的命令和结果展开解析,同时结合实例说明,能够深入理解Hello程序在计算机系统中的运行过程和原理。本文系统地展现了Hello程序在计算机系统中的生命周期,对计算机系统的运行原理有了更加深入的理解,并且具有重要的指导作用。同时本文也为进一步研究和探讨计算机系统的运行原理提供了重要参考。
关键词:Hello程序;预处理;编译;汇编;链接;进程管理;存储管理;IO管理;UBUNTU操作系统;
目 录
第1章 概述................................................................................... - 4 -
1.1 Hello简介............................................................................ - 4 -
1.2 环境与工具........................................................................... - 5 -
1.3 中间结果............................................................................... - 5 -
1.4 本章小结............................................................................... - 5 -
第2章 预处理............................................................................... - 6 -
2.1 预处理的概念与作用........................................................... - 6 -
2.2在Ubuntu下预处理的命令................................................ - 6 -
2.3 Hello的预处理结果解析.................................................... - 6 -
2.4 本章小结............................................................................... - 9 -
第3章 编译................................................................................. - 10 -
3.1 编译的概念与作用............................................................. - 10 -
3.2 在Ubuntu下编译的命令.................................................. - 10 -
3.3 Hello的编译结果解析...................................................... - 10 -
3.4 本章小结............................................................................. - 16 -
第4章 汇编................................................................................. - 17 -
4.1 汇编的概念与作用............................................................. - 17 -
4.2 在Ubuntu下汇编的命令.................................................. - 17 -
4.3 可重定位目标elf格式...................................................... - 17 -
4.4 Hello.o的结果解析........................................................... - 20 -
4.5 本章小结............................................................................. - 22 -
第5章 链接................................................................................. - 23 -
5.1 链接的概念与作用............................................................. - 23 -
5.2 在Ubuntu下链接的命令.................................................. - 23 -
5.3 可执行目标文件hello的格式......................................... - 23 -
5.4 hello的虚拟地址空间....................................................... - 26 -
5.5 链接的重定位过程分析..................................................... - 27 -
5.6 hello的执行流程............................................................... - 29 -
5.7 Hello的动态链接分析...................................................... - 30 -
5.8 本章小结............................................................................. - 33 -
第6章 hello进程管理.......................................................... - 34 -
6.1 进程的概念与作用............................................................. - 34 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 34 -
6.3 Hello的fork进程创建过程............................................ - 35 -
6.4 Hello的execve过程........................................................ - 36 -
6.5 Hello的进程执行.............................................................. - 37 -
6.6 hello的异常与信号处理................................................... - 37 -
6.7本章小结.............................................................................. - 41 -
第7章 hello的存储管理...................................................... - 42 -
7.1 hello的存储器地址空间................................................... - 42 -
7.2 Intel逻辑地址到线性地址的变换-段式管理.................. - 42 -
7.3 Hello的线性地址到物理地址的变换-页式管理............. - 43 -
7.4 TLB与四级页表支持下的VA到PA的变换................... - 44 -
7.5 三级Cache支持下的物理内存访问................................ - 45 -
7.6 hello进程fork时的内存映射......................................... - 46 -
7.7 hello进程execve时的内存映射..................................... - 47 -
7.8 缺页故障与缺页中断处理................................................. - 48 -
7.9动态存储分配管理.............................................................. - 48 -
7.10本章小结............................................................................ - 49 -
第8章 hello的IO管理....................................................... - 50 -
8.1 Linux的IO设备管理方法................................................. - 50 -
8.2 简述Unix IO接口及其函数.............................................. - 50 -
8.3 printf的实现分析.............................................................. - 51 -
8.4 getchar的实现分析.......................................................... - 54 -
8.5本章小结.............................................................................. - 54 -
参考文献....................................................................................... - 58 -
第1章 概述
1.1 Hello简介
P2P:From Program to Process,从程序到进程,是指将源代码转变为可执行程序并最终在操作系统中运行的整个过程。首先开发者编写源代码,代码通常以高级编程语言编写。接着通过编译器将源代码转换为机器代码或中间代码。如果是编译语言,编译器会生成目标代码文件;如果是解释语言,解释器则在运行时逐行翻译和执行代码。编译过程中,源代码先经过词法分析,将代码分解成一系列记号;然后进行语法分析,生成语法树;接着是语义分析,确保代码逻辑正确;最后进行优化和代码生成,将高级语言转换为机器指令。生成的机器代码通过链接器与库文件和其他模块结合,形成最终的可执行文件。可执行文件在操作系统的控制下被加载到内存中,操作系统为其分配内存空间和系统资源,创建进程控制块,初始化堆栈和堆,设置进程的运行环境。当进程被调度器调度运行时,CPU开始执行该进程的指令。此时,程序从静态的代码变成了一个动态的运行实体。操作系统通过进程管理、内存管理、文件系统和I/O控制等机制来管理进程的执行,确保其高效、安全地运行。整个P2P过程展示了从编写代码到程序最终在计算机系统中运行的详细流程。
020:From Zero-0 to Zero-0,从未无到有再到无的过程,最初程序员在文本编辑器中编写文件,接下来通过预处理,源码被转换成中间代码,随后编译器将中间代码翻译成汇编代码,这一过程会优化代码,提高执行效率。接着汇编器将汇编代码转换成机器码,生成目标文件。最后链接器将目标文件与库文件结合,生成可执行文件。当用户在命令行输入时,操作系统启动新进程。首先shell调用fork()创建子进程,然后通过execve()执行程序。操作系统为新进程分配内存,使用mmap()映射文件到内存,分配时间片在CPU上执行指令。虚拟地址通过MMU转换为物理地址,TLB和多级页表加速地址转换,缓存进一步优化数据访问速度。程序执行过程中,操作系统管理I/O操作,处理键盘输入、屏幕输出等。键盘输入由驱动程序捕获并传递给操作系统,再传递给进程;屏幕输出则是通过系统调用将文本显示在终端上。在程序运行的短暂时间内,硬件和操作系统默契合作,确保每一步都顺利进行。程序执行完毕后,操作系统回收资源,子进程通过exit系统调用结束生命,父进程通过wait系统调用收集子进程的退出状态。此时程序从内存中被移除,从进程表中消失,归于初始状态。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上。
软件环境:Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟64位以上。
开发与调试工具:Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc。
1.3 中间结果
hello.i:hello.c预处理后生成的预处理文件;
hello.s:hello.i编译后生成的汇编程序文件;
hello.o:hello.s汇编后生成的可重定位目标文件;
hello.elf:hello.o生成的ELF文件;
hello.asm:hello.o反汇编后生成的反汇编文件;
hello:hello.o链接后生成的可执行文件;
hellold.elf:hello生成的ELF文件;
hellold.asm:hello反汇编后生成的反汇编文件;
1.4 本章小结
本章介绍了Hello程序P2P,020的整体过程以及完成实验所需要的硬件、软件以及开发与调试工具,并列出了实验所需要生成的全部中间结果。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理是编译过程的第一步,它主要是对源代码进行文本替换和宏扩展等操作,为编译器生成更容易处理的代码。预处理由预处理器完成,在 C 和 C++ 编程语言中,预处理器命令以 # 号开头。
作用:预处理的主要作用是简化代码、提高代码的可读性和维护性,同时通过宏和条件编译等手段,使代码更灵活和可配置。在编译器接收到预处理器输出的代码后,后续的编译、汇编和链接步骤才能更顺利地进行。
1.宏定义和替换:通过#define定义的宏,在预处理过程中会被相应的值或代码块替换。宏可以是简单的文本替换,也可以是带参数的宏函数。
2.文件包含:通过#include指令,将其他文件的内容插入到当前文件中。常见的用途是包含头文件,这样可以实现代码的模块化和复用。
3.条件编译:通过#if、#ifdef、#ifndef、#else和#endif等指令,可以根据特定条件编译代码的某些部分。这在跨平台编程和调试中非常有用。
4.行控制:通过#line指令可以改变编译器报告的行号和文件名,这在生成代码和调试时很有用。
5.错误和警告:通过#error和#warning指令,可以在预处理阶段产生错误或警告信息。
2.2在Ubuntu下预处理的命令
预处理指令:gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -E hello.c -o hello.i。
图2.2.1 Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
图2.3.1 hello.c源程序文件内容
图2.3.2 hello.c hello.i文件大小
图2.3.3hello.i main函数
图2.3.4 hello.i信息记录
图2.3.5 hello.i extern部分
图2.3.6 hello.i include部分
- 文件大小:预处理后文件的大小相对于源程序文件变大,大小由源文件的4K变为108K,主要原因是宏替换、文件包含、条件编译等预处理操作导致的代码量增加。
- main函数:源代码中的main函数与预处理后的main函数内容保持一致,并未发生任何变化,原因是主函数本身并不包含宏定义、条件编译或文件包含等需要预处理的指令。因此在预处理阶段,主函数的内容并不会发生变化。
- 源程序注释部分:预处理后注释部分被完全移除,注释的移除是因为在编译阶段,注释对于生成目标代码是没有意义的,这种注释移除的行为使得预处理后的文件只包含实际的代码,不再包含任何注释,从而减少了生成的目标代码的大小。这有助于提高编译速度和减少目标代码的大小,同时也确保了代码的保密性,因为注释可能包含敏感信息。
- 信息记录:在hello.i文件开头会生成一串代码,它们的主要作用是在预处理阶段记录文件来源、标识命令行、指定包含的文件,并提供相关的信息以供调试和记录。
- extern部分:来自系统头文件的一部分,包含了一系列外部函数声明和函数属性声明,这些声明的目的是在预处理阶段中标识外部函数的原型以及函数的属性,以便在后续的编译和链接阶段使用这些函数。
- include部分:来自系统头文件,这些指令的目的是在预处理阶段中标识文件位置、定义类型以及包含其他头文件,以便生成最终的预处理结果。
2.4 本章小结
在本章中,我们深入了解了预处理阶段在编译过程中的作用以及预处理器在其中的角色。预处理是编译过程中的第一个阶段,它主要负责对源代码进行一系列的处理,为后续的编译过程做准备。首先我们介绍了预处理的基本概念及其作用,接着我们详细介绍了预处理阶段的各项操作。最后通过对预处理后的代码进行解析,说明了预处理后代码的格式和内容,以及各个指令的作用。通过本章可以更加深入地理解预处理阶段在编译过程中的重要性和作用。
3.1 编译的概念与作用
概念:编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序,编译是软件开发中至关重要的过程,它将人类可读的高级语言源代码转换成计算机可执行的低级语言机器代码。这个复杂的过程包括了词法分析、语法分析、语义检查、中间代码生成、代码优化和目标代码生成等多个阶段。词法分析器将代码分解成词法单元序列,语法分析器将这些单元组织成语法结构,形成语法分析树;语义检查阶段确保代码的语义正确性,并生成中间代码表示,使得后续的优化和目标代码生成更为简单。随后的代码优化阶段通过多种等价变换提高代码效率,最终,目标代码生成器将优化后的中间代码转换为目标机器上的机器语言代码。编译的目的是确保程序的正确性、效率和可维护性,使得程序可以在特定平台上高效运行。
作用:编译是一项关键的软件开发过程,其作用不仅在于将人类可读的高级语言代码转换成计算机可执行的低级语言代码,更重要的是通过一系列精细的处理过程,确保程序在不同平台上的正确性、性能和可维护性。到目标代码生成,编译过程涉及词法分析、语法分析、语义检查、中间代码生成、代码优化等多个阶段,每个阶段都致力于解决特定的问题,如语法错误检测、优化程序结构、减少执行时间等。通过编译,程序员可以专注于高级抽象,而不必关注底层硬件细节,同时生成的优化目标代码可以显著提升程序的性能和效率,从而使得软件开发更加高效、可靠,为用户提供更优质的软件体验。
3.2 在Ubuntu下编译的命令
编译指令:gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s。
图3.2.1 Ubuntu下编译的命令
3.3 Hello的编译结果解析
图3.3.1 hello.c源程序文件内容
图3.3.2 hello.i hello.s文件大小
图3.3.3 hello.s文件内容
3.3.1 文件大小
编译后文件的大小相对于预处理后文件变小,大小由预处理后文件的108K变为4K,主要原因包括移除注释、宏展开、优化和精简以及符号表和元数据的处理。这些因素使得编译后的文件更加精简、高效,同时保留了源代码的功能和逻辑。
3.3.2 数据
常量:在程序中包含printf打印的字符串常量以及if和for里的数字常量,字符串常量通过.LC0
和.LC1
标签来表示。.LC0
和.LC1
分别代表字符串常量\347\224\250\346\263\225:Hello\345\255\246\345\217\267\345\247\223\345\220\215\346\211\213\346\234\272\345\217\267\347\247\222\346\225\260\357\274\201和Hello %s %s %s\n,这些字符串常量被存储在只读数据段.rodata
中,并由编译器给出唯一的标识符以供代码引用。数字常量被存储在.text段中,且作为立即数出现。
变量:已经初始化的全局变量存储在.data段,未初始化的全局变量存储在.bss段,且在程序开始时会自动被初始化为0。
图3.3.4 hello.s局部变量存储地址
局部变量是函数参数argc和argv以及for循环的i,编译器在处理C语言的局部变量时,会在栈上分配空间、管理它们的生命周期,并生成相应的机器代码来访问和操作这些变量。局部变量通过调整栈指针进行管理,并通过寄存器和内存操作指令进行访问和修改。这样的处理方式确保了局部变量在函数调用之间的隔离和独立。
类型及转化:编译器会根据变量声明确定其类型,并在生成的汇编代码中使用相应的数据大小和指令操作。例如,int i;
声明了一个整型变量i
,在汇编中对应的操作可能是movl
指令,用于操作4字节的整数数据。在main
函数的参数列表中,char *argv[]
声明了一个指向字符型指针数组的指针。编译器会将指针类型的变量表示为相应的内存地址,并在汇编代码中使用movq
指令来处理64位指针数据。在函数调用过程中,参数传递涉及到类型的匹配和转换。例如,atoi(argv[4])
将字符串类型的参数转换为整数类型。编译器会生成相应的指令来实现类型转换,确保函数能够正确地接收和处理参数。
图3.3.4 hello.s赋值操作语句
赋值:在C语言中,赋值操作是将一个值或表达式的结果存储到变量中。在编译器处理赋值时,它会将赋值语句翻译成相应的汇编代码,以便在程序执行时正确地执行赋值操作。在汇编代码中,赋值操作的具体实现通常遵循类似的原则:确定源操作数的地址,将该地址中的值存储到目标寄存器或内存位置中。
图3.3.4 hello.s算数操作语句
算术操作:在hello.c源代码中使用了i++的算术操作,对应的汇编代码中的操作为栈上存储变量i的值每次加一。
图3.3.4 hello.s关系操作语句
关系操作:if条件判断时所使用的!=和<是逻辑关系操作,对应的汇编代码中将其转换为比较cmpX指令,然后再通过jX指令来判断是否满足。
图3.3.4 hello.s数组/指针操作语句
数组/指针操作:对于数组,将数组名解释为第一个元素的指针,由于数组是顺序排列的且在相邻地址空间上,因此用数组名的偏移量来表示数组中的元素。
图3.3.4 hello.s控制转移语句
控制转移:if或者for循环是控制转移操作,在汇编代码中控制转移(跳转指令)往往与关系操作一起出现,是否进行控制转移取决于cmp设置的逻辑关系操作的结果位,亦或者直接执行跳转指令jmp。
图3.3.4 hello.s函数操作语句
函数操作:在hello.i中涉及了多个函数调用,如puts、exit、_printf_chk、strtol、sleep、_IO_getc。函数调用传递参数的规则:第1~6个参数依次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈中,按照参数逐个从后向前的顺序。且在这些指令前都有mov指令,目的是再调用函数前将函数参数保存在相应的寄存器中一边调用函数时函数的执行。
3.4 本章小结
本章深入探讨了编译过程的各个阶段,每个阶段在编译过程中都有特定的作用和任务。此外还详细探讨了全局变量在编译过程中的处理方式。通过具体示例分析了编译器如何处理C语言中的数据、赋值、类型转换、算术操作、关系操作、数组/指针操作、控制转移和函数操作。综上所述,本章为我们理解编译器的工作原理提供了全面且详细的知识框架。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程,汇编是将高级编程语言生成的汇编代码转化为可执行机器代码的过程。汇编语言是一种低级编程语言,它使用人类可读的符号和助记符来表示机器指令,与特定处理器架构直接对应。汇编程序通常由汇编器(Assembler)来处理,将汇编代码转化为二进制机器指令。
作用:汇编的作用在于将汇编语言代码转化为机器可以直接执行的机器代码,从而实现程序在硬件上的实际运行。通过汇编器,将人类可读的汇编代码转化为二进制指令,确保程序能够高效地与计算机硬件交互,并充分利用处理器的指令集架构,提高程序执行的性能和效率。
4.2 在Ubuntu下汇编的命令
汇编指令:as hello.s -o hello.o。
图3.2.1 Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.3.1 ELF格式与readelf命令
图4.3.1 ELF格式
图4.3.2 readelf命令
4.3.2 ELF头
图4.3.3 ELF头
以16字节序列开始,描述生成该文件系统字的大小和字节顺序,而后包含帮助连接器语法分析和解释目标文件的信息。
4.3.3 头节表
图4.3.4 头节表
描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息。
4.3.4 重定位节
图4.3.5 重定位节
各个段引用的外部符号等在链接时需要通过重定位对这些位置的地址进行修改。链接器会通过重定位节的重定位条目计算出正确的地址。hello.o需重定位的内容:.rodata中的模式串,puts,exit,_printf_chk,strtol,sleep,stdin、_IO_getc等符号。
4.3.5 符号表
图4.3.6 符号表
.symtab存放在程序中定义和引用的函数和全局变量的信息,包括Value、Size、Type、Bind、Vis、Ndx、Name等信息。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.4.1 反汇编
反汇编指令:objdump -d -r hello.o > hello.asm。
图4.4.1 反汇编指令
图4.4.2 反汇编内容
4.4.2 结果解析
hello.asm文件展示了hello.s文件的汇编后的机器码的反汇编输出,每一行对应着一个机器指令,以十六进制表示,其左侧列显示了指令的内存地址。指令中包含了寄存器的操作,内存的访问,条件跳转等操作,以及对标准库函数的调用。文件中还包含了一些注释,解释了部分指令的作用,以及指令的相对地址偏移。分支转移不采取.L3 .L2这样的段名称助记符,而是使用相对地址。函数调用汇编代码文件直接调用函数名称,但hello.o的反汇编文件则是直接call了一段地址,但这个相对地址目前就是下一条指令即0,在后面留下了虚拟地址,需要在链接后才能变为确定的地址,对共享库的函数调用要在执行时才能被动态链接器确定。Leaq指令需要读取rodata段地址也采用了这种形式。
4.5 本章小结
本章深入探讨了汇编过程的概念及其作用,了解了汇编过程在将高级语言代码转换为机器码的过程中所起到的关键作用。通过学习`readelf`命令,我们对ELF格式有了详细的了解,了解了ELF文件结构以及各个部分的作用,包括代码段、数据段、符号表等。通过反汇编,我们可以将目标文件转换为汇编代码,进而进行分析和理解。这种分析能够帮助我们深入了解程序的运行机制和内部结构,对代码的优化和调试提供了重要的参考。同时,我们也学习了汇编代码与.s文件之间的差别,这有助于我们更好地理解汇编过程中的转换和优化。
第5章 链接
5.1 链接的概念与作用
概念:链接是指从 hello.o 到hello生成过程,链接是将多个目标文件或库文件组合在一起,形成可执行文件或库文件的过程。链接器负责执行链接过程,将各个目标文件中的代码段、数据段等部分组合在一起,解析符号引用,处理重定位信息,并生成最终的可执行文件或库文件。
作用:链接可以将多个模块的功能整合到一个文件中,确保各个模块之间的地址引用是正确的,减少了重复代码,提高了代码的复用性,并且生成了一个可以被操作系统加载和执行的完整的程序或库。此外,链接还可以实现动态链接,将程序与动态库文件进行链接,实现共享库的动态加载和链接,从而提高了程序的灵活性和运行效率。
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.2.1 Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
5.3.1 ELF格式与readelf命令
图5.3.1 ELF格式
图5.3.2 readelf命令
5.3.2 ELF头
图5.3.3 ELF头
以16字节序列开始,描述生成该文件系统字的大小和字节顺序,而后包含帮助连接器语法分析和解释目标文件的信息。
5.3.3 头节表
图5.3.4 头节表
描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息。
5.3.4 程序头表
图5.3.5 程序头表
其中Offset代表目标文件中的偏移,VirtAdrr代表虚拟内存的地址,PhysAddr代表物理地址的内存,FileSiz代表目标文件中的段大小,MemSiz代表内存中的段大小,flags代表运行时的访问权限,align代表对齐要求。
5.4 hello的虚拟地址空间
使用edb加载hello, data dump窗口可以查看加载到虚拟地址中的hello程序。program headers告诉链接器运行时加载的内容并提供动态链接需要的信息。
图5.4.1 hello的虚拟地址空间起始位置
虚拟地址空间的起始位置是0x400000。
由5.3头节表相关信息知.interp段的起始地址为0x400000+0x200=0x400200,在edb中查询地址可得到信息如下:
图5.4.2 .interp段内容
由5.3头节表相关信息知.rodata段的起始地址为0x400000+0x700=0x400700,在edb中查询地址可得到信息如下:
图5.4.3 . rodata段内容
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.5.1 反汇编
反汇编指令:objdump -d -r hello > hellold.asm。
图5.5.1 反汇编命令
5.5.2 新增函数
图5.5.2 新增函数
链接中加入了在hello.c中用到的库函数,如puts、strtol、_IO_getc、_printf_chk、exit、sleep等函数。
5.5.3 新增节
图5.5.3 新增节
hello中增加了.init和.plt节。
5.5.4 函数调用地址
图5.5.4 函数调用地址
hello已经完成了调用函数时的重定位,因此在调用函数时调用的地址已经变成了函数确切的虚拟地址。
5.5.5 控制流跳转地址
图5.5.4 控制流跳转地址
hello已经完成了调用函数时的重定位,因此在跳转时调用的地址已经变成了函数确切的虚拟地址。
5.6 hello的执行流程
图5.6.1 执行流程
则可知puts函数地址为0x400560,strtol函数地址为0x400570,_IO_getc函数地址为0x400580,_printf_chk函数地址为0x400590,exit函数地址为0x4005a0,sleep函数地址为0x4005b0,main函数地址为0x4005f2等。
5.7 Hello的动态链接分析
几乎每个C程序都使用标准I/O函数,比如printf和scanf,在运行时,这些函数的代码会被复制到每个运行进程的文本段中。在运行多进程的典型系统上,是对内存的极大浪费。共享库是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来,这个过程就称为动态链接,是由一个叫动态链接器的程序来执行的。共享库也称为共享目标,在Linux系统中通常用.so后缀表示。
可以加载而无需重定位的代码称为位置无关代码(PIC),用户对GCC使用-fpic选项指示GNU编译系统生成PIC代码。共享库的编译必须始终使用该选项。
1.PIC数据引用
编译器在数据段开始的地方创建了一个表,叫做全局偏移量表(GOT),在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的绝对地址。
2.PIC函数调用
采用延迟绑定技术
图5.7.1 PLT和GOT
3.hello中的动态链接项目分析
图5.7.2 动态链接
图5.7.3 gdb断点设置
图5.7.4 init前
图5.7.5 init后
通过gdb调试,在_init前后,这些项目的内容变化正体现了延迟绑定的过程,通过plt与got协同工作实现延迟绑定,我们也在调用过后看到了GOT中的内容被动态链接器修改。
5.8 本章小结
在本章中主要介绍了链接的概念与作用,分析了hello的ELF格式,在linux中使用edb,gdb等分析了hello的虚拟地址空间、重定位过程、执行流程和动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是计算机中正在执行的程序的实例,包括程序代码和当前活动的相关数据。它由操作系统管理,是资源分配的基本单位,每个进程拥有独立的地址空间和控制结构,包括程序计数器、堆栈指针和其他处理器寄存器,以确保程序的正确执行和隔离。
作用:进程的作用在于提供一个独立的执行环境,使得操作系统能够多任务处理,即同时运行多个程序。通过进程,系统可以分配资源(如CPU时间、内存和I/O设备)并进行进程间通信与同步,从而提高系统效率和实现复杂的任务管理。
6.2 简述壳Shell-bash的作用与处理流程
作用:是用户与操作系统之间的桥梁,是一种命令行解释器。其主要作用包括:
- 命令解释与执行:Shell读取用户输入的命令,并调用相应的操作系统服务或应用程序执行这些命令。
- 脚本编程:Shell提供编程语言功能,使用户能够编写脚本,自动化执行一系列命令。
- 进程管理:Shell可以启动和控制进程,支持后台运行、进程间通信等。
- 文件管理:通过Shell,用户可以进行文件创建、删除、移动、复制等操作。
- 环境管理:Shell维护和管理用户的工作环境,包括环境变量的设置和使用。
处理流程:
- 启动阶段:当用户登录到系统或打开一个终端时,bash Shell会启动并进行初始化。
读取配置文件:bash在启动时会读取系统级和用户级的配置文件。
初始化环境:根据配置文件内容,bash设置命令搜索路径、环境变量、别名等。
- 命令输入与读取
显示提示符:bash显示提示符(如$
)等待用户输入。
读取命令:用户输入命令后,bash从标准输入读取该命令。支持多行输入和命令编辑功能。
- 解析与解释
词法分析:bash将用户输入的命令行拆分成命令和参数。它会识别命令分隔符、管道符、重定向符等。
语法分析:bash检查命令的语法结构,识别命令、选项和参数。
- 查找命令
内建命令检查:首先检查命令是否是bash的内建命令。
外部命令查找:如果不是内建命令,bash会在环境变量PATH
指定的目录中查找可执行文件。
- 执行命令
内建命令执行:如果是内建命令,bash直接执行。
外部命令执行:如果是外部命令,bash创建一个子进程(通过fork
),然后在子进程中加载并执行该命令(通过exec
)。
- 处理重定向和管道
重定向:bash根据命令中的重定向符,重定向输入和输出文件描述符。
管道:如果命令行包含管道符,bash将多个命令连接起来,使前一个命令的输出作为后一个命令的输入。
- 等待与收集进程状态
等待命令执行完成:对于前台进程,bash会等待命令执行完成,并收集其退出状态。
后台进程管理:对于后台进程,bash立即返回提示符,允许用户继续输入其他命令。
- 显示结果与提示符
显示命令输出:bash显示命令的输出结果到标准输出(通常是终端)。
显示提示符:完成命令执行后,bash再次显示提示符,等待用户输入下一个命令。
- 脚本执行
读取脚本文件:如果执行的是脚本文件,bash按照上述步骤读取并解释脚本中的每一行命令。
执行脚本内容:逐行解析并执行脚本内容,包括命令、控制结构(如循环、条件语句)和函数定义等。
6.3 Hello的fork进程创建过程
1.父进程调用fork:当父进程执行fork函数时,操作系统会开始创建一个新的子进程。fork调用是单次执行,但有两个返回值,分别在父进程和子进程中返回。
2.子进程的创建:在fork调用过程中,操作系统为子进程分配一个新的进程ID (PID),并复制父进程的虚拟地址空间。虽然子进程的内存内容与父进程相同,但这两者在内存中的实际位置是独立的,因此子进程的任何修改不会影响父进程,反之亦然。
3.复制文件描述符:子进程会继承父进程中所有打开的文件描述符。文件描述符是指向文件表项的引用,而这些文件表项记录了文件的状态和偏移量。因此,子进程和父进程共享相同的文件表项,但拥有独立的文件描述符。
4.返回值:在父进程中,fork函数返回子进程的PID;在子进程中,fork函数返回0。通过检查fork的返回值,程序可以分辨自己是父进程还是子进程,从而执行不同的代码路径。
5.执行路径:父进程继续从fork返回值之后的代码执行;子进程从fork返回值之后的代码执行,这使得它看起来像是fork调用了一次,但返回了两次。
6.PID的差异:子进程和父进程的最大区别在于它们拥有不同的进程ID。通过不同的PID,操作系统可以区分和管理这些进程。
7.独立的执行:子进程是父进程的独立副本,尽管最初它们的状态相同。子进程可以执行与父进程完全不同的任务,或者继续执行相同的任务但在不同的数据集上操作。
在调用fork函数后,系统创建了一个新的子进程,这个子进程是父进程的几乎完全的副本,但有独立的内存空间和文件描述符。fork函数调用一次,但返回两次,分别在父进程和子进程中返回不同的值。通过这些返回值,父进程和子进程可以执行不同的代码路径,进行不同的操作。这种机制是UNIX和类UNIX系统中多任务处理的基础。
6.4 Hello的execve过程
int execve(char *filename, char *argv[], char *envp[]),在当前进程中载入并运行一个新程序:filename是可执行文件,目标文件或脚本(用#!指明解释器);argv是参数列表,惯例;envp是环境变量列表。覆盖当前进程的代码、数据、栈,保留有相同的PID,继承已打开的文件描述符和信号上下文。调用一次并从不返回除非有错误。在当前进程中加载并运行新程序a.out的步骤为:删除已经存在的用户区域;创建新的区域结构,代码和初始化数据映射到.text和.data区(目标文件提供),.bss和栈映射到匿名文件;设置PC,指向代码区域的入口点,Linux根据需要换入代码和数据页面。
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
进程调度是操作系统中的重要功能,它负责决定在多个就绪态进程中,哪个进程将获得CPU的使用权。调度过程涉及到多个方面,包括进程的上下文信息、时间片、用户态与核心态转换等等。
1.进程上下文信息:每个进程都有自己的上下文信息,包括进程的寄存器状态、程序计数器、内存管理信息、打开文件描述符等。调度时,操作系统会保存当前运行进程的上下文信息,以便在恢复执行时能够准确恢复到上一次执行的状态。
2.进程时间片:时间片是操作系统分配给每个进程的CPU使用时间。当一个进程的时间片用尽时,操作系统会进行调度,将CPU分配给下一个就绪态进程,从而实现多任务处理。时间片的长度可以根据系统的策略和需求进行调整,常见的调度算法包括轮转调度、优先级调度等。
3.用户态与核心态转换:在进程调度过程中,涉及到用户态与核心态之间的转换。当一个进程需要执行特权指令或访问受保护的资源时,需要切换到核心态。而大多数时间,进程都在用户态下执行,执行一般指令。调度器会确保进程在需要时能够在用户态和核心态之间进行切换,从而实现对系统资源的合理管理和保护。
4.调度过程:当一个进程的时间片用尽或者发生阻塞时,操作系统会触发调度;调度器会根据预先定义的调度算法,选择下一个就绪态进程;操作系统会保存当前运行进程的上下文信息,将CPU控制权转移到选定的下一个进程;如果需要,调度器会更新进程的状态,例如将一个运行态进程变为等待态,或将一个等待态进程变为就绪态;新的进程开始执行,并且CPU的执行权交给了它。
进程调度过程是操作系统中的核心功能之一,通过合理的调度算法和上下文管理,实现了多个进程之间的公平竞争和有效利用CPU资源。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
正常运行时情况:程序完成被正常回收。
图6.6.1 正常运行状态截图
输入Ctrl-Z:进程收到SIGSTP信号,hello进程挂起并向父进程发送SIGCHLD。
图6.6.2 输入Ctrl-Z运行状态截图
运行ps:可以看到hello进程。
图6.6.3 运行ps状态截图
运行jobs:可以看到shell程序中停止的作业,hello程序没有结束而是被挂起。
图6.6.4 运行jobs状态截图
运行pstree:
图6.6.4 运行pstree状态截图
运行fg:hello程序恢复前台执行。
图6.6.4 运行fg状态截图
运行kill:程序结束。
图6.6.4 运行kill状态截图
输入Ctrl-C:进程收到SIGINT信号,hello进程结束。
图6.6.2 输入Ctrl-Z运行状态截图
6.7本章小结
本章介绍了进程的概念与作用,分析了hello程序的进程由shell创建,执行,通过异常控制流调度到结束回收等一系列过程。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址也称为程序地址或虚拟地址,是由程序员编写的程序所使用的地址。在程序编写和编译过程中,程序中的变量、指令等都使用逻辑地址来表示。这些地址是相对于程序的起始地址而言的,通常从零开始。逻辑地址是抽象的,与实际的物理内存地址无关,程序员可以使用逻辑地址来访问内存,而不需要了解底层硬件的细节。
线性地址:线性地址是在内存管理单元(MMU)的帮助下,由逻辑地址转换得到的地址。在使用分段机制的系统中,线性地址是通过将逻辑地址映射到段基址并加上段内偏移量得到的。在使用分页机制的系统中,线性地址是通过将逻辑地址映射到页表得到的。线性地址提供了一种统一的地址空间,对于程序来说是连续的,不需要考虑物理内存的实际布局。
虚拟地址:虚拟地址是指在程序运行时,由CPU生成的地址,用于访问内存中的数据和指令。在现代操作系统中,程序所使用的地址都是虚拟地址。这些地址是相对于进程的虚拟地址空间而言的,通常从零开始。虚拟地址提供了一种抽象,使得每个进程都认为自己在独享整个内存空间,而实际上,操作系统通过内存管理单元(MMU)将虚拟地址映射到物理地址。
物理地址:物理地址是指在内存芯片上的实际存储位置,也称为真实地址。每个存储单元(例如RAM)都有自己的物理地址。当CPU发出访问内存的请求时,MMU会将虚拟地址转换为物理地址,从而确定在内存中实际存储数据的位置。
总的来说,逻辑地址是程序员编写程序时使用的抽象地址,线性地址是通过逻辑地址经过映射得到的地址,虚拟地址是程序运行时由CPU生成的地址,而物理地址则是内存中实际存储数据的地址。这些地址之间存在着复杂的映射关系,由操作系统和硬件共同管理。
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel x86 架构中,逻辑地址到线性地址的变换采用了分段管理机制。
1.逻辑地址:逻辑地址是程序员在编写代码时使用的地址。在 x86 架构中,逻辑地址通常由两部分组成:段选择子(Segment Selector)和偏移量(Offset)。段选择子包含了段描述符的索引,用于定位段描述符表中的段描述符;偏移量是相对于段的起始地址的偏移量。
2.段描述符:每个段在内存中都有一个对应的段描述符,用于描述段的属性和边界信息。段描述符包含了段的起始地址、段的长度、访问权限、段的类型等信息。
3.段选择子:段选择子是一个 16 位的字段,包含了两部分信息:索引和请求特权级别。索引用于定位段描述符表中的段描述符,而请求特权级别用于指定访问该段时所需的特权级别。
4.段描述符表:段描述符表是一个存储所有段描述符的数据结构。在 x86 架构中,有两种类型的段描述符表:全局描述符表和局部描述符表。GDT 是系统范围内共享的,而 LDT 是每个进程私有的。
5.分段管理过程:当程序访问内存时,CPU会根据逻辑地址中的段选择子索引到 GDT 或 LDT 中相应的段描述符。然后,CPU会使用段描述符中的段基址和偏移量,计算出线性地址。线性地址是通过将段描述符中的段基址与偏移量相加得到的,即线性地址=段基址+偏移量。这个过程称为分段变换。
6.线性地址:线性地址是逻辑地址通过分段变换计算得到的地址。在分段管理模式下,逻辑地址等于线性地址。
总的来说,分段管理机制通过将逻辑地址中的段选择子映射到相应的段描述符,并使用段描述符中的信息计算线性地址。这种方式使得内存管理更加灵活,可以对不同段设置不同的访问权限和属性。然而,分段管理模式下的内存访问是以段为单位进行的,不能很好地支持大型地址空间的处理,因此在后续的发展中,分页管理机制逐渐取代了分段管理机制。
7.3 Hello的线性地址到物理地址的变换-页式管理
在 Intel x86 架构中,线性地址到物理地址的变换采用了页式管理机制。
1.线性地址:线性地址是程序在运行时使用的地址。它由一个固定位数的地址空间组成,通常是32位或64位。线性地址是逻辑地址经过分段变换后得到的结果,用于访问进程的虚拟内存空间。
2.页表:页表是一种数据结构,用于将线性地址映射到物理地址。在x86架构中,页表是由页目录和页表两级结构组成的。页目录存储了一组页表的起始地址,而页表存储了一组物理页框的映射关系。
3.页目录:页目录是一个包含1024个项(32位系统)或512个项(64位系统)的数组。每个项都是一个指向页表的指针,用于定位相应的页表。
4.页表项:页表项是页表中的一个条目,用于存储线性地址与物理地址之间的映射关系。每个页表项通常包含以下字段:
有效位:指示该页是否在内存中。
物理页框号:指示线性地址对应的物理页框号。
访问权限位:指示对该页的访问权限(读、写、执行等)。
5.页大小:x86架构中常见的页大小为4KB或4MB。较小的页大小可以提供更精细的内存管理,而较大的页大小可以提高地址转换的速度。
6.页式管理过程:当程序访问内存时,CPU会将线性地址中的高位作为页目录的索引,中间位作为页表的索引,低位作为页内偏移量。首先,CPU通过线性地址的高10位(32位系统)或9位(64位系统)找到页目录中的相应项,得到页表的起始地址。然后,CPU通过线性地址的中间10位(32位系统)或9位(64位系统)找到页表中的相应项,得到物理页框号。最后,CPU将页表项中的物理页框号与线性地址的低12位(32位系统)或低21位(64位系统)相结合,得到最终的物理地址。
总的来说,页式管理机制通过将线性地址分解为页目录索引、页表索引和页内偏移量,实现了从线性地址到物理地址的映射过程。这种方式使得内存管理更加高效和灵活,同时提供了更细粒度的内存保护和共享机制。
7.4 TLB与四级页表支持下的VA到PA的变换
图7.4.1 页表地址翻译
TLB是一种高速缓存,用于存储最近访问的页表项的映射关系,以加速虚拟地址到物理地址的转换过程。
1.虚拟地址(VA):虚拟地址是程序在运行时使用的地址,通常由一个固定位数的地址空间组成。在 x86 架构中,虚拟地址通常是32位或64位。
2.物理地址(PA):物理地址是内存中存储数据的实际地址,用于访问内存中的实际数据。
3. 页表:页表是一种数据结构,用于将虚拟地址映射到物理地址。在四级页表中,页表由四级结构组成,包括页全局目录(PGD)、页中间目录(PMD)、页表(Page Table)和页内偏移量。
4.TLB: TLB是一种高速缓存,用于存储最近访问的页表项的映射关系。TLB中的每个条目都包含一个虚拟地址到物理地址的映射关系,以及其他控制位。
5.TLB命中:当CPU访问内存时,它首先会查找TLB来检查虚拟地址到物理地址的映射关系是否已经存在于TLB中。如果存在,这种情况称为TLB命中(TLB Hit),CPU可以直接使用TLB中的映射关系,加速地址转换过程。
6.TLB未命中:如果TLB中没有找到虚拟地址到物理地址的映射关系,这种情况称为TLB未命中(TLB Miss)。在TLB未命中的情况下,CPU将会访问页表来获取虚拟地址到物理地址的映射关系,并将其加载到TLB中。
7.TLB更新:当CPU修改页表中的映射关系时,TLB中的对应条目可能会变得无效。这种情况下,TLB需要更新,以确保其中存储的映射关系是最新的。
总的来说,TLB是一种用于加速虚拟地址到物理地址转换的高速缓存,它存储了最近访问的页表项的映射关系。通过TLB,CPU可以快速获取虚拟地址到物理地址的映射关系,从而加速内存访问过程。在四级页表的支持下,TLB可以存储更多的映射关系,提高地址转换的效率。
7.5 三级Cache支持下的物理内存访问
三级缓存(L3 Cache)是位于处理器核心和主内存之间的高速缓存,用于加速处理器对内存的访问。
1.物理内存:物理内存是计算机系统中用于存储数据和指令的硬件设备。它由DRAM芯片组成,是CPU访问数据和指令的主要来源。
2.Cache:缓存是一种高速存储设备,用于临时存储处理器频繁访问的数据和指令。三级缓存是处理器内部的一种高速缓存,通常位于二级缓存(L2 Cache)和主内存之间。
3. Cache层次结构:三级缓存通常是共享的,即多个处理器核心共享同一个三级缓存。这种设计可以减少缓存的重复存储,提高缓存的利用率。三级缓存与处理器核心之间通过高速总线或互联网络连接,以实现快速的数据传输。
4. Cache的工作原理:当处理器需要访问内存中的数据时,它首先会查找三级缓存来检查数据是否已经存在于缓存中。如果数据存在于缓存中,这种情况称为缓存命中(Cache Hit),处理器可以直接从缓存中读取数据,加速数据访问过程。如果数据不在缓存中,这种情况称为缓存未命中(Cache Miss)。在缓存未命中的情况下,处理器将会访问主内存来获取数据,并将数据加载到缓存中,以便下次访问时可以直接从缓存中读取。
5. Cache的替换策略:当缓存空间不足以容纳新的数据时,需要替换缓存中的部分数据。常见的替换策略包括最近最少使用(LRU)和随机替换。
6. Cache的更新策略:在写操作时,如果数据同时存在于缓存和主内存中,则需要考虑如何更新缓存和主内存中的数据。常见的更新策略包括写回和写直达。
总的来说,三级缓存是处理器内部的一种高速缓存,用于加速处理器对内存的访问。通过缓存的存在,可以减少处理器对主内存的访问次数,提高数据访问的效率。三级缓存与处理器核心之间的高速连接以及合理的缓存管理策略,可以有效地提高系统的整体性能。
7.6 hello进程fork时的内存映射
当一个进程调用fork()函数时,操作系统会创建一个新的子进程,该子进程是父进程的副本。
1.父进程的内存映射:在调用fork()函数之前,父进程已经存在并且已经分配了一定的内存空间用于存储代码、数据和堆栈等信息。这些内存区域包括代码段、数据段、堆区和栈区等,它们都有各自的权限和属性,用于存储不同类型的数据和指令。
2.子进程的内存映射:当父进程调用fork()函数创建子进程时,操作系统会为子进程分配一份与父进程相同的内存空间副本。子进程的内存映射会与父进程的内存映射相同,包括代码段、数据段、堆区和栈区等。这意味着子进程会继承父进程的内存布局和数据内容,但是父子进程之间的内存空间是相互独立的,每个进程都有自己的地址空间。
3.写时复制:为了节省内存和提高性能,操作系统通常采用写时复制技术来延迟内存的复制。在fork()函数调用之后,子进程和父进程会共享相同的物理内存页面。只有当父进程或子进程尝试修改其中的数据时,才会触发内存页面的复制,以确保父子进程之间的数据不会相互影响。
4.内存映射的变化:在fork()函数调用之后,父进程和子进程会共享相同的内存映射关系。但是,随着进程的执行和内存的修改,父子进程的内存映射可能会发生变化。例如,如果父进程或子进程调用了exec()函数加载新的程序,那么它们的内存映射会发生改变,原来的内存映射会被新的程序替换。
总的来说,当父进程调用fork()函数创建子进程时,子进程会继承父进程的内存映射,并共享相同的物理内存页面。通过写时复制技术,操作系统可以延迟内存的复制,提高内存的利用效率。然而,父子进程之间的内存空间是相互独立的,每个进程都有自己的地址空间。
7.7 hello进程execve时的内存映射
当一个进程调用execve()函数时,它会加载并运行一个新程序,这会导致当前进程的内存映射发生变化。
1.原有内存映射的清除:在调用execve()函数之前,进程已经存在并且已经分配了一定的内存空间用于存储代码、数据和堆栈等信息。调用execve()函数会导致当前进程的所有原有内存映射被清除,包括代码段、数据段、堆区和栈区等。
2.加载新程序的内存映射:调用execve()函数加载一个新程序时,操作系统会为新程序分配一份内存空间并将其加载到当前进程的地址空间中。新程序的内存映射包括代码段、数据段、堆区和栈区等,它们会替换掉当前进程原有的内存映射。
3.内存映射的创建:当新程序被加载到内存中时,操作系统会根据程序的可执行文件格式将程序的各个段映射到进程的地址空间中。这些内存映射通常会包含程序的代码、全局变量、堆空间和栈空间等,以及一些特殊的段。
4.共享库的加载:如果新程序依赖于共享库,那么在execve()函数执行期间,操作系统会根据程序的需要加载这些共享库到进程的地址空间中。共享库的加载通常涉及到动态链接器,它负责解析共享库的依赖关系并加载所需的库文件。
5.初始化和重定位:在新程序被加载到内存中后,操作系统会执行一些初始化和重定位操作,以确保程序能够正确运行。这些操作包括对全局变量进行初始化、解析符号引用、执行重定位等,以便程序能够正确地访问各种数据和函数。
6.执行新程序:最终,当所有的初始化和重定位操作完成后,操作系统会将控制权转移给新程序的入口点,并开始执行新程序的代码。
总的来说,当一个进程调用execve()函数加载一个新程序时,会导致当前进程的内存映射被清除并重新创建。新程序的内存映射会替换掉原有的内存映射,包括代码段、数据段、堆区和栈区等。加载新程序可能还涉及到共享库的加载和初始化操作,以确保程序能够正确运行。
7.8 缺页故障与缺页中断处理
缺页故障(Page Fault)是指当程序试图访问虚拟地址空间中的某一页时,而该页当前不在主存(物理内存)中时发生的一种异常情况。缺页中断(Page Fault Interrupt)是处理缺页故障的机制,它会触发操作系统的内存管理模块来处理缺页情况。
1.缺页故障的发生:当程序试图访问虚拟地址空间中的某一页时,操作系统会首先检查该页是否已经在物理内存中。如果该页已经在物理内存中,则访问可以顺利进行,否则就会发生缺页故障。
2.缺页中断的触发:当程序发生缺页故障时,CPU 会产生一个异常,称为缺页中断。缺页中断会将控制权转移到操作系统的内核态,并执行相应的中断处理程序。
3.缺页中断处理过程:操作系统内核收到缺页中断后,会根据缺页故障的原因进行不同的处理,如果是由于页面不在物理内存中而导致的缺页故障,操作系统会执行页面调度算法来选择一个牺牲页;如果是由于访问权限导致的缺页故障,操作系统会检查访问权限是否合法,如果不合法则产生相应的异常;如果是由于虚拟地址无效导致的缺页故障,操作系统会终止当前进程并报告错误。
4.缺页处理流程:当操作系统确定了缺页的原因,并选择了牺牲页后,会从磁盘上将所需页面读入物理内存。如果所需页面在磁盘上不存在,则操作系统会向文件系统请求将页面内容加载到内存中。一旦页面被加载到内存中,操作系统会更新页表将新页面映射到请求进程的虚拟地址空间中。最后,操作系统会重新执行导致缺页故障的指令,使程序能够正常访问所需页面。
5.异常处理的返回:处理完缺页中断后,操作系统会将控制权返回给用户程序,并重新执行导致缺页故障的指令。如果导致缺页故障的指令依然无法正常执行,则可能会再次触发缺页中断,进入相应的处理流程。
总的来说,缺页故障和缺页中断处理是操作系统中重要的内存管理机制,它们确保了程序在访问虚拟地址空间时能够有效地利用物理内存,并保证了内存访问的正确性和安全性。
7.9动态存储分配管理
动态存储分配是指在程序运行时,根据需要动态地分配和释放内存空间。在C语言中,malloc和free是常用的动态内存管理函数。
1.基本方法:
分配内存:使用malloc函数动态分配内存空间。malloc接受一个参数,即要分配的字节数,返回一个指向分配内存区域的指针。
释放内存:使用free函数释放先前分配的内存空间。free接受一个参数,即要释放的内存块的指针。
2.动态内存管理策略:
首次适配:按顺序查找第一个大小足够的空闲块进行分配。这是最简单和最常用的分配策略,但可能会产生外部碎片。
最佳适配:在所有合适的空闲块中选择最小的一个进行分配。虽然能够减少外部碎片,但需要遍历整个空闲块链表,效率较低。
最坏适配:选择最大的合适空闲块进行分配。这种策略可能会导致大量的内部碎片,不太常用。
快速适配:维护多个不同大小的空闲块链表,根据需要选择合适大小的链表进行分配。这种策略可以减少搜索时间和内存碎片,但需要额外的管理开销。
3.动态内存管理的注意事项:
内存泄漏:使用完动态分配的内存后,应及时释放,否则会导致内存泄漏,造成系统资源浪费。
指针悬挂:在释放内存后,应将指向该内存的指针置为 NULL,以防止指针悬挂,避免访问已释放的内存。
内存越界访问:应该确保对动态分配的内存进行正确的访问,避免发生内存越界访问的情况,否则可能导致程序崩溃或安全漏洞。
动态内存管理在程序设计中具有重要作用,能够灵活地管理内存资源,提高程序的效率和灵活性。但同时也需要注意内存泄漏、指针悬挂等问题,保证程序的稳定性和安全性。
7.10本章小结
本章介绍了动态存储分配管理的概念和实现方式。动态存储分配允许程序在运行时根据需要动态地申请和释放内存,提高了内存的灵活利用。常见的动态存储分配方法包括堆、栈和内存池分配。操作系统内核和内存管理单元共同实现了动态存储分配管理,确保了系统内存资源的有效管理和利用。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
在Linux中,一切都是文件,这意味着设备可以像文件一样被访问和管理。每个设备都被映射到文件系统的一个特定位置,通常位于/dev
目录下。例如,硬盘、串口、USB设备等都可以被表示为文件,并通过读取和写入这些文件来进行设备的读写操作。这种模型化使得设备管理变得统一和灵活,开发人员可以使用相同的API来访问不同类型的设备。
设备管理:unix io接口
Linux提供了丰富的系统调用和库函数,用于进行设备的I/O操作。常用的系统调用包括open
、read
、write
和close
,用于打开、读取、写入和关闭设备文件。库函数包括fopen
、fread
、fwrite
和fclose
,提供了更方便的文件操作接口,封装了底层的系统调用。除了文件I/O,Linux还提供了专门的设备文件操作系统调用,如ioctl
用于对设备进行控制和配置,mmap
用于内存映射设备文件等。
8.2 简述Unix IO接口及其函数
Unix I/O接口是Unix/Linux操作系统提供的一组函数和系统调用,用于在用户空间程序中进行输入和输出操作。这些函数和系统调用允许程序与文件、设备和其他 I/O 对象进行交互。以下是 Unix I/O 接口中一些常用的函数和系统调用:
1. open:int open(const char *pathname, int flags, mode_t mode),用于打开一个文件,并返回文件描述符。如果成功,返回文件描述符;否则返回 -1。
2. close:int close(int fd),用于关闭一个已打开的文件。传入文件描述符作为参数,成功关闭返回 0,否则返回 -1。
3. read:ssize_t read(int fd, void *buf, size_t count),用于从已打开的文件中读取数据到缓冲区中。传入文件描述符、缓冲区和要读取的字节数。成功时返回实际读取的字节数,若到达文件末尾返回 0,发生错误返回 -1。
4. write:ssize_t write(int fd, const void *buf, size_t count),用于将数据从缓冲区写入到已打开的文件中。传入文件描述符、缓冲区和要写入的字节数。成功时返回实际写入的字节数,发生错误返回 -1。
5. lseek:off_t lseek(int fd, off_t offset, int whence),用于移动文件指针到指定位置。传入文件描述符、偏移量和起始位置。成功时返回新的文件偏移量,否则返回 -1。
6. ioctl:int ioctl(int fd, unsigned long request, ...),用于对设备进行控制。传入文件描述符、请求代码和可选的参数。具体的控制操作和参数取决于请求代码。
7. fcntl:int fcntl(int fd, int cmd, ...),用于对文件描述符进行各种控制操作。可以进行文件的复制、设置或获取文件属性等操作。
8. dup/dup2:int dup(int oldfd)/int dup2(int oldfd, int newfd),用于复制文件描述符。dup复制参数指定的文件描述符,并返回新的文件描述符;dup2复制旧的文件描述符到新的文件描述符,并关闭新的文件描述符。
这些函数和系统调用提供了 Unix I/O 接口的基本功能,可以满足用户程序对文件和设备的常规操作需求。通过这些接口,用户程序可以进行文件的打开、读写、关闭以及对设备的控制等操作。
8.3 printf的实现分析
图8.3.1 printf函数体
图8.3.2 vsprintf函数体
图8.3.3 write函数参数传递
图8.3.4 sys_call函数参数传递
(char*)(&fmt) + 4)表示的是fmt中的第一个参数。
printf接受一个格式化命令,并把指定的匹配的参数格式化输出。
vsprintf返回的是要打印出来的字符串的长度。
write是写操作,把buf中的i个元素的值写到终端。
vsprintf的作用就是格式化,它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write作用是给寄存器传递参数,然后一个int结束,int表示要调用中断门了。通过中断门,来实现特定的系统服务,一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。
sys_call函数中的call save,是为了保存中断前进程的状态,syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过 ASCII 码在字模库中找到点阵信息将点阵信息存 储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),最终打印出了我们需要的字符串。
8.4 getchar的实现分析
图8.4.1 getchar结构体
当程序调用 getchar 时,它会等待用户输入字符。用户输入的字符被存放在键盘缓冲区中,直到用户按下回车键(回车也被存放在缓冲区中)。此时,getchar 开始通过系统调用 read 读取存储在键盘缓冲区中的 ASCII 码。
getchar 函数的返回值是用户输入的第一个字符的 ASCII 码,如果出错则返回 -1,并将用户输入的字符回显到屏幕上。如果用户在按下回车键之前输入了多个字符,那么这些字符会保留在键盘缓冲区中,等待后续的 getchar 函数调用读取。
对于后续的 getchar 调用,如果缓冲区中还有字符,它们会被直接读取而不需要等待用户输入。直到缓冲区中的字符被读取完毕,后续的 getchar 调用才会再次等待用户输入。
在异步异常情况下,例如键盘中断,系统会调用键盘中断处理子程序。该子程序接收按键扫描码,并将其转换为 ASCII 码,然后将其保存到系统的键盘缓冲区中。
getchar 函数通过系统调用 read 读取键盘缓冲区中的字符,直到接收到回车键才返回。这样设计使得程序能够实现在用户输入字符时逐个读取并处理输入,而不会阻塞程序的执行。
8.5本章小结
本章介绍了进程与内存管理、IO 设备管理、动态存储分配管理等操作系统的核心概念和机制。进程管理涉及进程的创建、调度和终止,内存管理包括虚拟内存、页式管理和缺页中断处理,IO 设备管理涉及设备模型化和 Unix IO 接口,动态存储分配管理涉及动态内存分配的基本方法与策略。这些概念和机制是操作系统实现各种功能的基础,对于理解和设计操作系统至关重要。
结论
hello所经历的过程:
编写hello.c的源程序,完成hello.c文件;
hello.c源文件经过预处理生成hello.i;
hello.i编译后生成汇编文本文件hello.s;
hello.s汇编后生成可重定位目标文件hello.o;
hello.o生成ELF文件hello.elf;
hello.o反汇编后生成反汇编文件hello.asm;
hello.o链接后生成可执行文件hello;
hello生成ELF文件hellold.elf;
hello反汇编后生成反汇编文件hellold.asm。
shell中输入./hello 2022112863 王梦鑫 19855855201 4;
终端shell调用fork函数,创建一个子进程,为程序的加载运行提供虚拟内存空间等上下文;
子进程调用execve函数加载并运行程序hello通过传递的参数,操作系统为这个进程分配虚拟内存空间;
通过TLB和多级页表,实现虚拟内存和物理内存的翻译,进而访问计算机的存储结构,访问内存;
内核通过异常控制流调度hello进程;
hello调用printf函数和getchar等函数进行IO与外界交互;
hello最终被父进程回收,内核收回为其创建的所有信息。
在计算机系统的设计与实现中,我深刻体会到了系统的复杂性和挑战性,同时也认识到了创新的重要性。我坚信,对于现代计算机系统的设计与实现,需要持续不断地探索新的方法和理念。
首先,我认为在系统设计中,注重模块化和抽象是至关重要的。将系统分解成模块化的部分,并在设计和实现中保持适当的抽象水平,可以降低系统的复杂度,提高可维护性和扩展性。其次,我认为性能与效率是系统设计和实现过程中需要重点考虑的方面。除了在硬件层面进行优化外,还需要注重算法、数据结构和软件架构等方面的优化,以提高系统的响应速度和资源利用率。此外,安全和可靠性也是我非常重视的方面。在设计和实现过程中,要注重系统的安全性,包括数据安全、用户隐私、系统稳定性等方面的保护,以确保系统能够抵御各种攻击和故障。最后,我认为创新和持续改进是推动系统发展的关键。不断探索新的技术和方法,修复bug,优化性能,持续改进现有系统,以保持系统的竞争力和可持续发展性。
附件
hello.i:hello.c预处理后生成的预处理文件;
hello.s:hello.i编译后生成的汇编程序文件;
hello.o:hello.s汇编后生成的可重定位目标文件;
hello.elf:hello.o生成的ELF文件;
hello.asm:hello.o反汇编后生成的反汇编文件;
hello:hello.o链接后生成的可执行文件;
hellold.elf:hello生成的ELF文件;
hellold.asm:hello反汇编后生成的反汇编文件。
参考文献
[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] 张菊.浅析C语言printf函数的功能及使用[J].中国科技信息,2012,(10):111+116.
[8] 张和君,张跃.Linux动态链接机制研究及应用[J].计算机工程,2006,(22):64-66.
[9] Pianistx.printf函数的深入剖析https://www.cnblogs.com/pianist/p/3315801.html.
[10] 月光下的麦克.readelf指令使用.2023-0201 http://t.csdnimg.cn/mpcVG.
[11] 深入理解计算机系统,Computer Systems:A Programmer's Perspective (美)布赖恩特(Bryant,R.E.)等.