一个Hello程序的一生

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业        物联网工程                  

学     号        2021112705             

班     级          2137301             

学       生          杨童莘           

指 导 教 师            吴锐       

计算机科学与技术学院

2023年5月

摘  要

本篇论文分析了一个简单的C语言程序 "hello" 的整个生命周期,包括代码编写、进程启动和结束。文章通过这个过程展示了Linux下程序的一般过程(P2P)和从无到有的过程(020),并揭示了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的预处理结果解析................................................................................. - 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简介   

尽管"hello"程序本身只是一个带有延时的打印"hello"信息的简单程序,但本文揭示了它并不简单。

在"hello"的P2P过程中,程序员先编写了原始C语言代码,然后通过预处理、编译、汇编和链接,得到可执行文件。接着,shell为"hello"创建进程并加载可执行文件,使得"hello"从代码变成了运行的进程(from program to process)。

在"hello"的020过程中,shell为它创建进程并加载可执行文件,为其提供了虚拟地址空间和其他进程上下文,在实现"hello"从无到有的过程中起到了关键作用。

在"hello"运行期间,它会经历多种异常和信号,并通过各种机制访问存储器以及通过中断和I/O端口与外部设备交互等。最终,当"hello"正常退出或收到信号后终止,操作系统将结束它的进程并释放占用的所有资源,返回到shell,这就是"hello"从无到有再到无的完整过程(from zero to zero)。

1.2 环境与工具

硬件环境: CPU:标压8核i7-11800H 32G
系统环境: 虚拟机:Ubuntu 20.04.4 LTS,VMWare Workstation
工具: 文本编辑器gedit,反汇编工具edb 1.3,反汇编工具objdump,编译环境gcc等。

1.3 中间结果

①原始代码hello.c

②预处理后的代码hello.i。

③编译后的汇编语言代码hello.s。

④可重定位目标文件hello.o。

⑤hello.o的objdump结果hello_o_disasm.txt。

⑥可执行文件hello。

⑦hello的objdump结果hello_disasm.txt。

第2章 预处理

2.1 预处理的概念与作用

预处理是指在源代码文件被编译之前,对源代码进行预处理操作的过程。预处理器是一种特殊的程序,可以根据预处理指令修改源代码文件。预处理的主要作用是在源代码编译之前,对代码进行预处理操作以方便编码和编译。因此,预处理器会在源代码中执行一些指令,如宏定义、条件编译、头文件包含等。

宏定义是预处理器最常用的指令之一,它使程序员可以在源代码中定义一些常量和函数,以便在编译时使用。宏定义可以提高代码的可读性和可维护性,同时也可以减少编码工作量和代码重复。

条件编译是另一个预处理指令,它可以根据条件编译指令来选择性地编译代码。这通常用于编写跨平台程序或者根据不同的条件编译不同的代码。

头文件包含也是预处理器的一个常用指令,它用于在源代码中引入其他代码文件的内容。头文件包含可以使代码更加模块化和可重用,同时也可以使代码更加易于维护。

总之,预处理器是源代码编译的重要组成部分,它可以使程序员更加方便地编写和调试代码,同时也可以提高代码的可读性和可维护性。

2.2在Ubuntu下预处理的命令

在终端内输入命令gcc -E hello.c,在屏幕上得到hello.c的预处理结果

 

图1:预处理

2.3 Hello的预处理结果解析

       将hello.c的预处理结果保存在hello .i中:gcc -E hello.c -o hello.i

查看hello.i文件:

图2:hello.i头文件信息

最开头是hello.c涉及到的头文件信息

图3:头文件定义的类型名

接下来是头文件定义的各种类型名

图4:头文件主体

然后是被包括的头文件主体内容,包含struct定义,大量的函数声明等等,它们都经过预处理器的宏展开

图5:main.c

最后才是真正的hello.c内容

2.4 本章小结

由于"hello.c"文件经过预处理后的结果,我们可以看到预处理器对源代码进行了大量的展开操作。虽然预处理后的结果仍然是合法的C语言源文件,但它比原来要扩展得多,包括许多被包含进来的内容。尽管其中大部分内容都是对程序本身没有任何用处的声明,实际上程序只用到了其中声明的一小部分函数。因此,从预处理步骤就可以初步发现,一个小小的hello world程序,背后隐藏的东西要多得多。

第3章 编译

3.1 编译的概念与作用

编译(compilation)是指将高级语言代码转换为计算机可以执行的机器代码的过程。在这个过程中,源代码经过一系列的转换和优化,最终生成可执行程序。具体地,编译可以分为以下几个阶段:

1. 预处理(preprocessing):在编译之前,首先需要对源代码进行预处理。预处理器会执行指令,如宏替换、条件编译等,生成一个中间文件(通常以.i为后缀)。

2. 编译(compiling):编译器将预处理后的源代码转换为汇编代码,即将高级语言代码转换成汇编语言代码。这个阶段主要完成词法分析、语法分析、语义分析等工作,生成中间代码。

3. 汇编(assembling):汇编器将汇编代码转换成机器代码(即二进制代码),这些二进制代码可以直接在计算机上运行。这个阶段主要完成符号决议、生成目标文件等工作。

4. 链接(linking):链接器将目标文件和库文件等合并成一个完整的可执行文件。这个阶段主要完成地址和空间的分配、符号解析等工作。

编译的作用是将高级语言程序转换为机器代码,使计算机可以执行这些程序。编译器不仅可以检查程序中的语法错误,还可以对程序进行优化,使得生成的机器代码更加高效。此外,编译器还可以提供一些调试和分析工具,帮助开发人员诊断和调试程序。

在编译过程中,从.i到.s的阶段是将中间代码转换成汇编代码的过程。这个阶段主要完成了代码生成、寄存器分配等工作。汇编代码是一种低级的程序表示形式,可以直接翻译成机器语言,因此可以进一步优化和调试。汇编代码也可以被反汇编成高级语言代码,用于恶意分析等用途。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

       

3.2 在Ubuntu下编译的命令

输入命令gcc -S hello.i -o hello.s -fno-PIC -no-pie -m64,得到编译结果hello.s

6:编译命令

3.3 Hello的编译结果解析

接下来研究hello.s中main函数的汇编语言代码。分类研究编译器hello.c语言代码的处理

3.1数据存储

1.数字常量

在源代码中使用的数字常量都是存储在.test节

图7:main源代码

2.字符串常量

字符串常量:可以发现在printf等函数中使用的字符串常量是储存在.rotate段的,具体储存情况可以见如下截图:

图8:.LC0,.LC1

3.变量

       main函数中包含3个局部变量:argc和argv分别存储在rbp-20和rbp-32的位置,而i是一个局部变量,存储在rbp-4的位置。因此,main函数的堆栈帧需要为这3个变量分配内存空间,总共需要32字节的空间

图9:变量

3.2数据操作

1.比较argc和4

图10:比较操作

2.argv是一个保存变量的数组,存储在栈里

图11:argv

3.变量i的赋值i=0

图12:赋值

4.比较 i<8

图13:比较

5.加法 i++

 

图14:加法

3.3 数组argv

涉及的数组操作就是对argv取元素

图15:对数组取操作

本段汇编代码将argv[2] 加载到rdx,将argv[1]加载到rax。&argv[2]=argv+2*8=argv+16,&argv[1]=argv+8

3.4 条件判断

图16:条件判断

这段代码使用了cmp来检查argc是否等于4。如果是,则设置状态寄存器的ZF标志位,接着使用条件转移指令je直接跳转到.L2位置,从而跳过中间的语句块(包括printf和exit的调用)。如果argc不等于4,则je不会跳转,代码将继续执行中间的语句块。

3.5循环

图17:循环

该程序包含一个for循环,变量i从0循环到7。循环开始时,将i初始化为0。指令使用cmp将i与7进行比较,如果i<=7,则跳转到循环体的开头继续执行循环,否则跳出循环。循环体内的语句块执行完毕后,i的值将会加1。

3.6函数调用与参数传递

在x86-64平台下的C语言函数调用规范中,参数不超过6个时,会按照rdi, rsi, rdx, rcx, r8, r9的顺序进行传递。在调用printf("用法: ...")时,只有一个参数,即字符串地址(在只读数据节的LC0位置)。编译器会将该地址放入rdi中,然后调用puts()函数输出该字符串。需要注意的是,由于只输出一个字符串,因此编译器会自动将printf处理为puts。

在调用exit(1)时,只有一个参数,即整数1。编译器会将该值放入edi中,然后调用exit()函数。

在调用printf("Hello %d %d\n", argv[1], argv[2])时,有三个参数。字符串地址会放入rdi中,argv[1]的值会放入rsi中,argv[2]的值会放入rdx中。然后,编译器会调用printf()函数进行格式化输出。

在main函数被调用后,需要获取参数argc和argv。由于该程序未开启O2优化,包括参数在内的所有局部变量都会存在堆栈帧上。因此,编译器会将edi中的32位整数argc和rsi中的64位地址argv分别作为局部变量存储在堆栈帧的rbp-20和rbp-32处。

3.7函数返回

图18:函数返回

函数返回时会通过leave释放栈堆栈,将返回值存在rax中

3.4 本章小结

预处理后的hello.i是C源代码的预处理结果,其中包含了所有的宏定义和头文件引用等内容,而hello.s是编译器将C源代码转换成汇编代码的结果。由于汇编代码是机器级代码的一种表现形式,因此相对于预处理后的C源代码,它更加精简和高效。

在编译器将C源代码转换成汇编代码的过程中,大部分的代码都是与main函数相关的代码。因此,hello.s中的汇编代码主要都是main函数的汇编代码。这些汇编代码已经开始为程序的机器级表示定型,它们已经能够在指令级层面控制CPU了。同时,这些汇编代码还能够被程序员理解和分析,因此可以说是介于程序员与机器之间的桥梁。

第4章 汇编

4.1 汇编的概念与作用

汇编语言是一种低级语言,它是机器语言的一种符号表示方法。汇编语言使用助记符(mnemonic)来代替二进制指令码,以便于程序员在写程序时更容易理解和维护。汇编语言程序需要经过汇编器的处理,才能被转换成机器语言的二进制程序,从而能够在计算机上运行。

汇编的作用主要体现在以下几个方面:

1. 汇编语言程序是编译器生成机器码的中间产物。编译器将高级语言编译成汇编语言,再由汇编器将汇编语言翻译成机器语言。因此,汇编语言在编译器生成机器码的过程中扮演了重要的角色。

2. 汇编语言可以直接操作计算机硬件。汇编指令可以直接控制CPU寄存器、内存和输入输出等硬件,因此汇编语言在进行底层编程时非常有用。

3. 通过阅读汇编语言程序,可以更深入地了解计算机结构和工作原理。汇编语言程序直接对应着机器码指令,因此可以通过阅读汇编语言程序来了解机器码指令的含义和作用,从而更深入地了解计算机的结构和工作原理。

总之,汇编语言在计算机编程中扮演着重要的角色,它是编译器生成机器码的中间产物,可以直接操作计算机硬件,同时也是深入了解计算机结构和工作原理的重要工具。

4.2 在Ubuntu下汇编的命令

对hello.s使用命令gcc -c

4.3 可重定位目标elf格式

   使用readelf解析hello.o,我们可以看到,hello.o文件中共有13个节,其中包括代码段(.text)、只读数据段(.rodata)等。此外,还有8个重定位条目和7个全局符号(都是函数声明,即强符号)。在这些重定位条目中,有两个对应着.rodata节中的数据地址,显然它们是由printf函数使用的那两个字符串地址。另外6个重定位条目则是被call指令调用过的函数地址,这些地址实际上还是空的,需要和对应的库进行链接才能被填充上真正的函数地址

ELF头以一个16字节的序列开始,这个序列描述了生成该文件系统下的字的大小以及一些其他信息。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。

图19:ELF头

节头表
描述了.o文件中每一个节出现的位置,大小,目标文件中的每一个节都有一个固定大小的条目。

图20:节头表

重定位节
重定位节中包含了在代码中使用的一些外部变量等信息,在链接的时候需要根据重定位节的信息对这些变量符号进行修改。链接的时候链接器会根据重定位节的信息对外部变量符号决定选择何种方法计算正确的地址,通过偏移量等信息计算出正确的地址。

图21:重定位

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编

图22:hello.o反汇编1

图23:hello.o反汇编2

对于hello.o的反汇编,我们可以与第3章中的hello.s进行对照分析,发现以下几点不同之处:

1. 进制不同:hello.s中反汇编的数字表示为十进制,而hello.o中反汇编的数字表示为十六进制。

2. 分支转移:hello.s中对于条件跳转,使用段的名称(如.L2)来表示跳转的地址,而hello.o中由于已经是可重定位文件,每一行都已经分配了相应的地址,因此跳转命令后跟的是需要跳转部分的目标地址。

3. 函数调用:hello.s中,call指令后跟的是需要调用的函数的名称,而hello.o中call指令使用的是main函数的相对偏移地址。同时可以发现在hello.o反汇编代码中调用函数的操作数都为0,即函数的相对地址为0。这是因为在链接生成可执行文件时,才会给函数分配确定的地址,因此在可重定位文件中,函数的地址都是相对地址,需要再进行链接才能得到真正的地址。

总的来说,hello.o反汇编代码相对于hello.s反汇编代码,包含了更多的信息,这些信息包括了每条指令的真实地址和操作数的真实值。同时,由于hello.o是可重定位文件,因此它的反汇编代码还可以显示出重定位条目和符号表等信息,这些信息对于后续的链接过程是非常重要的。

4.5 本章小结

汇编是将高级语言编写的程序转化为可执行的机器码的过程。但是与高级语言编译器不同的是,汇编器生成的机器码并不是最终的可执行文件,而是“可重定位的机器码”。这意味着,在链接的过程中,这些可重定位的二进制文件可以被链接在一起,最终生成一个完整的可执行文件。

而这个“可重定位”的特性,也正是汇编器与高级语言编译器的一大区别。高级语言编译器生成的是非常具体的、无法修改的二进制代码,而汇编器生成的则是在链接过程中可以被修改的机器码。这使得链接过程变得非常灵活,程序员可以通过链接器的一些参数,决定如何将多个可重定位文件组合成一个可执行文件。

因此,重定位机制在链接过程中起着非常重要的作用,它可以将不同可重定位文件中的符号(如函数、变量等)链接起来,从而使得资源的使用变得更加高效。同时,重定位机制还可以处理一些跨越多个可重定位文件的符号引用,使得这些跨越多个文件的符号也能够正确地链接起来,最终生成一个完整的可执行文件。

5章 链接

5.1 链接的概念与作用

链接(linking)是指将多个程序模块(例如函数、变量、类、库等)合并在一起,形成一个可执行程序或库的过程。链接器(linker)是负责执行链接的程序,通常是编译器的一部分。

链接的主要作用是将程序模块之间的符号引用(symbol references)和符号定义(symbol declarations)关联起来,使得程序能够正确地运行。当程序需要使用某个符号时,链接器会在符号表(symbol table)中查找该符号的定义,然后将符号引用替换为其对应的地址或偏移量。如果某个符号在程序中被多次引用,链接器会将它们合并为同一个地址或偏移量,以避免重复定义。

链接可以分为静态链接(static linking)和动态链接(dynamic linking)两种。静态链接是指将程序模块直接合并到可执行文件中,使得程序在运行时无需加载外部库,但会增加可执行文件的大小。动态链接是指将程序模块编译为独立的库文件,使得多个程序可以共享同一个库,减少了内存占用和磁盘空间,但需要在运行时动态加载库文件。

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-gnu/crtn.o -z relro -o hello来进行链接

图24:链接命令

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

使用readelf解析helloELF格式,得到hello的节信息和段信息:

图25:ELF头

                                         图26:ELF节头

图27:ELF节头2

图28:ELF程序头

5.4 hello的虚拟地址空间

图29:hello的虚拟地址空间

可执行文件hello本身包含4个段,其中存储了hello主要的代码和数据。这4个段对应于使用readelf命令查看hello的program header时看到的4个LOAD段。

此外,还有中间的4个段是由动态链接库ld-2.31.so提供的。最后,还有一些段用于存储栈信息和与内核相关的数据。

5.5 链接的重定位过程分析

输入objdump -d -r hello得到hello反汇编结果 。与可重定位目标文件hello.o的反汇编结果相比,可执行文件hello主要有两个方面的差异。

首先,可执行文件hello扩充了很多函数代码,包括程序加载后执行main函数之前需要进行的一些准备工作,以及hello程序需要用到的一些库函数的定义等。这些代码不会在可重定位目标文件中出现,而是在链接和加载过程中添加进来的。

其次,在main函数中,可重定位目标文件hello.o中等待重定位而暂时置0的地址操作数被设置为了虚拟地址空间中真正的地址。这是因为在链接和加载过程中,链接器会将这些操作数替换为真正的地址,使得程序能够正确地访问变量和函数。这也是可执行文件和可重定位目标文件之间最显著的差异之一。

图30:链接重定位分析

图31:重定位定位分析2

图32:重定位分析3

图24:重定位分析4

图25:重定位分析5

链接器会将所有模块的节组织起来,为可执行文件的虚拟地址空间定型。当链接器将可重定位目标文件组合成可执行文件时,它会解析目标文件中的重定位条目,并将它们中的地址操作数替换为正确的地址。这样,程序就能够正确地访问变量和函数,并在运行时按照预期执行。

5.6 hello的执行流程

图26:子函数名和地址

当hello程序启动时,它的最初程序地址会在0x7f2433995100处,这是动态链接库ld-2.31.so的入口点_dl_start。这个函数会完成一些初始化工作,然后跳转到_dl_init继续初始化工作。接下来,程序会跳转到hello的程序入口点_start,开始执行hello程序本身的代码。

接着,程序通过一个间接call指令跳到动态链接库ld-2.31.so的__libc_start_main函数处。这个函数会完成一些标准库的初始化工作,并负责调用main函数。在main函数执行之前,__libc_start_main函数还会调用动态链接库中的__cxa_atexit函数,用于在程序结束时调用指定的函数。然后,程序会调用hello可执行文件中的__libc_csu_init函数,这个函数是由静态库引入的,也是用于执行一些初始化工作的。

接下来,程序会调用动态链接库里的_setjmp函数,这个函数主要是用于设置非本地跳转所需的一些信息。之后,程序开始调用main函数。

当main函数执行完毕后,程序会通过hello本身携带的exit函数跳转到地址0x7f24339acbb0处,这个地址位于动态链接库中。在进行了一些清理工作之后,程序退出。

图27:hello程序的调试过程

5.7 Hello的动态链接分析

   在使用共享库时,编译系统提供了延迟绑定的方法,以避免在编译时确定函数地址,而是在第一次调用函数时进行绑定。这种延迟绑定的实现方式,是通过GOT和PLT两个节的协作来完成的。

在ELF格式中,GOT(Global Offset Table)节用于存储全局偏移表,它包含了程序中所有需要访问全局变量和函数的地址,以及这些地址的初始值。PLT(Procedure Linkage Table)节则用于存储过程链接表,它包含了程序中所有需要调用的函数的地址,以及这些地址的初始值。

在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同的函数。当程序第一次调用一个由共享库定义的函数时,PLT会跳转到动态链接器中的特定函数,这个函数会将函数的地址写入GOT中的相应条目,以避免再次进行延迟绑定。之后,PLT会直接跳转到该函数的地址,以执行函数。

因此,通过观察EDB调试器中的内存布局,可以发现在dl_init函数执行之后,.got.plt节的内容发生了变化,其中包含了程序中需要访问的全局变量和函数的地址,以及这些地址的初始值。具体来说,GOT中的条目被重定位为实际的函数地址,而PLT中的条目则包含了跳转到动态链接器中特定函数的指令。

总之,通过GOT和PLT的协作,动态链接器实现了延迟绑定的目的,从而提高了程序的运行效率和灵活性。可以用readelf看got.plt节的内容:

图28:got.plt节的内容

图29:调用_dl_init之前:

图30:调用_dl_init之后:

5.8 本章小结

本章主要介绍了链接的概念及作用,并在linux下实际进行了链接操作,并对生成的hello可执行文件的ELF格式、虚拟地址空间、重定位过程、执行流程、动态链接等方面进行了详细分析。

6章 hello进程管理

6.1 进程的概念与作用    

进程是操作系统中的一个核心概念,它是指正在执行的一个程序在操作系统中的实例。具体来说,进程包括了一个程序的代码、数据、堆栈等信息,还包括了与这个程序相关的运行状态,如寄存器、打开的文件、内存分配等。

操作系统通过进程管理器来创建、启动、暂停、恢复、终止进程。每个进程都有一个唯一的进程标识符(PID),用于在操作系统中标识该进程。一个进程可以包括多个线程,每个线程可以执行不同的任务,但它们共享进程的内存和其他资源。

进程的作用非常重要。首先,进程实现了操作系统的多任务处理。多个进程可以同时运行,操作系统通过时间片轮转等算法来分配CPU时间,从而实现了多个进程交替运行的效果。其次,进程提供了一种隔离机制,使得不同程序之间的内存、数据等资源得以独立管理,不会相互干扰。此外,进程还可以实现程序的模块化,不同的进程可以专注于不同的任务,从而降低程序复杂度,提高代码可维护性。

总之,进程在操作系统中扮演着非常重要的角色,它为操作系统的多任务处理、资源管理、程序隔离等提供了功能强大的支持。

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

Shell是一种命令行解释器,它是一种特殊的程序,用于处理用户输入的命令,并将其转换为计算机能够理解的形式。其中,bash是一种Unix/Linux系统中最常用的Shell程序,它支持各种命令和操作,以及各种脚本和批处理文件。

Shell(包括bash)的主要作用是接受用户输入的命令,将其解析并执行。当用户输入一个命令时,Shell会首先将其解析为一个命令行单元,然后将其传递给相应的程序或脚本来执行。在执行过程中,Shell还会负责输入输出、错误处理、环境变量设置等方面的工作。

Shell的处理流程一般包括以下几个步骤:

1. 读取用户输入:Shell从标准输入中读取用户输入的命令行。

2. 解析命令行:Shell将命令行解析为命令和参数,并进行语法检查。

3. 查找命令:如果命令是内置命令,则Shell直接执行;否则,Shell会在系统的PATH路径中查找外部可执行程序,并执行之。

4. 执行命令:Shell根据用户输入的命令执行相应的命令或脚本,执行过程中还会处理输入输出、环境变量设置、错误处理等任务。

5. 输出结果:Shell将命令执行结果输出到标准输出,供用户查看。

在执行过程中,Shell还支持很多高级功能,如通配符扩展、管道、重定向等,这些功能可以使用户更加方便地操作系统和应用程序。

总之,Shell(如bash)是一种重要的命令行解释器,它能够帮助用户通过命令行快速、方便地操作计算机和应用程序。Shell的处理流程主要包括命令解析、命令查找、命令执行等步骤,通过这些步骤,用户可以轻松地控制计算机和应用程序的行为。

6.3 Hello的fork进程创建过程

当在shell中输入./hello这个命令时,shell会对命令进行解析,并创建一个新的进程来运行这个可执行文件。具体来说,整个过程分为以下几个步骤:

首先,shell会将命令行参数解析成对应的可执行文件路径和参数列表,即./hello可执行文件和空的参数列表。

其次,shell通过fork()函数创建一个子进程,子进程与父进程几乎相同,包括代码段、数据段、堆、共享库以及栈段等,但它们的进程ID(PID)不同。这个新的进程会继承父进程的环境变量、文件描述符等信息。

接下来,子进程调用execve()函数来加载可执行文件,并用解析出的参数列表来替换当前进程的映像,然后开始执行新的程序。

同时,父进程会使用setpgid()函数将子进程放入一个新的进程组中,这个进程组的ID与这个作业的ID相同,以便shell可以向作业中的所有进程发送信号,以管理作业。

最后,子进程开始执行新的程序,如果有需要的话,它会在运行过程中创建新的进程并放入同一个进程组中。当程序运行结束时,它会通过exit()函数退出,并向父进程发送一个退出状态码。

因此,通过创建新的进程并将其放入单独的进程组中

6.4 Hello的execve过程

当子进程被创建后,它会通过调用execve()函数来执行新的程序。具体来说,execve()函数会将新的程序加载到子进程的进程空间中,以替换原有的程序。执行execve()函数后,子进程的用户空间会被覆盖,新的程序的各个段(如代码段、数据段、堆、栈等)会被映射到相应的地址空间中,以实现新程序的执行。

值得注意的是,新的程序可能会使用一些共享库,如上文提到的ld-2.31.so等,这些共享库也会被加载到子进程的内存空间中,并通过虚拟内存映射的方式来实现。在加载共享库时,系统会使用动态链接器来解析程序中的符号引用,并将它们链接到正确的共享库中,以确保程序能够正常运行。

当所有的可执行文件和共享库都被加载到内存中之后,子进程的控制流会被直接转移到新程序的入口点,从而开始执行新的程序。这个过程中,原来的程序代码和数据已经被完全替换,因此新程序将拥有全新的用户区域,可以执行它自己的操作。

6.5 Hello的进程执行

进程的上下文是由程序状态和资源组成的,它包括程序代码、数据、寄存器、进程控制块等信息。进程的切换是由操作系统内核完成的,当时钟中断或者其他异常事件发生时,操作系统内核会暂停当前进程的执行,保存其上下文信息,然后从进程队列中选择另一个可运行的进程,恢复该进程的上下文信息,将控制权转移到该进程的代码中继续执行。

在hello程序被执行的时候,初始时正常运行,然后hello调用sleep函数,这时sleep通过syscall引发异常(陷阱),转入内核态,内核保存hello的上下文,然后将hello进程置于休眠态,切换到其它进程。等到休眠时间到了的时候,此时时钟中断使得控制流从其它进程跳到内核,内核发现hello进程的休眠时间到了,就把hello解除休眠状态。之后在应当进行进程切换的时候,恢复hello的上下文,控制流转入hello进程,处理器切换到用户态。

6.6 hello的异常与信号处理   

6.6.1 正常运行状态

图31:hello正常运行截图

6.6.2 异常处理

图32:异常处理及原因

6.6.3 发送信号

按下Ctrl+Z:进程收到 SIGSTP 信号, hello 进程挂起。用ps查看其进程PID,可以发现hello的PID是5085;再用jobs查看此时hello的后台 job号是1,调用 fg 将其调回前台。

图33:按下Ctrl Z后查看job号

图34:调用fg将其调回前台

6.6.4 Ctrl+C:

进程收到 SIGINT 信号,结束 hello。在ps中查询不到其PID,在job中也没有显示,可以看出hello已经被彻底结束。

6.6.5

中途乱按:只是将屏幕的输入缓存到缓冲区。乱码被认为是命令。

6.6.6

Kill命令:挂起的进程被终止,在ps中无法查到到其PID。

图35:用kill命令杀死进程

6.7本章小结

我深入了解了作为一个进程的hello程序在运行中所涉及的各种机制。尽管这个过程表面上看起来简单,但实际上它背后的机制是非常复杂的。hello程序在运行时,不是像我们表面上看到的那样独占CPU和内存,而是通过操作系统的进程调度机制与其他进程并发地运行。

同时,在hello程序运行时,它也会遇到一些异常和收到一些信号。这些异常或信号会导致程序的执行流程不再是线性的,表现出异步行为。这些异常和信号的处理,实现了操作系统中的控制流转移和消息传递,是操作系统中非常重要的机制。

总之,进程的并发执行和异步事件的处理都是操作系统中不可或缺的机制。它们为操作系统的可靠性、安全性和性能提供了保障。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址是程序直接使用的地址,通常表示为“段:偏移地址”的格式,由一个段选择子和段内的偏移地址组成。段选择子一般存储在段寄存器中。逻辑地址是程序内部使用的地址,用于在程序中寻找代码和数据。

线性地址(也称为虚拟地址)是虚拟内存空间中的地址,它对应着虚拟内存空间中的代码或数据,通常表示为一个64位整数。线性地址是由操作系统提供的,用于将虚拟内存空间映射到物理内存空间。线性地址提供了一种在不同程序之间共享内存的方式,同时也提供了一种保护机制,以防止程序越界访问内存。

物理地址是真正的内存地址,它指的是CPU可以直接将物理地址传送到与内存相连的地址信号线上,并对内存中的数据进行访问。物理地址决定了数据在内存中真正存储的位置。物理地址通常由内存控制器管理,并用于从内存中读取或写入数据。

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

在Intel平台下,逻辑地址是由选择符和偏移量组成的,通常表示为selector:offset的形式。选择符存储在CS寄存器中,偏移量存储在EIP寄存器中。为了将逻辑地址转换为线性地址,需要通过选择符从全局描述符表(GDT)或局部描述符表(LDT)中获取段描述符,然后将其基址与偏移量相加。这个过程称为段式内存管理。

段式内存管理中,一个逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段,可以通过它在GDT或LDT中查找到对应的段描述符,进而获取段的基址。通过将段基址与段内偏移量相加,可以得到线性地址。

总之,在Intel平台下,段式内存管理是将逻辑地址转换为线性地址的重要机制。它允许程序共享内存,并提供了保护机制来防止程序越界访问内存。

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

虚拟地址空间会被分为若干页,即分页机制。CPU对于一个线性地址会取它的高若干位,通过它们去存储在内存中的页表里查询对应的页表条目,得到这个线性地址对应的物理页起始地址,然后与线性地址的低位(页中的偏移)相加就是物理地址。

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

在为了节省页表的空间,CPU普遍使用多级页表机制,其中上一级页表中的条目指向下一级页表。在一般的x86-64模式下,CPU采用四级页表,将线性地址划分为五个部分,前四个部分分别用作该级页表的索引,最低的12位用作页内偏移地址。通过逐级查找,CPU可以找到对应物理页的页表条目(PTE),从而得到物理地址。

此外,为了优化对页表的查找效率,CPU提供了专门用于页表的缓存TLB(Translation Lookaside Buffer),它可以将页表的PTE缓存在TLB中,以减少对内存的访问。这样,CPU可以在查找页表时更快地访问已缓存的PTE,从而提高系统性能。

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

为了提高CPU对内存的访问效率,CPU提供了三级Cache缓存来暂存之前访问的内存块,以减少对物理内存的访问。当CPU需要访问一个物理地址时,先查找L1 Cache中是否存在对应内存块,如果存在则直接访问,否则继续查找L2 Cache和L3 Cache。如果三级Cache中都没有对应内存块,则CPU将直接访问物理内存,并将该内存块加载到L3、L2、L1 Cache中,并使用最近最少访问策略替换掉Cache中的某个内存块。这样可以大幅提高CPU对内存的访问速度和效率。

7.6 hello进程fork时的内存映射

调用fork函数时,操作系统内核会为新进程创建各种数据结构,并为其分配一个唯一的PID。此外,操作系统内核还会为新进程创建虚拟内存,并创建当前进程的mm_struct、区域结构和页表的原样副本。在这之后,操作系统内核还会将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork函数在新进程中返回时,新进程的虚拟内存与调用fork时存在的虚拟内存刚好相同。但是,当这两个进程中的任何一个进程进行写操作时,写时复制机制就会创建新页面,这样可以保证两个进程之间的内存空间彼此独立。

7.7 hello进程execve时的内存映射

在新进程执行execve加载可执行文件hello时,操作系统内核会先删除当前进程虚拟内存空间中的用户部分已存在的区域结构,然后为新程序hello的代码、数据、bss段和栈段区域创建新的区域结构,这些新的区域都是私有且写时复制的。具体来说,代码和数据区域会被映射为hello可执行文件中的.text和.data节,bss区域请求二进制0故映射到匿名文件,栈和堆则被初始化为空。

接下来,execve函数会将hello链接的动态链接库(共享对象)映射到虚拟地址空间的共享区域内。最后,execve函数会跳转到hello的入口点,让进程开始执行hello可执行文件中的代码。

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

当CPU执行某条指令的内存访问时,如果页表中的PTE表明这个地址对应的页不在物理内存中,就会引发缺页故障。此时,CPU将跳转到内核态,执行操作系统提供的缺页中断处理程序,该程序会使用一定的替换策略,将存在于磁盘上的页加载到物理内存中,并更新页表。处理完缺页故障后,CPU会重新执行之前的指令。

7.9动态存储分配管理

动态内存分配是指在程序运行期间,通过动态申请内存来满足程序运行时的需要。计算机中动态内存的分配管理通常由操作系统的内存管理模块负责。常见的动态内存分配管理方式有以下几种:

1. 堆管理:堆是指进程中未被使用的内存空间,堆管理就是通过在堆中分配和回收内存空间来管理动态内存。常见的堆管理算法有最先适配算法、最优适配算法和最差适配算法等。

2. 引用计数:引用计数是通过计数来管理动态内存的一种方式,即在每个动态内存块中记录该内存块被引用的次数。当引用计数为0时,该内存块就可以被回收。

3. 标记-清除:标记-清除是一种基于垃圾回收的动态内存分配管理方式。首先通过标记算法标记出所有仍然被引用的内存块,然后将未被标记的内存块从内存中清除,以释放内存。

4. 分代回收:分代回收是一种基于垃圾回收的动态内存分配管理方式,它将动态内存分为不同的代,每一代内存块的生命周期不同,对于存活时间较短的内存块,可以采用更加快速和频繁的回收策略,提高内存的管理效率。

7.10本章小结

通过上述分析,我们揭示了hello内存访问背后的复杂机制,特别是基于页式管理的虚拟内存机制的重要性。你指出,访问某个地址的数据需要经历逻辑地址到线性地址的转换、线性地址到物理地址的转换、TLB与Cache、缺页故障等机制。同时,虚拟内存空间使得程序在表面上独占整个内存。

                    结论   

可以发现Hello程序的生命周期虽然简单,但它展现了每个C语言程序执行前必须经历的过程。这包括了预处理、编译、汇编、链接、生成子进程、Execve加载、I/O设备等步骤,在这些步骤中,每个程序都需要能够输出准确的结果,因此,在计算机系统的设计与实现中,准确性是必须满足的前提。此外,在这个过程中,还可以通过一些优化,如cache、流水线、超标量等设计来提高程序的执行效率。通过完成这个大作业,我不仅回顾了这学期的学习内容,还对计算机系统的设计与实现有了更深刻的认识。

 
附件

列出所有的中间产物的文件名,并予以说明起作用。

①原始代码hello.c。

②预处理后的代码hello.i。

③编译后的汇编语言代码hello.s。

④可重定位目标文件hello.o。

⑤hello.o的objdump结果hello_o_disasm.txt。

⑥可执行文件hello。

⑦hello的objdump结果hello_disasm.txt。

参考文献

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值