CSAPP 程序人生-Hello’s P2P

摘  要

本篇论文阐述了C语言程序如何从源代码转换成可执行文件的过程。以hello.c程序为例深入剖析了计算机在创建hello可执行文件过程中的各个阶段,包括预处理、编译、汇编、链接以及进程管理,不仅从理论层面探讨了这些工具的工作机制和方法,还通过实际操作展示了它们的效果,解释了计算机系统的工作原理和架构,帮助读者更全面地理解和掌握C语言程序在计算机上的编译与执行流程。

关键词:生命周期;计算机系统;体系结构;预处理;编译;汇编;链接;进程管理;存储管理;IO管理                          

目  录

第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的预处理结果解析................................................................................. - 5 -

2.4 本章小结............................................................................................................ - 5 -

第3章 编译................................................................................................................ - 6 -

3.1 编译的概念与作用............................................................................................ - 6 -

3.2 在Ubuntu下编译的命令................................................................................ - 6 -

3.3 Hello的编译结果解析..................................................................................... - 6 -

3.4 本章小结............................................................................................................ - 6 -

第4章 汇编................................................................................................................ - 7 -

4.1 汇编的概念与作用............................................................................................ - 7 -

4.2 在Ubuntu下汇编的命令................................................................................ - 7 -

4.3 可重定位目标elf格式.................................................................................... - 7 -

4.4 Hello.o的结果解析......................................................................................... - 7 -

4.5 本章小结............................................................................................................ - 7 -

第5章 链接................................................................................................................ - 8 -

5.1 链接的概念与作用............................................................................................ - 8 -

5.2 在Ubuntu下链接的命令................................................................................ - 8 -

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

5.4 hello的虚拟地址空间..................................................................................... - 8 -

5.5 链接的重定位过程分析.................................................................................... - 8 -

5.6 hello的执行流程............................................................................................. - 8 -

5.7 Hello的动态链接分析..................................................................................... - 8 -

5.8 本章小结............................................................................................................ - 9 -

第6章 hello进程管理...................................................................................... - 10 -

6.1 进程的概念与作用.......................................................................................... - 10 -

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

6.3 Hello的fork进程创建过程........................................................................ - 10 -

6.4 Hello的execve过程.................................................................................... - 10 -

6.5 Hello的进程执行........................................................................................... - 10 -

6.6 hello的异常与信号处理............................................................................... - 10 -

6.7本章小结.......................................................................................................... - 10 -

第7章 hello的存储管理................................................................................... - 11 -

7.1 hello的存储器地址空间............................................................................... - 11 -

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

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

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

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

7.6 hello进程fork时的内存映射..................................................................... - 11 -

7.7 hello进程execve时的内存映射................................................................. - 11 -

7.8 缺页故障与缺页中断处理.............................................................................. - 11 -

7.9动态存储分配管理.......................................................................................... - 11 -

7.10本章小结........................................................................................................ - 12 -

第8章 hello的IO管理.................................................................................... - 13 -

8.1 Linux的IO设备管理方法............................................................................. - 13 -

8.2 简述Unix IO接口及其函数.......................................................................... - 13 -

8.3 printf的实现分析........................................................................................... - 13 -

8.4 getchar的实现分析....................................................................................... - 13 -

8.5本章小结.......................................................................................................... - 13 -

结论............................................................................................................................ - 14 -

附件............................................................................................................................ - 15 -

参考文献.................................................................................................................... - 16 -

第1章 概述

1.1 Hello简介

P2P:第一个P(Program),即我们写的源程序文件hello.c,被GCC编译器驱动程序读取,先经过预处理器(cpp)预处理变成hello.i(修改了的源程序),然后再经过编译器(ccl)编译成hello.s(汇编程序),再由汇编器(as)汇编成hello.o(可重定位目标程序),最后通过链接器(ld)生成可执行目标程序hello。之后我们用shell通过fork函数为可执行目标程序hello创建进程,即第二个P(Process)。

020:在P2P之后,我们通过exceve加载运行hello,系统会为它分配响应的虚拟内存和物理内存,CPU以流水线的方式通过高速缓存高效的去读取执行并执行hello.c的指令,程序运行结束后就会被回收,所占用的内存也会得到释放。

1.2 环境与工具

软件环境:Windows 11 家庭中文版, Ubuntu 20.04

硬件环境:12th Gen Intel(R) Core(TM) i7-12700H   2.30 GHz,32G RAM,2TB

开发与调试工具:Visual Studio 2022, CodeBlocks 64位,vim,gdb,gcc,,edb,

objdump

1.3 中间结果

hello.c : 源程序

hello.i : hello.c预编译之后生成的修改了的源程序,用于研究预编译

hello.s : hello.c经过编译器编译之后的汇编程序,用于研究编译器的汇编操作

hello.o : hello.s经过汇编器生成的可重定位目标程序

hello : hello.o由链接器链接后生成的可执行目标程序,用来反汇编、分析链接过程以及程序运行过程

hello.asm : 对可执行目标程序hello反汇编得到,用来研究链接过程与寻址过程。

1.4 本章小结

本章我们了解了hello程序的生命周期、进行实验所使用的软硬件环境、开发与调试工具以及研究时候产生的中间结果文件,为后续更深入的探究hello的生命周期做了充足的准备。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

预处理器在程序运行之前会对源文件进行简单的处理,这个过程叫做预处理。在此期间预处理器会将一些代码比如#include、#define进行文本替换,把它们转化为实际代码,并且会删除注释和空白字符,因为注释对机器来说是没用的。

预处理的作用:

预处理的时候并不会对源代码进行解析,只是对它进行一些处理,例如将所包含头文件的指令替代、将宏定义替换为实际代码中的内容、根据条件判断是否编译某段代码以及删除注释,为后续的处理做好准备。

2.2在Ubuntu下预处理的命令

gcc -e hello.c > hello.i

2.3 Hello的预处理结果解析

经过预处理之后,注释被删除了,#include被替换成了相应的头文件,而且可以看到原本的代码并没有被改变。

出现了许多在hello.c中没有出现的头文件,这些头文件在原有的头文件中也有,所以即使我们没有写它也会出现。

宏N被替换成了5

2.4 本章小结

这一章我们在Ubuntu上对hello.c进行了预处理,通过查看hello.i进一步体会到预处理器对源文件进行预处理的具体操作就是一些文本替换。

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

第3章 编译

3.1 编译的概念与作用

编译的概念:

编译就是把用高级语言源程序写的代码翻译成具有等价作用的汇编语言的过程。编译阶段将预处理后的文件(.i)转换为汇编语言(.s),这一步骤是编译过程中非常关键的一部分,因为它将高级语言转换为低级的、更接近硬件的汇编语言。

编译的作用:

编译程序将源代码转换成目标代码的过程可以划分为五个主要阶段:首先是词法分析,其次是语法分析,接着进行语义检查并生成中间代码,然后是代码优化,最后生成目标代码。在编译的初期阶段,主要集中在词法分析和语法分析,这部分通常被称为源程序的分析阶段。在此过程中,如果存在语法错误,系统会检测到并提供相应的错误提示信息。

3.2 在Ubuntu下编译的命令

gcc -S hello.i > hello.s

3.3 Hello的编译结果解析

3.3.1汇编初始部分

main函数前的字段展示节名称:

.file               声明源文件

.text               表示代码节

.section   .rodata    表示只读数据段

.align              声明对指令或者数据的存放地址进行对齐的方式

.string              声明字符串

.globl              声明全局变量

.type               声明符号的类型

3.3.2 数据

源程序中的宏在预处理阶段已经被替换,在编译后的文件中出现为$5

原文件中i<10在编译文件中变成了<= $9

从两个文件的对比中我们可以看出在编译后的文件中两个字符常量先被处理了,而且存储在.rodata节常量区即只读数据段中。

   

然后通过寄存器%rax和%rdi把它们打印出来。

上面两行指令分别将rdi设置为两个字符串的起始地址

*arg[]数组中的每个元素都是一个指向字符类型的指针,起始地址存放在-32(%rbp)中,调用三次把参数传给printf函数。

被存放在寄存器%edi中的参数argc被压入栈中,与立即数5比较。

局部变量i存放在地址-4(%rbp)上

3.3.3全局函数

声明全局函数main

3.3.4赋值

For中的i=0操作,将0赋给i

3.3.5算术操作

对应i++操作,为i的值加一

3.3.6关系操作

Cmpl指令比较立即数4和-20(%rbp)即参数argc的大小,对应条件判断语句if(argc!=4),je是jump equal的缩写,如果相等则会跳到.L2处

cmpl指令比较立即数9和i的大小,对应for循环中的判断i<10,jle在i不符合条件后就会跳转

3.3.7控制转移指令

如果argc不为5就跳转

无条件跳转

i>=10时跳转

3.3.8函数操作

1.main函数

参数传递:

参数为int argc和char *argv[],分别保存在-20(%rbp)和-32(%rbp)中,与立即数比较以及通过寄存器%rax传给printf函数。

函数调用:

main函数调用了printf、exit、sleep、getchar函数,在内部使用call指令,将函数地址压入栈中,然后自动跳转到函数内部。

2.printf函数

参数传递:

printf函数调用了参数argv[1],argv[2],argv[3]。

函数调用:

printf被调用了两次,第一次将寄存器%rdi设置为待传递字符串"用法:Hello学号 姓名 手机号 秒数!\n"的起始地址,第二次将其设置为“Hello %s %s %s\n”的起始地址。

3.exit函数

参数调用:将寄存器%rdi设置为1

函数传递:用call指令直接调用

4.atoi和sleep函数

将参数argv[4]放入寄存器%rdi中用作参数传递,使用call指令调用atoi函数。然后,将转换完成的秒数从%eax传递到%edi中,edi存放sleep的参数,再使用call调用。

5.getchar函数

使用call指令直接调用即可,无需参数传递

3.3.9类型转换

atoi函数将字符转换为sleep函数需要的整型参数

3.4 本章小结

这一章详细探讨了gcc编译器如何把hello.i文件转换成hello.s文件的过程,简要说明了编译的含义和功能,演示了编译的指令,并通过分析hello.s文件中的汇编代码,探讨了数据处理,函数调用,赋值、算术、关系等运算以及控制跳转和类型转换等方面,比较了源代码和汇编代码分别是怎样实现这些操作的,加深了对汇编的理解。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:

汇编就是汇编器是将汇编语言程序(.s)转换为机器语言指令,把这些指令打包为一个可重定位目标文件(.o)的过程。在这个过程中,汇编器将汇编代码转换为机器可以直接执行的机器代码,这些机器代码以二进制形式存在,通常存储在目标文件中。

汇编的作用:

汇编是编译过程的一部分,在高级程序语言的编译过程中,汇编器将汇编代码转换为机器代码。这是编译器生成最终可执行文件或库文件的必要步骤。

4.2 在Ubuntu下汇编的命令

gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

4.3 可重定位目标elf格式

1.ELF

ELF头是ELF文件格式的起始部分,它包含了描述整个文件结构和组织的信息,是一种用于可执行文件、目标文件、共享库和核心转储的文件格式。

ELF头主要包括以下几个部分:

1. 魔数(Magic Number):文件开头一个16字节的序列,用于标识文件的类型和格式,对于ELF文件,魔数是固定的,以便系统可以快速识别文件格式。

2. 文件类别:区分文件是可执行文件、重定位文件还是共享库。

3. 数据编码:指定了文件是小端或大端和数据宽度。

4. 版本: ELF文件格式的版本号。

5. 入口点地址:可执行文件程序开始执行的内存地址。

6. 程序头表偏移:程序头表在文件中的偏移量。

7. 节头表偏移:节头表在文件中的偏移量,节头表包含了文件中各个节的详细信息。

8. 机器类型:用于指示处理器特定的特征。

9. ELF头大小:ELF头的大小(以字节为单位)。

ELF头为操作系统的加载器提供了必要的信息,使得加载器能够正确地解析和加载ELF文件。在链接和执行过程中,链接器和其他工具也会使用ELF头中的信息来处理目标文件和可执行文件。

2.节头

节头是ELF文件中的一个重要组成部分,它描述了文件中的各个节的属性和位置信息,记录了节的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等信息。

3. 重定位节

在ELF文件中,重定位节是一个重要的部分,它包含了关于如何修改代码和数据节的信息,以确保程序能够在不同的内存地址中正确地运行。当程序被编译时,编译器可能不知道某些符号(如函数或变量)的最终地址,因为这些符号的定义可能位于其他编译单元中,或者可能在运行时动态链接。重定位就是在程序加载或链接时,将这些未知的符号引用修正为它们在内存中的正确地址。一般而言,调用外部函数或者引用全局变量的指令需要修改,调用本地函数的指令不需要修改。

4. 符号表

在ELF文件中,符号表通常以.symtab节的形式存在,它包含了程序中定义和引用的所有符号的信息,除了局部变量的信息。符号表对于链接器来说尤其重要,因为它需要解析和重定位符号引用,以确保程序能够在运行时正确地访问变量和函数。在文件中除了.symtab,还可能有.dynsym节,它是一个较小的符号表,仅包含动态链接器需要的符号信息,用于动态链接。

4.4 Hello.o的结果解析

机器语言的构成:

1. 指令集:指令集是计算机能够理解和执行的命令的集合,每条指令都对应于一个特定的操作。

2. 操作码:每条机器语言指令都包含一个操作码,它指定了指令要执行的操作类型。

3. 操作数:操作数是指令执行操作所需的数据。

4. 寄存器:寄存器是CPU内部的高速存储单元,用于存储指令执行过程中的数据和指令本身。

5. 内存地址:内存地址用于指定内存中数据的位置。

6. 模式:某些机器语言指令可能包含模式位,用于指定操作数的大小(如字节、字、双字等)或寻址模式(如直接寻址、间接寻址、立即寻址等)。

7. 标志位:标志位是CPU内部用于表示某些状态或结果的位,如零标志、进位标志、溢出标志等。

8.指令格式:机器语言指令的格式通常由计算机的架构决定。指令可以有不同的格式,如固定长度格式、变长格式等,不同格式的指令有不同的编码方式。

机器语言与汇编语言的不同:

1. 机器语言中每一条指令前增加了一个十六进制的表示,即该指令的机器语言。

2. 反汇编文件中的所有操作数数值不变,但都改为了十六进制。

3. 反汇编的跳转指令中,所有跳转的位置被表示为主函数+段内偏移量这样确定的地址,而不再是段名称。

4. 反汇编文件中对函数的调用与重定位条目相对应。

4.5 本章小结

本章阐述了汇编语言的基本概念及其作用。以在Ubuntu操作系统中的hello.s源文件为例,演示了如何将它编译成hello.o对象文件,并进一步链接成ELF格式的可执行文件hello.elf。通过将可重定位目标文件转换为ELF格式并检查其内容,对文件内各个节进行了简要分析。进一步地,通过比较hello.o的反汇编输出文件hello.asm与原始的汇编文件hello.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.3 可执行目标文件hello的格式

1. ELF头(ELF Header)

在hello.elf.0和hello.elf这两个ELF文件中,它们的ELF头部包含的信息类型大致一样。都是以一个16字节的Magic序列作为开始,这个序列用于指明生成该文件的系统的字节顺序和字的大小。ELF头部的其余部分则包含了辅助链接器解析和解释目标文件的信息。对比hello.elf.0,hello.elf中的基础信息保持不变(例如Magic数字、文件类别等),但是文件的类型有所变化,此外,程序头部的大小增加了,节头部的数量也有所增加,并且文件现在有了指定的入口地址。

2.节头

节头描述了各个节的尺寸、它们在文件中的位置(偏移量)以及其他特性。当链接器执行链接操作时,它会将来自不同文件的同类段合并成一个更大的段,然后根据这个合并后的大段的大小和位置来更新所有符号的地址。

3.程序头

程序头是计算机程序中的一个部分,它包含了程序的基本信息和指令,这些信息和指令对于程序的加载和执行是必要的。

4.符号表

符号表用于存储程序源代码中各种符号的信息。这些符号可以是变量名、函数名、类名、宏定义等。符号表通常由编译器或解释器在编译或解释程序时创建和维护,用于在编译的不同阶段进行符号的查找、插入、删除和更新操作。符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。

5.4 hello的虚拟地址空间

程序头的LOAD可加载的程序段为0x400000

在edb上打开程序,我们可以从Data Dump窗口看到虚拟地址的情况,程序从地址0x400000开始到0x401000被载入,虚拟地址在0x4000000x400f0处结束。.text节的信息:

5.5 链接的重定位过程分析

不同之处:

  1. 在链接完成后,函数的数量有所增加。在反汇编文件hello.asm.0中,可以看到新增了一些函数的代码,如.plt节中的puts@plt、printf@plt、getchar@plt、exit@plt、sleep@plt等。这是因为动态链接器将那些在hello.c中使用的共享库函数的代码嵌入到了可执行文件中。

2. 在链接过程中,当链接器处理重定位条目时,会对函数调用指令call的参数进行修改。链接器会直接更新call指令后面的字节码,使其成为目标函数地址与紧接着call指令的地址之间的差值,这样就能够确保call指令指向正确的代码段。通过这种方式,最终得到了完整的反汇编代码。

3. 在链接过程中,链接器会解析重定位条目,并计算出相对距离。它会修改跳转指令参数对应位置的字节码,使其表示从程序链接表(PLT)中相应函数的地址到下一条指令的相对地址。这样,就生成了完整的反汇编代码。

hello的重定位:

1.地址解析:链接器解析目标文件中的所有外部引用,确定它们的地址。例如,printf和sleep函数的地址将从它们所在的库中找到。

2.调整地址:链接器根据地址解析的结果,调整所有对这些符号的引用。这包括修改目标文件中的指令和数据的地址。

3.分配运行时地址:链接器为程序中的每个段(如代码段、数据段)分配最终的运行时地址。

4.处理符号表:链接器处理符号表,确保所有引用的符号都有明确的定义。

5.生成可执行文件:最后,链接器生成可执行文件,其中包含了所有必要的重定位信息,使得程序能够在加载到内存时正确地定位函数和变量。

5.6 hello的执行流程

程序名               程序地址

hello!start                 0x4010f0

hello!main                 0x401125

hello!printf@plt            0x4010a0

hello!exit@plt                0x4010d0

hello!sleep@plt               0x4010e0

hello!getchar@plt             0x4010b0

start开始执行,主程序main执行printf、exit、sleep、getchar函数,exit退出

5.7 Hello的动态链接分析

动态链接的核心概念是将程序分割成独立的模块,并在程序执行时将这些模块链接起来,形成一个完整的程序。由于共享库在程序运行时可以加载到内存中的任意位置,编译器无法预先知道库函数的地址。为了解决这个问题,编译器会为这些函数引用生成重定位记录,动态链接器在程序加载时解析这些记录。延迟绑定是一种优化技术,它通过全局偏移表(GOT)和过程链接表(PLT)来实现。在hello.elf.0文件中,GOT表的起始位置是0x404000。在调用dl_init之前,GOT表中从0x404008开始的16个字节都是0。调用dl_init之后,这些字节的内容发生了变化。

5.8 本章小结

本节首先介绍了链接的基本概念及其作用,并通过命令链接演示了如何生成hello可执行文件。随后,我们查看了hello文件在ELF格式下的内容,并使用edb工具来观察hello文件的虚拟地址空间使用情况。最后,本节以hello程序为案例,对重定位过程、执行过程和动态链接进行了分析。

6章 hello进程管理

6.1 进程的概念与作用

进程的概念:

进程是就是一个执行中程序的实例,它包括程序代码、当前的活动、进程状态、以及进程所拥有的资源,既是基本的分配单元,也是基本的执行单元。在操作系统中,每个进程都拥有自己的虚拟地址空间,这意味着不同进程的内存空间是相互隔离的。进程之间可以通过进程间通信(IPC)机制进行数据交换。

进程的作用:

进程会为每个程序提供一种独占处理器和内存的假象,通过让系统中每个程序运行在某个进程的上下文中让处理器就像是在无间断地执行程序中的指令。

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

作用:

Shell是一个命令行解释器,它为用户提供了与操作系统内核交互的界面,接受用户的命令调度程序。

处理流程:

首先,Shell会从终端接收用户输入的命令。接收到命令后,Shell会对命令进行解析。如果该命令是Shell内置的命令,Shell会直接执行该命令。如果不是内置命令,Shell会通过调用fork()系统调用创建一个新的子进程。在子进程中,Shell将执行用户指定的程序。Shell会判断该程序是前台程序还是后台程序。如果它是前台程序,Shell会等待程序执行完毕后再继续执行。如果它是后台程序,Shell会将它放入后台并继续接收其他命令。在这个过程中,Shell能够接收来自键盘的信号并对其进行处理。

6.3 Hello的fork进程创建过程

在Shell界面输入一条命令:./hello 2022112774 yyh 12345678916。Shell解析这条命令后发现它不是Shell的内置命令,因此父进程执行fork()函数来创建一个新的子进程。这个新创建的子进程会得到一份与父进程相同的用户级虚拟地址空间的副本,这包括代码段、数据段、堆、共享库以及用户栈。子进程与父进程主要的区别在于它们拥有不同的进程ID(PID)。在父进程中,fork()函数会返回子进程的PID,而在子进程中,fork()函数会返回0。这个返回值提供了一个明确的方式来区分程序是在父进程还是在子进程中执行。

6.4 Hello的execve过程

execve函数用于在当前进程的上下文中加载并执行一个程序。该函数的原型定义如下:

int execve(const char *filename, const char *argv[], const char *envp[]);

execve函数负责加载名为filename的可执行文件,并传递参数数组argv和环境变量数组envp给该程序。如果execve执行成功,它不会返回到调用它的程序,因为当前进程的内存空间会被新程序替换。只有当发生错误,比如无法找到指定的可执行文件时,execve才会返回到调用点。因此,与fork函数不同,fork在父进程和子进程各返回一次,execve函数一旦调用成功,就不会返回。

6.5 Hello的进程执行

hello程序运行时,操作系统为应用程序提供了以下抽象:

1. 逻辑控制流:操作系统提供一个假象,让每个进程都感觉自己在独占处理器。程序计数器(PC)的值序列对应于程序中的指令,这个序列称为逻辑控制流。当两个逻辑流的执行在时间上重叠时,它们被称为并发流。

2. 私有地址空间:操作系统为每个进程提供独立的内存地址空间,让进程感觉自己在独占内存。

操作系统自身提供了以下抽象概念:

1. 上下文切换:操作系统内核使用上下文切换来实现多任务。每个进程都有自己的上下文,即内核重新启动进程所需的状态。

2. 时间片:进程执行其控制流的一部分的时间段称为时间片。多任务也被称为时间分片,因为CPU时间被划分为多个片段,分配给不同的进程。

3. 用户模式和内核模式:处理器通过一个模式位来区分用户模式和内核模式。在内核模式中,进程可以执行所有指令并访问任何内存位置。在用户模式中,进程不能执行特权指令,也不能直接访问内核区域的代码和数据。

4. 上下文信息:上下文包括通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和内核数据结构等值。

当hello程序通过execve函数启动时,操作系统为它分配新的虚拟地址空间。程序开始时在用户模式下运行,调用printf函数输出信息。当调用sleep函数时,进程进入内核模式,运行信号处理程序,然后返回用户模式。在执行过程中,CPU通过上下文切换来切分时间片,使得hello程序和其他进程能够交替使用CPU,实现进程的调度。

6.6 hello的异常与信号处理

中断、陷阱、故障和终止是四种类型的异常,它们会产生不同的信号,并且有不同的处理方式。以下是对每一种异常的描述,以及它们可能产生的信号和处理方法:

1. 中断:

   - 描述:中断是由外部事件引起的,如硬件设备请求服务或定时器超时。

   - 产生的信号:例如,SIGINT(通常由用户通过键盘发送,如Ctrl+C)、SIGALRM(由定时器超时产生)。

   - 处理方式:中断会打断当前正在执行的进程,并将控制权交给内核的中断处理程序。内核会处理中断,然后可能会恢复被中断的进程。

2. 陷阱:

   - 描述:陷阱是由执行特定指令时触发的,通常是系统调用指令。

   - 产生的信号:不直接产生信号,而是触发系统调用。

   - 处理方式:当执行系统调用指令时,CPU从用户模式切换到内核模式,执行内核提供的服务。系统调用完成后,控制权返回给用户模式下的进程。

3. 故障:

   - 描述:故障是由程序执行中的错误引起的,如非法访问内存。

   - 产生的信号:例如,SIGSEGV(段错误,通常由非法内存访问引起)、SIGBUS(总线错误,通常由内存访问错误引起)。

   - 处理方式:内核会尝试修复故障,如果可能的话,内核会将控制权返回给进程。例如,如果进程试图访问一个页不在内存中,内核可能会触发一个页面错误,将页面加载到内存中,然后重新执行导致故障的指令。

4. 终止:

   - 描述:终止是由不可恢复的错误引起的,如执行了非法指令。

   - 产生的信号:例如,SIGILL(非法指令信号)、SIGABRT(由abort()函数产生的终止信号)。

   - 处理方式:内核通常会终止进程并清理资源。进程可以在终止前捕获这些信号,执行一些清理操作,例如,释放内存、关闭文件等。

处理这些异常的信号时,进程可以选择以下几种方式:

- 忽略信号,即不做任何处理。

- 捕获信号并处理,通过安装信号处理函数(如使用 signal() 或 sigaction() 函数)。

- 执行默认操作,即让操作系统处理信号,通常是终止进程。

无操作,按回车键结束回收程序

按下Ctrl+C,Shell直接结束并回收hello进程。

按下Ctrl+Z,屏幕就会提示hello进程被挂起

使用ps和jobs命令可以查看hello进程的状态,其job代号为1

Pstree命令可用树状图的形式查看所以进程

使用kill命令可以杀死后台进程

用fg 1命令让挂起的进程继续,正常结束然后回收进程

乱按的输入会被缓存到stdin中,每次调用getchar函数会输出’\n’结尾的字符串,并不影响进程的进行(如果没有按到ctrl+z这样的组合)。

6.7本章小结

本节内容主要围绕计算机系统中的进程与shell进行讨论。首先,通过一个简单的hello程序,概述了进程的基本概念、功能以及shell的作用和操作流程。接着,详细阐述了hello程序从进程的创建、启动到执行的全过程。最后,本节对hello程序可能遇到的异常状况以及运行结果中的不同输入情况进行了阐释和说明。

7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1逻辑地址

在具备地址转换功能的计算机系统中,程序访问指令中提供的地址(操作数)被称为逻辑地址,它也被称作相对地址。这个逻辑地址需要通过寻址过程的计算或者转换,才能确定对应的内存中的物理地址。逻辑地址由一个段标识符和一个偏移量组成,偏移量指定了在段内的相对位置。在hello程序中,与特定段相关的偏移地址部分就是逻辑地址。

7.1.2线性地址

线性地址是逻辑地址转换为物理地址过程中的中间步骤。当程序hello执行时,它会产生逻辑地址,这个逻辑地址在分段机制中代表段内的偏移量。逻辑地址与基地址相结合后,就形成了线性地址。

7.1.3虚拟地址

程序在访问存储器时使用的逻辑地址被称作虚拟地址。这些虚拟地址通过地址转换过程被映射到物理地址。虚拟地址空间的大小与实际物理内存的容量无关,hello程序在执行时使用的就是这种虚拟地址。

7.1.4物理地址

在内存中,信息是以字节为单位存储的,每个字节单元都对应一个唯一的内存地址,这个地址被称为物理地址,对于hello程序来说,这就是它的实际地址或者绝对地址。

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

段式内存管理是一种将程序划分为多个段进行存储的方法,每个段代表一个逻辑单元。这种管理方式通过段表来实现,段表包含了段号(或段名)、段的起始点、加载位、段的长度等信息。程序因此可以被分为多个部分,比如代码段、数据段、共享段等。

逻辑地址由两部分构成:段标识符和段内偏移量。段标识符是一个16位的字段,也叫做段选择符,它由13位的索引号和一些硬件相关的细节组成。索引号用于在段描述符表中定位特定的段描述符,而段描述符则详细定义了一个段的属性,包括其地址信息。多个段描述符集合在一起构成了段描述符表。

全局描述符表(GDT)在系统中是唯一的,它包含了操作系统使用的代码段、数据段、堆栈段的描述符,以及各个任务和程序的局部描述符表(LDT)的段描述符。

每个任务或程序都有自己的LDT,它包含了该任务或程序私有的代码段、数据段、堆栈段的描述符,以及该任务或程序使用的门描述符,如任务门、调用门等。

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

虚拟内存被构造成一个由存放在磁盘上的N个连续字节单元组成的数组。在虚拟内存系统中,虚拟内存被划分成称为虚拟页的单元,同样地,物理内存也被划分为物理页。虚拟页的管理是通过页表来完成的,页表是一个由页表条目(PTE)组成的数组,每个PTE包括一个有效位和一个地址字段。有效位用于指示虚拟页是否已经被加载到DRAM中,如果有效位被设置,地址字段就会指向DRAM中对应的物理页的起始地址。如果发生缺页,系统会从磁盘中加载相应的页面到内存中。内存管理单元(MMU)使用页表来转换虚拟地址到物理地址,从而实现地址的映射。

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

Core i7处理器使用一个四级的页表层次结构来管理虚拟内存。当CPU生成一个虚拟地址(VA)时,这个地址会被发送到内存管理单元(MMU)。MMU会使用虚拟地址中的高位虚拟页号(VPN)来查询转换后备缓冲器(TLB)。如果在TLB中找到了匹配的条目,那么就直接得到对应的物理地址(PA)。如果TLB中没有找到匹配的条目,即发生TLB缺失,MMU就会查询页表。控制寄存器CR3包含了第一级页表的起始物理地址,而VPN的第一部分(VPN1)则用于确定在第一级页表中的偏移量,以此来查询页表条目(PTE)。  这个过程会递归地应用到接下来的每一级页表中,直到在第四级页表中找到物理页号(PPN)。最后,将PPN与虚拟地址中的偏移量(VPO)组合起来,形成最终的物理地址(PA),并将其添加到物理线性地址(PLT)中。

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

在支持三级缓存的计算机体系结构中,物理内存访问涉及多个缓存层次,包括L1、L2和L3缓存。处理器首先在L1缓存中查找数据,如果未命中,则继续在L2缓存中搜索,L2缓存比L1缓存大但速度稍慢。如果数据在L2缓存中仍未找到,处理器会检查L3缓存,这是三级缓存中最大的一层,速度比L2缓存慢但仍然远快于主内存,且通常被所有核心共享。如果数据在L3缓存中也未命中,处理器最终必须访问主内存,这通常需要更多的时钟周期。在多核心系统中,缓存一致性机制确保了当一个核心修改数据时,其他核心的缓存中的数据副本会被更新或失效。缓存的写策略可以是直写或回写,直写策略中数据同时写入缓存和主内存,而回写策略中数据只写入缓存,稍后再写入主内存。通过这种方式,缓存减少了处理器直接访问主内存的次数,提高了系统的整体性能。

7.6 hello进程fork时的内存映射

当hello进程在操作系统中使用fork系统调用创建一个新的进程时,新进程会得到与原进程几乎相同的内存映射。具体来说,子进程会获得父进程虚拟内存空间的一份副本,包括代码段、数据段、堆、共享库以及用户栈等。这个过程中,父进程的页表会被复制到子进程中,但是物理内存页并不会立即复制,因为父进程和子进程会共享这些内存页,这种机制称为写时复制(Copy-on-Write)。

在写时复制机制下,只有当父进程或子进程尝试修改共享的内存页时,才会实际复制这些页。如果父进程或子进程中的一个试图写入一个共享的页,操作系统会介入,创建一个新的内存页,并将原始页的内容复制到新页中。然后,修改的进程会使用新页,而另一个进程仍然使用原始页。这样,尽管子进程在虚拟内存中有自己独立的地址空间,但在物理内存中,它们与父进程共享未修改的页面,从而提高了效率和速度。

在fork操作完成后,子进程会继承父进程的所有打开文件描述符,这意味着子进程可以访问父进程打开的文件。此外,子进程的执行环境,如环境变量和当前工作目录,也是从父进程继承而来的。然而,fork是一个单一调用点,子进程和父进程的执行流程在fork返回后立即分叉,子进程的返回值通常为0,而父进程的返回值为子进程的进程ID。

7.7 hello进程execve时的内存映射

当hello进程调用execve系统调用时,它会替换当前进程的内存映像,包括代码段、数据段、堆和栈等,而不是创建一个新的进程。execve加载一个新的可执行文件覆盖当前进程的地址空间,但进程ID、打开的文件描述符、信号处理函数等会保持不变。

在execve执行过程中,首先会清除当前进程的地址空间中的所有内存映射,然后根据新的可执行文件(如hello程序)的ELF头部信息,设置新的内存映射。这包括将可执行文件的代码段、数据段等映射到进程的地址空间。堆和栈通常会根据可执行文件的需求重新初始化。

execve调用成功后,进程的执行从新加载的程序的入口点开始,这意味着hello进程的执行流程会转移到新的程序的主函数。此时,原有程序的代码、数据和堆栈等内存空间已被新程序的相应部分替换,但进程ID、已打开的文件描述符和信号处理等会保持不变,因为它们属于进程的上下文,而execve并不改变进程的上下文。

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

当程序在运行时遭遇缺页中断,操作系统内核会激活缺页中断处理程序来处理这一情况。缺页中断处理程序会依照以下步骤操作:

(1) 首先确认产生的虚拟地址是否在程序的合法地址范围内,如果地址不合法,则会引发段错误,导致进程被终止。

(2) 接着,检查进程是否具备对所请求页面的读、写或执行权限,如果权限不足,则会触发保护异常,同样会导致程序终止。

(3) 如果上述两个检查都通过了,内核会选取一个物理内存中的页面作为牺牲页,如果这个页面已被修改,则将其写回磁盘。随后,内核会将所需的页面从磁盘加载到物理内存中,并更新页表以反映新的映射关系。完成这些操作后,控制权会返回给hello进程,程序会重新尝试执行那条导致缺页中断的指令。

7.9动态存储分配管理

动态存储分配是操作系统提供的一种功能,它使得程序能够在运行时根据需求来分配和释放内存。这种管理方式使得程序在内存使用上具有更大的灵活性和效率,因为它不要求在程序编译时确定内存需求。动态存储分配通常由专门的内存分配器完成,比如C语言中的malloc和free函数,这些分配器负责从系统的堆空间中分配和回收内存块。

   动态存储分配管理策略是指操作系统或程序用来在运行时分配和释放内存的方法和技术。这些策略旨在优化内存使用、减少碎片、提高分配效率,并确保内存管理的可靠性。比如首次匹配、最坏适配、最佳适配等。

7.10本章小结

本节内容主要围绕hello程序的存储器地址空间展开,包括Intel处理器的段式管理机制、hello程序的页式管理方式,以及在特定环境下,Intel Core i7处理器的虚拟地址(VA)到物理地址(PA)的转换过程和物理内存的访问方法。此外,还分析了hello进程在调用fork系统调用时内存映射的变化,hello进程执行execve系统调用时的内存映射变化,缺页故障发生时的缺页中断处理过程以及动态存储分配管理的方式和策略。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

1. 设备驱动程序:

   Linux内核通过设备驱动程序来管理I/O设备。驱动程序是一个软件组件,它为内核提供与硬件设备通信的接口。

2. 文件系统接口:

   Linux将所有的I/O设备抽象为文件,通过文件系统接口进行访问。

3. 系统调用:

   系统调用(如read()、write()、open()、close()等)是用户空间应用程序与内核交互的标准方式,用于执行I/O操作。

4. 用户空间I/O库:

   用户空间有许多库(如C标准库)提供了对系统调用的封装,使得I/O操作更加方便。

5. 异步I/O(AIO):

   Linux支持异步I/O,允许应用程序发起I/O操作后继续执行,而不必等待I/O完成。

6. 直接I/O:

   直接I/O允许应用程序绕过内核的缓冲区,直接在用户空间和设备之间进行数据传输,这在某些高性能应用中非常有用。

7. I/O调度:

   Linux内核的I/O调度器负责优化磁盘I/O请求的顺序,以减少寻道时间和提高吞吐量。

   。

8. 内存映射I/O:

     内存映射I/O(mmap)允许将文件的内存区域直接映射到应用程序的地址空间,从而实现高效的数据传输。

9. 设备特殊文件:

   特殊文件(如字符设备和块设备文件)提供了对设备I/O的访问。字符设备文件用于一次处理一个字符的设备,而块设备文件用于一次处理一块数据的设备。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

Unix提供了基本的I/O函数,包括打开和关闭文件的open和close,读取和写入数据的read和write,移动文件指针的lseek,创建文件的creat,以及控制文件描述符和同步文件状态的fcntl和fsync。这些函数是Unix I/O操作的基础,适用于各种文件和设备。

8.3 printf的实现分析

printf函数解析传入的格式字符串,提取输出项的格式和参数,然后调用转换函数处理数据,最终使用系统调用输出格式化后的数据到标准输出。该过程循环处理每个输出项,并确保输出格式正确。

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

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

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

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

8.4 getchar的实现分析

getchar 函数从标准输入读取一个字符,首先检查缓冲区是否有字符,然后读取并返回ASCII码值对应的字符。

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

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

8.5本章小结

这章探讨了Linux的IO设备管理方法、Unix IO接口及其函数、printf和getchar两个函数的实现分析,对hello的IO管理有了更深刻的认识。

结论

程序员将hello代码输入后,代码经历以下过程:

1. 预处理:生成hello.i文件。

2. 编译:生成hello.s汇编文件。

3. 汇编:生成hello.o可重定位目标文件。

4. 链接:生成hello可执行文件。

5. 运行:输入./hello 2022112774 yyh 12345678916 1。

6. 创建进程:调用fork函数。

7. 加载程序:调用execve函数,进入main函数。

8. 执行指令:CPU分配时间片,hello顺序执行。

9. 访问内存:MU将虚拟地址映射成物理地址。

10. 信号管理:SIGINT和SIGTSTP信号管理。

11. 终止:回收子进程,传递退出状态。

计算机一个简单指令的背后存在很多复杂的实现操作,了解这些操作对程序员来说很重要,它可以帮助我们写出更好的代码。

附件

hello.c      源程序

hello.i      预处理后的文本文件

hello.s      编译后的汇编语言文件

hello.o      汇编后的可重定位目标文件

hello        可执行文件

hello.asm    反汇编hello得到的反汇编文件

hello.elf     用readelf读取hello.o得到的ELF格式信息

hello.elf.0    用readelf读取hello得到的ELF格式信息

参考文献

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

[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.

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值