程序人生-Hello’s P2P

摘  要

本论文从预处理、编译、汇编、链接、进程、存储空间等角度,描述了hello从源文件到程序,从程序到进程,从进程开始到终止的过程。

关键词:hello;预处理;编译;汇编;反汇编;链接;进程;存储空间。                           

目  录

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

2.4 本章小结............................................................................... - 8 -

第3章 编译................................................................................... - 9 -

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

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

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

3.4 本章小结............................................................................. - 12 -

第4章 汇编................................................................................. - 13 -

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

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

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

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

4.5 本章小结............................................................................. - 17 -

第5章 链接................................................................................. - 18 -

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

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

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

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

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

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

5.7 本章小结............................................................................. - 29 -

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

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

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

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

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

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

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

6.7本章小结.............................................................................. - 36 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结............................................................................ - 42 -

结论............................................................................................... - 45 -

附件............................................................................................... - 46 -

参考文献....................................................................................... - 47 -

第1章 概述

1.1 Hello简介

1)编写代码:从零开始,程序员编写了这段hello.c源程序,定义了一个main函数,并使用C语言的标准库函数进行输入输出和控制流程。

(2)预处理:C预处理器(cpp)通过解析宏定义、文件包含、条件编译等,将2hello.c翻译为ASCII码的中间文件hello.i。

(3)编译:C编译器(cc1)将C语言翻译为汇编指令,得到ASCII汇编语言文件hello.s。

(4)汇编:汇编器(as)将汇编指令翻译成机器语言,并生成重定位信息,得到可重定位目标文件hello.o。

(5)链接:链接器(ld)经过符号解析、重定位等生成可执行目标文件hello。此时hello成为了一个程序。

(6)运行:在操作系统中输入./hello 2021112963 xushengkai 1,以运行hello,操作系统会为其创建一个进程。

(7)进程创建:shell调用fork函数为hello创建一个子进程,供hello程序的运行。

(8)加载程序:子进程中调用execve函数,将hello程序加载到进程的内存空间,进入hello的程序入口。

(9)执行代码:进程开始执行main函数中的代码。

(10)参数传递:操作系统将命令行参数传递给进程,存储在argc和argv参数中。

(11)参数校验:程序检查命令行参数的数量是否符合预期,如果不符合,则输出错误提示信息并退出。

(12)循环输出:使用一个循环,输出 "Hello 2021112963 xushengkai" 消息,循环次数为8次。

(13)休眠:在每次输出消息后,调用sleep函数使进程暂停1秒。

(14)等待用户输入:在循环结束后,程序调用getchar函数等待用户按下回车键。

(15)进程终止:程序执行完所有代码后,返回0表示成功执行,并终止进程。shell负责回收终止的hello进程,内核删除为hello进程创建的所有数据结构,至此hello回归于零。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

1.2.1 硬件环境

i7-11800H;2.30GHz;16G RAM;256GHD Disk。

1.2.2 软件环境

Window10 64位;Vmware 17;Ubuntu 22.04。

1.2.3 开发工具

CodeBlocks 64位;gcc。

1.3 中间结果

中间结果如下表:

文件名

作用

hello.c

源文件

hello.i

预处理后得到的中间文件,用于分析预处理的作用

hello.s

编译后得到的汇编指令文件,用于分析各种数据、操作

hello.o

汇编后得到的可重定位文件

hello.elf

hello.o的elf文件,用于分析各节、数据段

helloasm.txt

hello.o的反汇编文件,用于与hello.s进行对比分析

hello

链接后得到的可执行文件,可以在终端中运行

phello.elf

hello的elf文件,用于与hello.elf进行对比分析

phelloasm.txt

hello的反汇编文件,用于与helloasm.txt进行对比分析

1.4 本章小结

本章主要介绍了hello的P2P、020的过程,以及撰写本论文所使用的环境与工具和得到的中间结果。

第2章 预处理

2.1 预处理的概念与作用

预处理是指在源代码编译之前,对源代码进行一系列的预处理操作。这些预处理操作由预处理器(preprocessor)执行,预处理器是编译器的一部分。预处理的主要目的是在实际编译之前对源代码进行一些文本替换和宏展开等操作,以便为编译器提供经过处理的源代码。预处理器根据预处理指令(以#开头的指令)来执行相应的处理。

预处理主要有以下作用:

(1)宏替换:预处理器可以替换源代码中的宏定义。宏定义是使用#define指令定义的标识符常量或带参数的函数样式宏。预处理器会在源代码中找到对应的宏,并将其替换为相应的代码片段。

(2)文件包含:使用#include指令可以将其他文件的内容插入到当前源文件中。预处理器会根据指令中指定的文件路径找到对应的文件,并将其内容插入到指令所在的位置。

(3)条件编译:使用条件编译指令(如#ifdef、#ifndef、#if等)可以根据条件来选择性地编译源代码的部分内容。这使得我们可以根据不同的编译条件来编译不同的代码。

(4)符号定义:预处理器允许使用#define指令定义符号常量。这些符号常量可以在源代码中使用,并且在编译时会被替换为相应的文本。

(5)注释删除:预处理器可以删除源代码中的注释部分。注释对于程序员来说是有用的,但在编译时并不需要注释,因此预处理器可以去除这些注释,减小编译后的文件大小。

2.2在Ubuntu下预处理的命令

预处理命令cpp hello.c > hello.i 或 gcc –E hello.c –o hello.i

如下图,输入预处理命令后,文件夹中出现了hello.i文件。

2.3 Hello的预处理结果解析

打开hello.i文件,在文件的最下面发现了原始的c语言代码的main函数,上面则是各种头文件的展开,如下图:

2.4 本章小结

本章简要介绍了预处理的概念和作用,并通过执行预处理的指令和分析预处理得到的hello.i文件,得到了hello.i是在hello.c基础上进行补充和替换的文件的结论。

第3章 编译

3.1 编译的概念与作用

编译是指将预处理后的ASCII码的中间文件转换为ASCII汇编语言文件的过程。这个过程主要涉及到编译器的前端部分,包括词法分析、语法分析和语义分析等步骤。

编译的作用是将C语言翻译为更接近底层的汇编语言,使其与硬件平台相适配。此外,编译还可以进行优化、交叉编译等。

3.2 在Ubuntu下编译的命令

编译命令:cc1 hello.i –o hello.s 或 gcc –S hello.c –o hello.s

如下图,输入编译命令后,文件夹中出现了hello.s文件。

3.3 Hello的编译结果解析

3.3.1

如下图,main函数前有一些节的信息。

其中,.file表明该文件的源文件为hello.i,.text表示代码节,.section.rodata表示只读数据段,.align声明8字节对齐,.string声明了一个字符串,.globl声明全局变量为main,.type声明main的类型为函数。

3.3.2 数据

(1)字符串

程序有两个字符串存放在只读数据段中,如下图:

hello.c中的main函数的第二个参数char *argv[]为指针数组,由第20行subq $32, %rsp得数组起始地址存放于栈中-32(%rsp)的位置,且该数组被两次调用作为参数传入printf中。第25行leaq .LC0(%rip), %rax将第一个字符串的起始地址传入rax,第41行leaq .LC1(%rip), %rax将第二个字符串的起始地址传入rax

(2)参数

main函数共有两个参数。argc为main函数的第一个参数,被存放在寄存器%edi中,由第21行movl %edi, -20(%rbp)得argc被压入栈中。23行cmp $4, -20(%rbp)将argc与4进行比较。

main函数的第二个参数argv已经介绍过。

(3)局部变量

hello.c中局部变量只有i,由31行movl $0, -4(%rbp)得i被压入栈中-4(%rbp)的位置。

3.3.3 全局函数

由第10行.globe main得hello.c中只有一个全局函数main。

3.3.4 赋值操作

hello.c中的赋值操作只有循环开始时的i=0。汇编语言的实现在上文已提到,即第31行的movl $0, -4(%rbp),由于i是int类型,大小为4字节(32位),故使用movl传送双字。

3.3.5 算术操作

第52行addl $1, -4(%rbp),即循环中对i每次加1的i++操作。同理,应使用addl对双子操作。

3.3.6 关系操作

hello.c中的关系操作有argc!=4和i<8。argc!=4的汇编代码为23行cmp $4, -20(%rbp),i<8的汇编代码为第54行cmp $7, -4(%rbp)。

3.3.7 控制转移

在关系操作argc!=4和i<8后,分别都有控制转移,它们分别为24行的je .L2和55行的jle .L4。此外,在将i初始化为0后,第32行有jmp .L3,即跳转到循环中。第24行的je .L2表明只有当argc等于4时才能跳转到后面的循环中,第55行的jle .L4表明每次循环结束应判断i<8,若不成立循环结束。

3.3.8 函数操作

(1)main函数

参数传递:该函数的参数为argc、argv。

函数调用:通过使用call内部指令调用语句进行函数调用,并且将要调用的函数数据写入栈中,然后跳转到该函数的内部。main函数中调用了printf、exit、sleep函数。

局部变量:在for循环中使用了局部变量i。

(2)printf函数

参数传递:printf函数调用了参数argv[1]、argv[2]。

函数调用:该函数被调用了两次。

(3)exit函数

参数传递:第28行movl $1, %edi,将参数1存入edi。

函数调用:第29行call exit@PLT,调用了exit@PLT。

(4)atoi函数

参数传递:第48行movq %rax, %rdi,将参数argv[3]存入寄存器%rdi。

函数调用:第49行call atoi@PLT,调用atoi@PLT。

(5)sleep函数

参数传递:第50行movl %eax,%edi,将转换完的秒数参数存入寄存器%edi。

函数调用:第51行call sleep@PLT,调用了sleep@PLT。

(6)getchar函数

参数传递:无

函数调用:第56行call getchar@PLT,调用了getchar@PLT。

3.3.9 类型转换

atoi函数将字符串argv[3]转换为整型的秒数。

3.4 本章小结

本章简要介绍了预处理的概念和作用,展示了编译的命令,并重点对编译后得到的hello.s文件中的各种操作从数据、赋值操作、算术操作、关系操作、控制转移、函数操作、类型转换等方面进行了分析。

第4章 汇编

4.1 汇编的概念与作用

汇编是指把汇编指令翻译成机器语言,将ASCII汇编语言文件转换为可重定位目标文件的过程。这个过程涉及到汇编器的工作。

汇编的作用主要为生成机器代码,除此之外,汇编还有平台适配、符号解析、优化等作用。

4.2 在Ubuntu下汇编的命令

汇编命令:as hello.s –o hello.o 或 gcc –c hello.s –o hello.o

如下图,输入汇编命令后,文件夹中出现了hello.o文件。

4.3 可重定位目标elf格式

典型的ELF格式的可重定位目标文件包括ELF头、.text、.rodata、.data、.bss、.symtab、.rel.text、.rel.data、.debug、.line、.strtab、节头部表。

如下图,输入readelf -a hello.o > hello.elf后,文件夹中出现了hello.elf文件。

hello.elf文件中包括以下内容:

  1. ELF头(ELF Header)

ELF头位于文件的开头,用于描述整个ELF文件的基本属性和结构。ELF头包含了一些重要的字段,用于标识文件类型、目标体系结构、入口点地址等关键信息。hello.elf的ELF头内容如下:

正在上传…重新上传取消

  1. 节头(Section Header)

节头用于描述目标文件中各个节的属性和位置信息。节头包含了关于节的重要信息,如节的名称、类型、大小、起始地址、属性等。通过节头表,链接器和其他工具可以了解目标文件中的不同节,并进行适当的操作和处理。hello.elf的节头内容如下:

           正在上传…重新上传取消

  1. 重定位节(Relocation Section)

重定位节是ELF文件中的一种特殊节,用于存储需要进行重定位的地址和相关信息。重定位节记录了需要修改的指令或数据的位置以及如何修改的信息,以便在链接过程中进行符号解析和地址修正。重定位节包含了一个或多个重定位条目,每个条目描述了一个需要进行重定位的位置和修正方式。hello.elf的重定位节内容如下:

正在上传…重新上传取消

  1. 符号表(Symbol Table)

符号表是在可执行文件、共享库或目标文件中存储符号信息的数据结构。符号表记录了程序中定义和引用的符号(如函数名、变量名)以及与这些符号相关联的属性和地址信息。它是链接过程中重要的一部分,用于实现符号解析和地址重定位。hello.elf的符号表内容如下:

4.4 Hello.o的结果解析

如下图,输入objdump -d -r hello.o > helloasm.txt后,文件夹中出现了helloasm.txt文件。

对比hello.s和helloasm.txt,发现两者存在以下区别:

(1)helloasm.txt左侧都有一串十六进制的机器语言,如下图:

(2)helloasm.txt中的操作数均变为十六进制,如hello.s中第35行的addq $16, %rax,在helloasm.txt变为第26行的add $0x10, %rax。

(3)helloasm.txt中的跳转指令的地址表示为主函数+段内偏移格式的确定地址,如hello.s中第32行的jmp .L3,在helloasm.txt变为第24行的jmp 82<main+0x82>。

(4)helloasm.txt中的调用指令与跳转指令相似,call后为重定位条目指引信息而非函数名。如helloasm.txt调用exit@PLT的格式为:

4.5 本章小结

本章简单概括了汇编的概念和作用,展示了汇编、反汇编的命令和将.o文件转换为.elf文件的操作,并对反汇编文件和.s文件进行了对比分析。

5章 链接

5.1 链接的概念与作用

链接是指将多个可重定位目标文件合并为一个单独的可执行文件的过程,其主要目的是解析符号引用、分配内存地址并生成最终可执行文件,以便程序可以被正确加载和执行。链接的作用有解析符号引用、重定位、合并节、地址分配、生成可执行文件。

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

如下图,输入链接命令后,文件夹中出现了hello文件。

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

如下图,输入readelf -a hello.o > phello.elf后,文件夹中出现了phello.elf文件。

phello.elf文件中包含以下内容:

  1. ELF头

对比hello.elf发现hello的类型为EXEC(可执行文件),而hello.o的类型为REL(可重定位文件),hello的入口点地址为0x401090,而hello.o的入口点地址为0x0。此外,hello的节头比hello.o多,而且有程序头。

  1. 节头
  2. 程序头
  3. 段节
  4. 动态节
  5. 重定位节
  6. 符号表
  7. 版本信息
  8. 显示注释

5.4 hello的虚拟地址空间

使用edb加载hello,可以查看本进程的虚拟地址空间各段信息。

(1).init

(2).plt

(3).text

5.5 链接的重定位过程分析

如下图,输入objdump -d -r hello > phelloasm.txt后,文件夹中出现了phelloasm.txt文件。

对比helloasm.txt与phelloasm.txt,发现phelloasm.txt在main函数前面新增许多经过重定位的函数如_init、puts@plt、getchar@plt、_start。如下图:

此外,hello.o的地址从0开始,而hello的地址从401000开始,hello的地址经过重定位。而且,hello.o中跳转指令和调用指令的地址为相对main函数的相对地址,而hello中为绝对地址。

重定位由重定位节和符号定义、重定位节中的符号引用两步组成。hello.o的重定位节如下:

其中,R_X86_64_PLT32的修正过程如下:

1.符号引用的位置是一个32位的偏移量,指向PLT中对函数的调用。

2.重定位过程将该32位偏移量与目标符号的地址进行相对计算,得到一个32位相对地址。

3.该32位相对地址将被写回到符号引用的位置,以替换原始的32位偏移量。

R_X86_64_PC32的修正过程如下:

1.符号引用的位置是一个32位的偏移量,相对于当前指令的地址。

2.重定位过程将该32位偏移量与目标符号的地址进行相对计算,得到一个32位相对地址。

3.该32位相对地址将被写回到符号引用的位置,以替换原始的32位偏移量。

5.6 hello的执行流程

  1. 开始执行:_start、_libc_start_main@GLIBC_2.34。
  2. 执行main:main、puts@plt、printf@plt、exit@plt、sleep@plt、getchar@plt、atoi@plt。
  3. 终止:_exit。

程序名

程序地址

_start

0x401090

_libc_start_main@GLIBC_2.34

0x403fd8

main

0x4010c5

puts@plt

0x401030

printf@plt

0x401040

exit@plt

0x401070

sleep@plt

0x401080

getchar@plt

0x401050

atoi@plt

0x401060

5.7 本章小结

本章简单概括了链接的概念和作用,展示了链接命令,观察了hello文件ELF格式下的内容,利用edb观察了hello的虚拟地址使用情况,并对重定位过程进行了分析。

6章 hello进程管理

6.1 进程的概念与作用

进程是程序在执行过程中的实体,可以看作是一个具有独立功能的任务或工作单元。进程由操作系统创建、调度和管理,可以独立运行,相互之间是隔离的。每个进程都有自己的执行状态、指令指针、寄存器和堆栈等运行时上下文。

进程是操作系统进行任务调度和资源分配的基本单位,它使得计算机能够同时执行多个任务,并提供了隔离、通信和协作的机制,从而实现了多任务的并发执行和复杂功能的实现。进程有以下作用:

(1)并发执行:操作系统可以同时运行多个进程,通过时间片轮转等调度算法,让每个进程以看似同时运行的方式执行,从而实现多任务并发。

(2)资源分配:操作系统为每个进程分配一定的计算资源(如CPU时间片、内存、文件句柄等),确保进程能够独立运行。

(3)任务隔离:每个进程拥有独立的内存空间,防止进程间的数据相互干扰。一个进程的错误或崩溃不会影响其他进程的执行。

(4)进程间通信:进程可以通过各种进程间通信机制(如管道、共享内存、消息队列等)进行数据交换和协作,实现信息共享和协同处理。

(5)实现复杂功能:多个进程可以协同工作,实现复杂的计算和任务。例如,一个进程负责接收网络请求,另一个进程负责处理请求并返回结果,从而实现了服务器的功能。

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

Shell是一种命令行解释器,其中bash(Bourne Again Shell)是最常见和广泛使用的Shell之一。Shell提供了与操作系统内核进行交互的界面,允许用户通过命令行输入和执行命令,并与系统进行交互。

Shell-bash的作用如下:

(1)命令执行:Shell接收用户输入的命令,并将其解释和执行。用户可以通过Shell运行系统命令、执行可执行文件、运行脚本等。

(2)环境控制:Shell可以管理和设置环境变量、工作目录、别名和函数等,为用户提供一个定制化的工作环境。

(3)输入输出重定向:Shell支持将命令的输入和输出重定向到文件、管道等,实现数据的输入输出控制和重定向。

(4)脚本编写:Shell脚本是一种将多个Shell命令按照一定顺序组合成的脚本文件,可以实现自动化任务、批量处理等功能。

(5)系统管理:Shell提供了一些系统管理命令和工具,用于管理用户账户、进程、文件系统等系统级操作。

Shell-bash的处理流程如下:

(1)显示提示符:Shell通常会显示一个提示符,等待用户输入命令。

(2)读取命令:Shell读取用户输入的命令,并对其进行解析。

(3)命令解析:Shell对命令进行解析,将命令拆分成命令名和参数。

(4)命令执行:根据命令类型,Shell执行相应的操作。它可能是运行一个系统命令,调用一个可执行文件,执行一个Shell内置命令,或者运行一个Shell脚本。

(5)输出显示:执行命令后,Shell将结果显示在终端上,供用户查看。

(6)返回提示符:命令执行完毕后,Shell再次显示提示符,等待用户输入下一个命令。

该过程循环进行,用户可以根据需要输入不同的命令,Shell会根据用户输入解析并执行相应的操作。通过Shell,用户可以与操作系统进行交互,并利用各种命令和功能完成任务。

6.3 Hello的fork进程创建过程

用户在shell界面输入指令:./hello 2021112963 xushengkai 1,shell判断该指令不是内置命令,于是父进程调用fork函数创建一个新的子进程,子进程得到与父进程用户级虚拟地址空间相同的副本,但拥有不同的PID。此外,在父进程中fork返回子进程的PID,在子进程中fork返回0,可以利用该返回值判断程序是父进程或子进程。

6.4 Hello的execve过程

execve的功能是在当前进程的上下文中加载并运行一个新程序。在执行fork得到子进程后随即使用解析后的命令行参数调用execve,execve调用启动加载器来执行hello程序。加载器执行的操作是,删除子进程现有的虚拟内存段,并创建新的代码、数据、堆和栈段。代码和数据段被初始化为hello的代码和数据。堆和栈被置空。然后加载器将PC指向hello程序的起始位置,即从下条指令开始执行hello程序。

6.5 Hello的进程执行

当操作系统创建并启动hello程序时,它会为该程序分配一个进程控制块(PCB),其中包含了该进程的上下文信息。hello程序开始执行后,进入main函数。在用户模式下,它按照指定的循环次数打印"Hello 学号 姓名"的信息,然后通过sleep函数休眠一段时间,此时进程进入内核模式。在每次循环中,hello程序消耗一定的时间片执行,当时间片用完后,操作系统会将其挂起。当hello程序被挂起后,操作系统会在就绪队列中选择下一个可执行的进程,并将CPU分配给它,这是进程调度的一部分。如果hello程序是唯一的可执行进程,它会继续获得CPU时间片并继续执行,直到完成指定的循环次数。如果hello程序在休眠期间被唤醒(例如按下键盘或接收到信号),操作系统会将其重新调度到CPU上继续执行。当hello程序完成指定的循环次数后,它会执行return语句退出,操作系统会终止该进程并释放相关资源。

6.6 hello的异常与信号处理

 hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

异常可以分为四类:中断、陷阱、故障、终止。如下表:

异常类别

原因

异步/同步

返回行为

中断

来自I/O设备的信号

异步

总是返回到下一条指令

陷阱

有意的异常

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

可能返回到当前指令

终止

不可恢复的错误

同步

不会返回

(1)正常运行hello的结果如下图:

(2)若在运行过程中按下ctrl+c,内核发送SIGINT信号,默认处理行为是终止前台作业。查看前台进程组,发现hello进程被终止,结果如下图:

(3)若在运行过程中按下ctrl+z,内核发送SIGTSNT信号,默认处理行为是挂起前台任务。查看前台进程组,发现hello进程没有被回收,输入fg后hello继续运行,如下图:

  1. 若在按下ctrl+z后输入jobs,将显示已停止的进程,如下图:
  2. 若在按下ctrl+z后输入kill指令,可以终止hello进程,如下图:

(6)输入ptree会列出当前进程及其所有子进程的层级结构。每个进程会以进程ID(PID)和进程名称的形式显示,并使用缩进表示父子关系。结果如下图:

  1. 若在运行时随便输入,不影响程序输出信息,如下图:

6.7本章小结

本章简要介绍了进程的概念和作用,以及shell的作用和处理流程,通过观察在不同情况下hello的运行结果,对hello运行时遇到的各种异常进行了分析。

7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1 逻辑地址

逻辑地址是指在程序执行过程中,由进程使用的地址空间中的地址。在hello程序中,逻辑地址用于访问代码、数据和堆栈等部分。逻辑地址是相对于进程的上下文而言的,它在进程内部具有唯一性。

7.1.2 线性地址

线性地址是指在虚拟内存系统中使用的地址。虚拟内存是操作系统提供的一种机制,将逻辑地址空间映射到物理内存空间。在执行hello程序时,逻辑地址被转换为线性地址。这个转换过程是由操作系统的内存管理单元(MMU)负责完成的。

7.1.3 虚拟地址

虚拟地址是进程在执行过程中使用的地址,它是由线性地址经过虚拟内存映射转换得到的。虚拟地址空间是一个抽象的地址空间,每个进程都有自己的虚拟地址空间,使得每个进程都可以独立地访问其逻辑地址空间。虚拟地址通过翻译可以得到物理地址。

7.1.4 物理地址

物理地址是实际存在于计算机物理内存中的地址。当进程执行hello程序时,操作系统将线性地址转换为物理地址,以便访问实际的内存单元。物理地址是硬件可直接使用的地址。

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

Intel的段式管理是一种内存管理方式,用于将逻辑地址转换为线性地址。它是在x86架构中广泛使用的一种内存管理方式。

在段式管理中,内存被划分为多个段,每个段具有不同的属性和长度。常见的段包括代码段、数据段、堆栈段等。每个段都由一个描述符(段描述符)来描述,其中包含了段的基地址、段的长度、访问权限等信息。

当程序中使用逻辑地址时,它是由两个部分组成:段选择子和偏移量。段选择子包含了对应段的描述符的索引,而偏移量则是相对于段的起始位置的偏移量。

下面是Intel逻辑地址到线性地址的变换过程:

(1)根据段选择子的索引,从全局描述符表(GDT)或局部描述符表(LDT)中获取对应的段描述符。

(2)根据段描述符中的基地址和偏移量,计算线性地址。计算方法为:线性地址=段基地址+偏移量。

(3)对线性地址进行访问权限检查。根据段描述符中的访问权限信息,检查是否允许对该地址的读、写或执行操作。

(4)如果访问权限检查通过,则线性地址可以被访问。否则,会触发异常或中断,导致程序终止或转入异常处理程序。

段式管理使得每个段可以独立地管理和保护,提供了更灵活和安全的内存访问控制。它为操作系统提供了一种机制来隔离不同的程序和数据,以及对内存的保护和权限控制。

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

页式管理是一种常见的内存管理技术,用于将线性地址转换为物理地址,并提供了虚拟内存的抽象和管理。

在页式管理中,内存被划分为固定大小的页框(页帧),同时也将线性地址空间划分为相同大小的页。每个页都有一个唯一的页号,与一个页框对应。页表用于映射线性地址的每个页号到对应的页框号,从而实现地址转换。

下面是Hello程序的线性地址到物理地址的变换过程:

(1)程序使用线性地址进行内存访问。

(2)将线性地址划分为两个部分:页目录索引和页表索引。通常,线性地址的高位表示页目录索引,低位表示页表索引。

(3)使用页目录索引从页目录表中获取对应的页表基地址。

(4)使用页表索引从页表中获取对应的页框号。

(5)将获取的页框号与线性地址中的页内偏移量组合,得到物理地址。

(6)使用物理地址进行实际的内存访问。

通过页式管理,每个程序都可以拥有自己的页表,将其线性地址空间映射到物理内存中的页框,实现了虚拟内存的抽象和隔离。页式管理使得每个程序都可以拥有连续的线性地址空间,而不需要连续的物理内存空间。同时,它也提供了内存保护和权限控制的机制,通过页表中的权限位可以限制对特定页的访问。

在Hello程序中,当程序访问线性地址时,操作系统会根据页表的映射关系将线性地址转换为物理地址,然后进行实际的内存访问。这样,程序就可以在虚拟内存的抽象层面上进行内存访问,而不需要关心具体的物理地址。

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

TLB(Translation Lookaside Buffer)即快表是一种高速缓存,用于加速虚拟地址到物理地址的转换过程。在使用四级页表支持的情况下,虚拟地址(VA)到物理地址(PA)的变换通常涉及以下步骤:

(1)程序使用虚拟地址进行内存访问。

(2)将虚拟地址划分为四个部分:页表级别1(L1)索引、页表级别2(L2)索引、页表级别3(L3)索引和页内偏移量。

(3)访问TLB进行快速的虚拟地址到物理地址的转换。TLB是一个高速缓存,用于存储最近使用的虚拟地址和物理地址的映射关系。如果TLB中包含了所需的映射关系,则直接从TLB中获取物理地址。

(4)如果TLB中没有找到所需的映射关系,则需要访问页表进行地址转换。

(5)根据L1索引,从一级页表中获取对应的二级页表的基地址。

(6)根据L2索引,从二级页表中获取对应的三级页表的基地址。

(7)根据L3索引,从三级页表中获取对应的页框号。

(8)将获取的页框号与页内偏移量组合,得到物理地址。

(9)将物理地址用于实际的内存访问。

TLB作为高速缓存存储了虚拟地址到物理地址的映射关系,可以显著提高地址转换的速度。当TLB未命中时,需要访问页表来获取映射关系,这涉及多级的页表访问。通过多级页表的层次结构,可以有效地管理和映射大型的虚拟地址空间。

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

在一个具有三级Cache的系统中,物理内存的访问通常涉及第一级Cache(L1 Cache)访问、第二级Cache(L2 Cache)访问、第三级Cache(L3 Cache)访问、主存访问四个阶段。通常来说,从L1 Cache到L3 Cache,速度依次降低、容量依次增大。

当CPU需要访问物理内存中的数据时,首先会检查L1 Cache。如果所需数据在L1 Cache中命中,则可以立即从缓存中读取数据,无需进一步的内存访问。如果数据L1 Cache中未命中,则CPU将继续检查L2 Cache。如果所需数据在L2 Cache中命中,则可以从L2 Cache中读取数据,并将其加载到L1 Cache中供CPU使用。如果数据在L2 Cache中仍未命中,则CPU将进一步检查L3 Cache。如果所需数据在L3 Cache中命中,则可以从L3 Cache中读取数据,并将其加载到L2 Cache和L3 Cache中供CPU使用。

如果数据在三级Cache中都未命中,则需要进行主内存访问。在这种情况下,CPU会将请求发送到内存总线,并与主内存进行交互。主存相对于Cache而言速度较慢,但容量较大。数据将从主存中读取到Cache中,并在Cache中建立相应的缓存行,以便后续的快速访问。

Cache的存在可以提供快速的数据访问速度,因为Cache与CPU之间的距离更近,访问速度更快。当数据在Cache中命中时,可以避免访问主内存,提高系统的整体性能。如果数据未命中Cache,则需要通过逐级的Cache层次结构和主内存来获取所需数据。

7.6 hello进程fork时的内存映射

在shell输入命令行后,内核调用fork创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的PID。通过fork创建的子进程拥有父进程相同的区域结构、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。

(2)映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。

(3)映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

(4)设置程序计数器,execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

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

缺页故障是指在程序访问虚拟内存时,所需的页面不在主存中,需要从磁盘中调入的情况。当程序访问一个不在内存中的页面时,操作系统会产生缺页故障。

缺页中断处理是操作系统针对缺页故障的一种机制。当发生缺页故障时,处理器会暂停当前的指令执行,并将控制权交给操作系统内核,执行相应的缺页中断处理程序。缺页中断处理程序的主要任务是将所需的页面从磁盘加载到主存,然后更新页表,使得程序可以继续执行。

缺页中断处理的基本流程如下:

(1)中断处理程序的执行:

当发生缺页故障时,处理器暂停当前进程的执行,并跳转到操作系统内核中的缺页中断处理程序。

(2)中断处理程序的处理:

缺页中断处理程序首先会检查引发缺页故障的原因,例如是读操作还是写操作,是否是合法的内存访问等。

(3)页面调入:

如果所需的页面不在主存中,中断处理程序会将页面从辅存(磁盘)加载到主存(物理内存)中的空闲位置。这个过程可能涉及磁盘的读取操作,耗时较长。

(4)更新页表:

加载页面后,中断处理程序会更新页表,将页面映射到正确的虚拟地址。这样,程序可以继续执行,并访问所需的页面。

(5)恢复执行:

缺页中断处理程序完成后,处理器会将控制权返回给引发缺页故障的进程,进程从中断的位置继续执行。

缺页中断处理的目标是将所需的页面加载到内存中,以满足程序的内存访问需求。通过缺页中断处理,操作系统实现了虚拟内存管理和页面调度,使得程序可以访问远远超过物理内存容量的虚拟内存空间,并根据需要将页面从磁盘加载到内存中,提高了内存利用率和系统性能。

7.9动态存储分配管理

动态内存管理是指在程序运行时,根据需要动态分配和释放内存空间。它可以通过以下基本方法和策略来实现:

(1)动态内存分配函数:

malloc()函数:用于分配指定大小的内存块,并返回指向该内存块的指针。

calloc()函数:用于分配指定数量和大小的连续内存块,并返回指向该内存块的指针。

realloc()函数:用于调整之前分配的内存块的大小。

(2)内存管理策略:

及时释放内存:当不再需要使用某块动态分配的内存时,应该调用相应的释放函数(如free())来释放内存,避免内存泄漏。

合理分配内存:根据实际需求,合理估计所需的内存大小,避免过分分配或不足分配内存。

内存池管理:预先分配一块固定大小的内存池,然后从内存池中分配和释放内存,减少频繁的系统调用和碎片化。

分配算法选择:根据具体场景和需求选择适当的内存分配算法,如首次适应、最佳适应或最坏适应算法。

内存复用:尽可能重复利用已分配的内存块,避免频繁的分配和释放操作。

内存对齐:根据平台要求或数据类型的需求,将内存块的起始地址对齐到特定的边界。

(3)错误处理和异常情况:

检查分配结果:在使用动态分配的内存之前,需要检查分配函数的返回结果,确保内存分配成功。

处理分配失败:当内存分配失败时,需要处理分配失败的情况,可以选择释放其他已分配的内存,或者进行错误处理和恢复操作。

动态内存管理的目标是有效地管理系统内存资源,提供灵活、可扩展的内存分配和释放机制,以满足程序的动态内存需求,并避免内存泄漏和内存碎片化问题。合理的内存管理可以提高程序的性能和资源利用率。

7.10本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,分析了hello进程fork、execve时的内存映射,以及缺页故障与缺页中断处理和动态存储分配管理。

结论

hello所经历的过程如下:

(1)编写代码:程序员编写了这段hello.c源程序,定义了一个main函数,并使用C语言的标准库函数进行输入输出和控制流程。

(2)预处理:C预处理器通过解析宏定义、文件包含、条件编译等,将2hello.c翻译为ASCII码的中间文件hello.i。

(3)编译:C编译器将C语言翻译为汇编指令,得到ASCII汇编语言文件hello.s。

(4)汇编:汇编器将汇编指令翻译成机器语言,并生成重定位信息,得到可重定位目标文件hello.o。

(5)链接:链接器经过符号解析、重定位等生成可执行目标文件hello。

(6)运行:在操作系统中输入./hello 2021112963 xushengkai 1,以运行hello,操作系统会为其创建一个进程。

(7)进程创建:shell调用fork函数为hello创建一个子进程,供hello程序的运行。

(8)加载程序:子进程中调用execve函数,将hello程序加载到进程的内存空间,进入hello的程序入口。

(9)执行代码:进程开始执行main函数中的代码。

(10)进程终止:程序执行完所有代码后,进程终止。shell负责回收终止的hello进程,内核删除为hello进程创建的所有数据结构。

hello的出生是漫长的,hello的成长是曲折的,hello的消逝是无声的。

撰写完这篇论文,我加深了对计算机系统的理解,我认为计算机系统的设计与实现是一个广泛而复杂的领域。计算机系统的设计与实现涉及硬件设计、操作系统、编译器、算法等多个方面。软硬件协同设计可能是一种新的设计,将软件和硬件设计紧密结合,通过硬件加速和专用硬件设计将有助于提高系统性能和效率。此外,用人工智能技术,如机器学习和自动化方法,优化系统设计和运行时的决策和管理,可以提高系统的智能化和自动化水平。

附件

文件名

作用

hello.c

源文件

hello.i

预处理后得到的中间文件,用于分析预处理的作用

hello.s

编译后得到的汇编指令文件,用于分析各种数据、操作

hello.o

汇编后得到的可重定位文件

hello.elf

hello.o的elf文件,用于分析各节、数据段

helloasm.txt

hello.o的反汇编文件,用于与hello.s进行对比分析

hello

链接后得到的可执行文件,可以在终端中运行

phello.elf

hello的elf文件,用于与hello.elf进行对比分析

phelloasm.txt

hello的反汇编文件,用于与helloasm.txt进行对比分析

参考文献

[1]  https://blog.csdn.net/tangodope/article/details/124869956

[2]  https://blog.csdn.net/qq_34767140/article/details/103843792

[3]  https://www.jianshu.com/p/fd2611cc808e

[4]  https://zhuanlan.zhihu.com/p/380908650

[5]  深入理解计算机系统(原书第3版)/(美)兰德尔 E. 布莱恩特(Randel E. Bryant)等著

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值