ICS大作业程序人生

打印 上一主题 下一主题

主题 809|帖子 809|积分 2437




计算机系统


大作业



题     目  程序人生-Hellos P2P 
专       业      网络空间安全                  
学     号       2022112241                 
班   级        2203901                
学       生         孙浩馨         
指 导 教 师        史先俊             






计算机科学与技能学院

20245

摘  要

本文围绕hello.c程序而睁开,研究了这一最基本c语言代码在Linux系统下的P2P,020的生命周期,对hello.c文件颠末计算机的预处理惩罚、编译、汇编、链接、加载运行、执行、访问内存、动态申请内存、信号处理惩罚、停止与回收的全过程,从而相识hello.c的完整一生。通过对hello.c的研究,对计算机系统课程的体系进行了回首与串联,实现了对计算机体系的深入明白。

关键词:计算机系统;  hello.c;计算机体系布局   ;程序生命周期   ;                    









目  录


第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理惩罚

2.1 预处理惩罚的概念与作用

2.2在Ubuntu下预处理惩罚的命令

2.3 Hello的预处理惩罚结果分析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果分析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果分析

4.5 本章小结

5链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的假造地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

6hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理惩罚

6.7本章小结

7hello的存储管理

7.1 hello的存储器地址空间

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

8hello的IO管理

8.1 Linux的IO装备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献



第1章 概述

1.1 Hello简介




      • P2P
        P2P即 From Program to Progress,从程序到进程。当hello被输入到计算机中,会被存储为hello.c的源代码文件。hello.c源代码颠末cpp预处理惩罚后,办理了包罗头文件引用等问题,生成了文本文件hello.i。hello.i通过cc1编译生成了汇编语言文件hello.s。hello.s颠末as汇编将代码转换为了可执行的呆板码,再通过ld将编译后的目标文件与系统库连接,形成可执行文件hello。
        进程管理中,操作系统(OS)通过fork()系统调用为hello创建了一个新的进程。OS调用execve()加载了hello的可执行文件,并开始执行。内存管理单位(MMU)和操作系统为hello提供了假造地址到物理地址的映射,以及内存管理的支持,包罗TLB,页表等。IO管理和信号处理惩罚答应hello与外部装备进行交互。hello使用CPU、RAM和IO资源,在计算机硬件上运行,并通过OS提供的各种服务实现其功能。
      • 020
        020即From Zero to Zero,从“零”到“零”。hello在被创造出来时,是一张白纸,即“From Zero”。当hello颠末了P2P的过程后,成为了一个完整的程序,在程序执行完成后,hello进程被回收,与hello相干的所有状态信息与数据被清除,即“to Zero”。


1.2 环境与工具


1.2.1 硬件环境
CPU:11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz   2.30 GHz
RAM:16GB
1.2.2 软件环境
Windows11 64位
VMware Workstation Pro 17.5.2
Ubuntu 22.04.3
1.2.3 调试工具
Visual Studio 2022
cpp
gcc
as
ld
readelf
gdb
edb
1.3 中间结果

文件名

作用

hello.i
hello.c预处理惩罚后得到的文本文件
hello.s
hello.i编译后得到的汇编代码
hello.o
hello.s汇编得到的可重定位目标文件
helloo.elf
readelf读取hello.o得到的文本
helloo.asm
objdump反汇编hello.o得到的反汇编文件
hello
hello.o链接后得到的可执行文件
hello.elf
readelf读取hello得到的文本
hello.asm
objdump反汇编hello得到的反汇编文件

1.4 本章小结

本章起首扼要解释了P2P,020的含义,然后列出了本文研究所使用的软硬件环境,最后列出了研究中所生成的中间结果文件以及其作用。



第2章 预处理惩罚

2.1 预处理惩罚的概念与作用

预处理惩罚(Preprocessing)是编译过程中的第一个阶段,在源代码被编译之前执行。它的主要作用是对源代码进行一些文本上的处理惩罚,以预备后续的编译工作。预处理惩罚器会扫描源代码文件,并根据预界说的规则执行以下一系列操作:

宏更换(Macro substitution): 处理惩罚源代码中的宏界说,将宏名称更换为其对应的值,使代码更加易读并淘汰代码的重复性。

文件包罗(File inclusion): 处理惩罚源代码中的文件包罗指令(如#include),将指定的文件内容插入到当前文件中,进步代码的可维护性和重复性。

条件编译(Conditional compilation): 根据条件判断指令(如#ifdef、#ifndef、#if、#elif、#else和#endif)来选择性地包罗或排除部门代码,根据差别的编译选项或环境控制代码的编译举动。

行连接(Line splicing): 将跨越多行的代码连接成一行,增长代码的可读性。

解释删除(Comment stripping): 删除源代码中的解释,淘汰编译后的文件大小。

其他预处理惩罚指令处理惩罚: 处理惩罚其他预处理惩罚指令,如#error、#pragma等。

预处理惩罚完成后,生成的输出通常是另一个颠末预处理惩罚后的源代码文件,其中已经包罗了预处理惩罚器所执行的所有操作。这个颠末预处理惩罚的文件将作为编译器的输入,进行后续的编译工作。






2.2在Ubuntu下预处理惩罚的命令

在Ubuntu下可以通过cpp hello.c > hello.i
来预处理惩罚hello.c,得到hello.i文本。
预处理惩罚执行命令截图如下:

图2-1 预处理惩罚命令截图

2.3 Hello的预处理惩罚结果分析

hello.c源文件只含有如下的24行代码。

图2-2 hello.c文件截图

颠末预处理惩罚后得到hello.i。

图2-3 hello.i文件截图

颠末预处理惩罚后,代码扩展至3061行。
1——7行代码中是源代码文件中相干信息,14——3047举动预处理惩罚扩展的内容。hello.i中3048——3061行代码对应hello.c中11——24行主函数代码。

图2-4 hello.i 1-7行代码截图

14——58举动部门文件包罗信息。


图2-5 文件包罗信息截图

61——94举动部门范例界说信息


图2-6 范例界说信息

329——378举动部门函数声明信息。


图2-7 函数声明信息

14——3047行代码为hello.c中引用的头文件stdio.h,unistd.h,stdlib.h的睁开。

以stdio.h叙述睁开的具体流程如下:

stdio.h为标准库文件,当被引用时,原有的#inlcude <stdio.h>会被删除,cpp会在系统默认的环境变量中探求stdio.h,找到其路径为/usr/include/stdio.h后打开stdio.h,用stdio.h更换被删除掉的#include<stido.h>段,并对stdio.h中的#include,#define段代码递归执行上述操作直至所有#include,#define全部被更换。同时,cpp还会将代码中的解释与空白字符部门进行删除,并对一些量进行更换。


图2-8 默认的环境变量



2.4 本章小结

本章主要对预处理惩罚部门进行了阐明,先容了预处理惩罚的概念以及作用,展示了对hello.c文件进行预处理惩罚的过程,并对得到的hello.i文件进行了分析。


第3章 编译


3.1 编译的概念与作用

编译(Compilation)是计算机程序开发中的重要步调,它将高级编程语言(如C、C++、Java等)编写的源代码转换为低级的呆板语言或汇编语言程序。在这个过程中,编译器饰演着关键的脚色。

编译的主要作用如下:

语法分析与词法分析:编译器起首对颠末预处理惩罚的源代码进行词法分析和语法分析。词法分析器将源代码分解成一个个的词法单位(tokens),而语法分析器则将这些词法单位组合成语法布局,以验证语法的精确性。

语义分析:编译器进行语义分析,检查源代码中的语义错误,并生成相应的错误信息。

优化:在编译过程中,编译器大概会对源代码进行优化,以进步程序的性能和服从。这包罗控制流优化、数据流优化、指令级优化等。

代码生成: 颠末语法和语义分析后,编译器将源代码转换成等价的呆板语言或汇编语言程序。这个阶段的输出可以是目标呆板的汇编代码或是中间表现(Intermediate Representation),它们可以被进一步转换成目标呆板的呆板码。

生成汇编语言程序: 在某些编译器中,编译器大概会直接生成汇编语言程序,而不是目标呆板的呆板码。生成汇编语言程序的过程是将高级语言的源代码转换成汇编语言的等效表现,其中每一条指令都对应着目标呆板上的一条指令。

编译的结果是一个与源代码等价的、可执行的程序,它可以被计算机硬件直接执行,大概通过进一步的链接和加载过程转换成可执行文件。总的来说,编译是将高级语言的源代码转换成呆板语言或汇编语言的过程,使得计算机可以明白和执行程序。

        

3.2 在Ubuntu下编译的命令

在Ubuntu下通过gcc -S -o hello.i hello.s将hello.i编译成hello.s。


 

3.3 Hello的编译结果分析


3.3.1 文件布局
内容
含义
.file
源文件
.text
代码段
.globl
全局变量
.data
存放已经初始化的全局和静态C变量
.section .rodata
存放只读变量
.align
对齐方式
.type
表现是函数范例/对象范例
.size
表现大小
.long
表现是long范例
.string
表现是string范例



3.3.2 数据范例
(1)常量数据
a)printf中的输出,格式字符串都被存放在.rodata中
hello.c源程序中代码:



hello.s汇编程序中代码:



b)if条件判断值、for循环停止条件值在.text段

hello.c源程序中代码:



hello.s汇编程序中代码:




  • 变量数据

  • 全局变量:程序中无全局变量
  • 局部变量:局部变量i(4字节int型)在运行时生存在栈中,使用一条movl指令进行赋值,使用一条addl指令进行增一。
hello.c源程序中代码:


hello.s汇编程序中代码:



局部变量i在赋初值后被生存在地址为%rbp-4的栈位置上。
3.3.3 算数操作
for循环体中,对循环变量i的更新使用了++自增运算,汇编代码翻译成addl指令(4字节int型对应后缀“l”):
hello.c源程序中代码:

hello.s汇编程序中代码:


3.3.4 关系操作与控制转移
(1)if条件判断
hello.c源程序中代码:

hello.s汇编程序中代码:


je使用cmpl设置的条件码(ZF),若ZF = 0,阐明argc等于5,条件不 成立,控制转移至.L2(for循环部门,程序主体功能);若ZF = 1,阐明argc 不等于5(即执行程序时传入的参数个数不符合要求),继承执行输出提示信 息并退出。
(2)for循环条件停止
hello.c源程序中代码:

hello.s汇编程序中代码:


此处jle使用cmpl设置的条件码(ZF SF OF),若(SF^OF) | ZF = 1,阐明循环停止条件不成立(变量i的值小于或等于9),控制转移至.L4,继承执行循环体;若(SF^OF) | ZF = 0,则循环停止条件成立(变量i的值达到10),不再跳转至循环体开始位置,继承向后执行直至退出。
值得留意的是,源程序代码的逻辑与编译器翻译生成的逻辑有细微的差别。源代码中判断i<10,而编译器将其调整为判断i<=9,但实际上二者等价。

3.3.5 数组、指针、布局操作
数组*argv[]为参数字符串数组指针,在数组*argv[]中,argv[0]为输入程序的路径和名称字符串起始位置,argv[1]、argv[2]、argv[3]依次为后面三个参数字符串的起始位置。
hello.s汇编程序中代码:

将main()的第二个参数从寄存器写到了栈空间中。

从栈上取这一参数,并按照基址-变址寻址法访问argv[1]、argv[2]、argv[3](由于指针char*大小为8字节,分别偏移8、16、24字节来访问)。
3.3.6 函数操作
hello.c源文件中含有main主函数、printf()函数、exit()函数、sleep()函数、atoi()函数。

  • main函数
a) 参数传递:


汇编代码:

在写入栈空间时,第一个参数通过寄存器EDI传递,第二个参数通过寄存器RSI传递。
b) 函数调用

此处对程序入口进行标记。
c) 函数返回
参数不精确返回值为1,调用exit()函数。

参数精确返回0。



  • printf()函数
a) 参数传递:需要输出的字符串
源代码:


汇编代码:


b) 函数调用:由主函数通过call调用。
c) 函数返回:返回输出的字符数量。


  • exit()函数
a) 参数传递:退出状态值1


b) 函数调用:由主函数通过call调用。
c) 函数返回:无返回值,正常停止程序,返回一个状态代码给调用程序或操作系统

  • sleep()函数

  • 参数传递:
源代码:

汇编代码:


  • 函数调用:由主函数通过call调用。
  • 函数返回:返回实际休眠时间。

  • atoi()函数

  • 参数传递:
源代码:

汇编代码:


  • 函数调用:由主函数通过call调用。
  • 函数返回:参数字符串被转换后的整数值。

  • getchar()函数

  • 参数传递:
源代码:

汇编代码:


  • 函数调用:由主函数通过call调用。
  • 函数返回:返回char范例值。


3.4 本章小结

本章由对编译进行扼要的先容开始,然后展示了编译的过程,后又对编译的结果进行了渐渐的分析。

第4章 汇编


4.1 汇编的概念与作用


汇编(Assembly)是将汇编语言程序转换成呆板语言二进制程序的过程。在这个过程中,汇编器(Assembler)饰演着关键的脚色。

汇编的主要作用包罗:

转换为呆板语言: 汇编器将汇编语言程序翻译成对应的呆板语言指令序列,这些指令直接由计算机硬件执行。

生成目标文件: 汇编器生成的输出通常是目标文件,其中包罗了转换后的呆板语言指令。目标文件通常是二进制格式的文件,可以被进一步处理惩罚。

符号分析和地址计算: 汇编器负责分析程序中的符号(如标签、变量名等),并计算它们的地址。这是为了确保生成的呆板代码能够精确地引用和访问这些符号所代表的内存位置。

代码和数据段分配: 汇编器将程序中的代码段(包罗可执行指令)和数据段(包罗静态数据)分配到内存中的差别区域,并生成相应的指令和数据的存储地址。

处理惩罚伪指令和指令格式: 汇编器处理惩罚汇编语言中的伪指令(如宏指令、伪操作符等)以及指令格式(如操作码、操作数等),将它们转换成对应的呆板语言表现。

生成可链接的目标文件: 汇编器生成的目标文件通常是可链接的,可以被链接器进一步处理惩罚,与其他目标文件进行链接,形成最终的可执行文件。

总的来说,汇编的作用是将汇编语言程序转换成呆板语言二进制程序,使得计算机可以直接执行。它是编译过程中的一个重要阶段,负责将高级语言或汇编语言转换成呆板可执行的指令序列。

4.2 在Ubuntu下汇编的命令

通过gcc -c hello.s -o hello.o将hello.s汇编成hello.o。

4.3 可重定位目标elf格式

通过readelf -a hello.o>helloo.elf得到hello.o的elf格式:

4.3.1 elf头:
以 16字节序列 Magic 开始,其描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部门包罗帮助链接器语法分析息争释目标文件的信息,其中包罗 ELF 头大小、目标文件范例、呆板范例、节头部表的文件偏移,以及节头部表中条目标大小和数量等相干信息。

4.3.2 节头:

包罗了文件中出现的各个节的意义,包罗节的范例、位置和大小等信息。

4.3.3 重定位节:
重定位节记录了各段引用的符号相干信息,在链接时,需要通过重定位节对这些位置的地址进行重定位。链接器会通过重定位条目标范例判断怎样计算地址值并使用偏移量等信息计算出精确的地址。本程序需要重定位的符号有:.rodata,puts,exit,printf,atoi,sleep,getchar及.text。留意到重定位范例仅有R_X86_64_PC32(PC相对寻址)和R_X86_64_PLT32(使用PLT表寻址)两种,而未出现R_X86_64_32(绝对寻址)。

4.3.4 符号表
符号表中生存着定位、重定位程序中符号界说和引用的信息,所有重定位需要引用的符号都在其中声明。

4.4 Hello.o的结果分析

通过objdump -d -r hello.o > helloo.asm 得到hello.o的反汇编,并与第3章的 hello.s进行对照分析。

4.4.1 分支转移:
在 hello.s 中,分支跳转的目标位置是通过 .L1、.L2 这样的助记符来实现的,而 hello.o中,跳转的目标位置是具体的数值。但留意这个数值还不是具体的一个地址,因为此时还没进行链接,它是通过重定位条目进行计算得来的,是一个相对的地址值,由于差别文件代码链接合并和,一个文件自己的代码的相对地址不会改变,所以不需要与外部重定位,而可以直接计算出具体的数值,因此这里就已经完成了所有的操作,这条语句将以这种情势加载到内存中被cpu读取与执行。                        

4.4.2 函数调用:
在hello.s中,用call指令进行调用函数时,总会在call指令后直接加上函数名,而通过.o文件反汇编得到的汇编代码中,call指令后会跟着的是函数通过重定位条目指引的信息,由于调用的这些函数都是未在当前文件中界说的,所以肯定要与外部链接才气够执行。在链接时,链接器将依靠这些重定位条目对相应的值进行修改,以包管每一条语句都能够跳转到精确的运行时位置。

4.4.3 操作数:
反汇编代码中的立即数是十六进制数,而 hello.s 文件中的数是十进制的。寄存器寻址两者类似。内存引用 hello.s 中会用伪指令(如.LC0)代替,而反汇编则是基址加偏移量寻址:0x0(%rip)。

4.5 本章小结

本章起首扼要先容了汇编的概念与作用,并展示了汇编的具体过程,并对汇编后的文件进行分析,先容了可重定位文件的格式以及各部门的作用,最后对hello.o的结果进行分析,对hello.o文件和hello.s差别进行辨析。

5链接


5.1 链接的概念与作用

链接(Linking)是指将多个目标文件或库文件链接在一起,形成一个可执行文件或共享目标文件的过程。在这个过程中,链接器(Linker)会分析各个目标文件之间的符号引用和界说关系,然后将它们合并成一个团体,最终生成一个可执行文件或共享目标文件。过程包罗分析符号、办理符号引用、地址重定位等步调,最终生成一个完整的可执行文件,其中包罗了程序的所有代码和数据,以便在计算机上执行。

链接的作用包罗:

符号分析:链接器通过符号表分析各个目标文件中使用的符号,并将其与相应的界说进行关联。这包罗全局变量、函数、外部库函数等。

符号重定位:在链接过程中,链接器会调整各个目标文件中的符号地址,以便将它们精确地映射到最终的内存地址上。

库链接:链接器还负责将程序所需的库文件链接到程序中,以便在运行时能够精确地调用库中的函数和符号。

5.2 在Ubuntu下链接的命令

通过ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o hello.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -o hello命令得到hello


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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包罗各段的起始地址,大小等信息。
通过readelf -a hello>hello.elf得到hello.elf。

5.3.1 elf头

5.3.2 节头

5.3.3 程序头

5.3.4 段节

5.3.5 动态section

5.3.6 重定位节

5.3.7 符号表

5.4 hello的假造地址空间

用EDB查看hello的假造地址 。


假造地址范围为0x401000至0x402000。


通过symbol一一对照,得到假造地址与节头部表的对应关系。

5.5 链接的重定位过程分析

通过objdump -d -r hello > hello.asm得到反汇编文件hello.asm。

差别之处:

  • hello.o的反汇编中只含有.text节,而hello的反汇编中另有.init,.plt,.plt.sec。
  • 在hello中链接加入了exit、printf、sleep、getchar等在hello.c中用到的库函数。
  • hello中不再存在hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了假造内存地址。


5.6 hello的执行流程

以下格式自行编排,编辑时删除
使用gdb/edb执行hello,阐明从加载hello到_start,到call main,以及程序停止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
使用EDB开始执行hello。


根据symbol阐明程序执行的函数以及其地址。

函数名

函数地址

.init

0x401000

.plt

0x401020

puts@plt

0x401030

printf@plt

0x401040

getchar@plt

0x401050

atoi@plt

0x401060

exit@plt

0x401070

sleep@plt

0x401080

_start

0x4010f0

main

0x401125

.fini

0x401248


5.7 Hello的动态链接分析

   编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理惩罚,为制止运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,在GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数,在加载时,动态链接器会重定位GOT中的每个条目,使得它包罗目标的精确的绝对地址。



调用dl_init之前.got.plt段的内容:

调用dl_init之后.got.plt段的内容:

5.8 本章小结

本章起首先容了链接的概念与作用,然后对文件进行链接,并对可执行文件hello的格式进行了分析,然后查看了hello的假造地址,并对链接的重定位过程进行了分析,概述了hello的执行流程,并对hello的动态链接进行了分析。


6hello进程管理


6.1 进程的概念与作用

进程是程序执行的一个实例。一个程序在运行时会被操作系统加载到内存中,并分配一个独立的执行环境,这个执行环境就是一个进程。进程是操作系统进行资源分配和调度的基本单位,它具有以下特点和作用:

独立性: 每个进程都拥有独立的地址空间,使得它们相互之间不会相互干扰。进程之间通常是隔离的,一个进程的崩溃不会影响其他进程的正常运行。

并发执行: 操作系统可以同时运行多个进程,每个进程都在自己的执行环境中独立执行。这种并发执行的方式使得计算机系统可以更有用地使用多核处理惩罚器和其他硬件资源。

资源分配: 操作系统为每个进程分配了肯定的系统资源,包罗内存空间、CPU时间、文件描述符等。进程可以通过操作系统提供的接口来请求和开释这些资源。

调度和管理: 操作系统负责对进程进行调度和管理,以确保系统资源被合理地分配和使用。这包罗进程的创建、销毁、挂起、规复以及切换等操作。

通讯与同步: 进程之间可以通过各种机制进行通讯和同步,包罗共享内存、消息队列、管道、信号量、锁等。这使得差别进程之间可以进行数据交换和协作,实现复杂的任务分解和并发处理惩罚。

程序执行环境: 每个进程都有自己的程序执行环境,包罗代码、数据、堆栈、寄存器状态等。操作系统负责管理这些执行环境,并在需要时进行切换和调度。


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

壳是用户与操作系统内核之间的接口,它吸收用户输入的命令并将其转换成操作系统内核能够明白和执行的指令。

壳的主要作用是将我们的指令翻译给OS内核,让内核来进行处理惩罚,并把处理惩罚的结果反馈给用户。(Windows下的壳程序就是图形化界面)shell的存在使得用户不会直接操作OS,包管了OS的安全性。

壳的处理惩罚流程通常包罗以下步调: 壳从标准输入(通常是终端)读取用户输入的命令,对用户输入的命令进行分析,分析命令的布局和含义。根据分析后的命令调用相应的系统程序或应用程序进行执行。如果是内建命令(如cd、echo等),则直接在壳内部执行。根据命令中的I/O重定向符号(如<、>、|等)对输入输出进行重定向处理惩罚。 如果命令中包罗管道符号(|),壳将多个命令连接起来,形成管道,将前一个命令的输出作为后一个命令的输入。检测并处理惩罚命令执行过程中大概出现的错误,并将错误信息输出给用户。等待用户输入命令并执行,直到用户退出。


6.3 Hello的fork进程创建过程

输入命令后,shell会判断该命令不是内部指令,转而通过fork函数创建一个子进程hello。hello会得到一份包罗数据段、代码、共享库、堆、用户栈等均与父进程类似且独立的副本。同时子进程还会获得与父进程打开任何文件描述符类似的副本,这表现当父进程调用fork时子进程可以读写父进程的内容。父进程和子进程只有PID差别,在父进程中,fork返回子进程的PID,在子进程中,fork返回0.
6.4 Hello的execve过程

在fork创建子进程后,execve函数会加载并运行可执行目标文件hello,删除当前进程假造地址的用户部门中的已存在的区域布局。然后为hello程序的代码、数据、bss和栈区域创建新的私有的、写时复制的数据布局,然后将标准库libc.so映射到用户假造地址空间中的共享区域内。最后设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点,即设置PC指向_start地址。
6.5 Hello的进程执行

6.5.1 上下文
内核重新启动一个被抢占的进程所需要规复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据布局等对象的值构成。
6.5.2 进程上下文切换:
在内核调度了一个新的进程运行时,它就抢占当前进程,并使用一种上下文切换的机制来控制转移到新的进程。具体过程为:①生存当前进程的上下文;②规复某个先前被抢占的进程被生存的上下文;③将控制传递给这个新规复的进程。
6.5.3 进程时间片
一个进程执行它的控制流的一部门的每一个时间段叫做时间片(time slice),多任务也叫时间分片(time slicing)。
6.5.4 进程调度:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这种决议称为调度,是由内核中的调度器代码处理惩罚的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
hello程序调用sleep函数休眠时,内核将通过进程调度进行上下文切换,将控制转移到其他进程。当hello程序休眠竣事后,进程调度使hello程序重新抢占内核,继承执行。
6.5.5 用户态与核心态的转换:
为了包管系统安全,需要限制应用程序所能访问的地址空间范围。因而存在用户态与核心态的划分,核心态拥有最高的访问权限,而用户态的访问权限会受到一些限制。处理惩罚器使用一个寄存器作为模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他环境下始终处于用户权限之中,肯定程度上包管了系统的安全性。
6.5.6 hello程序进程执行表现图:

6.6 hello的异常与信号处理惩罚

6.6.1 程序正常执行
程序正常运行时,循环输出十次消息即停止输出,并竣事程序回收进程。

6.6.2 不绝乱按
当程序运行时不绝乱按,程序仍能正常输出十次信息,且程序竣事后输入的信息被看成命令输入。

6.6.3 输入回车
输入回车后程序仍能正常运行,正常输出十次信息。

6.6.4 输入ctrl+c
输入后shell进程收到SIGINT信号,程序直接停止运行,并回收hello进程。

6.6.5 输入ctrl+z
输入后shell进程收到SIGSTP信号,程序直接停止运行,并挂起hello进程。

通过ps命令查看证明hello简直不是被回收而是被挂起,且其job代号为1。

输入pstree命令,以树状图表现所有的进程。

再输入kill命令,则可以将该挂起的命令杀死。

使用kill命令前

使用kill命令后

证明该挂起的hello进程已被杀死。
6.7本章小结

本章起首简述了进程的概念与作用,然后概述了壳的作用与处理惩罚流程,并对hello的fork进程创建过程以及hello的execve过程进行了先容,然后对hello的进程执行进行了阐明,最后分析了hello的异常与信号处理惩罚。

7hello的存储管理


7.1 hello的存储器地址空间

逻辑地址(Logical Address):逻辑地址是指程序中使用的地址空间,是相对于进程而言的。它是在程序执行过程中使用的抽象地址,通常由程序员或操作系统内核管理,不直接对应到实际的物理内存位置。逻辑地址是在程序运行时生成的,用于访问进程的假造地址空间,而不考虑该地址在物理内存中的具体位置。

线性地址(Linear Address):线性地址是指逻辑地址颠末分页机制或段页式内存管理机制转换后得到的地址,也称为假造地址(Virtual Address)。在分页系统中,逻辑地址被分为页号和页内偏移量,通过页表将页号转换为物理页框号,然后加上页内偏移量得到线性地址。在段页式系统中,逻辑地址被分为段选择符和段内偏移量,通过段描述符将段选择符转换为段基址,然后加上段内偏移量得到线性地址。

假造地址(Virtual Address):假造地址是指在程序执行时,由程序中的指针所引用的地址空间,通常是相对于进程而言的,与物理内存的实际地址无直接关系。假造地址由逻辑地址颠末内存管理单位(MMU)转换后得到的线性地址。

物理地址(Physical Address):物理地址是指内存中实际的存储单位的地址,是RAM或其他存储装备上的真实地址。物理地址是在内存访问过程中由内存管理单位(MMU)将线性地址转换为的最终地址,用于实际的数据读写操作。


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

在 Intel x86 架构中,逻辑地址到线性地址的变更涉及到段式管理机制。这个过程有以下步调:

1.逻辑地址的生成:

2.逻辑地址通常由 CPU 中的段寄存器和偏移量构成。在 x86 架构中,有四个段寄存器:CS(代码段)、DS(数据段)、SS(堆栈段)和ES(附加数据段)。偏移量表现在指定段内的位置。

3.段选择符分析:

4.CPU 使用逻辑地址中的段选择符从全局描述符表(GDT)或局部描述符表(LDT)中获取相应的段描述符。段选择符包罗了段描述符在描述符表中的索引。

5.段描述符分析:

6.从描述符表中获取到的段描述符包罗了段的基址和长度等信息。段基址是一个32位的线性地址,用于指示段在内存中的起始位置。段长度则决定了段的大小。

7.线性地址的计算:

8.将段基址与偏移量相加,得到线性地址。这个线性地址是一个32位地址,用于访问物理内存。

9.地址转换和访问:

10.当 CPU 访问内存时,将逻辑地址转换为线性地址。这个线性地址通过内存管理单位(MMU)进行转换,最终映射到物理内存中的相应位置。

这个过程中,段式管理机制答应操作系统将程序的地址空间划分为多个段,每个段可以具有差别的访问权限和大小。这样可以更灵活地管理内存,并提供更好的安全性和隔离性。

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

线性地址(VA)到物理地址(PA)的转换通过假造地址内存空间的分页机制实现。起首,从段式管理中获得线性地址,然后将其划分为假造页号(VPN)和假造页偏移量(VPO)。VPN指示了页面在假造地址空间中的位置,而VPO指示了页面内的具体偏移量。由于假造内存与物理内存的页大小类似,所以VPO与物理页偏移量(PPO)一致。

然后,需要通过访问页表中的页表条目(PTE)来获取物理页号(PPN)。如果PTE的有用位为1,则发生页掷中,可以直接获取到物理页号PPN。PPN与PPO组合成物理地址。

然而,如果PTE的有用位为0,则表现对应的假造页没有缓存在物理内存中,会触发缺页故障。在这种环境下,操作系统的内核会参与,执行缺页处理惩罚程序。该程序会确定断送页,并将新的页面调入物理内存。随后,控制返回到原始进程,并重新执行导致缺页的指令。这时候发生了页掷中,可以获取到PPN,然后与PPO组合成物理地址。


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

TLB:

每当CPU生成一个假造地址时,MMU需要检索相干的PTE以将其转换为物理地址。在最糟糕的环境下,这个过程需要额外的内存访问,消耗几十到几百个周期。但如果MMU的TLB中恰好缓存了PTE,这个开销就能降低到1或2个周期。因此,许多系统都引入了TLB,它是一个小型的PTE缓存,用于淘汰内存访问的开销。

多级页表:

多级页表采用了条理布局,以淘汰对内存的需求。起首,如果一级页表中的某个PTE为空,那么对应的二级页表就不需要存在,从而节省了内存空间。其次,只有最顶层的一级页表必须不停驻留在主存中,而其他级别的页表可以根据需要在主存和辅存之间进行调度,最常使用的二级页表则会被缓存在主存中,有用减轻了主存压力。

VA到PA的转换:

对于四级页表,假造地址(VA)被划分为4个VPN和1个VPO。每个VPN i都是到第i级页表的索引。在前三级页表中,每个PTE指向下一级的页表基址。而在最底层的页表中,每个PTE则包罗某个物理页面的PPN,大概指向磁盘块的地址。在构建物理地址之前,MMU需要访问k个PTE。与单级页表相似,PPO和VPO具有类似的值。


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

起首,根据高速缓存的组数和块大小来确定高速缓存块的偏移量(CO)、组索引(CI)和高速缓存标记(CT)。通过组索引,将数据行与高速缓存标记进行匹配。如果匹配成功且该数据行的有用位为1,则表现发生了掷中,此时可以根据偏移量取出数据并将其返回给CPU。

如果在当前级别的高速缓存中未找到匹配的数据行,大概找到的数据行的有用位为0,则表现发生了未掷中。此时,系统将会继承在下一级高速缓存(如L2)中进行类似的查找操作。如果在L2中仍旧未找到匹配项,则继承在更低一级的高速缓存(例如L3)中进行查找。如果直到三级高速缓存均未掷中,则需要访问主存来获取所需数据。

如果发生了高速缓存未掷中,意味着至少有一级高速缓存未能存储所需数据。在从更低级别的存储条理获取数据后,系统需要更新未掷中的高速缓存。起首,系统会检查是否存在空闲的高速缓存块,即有用位为0的块。如果存在空闲块,则可以直接将获取的数据写入其中。如果不存在空闲块,则需要根据更换策略(例如最近最少使用或最不经常使用)选择一个块进行更换,然后将获取的数据写入该块中。


7.6 hello进程fork时的内存映射

当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据布局,并分配给它一个唯一的PID。为了给这个新的hello创建假造内存,它创建了当前进程的mm_struct、区域布局和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域布局都标记为私有的写时复制。

当fork在新进程中返回时,新进程现在的假造内存刚好和调用fork时存在的假造内存类似。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。


7.7 hello进程execve时的内存映射

execve函数加载并运行hello需要以下几个步调:

1.删除已存在的用户区域

删除当前进程hello假造地址的用户部门中的已存在的区域布局。

2.映射私有区域

为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域布局。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包罗在hello中。栈和堆区域也是请求二进制零的,初始长度为零。

3.映射共享区域

若hello程序与共享对象或目标(如标准C库libc.so)链接,则将这些对象动态链接到hello程序,然后再映射到用户假造地址空间中的共享区域内。

4.设置程序计数器

最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。


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

缺页概念:在假造内存系统中,当DRAM缓存中未找到所需的页面时,被称为缺页。缺页是一种异常环境,属于可规复错误的范畴。相干处理惩罚流程可以在本文的第6.6节中找到。

处理惩罚流程:缺页异常触发内核中的缺页异常处理惩罚程序。该程序起首会选择一个断送页,如果该断送页已经被修改,则内核会将其写回磁盘。随后,内核会从磁盘中复制导致缺页异常的页面到内存中,并更新相应的页表项,将其指向这个新复制的页面。处理惩罚程序执行完成后,内核将重新启动导致缺页的指令。该指令将重新发送导致缺页的假造地址给地址翻译硬件,这次访问会掷中页面。

这样处理惩罚的结果是,在缺页异常处理惩罚程序的调用和重新发送缺页地址之后,系统能够成功地将缺页的页面复制到内存中,并更新页表项,以便后续的访问可以掷中所需的页面。


7.9动态存储分配管理

动态内存分配管理是通过动态内存分配器进行的。这个分配器负责管理进程的假造内存区域,也称为堆。它将堆看作是一系列差别大小的块的集合,每个块都是一段一连的假造内存片段,大概是已分配的,也大概是空闲的。已分配的块被明白生存供应用程序使用,而空闲块可以被分配。空闲块保持空闲状态,直到应用程序明白分配它们。已分配的块保持已分配状态,直到被开释,开释可以由应用程序显式执行,也可以由内存分配器隐式执行。

内存分配器有两种风格:显式和隐式。C语言中的malloc函数属于显式分配器。显式分配器必须满意一些严格的条件:处理惩罚恣意请求序列,立即相应请求,只使用堆,对齐块(对齐要求),不修改已分配的块。在这些限制条件下,分配器的目标是最大化吞吐量和内存使用率。

常见的放置策略包罗:

1.初次适配:从空闲链表的头部开始搜索,选择第一个合适的空闲块。

2.下一次适配:类似于初次适配,但是从上一次搜索竣事的地方开始。

3.最佳适配:选择所有空闲块中得当所需请求大小的最小空闲块。

有几种组织内存块的方法:

1.隐式空闲链表:空闲块通过头部的大小字段隐式连接,可以添加界限标记以进步合并空闲块的速度。

2.显式空闲链表:在隐式空闲链表的基础上,每个空闲块中都添加了一个前驱(pred)指针和一个后继(succ)指针。

3.分离的空闲链表:将块按大小分类,分配器维护一个空闲链表数组,每个大小类一个空闲链表,以淘汰分配时间并进步内存使用率。C语言中的malloc函数采用了这种方法。

4.树形布局(如红黑树等):按块大小将空闲块组织成树形布局,同样可以淘汰分配时间和进步内存使用率。


7.10本章小结

本章主要对hello的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变更、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理惩罚、动态存储分配管理进行了先容。



8hello的IO管理


8.1 Linux的IO装备管理方法

1.装备的模子化——文件

所有的I/O装备(例如网络、磁盘和终端)都被模子化为文件。

例如:/dev/sda2文件是用户磁盘分区,/dev/tty2文件是终端。

2.装备管理——Unix IO接口

将装备模子化为文件的方式答应Linux内核引入一个简单、低级的应用接口,称为Unix IO,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数


  • Unix I/O接口:
1.打开文件

一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O装备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。

2.改变当前的文件位置

对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

3.读写文件

一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增长到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

4.关闭文件

内核开释文件打开时创建的数据布局,并将这个描述符规复到可用的描述符池中去。



  • Unix I/O函数:
1.int open(char* filename,int flags,mode_t mode)

进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算怎样访问这个文件,mode参数指定了新文件的访问权限位。

2.int close(fd)

fd是需要关闭的文件的描述符,close返回操作结果。

3.ssize_t read(int fd,void *buf,size_t n)

read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表现一个错误,0表现EOF,否则返回值表现的是实际传送的字节数量。

4.ssize_t wirte(int fd,const void *buf,size_t n)

write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。


8.3 printf的实现分析

1.printf函数体:

int printf(const char *fmt, ...)

{

    int i;

    va_list arg = (va_list)((char *)(&fmt) + 4);

    i = vsprintf(buf, fmt, arg);

    write(buf, i);

    return i;

}

分析:

printf函数调用了vsprintf函数,最后通过系统调用函数write进行输出;

va_list是字符指针范例;

((char *)(&fmt) + 4)表现...中的第一个参数。

2.printf调用的vsprintf函数:

int vsprintf(char *buf, const char *fmt, va_list args)

{

    char *p;

    chartmp[256];

    va_listp_next_arg = args;

    for (p = buf; *fmt; fmt++)

    {

        if (*fmt != '%')

        {

            *p++ = *fmt;

            continue;

        }

        fmt++;

        switch (*fmt)

        {

        case 'x':

            itoa(tmp, *((int *)p_next_arg));

            strcpy(p, tmp);

            p_next_arg += 4;

            p += strlen(tmp);

            break;

        case 's':

            break;

        /* 这里应该另有一些对于

        其他格式输出的处理惩罚 */

        default:

            break;

        }

        return (p - buf);

    }

}

分析:vsprintf的作用就是格式化。它担当确定输特别式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出写入buf供系统调用write输出时使用。

3.write系统调用:

write: 

mov eax, _NR_write

mov ebx, [esp + 4]

mov ecx, [esp + 8]

int INT_VECTOR_SYS_CALL

分析:这里通过几个寄存器进行传参,随后调用中断门int INT_VECTOR_SYS_CALL

即通过系统来调用sys_call实现输出这一系统服务。

4.sys_call部门内容:

sys_call:

     /* 

      * ecx中是要打印出的元素个数

      * ebx中的是要打印的buf字符数组中的第一个元素

      * 这个函数的功能就是不停的打印出字符,直到遇到:'\0'

      * [gs:edi]对应的是0x80000h:0采用直接写显存的方法表现字符串

      */

     xor si,si

     mov ah,0Fh

     mov al,[ebx+si]

     cmp al,'\0'

     je .end

     mov [gs:edi],ax

     inc si

    loop:

     sys_call

   

    .end:

     ret 

分析:通过逐个字符直接写至显存,输特别式化的字符串。

5.最后一部门工作:

字符表现驱动子程序实现从ASCII到字模库到表现vram(即显存,存储每一个点的RGB颜色信息)。表现芯片按照刷新频率逐行读取vram,并通过信号线向液晶表现器传输每一个点(RGB分量)。


8.4 getchar的实现分析

异步异常-键盘中断的处理惩罚:键盘中断处理惩罚子程序。担当按键扫描码转成ascii码,生存到系统的键盘缓冲区。

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

getchar调用系统函数read,发送一个中断信号,内核抢占这个进程,用户输入字符串,键入回车后(字符串和回车都生存在缓冲区内),再次发送信号,内核重新调度这个进程,getchar从缓冲区读入字符。

8.5本章小结

本章主要先容了Linux的IO装备管理方法,并扼要先容了Unix IO接口及其函数。也对printf和getchar的实现进行了分析。

结论

Hello程序的一生可以描述为以下过程:

预处理惩罚阶段:Hello程序的源代码文件hello.c起首颠末预处理惩罚器处理惩罚,其中包罗将所有被包罗的外部头文件的内容直接插入到程序文本中,完成字符串的更换等操作,得到调整和睁开后的ASCII文本文件hello.i。

编译阶段:颠末编译器的编译,hello.i被转换成等价的汇编代码文件hello.s。这个过程包罗词法分析和语法分析,将源代码翻译成相应的汇编语言代码。

汇编阶段:汇编器将hello.s汇编程序翻译成呆板语言指令,并将这些指令打包成可重定位目标程序格式,最终生成可重定位目标文件hello.o。

链接阶段:链接器将hello程序的目标文件hello.o与动态链接库等网络整理,生成一个单一的文件,即完全链接的可执行目标文件hello。

加载与运行阶段:用户在shell中输入命令,shell解释这个命令并为其创建一个新进程,调用execve加载hello程序到新进程的内存空间,并开始执行。

执行指令阶段:当CPU调度到hello进程时,它分配一个时间片给hello程序,CPU按顺序执行hello程序中的指令,PC寄存器不停更新,CPU取指并执行。

访存阶段:在执行过程中,内存管理单位将逻辑地址映射成物理地址,通过三级高速缓存系统访问物理内存/磁盘中的数据。

动态申请内存阶段:如果程序中有动态内存分配的操作,比如调用printf函数,会向动态内存分配器申请堆中的内存。

信号处理惩罚阶段:程序大概会吸收到各种信号,如Ctrl-C、Ctrl-Z等,在收到这些信号时,操作系统会调用相应的信号处理惩罚函数来进行相应的操作。

停止与回收阶段:程序执行完成后,父进程(通常是shell)等待并回收hello子进程,内核删除为hello进程创建的所有数据布局,清算资源。

这些阶段的协同工作构成了Hello程序的一生。


附件

文件名

作用

hello.i
hello.c预处理惩罚后得到的文本文件
hello.s
hello.i编译后得到的汇编代码
hello.o
hello.s汇编得到的可重定位目标文件
helloo.elf
readelf读取hello.o得到的文本
helloo.asm
objdump反汇编hello.o得到的反汇编文件
hello
hello.o链接后得到的可执行文件
hello.elf
readelf读取hello得到的文本
hello.asm
objdump反汇编hello得到的反汇编文件

参考文献



[1] Randal E.Bryant, David O'Hallaron. 深入明白计算机系统[M]. 机械工业出版社.2018.4 
[2] Pianistx.printf 函数实现的深入分析[EB/OL].2013[2021-6-9].

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

[3]  ELF文件头布局. CSDN博客.
ELF文件头布局_elf32头-CSDN博客
[4]  read和write系统调用以及getchar的实现. CSDN博客.
read和write系统调用以及getchar的实现_getchar 和read-CSDN博客
[5]   GCC online documentation. GCC online documentation- GNU Project
[6]  深入明白计算机系统-之-内存寻址(三)--分段管理机制(段描述符,段选择子,描述符表). CSDN博客.
深入明白计算机系统-之-内存寻址(三)--分段管理机制(段描述符,段选择子,描述符表)_ds的段地址是固定的吗-CSDN博客

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

篮之新喜

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表