程序人生-Hello‘P2P

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业    计算学部            

学     号    2023111080          

班   级    23L0508             

学       生    杜夏源            

指 导 教 师     史先俊           

计算机科学与技术学院

20255

摘  要

本次大作业报告围绕“程序人生-Hello’s P2P”这一主题,详细阐述了从程序(Program)到进程(Process)的转变过程,即P2P过程。通过对Hello程序在计算机系统中的预处理、编译、汇编、链接等阶段的深入分析,展示了每个阶段的关键操作、命令及其结果解析。同时,探讨了进程管理、存储管理和IO管理等方面的知识,揭示了计算机系统如何将静态的程序代码转化为动态的进程执行,以及在执行过程中对资源的管理和调度。报告旨在加深对计算机系统底层运作机制的理解,提升对程序开发和系统资源管理的综合认识。

关键词:预处理;编译链接;进程管理;存储管理                            

目  录

第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 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

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本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

Hello是一个由程序员编写的源代码文件,经由计算机预处理,编译,汇编,链接,最终成为一个可执行程序。可执行程序进而可以被载入内存,分配进程,进行指令的执行。其中包括P2P和020两个阶段

P2P:From Program to Process

Program:指编程过程中,从源代码文件编写到计算机预处理,编译,汇编,链接,最终成为一个可执行程序Hello的过程。程序,即program,是存储在磁盘上的静态实体。它由代码、数据和元信息(如资源需求)组成,但尚未运行。

Process:指运行已生成的程序时,在bash里,OS会通过fork()函数为该程序产生一个进程(process),进而通过execve(),mmap()等操作实现这个进程的运行。一个进程是程序的一次动态执行实例,此时程序被加载到内存来运行

020:From 0 to 0

1.2 环境与工具

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

硬件环境:

CPU:13th Gen Intel(R) Core(TM)i9-13900HX   2.20 GHz

Memory: 16GB DDR5 RAM

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

软件环境:Windows11 64位 版本号24H2,VMware,Ubuntu 18.04 LTS

开发与调试工具:Code Blocks,vim,gdb ,objdump,gcc等开发工具

1.3 中间结果

1.3.1预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插人程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。

hello.i:源代码文件预处理得到的文件

1.3.2编译阶段。编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含个汇编语言程序。该程序包含函数main的定义

hello.s:汇编语言的代码文件

1.3.3汇编阶段。接下来,汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的17个字节是函数main的指令编码。如果我们在文本编辑器中打开hello.o文件,将看到一堆乱码。

hello.o:hello的可链接重定位文件

1.3.4链接阶段。hello程序调用了printf函数,它是每个C编泽器都提供的标准C库中的一个函数。printf函数存在于一个名为printf.。的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。链接器(ld)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行

hello:可执行程序

 

图1.1,结果演示

1.4 本章小结

本章概述了hello,首先介绍了P2P和O2O的含义及其过程,接着介绍了作业中使用的硬件环境、软件环境和开发工具,最后简要说明了从.c文件到可执行文件的转换过程。


第2章 预处理

2.1 预处理的概念与作用

在计算机编程中(尤其是C/C++等语言),预处理(Preprocessing)是源代码被正式编译前的一个处理阶段,由预处理器(Preprocessor)负责执行。它通过解析源代码中的特定指令(以 # 开头的命令),对代码进行文本级别的修改和扩展,最终生成一个“预处理后的源代码文件”,供编译器进一步处理。

预处理的步骤有:

读取源代码:预处理器逐行扫描原始代码。

执行指令:处理所有 # 开头的指令(如宏替换、文件包含)。

生成中间文件:输出一个“干净”的代码文件(可通过 gcc -E 查看)。

2.2在Ubuntu下预处理的命令

图2.1 指令

或者

图2.2 指令

如果需要进一步细化要求,则可在指令中添加相应的内容

图2.3 指令

这条指令中的各部分指的是

-m64 是一个编译选项,用于指定生成 64 位代码。它告诉 GCC 编译器为目标平台生成适用于 64 位体系结构的指令和数据结构。

-no-pie 表示禁止生成位置无关可执行文件(Position - Independent Executable)。而使用-no-pie选项可以关掉这个特性,生成传统的非位置无关可执行文件,程序的代码段在内存中的加载地址是固定的。

-fno-PIC 是告诉 GCC 不要生成位置无关代码(Position - Independent Code)。

-E 是 GCC 的一个预处理选项。它告诉 GCC 只进行预处理阶段的操作,然后停止进一步的编译过程。

2.3 Hello的预处理结果解析

通过对比hello.c 和 hello.i并且查询相关资料发现,预处理主要有以下几大作用:

  1. 文件包含(File Inclusion)

观察发现,预处理后的文件由原来的短短几十行拓展到了3000多行,而将源文件的主函数放到了最后。这是因为预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。这样一来,cpp读取了hello.c中的

#include <stdio.h>

#include <unistd.h>

#include <stdlib.h>

三项的内容,并且直接插入了程序中。

用途:

引入库函数声明(如 #include <stdio.h>)。

模块化代码,分离声明与实现。

图2.4 hello.i文件

  1. 删除注释:预处理会移除所有注释(// 和 /* ... */)

显而易见,源文件中的所有注释都被删除了,因为它们对程序的执行们没有任何意义。

  1. 行号标记:添加 #line 指令,便于编译器生成调试信息

观察发现,预处理文件中会出现一些数字,这些数字实际上是行号标记。这是一个正整数,表示后续代码的行号。它通常用于告诉编译器,下一行代码的行号应该被视作这个指定的数值。

如图:

当编译器处理到这段代码时:

会将后续代码的行号视为从stdio.h 的第1行开始。

然后是第三行

然后是第四行

用途:

修正调试信息的行号和文件名

当代码经过预处理或工具(如Bison、Flex、代码生成器)处理后,生成的中间代码行号可能与原始代码不一致。使用 #line 指令告诉编译器:“后续代码的行号和文件名应映射到原始文件,而非当前文件”。

除此之外,预处理还能够实现宏替换等功能(通过 #define 定义宏,将代码中的标识符替换为指定的文本。)不过在hello.c中未涉及这一功能

2.4 本章小结

本章讲述了在linux环境中,如何用命令对C语言程序进行预处理,以及预处理的含义和作用,接着以hello.c为例,演示了在Ubuntu下如何预处理程序,并对结果进行分析。通过分析,我们可以发现预处理后的文件hello.i包含了源文件所应用的头文件内容,还有一些行号信息和条件编译指令。


第3章 编译

3.1 编译的概念与作用

编译是指将人类可读的源代码翻译成汇编代码的过程。

编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含个汇编语言程序

编译器通过前端进行词法分析、语法分析等,然后经过优化器进行代码优化,最终由后端将代码翻译汇编代码。编译的过程包括翻译、优化、检查和生成可执行文件等步骤,以确保程序的正确性和性能。

(这里的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序)

3.2 在Ubuntu下编译的命令

图3.1指令

同样的,在编译阶段也可以细化指令

图3.2指令

3.3 Hello的编译结果解析

编译后得到的hello.s文件如图:

图3.3 hello.s文件

3.3

先对该文件的结构进行分析,大体可以分为三部分:

第一部分:从一开始到main之前:这是在做程序的初始化设置和定义只读数据。

初始化设置

.file "hello.c":指定源文件名,用于调试信息。

.text:切换到代码段,表示接下来的指令是程序的代码部分。

.section .rodata:切换到只读数据段,用于存放只读数据,如字符串常量。

.align 8:对齐指令,确保数据在内存中按 8 字节对齐,以提高性能和正确性。

定义只读数据

.LC0: 和 .LC1::定义两个字符串常量。

.LC1:定义了一个格式化字符串,用于 printf 函数,包含三个 %s 格式说明符和一个换行符。

.global:声明全局变量

.text:再次切换回代码段,表示接下来的指令是程序的代码部分。

第二部分:_LFB6和_LFE6之间的那一部分,这部分是对源文件main函数的汇编代码形式,也是程序的主要内容

第三部分:之后剩下的那一部分,这一部分是于记录程序的编译器信息和一些特殊的段(section)定义。这些部分主要提供有关程序编译环境和一些特定要求的元数据信息,用于调试、工具分析或其他特定目的。

.ident 指令

.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0"

该指令用于记录编译该程序的编译器的版本信息。在这里,它表明程序是使用 GCC(GNU Compiler Collection)版本 9.4.0(Ubuntu 9.4.0-1ubuntu1~20.04)编译的。这对于程序的维护、调试和确保兼容性等方面非常有用。例如,当开发者需要排查程序中的某些编译器特定行为,或者需要确保在不同环境中重新编译程序的一致性时,这个信息可以提供重要的参考。

.section .note.GNU-stack 指令

这个指令用于定义一个名为 .note.GNU-stack 的段。.note.GNU-stack 段通常用于指示堆栈的属性。它告诉链接器和操作系统关于堆栈的一些特殊要求,例如是否需要执行权限等。在程序运行时,操作系统会根据这些信息来设置堆栈的属性,以确保程序的正确执行和安全。例如,在某些情况下,为了防止某些类型的恶意攻击(如堆栈缓冲区溢出攻击),堆栈可能被标记为不可执行,而这个段的信息可以帮助操作系统正确地进行此类设置。

.section .note.gnu.property 和相关指令

.section .note.gnu.property,"a" :定义了一个名为 .note.gnu.property 的段,并且 "a" 表示该段具有分配(allocatable)属性,即该段在程序运行时会被分配内存空间。

.align 8 :对齐指令,确保段中的数据按照 8 字节对齐。

.long 指令序列 :

这些 .long 指令用于定义一系列 32 - 位的整数常量。这些常量的含义和用途通常与特定的工具或调试器有关。例如,1f - 0f、4f - 1f 和 5 等表达式可能用于描述段的长度或其他属性。

.string "GNU" :定义了一个字符串常量 "GNU",这通常用于标识该段与 GNU 工具链相关。

后面的一系列 .long 指令 :0xc0000002、3f - 2f、0x3、0x3 等常量可能用于描述程序的一些特定属性或要求,这些属性或要求可能与处理器架构、运行时环境等有关。这些信息通常是由编译器根据目标平台和编译选项自动生成的,用于提供程序的额外元数据,帮助工具或系统更好地理解和处理该程序。

第二部分是hello.s文件的重点,接下来做详细说明编译器(cc1)对各个数据类型以及各类操作的处理

3.3.1数据

常量:以立即数的形式出现,立即数是指指令中直接给出的数值。

立即数通常用$符号开头来表示

如图:

图3.4 立即数

图像中24行的5就是一个立即数,它来自于源文件中的if语句:

图3.5立即数

所以通过$做前缀

变量:

全局变量:在 .data 或 .bss 节区分配固定内存地址。(因为全局变量是符号)

局部变量:在栈上动态分配(通过 ebp/esp 或 rsp)。(局部变量不是符号)

如图:

图3.6 变量

先分配了空间,包括局部变量int i在内的数据处于这个部分,然后再对变量i进行初始化。两条指令不在一起的原因是源文件中i并不是定义后就初始化的。

图3.7变量

静态变量:类似全局变量,但作用域限于当前文件(.data 或 .bss + static 标签。静态变量由当前文件定义,也只能被当前文件引用)。

表达式:

汇编中直接展开表达式

类型:

宏:宏定义的数据在汇编中直接被其真正的值替代

3.3.2 赋值

“=”:通过mov实现

逗号操作符:被当作两条指令,分行执行

赋初值/不赋初值:

不赋初值的时候,只会减小rsp留出空间,等到赋值的时候再把数值存在栈中

这就是一个不赋初值的例子,int i 直到赋初值是才存入数据

3.3.3 类型转换

隐式转换:在汇编语言中,当操作数的类型不同时,可能会发生隐式类型转换。例如,将一个 8 位数移动到一个 16 位寄存器时,通常会自动进行符号扩展或零扩展,这取决于寄存器的类型和操作数的符号。例如:

MOVZX:无符号扩展

MOVSX:有符号扩展

显示转换:可以通过特定的指令来实现显式类型转换。例如:

CBW、CWD、CDQ:对累加器有符号扩展
其中B表示一字节大小,W表示一个字大小,D表示两个字,Q表示四个字

3.3.4  sizeof

汇编语言中直接替换为对应的常量

3.3.4 算数操作

算术运算类指令指令(20条)

ADD、ADC、AAA、DAA:  加法

INC:加“1”

SUB、SBB、AAS、DAS:  减法

DEC:减“1”

CMP:比较;就是做减法,改变标志位,不修改被减数

NEG:求补指令;x=-x 等价于0-X

MUL、IMUL、AAM:乘法;

DIV、IDIV、AAD:除法

CBW、CWD、CDQ:对累加器有符号扩展
 linux CBTW、CWTD、CLTQ

MOVZX:无符号扩展

MOVSX:有符号扩展

3.3.5 逻辑/位操作

逻辑运算指令(13条)-位操作指令

NOT:  求反

AND: 与、逻辑乘

OR: 或、逻辑加

XOR: 异或

TEST:位测试,同AND,不修改操作数

SHL、SHR、SAL、SAR:逻辑/算术移位

ROL、ROR、RCL、RCR:循环移位、带进位循环移位

3.3.6 关系操作

cmpl %ebx, %eax        # 比较 eax 和 ebx

je  equal_label        # if (eax == ebx)

jg  greater_label      # if (eax > ebx)

3.3.7 数组/指针/结构操作

数组操作:

movl array(,%esi,4), %eax  # eax = array[i](i 在 esi)

指针操作:

leal var, %eax         # eax = &var

movl (%eax), %ebx      # ebx = *eax

结构操作:

movl struct_ptr, %eax

movl 4(%eax), %ebx     # ebx = struct_ptr->id

3.3.8 控制转移

控制转移类指令(28条)

CALL、RET:  子程序调用、返回

JMP:无条件转移指令

JZ/JNZ/JE/JNE、JC/JNC、JO/JNO、/JS/JNS、JP/JNP/JPE/JNPE:条件转移-按标志位转移指令

JA、JAE、JB、JBE:条件转移-无符号数比较转移

JG、JGE、JL、JLE:条件转移-有符号数比较转移

LOOP:循环  cx- -, jnz L  

LOOPE 、LOOPNE:条件循环

JCXZ:计数器CX==0转移

INT n:中断调用(BIOS/OS系统调用)

IRET:中断返回

INTO:溢出中断指令

3.3.9 函数操作

call function_name     # 调用函数

movl %ebp, %esp

popl %ebp

Ret 函数返回

3.4 本章小结

本章聚焦于编译阶段,阐述了编译的概念与作用,即将预处理后的源代码翻译成汇编代码的过程。介绍了在Ubuntu下进行编译的命令,并对编译后的结果进行了深入解析。分析了编译器对数据类型、变量、表达式、控制结构等的处理方式,如立即数的表示、局部变量和全局变量的分配、赋值操作的实现等。通过对Hello程序编译结果的详细解读,展示了编译器如何将高级语言代码转换为汇编语言代码,为后续的汇编和链接阶段做好准备。本章使读者深入了解编译过程中代码的转换和优化机制。


第4章 汇编

4.1 汇编的概念与作用

接下来,汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的17个字节是函数main的指令编码。

(这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。)

4.2 在Ubuntu下汇编的命令

或者

当然也可以

4.3 可重定位目标elf格式

首先输入命令readelf -a hello.o > hello.elf

获得hello.o的ELF格式。其中包含ELF头,程序表头,段,节头表,节。

(此外也可以对可执行文件进行readelf指令来查看各个结构,下文的部分图示就是通过这种方法实现的)

4.3.1ELF头:

ELF(Executable and Linkable Format)头是ELF文件的开头部分,包含描述文件结构和属性的关键元数据。它是操作系统和链接器加载、执行或分析ELF文件的基础。

包含字大小、字节顺序、文件类型(.o,exec,.so),机器类型,等等

图4.1ELF文件头

如图,其中包含了描述整个文件的基本属性的信息。这些信息包括文件的类型(例如可执行文件、可重定位文件等)、机器类型(如 x86、ARM 等架构)、文件的入口点地址(程序开始执行的地址)、程序头表和段头表的位置等关键信息。例如,在一个 ELF 可执行文件中,入口点地址指向程序的主函数(main 函数)或者操作系统引导程序的入口点。

4.3.2节头:

ELF文件的节头(Section Header) 是描述文件中各个节区(Section) 的元数据表。每个节区存储特定类型的数据(如代码、数据、符号表等),而节头表则记录了这些节区的名称、类型、地址、大小、对齐方式等关键信息。节头表主要用于链接和调试,例如确定代码段的位置、解析符号或提取调试信息。

节头表包含了每个节的偏移量和大小

图4.2节头

4.3.3程序头:

程序头表在可执行文件(ET_EXEC)和共享库(ET_DYN) 中尤为重要,而目标文件(.o文件)通常不包含程序头表(因为不需要加载到内存)。

所以在生成的ELF文件中,则有如下的情况:

图4.3程序头

4.3.4 符号表

符号表主要用来记录程序中的各种符号(symbol)及其相关信息。符号可以是函数名、全局变量名等。

.symtab 节(符号表)包括:

符号表

函数和静态变量名

节名称和位置

在编译过程中,编译器会生成符号表,其中包含了各个符号的名称、地址、大小等信息。当进行链接操作时,链接器需要通过符号表来解析不同模块之间的外部引用关系。比如,一个程序由多个源文件组成,在链接阶段,链接器要找到各个源文件中相互引用的函数和变量,并将它们正确地连接起来,符号表就是链接器完成这个任务的关键依据。

如图:

图4.4符号表

在符号表中,Num为某个符号的编号,Name是符号的名称。Size表示他是一个位于.text节中偏移量为0处的146字节函数。Bind表示这个符号是本地的还是全局的。

从图中可以看到符号表中记录了这个文件中用到的所有的函数。由于在此还没有进行相关库函数的链接,因而Value都为0。

4.3.5重定位项目

ELF的重定位项目(Relocation Entries)是链接过程中用于修正代码和数据中地址引用的关键信息。它们记录了目标文件中需要调整的位置、符号依赖关系及计算方式,确保程序在链接或加载时能正确指向最终的内存地址。

可重定位的节以.rel开头,通过节头看出来包括

.rel.text (可重定位文件,含有:.text 节的可重定位信息,

在可执行文件中需要修改的指令地址,

需修改的指令,)

.rel.eh_frame(.rel.eh_frame 是 .eh_frame 段的重定位表,用于在链接过程中对 .eh_frame 段中的某些地址进行重定位,以便正确地设置异常处理机制中涉及到的地址信息,使程序能够在不同的内存地址空间中正确运行。)

除此之外有时还有

.rel.data(可重定位数据,是.data节的可重定位信息,含有在合并后的可执行文件中需要修改的指针数据的地址)

如图:

图4.5重定位节

从中可以看到,重定位节通过偏移量确定了文件中各个函数的地址位置

偏移量 :这是在该重定位节中的偏移地址。简单来说,就是在这个重定位节中,这个条目对应的位置。

信息 :这是用于重定位的一些控制信息,通常包含一些标志位,用于指示重定位的操作细节。

类型 :这是重定位的类型,用于处理指令或数据地址相对于当前程序计数器位置的计算方式。

符号值和符号名称

4.4 Hello.o的结果解析

对hello.o进行反汇编,得到了汇编代码:

图4.6 反汇编

1.通过观察可以发现hello.o的反汇编只包含main函数中的代码,与hello.s中基本一致。相比于hello.s没有其他部分,这是因为:

在将 .s 文件汇编成 .o 文件的过程中,汇编器主要关注的是将汇编指令转换为机器指令。对于初始化设置等内容,它们在编译阶段已经被处理好,但在目标文件的组织上会有所变化。例如,程序的初始化代码(如初始化全局变量的代码)在 .s 文件中可能会以一段独立的汇编代码形式存在,但在生成 .o 文件时,这些初始化相关的操作可能会被组织到特定的数据段或由链接器在链接过程中与其他目标文件中的相关部分进行整合,而不会像在 .s 文件中那样明显地作为一段独立的代码存在。

  1. 反汇编代码与hello.s文件的第二点不同就是每一行反汇编代码前都有机器语言指令,这是计算机执行时真正接受的指令。每一条汇编语言都可以用机器二进制数据来表示,汇编语言中的操作码和操作数以一种相当于映射的方式和机器语言进行对应,从而让机器能够真正理解代码的含义并且执行相应的功能。

如图所示:

  1. 机器代码与汇编代码不同的地方在于:

分支转移:在hello.s中,分支转移是由通过节头(.L2等)来标识,而在机器语言反汇编程序中,分支转移直接跳入目的地址

立即数:原本十进制的立即数都变成了二进制。这个很好理解,输出的文件是二进制的,对于objdump来说,直接将二进制转化为十六进制比较方便,也有利于程序员以字节为单位观察代码。

函数调用:在hello.s中,函数调用通过call函数名称实现函数调用;而在反汇编程序中,call的目标地址是当前指令的下一条指令。

如图,在这之中这些特殊的指令就是可重定位条目,可重定位的目标文件会插入此处

4.5 本章小结

本章介绍了汇编的概念与作用,即汇编器将汇编语言代码翻译成机器语言指令,并生成可重定位目标文件。讲述了在Ubuntu环境下进行汇编的命令及其选项。重点分析了可重定位目标文件的ELF格式,包括ELF头、节头、程序头、符号表和重定位项目等结构。通过对Hello程序的hello.o文件的解析,展示了汇编过程如何将汇编代码转换为机器代码,并为链接阶段提供必要的信息。例如,重定位项目记录了目标文件中需要调整的位置和符号依赖关系,确保程序在链接后能正确指向最终的内存地址。本章帮助读者理解汇编阶段在程序开发中的重要性和具体实现。


5链接

5.1 链接的概念与作用

在将代码生成可执行文件的过程中,链接阶段是一个至关重要的环节,它起着承上启下的作用,将编译后的多个目标文件(通常是 .o) 文件整合成一个完整的可执行程序,以下是其详细的概念与作用:

概念

链接阶段是软件构建过程中的一个关键步骤,它发生在编译阶段之后。在编译阶段,源代码文件被编译成目标文件(.o 文件),这些目标文件包含了机器代码、数据、符号表等信息。但每个目标文件通常是不完整的,因为它们可能依赖于其他目标文件或库中的函数、变量等。链接阶段的主要任务就是将这些相互依赖的目标文件和库文件进行整合和处理,使其成为一个可以独立运行的可执行文件。

作用

1: 模块化

程序可以编写为一个较小的源文件的集合,而不是一个整体巨大的一团.

可以构建公共函数库 (稍后详述)

例如, 数学运算库, 标准C库

 2: 效率

时间: 分开编译

更改一个源文件,编译,然后重新链接.

不需要重新编译其他源文件.

空间: 库

可以将公共函数聚合为单个文件...

然而,可执行文件和运行内存映像只包含它们实际使用的函数的代码.

5.2 在Ubuntu下链接的命令

在终端中,输入ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o并回车,结果如下图所示:                        

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

 可执行目标文件的格式类似于可重定位目标文件(hello.o)的格式,但稍有不同。ELF头中字段e_entry给出执行程序时的第一条指令的地址,而在可重定位文件中,此字段为0。可执行目标文件多了一个程序头表,也称为段头表,是一个结构数组。可执行目标文件还多了一个.init节,用于定义_init函数,该函数用来执行可执行目标文件开始执行时的初始化工作。因为可执行目标文件不需要重定位,所以比可重定位目标文件少了两个.rel节。

与观察hello.o文件的ELF文件方法基本一致

ELF头:

图5.1 ELF头

对比发现类型由可重定位文件变成了可执行文件,程序头/段头文件也出现了,节头的数量增加到了27个。

节头:

链接后,节头数量有所增加,但各条目内包含信息种类并未发生变化

程序头:ELF文件的程序头(Program Header) 是描述可加载段(Segment) 的元数据表,用于指导操作系统或动态链接器如何将程序加载到内存并执行。

其中包括了页面大小,虚拟地址内存段(节),段大小

符号表:

链接后,符号表条目显著增加,包含各目标文件中符号定义及引用信息。

重定位项目:

链接后,原来用于重定位代码和数据等的重定位文件已经不需要了,产生了新的重定位节,重定位节内容变化为执行过程中需要通过动态链接调用的函数,同时类型也发生改变。

5.4 hello的虚拟地址空间

使用edb打开hello

打开memory regions,得到

找到hello的起始和终止地址是0x400000-0x405000

其中0x401000-0x402000是可执行部分,这和段表中.init的位置是吻合的

.init

查看其他段的地址,也都和节头给出的地址吻合

.interp

.rodata 只读数据

.dynamic

.eh_frame   

5.5 链接的重定位过程分析

首先先对可执行文件hello进行反汇编:

  1. hello文件反汇编后多出了许多内容,因为它将节头中所有的段全部反汇编了,而重定位文件只反汇编了主函数。
  2. 虚拟地址不同,hello.o的反汇编代码虚拟地址从0开始,而hello的反汇编代码虚拟地址从0x400000开始。这是因为hello.o在链接之前只能给出相对地址,而hello在链接之后得到的是绝对地址。

可重定位文件(如 .o 文件)生成可执行文件的链接过程,主要分为 符号解析、地址与空间分配 和 重定位 三个核心步骤。重定位是链接过程中最关键的部分,用于修正代码中的地址引用,使其指向正确的内存位置。

符号解析(Symbol Resolution)

这一步骤主要依靠符号表对符号的解析实现,符号包括全局变量和函数

目标:解决符号(函数、变量等)的引用关系,确保每个符号都有唯一的定义。

链接器扫描所有输入文件(.o 和静态库),构建全局符号表。

若符号未定义或重复定义,链接器会报错(如 undefined reference)。

  1.  重定位(Relocation)

这是一个方便解释的示例图:

链接器将所有输入文件的段(Section,如 .text、.data)合并,并为它们分配最终的内存地址。每个段在可执行文件中的起始地址被确定

可以看到,我们在ld 指令中选择的其他可重定位文件的函数,就紧跟在main函数后面,代表它们确实是一起放在了代码段,并且每个函数都有了确定的起始地址

另一个例子就是所有main函数里引用的共享库的函数,也放在同一个端里面,

可以发现相比于可重定位文件里面每次调用函数都直接从下一条指令开始,可执行文件的函数都有固定的地址,使用时跳转到对应位置执行

2. 重定位的实现机制

(1) 重定位表(Relocation Table)

每个可重定位文件包含重定位表,记录了需要修正的位置及其类型。

重定位条目包含以下信息:

偏移量(Offset):需要修正的位置在段内的偏移。

符号(Symbol):引用的目标符号(如 printf)。

类型(Type):重定位类型(如绝对地址或相对地址)。

(2) 重定位类型

绝对地址重定位(如 R_X86_64_32):

直接替换为符号的最终地址。

相对地址重定位(如 R_X86_64_PC(或者PLT,表示这是一个共享库的函数) _32)

例如:

计算符号地址与当前指令地址的差值。

  1. 重定位计算

根据重定位类型进行相应的计算即可

5.6 hello的执行流程

1.使用edb执行hello,首先,最初的程序地址是hello使用的动态链接库ld-2.2.27.so的入口点_dl_start:

2.然后,程序跳转到_dl_init,在经过了一系列初始化后,跳到hello的程 序入口点_start;

3.然后程序通过call指令跳到动态链接库ld-2.27.so的_libc_start_main 处,这个函数会进行一些必要的初始化,并负责调用main函数;

4. 下一步,程序调用动态链接库中的__cxa_atexit函数,它会设置在程序结束时需要调用的函数表;

5. 然后返回到__libc_start_main继续,然后调用hello可执行文件中的__libc_csu_init函数,这函数是由静态库引入的,也是做一些初始化的工作;

6. 然后程序返回到__libc_start_main继续,紧接着程序调用动态链接库里的_setjmp函数,设置一些非本地跳转;

7.然后返回到__libc_start_main继续,正式开始调用main函数;

8. 由于我们在edb运行hello的时候并未给出额外的命令行参数,因此它会在第一个if处通过exit(1)直接结束程序;

9. 通过hello本身携带的exit函数,程序会跳转;

10. 之后,在进行了若干操作后,程序退出。

5.7 Hello的动态链接分析

 当程序调用共享库函数时,由于共享模块的加载地址不确定,编译器无法预先确定函数地址。为此,编译系统采用延迟绑定技术,将函数地址解析推迟到首次调用时完成。

延迟绑定通过两个关键数据结构实现:

PLT(过程链接表):由16字节代码单元组成的数组,其中PLT[0]为特殊入口,用于跳转至动态链接器。每个被调用的库函数都有对应的PLT条目,负责触发具体函数的绑定流程。

GOT(全局偏移表):存储8字节地址的数组,GOT[0]和GOT[1]保存动态链接器所需信息,GOT[2]指向动态链接器入口。其余条目与需解析的函数一一对应,并关联特定PLT条目。

函数首次调用流程如下:

控制流跳转至目标函数的PLT条目,其首指令通过GOT间接跳转(初始时GOT指向PLT的第二条指令,形成回调)。

随后将函数ID压栈,跳转至PLT[0]触发动态链接器(通过GOT[1]传递参数,经GOT[2]进入链接器)。

动态链接器解析函数地址并更新GOT,最终将控制传递给目标函数。后续调用可直接通过GOT跳转,无需重复解析。

hello程序的重定位操作仅在动态链接器加载后执行,加载前后的重定位状态存在差异。

.plt

.got

5.8 本章小结

本章详细讲解了链接的概念与作用,链接是将多个目标文件和库文件整合成一个可执行程序的关键步骤。介绍了在Ubuntu下进行链接的命令,并分析了可执行目标文件hello的格式,包括ELF头、节头、程序头、符号表等的变化。重点探讨了链接过程中的重定位机制,如符号解析、地址与空间分配、重定位表的结构和重定位类型的计算方法。通过对比链接前后的文件,说明了链接如何将分散的目标文件和库文件中的符号引用进行修正,使其指向正确的内存位置。此外,还对Hello程序的虚拟地址空间进行了分析,展示了链接后的程序在内存中的布局情况。本章使读者深入了解链接阶段在程序构建中的重要作用和复杂过程。


6hello进程管理

6.1 进程的概念与作用

定义: A process is an instance of a running program.一个执行中程序的实例

计算机科学最深刻的概念之一

不同于“程序”或“处理器”

进程提供给应用程序两个关键抽象

1.逻辑控制流 (Logical control flow)

每个程序似乎独占地使用CPU

通过OS内核的上下文切换机制提供

2私有地址空间 (Private address space)

每个程序似乎独占地使用内存系统

OS内核的虚拟内存机制提供

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

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

Shell是一种交互型程序,用于代表用户运行其他程序。

Shell壳:shell  是一个交互型应用级程序,代表用户运行其他程序。如Windows下的命令行解释器,cmd、powershell,图形界面的资源管理器。Linux下的Terminal/tcsh、bash等等,当然也包括图形化的GNOME桌面环境

Shell是信号处理的代表,负责各进程创建与程序加载运行及前后台控制,作业调用,信号发送与管理等。

Shell执行一系列的读/求值 步骤

读步骤读取用户的命令行,求值步骤解析命令,代表用户运行

Shell的处理流程如下:首先对命令行参数求值,判断命令行是否为空,如果不为空则判断第一个命令行参数是不是一个内置的命令,如果是一个内置命令则直接执行,否则检查是否是一个应用程序。之后在搜索路径里寻找这些应用程序,如果键入的命令不是一个内部命令并且路径里没有找到这个可执行文件,则会显示一条错误信息。如果能够成功找到命令,那么该内部命令或者应用程序将会被分解为系统调用并传递给linux内核。

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的运行的子进程

int fork(void)

子进程返回0,父进程返回子进程的PID

新创建的子进程几乎但不完全与父进程相同:

子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(代码、数据段、堆、共享库以及用户栈)

子进程获得与父进程任何打开文件描述符相同的副本

子进程有不同于父进程的PID

fork函数:被调用一次,却返回两次

调用一次,返回两次

并发执行

不能预测父进程与子进程的执行顺序

相同但是独立的地址空间

fork返回时(第6行),x在父进程和子进程中都为1

X为局部变量-reg/stack

后面,父进程和子进程对x所做的任何改变都是独立的

共享文件

stdout文件在父、子进程是相同的

6.4 Hello的execve过程

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

在当前进程中载入并运行程序:  loader加载器函数

filename:可执行文件

目标文件或脚本(用#!指明解释器,如 #!/bin/bash)

argv:参数列表,惯例:argv[0]==filename

envp:环境变量列表

"name=value" strings (e.g., USER=droh)

getenv, setenv, unsetenv, printenv

Loader删除子进程现有的虚拟内存段,创建一组新的段(栈与堆初始化为0),并将虚拟地址空间中的页映射到可执行文件的页大小的片chunk,新的代码与数据段被初始化为可执行文件的内容,然后跳到_start………… 除了一些头部信息实际没读文件,直到缺页中断

覆盖当前进程的代码、数据、栈

保留:有相同的PID,继承已打开的文件描述符和信号上下文

Called once and never returns(调用一次并从不返回)

…除非有错误,例如:指定的文件不存在

6.5 Hello的进程执行

6.5.1逻辑控制流

逻辑控制流是一个PC值的序列,PC值就是程序计数器的值,这些值与可执行目标文件的指令或者包含在运行时动态链接到程序的共享对象中的指令一一对应。

6.5.2 时间分片

在现代计算机体系中,进程是轮流使用处理器的,每个进程都执行它的流的一部分,然后被抢占(暂时挂起),再轮到其它进程。一个逻辑流的执行在时间上与另一个流重叠被称为并发流,这两个流并发运行。

多个流并发执行的概念被称为并发。一个进程与其他进程轮流运行的概念称为多任务。一个进程执行其控制流一部分的每一个时间段叫做时间片,多任务也就被称作是时间分片。

6.5.3 用户模式与内核模式

为了保护操作系统内核,处理器在某一个控制寄存器中的一个模式位,设置模式位时,进程就运行在内核模式中,否则运行在用户模式。内核模式的代码可以无限制地访问所有处理器指令集以及全部内存和 I/O 空间。如果用户模式的进程要享有此特权,它必须通过系统调用向设备驱动程序或其他内核模式的代码发出请求。另外,用户模式的代码允许发生缺页,而内核模式的代码则不允许。

运行程序代码初始时都是在用户模式中的,当发生中断故障或系统调用的异常时,进程从用户模式转变为内核模式。当异常发生时,控制传递到异常处理程序,处理器将模式转变为内核模式。内核处理程序运行在内核模式中,当它返回到应用程序代码时,处理器把模式从内核模式改回到用户模式。

6.5.4 进程上下文切换

上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这样的决策叫做调度,由内核中的调度器的代码处理。在这个抢占过程中需要用到上下文切换,上下文切换保存当前进程的上下文,恢复先前某个被抢占的上下文,并将控制传递给新恢复的进程。

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

6.6 hello的异常与信号处理

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

异常:异常是指为响应某个事件将控制权转移到操作系统内核中的情况

内核指操作系统常驻内存的部分

事件示例:被零除、算术运算溢出、缺页、 I/O请求完成、键盘输入

异常基本有四种,其中中断是异步异常,陷阱,故障和终止是同步异常

中断:

处理器外部I/O设备引起

由处理器的中断引脚指示

中断处理程序返回到下
一条指令处

Examples:

时钟中断(Timer interrupt)

定时器芯片每隔几毫秒触发一次中断

内核从用户程序取回控制权

外部设备的I/O中断(I/O interrupt from external device)

键盘上敲击一个 Ctrl-C

网络数据包到达

磁盘数据的到达

陷阱:

有意的,执行指令的结果

Examples: 系统调用(System Call),用户程序和内核之间的一个接口

陷阱处理程序将控制返回到下一条指令

故障

不是有意的,但可能被修复

Examples: 缺页(可恢复),保护故障(protection faults,不可恢复), 浮点异常(floating point exceptions)

处理程序要么重新执行引起故障的指令(已修复),要么终止

终止

非故意,不可恢复的致命错误造成

Examples: 非法指令,奇偶校验错误(parity error),机器检查(machine check)

中止当前程序

信号:

就是一条小消息,它通知进程系统中发生了一个某种类型的事件

类似于异常和中断

从内核发送到(有时是在另一个进程的请求下)一个进程

信号类型是用小整数ID来标识的(1-30)   ;     现代OS  64类信号

信号中唯一的信息是它的ID和它的到达

SIGINT:当用户按下Ctrl+C时发送,通常用于中断程序。

SIGTSTP:当用户按下Ctrl+Z时发送,用于暂停程序。

SIGTERM:请求程序终止的正常信号。

处理方法;

忽略信号:可以通过将信号设为待处理或者屏蔽

响应该信号:包括终止,挂起等

各种情况:

不停乱按(包括回车)

可以看到不停乱按的不同信号被忽略了

带有回车的输入内容被视为指令查找,然后判断是否存在该指令

Ctrl-c:

程序直接终止

Ctrl-Z:

首先,程序被终止(暂停)

使用ps:

看到了所有的前台进程

使用jobs:

显示了shell中的后台进程

使用pstree:

显示了进程间的父子关系

fg:

让hello重新变成前台程序,变成运行状态(图中为fg之后再ctrl-Z一次)

Kill:

将选定的进程杀死

6.7本章小结

本章围绕hello程序的进程管理展开讨论。首先介绍了进程的概念与作用,强调了进程作为程序运行实例的重要性。接着阐述了壳Shell-bash的作用与处理流程,说明了Shell如何代表用户运行其他程序。详细分析了Hello程序在fork进程创建过程中的特点,如子进程与父进程的相似性与独立性。深入探讨了execve过程,解释了如何在当前进程中加载并运行新的程序。最后,描述了Hello程序的进程执行过程,包括逻辑控制流、时间分片、用户模式与内核模式的转换以及进程上下文切换等内容。本章为读者揭示了程序在操作系统中如何被管理和调度的机制。


7hello的存储管理

7.1 hello的存储器地址空间

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

物理地址:CPU通过地址总线的寻址,找到真实的物理内存对应地址。CPU对内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的。在前端总线上传输的内存地址都是物理内存地址。

物理地址空间: M = 2m 个物理地址的集合
{0, 1, 2, 3, …, M-1}

逻辑地址空间:段地址:偏移地址

23:8048000     段寄存器(CS等16位):偏移地址(16/32/64)

实模式下:   逻辑地址CS:EA    =è物理地址CS*16+EA

保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址,         

            段地址+偏移地址=线性地址。

线性地址:线性地址空间是一个非负整数的集合。逻辑地址经过段机制后转化为线性地址,以段地址:偏移量的组合形式。在调试hello时,gdb中查看到的就是线性地址,或者虚拟地址。

线性地址空间: 非负整数地址的有序集合:
{0, 1, 2, 3 … }

虚拟地址:虚拟地址空间是0到N的所有整数的集合(N是正整数),是线性地址空间的有限子集。分页机制以虚拟地址为桥梁,将硬盘和物理内存联系起来。

虚拟地址空间: N = 2n 个虚拟地址的集合 ===线性地址空间
{0, 1, 2, 3, …, N-1}

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

被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址

在 Intel 平台下,逻辑地址是 selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。

CS寄存器(代码段寄存器): CS寄存器存储了当前执行的指令所在的代码段的起始地址。它是一个16位寄存器,指示了代码在内存中的位置。CS寄存器的值与代码段的段基址相关,形成了代码段的起始物理地址。

EIP寄存器:用来存储CPU要读取指令的地址,CPU通过EIP寄存器读取即将要执行的指令。每次CPU执行完相应的汇编指令之后,EIP寄存器的值就会增加。

如果用 selector 去 GDT( Global Descriptor Table,全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 linear address(线性地址)。这个过程就称作段式内存管理。

逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符),可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

全局的段描述符,放在GDT中,一些局部的段描述符,放在“LDT(Local Descriptor Table,局部段描述符表)”中。

给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地址。再由基地址加上偏移量的值,便得到了线性地址。

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

线性地址(VA)到物理地址(PA)之间的转换通过分页机制完成。分页机制类似主存和Cache之间的分块机制,分页机制对虚拟地址和物理内存进行分页,页的大小通常是4KB到4M。在x86-64机器上,虚拟地址空间的N是2的48次方,有256TB,比正常的硬盘大得多。

在分页机制中,硬盘空间的每个字节到虚拟地址空间的每个字节存在映射关系,且这个映射是单射。虚拟地址空间和硬盘空间都以字节为单位,从0开始编地址号。设硬盘空间为H,虚拟地址空间为V,设他们之间的映射关系为,f是单射,则于是,我们知道了物理地址中某个地址所在页与虚拟空间的页的对应关系,也就知道了物理地址中某个地址所在页与硬盘中某个页的对应关系。

每个进程都有一个页表,页表中的每一项,即PTE(页表条目),记录着该对应的虚拟地址空间的那一页是否有效(即是否有对应的物理内存上的页),物理页的起始位置或磁盘地址,访问权限等信息。PTE根据不同的映射状态也被划分为三种状态:未分配、未缓存、已缓存。

未分配:虚拟内存中未分配的页

未缓存:已经分配但是还没有被缓存到物理内存中的页

已缓存:分配后缓存到物理页块中的页

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

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

页表是 PTE(页表条目)的数组,它将虚拟页映射到物理页,每个 PTE 都有一个有效位和一个 n 位地址字段,有效位表明该虚拟页是否被缓存在 DRAM 中。虚拟地址分为两个部分,虚拟页号(VPN,Virtual Page Number)和虚拟页面偏移量(VPO,Virtual Page Offset)。其中VPN需要在PTE中查询对应,而VPO则直接对应物理地址偏移(PPO)。

TLB(translation lookaside buffer,地址转换后备缓冲器,习惯称之为“快表”)是一个位于MMU(Memory Management Unit,内存管理单元)中,关于PTE的一个缓存。TLB是一个小的、虚拟寻址的缓存,其中每一行均保存了一个由单个PTE组成的块。TLB有高度的相联性,能够加速地址翻译,而多级页表能够对页表进行压缩,便于大量存储。

在从VA翻译得到PA的过程中,MMU首先用VPN向TLB申请请求对应的PTE,如果命中,那么直接跳过后面的步骤;之后MMU生成PTE地址,从高速主存请求得到PTE,高速缓存或主存会向MMU返回PTE。若PTE有效位为0,说明缺页,MMU触发缺页异常,缺页处理程序确定物理内存中的牺牲页(若页面修改,则换出到磁盘)。之后缺页处理程序调入新的页面,并更新PTE。之后却也处理程序返回原进程,并重新执行导致缺页的指令。

假设:

4KB (212) 页面, 48位地址空间, 8字节 PTE

问题:

将需要一个大小为 512 GB 的页表!

248 * 2-12  * 23 = 239 bytes

常用解决办法: 多级页表

以二级页表为例:

一级页表: 每个 PTE 指向一个页表 (常驻内存)

二级页表: 每个 PTE 指向一页(paged in and out like any other data页面可以调入或调出页表)

这样做能够极大地节省空间,原因是只有二级(最高级)页表是常驻内存,一级页表并不需要永远存在于内存中,只是根据二级页表的数量合理分配即可。这样大大减少了内存中实际存在页表的个数,节省了空间。

而四级页表同理,通过多级页表的操作,极大的减少了所需要的空间。

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

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

通过内存地址的组索引获得值,如果对应的值是data则像L1 d-cache对应组中查找,如果是指令,则向L1 i-cache对应组中查找。将L1对应组中的每一行的标记位进行对比,如果相同并且有效位为1则命中,获得偏移量,取出相应字节,否则不命中,向下一级cache寻找,直到向内存中寻找。

7.6 hello进程fork时的内存映射

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

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

当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.设置程序计数器(PC)。execve 做的最后一件事情就是设置当前进程 上下文的程序计数器,使之指向代码区域的入口点。

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

Page fault缺页: 虚拟内存中的字不在物理内存中 (DRAM 缓存不命中)

缺页故障:一个虚拟页没被缓存在DRAM中,即DRAM缓存不命中被称为缺页。当CPU引用了一个页表条目中的一个字,而该页表条目并未被缓存在DRAM中,地址翻译硬件从内存中读取该页表条目,从有效位为0可以判断尚未被缓存,进而触发缺页异常。

缺页中断处理:缺页异常调用缺页异常处理程序,该程序会选择一个牺牲页,如果这个牺牲页在DRAM中已被修改,那么就将他写回磁盘,之后将引用的虚拟页复制到内存中的原来牺牲页所在位置,并对页表条目进行更新,随后返回。当异常处理程序返回时,它会重新启动缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。此时需要调用的虚拟页已经缓存到主存中了,则页命中可以由地址翻译硬件正常处理。

7.10本章小结

本章聚焦于hello程序的存储管理。介绍了hello程序的存储器地址空间,包括物理地址、逻辑地址、线性地址和虚拟地址的概念及其相互关系。详细阐述了Intel逻辑地址到线性地址的变换过程——段式管理,以及线性地址到物理地址的变换过程——页式管理。分析了TLB与四级页表在虚拟地址到物理地址转换中的作用,以及三级Cache支持下的物理内存访问机制。探讨了hello进程在fork和execve时的内存映射情况,以及缺页故障与缺页中断处理的过程。本章使读者深入了解程序在运行过程中如何进行存储管理,以及存储管理对程序性能的影响。

结论

通过以上各章的详细分析,我们对Hello程序从源代码编写到最终执行的整个过程有了全面而深入的理解。从预处理阶段的文件包含和宏替换,到编译阶段的代码转换和优化,再到汇编阶段的指令生成和链接阶段的地址修正,每一个环节都紧密相关,共同决定了程序的最终质量和性能。在进程管理方面,操作系统通过fork和execve等机制为程序提供了独立的运行环境和资源分配,确保了程序的并发执行和高效调度。存储管理则通过段式和页式管理等技术,实现了虚拟地址到物理地址的映射,提高了内存的利用率和程序的运行效率。IO管理则为程序提供了与外部世界交互的接口,使得程序能够接收输入和输出结果。

在整个过程中,我们深刻体会到计算机系统各层之间的协同工作是实现程序成功运行的关键。从硬件平台到操作系统,再到编译器和链接器等工具,每一层都发挥着不可或缺的作用。通过对Hello程序的深入剖析,我们不仅掌握了程序开发的基本流程和原理,还对计算机系统的整体架构和运行机制有了更深刻的认识。这种理解将为我们未来在软件开发、系统优化和创新设计等方面的工作打下坚实的基础,使我们能够更好地应对计算机科学领域的各种挑战和机遇。


附件

hello.c:源代码文件,包含Hello程序的初始代码。

hello.i:预处理后的文件,包含了源代码文件经过文件包含、宏替换等预处理操作后的结果。

hello.s:编译后的汇编语言文件,由编译器将预处理后的代码转换而成。

hello.o:汇编后的可重定位目标文件,是汇编器将汇编语言代码转换为机器语言指令后的结果。

hello:链接后的可执行目标文件,是最终可以被操作系统加载并执行的程序文件。

这些中间文件展示了Hello程序从源代码到可执行程序的各个阶段的转换结果,对于理解程序开发过程具有重要的参考价值。

此外,在这过程中还可以生成了hello.o 和hello的ELF文件,用于查看相关的信息。但是本人使用的是在终端直接查看的方法,所以附件中没有。


参考文献

[1] Randal E. Bryant; David R.O'Hallaron. 深入理解计算机系统. 2016.7

[2]唐塑飞 计算机组成原理

[转]GCC编译的背后( 预处理和编译 汇编和链接 ) by falcon<zhangjinw@gmail.com> 2008-02-22 平时在Linux下写代码,直接用"gcc -o out in.c"就把代码编译好了,但是这后面到底做了什么事情呢?如果学习过编译原理则不难理解,一般高级语言程序编译的过程莫过于:预处理、编译、汇编、链 接。gcc在后台实际上也经历了这几个过程,我们可以通过-v参数查看它的编译细节,如果... 阅读详情

相关推荐

哈工大CSAPP2025春课程论文——程序人生Hello‘s P2P

P2P(From Program to Process):指高级语言(C语言)程序(program)经过一系列操作成为实际运行的进程(process)的过程。要让程序变成进程,需要先生成可执行文件,这一部分包括预处理、编译、汇编和链接四个过程。然后在shell中创建进程,运行进程。020(From zero to zero):最初的 “0” 代表系统初始状态,此时操作系统未为 “hello程序分配任何资源,内存中没有其相关内容,进程表中也没有对应的进程项。

Hardcore_Peak的博客 691

x86 - CPU架构/寄存器详解 (三) 保护模式

本文内容: CPU架构通用寄存器状态寄存器状态标志(Status Flags)控制标志(DF flag)系统标志以及IOPL域(System Flags and IOPL Field)系统地址寄存器GDTRLDTRIDTRTR段寄存器控制寄存器CR0CR1CR2CR3CR4调试寄存器其他寄存器EIPTSC浮点寄存器MSR参考资料   80286 也是一款 16 位的处理器,大部分的寄存器都和 8086 处理器一样。因此,80286 和 8086 一样,因为段寄存器是 16 位的,而且只能使用 16 位的偏移

Gyc8787的博客 7501

重温C语言 - 基本特性的实现

接着上篇继续对C语言做一些探索,这篇主要分析C语言中一些基本特性的实现。包括全局变量,静态变量,以及他们是否初始化以及链接属性做一些解析 一、全局变量 全局变量有初始化或未初始化之分,初始化了的全局变量保存在data段,未初始化全局变量保存在BSS段, data段和bss段都是程序的数据段。 对于初始化的全局变量,看下面程序: int global1 = 100;int mai

小小程序匠 920

程序人生 HELLO P2P

计算机系统 大作业 题 目 程序人生-Hello’s P2P 专 业 计算机系 学   号 1170301005 班   级 1703010 学 生 白镇北     指 导 教 师 计算机科学与技术学院 2018年12月 摘 要 摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全...

qq_41986714的博客 6906

HIT-ICS 2019程序人生Hello P2P

HIT-ICS 2019程序人生大作业 题目:Hello‘s P2P 学生:王世龙

weixin_43980832的博客 221

程序人生-Hello’s P2P

计算机系统 大作业 题 目程序人生-Hello’s P2P 专 业 人工智能(未来技术) 学   号 8200880122 班   级 2036014 学 生 高幸 指 导 教 师刘宏伟 计算机科学与技术学院 2022年5月 摘 要 本文以hello程序为例,讲述了程序在计...

m0_63949143的博客 393

程序人生Hello‘s P2P

1.1P2P:全称为From Program to Progress,指从项目到进程,期间需要通过预处理、编译、汇编、链接等一系列操作,将.c文件转化为可执行目标程序。接着由操作系统执行可执行目标文件,结束后退出。图表 1 P2P全过程020:全称是from 0 to 0,在程序一开始启动时内存、变量等初始化都是默认值,在编译运行的过程中,程序执行逻辑,处理输入,修改变量和内存,当程序成功退出时,一切又恢复默认。1.2。

2401_83275567的博客 734
originoa
博客等级 码龄3年 4粉丝 1原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值