计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术学院
学 号 2021113306
班 级 21WL025
学 生 孙志豪
指 导 教 师 吴锐
计算机科学与技术学院
2023年5月
摘 要
这篇文章以一个名为"hello"的简单C语言程序为例,通过详细分析该程序从编写代码到运行结束的完整生命周期,在Linux系统下展示了程序的进程转换过程(Program to Process,简称P2P)以及从零到零的过程(zero to zero,简称020)。通过这个案例,我们可以深入了解一般程序在Linux系统中的运行机制。文章还着重介绍了x86-64计算机系统的关键工作原理,从而帮助读者理解底层机器与顶层程序员之间的沟通方式。通过揭示这个主要工作机制,读者可以更好地了解程序在计算机系统中的运行过程,并掌握与底层机器交互的基本原理。综合而言,这篇文章提供了一个全面而深入的视角,旨在展示Linux系统下一般程序的运行过程,并探索了x86-64计算机系统的关键工作机制,为程序员们提供了更深入的理解和应用知识。
关键词:预处理;编译;汇编;链接;进程;内存管理;I/O管理;
目 录
第1章 概述 - 4 -
1.1 HELLO简介 - 4 -
1.2 环境与工具 - 4 -
1.3 中间结果 - 4 -
1.4 本章小结 - 4 -
第2章 预处理 - 5 -
2.1 预处理的概念与作用 - 5 -
2.2在UBUNTU下预处理的命令 - 5 -
2.3 HELLO的预处理结果解析 - 6 -
2.4 本章小结 - 7 -
第3章 编译 - 8 -
3.1 编译的概念与作用 - 8 -
3.2 在UBUNTU下编译的命令 - 8 -
3.3 HELLO的编译结果解析 - 9 -
3.4 本章小结 - 12 -
第4章 汇编 - 14 -
4.1 汇编的概念与作用 - 14 -
4.2 在UBUNTU下汇编的命令 - 14 -
4.3 可重定位目标ELF格式 - 14 -
4.4 HELLO.O的结果解析 - 17 -
4.5 本章小结 - 19 -
第5章 链接 - 20 -
5.1 链接的概念与作用 - 20 -
5.2 在UBUNTU下链接的命令 - 20 -
5.3 可执行目标文件HELLO的格式 - 20 -
5.4 HELLO的虚拟地址空间 - 24 -
5.5 链接的重定位过程分析 - 25 -
5.6 HELLO的执行流程 - 27 -
5.7 HELLO的动态链接分析 - 27 -
5.8 本章小结 - 28 -
第6章 HELLO进程管理 - 29 -
6.1 进程的概念与作用 - 29 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 29 -
6.3 HELLO的FORK进程创建过程 - 29 -
6.4 HELLO的EXECVE过程 - 30 -
6.5 HELLO的进程执行 - 30 -
6.6 HELLO的异常与信号处理 - 31 -
6.7本章小结 - 33 -
第7章 HELLO的存储管理 - 33 -
7.1 HELLO的存储器地址空间 - 33 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 33 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 34 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 35 -
7.5 三级CACHE支持下的物理内存访问 - 35 -
7.6 HELLO进程FORK时的内存映射 - 36 -
7.7 HELLO进程EXECVE时的内存映射 - 36 -
7.8 缺页故障与缺页中断处理 - 36 -
7.9动态存储分配管理 错误!未定义书签。
7.10本章小结 - 37 -
第8章 HELLO的IO管理 错误!未定义书签。
8.1 LINUX的IO设备管理方法 错误!未定义书签。
8.2 简述UNIX IO接口及其函数 错误!未定义书签。
8.3 PRINTF的实现分析 错误!未定义书签。
8.4 GETCHAR的实现分析 错误!未定义书签。
8.5本章小结 错误!未定义书签。
结论 - 38 -
附件 - 38 -
参考文献 - 39 -
第1章 概述
1.1 Hello简介
这个看似简单的"hello"程序,它却展示了程序在运行过程中的复杂性。
首先,程序员编写了"hello"的原始C语言代码。然后,通过一系列步骤,包括预处理、编译、汇编和链接,将代码转化为可执行文件。接着,通过shell创建一个进程,并加载可执行文件,使得"hello"成为一个正在运行的进程(从程序到进程的转变)。
在运行过程中,"hello"会面临各种异常和信号的处理,以及与存储器的交互涉及多种机制。此外,通过中断和IO端口,"hello"还需要与外部设备进行交互。
最终,当"hello"正常退出或收到信号后,操作系统会终止该进程,并释放其占用的资源,然后返回shell。这个过程使得"hello"从存在到不存在,完成了一个完整的生命周期(从零到零的过程)。
总之,尽管"hello"程序看起来很简单,但通过深入分析,我们可以看到其中涉及到的诸多复杂性和底层机制,以及程序与操作系统之间的互动。
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.c。
②预处理后的代码hello.i。
③编译后的汇编语言代码hello.s。
④可重定位目标文件hello.o。
⑤hello.o的objdump结果hello_o_disasm.txt。
⑥可执行文件hello。
⑦hello的objdump结果hello_disasm.txt。
1.4 本章小结
本章主要介绍了hello.c程序P2P,020的过程。列出了本次实验所需的环境和工具以及过程中所生成的中间结果。
第2章 预处理
2.1 预处理的概念与作用
预处理指的是程序在编译之前进行的处理,是计算机在处理一个程序时所进行的第一步处理,可以进行代码文本的替换工作,但是不做语法检查。预处理是为编译做的准备工作,能够对源程序.c文件中出现的以字符“#”开头的命令进行处理,包括宏定义#define、文件包含#include、条件编译#ifdef等,最后将修改之后的文本进行保存,生成.i文件,至此预处理结束。
预处理的主要作用是增强代码的可读性、灵活性和可维护性。通过使用预处理指令,可以在编译时根据不同的条件执行不同的代码块,定义常用的宏,简化代码编写,以及引入外部头文件等。
预处理可以主要概括为以下3个方面的作用:
1.将源文件中以"include"格式包含的文件复制到编译的源文件中;
2.用实际值把#define的宏进行替换;
3.根据条件选择#if内的代码等。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
图2.1预处理命令
或者命令:gcc -E hello.c
图2.2预处理命令
2.3 Hello的预处理结果解析
程序扩展为3091行,hello源程序出现在3078行,#include <stdio.h> #include <stdlib.h> #include <unistd.h>三个头文件消失,替代的是一大段代码,描述的是运行库在计算机中的位置,方便下一步翻译成汇编语言。
图2.3预处理结果
2.4 本章小结
在本章中,我们着重介绍了预处理的概念以及其在编程中的应用功能。我们特别关注了一个名为hello.c的源代码文件,并对其进行了预处理操作,生成了一个名为hello.i的文本文件。通过分析hello.i文件的内容,我们深入了解了预处理的内涵和工作原理。
预处理过程是编译过程中的一个重要步骤,它通过预处理器对源代码进行处理。在我们的示例中,我们使用了C语言的预处理器。预处理器根据预处理指令,如宏定义、条件编译指令和头文件包含等,对源代码进行处理和转换。
通过将hello.c文件进行预处理,我们生成了hello.i文件,其中包含了经过宏替换、条件编译和头文件包含等处理后的代码。我们详细分析了hello.i文件的内容,以深入理解预处理的实际效果和作用。
总结而言,本章主要围绕预处理的概念展开,介绍了其在编程中的应用功能。通过实际操作并分析生成的hello.i文件,我们对预处理过程有了更深入的认识。
第3章 编译
3.1 编译的概念与作用
编译是利用编译程序从源语言编写的源程序产生目标程序的过程,是用编译程序产生目标程序的动作,把高级语言变成计算机可以识别的2进制语言。
作用:
编译的作用是将高级语言的源代码转换为可执行的机器码,使得计算机能够执行程序,并具有提高执行效率、实现平台独立性、检查错误和提高可靠性以及隐藏源代码等重要作用。编译可以把用高级程序设计语言书写的源程序,翻译成等价的计算机汇编或机器语言书写的目标程序,编译程序以高级程序设计语言书写的源程序作为输入,而以汇编或机器语言表示的目标程序作为输出。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello
图3.1编译命令
或者命令:gcc -S hello.i -o hello.s -fno-PIC -no-pie -m64
图3.2编译命令
3.3 Hello的编译结果解析
3.3.1数据
1) 字符串:如下两个函数中的字符串被存储在 .rodata节中
存储如下:
2)变量
1.局部变量
局部变量存储在寄存器或栈中。程序中有局部变量 int i
在汇编代码中如下:
i被存储在栈中,-4(%rbp)的位置。
3.3.2操作
1.算术操作
在循环操作中,使用 i++ 自增操作,每次循环结束后对 i 加1,对栈上存储i 的位置加1;
- 关系操作
-
程序第16行中判断argc是否等于4,源代码为:
汇编代码为:
-
程序第21行中判断 i 是否小于8,源代码为:
汇编代码为,汇编优化为 i <= 7:
-
控制转移
在使用比较关系操作进行判断后,程序将按判断结果经如下代码跳转至L2或L4,进入if语句或继续进入循环
-
数组/指针/结构操作
主函数main的参数中有指针数组char *argv[],argv[0]指向输入程序的路径和名称,argv[1]和argv[2]分别表示两个字符串。
图 3.3 main函数参数存储
图 3.4 argv数组实现汇编代码 -
函数操作
- main函数
参数传递:传入参数argc,argv[],分别用寄存器%edi和%rsi存储
函数调用:被系统启动函数调用
函数返回:设置%eax为0,并返回
- main函数
图 3.5 main函数返回
2) printf函数
i. call puts@PLT
参数传递:传入字符串参数首地址
函数调用:if判断满足条件后被调用
图 3.6 call puts汇编代码
ii. call printf@PLT
参数传递:传入 argv[1]和argc[2]的地址
函数调用:for循环中被调用
图 3.7 call printf汇编代码
3) exit函数
参数传递:传入参数1
函数调用:if判断满足条件后被调用
图 3.8 exit函数汇编代码
4) sleep函数和atoi函数
参数传递:sleep函数传入参数变量atoi(argv[3])
函数调用:for循环中被调用
图 3.9 sleep函数和atoi函数汇编代码
5) getchar函数
函数调用:在for循环结束后被调用
图 3.10 getchar函数汇编代码
3.4 本章小结
在本章中,我们详细介绍了编译的概念和过程,并以hello程序为例进行了编译转换为汇编代码的实践。我们分析了编译生成的汇编代码文件,重点介绍了汇编代码如何处理变量、常量、参数传递以及实现分支和循环等功能。
编译程序在确认所有指令符合语法规则后,将源代码翻译成等价的中间代码或汇编代码表示形式。值得注意的是,编译后生成的汇编代码文件(hello.s)相较于预处理后的文件(hello.i)大幅缩减。这是因为汇编代码中主要包含了真正有用的部分,即主函数(main),而其他部分主要是与main函数相关的代码。汇编代码已经开始将程序转换为机器级表示,它们能够在指令级别上控制CPU的操作,并且还能被人类理解,可以说是程序员和机器之间的桥梁。
总结而言,本章着重探讨了编译的概念和过程,并通过实例演示了编译将源代码转换为汇编代码的过程。我们深入分析了汇编代码的实现方式,特别关注了变量、常量、参数传递以及分支和循环等方面。这些汇编代码是程序员和机器之间沟通的桥梁,能够实现底层的指令级操作,进一步将程序转化为机器可执行的形式。
第4章 汇编
4.1 汇编的概念与作用
汇编是通过汇编器,把汇编语言翻译成机器语言的过程。
作用:通过汇编过程把汇编代码转换成计算机能直接识别的机器代码,把指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
图 4.1 汇编命令
4.3 可重定位目标elf格式
1.命令:readelf -a hello.o > ./elf.txt
图 4.2 生成elf文件命令
2. ELF文件头
包含了系统信息,编码方式,ELF头大小,节的大小和数量等等一系列信息。
图 4.3 ELF文件头内容
3. 节头部表
描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息。
图 4.4 节头部表内容
4. 重定位节
在链接过程中,汇编代码文件中的各个段(如代码段、只读数据段等)可能会引用外部符号,需要通过重定位节来修改这些位置的地址。链接器会根据重定位条目的类型来确定如何计算正确的地址值,利用偏移量等信息来计算出准确的地址。
在hello.o文件中,共有13个节,其中包括代码段(.text)、只读数据段(.rodata)等。还有8个重定位条目,以及7个全局符号(都是函数声明,属于强符号)。这些重定位条目中,有两个对应于.rodata节中的数据地址,很明显它们是printf函数使用的两个字符串的地址。另外的6个重定位条目是被call指令调用过的函数的地址,实际上它们还没有被填充,需要与相应的库进行链接。
换句话说,链接过程中,重定位节被用于修正汇编代码中引用的外部符号的地址。链接器通过解析重定位条目并计算正确的地址值来完成这一任务。在hello.o文件中,存在多个重定位条目,包括对.rodata节中字符串地址的引用以及对被调用函数地址的引用。这些条目需要在链接时与相应的库进行连接,以获得正确的地址值。
图 4.5 重定位节内容
5. 符号表
存放在程序中定义和引用的函数和全局变量的信息。
图 4.6 符号表内容
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > hello_disasm.txt
图 4.7 反汇编文件内容
对hello.o进行反汇编并与第3章中的hello.s进行对比分析,我们可以得出以下观察结果:
- 机器码的存在:反汇编结果中,每条汇编指令的左边多出了机器码。这是因为一条汇编指令在机器码中占用了若干字节,其中开头部分的字节描述了指令类型,后面的字节表示操作数。
- 操作数的表示方式:在hello.s中,操作数以十进制表示;而在反汇编结果中,操作数以十六进制表示。
- 跳转指令的处理:在hello.s文件中,直接使用函数名作为跳转指令的操作数;而在反汇编文件中,跳转指令的操作数是相对偏移地址。这是因为函数的执行地址在链接过程中才能确定,因此反汇编文件添加了对应的重定位条目。这展示了机器语言的构成以及与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言存在差异,尤其是在分支、转移和函数调用等方面。
- 可重定位目标文件的地址:在反汇编文件中,可重定位目标文件中的地址暂时还不是真正的地址,因为尚未经过链接。在目标文件中,汇编器将这些需要地址的指令的操作数设置为0,并对应一个重定位条目。在链接之后,通过重定位条目重新设置这些指令的操作数,才能得到真正的地址。
综上所述,反汇编结果提供了关于hello.o文件的详细信息,展示了汇编指令与机器码之间的映射关系以及操作数的不同表示方式。同时,反汇编结果还展示了可重定位目标文件中地址的临时性质和链接过程中的重定位操作。这些分析有助于我们理解机器语言和汇编语言之间的关系,以及链接过程对程序的最终执行地址的影响。
4.4 本章小结
在了解了汇编的概念和作用后,我们可以将源代码编译成汇编语言,并生成可重定位目标文件(.o文件)。通过分析可重定位目标文件的ELF格式,我们深入理解了其中存储的信息和结构。
然后,我们使用工具如objdump对可重定位目标文件进行反汇编,并将其与源代码对应的汇编文件进行比较。通过比较,我们能够更加深入地理解机器语言与汇编语言之间的关系。
需要明确的是,汇编这一步骤使得程序从文本状态转化为二进制状态,但我们不能简单地认为它只是简单地翻译为真正的机器码。实际上,生成的是"可重定位的机器码"。可重定位目标文件中的机器码还需要进行重定位,以供链接过程使用。
通过汇编阶段,我们不仅将源代码转化为了机器可执行的形式,而且还为后续的链接过程提供了必要的信息。重定位机制在这一过程中发挥了重要作用,它确保了程序在链接时能够正确地连接到其他模块,并得到最终的执行地址。
综上所述,汇编阶段是将源代码转化为可重定位目标文件的关键步骤。通过分析可重定位目标文件和反汇编结果,我们可以更深入地理解机器语言与汇编语言之间的联系,并了解重定位机制在链接过程中的重要性。
第5章 链接
5.1 链接的概念与作用
链接是将各种不同文件的代码和数据部分收集并组合成一个单一文件的过程这个文件可被加载(复制)到内存并执行。
作用:将源程序与为了节省空间而未编入的常用函数文件进行合并,生成可以正常工作的可执行文件。令分离编译成为可能,节省了大量的工作空间。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
命令: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-gn
/crtn.o -z relro -o hello
图 5.1 链接命令
5.3 可执行目标文件hello的格式
1.命令:readelf -a hello > elf_hello.txt
2.ELF文件头
图 5.2 ELF文件头
3.节头部表
图 5.3 节头部表
4.符号表
图 5.4 符号表
5.4 hello的虚拟地址空间
图 5.5 虚拟地址空间信息
图中标红的4个段来自hello可执行文件本身,是hello主要的代码与数据,对应5.3中readelf查看的hello的program header的4个LOAD段。标黄的4个段来自hello加载的动态链接库ld-so.2,剩下的若干个段是栈段和内核相关段。
5.5 链接的重定位过程分析
命令:objdump -d -r hello.out > hello_disasm1.txt
图 5.6 hello反汇编代码
在分析hello和hello.o的反汇编代码时,我们可以观察到以下不同之处:
-
地址表示方式不同:在hello.o的反汇编中,地址是相对偏移地址;而在hello的反汇编中,地址是可以由CPU直接访问的虚拟内存地址。
-
额外的节和库函数:hello的反汇编文件中出现了额外的节,如_init,.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt,_start,_dl_relocate_static_pie,__libc_csu_init,__libc_csu_fini,_fini等。这些节和库函数是在链接过程中添加的,用于程序的执行和与外部库的交互。
-
重定位条目的处理:在hello.o的反汇编中,我们观察到使用了lea指令后,操作数被设置为0,并添加了重定位条目。这是因为可重定位目标文件中的地址是临时性的,链接器在链接过程中将这些重定位条目指向的位置的操作数设置为正确的地址。
链接的过程是将所有模块的节组织起来,为可执行文件的虚拟地址空间确定结构。在这个过程中,链接器根据虚拟地址空间的布局将hello.o中的.rel.text和.rel.data节的重定位条目指向正确的地址。
通过这些观察和分析,我们可以更好地理解链接过程中的重定位和地址解析,以及链接器在将各个模块组合成可执行文件时所扮演的角色。
5.6 hello的执行流程
①开始执行:_start、_libc_start_main
②执行main:_main、_printf、_init、_sleep、_getchar
③退出:exit
程序名称 程序地址
init 0x401000
libc start main 0x401120
main 0x4011d6
printf 0x401090
start 0x4010f0
sleep 0x4010e0
getchar 0x4010b0
exit 0x4010d0
5.7 Hello的动态链接分析
动态链接的基本思想是将程序模块拆分成独立的部分,并在程序运行时将它们动态地链接在一起。与静态链接不同,动态链接不会将所有模块打包成一个独立的可执行文件,而是在需要时加载和链接所需的共享库。因为共享库在运行时可以加载到不同的地址,编译器无法预测函数的准确地址。因此,动态链接会生成重定位记录,供动态链接器在程序加载时进行解析和调整,以确保正确的链接和调用。动态链接的优势在于提供了更高的灵活性和资源共享,使得多个程序可以共享同一份共享库代码,并支持共享库的动态更新。
在使用动态链接时,程序中的函数调用通常需要与共享库中的函数进行绑定,以确保正确的调用。传统的动态链接方式是在程序启动时就将所有函数进行绑定,即提前解析所有函数的地址并建立关联。但是,对于大型程序或包含大量共享库函数的程序,这种提前绑定的方式可能会导致较长的启动时间和较高的内存开销。
为了解决这个问题,引入了延迟绑定的概念。延迟绑定允许在程序运行时,当函数第一次被调用时才进行绑定操作。这意味着在程序启动时,并不立即解析所有函数的地址,而是等到真正需要调用函数时才进行解析和绑定。通过延迟绑定,可以避免不必要的绑定操作,提高程序的启动速度和内存效率。
延迟绑定的实现方式通常涉及使用特殊的指令或数据结构来延迟解析和绑定函数地址,可以通过GOT和PLT实现,根据hello ELF文件可知,GOT起始表位置为0x404000如图在elf文件中:
进入edb查看:
图 5.7 edb执行init之前的地址
图 5.8 edb执行init之后的地址
一开始地址的字节都为0,调用_init函数之后GOT内容产生变化,指向正确的内存地址,下一次调用跳转时可以跳转到正确位置。
5.8 本章小结
通过上述操作,我们深入了解了hello.o、静态库和动态链接库之间的链接机制,以及它们在组合成一个完整的程序时所起到的重要作用。我们也初步探索了一个C语言程序从加载到退出的整个过程。在这个过程中,我们发现静态库和动态链接库在程序内部发挥着关键的作用,尽管这些细节并不直接呈现给我们。事实上,hello程序的背后比表面上的简单更加复杂。所有这些得益于链接机制,它使我们能够方便地利用各种库来编写程序,并确保它们能够在操作系统提供的平台上正常运行。链接机制是程序开发中不可或缺的一环,它为我们提供了强大的功能和灵活性,使得我们能够构建复杂而高效的软件系统。
第6章 hello进程管理
6.1 进程的概念与作用
概念:经典定义就是一个执行中程序的实例。广义定义是进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
作用: 通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
1.作用:
shell是一个命令解释器,它解释用户输入的命令并把它们送到内核,用于用户和系统的交互。
处理流程:
1)提示符显示并等待用户输入命令。
2)读取并解析用户输入的命令。
3)执行命令,可能是内置命令、外部程序或控制结构。
4)可以进行输入输出重定向和管道操作。
5)支持命令替换、环境变量和通配符扩展。
6)设置命令执行的退出状态码。
7)循环执行命令(如有需要)。
8)当所有命令执行完毕或脚本执行结束时,处理流程结束。
6.3 Hello的fork进程创建过程
进程创建过程:
1. 程序开始执行,并调用 fork() 函数。
2. fork() 函数创建了当前进程的一个副本,包括代码、数据和堆栈等信息。
3. 新创建的子进程具有独立的进程 ID(PID)。
4. 在父进程中,fork() 函数返回子进程的 PID,可以用于识别子进程。
5. 在子进程中,fork() 函数返回值为 0,用于识别自己是子进程。
6. 子进程和父进程同时执行后续指令,但它们是独立运行的,相互之间没有影响。
7. 在 “hello” 程序中,父进程和子进程分别输出各自的消息。
8. 父进程可以利用子进程的 PID 进行进程管理,比如等待子进程结束等操作。
9. 子进程执行完毕后,可以终止自己的执行,或者执行其他操作。
10. 程序执行完毕,父进程和子进程都会结束。
6.4 Hello的execve过程
在子进程创建后,它会调用 execve()
函数来加载一个新的可执行文件到当前进程。execve()
函数会在子进程的上下文中加载并运行目标文件(比如 hello
),同时传递参数列表 argv
和环境变量列表 envp
。子进程原有的用户区域会被删除,然后使用虚拟内存机制将hello
可执行文件中的不同段映射到对应的代码段、数据段等地址空间中,从而创建了新的用户区域。
在加载过程中,execve()
函数会处理hello
所需的共享库,也会通过虚拟内存映射的方式加载它们。最终,子进程的执行流会直接跳转到hello
的入口点,开始执行hello
程序。只有在发生错误时(比如找不到hello
文件),execve()
才会返回到调用程序。因此,与fork()
调用返回两次不同,execve()
调用一次后不再返回。
6.5 Hello的进程执行
- 进程创建:操作系统为 “Hello” 程序创建一个新的进程,用于执行程序代码。
- 上下文切换:在执行 “Hello” 程序之前,操作系统会保存当前进程的上下文信息,包括寄存器状态和程序计数器等。然后,加载 “Hello” 程序的上下文信息,切换到该程序的执行环境。
- 用户态和核心态转换:当 “Hello” 程序需要进行特权操作,例如输出消息时,会触发用户态到核心态的转换,以获取执行特权操作所需的权限。
- 时间片分配:操作系统为 “Hello” 程序分配一定的时间片,在该时间段内允许程序执行。一旦时间片用尽,操作系统会中断进程,并根据调度算法选择下一个要执行的进程。
- “Hello” 程序执行:在分配的时间片内,“Hello” 程序按照其代码逐行执行,执行相应的操作,例如输出消息。
- 进程调度:当 “Hello” 程序的时间片用尽或执行结束时,操作系统进行进程调度,选择下一个要执行的进程。这可能根据调度算法的决策,考虑优先级、轮转调度等因素。
- 上下文恢复:在进行进程切换时,操作系统会从之前保存的上下文信息中恢复先前暂停的进程的执行状态,包括寄存器状态和程序计数器等。
- 进程终止:当所有进程执行完毕或操作系统终止 “Hello” 程序的执行时,进程终止,释放相应的资源。
6.6 hello的异常与信号处理
在 “Hello” 程序的执行过程中,可能会出现以下几类异常情况,并产生相应的信号:1. 错误信号:如果程序发生错误或执行异常,例如访问未分配的内存或除以零,操作系统会向进程发送错误信号(如 SIGSEGV、SIGFPE)。这些信号用于通知进程发生了不可恢复的错误。 - 终止信号:如果用户在执行过程中按下 Ctrl-C 组合键,操作系统会向进程发送终止信号(SIGINT)。这个信号通常用于请求进程终止执行。
- 暂停信号:如果用户在执行过程中按下 Ctrl-Z 组合键,操作系统会向进程发送暂停信号(SIGTSTP)。这个信号用于将进程置于后台运行,并暂停其执行。
运行结果:
① 正常运行
图6.1正常运行结果
② 按下ctri-z
图6.2按下ctri-z的运行结果
ctrl-z默认结果是挂起前台的作业,hello进程并没有回收,而是运行在后台下,用ps命令可以看到,hello进程并没有被回收。调用 fg 将其调到前台,如果已打印完,则进程被回收,如下图:
图6.3观察和回收进程
③ 按下Ctrl+c
图6.4按下Ctrl+c的运行结果
在键盘上输入Ctrl+c会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况是终止前台作业,用ps查看前台进程组发现没有hello进程,如图所示:
图6.5观察进程结果
④ 不停乱按
无关输入被缓存到stdin,并随着printf指令被输出到结果。如图:
图6.6不停乱按运行结果
6.7本章小结
通过上述分析和实验,我对"hello"作为一个进程在运行过程中的各种机制有了清晰的认识。尽管"hello"看起来很简单,但其背后的实际机制却相当复杂。我们认识到,"hello"在运行时并不是独占CPU和内存的,而是通过操作系统的进程调度机制与其他进程并发地执行。
同时,我们也深入了解了"hello"在运行过程中可能遇到的异常情况以及信号处理的相关过程。异常可能包括内存访问错误、除零错误等,而信号则是操作系统或其他进程向"hello"发送的一种通信方式。"hello"需要正确处理这些异常和信号,以保证程序的稳定性和可靠性。
总结而言,通过对"hello"程序的分析,我们深入了解了进程运行的复杂性。我们认识到一个程序的执行并不仅仅局限于代码的执行,还涉及到操作系统的资源管理、进程调度、异常处理和信号处理等多个方面。这种深入的了解有助于我们编写高效、稳定的程序,并能更好地理解计算机系统的工作原理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
在hello程序的上下文中,我们可以知道逻辑地址、线性地址、虚拟地址和物理地址的概念。 - 逻辑地址(Logical Address):逻辑地址是指程序中使用的地址,它是相对于程序自身的虚拟地址空间而言的。在hello程序中,逻辑地址用于访问程序中的变量、函数和指令等。
- 线性地址(Linear Address):线性地址是指逻辑地址通过地址转换机制(如分段或分页)转换后得到的地址。在hello程序的执行过程中,逻辑地址通过分段机制或分页机制转换为线性地址,以便访问系统的虚拟地址空间。
- 虚拟地址(Virtual Address):虚拟地址是指线性地址再经过操作系统的虚拟内存管理机制映射到的地址。虚拟地址空间是每个进程独立拥有的地址空间,用于存放进程的代码、数据和堆栈等。在hello程序中,虚拟地址用于访问程序所需的内存空间。
- 物理地址(Physical Address):物理地址是指虚拟地址经过操作系统的地址映射后对应的真实物理内存地址。物理地址是实际存在于计算机系统中的内存地址,用于访问实际的存储单元。在hello程序的执行过程中,操作系统将虚拟地址映射到物理地址,以便程序可以在实际的内存中执行和存取数据。
总之而言,在hello程序中,逻辑地址是程序中使用的相对地址,线性地址是逻辑地址通过地址转换得到的地址,虚拟地址是线性地址经过虚拟内存映射得到的地址,而物理地址是虚拟地址经过操作系统的地址映射后对应的真实物理内存地址。这些地址之间的转换和映射机制使得程序可以在虚拟地址空间中独立运行,同时利用操作系统的虚拟内存管理将其映射到物理内存,实现了程序的执行和存储。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是一种内存管理机制,用于将逻辑地址转换为线性地址。它通过以下步骤实现地址转换: - 每个段在内存中都有一个对应的段描述符,描述了段的起始地址、长度和访问权限等信息。
- 段选择子是存储在段寄存器中的一个值,用于指示要访问的段的位置。它包含了段描述符的索引,通过它可以找到对应的段描述符。
- 逻辑地址由段选择子和段内偏移量组成。段选择子确定要访问的段,段内偏移量表示相对于段起始地址的偏移量。
- 段描述符中的段基址指示了段在内存中的起始地址,段限长指示了段的长度。通过段基址和段内偏移量,可以计算出线性地址。
- 段选择子通过在全局描述符表或局部描述符表中查找对应的段描述符,建立逻辑地址到线性地址的映射关系。
- 线性地址是经过段式管理转换后的地址,表示在分段机制下的内存位置。
段式管理提供了对内存的访问控制和隔离,通过设置合适的段描述符和映射关系,实现了不同程序或进程之间的内存空间隔离,并提供了安全的内存访问权限。
7.3 Hello的线性地址到物理地址的变换-页式管理
当Hello程序运行时,它使用页式管理来将线性地址转换为物理地址。页式管理是一种虚拟内存管理技术,通过将整个虚拟地址空间划分为固定大小的页面(通常为4KB),并使用页表进行地址映射。
具体而言,页式管理涉及以下步骤:
- 操作系统为每个进程创建一个页表:每个进程都有自己的页表,用于将线性地址映射到物理地址。页表是一个数据结构,通常由页目录和页表两级结构组成。其中,页目录存储页表的基址,页表存储页面的物理地址。
- 线性地址的组成:线性地址由两部分组成,即页目录索引和页表索引。线性地址的高位部分作为页目录索引,用于查找页目录中的相应页目录项。线性地址的低位部分作为页表索引,用于查找页表中的相应页表项。
- 映射过程:根据线性地址中的页目录索引找到对应的页目录项,该项存储着指向页表的指针。然后,根据线性地址中的页表索引找到对应的页表项,该项存储着页面的物理地址。
- 物理地址的计算:通过将页表项中存储的页面物理地址与线性地址中的页内偏移量相加,可以得到最终的物理地址。这样,程序就能够访问所需的物理内存空间。
通过页式管理,操作系统可以提供给每个进程一个连续的、私有的虚拟地址空间,使得进程可以独立地运行,互不干扰。同时,页式管理还实现了虚拟内存的概念,使得进程可以访问超出物理内存容量的数据,并且能够有效地管理内存资源。
7.4 TLB与四级页表支持下的VA到PA的变换
在页式管理中,虚拟地址(VA)首先被分成多个段,其中最常见的是将虚拟页号(VPN)分成三段。这些段的值用于在转换后的页表中进行查找,以获取对应的物理页号(PPN)。
转换过程从高层开始,根据翻译后备缓冲器(TLB)中的条目以及TLB索引(TLBI),尝试在TLB中找到与虚拟页号对应的物理页号。如果找到匹配的条目,那么对应的物理页号就是我们要找的。
如果在TLB中没有找到匹配的条目,就会发生缺页中断(Page Fault),需要进一步访问页表来获取物理页号。
此时,虚拟页号会被分成更多段(通常为4段),并通过控制寄存器CR3中存储的一级页表(L1PT)的物理地址,逐级进行寻址。每一级页表都包含多个页表条目,每个条目对应一个小的地址区域。
在进行逐级寻址的过程中,根据虚拟页号的各个段的值,我们可以依次访问下一级的页表,逐步细化寻址的范围。通过四层寻址,最终可以找到与虚拟页号对应的物理页号。然后,将物理页号与虚拟页内偏移量(VPO)拼接起来,得到最终的物理地址(PA)。
这个过程中,页表的层级结构使得地址转换过程变得更加灵活和高效,可以根据需求对内存进行更细粒度的管理。通过将虚拟地址转换为物理地址,页式管理实现了虚拟内存的概念,并提供了更大的地址空间和更好的内存管理能力。
7.5 三级Cache支持下的物理内存访问
在缓存(Cache)的层次中,物理地址(PA)根据缓存的大小、组数和块大小等要求进行拆分。一般来说,物理地址会被拆分为三部分:标记(Tag)、索引(Index)和偏移量(Offset)。
首先,将物理地址的一部分作为标记(Tag),用于标识缓存中存储的数据块所对应的主存块。标记的位数取决于缓存的大小和地址的位数。
其次,将物理地址的一部分作为索引(Index),用于在缓存的组中进行查找。索引的位数决定了缓存的组数和每组的块数量。
最后,剩下的部分是偏移量(Offset),表示在缓存块内的偏移位置。根据偏移量,可以直接获取缓存块中所需数据的位置。
在进行缓存访问时,根据索引的值,首先在缓存的相应组中查找标记。如果找到了匹配的标记,并且对应的有效位(Valid)为1,即缓存命中(Cache Hit),则可以根据偏移量直接从缓存中取出所需数据。
如果在当前层级的缓存中未命中(Cache Miss),即没有找到匹配的标记,就需要继续在更高层级的缓存中重复以上步骤。例如,可以进一步访问二级缓存、三级缓存,直到找到匹配的标记或到达主存。
7.6 hello进程fork时的内存映射
当使用fork
系统调用创建子进程时,操作系统会为子进程创建一些关键的数据结构。其中包括以下两个重要的数据结构: - mm_struct(内存描述符):
mm_struct
是一个内核数据结构,用于描述一个进程的整个虚拟内存空间。它包含了关于进程地址空间的重要信息,如页表、内存映射、内存区域等。每个进程都有自己独立的mm_struct
,用于管理其虚拟内存。 - vm_area_struct(区域结构描述符):
vm_area_struct
是一个内核数据结构,用于描述进程虚拟内存空间中的一个区间。每个区间对应一段连续的虚拟内存地址范围,它包含了该区域的起始地址、大小、访问权限等信息。通过多个vm_area_struct
可以将整个虚拟内存空间划分为不同的内存区域,如代码段、数据段、堆、栈等。
在fork
创建子进程时,操作系统会为子进程复制当前进程的mm_struct
和相关的vm_area_struct
。这样,子进程的虚拟内存空间最初与父进程相同,它们共享同一份物理内存页。然而,为了实现写时复制(Copy-on-Write)机制,父子进程的内存页会被标记为只读。当任意一个进程尝试对内存进行写操作时,操作系统会为该进程创建一个新的物理页,将修改的数据写入其中,使得父子进程的内存页不再共享。
这种写时复制机制确保了父子进程在逻辑上拥有独立的地址空间,但在实际上共享相同的物理内存页,直到有进程对内存进行写操作。这样可以避免无谓的内存复制和浪费,提高了效率和性能。
7.7 hello进程execve时的内存映射
当在Hello进程中调用execve()
函数时,会发生内存映射的变化,具体过程如下: - 清除旧的内存映射:在执行
execve()
函数之前,操作系统会清除当前进程的旧内存映射关系。这意味着之前的代码段、数据段、堆段和栈段等内存区域将被清除。 - 加载新的可执行程序:
execve()
函数将加载一个新的可执行程序到进程的地址空间。它会读取新程序的内容,并将其加载到适当的内存区域。 - 建立新的内存映射:一旦新的可执行程序被加载,操作系统会建立新的内存映射关系。通常,代码段会被映射到只读的区域,数据段会被映射到可读写的区域,而堆和栈段则会被映射到可扩展的区域。
- 更新进程上下文:内存映射完成后,操作系统会更新进程的上下文信息,包括指令指针、栈指针和其他寄存器的值。这样,进程可以从新的代码段开始执行新的可执行程序。
通过这个过程,execve()
函数可以替换当前进程的内存映射,加载新的可执行程序,并为其建立正确的内存映射关系。这样,进程可以执行新的程序代码,并使用新的数据和堆栈空间。
7.8 缺页故障与缺页中断处理
当CPU执行某条指令的内存访问时,如果页表中的PTE表明这个地址对应的页不在物理内存中,那么就会引发缺页故障,这使得跳转入内核态,执行操作系统提供的缺页中断处理程序,缺页中断处理程序会将缺失的页从磁盘加载到物理内存,并更新页表。加载页时可能需要使用替换策略选择合适的物理页,并更新页表。缺页故障处理完毕后,CPU重新执行该条指令,如下图所示:
(7.9动态存储分配管理不做)
7.10本章小结
在这个章节中,我们探讨了与内存管理相关的几个重要概念和过程:
首先,我们了解了 “Hello” 程序的存储器地址空间,即它在内存中的布局和组织方式。通过段式管理,我们可以将逻辑地址转换为线性地址,然后通过页式管理将线性地址转换为物理地址。
段式管理是 Intel x86 架构中的一种内存管理机制,通过使用段描述符和段选择子来实现逻辑地址到线性地址的转换。每个段描述符包含了段的起始地址、长度和访问权限等信息。
页式管理则将线性地址进一步转换为物理地址。通过使用页表和多级页表结构,我们可以将线性地址分解为多个段,并逐级查找页表来获取对应的物理地址。
在 “Hello” 程序中,我们还讨论了进程的创建和内存映射过程。使用 fork 函数创建子进程时,操作系统会复制当前进程的内存映射关系,并将其标记为只读。在 execve 函数调用时,新的可执行程序会被加载到进程的地址空间,并建立相应的内存映射关系。
此外,我们还探讨了缺页问题,当 CPU 访问一个不在物理内存中的页时,会引发缺页故障,操作系统会处理该中断并将相应的页加载到物理内存中。
总体而言,这些概念和过程帮助我们理解了内存管理的基本原理,以及操作系统如何管理和分配内存资源,为进程提供合适的内存空间,以及处理与内存相关的问题和异常。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello经历过程:
1.预处理:将hello.c所有的预处理指令执行,并将外部库拓展到hello.i文件中;
2.编译:将hello.i编译得到汇编代码文件hello.s;
3.汇编:将hello.s汇编成为二进制可重定位目标文件hello.o;
4.链接:将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello;
5.运行:shell中运行hello;
6.创建子进程:shell进程调用fork函数创建子进程;
7.运行程序:通过调用execve函数,加载程序到内存并执行。程序的入口点是main函数;
8.执行指令:CPU按时间片分配给程序,顺序执行程序中的指令;
9.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址;
10.动态申请内存:程序可能会使用malloc等函数向动态内存分配器请求在堆中分配内存;
11.信号:如果运行途中键入Ctrl-C、Ctrl-Z则调用shell的信号处理函数分别停止、挂起;
12.结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
感悟:
"计算机系统"是一个庞大而复杂的体系,它由许多部分和机制组成,为我们能够编写简单的"Hello, World!"程序提供了支持。这些机制的设计者和构造者们在多年的努力下,建立了一种可靠而高效的桥梁,使得应用程序员和底层机器能够无缝地交互。这个桥梁的设计是如此巧妙,以至于我们通常不会注意到其中的复杂细节。我们应该对这些背后默默付出的人们表示感谢,因为他们的工作使得计算机系统能够如此稳定和便捷地运行。
附件
hello.c 原始代码文件
hello.i 预处理后的代码文件
hello.s 编译后的汇编语言代码文件
hello.o 汇编后的可重定位目标执行文件
hello_o_disasm.txt hello.o的反汇编代码文本
elf.txt hello.o的ELF格式文本
elf_hello.txt hello的ELF格式文本
hello 链接后的可执行文件
hello_disasm1.txt hello的反汇编代码文件
参考文献
[1] 深入理解计算机系统原书第3版-文字版.pdf
[2] https://blog.csdn.net/qq_52979026/article/details/118109606
[3] https://blog.csdn.net/weixin_51744028/article/details/124716781