关注

25计算机系统大作业_程序人生-Hello’s P2P

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

摘  要

本篇论文旨在阐释 C 语言程序从源代码到可执行文件的转换过程。以 hello.c 程序为蓝本,本文深入剖析了计算机在生成 hello 可执行文件的过程中所涉及的预处理、编译、汇编、链接以及进程管理等各个阶段。文章不仅从理论上探讨了相关工具的原理与方法,还通过实际操作展示了其运行过程与结果。通过对计算机系统工作原理和体系结构的详细解读,本文助力读者更全面、更深刻地理解 C 语言程序的编译与执行机制。

关键词:计算机系统;生命周期;动态链接;进程管理;存储管理。

目  录

第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:全称为From Program to Process,意思是指从hello.c程序(Program)变为运行时进程(Process)。在这个过程中,hello.c源文件首先由预处理器展开宏定义和头文件,接着编译器将其转为汇编指令,汇编器生成目标文件,链接器则将目标文件与运行时库整合为符合ELF格式的可执行文件。这个可执行文件就可以在shell中执行,shell会给它分配进程空间。

020:全称为From Zero-0 to Zero-0,在020(From Zero-0 to Zero-0)阶段,进程启动前的虚拟内存不包含任何与 hello 程序相关的数据;execve() 调用负责加载并映射所有程序段,然后从入口点开始执行 main() 函数内的指令。当程序执行至最后的 return 0 或显式调用 exit(0) 时,内核接收退出码,依次关闭打开的文件描述符、注销信号处理例程、回收虚拟地址空间及对应的物理页框,并释放进程控制块(PCB)。父进程通过 wait()/waitpid() 函数获取子进程退出状态后,内核将该进程从进程表中移除。

1.2 环境与工具

硬件环境:

处理器:12th Gen Intel(R) Core(TM) i7-12700H   2.30 GHz

机带RAM16.0GB

系统类型:64位操作系统,基于x64的处理器

软件环境:Windows11 64位,VMwareUbuntu 24.04

开发与调试工具:Visual Studio 2022 64vim objump edb gcc readelf等工具

1.3 中间结果

hello.i:预处理后得到的文本文件

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

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

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

hello1.asm:反汇编hello可执行文件得到的反汇编文件。

1.4 本章小结

本章介绍了hello的P2P,020的全部流程;然后说明了本实验用到的软件平台、开发工具以及本实验过程中生成的各个中间结果文件的名称和功能。

第2章 预处理

2.1 预处理的概念与作用

在C语言的编译流程中,预处理是源代码变成可执行文件的第一步。预处理器会扫描所有以#开头的指令,将宏定义替换为它们对应的文本,将头文件的内容插入到源文件之中,并根据条件编译指令决定哪些代码需要保留、哪些可以忽略。它还会处理取消宏定义的指令,以及向编译器发出特定扩展或警告的命令。完成这一阶段后,原本包含各种预处理指令的源文件就被转换成一个纯粹的C代码文件,这个文件不再包含任何#指令,只剩下为后续编译做好准备的清晰代码。这一步虽不生成任何机器指令,却为编译器提供了完整的、可独立分析的翻译单元,奠定了整个编译、汇编和链接过程的基础。

预处理的作用可以体现在以下几个方面:

1.宏替换:

将宏定义替换为实际代码中的内容。

2.文件包含:

通过 #include 指令,预处理将外部文件(如头文件)内容插入到源代码中。

3.条件编译:

根据不同的条件选择性地包含或排除某些代码片段。

4.避免重复定义:

通过条件编译指令,避免重复包含同一文件,避免多次定义相同的宏或函数声明,减少潜在的错误。

5.代码调试与优化:

预处理还可以通过宏定义来控制调试输出或优化功能。

6.其他:

如注释删除等。

2.2在Ubuntu下预处理的命令

预处理的命令:gcc -E hello.c -o hello.i

图 1 终端界面截图

2.3 Hello的预处理结果解析

在虚拟机中打开hello.i,对源程序hello.c和预处理后的程序hello.i进行对比。结果显示,预处理指令被扩展成3000多行,源程序的其他部分都保持不变,说明.c文件被修改过了。

图 2 hello.i文件截图

在程序进main函数之前,几千行的代码都是由头文件<stdio.h>、<unistd.h>和 <stdlib.h>按顺序进行展开的。

以<stdio.h>为例:在预处理阶段,#include指令会将指定头文件的全部内容插入到源文件中。<stdio.h>是C标准库中负责输入输出的头文件,里面声明了各种读写文件和标准输入输出的函数原型,以及相关的宏定义。

当预处理器遇到#include <stdio.h>时,它会到系统预设的头文件目录(通常是 /usr/include)中寻找该文件,然后把其中的内容完整地拷贝到当前源文件里。另外,<stdio.h>本身也可能包含其它头文件(如<stddef.h>、<features.h>等),此时预处理器会按同样的方式递归展开它们。

整个过程只是“拷贝+替换”,并不会对头文件中的代码做任何实际计算或转换。

2.4 本章小结

本章首先在Linux环境下介绍了如何使用命令对C语言程序进行预处理,并阐明了预处理的含义与作用。接着,根据hello程序,演示了从源文件hello.c到预处理输出hello.i的完整流程,并结合具体代码对预处理结果展开解析。通过分析可见,生成的hello.i文件中不仅包含了标准输入输出库stdio.h的全部内容,还插入了若干宏与常量的定义,同时保留了行号标注和条件编译指令等信息。

第3章 编译

3.1 编译的概念与作用

计算机程序编译的概念是指将用高级程序设计语言书写的源程序,翻译成等价的汇编语言格式程序的翻译过程。

计算机程序编译的目的是将高级语言源代码转换成汇编代码,以提升开发效率和移植性。其基本流程包括词法分析、语法分析、语义分析、中间代码生成、代码优化以及目标代码生成等环节。

3.2 在Ubuntu下编译的命令

编译的命令:gcc -S hello.i -o hello.s

图 3 终端界面截图

3.3 Hello的编译结果解析

3.3.1 汇编初始部分

在main函数前,即文件最开始部分,有一部分字段展示了节名称:

图 4 汇编初始部分

他们是:

.file

声明出源文件

.text

表示代码节

.section   .rodata

表示只读数据段

.align

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

.string

声明一个字符串

.globl

声明全局变量

.type

声明一个符号的类型

表 1 节名称

3.3.2 数据部分

1. 字符串程序中有两个字符串存放在只读数据段中,.LC0,.LC1。如下图所示:

图 5 两个字符串

hello.c中唯一的数组是main函数的第二个参数char** argv,其每个元素都是指向字符的指针。该数组的起始地址存放在栈中-32(%rbp)的位置,并在两次调用printf时作为参数传递。

如图,分别将rax设置为两个字符串.LC0,.LC1的起始地址:

图 6 汇编内容截图

图 7 汇编内容截图

2. 参数argc

参数argc是main函数的第一个参数,被存放在寄存器%edi中,由语句可见寄存器%edi地址被压入栈中,而语句可知该地址上的数值与立即数5判断大小,从而得知argc被存放在寄存器并被压入栈中。

3. 局部变量

程序中的局部变量只有i,我们根据可知局部变量i是被存放在栈上-4(%rbp)的位置。

3.3.3全局函数

通过汇编文件中的内容.globl      main可知:本程序hello.c中只声明了一个全局函数——int main(int arge,.char*argv[])。

3.3.4 赋值操作

      在hello.c中,for循环开始时将i赋值为0,这在汇编中由mov指令完成,如:

图 8汇编内容截图

因为i是一个32位的int变量,所以使用movl来传送双字数据。

      

3.3.5 算术操作

hello.c中的算术操作是在每次for循环结束时执行i++,在汇编中由add指令实现,由于i是32位变量,因此使用addl指令。

图 9汇编内容截图

3.3.6 关系操作

hello.c中有两处关系操作:

(1) 在判断if(argc!=5) 时,汇编翻译为:

图 10汇编内容截图

这里使用 cmp 指令比较立即数5和参数argc的值,并设置相应的条件码。若不相等则执行后续指令,否则跳转到.L2。

(2) 在for循环结束时判断i<10,汇编翻译为:

图 11汇编内容截图

同样先用cmp指令设置标志位,然后根据条件码决定跳转到相应的位置。

3.3.7 控制转移指令

在设置条件码之后,就可以通过它来进行控制转移。本程序中共有两个控制转移: 

图 12汇编内容截图

该转移用于判断 argc 是否等于5;如果不等于,则执行if语句,否则跳过,在汇编中表现为当条件码表示不相等时跳转到.L2,否则继续执行cmp指令之后的指令。 

图 13汇编内容截图

第二处转移出现在for循环的每次结束时,用于判断 i<10;经过汇编转换后,通过条件码决定是否跳回.L4。for循环开始时则先将i置为0,如下所示: 

图 14汇编内容截图

然后通过无条件跳转指令跳向.L3,进入循环体。

3.3.8 函数操作

(1) main函数 

参数传递:该函数的参数是 int argc、char *argv[],具体的地址和值在前文已描述过。 

函数调用:通过call指令来调用函数,并把被调用函数的地址压入栈中后跳转。main中调用了printf、exit和sleep。 

局部变量:使用了局部变量i来控制for循环,具体的栈上地址和值同样在前文说明过。 

(2) printf 函数 

参数传递:两次调用分别传入argv[1]和argv[2]。 

第一次调用前,将寄存器 %rdi 设置为字符串 "用法:Hello 学号 姓名 秒数!\n" 的起始地址;第二次调用前,将 %rdi 设置为格式串 "Hello %s  %s\n" 的起始地址。寄存器 %rsi 用来传递 argv[1],%rdx 用来传递 argv[2]。 

(3) exit 函数 

参数传递与调用: 

图 15汇编内容截图

将 %edi 置为1,然后使用call 指令调用 exit。 

(4) atoi、sleep 函数 

参数传递与调用: 

图 16汇编内容截图

可以看到,atoi将argv[3]的地址放入%rdi,然后通过call调用。 

图 17汇编内容截图

接着把atoi返回的秒数从%eax移到%edi,作为sleep的参数,再用call调用 sleep。 

(5) getchar 函数 

无参数传递,直接通过 call 指令调用即可。

3.3.9 类型转换

       atoi函数会将传入的字符串解析成整数,以供sleep函数使用。

3.4 本章小结

本章首先介绍了编译器如何将预处理生成的hello.i文件翻译成汇编代码文件 hello.s,阐明了编译这一阶段在程序构建中的核心作用,把人可读的代码转换为低级指令并完成语法检查和基本优化。接着对hello.s中的汇编内容进行了深入剖析。从数据处理的角度,我们观察了变量在栈和寄存器之间如何用 mov、lea 指令来装载与存储;在函数调用部分,重点比较了调用约定下参数如何通过寄存器或栈传递,又如何通过 call 和 ret 指令实现调用与返回;赋值和算术运算部分则对应 movl、addl 等指令的使用;关系运算通过 cmp 加条件跳转指令来完成;控制流则由 jmp、jle、je 等无条件或有条件跳转指令实现循环和分支逻辑。

第4章 汇编

4.1 汇编的概念与作用

       汇编操作是通过汇编器(as)将汇编语言编写的.s源代码文件转化为机器可执行的指令序列,并将其组织成可重定位的目标文件格式,生成目标文件.o。这个.o文件是一个二进制文件,内嵌了main函数的指令编码信息。

       汇编是一种将高级语言转换为计算机可以直接运行的代码的过程。在这个过程中,汇编器会把.s文件中的汇编代码翻译成机器语言指令,并将这些指令组合成可重定位的目标程序格式。生成的.o文件是一个二进制文件,其中包含了程序的指令编码。

4.2 在Ubuntu下汇编的命令

在Linux环境下,对hello.s进行汇编的命令为:

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

图 18终端界面截图

4.3 可重定位目标elf格式

需要的指令为:readelf -a hello.o > hello.elf

通过该指令可以获得 hello.o 文件的 ELF 格式:

图 19终端界面截图

4.3.1 生成ELF格式的可重定位目标文件

典型的ELF格式的可重定位目标文件的结构如下:

部分

说明

ELF Header

描述整个文件的基本信息,如类型(可重定位/可执行等)、目标架构、入口点等

Section Headers表

描述各个段(Section)的元数据(偏移、大小、属性等)

.text 段

存放编译生成的机器代码(函数体)

.data 段

存放已初始化的全局变量和静态变量

.bss段

存放未初始化的全局/静态变量(占空间但不存数据)

.rodata 段

存放只读数据,如字符串常量、const 变量

.rel.text / .rela.text

与 .text 相关的重定位信息,说明哪些地方需要链接器调整

.symtab(符号表)

保存当前目标文件中定义和引用的所有符号(函数、变量等)

.strtab(字符串表)

存储符号表等部分引用的字符串(如符号名)

.shstrtab(段名称字符串表)

存储所有段名称的字符串,供 Section Header 引用

表 2典型的ELF格式的可重定位目标文件的结构

4.3.2

(1) ELF头部 

ELF头部(ELF header)以一个16字节的序列起始,该序列用于描述生成该文件的系统所使用的字的大小和字节顺序。ELF头部的其余部分包含了帮助链接器解析和理解目标文件的关键信息,例如ELF头部的大小、目标文件的类型(例如可重定位、可执行或共享)、机器类型(如x86-64)、节头部表(section header table)的文件偏移量,以及节头部表中条目的大小和数量。节头部表详细描述了各个节的位置和大小,目标文件中的每个节在节头部表中都有一个固定大小的条目(entry)。ELF头部的结构如下: 

图 20ELF头部的结构

(2) 节头部(section header) 

节头部记录了各节的名称、类型、地址、偏移量、大小、全体大小、标志、链接、信息以及对齐方式。 

图 21节头部

(3) 重定位节 

.rel.text节是一个列表,记录了.text节中需要修改的位置。当链接器将该目标文件与其他文件合并时,这些位置需要进行调整。通常,调用外部函数或引用全局变量的指令需要修改,而调用本地函数的指令则不需要修改。可执行目标文件中不包含重定位信息。需要重定位的内容如下图所示: 

图 22重定位节

(4) 符号表 

.symtab节中包含ELF符号表,符号表是一个条目的数组,用于存储程序中定义和引用的全局变量和函数的信息。该符号表不包含局部变量的信息。符号表的结构如下: 

图 23符号表

4.4 Hello.o的结果解析

在终端中输入 objdump -d -r hello.o > hello.asm 指令输出hello.o的反汇编文件,并与第3章的hello.s文件进行对照分析。

图 24终端界面截图

(1) 增加机器语言

在代码中,每条指令都添加了对应的十六进制表示,即机器语言形式。例如,在hello.s文件中的一个cmpl指令表示为 

图 25汇编内容截图

而在反汇编文件中,该指令表示为 

图 26反汇编内容截图

(2) 操作数进制变化

在反汇编文件中,所有操作数均以十六进制形式表示。例如,在(1)中的例子,立即数从hello.s中的$5变为$0x5,地址表示也从-20(%rbp)变为-0x14(%rbp)。这表明只是进制表示发生了变化,而数值本身并未改变。 

(3) 分支转移变化

在反汇编后的跳转指令中,所有跳转位置被表示为相对于主函数的段内偏移量,而不再是段名称(例如.L3)。例如,对于jmp指令,反汇编文件中表示为 

图 27反汇编内容截图

而在hello.s文件中表示为 

图 28汇编内容截图

(4) 函数调用变化

反汇编文件中的函数调用与重定位条目相对应。观察以下两个call指令调用函数的情况,在hello.s中表示为 

图 29汇编内容截图

而在反汇编文件中调用函数表示为 

图 30反汇编内容截图

在可重定位文件中,call指令后面不再是函数名称,而是一个指向重定位条目的信息。

4.5 本章小结

本章详细阐述了汇编的概念及其功能。以Ubuntu系统中的hello.s文件为例,展示了如何将其汇编为hello.o文件,并进一步生成ELF格式的可执行文件hello.elf。通过对可重定位目标文件转换为ELF格式后的文件内容进行观察,并对文件中的各个节进行简单分析,揭示了其结构和作用。此外,通过对比hello.o的反汇编代码(存储在hello.asm中)与原始hello.s文件的差异与相似之处,清晰地呈现了汇编语言向机器语言的转换过程,以及为链接所做的准备工作。

5章 链接

5.1 链接的概念与作用

链接(linking)是一种将多个代码和数据片段整合成一个单一文件的过程,该文件能够被加载到内存中并执行。链接可以在不同的阶段执行:它可以在编译时(compile time)进行,即在源代码被转换为机器代码的过程中;也可以在加载时(load time)进行,即在程序被加载器载入内存并准备执行时;甚至可以在运行时(runtime)进行。

在当今的计算机系统中,链接是由专门的程序——链接器(linker)自动完成的。链接器的存在使得分离式编译成为现实。我们无需将一个庞大的应用程序整合到一个巨大的源文件中,而是可以将其拆分成多个更小、更易于管理的模块。这些模块可以独立地进行修改和编译。当其中某个模块发生变化时,我们只需重新编译该模块,并重新进行链接,即可更新整个应用程序。

5.2 在Ubuntu下链接的命令

在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

图 31终端界面截图

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

通过使用`readelf`工具解析`hello`的ELF格式,可以获取其节信息和段信息:

(1)ELF头(ELF Header)

hello1.elf中的ELF头与hello.elf中的ELF头所包含的信息类型基本一致。它以一个16字节的序列Magic开始,该序列用于描述生成该文件的系统的字大小和字节顺序。ELF头的其余部分包含了帮助链接器解析和理解目标文件的关键信息。与hello.elf相比,hello1.elf中的基本信息(如Magic、类别等)保持不变,但文件类型发生了变化,程序头大小和节头数量有所增加,并且新增了入口地址。

图 32ELF头

(2)节头(Section Header)

节头详细描述了各个节的大小、偏移量以及其他属性。在链接过程中,链接器会将不同文件中相同类型的段合并为一个更大的段,并根据合并后的大段的大小和偏移量,重新调整各个符号的地址。

图 33节头

(3)程序头

程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。

图 34程序头

(4)Dynamic section

图 35Dynamic section

(5)Symbol table

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

图 36符号表

图 37符号表

5.4 hello的虚拟地址空间

观察程序头的LOAD可加载的程序段的地址为0x400000。如图:

图 38程序头部分内容截图

使用edb打开hello从Data Dump窗口观察hello加载到虚拟地址的情况,查看各段信息。如图:

图 39Data Dump窗口

程序从地址0x400000开始到0x401000被载入,虚拟地址从0x4000000x400f0结束,根据5.3中的节头部表,可以通过edb找到各段的信息。

我们可以找到如.text节的信息:

图 40.text节的信息

5.5 链接的重定位过程分析

5.5.1 分析hello与hello.o区别

在终端中使用命令objdump -d -r hello > hello1.asm生成反汇编文件hello1.asm

图 41终端界面截图

与hello.asm文件进行比较,其不同之处有以下几个方面:

(1)链接后函数数量增加

在链接后的反汇编文件hello1.asm中,出现了如.plt、puts@plt、printf@plt、getchar@plt、exit@plt、sleep@plt等额外的函数代码。这是因为在动态链接过程中,动态链接器将共享库中hello.c所使用的函数加入到了可执行文件中。 

图 42链接后反汇编内容截图

(2)函数调用指令call的参数发生变化

在链接过程中,链接器解析了重定位条目。call指令之后的字节代码被链接器直接修改为目标地址与下一条指令地址的差值,使其指向相应的代码段,从而生成完整的反汇编代码。 

图 43链接后反汇编内容截图

(3)跳转指令参数发生变化

在链接过程中,链接器解析了重定位条目,并计算相对距离。链接器将对应位置的字节代码修改为PLT中相应函数与下一条指令的相对地址,从而生成完整的反汇编代码。 

图 44链接后反汇编内容截图

5.5.2 重定位过程 

重定位过程由以下两步组成: 

(1)重定位节和符号定义

在这一步中,链接器将所有相同类型的节合并为同一类型的聚合节。然后,链接器为新的聚合节、输入模块定义的每个节以及输入模块定义的每个符号分配运行时的内存地址。至此,程序中的每条指令和全局变量都拥有了唯一的运行时内存地址。 

(2)重定位节中的符号引用

在这一步中,链接器修改代码节和数据节中对每个符号的引用,使其指向正确的运行时地址。为了完成这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。 

5.6 hello的执行流程

5.6.1整体过程

通过edb的调试,记录下call命令进入的函数。

图 45edb界面

(1)开始执行:_start、_libe_start_main

(2)执行main:_main、printf、_exit、_sleep、getchar

(3)退出:exit

5.6.2子程序名及地址

_start

0x4010f0

_libe_start_main

0x2f12271d

main

0x401125

printf

0x4010a0

_sleep

0x4010e0

getchar

0x4010b0

exit

0x4010d0

表 3子程序名及地址

5.7 Hello的动态链接分析

动态链接的核心思想是将程序按照功能模块拆分成多个相对独立的部分,并在程序运行时将这些模块动态地链接在一起,形成一个完整的程序。由于共享库函数的运行时地址在编译时无法确定(因为共享模块在运行时可以加载到任意内存位置),编译器通常会为这些函数引用生成重定位记录。在程序加载时,动态链接器会解析这些记录,从而确定函数的实际地址。

为了提高效率,动态链接通常采用延迟绑定机制,即只有在首次调用某个函数时,才解析该函数的实际地址。这种机制通过全局偏移表(GOT)和过程链接表(PLT)来实现。以hello.elf文件为例,其GOT的起始位置为0x403fe8。

表 4GOT的起始位置

       GOT表位置在调用dl_init之前0x403ff0后的16个字节均为0:

图 46data dump部分内容截图

调用了dl_init之后字节改变了:

图 47data dump部分内容截图

对于变量的地址计算,可以利用代码段和数据段相对位置不变的原则来确定其正确地址。而对于库函数的调用,则需要借助过程链接表(PLT)和全局偏移量表(GOT)的协作。具体来说,PLT 最初存储的是一批跳转代码,这些代码会跳转到 GOT 所指示的位置,进而调用链接器。在初始状态下,GOT 中存储的都是 PLT 的第二条指令。随后,链接器会修改 GOT,使得下一次调用 PLT 时,能够直接指向正确的内存地址。在程序的后续执行过程中,就可以通过 PLT 和 GOT 来实现动态链接。

5.8 本章小结

本章首先介绍了链接的基础知识及其功能,通过命令行演示了如何将代码链接成hello可执行文件,并对hello文件的ELF格式内容进行了查看。接着,借助edb工具观察了hello文件的虚拟地址空间使用情况。最后,以hello程序为案例,对重定位过程、程序执行过程以及动态链接进行了详细分析。

6章 hello进程管理

6.1 进程的概念与作用

进程的经典定义是程序的一个执行实例。它代表了计算机中程序针对某一数据集合的运行活动,是系统进行资源分配和调度的基本单位,也是操作系统结构的核心。在传统操作系统中,进程既是资源分配的基本单位,也是程序执行的基本单位。进程为程序提供了一种假象,让程序仿佛独占处理器和内存,处理器似乎不间断地依次执行程序中的指令。每个程序都在某个进程的上下文中运行。

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

Shell 是一种交互式应用级程序,也称为命令解析器。它为用户提供操作界面,接收用户输入的命令,并调度相应的应用程序。

首先从终端读取用户输入的命令,对命令进行解析。如果是内置命令,则直接执行;否则通过调用 fork 创建一个子进程,在子进程的上下文中执行指定程序。判断程序是前台程序还是后台程序:如果是前台程序,则等待其执行结束;如果是后台程序,则将其放回后台并返回。在此过程中,Shell 可以接收并处理来自键盘的信号。

6.3 Hello的fork进程创建过程

用户在 Shell 界面输入指令:./hello 2023113294 王冠智

Shell 判断该指令不是内置命令,于是父进程调用 fork 函数创建一个子进程。子进程获得与父进程用户级虚拟地址空间相同的副本,包括代码和数据段、堆、共享库以及用户栈。子进程与父进程的主要区别在于 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。只有在出现错误(如找不到 filename)时,execve 才会返回到调用程序。与 fork 不同,execve 调用一次后不会返回。当 main 函数运行时,用户栈的结构如下图所示:

图 48用户栈的结构

6.5 Hello的进程执行

Hello 程序运行时,进程为应用程序提供的抽象包括:

独立的逻辑控制流,让进程仿佛独占处理器。

私有地址空间,让程序仿佛独占 CPU 内存。

操作系统提供的抽象包括:

逻辑控制流:程序计数器(PC)的值序列,对应于程序可执行文件或动态链接的共享对象中的指令。多个逻辑流同时运行称为并发流。

上下文切换:操作系统内核通过上下文切换实现多任务。内核为每个进程维护一个上下文,包含重新启动进程所需的状态。

时间片:进程执行控制流的一部分的时间段。

用户模式和内核模式:处理器通过模式位区分。内核模式下,进程可执行所有指令并访问任何内存位置;用户模式下,进程不能执行特权指令,也不能直接访问内核区。

上下文信息:包含寄存器、栈、状态寄存器等的值。

Hello 程序执行过程中,调用 execve 函数后,进程为程序分配新的虚拟地址空间,初始运行在用户模式。调用 printf 函数输出“Hello 2023113294 王冠智”,之后调用 sleep 函数,进程进入内核模式运行信号处理程序,再返回用户模式。运行过程中,CPU 不断切换上下文,将运行过程切分成时间片,与其他进程交替占用 CPU,实现进程调度。

6.6 hello的异常与信号处理

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

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

6.6.1 异常的分类

图 49异常的分类

6.6.2异常的处理方式

图 50异常的处理方式1

图 51异常的处理方式2

图 52异常的处理方式3

图 53异常的处理方式4

6.6.3 运行结果及相关命令   

(1)正常运行状态

在程序正常运行时,打印10次提示信息,以输入回车为标志结束程序,并回收进程。

图 54终端界面截图

(2)运行时按下Ctrl + C

按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。

图 55终端界面截图

(3)运行时按下Ctrl + Z

按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。

图 56终端界面截图

(4)对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。

图 57终端界面截图

(5)在Shell中输入pstree命令,可以将所有进程以树状图显示:

图 58终端界面截图

(6)输入kill命令,则可以杀死指定(进程组的)进程:

图 59终端界面截图

(7) 输入fg 1则命令将hello进程再次调到前台执行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句。程序仍然可以正常结束,并完成进程回收。

图 60终端界面截图

(8)不停乱按

       在程序执行过程中乱按所造成的输入均缓存到stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),hello结束后,stdin中的其他字串会当做Shell的命令行输入。

图 61终端界面截图

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 位是索引号,后 3 位为一些硬件细节。索引号是“段描述符”的索引,段描述符具体描述了一个段,多个段描述符组成段描述符表。通过段标识符的前 13 位,可以直接在段描述符表中找到一个具体的段描述符。

全局描述符表(GDT)是整个系统唯一的,包含以下内容:

操作系统使用的代码段、数据段、堆栈段的描述符。

各任务、程序的局部描述符表(LDT)段。

每个任务程序有一个独立的 LDT,包含以下内容:

对应任务/程序私有的代码段、数据段、堆栈段的描述符。

对应任务/程序使用的门描述符,如任务门、调用门等。

段式管理图示如下:

图 62段式管理图示

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

虚拟内存被组织为一个由存放在磁盘上的 N 个连续字节大小的单元组成的数组。虚拟内存管理系统(VM 系统)将虚拟内存分割为虚拟页,类似地,物理内存也被分割成物理页。页表用于管理虚拟页,页表是一个由页表条目(PTE)组成的数组。每个 PTE 包含一个有效位和一个物理地址字段。有效位表明该虚拟页当前是否被缓存在 DRAM 中。如果设置了有效位,那么地址字段表示 DRAM 中相应的物理页的起始位置。如果发生缺页,则从磁盘读取。

内存管理单元(MMU)利用页表实现从虚拟地址到物理地址的翻译。

下面为页式管理的图示:

图 63页式管理的图示

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

Core i7 采用四级页表的层次结构。CPU 产生虚拟地址(VA),虚拟地址 VA 传送给 MMU,MMU 使用 VPN 高位作为 TLB 标签(TLBT)和 TLB 索引(TLBI),在 TLB 中寻找匹配。如果命中,则得到物理地址(PA)。如果 TLB 中没有命中,MMU 查询页表。CR3 确定第一级页表的起始地址,VPN1 确定在第一级页表中的偏移量,查询出 PTE,以此类推,最终在第四级页表中找到 PPN,与 VPO 组合成物理地址 PA,并添加到页表中。

图 64工作原理

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

高速缓存存储器的组织结构如下图所示:

图 65高速缓存存储器的组织结构

高速缓存的结构将 m 个地址位划分为 t 个标记位、s 个组索引位和 b 个块偏移位。如果选中的组中存在一行有效位为 1,且标记位与地址中的标记位相匹配,则为缓存命中;否则为缓存不命中。如果缓存不命中,需要从存储器层次结构的下一层中取出被请求的块,并将其存储在由组索引位指示的组中的一个高速缓存行中。具体替换哪一行取决于替换策略,例如,最近最少使用(LRU)策略会替换最后一次访问时间最久远的那一行。

7.6 hello进程fork时的内存映射

当 fork 函数被当前进程调用时,内核为新进程创建各种数据结构,并分配一个唯一的 PID。为了为新进程创建虚拟内存,内核创建当前进程的 mm_struct、区域结构和页表的副本。当 fork 在新进程中返回时,新进程的虚拟内存与调用 fork 时的虚拟内存相同。当这两个进程中的任何一个进行写操作时,写时复制机制会创建新页面,从而为每个进程保持私有地址空间的抽象概念。

图 66私有的写时复制

7.7 hello进程execve时的内存映射

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

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

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

映射共享区域:Hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到程序中的,然后映射到用户虚拟地址空间中的共享区域内。

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

图 67execve时的内存映射

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

如果程序执行过程中发生缺页故障,则内核调用缺页处理程序。

图 68各种缺页情况

处理程序执行以下步骤:

检查虚拟地址是否合法。如果不合法,则触发段错误并终止进程。

检查进程是否有读、写或执行该区域页面的权限。如果没有,则触发保护异常并终止程序。

如果上述两步检查均无误,内核选择一个牺牲页面。如果该页面被修改过,则将其交换出去,换入新的页面并更新页表。然后将控制权转移给 Hello 进程,再次执行触发缺页故障的指令。

7.9动态存储分配管理

动态内存管理的基本方法与策略

1.底层内存获取 :

a) sbrk/brk:通过调整程序数据段末端一次性申请或释放一大块连续内存,适合对堆进行常规增减

b) mmap:向操作系统请求一个新的匿名映射(页对齐),常用于申请大于阈值(如128 KB)的内存块或独立区域

2分配算法:

a) 空闲链表(Free List)

– First‑Fit:从链表头开始,找到第一个足够大的空闲块并拆分

– Best‑Fit:扫描全部空闲块,选择最接近所需大小的块以减少剩余碎片

– Worst‑Fit:选择最大的空闲块,以避免产生过多小碎片

分配时对大块进行拆分,释放时对相邻空闲块进行合并(Coalesce)

b) 伙伴系统(Buddy System)

将内存按 2 的幂次方大小划分,分配时不断二分直到符合需求;释放时只需检查并合并相邻的“伙伴”块,简化合并操作

3.小对象优化:

a) 分级分配器(Segregated Lists):按大小范围维护多个空闲链表(如小于64 B、64–128 B 等),快速定位、减少搜索成本

b) Slab/对象缓存(Slab Allocator):针对固定大小的对象预先分配若干缓存页(slab),分配/释放只操作位图或链表,无需遍历空闲链表

4.并发与线程局部缓存:

现代 malloc 实现(如 ptmalloc、jemalloc、tcmalloc)为每个线程维护私有缓存(Thread‑Local Cache),在本地快速分配和批量回收,减少全局锁竞争

5.碎片与回收策略:

a) 外部碎片:过多小而分散的空闲块,通过合并、页级紧凑或延迟合并策略缓解

b) 内部碎片:分配块内部未使用的空间,可通过更细粒度的大小分级或 Slab 缓存减少

c) 延迟释放或分批释放:在高并发或频繁分配场景下,合并多次小释放为一次系统调用,降低开销

5.自动回收(垃圾回收):

在需要自动内存管理的场景,引入引用计数、标记‑清扫、复制式、增量/并发垃圾回收等技术,自动跟踪对象可达性并批量回收,无需程序员手动调用 free

7.10本章小结

本章主要介绍了 Hello 的存储器地址空间、Intel 的段式管理、Hello 的页式管理,以及在 Intel Core i7 指定环境下,虚拟地址(VA)到物理地址(PA)的转换、物理内存访问,分析了 Hello 进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理,以及动态内存管理的基本方法与策略。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

设备的模型化是将所有 I/O 设备抽象为文件,通过统一的文件接口进行操作。这种统一接口允许用户空间程序通过标准的文件操作函数来访问不同类型的硬件设备。

8.2 简述Unix IO接口及其函数

8.2.1 Unix I/O 接口

打开文件:应用程序通过向内核请求打开特定文件来声明对某个 I/O 设备的访问。内核返回一个小的非负整数,称为文件描述符,后续对该文件的所有操作都将通过这个描述符进行标识。

改变当前文件位置:对于每个打开的文件,内核维护一个文件位置 k,初始值为 0,表示从文件开头起的字节偏移量。

读写文件:读取文件时,从文件中复制 n 个字节到内存,起始位置是当前文件位置 k,然后更新文件位置为 k+n。如果文件大小为 m 字节,且 k ≥ m,则触发 EOF(文件结束)条件。

关闭文件:完成文件操作后,内核释放为该文件打开时创建的数据结构,并将对应的文件描述符恢复为可用状态。

8.2.2 Unix I/O 函数

open():打开或创建一个文件,并返回与之关联的文件描述符。返回的描述符总是在进程中当前没有打开的最小描述符。

close():关闭一个打开的文件。

read():从描述符为 fd 的文件中读取最多 n 个字节到内存位置 buf。返回值 -1 表示错误,0 表示 EOF,否则返回实际传送的字节数量。

write():从内存位置 buf 复制最多 n 个字节到描述符为 fd 的文件中。

8.3 printf的实现分析

[转]printf 函数实现的深入剖析 - Pianistx - 博客园

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

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

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

8.4 getchar的实现分析

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

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

8.5本章小结

本章介绍关于hello的IO管理,包括Linux的IO设备管理方法,Unix IO接口及函数以及printf和getchar的实现分析。

结论

hello所经历的过程:

首先,程序员在终端中输入hello程序的源代码。该程序将依次经过以下阶段:

1. 预处理。编译器调用预处理器,将hello.c中所有的#include指令展开,把所引用的头文件内容合并到源文件中,生成hello.i。

2. 编译。将预处理后的hello.i转换为汇编代码,输出到hello.s。

3. 汇编。汇编器将hello.s翻译成可重定位的目标文件hello.o。

4. 链接。链接器把hello.o与其他目标文件及所需的动态链接库合并,生成最终的可执行文件hello。

5. 运行。在命令行中输入./hello 2023113294 王冠智,启动程序。

6. 创建进程。Shell判断该命令不是内置指令,调用fork创建子进程。

7. 加载程序。子进程通过execve启动加载器,映射虚拟内存,将程序映像载入物理内存,然后跳转到main函数入口。

8. 执行指令。操作系统为子进程分配 CPU 时间片,按照编译生成的机器指令序列执行程序的控制逻辑。

9. 访问内存。内存管理单元(MMU)根据页表将程序访问的虚拟地址映射到实际物理地址。

10. 信号管理。在程序运行期间,如果用户按下Ctrl+C,内核会向进程发送SIGINT信号并终止运行;若按下Ctrl+Z,内核发送SIGTSTP信号,暂停前台作业并将其挂起。

11. 终止。子进程执行完毕后,内核通知父进程并回收子进程的资源,同时销毁与子进程相关的所有内核数据结构。

感悟:

通过本次大作业,我深切感受到计算机系统的精细和强大,每一个表面上看似简单的任务都需要计算机内部程序的多种复杂的操作来完成,这背后体现出了计算机程序设计严谨的逻辑和近现代计算机科学家的卓越成就。

附件

文件名

功能

hello.c

源程序

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

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

hello.elf

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

hello.asm

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

hello1.asm

反汇编hello可执行文件得到的反汇编文件

hello

可执行文件

表 5文件附件说明

参考文献

[1]  Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.

[2]  https://www.cnblogs.com/pianist/p/3315801.html

[3]  https://www.cnblogs.com/fanzhidongyzby/p/3519838.html.

[4]  https://ysyx.oscc.cc/slides/hello-x86.html

[5]  https://blog.csdn.net/weixin_45186298/article/details/108762592

[6]  https://blog.csdn.net/ds1130071727/article/details/88787426

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/hair_protector/article/details/148198882

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--