哈工大ics大作业 程序人生-Hello’s P2P

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

计算机科学与技术学院

2025年5月

摘  要


本文以“程序人生-Hello's P2P”为研究对象,通过分析C语言程序“hello”从预处理、编译、汇编、链接到进程执行的全生命周期,结合Ubuntu环境下的工具链(如gcc、readelf、objdump、gdb等),深入探讨了计算机系统的核心机制。研究内容包括预处理阶段宏展开与头文件解析、编译阶段C语言到汇编代码的转换、汇编阶段生成可重定位目标文件、链接阶段符号解析与地址重定位,以及进程管理中的fork与execve机制、存储管理的虚拟地址到物理地址映射、动态链接与异常处理等。通过实验验证,揭示了程序在操作系统中的执行流程、内存管理策略及I/O接口的实现原理。最终,结合理论与实验分析,总结了程序P2P(Program to Process)和020(Zero to Zero)的运行本质,深化了对计算机系统分层设计与协同工作的理解。

关键词:预处理;编译;汇编;链接;进程管理;存储管理;动态链接;虚拟地址;异常处理                          

目  录

第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简介

1.1.1 P2P(Program to Process)过程:从程序到进程

  1. 预处理(Preprocessing)
    • 操作:预处理器(如cpp)处理源代码文件(如hell.c),展开宏定义、处理文件包含(如#include <stdi.h>)、删除注释等。
    • 输出:生成预处理后的文件(如hell.i),包含展开后的代码和头文件内容。
  2. 编译(Compilation)
    • 操作:编译器(如gcc)将预处理后的代码转换为汇编语言(如hell.s)。
    • 关键步骤:词法分析、语法分析、语义分析、代码优化和生成汇编代码。
    • 输出:汇编语言文件(如hell.s)。
  3. 汇编(Assembly)
    • 操作:汇编器(如as)将汇编代码转换为机器语言指令,生成可重定位目标文件(如hell.o)。
    • 关键点:机器指令与汇编指令一一对应,文件格式为二进制。
    • 输出:目标文件(如hell.o)。
  4. 链接(Linking)
    • 操作:链接器(如ld)将多个目标文件和库文件合并,解析符号引用,生成可执行文件(如hell)。
    • 关键点
      • 静态链接:合并代码和数据段。
      • 动态链接:加载共享库(如libc.so)。
    • 输出:可执行文件(如hell)。
  5. 进程创建(Process Creation)
    • 操作:在Shell中输入命令(如./hell),Shell通过frk()创建子进程,然后通过execve()加载可执行文件到子进程的地址空间。
    • 关键点
      • fork():复制父进程的地址空间,创建子进程。
      • execve():用可执行文件替换子进程的代码、数据和堆栈段。
    • 结果:程序从磁盘上的可执行文件转变为内存中的进程(Prcess)。


1.1.2 020(From Zero to Zero)过程:从无到有再到无

  1. 程序加载(Loading)
    • 操作:操作系统通过加载器将可执行文件的代码和数据加载到内存中,建立虚拟地址空间。
    • 关键点
      • 代码段(.text):只读,存放机器指令。
      • 数据段(.data和.bss):存放已初始化和未初始化的全局变量。
      • 堆和栈:动态分配内存和局部变量。
  2. 进程执行(Execution)
    • 操作:CPU从main()函数的入口地址开始执行指令,通过系统调用(如write)与终端交互。
    • 关键点
      • 上下文切换:操作系统通过时间片轮转调度进程。
      • 内存管理:虚拟内存通过页表映射到物理内存。
      • I/O管理:通过系统调用(如read/write)实现输入输出。
  3. 进程终止(Termination)
    • 操作:程序执行完毕后,操作系统回收进程资源(如内存、文件描述符等),进程状态变为“僵尸”(Zmbie),直到父进程通过wait()回收其退出状态。
    • 关键点
      • 资源释放:内存、打开的文件、进程控制块(PCB)等被回收。
      • 进程销毁:进程从系统中彻底消失,回归“零”状态。

1.2 环境与工具

CPU :13th Gen Intel(R) Core(TM) i7-13620H

RAM:16.0GB

系统:x64Windows11 x64Ubuntu

工具:Codeblocks 64位,vim objump gdb gcc readelf等工具

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

表1 中间结果文件

文件名

作用

hello.i

经预处理得到的文本文件

hello.s

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

hello.o

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

hello.c

源代码文件

hello

链接后的可执行文件。

1.4 本章小结

本章主要介绍了有关Hello的有关内容,包括P2P和020过程,同时给出了作业所使用的环境和工具,所有的中间结果文件和它们所对应的作用

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

2.1.1 预处理的概念

预处理是程序编译过程的初始阶段,由预处理器(如C语言的cpp)执行。其核心任务是对源代码进行文本替换和宏展开,处理以#开头的指令(如#include、#define、#ifdef等)。预处理器不涉及语法或语义分析,仅按指令规则操作文本,生成中间文件供后续编译使用。例如,#include <stdio.h>会直接将标准库头文件内容插入源代码中,而#define PI 3.14会将所有PI替换为3.14。

2.1.2 预处理的作用

模块化与复用:通过#include引入头文件,避免重复编写公共代码(如函数声明、宏定义)。

条件编译:利用#ifdef、#ifndef等指令根据环境(如平台、调试模式)选择性编译代码,提升可移植性。

宏替换:用#define定义常量或简化代码片段,减少硬编码并提升可读性(如错误码、数学公式)。

代码优化:预处理阶段移除注释、展开宏,减少后续编译负担,同时隐藏实现细节(如通过头文件暴露接口)。

2.2在Ubuntu下预处理的命令

gcc -E hello.c -o hello.i

图2.2 在Ubuntu下进行预处理

2.3 Hello的预处理结果解析

打开hello.i后得到:

图2.3 hello.c预处理结果(部分)

       发现所有注释消失。其中, hello.c中的main函数代码在hello.i程序中是3044行到3057行。宏定义也全部被展开。所有的#include内容被替换为原本的头文件内容。内容大幅增加。

2.4 本章小结

本章深入探讨了预处理阶段的核心概念与作用,通过分析 hello.c 程序的预处理过程,揭示了预处理器(如 cpp)如何对源代码进行文本替换和宏展开。预处理阶段通过处理 #include、#define 等指令,实现了代码的模块化与复用、条件编译、宏替换及代码优化,为后续编译阶段提供了标准化输入。实验结果表明,预处理后的文件(如 hello.i)包含了展开后的代码和头文件内容,且注释被移除,宏定义被替换。预处理作为编译流程的基石,确保了代码在语法分析前已具备清晰的结构和定义,为理解程序的编译过程奠定了基础。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

3.1.1概念

编译是将预处理后的源代码(如 .i 文件)转换为汇编语言程序(如 .s 文件)的过程。此过程由编译器(如 gcc)执行,涉及词法分析(识别源代码中的标记)、语法分析(验证标记序列是否符合语言规则)、语义分析(检查代码逻辑的合理性)、代码优化(提高代码执行效率)和生成汇编代码(将高级语言指令映射为低级汇编指令)等关键步骤。编译是连接高级语言与机器语言的桥梁,为后续的汇编和链接阶段提供了必要的中间表示。

3.1.2 作用       

语法与语义验证:编译器通过词法和语法分析,检查代码是否符合语言规范,确保代码的正确性。

代码优化:在生成汇编代码前,编译器会对代码进行优化,提高程序的执行效率。

平台无关性:编译器将高级语言代码转换为平台相关的汇编代码,但保留了部分抽象,使得代码在不同平台上具有一定的可移植性。

中间表示:生成的汇编代码作为中间表示,便于后续的汇编和链接阶段进行处理。

3.2 在Ubuntu下编译的命令

gcc -S hello.i -o hello.s

图3.2在Ubuntu下编译

3.3 Hello的编译结果解析

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

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析

在编译阶段,编译器将预处理后的 hello.i 文件转换为汇编语言文件 hello.s。此过程涉及对C语言的各种数据类型和操作的转换。以下是详细分析:

3.3.1 常量与变量处理

  • 常量:如 "Hello %s %s %s\n" 和 "用法: Hello 学号 姓名 手机号 秒数!" 等字符串常量,在汇编中会被存储在 .rodata 段(只读数据段)。编译器会为这些常量分配内存地址,并在汇编代码中引用这些地址。

图3.3.1.1常量

  • 变量
    • 局部变量:如 int i,在函数内部声明,存储在栈上。编译器会为局部变量分配栈空间,并在汇编代码中通过栈指针(如 %rbp)进行访问。

图3.3.1.2局部变量

    • 全局变量静态变量: 无

3.3.2 表达式与类型处理

  • 表达式和类型:如 i + 1,编译器会将其转换为相应的汇编指令(如 addl $1, -4(%rbp)),实现表达式的计算。通过不同宽度指令区分类型。

3.3.3 宏处理

  • 宏在预处理阶段已被展开,因此在编译阶段不再直接处理宏。但宏展开后的代码会参与编译过程,如 #define 定义的常量或代码片段会被替换到源代码中。但是源文件中无#define,所以没有展开痕迹。

3.3.4 赋值与初始化

  • 赋值操作: i = 0,编译器会生成相应的汇编指令( movl $0, -4(%rbp))来实现赋值。

图3.3.4.1初始化操作

下图为i=i+1的操作

图3.3.4.2加1

3.3.5 类型转换

  • 隐式转换:如从 int 到 unsigned的自动转换,编译器会在汇编代码中处理这些转换,确保数据类型的正确性。

图3.3.5 int转换为unsigned

3.3.6 sizeof 操作符

  • sizeof 操作符在编译时计算类型或对象的大小,编译器会将其结果替换为具体的数值,如 sizeof(int) 可能被替换为 4。本文件中无。

3.3.7 算术操作

  • 自增自减操作:如 ++i 和 i--,编译器会生成相应的自增或自减指令。

图3.3.7.1

指针算数

图3.3.7.2

argv[n] 通过 addq $8*n, %rax 实现;

3.3.8 逻辑与位操作

  • 逻辑操作:如 &&、||、!,编译器会将其转换为条件跳转指令。
  • 位操作:如 &、|、~、^、<<、>>,编译器会将其转换为相应的位操作指令。
  • 复合位操作:如 |= 和 <<=,编译器会将其展开为基本的位操作和赋值操作。

本文件中无相关操作。

3.3.9 关系操作

  • 关系操作:如 ==、!=、>、<、>=、<=,编译器会将其转换为条件跳转指令,用于实现条件判断。

图3.3.9.1

图3.3.9.2

以上分别为if (argc != 4)和for (i < 8)

后续执行跳转指令

3.3.10 数组、指针与结构操作

  • 数组访问:argv[i],编译器会将其转换为基于数组基址和偏移量的访问指令。
  • 指针操作:如 &v、*p,编译器会处理指针的取地址和解引用操作。

图3.3.10数组指针操作

  • 结构体操作:如 s.id 和 p->id,编译器会处理结构体的成员访问,通过偏移量计算成员地址。本文件无。

3.3.11 控制转移

  • 条件语句:如 if/else 和 switch,编译器会生成条件跳转指令,实现控制流的转移。if/else:通过 cmpl + je + jmp 实现;
    • 循环语句:如 for、while 和 do/while,编译器会生成循环控制指令,包括初始化、条件判断和迭代更新。
    • for:初始化 movl $0,… + 判断 cmpl + 条件跳转 + 自增 addl + 回跳 ;

3.3.12 函数操作

  • 参数传递:编译器会根据函数的参数类型(值传递或地址传递)生成相应的参数传递指令。

按 System V x86‑64 ABI,前六个参数分别放 %rdi, %rsi, %rdx, …

图3.3.12.1

  • 函数调用:编译器会生成调用指令(如 call),将控制流转移到被调函数,并保存返回地址。

call puts@PLT、call printf@PLT、call atoi@PLT、call sleep@PLT、call getchar@PLT

  • 局部变量:在函数内部声明的局部变量,编译器会为其分配栈空间,并在汇编代码中通过栈指针进行访问。
  • 图3.3.12.2局部变量存放栈上
  • 函数返回:编译器会生成返回指令( ret),将控制流返回到调用者,并返回函数的结果(0)。

图3.3.12.3函数返回

movl  $0, %eax  即为return 0

3.4 本章小结

本章聚焦于编译阶段的核心内容,通过深入分析编译器如何将预处理后的C语言代码(.i 文件)转换为汇编语言代码(.s 文件),揭示了编译器对C语言各种数据类型和操作的细致处理方式。具体而言,编译器对常量与变量进行了合理的内存分配与访问处理,包括局部变量、全局变量及静态变量的不同存储策略;对表达式与类型进行了精确的转换与计算,确保了数据类型的正确性与一致性;对宏进行了预处理阶段的展开,使其影响在编译阶段得以体现;对赋值、初始化、类型转换及sizeof等操作符进行了细致的处理,生成了相应的汇编指令;对算术、逻辑、位及关系操作进行了高效的转换,实现了复杂的计算与逻辑判断;对数组、指针与结构操作进行了精确的内存访问与计算,支持了高级数据结构的使用;对控制转移语句进行了巧妙的条件跳转与循环控制,实现了程序流程的灵活控制;对函数操作进行了全面的处理,包括参数传递、函数调用、局部变量分配及函数返回等,确保了函数调用的正确性与高效性。通过本章的分析,我们深入理解了编译阶段在程序生命周期中的重要作用,以及编译器如何将高级语言代码转换为低级的汇编指令,为后续的汇编与链接阶段奠定了坚实的基础。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

汇编是将编译器生成的汇编语言程序(如 .s 文件)转换为机器语言指令(二进制代码)的过程,生成可重定位目标文件(如 .o 文件)。此过程由汇编器(如 as)执行,其核心任务是将汇编指令逐条转换为对应的机器指令,并处理符号表、重定位信息等元数据,最终生成二进制格式的目标文件。汇编是连接高级语言(通过编译生成的汇编代码)与机器语言的桥梁,为后续的链接阶段提供了必要的二进制输入。

      1. 汇编的作用

指令转换:将汇编语言(人类可读的低级语言)转换为机器语言(CPU直接执行的二进制代码),实现程序的可执行性。

符号解析:处理汇编代码中的符号(如变量名、函数名),生成符号表,供链接阶段使用。

重定位信息生成:记录目标文件中需要链接器调整的地址信息(如外部符号引用),确保链接阶段能正确解析符号地址。

目标文件生成:生成可重定位目标文件(.o),包含机器代码、符号表、重定位信息等,为链接阶段提供输入。

优化支持:部分汇编器可对生成的机器代码进行简单优化(如指令调度),提升执行效率。

    1. 在Ubuntu下汇编的命令

gcc -c hello.s -o hello.o

图4.2汇编命令

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

图4.3重定位目标elf格式

4.3.1.ELF头信息分析

ELF头包含了文件的基本属性,例如:

  • Magic: 标识这是一个ELF文件。
  • Class: ELF64表示这是一个64位ELF文件。
  • Data: 2's complement, little endian表示数据采用2的补码表示,小端字节序。
  • Version: 1 (current)表示当前版本。
  • OS/ABI: UNIX - System V表示操作系统/应用程序二进制接口。
  • ABI Version: 0。
  • Type: REL (Relocatable file)表示这是一个可重定位文件。
  • Machine: Advanced Micro Devices X86-64表示目标架构是x86-64。
  • Version: 0x1。
  • Entry point address: 0x0(因为是可重定位文件,没有入口点)。
  • Start of program headers: 0 bytes(可重定位文件通常没有程序头表)。
  • Start of section headers: 文件中节头表的偏移量。
  • Size of this header: ELF头的大小。
  • Size of program headers: 0(没有程序头表)。
  • Number of program headers: 0。
  • Size of section headers: 每个节头的大小。
  • Number of section headers: 节的数量。
  • Section header string table index: 节头字符串表的索引。

4.3.2节头表信息分析

节头表描述了文件中各个节的基本信息,例如:

  • .text: 代码段,包含可执行代码。
  • .data: 数据段,包含已初始化的全局变量。
  • .bss: 未初始化的数据段,通常在程序加载时初始化为0。
  • .rela.text: 重定位信息,用于修正.text段中的地址引用。
  • .symtab: 符号表,包含文件中定义的符号信息。
  • .strtab: 字符串表,包含符号表和其他节中使用的字符串。

4.3.3 重定位项目分析

重定位表(例如.rela.text)包含了需要修正的地址引用信息。每个重定位项通常包含以下信息:

  • Offset: 需要修正的地址在目标节中的偏移量。
  • Info: 包含重定位类型和符号表索引的信息。
    • 重定位类型: 例如R_X86_64_PC32表示相对于程序计数器的32位相对地址重定位。
    • 符号表索引: 指向符号表中相关符号的索引。
  • Addend: 用于修正的附加值。

偏移量     类型              符号值      符号名称 + 加数

0000001c   R_X86_64_PC32    00000000   .rodata - 4

00000021   R_X86_64_PLT32   00000000   puts - 4

0000002b   R_X86_64_PLT32   00000000   exit - 4

0000005f   R_X86_64_PC32    00000000   .rodata + 2c

00000069   R_X86_64_PLT32   00000000   printf - 4

0000007c   R_X86_64_PLT32   00000000   atoi - 4

00000083   R_X86_64_PLT32   00000000   sleep - 4

00000092   R_X86_64_PLT32   00000000   getchar - 4

  1. 0000001c R_X86_64_PC32 00000000 .rodata - 4
    • 偏移量:0x1c,表示在.text段中距离段起始地址0x1c的位置需要修正地址引用。
    • 类型:R_X86_64_PC32,表示这是一个相对于程序计数器(PC)的32位相对地址重定位。在链接时,链接器会根据.rodata节的实际地址和当前指令地址计算出正确的相对地址。
    • 符号值:0x0,符号的初始值,通常在链接时确定。
    • 符号名称:.rodata,表示引用的符号是只读数据段(.rodata)。
    • 加数:-4,用于修正的附加值。
  2. 00000021 R_X86_64_PLT32 00000000 puts - 4
    • 偏移量:0x21,表示在.text段中距离段起始地址0x21的位置需要修正地址引用。
    • 类型:R_X86_64_PLT32,表示这是一个通过过程链接表(PLT)的32位相对地址重定位。链接器会将这个引用重定向到puts函数的PLT入口。
    • 符号值:0x0,符号的初始值,通常在链接时确定。
    • 符号名称:puts,表示引用的符号是puts函数。
    • 加数:-4,用于修正的附加值。
  3. 0000002b R_X86_64_PLT32 00000000 exit - 4
    • 偏移量:0x2b,表示在.text段中距离段起始地址0x2b的位置需要修正地址引用。
    • 类型:R_X86_64_PLT32,表示这是一个通过过程链接表(PLT)的32位相对地址重定位。链接器会将这个引用重定向到exit函数的PLT入口。
    • 符号值:0x0,符号的初始值,通常在链接时确定。
    • 符号名称:exit,表示引用的符号是exit函数。
    • 加数:-4,用于修正的附加值。
  4. 0000005f R_X86_64_PC32 00000000 .rodata + 2c
    • 偏移量:0x5f,表示在.text段中距离段起始地址0x5f的位置需要修正地址引用。
    • 类型:R_X86_64_PC32,表示这是一个相对于程序计数器(PC)的32位相对地址重定位。
    • 符号值:0x0,符号的初始值,通常在链接时确定。
    • 符号名称:.rodata,表示引用的符号是只读数据段(.rodata)。
    • 加数:+2c(十六进制,即十进制的44),用于修正的附加值。
  5. 00000069 R_X86_64_PLT32 00000000 printf - 4
    • 偏移量:0x69,表示在.text段中距离段起始地址0x69的位置需要修正地址引用。
    • 类型:R_X86_64_PLT32,表示这是一个通过过程链接表(PLT)的32位相对地址重定位。链接器会将这个引用重定向到printf函数的PLT入口。
    • 符号值:0x0,符号的初始值,通常在链接时确定。
    • 符号名称:printf,表示引用的符号是printf函数。
    • 加数:-4,用于修正的附加值。
  6. 0000007c R_X86_64_PLT32 00000000 atoi - 4
    • 偏移量:0x7c,表示在.text段中距离段起始地址0x7c的位置需要修正地址引用。
    • 类型:R_X86_64_PLT32,表示这是一个通过过程链接表(PLT)的32位相对地址重定位。链接器会将这个引用重定向到atoi函数的PLT入口。
    • 符号值:0x0,符号的初始值,通常在链接时确定。
    • 符号名称:atoi,表示引用的符号是atoi函数。
    • 加数:-4,用于修正的附加值。
  7. 00000083 R_X86_64_PLT32 00000000 sleep - 4
    • 偏移量:0x83,表示在.text段中距离段起始地址0x83的位置需要修正地址引用。
    • 类型:R_X86_64_PLT32,表示这是一个通过过程链接表(PLT)的32位相对地址重定位。链接器会将这个引用重定向到sleep函数的PLT入口。
    • 符号值:0x0,符号的初始值,通常在链接时确定。
    • 符号名称:sleep,表示引用的符号是sleep函数。
    • 加数:-4,用于修正的附加值。
  8. 00000092 R_X86_64_PLT32 00000000 getchar - 4
    • 偏移量:0x92,表示在.text段中距离段起始地址0x92的位置需要修正地址引用。
    • 类型:R_X86_64_PLT32,表示这是一个通过过程链接表(PLT)的32位相对地址重定位。链接器会将这个引用重定向到getchar函数的PLT入口。
    • 符号值:0x0,符号的初始值,通常在链接时确定。
    • 符号名称:getchar,表示引用的符号是getchar函数。
    • 加数:-4,用于修正的附加值。

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

图4.4hello.o的反汇编

通过 objdump -d -r hello.o 命令,我们得到了目标文件 hello.o 的反汇编代码。以下是对该反汇编代码的分析,并与第3章中的 hello.s 汇编代码进行对照,同时说明机器语言的构成及其与汇编语言的映射关系。

4.4.1. 机器语言的构成

机器语言由二进制指令组成,每条指令对应CPU的一个具体操作。在反汇编输出中,每条指令由机器码(如 f3 0f 1e fa)和对应的汇编指令(如 endbr64)组成。机器码是CPU直接执行的二进制格式,而汇编指令是机器码的人类可读形式。

4.4.2. 汇编语言与机器语言的映射关系

  • 指令映射:每条汇编指令都对应一条或多条机器指令。例如,汇编指令 push %rbp 对应机器码 55。
  • 操作数映射:汇编语言中的操作数(如寄存器、内存地址)在机器语言中以特定的编码形式出现。例如,汇编指令 mov %rsp,%rbp 中的 %rsp 和 %rbp 在机器码中通过寄存器编码表示。
  • 重定位信息:反汇编输出中的 R_X86_64_PC32 和 R_X86_64_PLT32 是重定位信息,表示这些地址在链接阶段需要被调整。

4.4.3. 关键指令对照分析

  • 函数入口
    • 汇编代码:endbr64, push %rbp, mov %rsp,%rbp
    • 机器码:f3 0f 1e fa, 55, 48 89 e5
    • 说明:这些指令用于设置函数栈帧,endbr64 是控制流完整性检查指令。
  • 参数传递与局部变量分配
    • 汇编代码:mov %edi,-0x14(%rbp), mov %rsi,-0x20(%rbp), sub $0x20,%rsp
    • 机器码:89 7d ec, 48 89 75 e0, 48 83 ec 20
    • 说明:这些指令将函数参数从寄存器传递到栈上,并分配局部变量空间。
  • 条件判断与跳转
    • 汇编代码:cmpl $0x5,-0x14(%rbp), je 2f <main+0x2f>
    • 机器码:83 7d ec 05, 74 16
    • 说明:这些指令比较参数值并决定是否跳转,je 指令在条件满足时跳转到指定地址。
  • 函数调用
    • 汇编代码:callq puts@PLT, callq exit@PLT
    • 机器码:e8 00 00 00 00(带重定位信息)
    • 说明:这些指令调用外部函数,@PLT 表示通过过程链接表(PLT)调用,机器码中的 00 00 00 00 将在链接阶段被替换为实际地址。
  • 循环控制
    • 汇编代码:jmp 8b <main+0x8b>, cmpl $0x9,-0x4(%rbp), jle 38 <main+0x38>
    • 机器码:eb 53, 83 7d fc 09, 7e a7
    • 说明:这些指令实现循环控制,jmp 无条件跳转,jle 在条件满足时跳转。
  • 函数返回
    • 汇编代码:leaveq, retq
    • 机器码:c9, c3
    • 说明:这些指令恢复栈帧并返回调用者。

4.4.4. 分支转移与函数调用的特殊处理

  • 分支转移:在机器语言中,分支转移指令(如 je, jle)使用相对地址或绝对地址进行跳转。反汇编输出中的地址(如 2f <main+0x2f>)是相对于函数入口的偏移量。
  • 函数调用:函数调用指令(如 callq)在机器语言中通过PLT或直接地址调用。反汇编输出中的 R_X86_64_PLT32 表示该调用需要通过PLT进行重定位。

4.5 本章小结

本章通过分析 hello.o 目标文件的反汇编代码,深入揭示了机器语言与汇编语言的内在联系及编译过程的底层机制。核心结论包括:

  1. 映射关系:汇编指令(如 mov, callq)与机器码(如 48 89 e5, e8 00 00 00 00)一一对应,操作数通过寄存器编码或内存地址编码实现。
  2. 重定位与符号解析:反汇编中出现的 R_X86_64_PC32 和 R_X86_64_PLT32 表明外部符号(如函数名、字符串)的地址需在链接阶段确定,体现了目标文件的未完成性。
  3. 控制流与函数调用:分支指令(如 je, jle)通过相对偏移实现跳转,函数调用(如 callq puts@PLT)依赖PLT机制处理间接调用。
    本章为理解程序在机器层面的执行提供了关键视角,强化了从高级语言到二进制代码的转换认知。

(第41分)

5章 链接

5.1 链接的概念与作用

5.1.1概念

链接(Linking 是将多个目标文件(.o 或 .obj)和库文件合并为可执行程序或共享库的过程,是编译流程的关键步骤。编译器将源代码(如 .c 文件)编译为目标文件,包含机器码、符号表和重定位信息,但未解决外部符号(如函数、变量)的引用。链接器负责合并目标文件和库文件,解析符号引用,分配最终内存地址,生成可执行文件或共享库。

5.1.2作用

  • 符号解析
    目标文件可能引用其他文件定义的符号(如函数、全局变量)。链接器通过符号表匹配所有引用与定义,确保所有符号被正确解析(例如,将 main.o 中对 puts() 的调用链接到标准库中的定义)。
  • 重定位
    目标文件中的地址(如函数调用、变量访问)通常是占位符。链接器根据最终内存布局,修改代码和数据中的地址,确保所有引用指向正确的内存位置(例如,将 callq puts@PLT 中的占位符替换为实际地址)。
  • 生成可执行文件
    链接器将所有目标文件和库文件合并,生成可直接运行的程序或共享库(如 .so 或 .dll 文件)。

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 -v

图5.2链接展示截图

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

5.3.1  ELF 头信息

图5.3.1ELF头信息

(1) Magic 字段

  • : 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  • 作用:
    • 7f 45 4c 46 是 ELF 文件的魔数(Magic Number),表示这是一个 ELF 文件。
    • 02 表示 64 位 ELF 文件(01 表示 32 位)。
    • 01 表示小端序(Little Endian)。
    • 00 表示当前版本为 1。

(2) 类别(Class

  • : ELF64
  • 作用: 表示这是一个 64 位的 ELF 文件。

(3) 数据(Data

  • : 2 补码,小端序 (little endian)
  • 作用: 指定文件中的数据存储方式为小端序(x86-64 架构默认使用小端序)。

(4) 类型(Type

  • : EXEC (可执行文件)
  • 作用: 表示这是一个可执行文件(与目标文件 .o 或共享库 .so 区分)。

(5) 系统架构(Machine

  • : Advanced Micro Devices X86-64
  • 作用: 表示该文件是为 x86-64 架构编译的。

(6) 入口点地址(Entry point address

  • : 0x4010f0
  • 作用:
    • 程序执行的起始地址。
    • 链接器在生成可执行文件时确定该地址,通常是 _start 函数的地址(由 C 运行时库初始化后调用 main)。

(7) 程序头起点(Start of program headers

  • : 64 (bytes into file)
  • 作用:
    • 程序头表(Program Headers)在文件中的偏移量。
    • 程序头表描述了如何将文件加载到内存中(如代码段、数据段等)。

(8) 节头表起点(Start of section headers

  • : 14208 (bytes into file)
  • 作用:
    • 节头表(Section Headers)在文件中的偏移量。
    • 节头表包含代码、数据、符号表等节的信息,主要用于链接和调试。

(9) 程序头数量(Number of program headers

  • : 12
  • 作用: 程序头表中有 12 个条目,每个条目描述一个段(如代码段、数据段等)。

(10) 节头数量(Number of section headers

  • : 27
  • 作用: 节头表中有 27 个条目,每个条目描述一个节(如 .text、.data、.rodata 等)。

(11) 节头字符串表索引(Section header string table index

  • : 26
  • 作用:
    • 节头字符串表的索引(.shstrtab 节)。
    • 该节存储了所有节名称的字符串(如 .text、.data 等)。

5.3.2段头表

图5.3.2段头表

文件类型与入口点

    • 类型:EXEC(可执行文件),入口地址 0x4010f0,程序从此处开始执行。

关键程序头段

    • PHDR:程序头表本身,位于文件偏移 0x40,仅可读(R)。
    • INTERP:指定动态链接器 /lib64/ld-linux-x86-64.so.2,用于加载依赖库。
    • LOAD(共4段):
      • 第1段:加载只读数据(如代码、符号表),虚拟地址 0x400000,大小 0x5c0。
      • 第2段:加载可执行代码(.text),含初始化/终止函数,标记为可读可执行(R E)。
      • 第3段:加载只读数据(.rodata、异常帧)。
      • 第4段:加载可读写数据(.data、.got),虚拟地址 0x403e50,大小 0x1fc。
    • DYNAMIC:动态链接信息(如依赖库、重定位表),位于可读写段。
    • NOTE:存储元数据(如ABI版本、构建属性)。
    • GNU_RELRO:标记只读重定位数据(.got 部分),增强安全性。

内存对齐与权限

    • 各段按 0x1000 对齐,权限包括可读(R)、可执行(E)、可写(W)。

总结:该ELF文件是动态链接的可执行程序,依赖 ld-linux-x86-64.so.2 加载,包含代码、数据及动态链接信息,支持安全特性(如RELRO)。

5.3.3动态段

图5.3.3动态段

依赖库:需 libc.so.6(NEEDED)。

关键地址

    • 初始化代码 INIT(0x401000),终止代码 FINI(0x401248)。
    • .got.plt 地址 PLTGOT(0x404000),用于动态链接。

符号与重定位

    • 符号表(SYMTAB)、字符串表(STRTAB),重定位表(RELA,大小48字节)。

版本控制:依赖版本信息(VERNEED/VERSYM)。

总结:动态段配置了程序依赖、加载入口及符号解析信息

5.3.4符号表

图5.3.4符号表

  1. 动态符号表(.dynsym
    • 含9个条目,均为未定义(UND)的外部函数(如 puts、printf、__libc_start_main 等),依赖 GLIBC_2.2.5。
  2. 完整符号表(.symtab
    • 含51个条目,包括:
      • 局部符号(如 .text、.data 等段起始地址)。
      • 全局符号:_start(入口)、main 函数、_IO_stdin_used(输入输出数据)等。
      • 弱符号(如 __gmon_start__,用于性能分析)。

总结:程序依赖动态库函数,自身包含入口、主函数及初始化/终止逻辑,部分符号用于链接和运行时支持。

5.4 hello的虚拟地址空间

使用gdb/edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

图5.4.1

  5.4.1. 虚拟地址空间分段分析

  • 代码段(.text
    • 权限:r-xp(只读、可执行)
    • 地址范围:从0x401000开始(0x400500-0x400800)。
    • 内容:存储编译后的机器指令(如main函数)。
    • 对比:与ELF文件PT_LOAD段中PF_X|PF_R权限的段对应。
  • 数据段(.data/.rodata/.bss
    • .data:已初始化全局变量,权限rw-p。
    • .rodata:只读常量(如字符串"Hello"),权限r--p。
    • .bss:未初始化全局变量,权限rw-p,但实际不占磁盘空间,由内核在加载时置零。
    • 地址范围:通常紧随代码段之后
  • 堆段(.heap
    • 权限:rw-p
    • 地址范围:从.brk基址向上增长。
    • 操作:通过malloc分配内存时扩展。
  • 栈段(.stack
    • 权限:rw-p
    • 地址范围:从高地址向低地址增长。
    • 内容:局部变量、函数参数、返回地址。
  • 动态链接段
    • .got.plt(全局偏移表):动态链接时解析函数地址。
    • .plt(过程链接表):存储动态函数的跳转指令。
    • 地址范围:通常在.data和.heap之间。
  • 内存映射段
    • 共享库:映射libc.so等动态库。
    • 权限:r-xp(代码)或rw-p(数据)。

5.4.2. 与ELF文件结构的对照

  • ELF文件中的段(Program Header
    • PT_LOAD段:指定需加载到内存的代码和数据段,包含p_vaddr(虚拟地址)、p_memsz(内存大小)等信息。
    • PT_DYNAMIC段:存储动态链接信息。
    • PT_INTERP段:指定动态链接器路径。
  • 地址空间布局(ASLR影响)
    • 代码段、堆、栈的基址可能随机化(通过/proc/sys/kernel/randomize_va_space控制)。
    • 共享库的加载地址由动态链接器计算,避免冲突。

图5.4..2 Memory Region

可以从Memory Region里面查看各段的信息,包括起始地址、结束地址,权限,名称

5.5 链接的重定位过程分析

图5.5.1和图5.5.2

hello.o(目标文件)

    • 未链接:包含编译后的机器指令,但未解析外部符号(如 puts、printf 等)的地址。
    • 重定位表:存储需要链接器修正的符号引用(如 R_X86_64_PLT32 类型)。
    • 无入口点:无 _start 或 main 的绝对地址,仅有相对偏移。

hello(可执行文件)

    • 已链接:所有外部符号的地址已解析(如 puts@GLIBC 指向 libc.so 中的实际地址)。
    • 动态段:包含 .got.plt、.dynamic 等段,用于运行时动态链接。
    • 入口点:_start 地址固定(0x4010f0),由链接器计算。

链接过程的关键步骤

符号解析

    • 链接器扫描 hello.o 的 .symtab,发现未定义的符号(如 puts、printf)。
    • 在动态库(如 libc.so)中查找这些符号的实际地址。

重定位

    • 链接器根据 hello.o 的 .rela.text、.rela.plt 等重定位表,修正指令中的符号引用。
    • 示例
      • hello.o 中的 callq puts 存储为 callq <偏移量>。
      • 链接后修正为 callq [.got.plt + puts@GLIBC的偏移](如 401090)。

段合并

    • 将 hello.o 的 .text、.data 等段合并到 hello 的对应段中。
    • 分配虚拟地址(如 .text 从 0x401000 开始)。

hello 中对 hello.o 的重定位示例

1.hello.o 的重定位表(.rela.plt

hello.o 的 .rela.plt 包含以下条目:

RELOCATION RECORDS FOR [.plt]:

OFFSET TYPE SYMBOL

0x0000 R_X86_64_JUMP_SLOT puts

0x0008 R_X86_64_JUMP_SLOT printf

2. 链接后的 hello 修正

  • .got.plt 填充
    • 链接器在 .got.plt(如 0x600e00)中写入 puts、printf 的实际地址(从 libc.so 解析)。
  • .plt 修正
    • hello.o 中的 callq puts 修正为 callq [.got.plt + 0](即 401090)。
    • hello 的 .plt 代码:

401090: ff 25 7d 2f 00 00 jmpq *0x2f7d(%rip) # 跳转到 .got.plt 中的 puts 地址

3. 动态链接时的进一步修正

  • 程序加载时,动态链接器(ld-linux-x86-64.so.2)将 libc.so 中的 puts 地址写入 .got.plt。

5.6 hello的执行流程

5.6.1 调试过程

在EDB中设置断点并逐步调试,可观察到程序从入口点 _start(0x4010f0)开始执行,逐步进入 main 函数,调用标准库函数(如 printf、sleep、getchar),最终通过 exit 退出。调试步骤如下:

程序入口

    • 程序从ELF头指定的入口地址 0x4010f0(_start)开始执行。
    • _start 调用 __libc_start_main 初始化运行时环境,并跳转到用户定义的 main 函数。

主函数执行流程

    • 在 main(0x401125)中,程序依次调用:
      • printf:输出提示信息(地址 0x4010a0,通过PLT跳转)。
      • sleep:暂停执行(地址 0x4010e0,通过PLT跳转)。
      • getchar:等待用户输入(地址 0x4010b0,通过PLT跳转)。

程序退出

    • 通过 exit(地址 0x4010d0)终止程序,释放资源并返回操作系统。


5.6.2 函数地址对照表

函数名

地址

说明

_start

0x4010f0

程序入口,由链接器设置,跳转到 __libc_start_main

__libc_start_main

0x2f12271d

glibc提供的启动函数,初始化环境并调用 main

main

0x401125

用户主函数,包含核心逻辑。

printf

0x4010a0

通过PLT跳转到动态库中的实际实现(如 libc.so)。

sleep

0x4010e0

通过PLT调用,暂停程序执行。

getchar

0x4010b0

通过PLT调用,等待用户输入。

exit

0x4010d0

通过PLT调用,终止程序并清理资源。

5.7 Hello的动态链接分析

5.7.1 动态链接原理与GOT/PLT机制解析

动态链接的核心思想是将程序拆分为独立模块(如可执行文件和共享库),在运行时按需加载并解析符号引用。这种设计允许代码复用(如多个程序共享同一库)和内存优化(如共享库的代码段只加载一次)。

由于共享库的加载地址在编译时未知,编译器无法直接生成绝对地址引用。为此,编译器在目标文件中为外部符号(如 printf、malloc)生成重定位记录,由动态链接器在运行时解析这些符号的实际地址。


5.7.2 延迟绑定(Lazy Binding)与GOT/PLT

ELF格式通过GOT(全局偏移表)PLT(过程链接表)实现延迟绑定,其优势在于:

  • 减少启动开销:仅在函数首次调用时解析地址,而非程序启动时解析所有符号。
  • 优化性能:避免不必要的符号解析,节省CPU和内存资源。

 hello.elf 为例

  1. GOT和PLT的布局
    • GOT表起始地址(如 0x404000)存储动态符号的实际地址。

    • PLT表(如 0x401020)是跳转指令的集合,每个条目对应一个外部函数。
  1. 延迟绑定的过程
    • 初始状态:GOT中与函数相关的条目(如 0x404008)为空(或指向PLT的回调代码)。
    • 首次调用
      1. 程序跳转到PLT条目(printf@plt)。
      2. PLT条目跳转到GOT条目,此时GOT条目指向动态链接器的解析逻辑(dl_runtime_resolve)。
      3. 链接器查找函数地址,并更新GOT条目为实际地址( libc.so 中的 printf 地址)。
    • 后续调用
      PLT直接跳转到GOT,而GOT已保存实际地址,程序直接跳转到目标函数。


5.7.3 GOT/PLT协同工作机制

1. 变量引用的重定位

  • 动态链接器通过重定位表(如 .rela.dyn)修正全局变量和静态变量的地址。
  • 编译器生成的重定位条目包含符号名和偏移量,链接器根据运行时加载地址计算绝对地址。

2. 函数调用的绑定过程

  • 初始阶段
    • PLT条目(如 printf@plt)的代码结构:

asm

printf@plt:

jmp [GOT + 8] ; 跳转到GOT条目(初始指向PLT的回调代码)

push $0x1 ; 函数索引(用于链接器解析)

jmp PLT[0] ; 跳转到PLT的公共入口(触发链接器)

    • GOT条目(如 0x404008)初始值为 PLT[1](即PLT的回调代码)。
  • 首次调用时
    1. 程序执行 call printf@plt,跳转到PLT条目。

    1. PLT跳转到GOT条目,发现GOT条目指向PLT的回调代码,触发链接器解析。

    1. 链接器通过符号表和重定位表找到 printf 的实际地址,并更新GOT条目为该地址。
  • 后续调用时
    • PLT直接跳转到GOT,GOT已保存实际地址,程序直接跳转到目标函数。


总结

  • 动态链接:通过GOT和PLT在运行时解析符号地址,支持共享库的灵活加载。
  • 延迟绑定:仅在首次调用时解析符号,优化启动性能。
  • GOT/PLT协同:PLT负责跳转逻辑,GOT存储实际地址,二者配合实现高效的函数调用。

5.8 本章小结

动态链接通过GOT(全局偏移表)和PLT(过程链接表)实现运行时符号解析,核心是延迟绑定:首次调用函数时触发动态链接器解析地址并填充GOT,续调用直接跳转至GOT中的实际地址,优化启动性能。

其优势在于代码复用(共享库)和灵活性(库可独立升级),典型场景包括标准库函数调用(如printf)和插件系统。调试可通过readelf查看GOT/PLT地址,用gdb观察GOT填充过程。

需注意安全性风险(如ROP攻击)和首次调用的解析开销。掌握GOT/PLT机制对理解程序运行、优化性能和排查链接问题至关重要。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

6.1.1概念

进程是操作系统对运行中程序的抽象,是资源分配和调度的基本单位。每个进程包含:

  • 代码段:程序指令的副本。
  • 数据段:全局变量、堆内存等。
  • :函数调用、局部变量等运行时数据。
  • 进程控制块(PCB:存储进程状态(如运行、阻塞)、优先级、资源占用等信息。

6.1.2作用

  • 隔离性:不同进程拥有独立地址空间,避免相互干扰(如浏览器崩溃不影响系统)。
  • 并发性:操作系统通过时间片轮转等调度算法,使多进程“同时”运行,提升CPU利用率。
  • 资源管理:进程是系统分配CPU、内存、文件等资源的基本单位,确保资源合理使用。

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

6.2.1作用

Shell是用户与操作系统内核之间的接口,充当命令解释器的角色。它接收用户输入的指令,将其翻译为内核可执行的命令,并将操作结果反馈给用户。以bash为例,其作为Linux默认Shell,通过避免用户直接操作内核,保障了系统安全性。用户可通过bash实现文件管理、权限设置、用户管理、磁盘操作、网络配置及软件部署等系统管理任务。

6.2.2处理流程

指令接收与解析

    • 用户输入命令后,bash首先判断是否通过绝对路径执行。若未使用绝对路径,则检查是否存在别名(alias)或内部命令。
    • 例如,cd为bash内置命令,而ls需通过PATH环境变量查找可执行文件路径。

命令执行

    • 内部命令:直接由bash解释执行。
    • 外部命令:bash通过PATH变量定位命令路径。若命令存在于缓存(hash表)中,则直接调用;否则遍历PATH路径查找命令位置。
    • 执行过程中,bash支持命令行补全(通过Tab键)、历史命令调用(上下方向键)及快捷键操作(如Ctrl+C终止任务)。

结果反馈

    • 命令执行完成后,bash将结果输出至终端。用户可通过重定向符号(如>、>>)将输出保存至文件,或通过管道(|)将结果传递给其他命令处理。

交互优化

    • bash提供历史命令记录(存储于~/.bash_histry)、自动补全、别名设置(如alias ll='ls -al')及脚本编程功能,显著提升用户操作效率。例如,用户可通过上下键回顾历史命令,或编写Shell脚本自动化重复任务。

6.3 Hello的fork进程创建过程

以在Shell中运行hello程序为例,其fork进程创建过程如下:

Shell调用fork函数:当在Shell中输入运行hello程序的命令(如./hello)后,Shell会调用fork函数创建一个新的子进程。fork函数通过系统调用创建一个与Shell进程几乎完全相同的进程,即子进程。此时,系统中出现两个进程,一个是Shell父进程,一个是用于运行hello程序的子进程。

图6.3 fork进程创建

fork函数返回值处理

    • 子进程:在子进程中,frk函数返回0。子进程会继续执行后续代码,操作系统内核提供的execve函数会创建虚拟内存的映射,然后开始加载物理内存,进入到hell程序的main函数当中执行相关的代码,比如打印出信息。
    • 父进程:在父进程(Shell)中,frk函数返回新创建子进程的进程ID。父进程可以根据这个返回值对子进程进行管理,比如等待子进程结束等。
  1. 程序运行与结束:hello程序在子进程中运行,完成相应任务后结束。程序运行完成后,Shell回收子进程,操作系统内核删除相关数据结构,释放其占据的资源。
    1. Hello的execve过程

6.4.1Shell调用fork函数

Shell通过fork函数创建一个新的子进程,该子进程是Shell进程的副本,拥有独立的进程空间。

6.4.2execve函数调用

在子进程中,Shell调用execve函数来加载并执行hello程序。execve函数的参数包括:

    • filename:指定要执行的程序路径(如./hello)。
    • argv:传递给hello程序的命令行参数数组。
    • envp:传递给hello程序的环境变量数组。

6.4.3程序加载与执行

    • 操作系统根据filename找到hello程序的可执行文件。
    • 操作系统为hello程序创建新的虚拟内存地址空间,并将程序的各个段(如代码段、数据段等)映射到物理内存。
    • 操作系统将argv和envp指定的参数和环境变量设置到新进程中。
    • 操作系统启动新进程的执行,从hello程序的入口点(通常是main函数)开始执行代码。

6.4.4程序运行与结束

    • hello程序按设计逻辑逐行执行代码,完成预定的任务。
    • 程序运行结束后,通过exit系统调用通知操作系统进程已完成。
    • 操作系统回收该进程所占用的资源,包括内存、文件描述符等。
    • Shell父进程通过等待子进程终止,并回收该进程的内存和其他系统资源。内核删除与hello程序相关的所有数据结构,释放内存。

6.5 Hello的进程执行

进程调度与时间片

    • 操作系统通过时间片轮转调度进程,hell进程与其他进程交替占用CPU。
    • 调度器根据进程优先级(如交互性)动态分配时间片,时间片耗尽时触发上下文切换。

上下文切换

    • 切换时,内核保存当前进程的寄存器、程序计数器等上下文至内核栈,恢复目标进程上下文。
    • 例如,hell进程因时间片耗尽或I/O阻塞时被切换,恢复后继续执行。

用户态与内核态转换

    • 触发方式
      • 系统调用(如hello调用printf,实际通过write系统调用输出)。
      • 异常(如非法指令)或中断(如定时器中断)。
    • 过程:用户态程序通过指令(如syscall)进入内核态,内核处理后返回。

hello进程执行流程

    • 创建:Shell调用frk创建子进程,子进程通过execve加载hell程序。
    • 执行:内核将CPU交给hell进程,执行main函数,调用系统调用(如write)时进入内核态。
    • 终止:hell调用exit系统调用,内核回收资源并通知Shell。

关键点

    • 上下文切换和用户态-内核态转换是性能瓶颈,内核通过优化减少开销(如vds技术)。
    • 单核系统通过时间片轮转实现并发,多核系统可并行执行多个进程。

6.6 hello的异常与信号处理

6.6.1不停乱按,包括回车。

Shell会将回车前输出的字符串当作命令。

图6.6.1乱按

6.6.2 使用ctrl c

图6.6.2 ctrl c

使用ps发现进程被回收,成功终止了进程

6.6.3 CTRL Z

图6.6.3 CTRL Z

在后台停止,fg放到前台运行时,会输出剩下的字符串

6.7本章小结

进程执行的核心机制

    • 进程调度:操作系统通过时间片轮转、优先级调度等策略,动态分配CPU资源,实现多进程并发。
    • 上下文切换:进程切换时,内核保存当前进程上下文(寄存器、程序计数器等)至内核栈,恢复目标进程上下文,确保执行连续性。
    • 用户态与内核态转换:进程通过系统调用(如write、read)或中断触发状态切换,内核处理后返回用户态,实现资源访问与隔离。

关键流程与示例

    • 进程创建:fork生成子进程,execve加载新程序(如hello进程的启动)。
    • 系统调用:用户程序通过软中断(如syscall)进入内核态,完成I/O、资源分配等操作。
    • 进程终止:exit系统调用触发内核回收资源,通知父进程(如Shell)。

性能优化与挑战

    • 上下文切换开销:频繁切换会导致缓存失效、TLB刷新等性能损耗,内核通过减少切换次数、优化数据结构(如哈希表管理进程)提升效率。
    • 用户态-内核态切换优化:现代系统采用vdso(虚拟动态共享对象)技术,将高频系统调用(如gettimeofday)映射至用户态,减少模式切换成本。

并发与并行的实现

    • 并发:单核系统通过时间片轮转模拟并行,提高CPU利用率。
    • 并行:多核系统可同时执行多个进程,显著提升性能。

总结
本章通过hello进程的执行,深入剖析了进程调度、上下文切换、状态转换等核心机制。理解这些机制有助于分析程序行为、优化系统性能,并为后续学习多线程、进程通信等高级主题奠定基础。

(第62分)

7章 hello的存储管理

    1. hello的存储器地址空间

7.1.1逻辑地址(Logical Address)

由程序生成的与段相关的偏移地址,是用户编程时使用的地址。在hello程序中,若某指令或数据位于代码段偏移0x1000处,则逻辑地址为段基址:0x1000(如0x08048000:0x1000)逻辑地址通过段式管理转换为线性地址,但现代操作系统(如Linux)通常将段基址设为0,使逻辑地址等于线性地址。


7.1.2线性地址(Linear Address)

逻辑地址经段式管理后的中间地址,是分页机制处理的输入。若hello程序的逻辑地址为0x08049000,在Linux中线性地址直接等于该值(因段基址为0)。

分段:逻辑地址的段选择符通过段描述符表(GDT/LDT)找到段基址,加上偏移量得到线性地址。

Linux简化:用户代码段和数据段的基址均为0,长度为4GB,因此逻辑地址直接作为线性地址。


7.1.3虚拟地址(Virtual Address)

进程视角下的内存地址空间,每个进程拥有独立的4GB虚拟地址空间(32位系统)。hello程序运行时,其代码、数据、堆栈等均位于虚拟地址空间中(如代码段从0x08048000开始)。

隔离性:不同进程的虚拟地址可映射到同一物理地址,但互不干扰。

安全性:通过页表权限位(如只读、可执行)防止越权访问。

与线性地址的关系
在x86架构中,虚拟地址与线性地址通常等价,均需通过页表转换为物理地址。


 7.1.4物理地址(Physical Address)

内存硬件的实际地址,CPU通过内存总线直接访问。hello程序的代码段可能被加载到物理地址0x10000000处,但程序本身仅使用虚拟地址0x08048000。

分页:线性地址通过页表(页目录+页表)映射到物理地址。

页目录项:线性地址高10位索引页目录表,找到页表物理地址。

页表项:线性地址中间10位索引页表,找到物理页帧号。

偏移量:线性地址低12位直接作为物理页内偏移。

TLB加速:CPU通过TLB缓存常用页表项,避免每次访问都查表。


7.1.5. Hello程序地址转换示例

编译与链接

    • hell.c经编译生成可执行文件,代码段、数据段等被分配虚拟地址(如代码段从0x08048000开始)。

程序加载

    • 执行./hell时,Shell通过frk+execve创建进程,内核为hell进程分配页表,将虚拟地址映射到物理内存或交换分区。

地址转换

    • CPU执行hell的指令时,将虚拟地址(如0x08049000)通过页表转换为物理地址(如0x10001000),再访问内存。

动态内存

    • 若hell调用mallc分配堆内存,内核通过修改页表扩展虚拟地址空间,可能触发缺页异常并分配物理页。

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

7.2.1段式管理核心机制

    • 逻辑地址组成:由段选择符(16位)和段内偏移量(32位)组成。
    • 段选择符作用
      • 索引全局描述符表(GDT)或局部描述符表(LDT)中的段描述符。
      • 包含请求特权级(RPL)和表指示符(TI,0=GDT,1=LDT)。

7.2.2转换步骤

    • 查找段描述符
      • 根据段选择符的TI位确定使用GDT或LDT。
      • 通过段选择符的索引(高13位)在对应表中定位段描述符。
    • 权限检查
      • 对比段描述符的DPL(描述符特权级)与当前进程的CPL(当前特权级)和RPL,确保特权级合法。
    • 计算线性地址
      • 从段描述符中获取段基址(32位),与段内偏移量相加,得到线性地址。

7.2.3示例说明

    • 逻辑地址0x1B:0x00400000:
      • 段选择符0x1B(二进制00011011):索引000000011(3),TI=0(GDT),RPL=11(特权级3)。
      • 假设GDT中索引3的段描述符基址为0x08000000,则线性地址为0x08000000 + 0x00400000 = 0x08400000。

7.2.4现代简化

    • Linux中,用户代码段和数据段的基址均设为0,长度为4GB,因此逻辑地址直接等于线性地址。段式管理主要用于权限检查,而非地址转换。

总结:段式管理通过段选择符和段描述符将逻辑地址转换为线性地址,但现代操作系统(如Linux)简化了这一过程,使其主要服务于权限控制。

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

7.3.1页式管理核心机制

    • 分页目的:将虚拟内存(线性地址空间)划分为固定大小的页(如4KB),映射到物理内存的页帧,实现地址转换与内存隔离。
    • 关键数据结构
      • 页目录表(Page Directory):存储页表的物理地址,每项4字节,共1024项(32位系统)。
      • 页表(Page Table):存储物理页帧号,每项4字节,共1024项。

7.3.2转换步骤

    • 拆分线性地址(32位):
      • 页目录索引(高10位):定位页目录表项。
      • 页表索引(中间10位):定位页表项。
      • 页内偏移(低12位):直接作为物理页帧的偏移。
    • 查找页表项
      • 通过页目录表项找到页表的物理地址,再通过页表项找到物理页帧号。
    • 计算物理地址
      • 物理地址 = 物理页帧号(高20位) + 页内偏移(低12位)。

7.3.3示例说明

    • 线性地址0x0804A004拆分:
      • 页目录索引:0x080(二进制000010000000),页表索引:0x4A(01001010),页内偏移:0x004。
      • 假设页目录表项指向页表物理地址0x200000,页表项0x4A的物理页帧号为0x3000,则物理地址为0x3000000 + 0x004 = 0x3000004。

7.3.4现代优化

    • TLB缓存:存储常用页表项,避免每次访问都查表。
    • 大页支持:如2MB/1GB页,减少页表项数量,降低TLB缺失率。
    1. TLB与四级页表支持下的VA到PA的变换

7.4.1TLB查询:CPU产生虚拟地址后,MMU首先查询TLB。TLB是页表的高速缓存,存储了最近使用的虚拟地址到物理地址的映射关系。如果TLB中存在对应的页表项(TLB命中),则直接获取物理地址,转换过程结束。

7.4.2四级页表查询(TLB缺失时):若TLB中不存在对应的页表项(TLB缺失),MMU则需要访问内存中的四级页表进行转换。四级页表包括页全局目录(PGD)、页上级目录(PUD)、页中间目录(PMD)和页表项(PTE)。

    • PGD查询:MMU根据虚拟地址中的PGD索引,在PGD中找到对应的PUD基地址。
    • PUD查询:根据虚拟地址中的PUD索引,在PUD中找到对应的PMD基地址。
    • PMD查询:根据虚拟地址中的PMD索引,在PMD中找到对应的PTE基地址。
    • PTE查询:根据虚拟地址中的PTE索引,在PTE中找到对应的物理页帧号(PFN)。

7.4.3物理地址计算:将得到的物理页帧号与虚拟地址中的页内偏移量组合,得到最终的物理地址。

7.4.4TLB更新:在完成地址转换后,MMU会将该虚拟地址到物理地址的映射关系更新到TLB中,以便后续访问时能够快速命中。

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

7.5.1三级Cache架构

L1 Cache:分为指令Cache(I-Cache)和数据Cache(D-Cache),容量小(如32KB-64KB),访问延迟低(1-3个时钟周期),紧邻CPU核心。

L2 Cache:统一缓存(Unified Cache),容量较大(如256KB-2MB),访问延迟较高(约10个时钟周期),为多个L1 Cache共享。

L3 Cache:全系统共享,容量最大(如8MB-32MB),访问延迟最高(约30-50个时钟周期),用于减少主存访问频率。

7.5.2物理内存访问流程

CPU请求数据

首先查询L1 Cache,若命中则直接返回数据。

若L1未命中,查询L2 Cache;若命中则返回数据,并更新L1 Cache(可能替换旧数据)。

若L2未命中,查询L3 Cache;若命中则返回数据,并更新L1和L2 Cache。

若L3未命中,则从主存读取数据,并更新L1、L2、L3 Cache(可能触发缓存替换策略,如LRU)。

写操作

采用写回(Write-Back)策略:数据先写入Cache,仅在替换时写回主存。

写穿透(Write-Through)策略(较少用):数据同时写入Cache和主存

    1. hello进程fork时的内存映射

7.6.1虚拟内存空间复用
fork()创建子进程时,父子进程的虚拟内存空间初始完全一致,但物理内存页并不立即复制。子进程与父进程共享同一组物理页帧,通过页表标记为只读,避免立即分配新内存。

7.6.2写时复制触发
当任一进程尝试修改共享内存页(如写操作)时,内核检测到写保护违规,触发缺页中断。此时内核为该进程分配新的物理页帧,复制原页数据至新页,并更新页表指向新物理地址。这一过程对用户程序透明,确保数据隔离。

7.6.3关键优化点

    • 延迟分配:仅在修改时复制内存,减少frk()的初始开销。
    • 零拷贝共享:代码段、只读数据段等未修改区域仍共享物理内存,提升内存利用率。
    • 页表隔离:父子进程的页表独立,但初始时可能指向同一物理页,通过页表权限控制复制时机。

7.6.4示例场景
若hello进程的全局变量int count = 0在fork()后被父子进程分别修改,则第一次修改时会触发COW,为两个进程分配独立的物理页帧,后续修改互不影响。

7.6.5与execve()的交互
若fork()后立即调用execve()加载新程序,子进程会丢弃原内存映射,建立全新的虚拟地址空间。此时COW的优化效果显著,因新进程内存布局通常与父进程完全不同。

7.7 hello进程execve时的内存映射

旧内存映射清除:execve()会替换当前进程的内存映像,丢弃原有的代码段、数据段、堆栈等映射,为新程序创建全新的虚拟地址空间布局。

ELF文件解析:内核读取hello可执行文件的ELF格式头部,解析Program Header Table,识别需要加载的段(如.text、.data、.bss等)。

段映射建立

    • 代码段(.text):映射为只读可执行内存,存放程序指令。
    • 数据段(.data/.rdata):映射为可读写或只读内存,存放已初始化全局/静态变量。
    • BSS段(.bss):映射为可读写匿名内存(不实际占用磁盘空间),存放未初始化全局/静态变量(初始化为零)。

动态链接处理:若hello依赖共享库(如libc.so),内核会:

    • 加载共享库到内存。
    • 解析动态符号表,完成重定位(将符号引用绑定到实际地址)。
    • 调整hell进程的内存映射,包含共享库的代码、数据段。

堆与栈初始化

    • :设置初始堆基址(如0x0985c000),但仅在首次mallc时按需分配物理页。
    • :分配用户态栈空间(如0xb192a000~0xb194b000),用于存储局部变量、函数参数等。

环境变量与参数传递:将Shell传递的命令行参数(argc、argv)和环境变量映射至栈或环境变量段。

内存权限设置:通过页表标记各段权限(如代码段r-x,数据段rw-),防止非法访问。

程序入口跳转:完成映射后,内核将CPU指令指针(eip/rip)指向hello的入口地址(如_start),开始执行新程序

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

7.8.1缺页故障

缺页故障(Page Fault)是指当软件试图访问已映射在虚拟地址空间中,但目前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元(MMU)所发出的中断。缺页故障实际上并不一定是一种错误,而是操作系统利用虚拟内存增加程序可用内存空间的一种常见且必要的机制。

缺页故障可分为以下几种情况:

  1. 软性页缺失:页缺失发生时,相关的页已经被加载进内存,但没有向MMU注册。操作系统只需在MMU中注册相关页对应的物理地址即可。
  2. 硬性页缺失:相关的页在页缺失发生时未被加载进内存。操作系统需要寻找到一个空闲的页,或者把另外一个使用中的页写到磁盘上(如果其在最后一次写入后发生了变化的话),并注销在MMU内的记录,然后将数据读入被选定的页,并向MMU注册该页。
  3. 无效页缺失:程序访问的虚拟地址不存在于虚拟地址空间内。这通常是一个软件问题,操作系统会采取相应的措施,如终止相关进程。

7.8.2缺页中断处理

缺页中断处理是操作系统响应缺页故障的过程,主要包括以下步骤:

硬件陷入内核:当CPU访问的虚拟地址所对应的页面不在物理内存时,硬件自动触发缺页中断,并将控制权交给操作系统内核。

保存进程状态:操作系统内核保存当前进程的寄存器等状态信息,并将进程置为阻塞态,避免竞争。

查找所需页面:操作系统通过页表项或外存页面表(如交换分区)确定该页在磁盘的物理地址。

分配内存:操作系统优先使用空闲页框,若不足则调用页面置换算法(如LRU)选出牺牲页,写回磁盘(若被修改过)。

调入更新:操作系统通过I/O操作读取磁盘数据到内存页框后,更新页表项的存在位和物理页框号,必要时重置访问/修改位。

恢复进程现场:操作系统恢复进程上下文后,CPU重新执行触发缺页的指令,此时页已在内存可正常访问。

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。(此节课堂没有讲授,选做,不算分)无。

7.10本章小结

7.10.1虚拟内存与缺页处理

  • 目的:通过分页/分段扩展进程地址空间,实现按需加载(Lazy Loading)。
    • 缺页流程
      • 触发:访问未加载页 → MMU抛出缺页中断。
      • 处理:内核分配物理页(必要时置换旧页)→ 磁盘I/O加载数据 → 更新页表 → 恢复执行。
    • 分类:软性页缺失(仅需注册页表)、硬性页缺失(需磁盘调页)、无效页缺失(程序错误)。

7.10.2页表与TLB加速

    • 多级页表:压缩页表内存占用(如x86四级页表),但增加访问延迟。
    • TLB作用:缓存页表项,命中时将地址转换延迟从多次内存访问降至1次。
    • 性能权衡:TLB命中率是关键,大页(Huge Pages)、预取(Prefetching)可优化。

7.10.3进程内存映射

    • fork()COW:子进程共享父进程页,仅在修改时复制,减少内存开销。
    • execve()ELF加载:丢弃原内存映像,解析ELF文件建立新地址空间(代码段、数据段、动态库等)。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

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

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

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

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

8.4 getchar的实现分析

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

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

8.5本章小结

(第8 选做 0分)

结论

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

用户输入与Shell解析

    • 用户通过Shell输入./hello,Shell解析命令行参数,通过fork()创建子进程,并调用execve()加载hello程序。

程序加载与内存映射

    • 内核解析hello的ELF文件,将代码段(.text)、数据段(.data、.rodata)、BSS段(.bss)映射到虚拟内存,并分配堆栈空间。
    • 动态链接库(如libc.so)通过共享内存映射加载,减少内存占用。

TLB与页表转换

    • CPU访问hello的代码或数据时,MMU通过页表(或TLB命中)将虚拟地址转换为物理地址。
    • 若发生缺页中断,内核从磁盘加载数据到物理内存,并更新页表。

指令执行与系统调用

    • CPU从hello的入口地址(如_start)开始执行指令,调用printf等库函数,最终通过系统调用(如write)向标准输出写入“Hello, World!”。

进程终止与资源释放

    • hello执行完毕后,通过exit()系统调用终止进程,内核释放其虚拟内存、页表、文件描述符等资源。

对计算机系统设计与实现的深切感悟

分层抽象的力量

    • 从硬件(MMU、TLB)到操作系统(虚拟内存、进程管理)再到用户程序(hello),每一层通过抽象隐藏复杂性,使上层无需关心底层实现(如printf无需关心物理内存地址)。

性能与复杂性的权衡

    • 多级页表减少了内存占用,但增加了访问延迟;TLB缓解了这一问题,但需要缓存一致性维护。这种权衡贯穿计算机系统设计(如缓存、指令流水线等)。

共享与隔离的平衡

    • 虚拟内存、进程隔离(如fork()的COW)和共享库(如libc.so)体现了系统对资源利用率与安全性的双重追求。

创新理念:面向未来计算机系统的设计与实现方法

动态自适应内存管理

    • 问题:传统页表固定大小(如4KB)对大数据/AI应用不高效。
    • 创新:设计动态调整页大小的机制,根据应用特性(如数据局部性)自适应选择页大小(如混合4KB/2MB页)。

硬件-软件协同的TLB优化

    • 问题:TLB命中率是性能瓶颈。
    • 创新
      • 硬件层面:引入预测性TLB(如基于分支预测的预加载)。
      • 软件层面:编译器插入提示(Hint),指导硬件预取关键页表项。

安全增强的内存隔离

    • 问题:侧信道攻击(如Spectre)利用内存访问模式泄露信息。
    • 创新
      • 硬件层面:引入随机化页表布局或访问模式混淆。
      • 软件层面:通过运行时验证(如基于形式化验证的内存访问检查)增强安全性。

跨层资源调度

    • 问题:传统系统仅在OS层调度CPU/内存,忽略硬件特性(如缓存、NUMA)。
    • 创新:设计跨层调度器,结合应用特征(如线程局部性)和硬件状态(如缓存占用)动态分配资源。

总结

hello程序的执行是计算机系统分层抽象的典型体现,而其背后的虚拟内存、进程管理、TLB等机制则是性能与复杂性的权衡结果。未来系统设计需突破传统架构的静态性,通过硬件-软件协同、动态自适应和安全增强等创新方法,应对新兴应用(如AI、大数据)的挑战。

(结论0分,缺失-1分)

附件

  1. hello.c:源代码文件。
  2. hello.i:预处理后的文本文件。
  3. hello.s:编译后的汇编文件。
  4. hello.o:汇编后的可重定位文件。
  5. hello:链接后的可执行文件。

参考文献

[1] Randal E.Bryant . 深入理解计算机系统[M]. 北京:机械出版社,2016.7

[2] https://blog.csdn.net/qq_36314864/article/details/121250743

[3] https://blog.csdn.net/hzp020/article/details/83765267

[4] https://www.runoob.com/w3cnote/gcc-parameter-detail.html

[5]https://www.cnblogs.com/skywang12345/archive/2013/05/30/3106570.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值