程序人生-Hello’s P2P

摘  要

    本文以“程序人生-Hello's P2P”程序为例,分析了C语言程序从源代码到可执行进程的全生命周期。通过预处理、编译、汇编、链接四阶段,结合Ubuntu环境下的工具链,生成可执行文件;详细探讨了进程管理机制、存储管理及动态链接原理。报告结合实验截图与代码解析,阐述了操作系统对进程资源的分配与回收,以及硬件与软件协同实现程序高效执行的核心机制。最终,程序通过系统调用完成输入输出,并在执行结束后由操作系统回收资源,实现“从无到有再归于无”的闭环。

关键词:预处理;编译;进程管理;虚拟地址;ELF格式

目  录

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

第3章 编译................................................................................... - 7 -

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

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

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

3.4 本章小结............................................................................... - 9 -

第4章 汇编................................................................................. - 10 -

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

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

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

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

4.5 本章小结............................................................................. - 15 -

第5章 链接................................................................................. - 16 -

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

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

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

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

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

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

5.7 Hello的动态链接分析...................................................... - 21 -

5.8 本章小结............................................................................. - 21 -

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

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

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

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

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

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

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

6.7本章小结.............................................................................. - 26 -

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

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

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

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

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

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

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

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

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

7.9本章小结.............................................................................. - 28 -

结论............................................................................................... - 29 -

附件............................................................................................... - 31 -

参考文献....................................................................................... - 32 -

第1章 概述

1.1 Hello简介

从文本程序(hello.c)到可执行进程的生命周期中,Hello经历了预处理、编译)、汇编(、链接四阶段。在Shell中执行时,操作系统通过fork()创建子进程副本,再经execve()加载Hello的代码段和数据段到进程地址空间,借助mmap()映射动态库与内存区域。进程管理分配时间片,CPU通过取指-译码-执行流水线运行Hello的指令,存储管理通过MMU/TLB/页表完成虚拟地址到物理地址的转换,三级Cache加速数据访问,最终Hello在硬件上短暂执行并输出结果。

Hello的生存始于操作系统从“零资源”为其分配进程控制块、虚拟内存空间和文件描述符,通过存储管理实现VA→PA映射,依赖页表、Cache和Pagefile协调内存与磁盘。I/O管理驱动键盘输入启动进程,显卡渲染输出结果,信号机制处理可能的异常。执行结束后,OS回收所有资源:释放物理内存、关闭文件、销毁页表与进程上下文,使系统回归“零占用”状态。整个生命周期中,硬件与软件协同实现Hello从无到有再归于虚无的闭环。

1.2 环境与工具

环境:ubuntu22.04

工具:gdb,gcc等

1.3 中间结果

hello.i作用:预处理后的文件,包含宏展开、头文件合并、注释删除后的纯C代码。

hello.s作用:编译生成的汇编代码文件,将C代码转换为x86-64架构的汇编指令。

hello.o作用:汇编生成的可重定位目标文件(ELF格式),包含机器码和未解析符号。

hello作用:链接后的最终可执行文件,整合了目标文件与库函数。

hello.o-r.txt等文件作用:readelf处理后包含头信息、属性段等ELF格式的文件。

hello-objump.txt与hello.o-objdump.txt作用:反汇编代码,展示机器指令与汇编代码的对应关系。

gdb.txt作用:gdb的日志,展现了相关内存地址。

1.4 本章小结

Hello的生命周期展现了计算机系统中 软硬件协同 的完整闭环——从编译器工具链到操作系统资源管理,最终在硬件上实现程序的高效执行与资源回收,体现了计算机系统分层抽象与动态管理的核心思想。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理是C语言编译流程的第一个阶段,由预处理器(C Preprocessor, CPP)执行,负责处理源代码中以 # 开头的指令(如宏定义、头文件包含、条件编译等),生成“纯C代码”供后续编译使用。

作用:

1.宏展开:替换代码中的宏(如 #define 定义的常量或函数宏)。

2.头文件包含:将 #include 指令指向的头文件(如 stdio.h)内容直接插入到源文件中。

3.条件编译:根据 #ifdef/#ifndef 等指令选择性保留或删除代码块。

4.删除注释:清除所有单行(//)和多行(/* ... */)注释。

5.行号标记:添加 #line 指令以保留调试信息(如源文件行号)。

2.2在Ubuntu下预处理的命令

指令:gcc -E -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.c -o hello.i 

以下为预处理截图:

图1 在Ubuntu下预处理hello.c的截图

2.3 Hello的预处理结果解析

预处理后的 hello.i 文件主要包含以下关键内容:

  1. 头文件展开

#include <stdio.h>、#include <unistd.h>、#include <stdlib.h> 被替换为对应头文件的实际内容。例如:

stdio.h 展开后包含标准输入输出函数(如 printf)的声明、FILE 结构体定义等。

unistd.h 展开后包含系统调用(如 sleep、getchar)的声明。

stdlib.h 展开后包含 exit、atoi 等函数的声明。

  1. 预处理标记

以 # 开头的行是预处理器的行号标记,用于保留原始代码的行号信息,方便调试和错误定位。

  1. 原始代码保留

 main 函数代码(如 printf、for 循环、sleep 等)完整保留在文件末尾,但注释已被删除。

  1. 类型与宏定义

插入系统级类型定义(如 size_t、ssize_t、__gnuc_va_list 等)。

展开标准库宏(如 __attribute__ 修饰符、__restrict 等)。

  1. 编译器兼容性处理

包含与平台相关的定义(如 x86_64-linux-gnu 路径、__USE_GNU 宏等),确保代码在不同环境下编译一致。

2.4 本章小结

预处理是C程序编译的首要阶段,由预处理器解析以 # 开头的指令,完成宏替换、头文件展开(如插入 stdio.h 中的函数声明)、条件编译(#ifdef 等)、删除注释及添加行号标记,生成纯C代码文件(如 hello.i)。通过 gcc -E 命令触发,其输出整合了系统类型定义、平台兼容性处理(如路径宏)和用户代码(保留逻辑但去注释),为后续编译提供规范化输入,确保跨环境一致性与调试便利性。

第3章 编译

3.1 编译的概念与作用

概念:编译是将预处理后的 .i 文件(纯C代码)转换为汇编语言文件 .s 的过程,由编译器(如GCC的 cc1)完成。此阶段对代码进行词法分析、语法分析、语义检查及中间代码优化,最终生成与目标机器架构(如x86-64)对应的汇编指令。

作用:

  1. 语法检查:验证代码是否符合C语言规范,检测语法错误(如类型不匹配、未定义变量)。
  2. 优化代码:根据编译选项(如 -Og)进行局部优化(删除冗余计算、简化逻辑),提升程序效率。
  3. 生成汇编代码:将高级C代码翻译为低级汇编指令,与目标CPU架构(如寄存器操作、内存寻址模式)紧密相关。
  4. 符号解析:为全局变量、函数生成符号表,为后续链接阶段提供地址引用信息。

3.2 在Ubuntu下编译的命令

gcc -S -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.i -o hello.s 

图2 在Ubuntu下编译hello.i的截图

3.3 Hello的编译结果解析

3.3.1 数据与变量

1.常量:

字符串常量存储在 .rodata 只读数据段:

.LC0: 中文错误提示字符串(UTF-8编码的八进制表示)。

.LC1: "Hello %s %s %s\n" 格式字符串。

立即数:如 $5(argc 参数检查)、$0(循环计数器初始化)。

2.变量:

局部变量:

%ebp 作为循环计数器 i,对寄存器进行分配。

%rbx 保存 argv 指针,通过偏移访问参数(如 16(%rbx) 对应 argv[2])。

      1. 赋值与操作符

1.赋值操作:

movl $0, %ebp:初始化 i = 0。

addl $1, %ebp:i++,循环计数器自增。

2.算术操作:

addq $8, %rsp:栈指针调整(rsp += 8)。

subq $8, %rsp:栈空间分配(rsp -= 8)。

3.逻辑与位操作:

cmpl $5, %edi:比较 argc 与 5。

jne .L6:若 argc != 5 则跳转,进行逻辑判断。

      1. 类型转换

显式转换:

call strtol:将 argv[4] 字符串显式转换为 long 类型,对atoi 底层实现。

      1. 控制转移

1.条件分支:

cmpl $5, %edi → jne .L6:if (argc != 5) 执行错误处理。

cmpl $9, %ebp → jle .L3:for (i=0; i<=9; i++) 控制循环。

2.循环结构:

.L2 和 .L3 标签实现 for 循环逻辑。

3.函数返回:

movl $0, %eax → ret:return 0(返回值通过 %eax 传递)。

      1. 函数操作

1.参数传递:

寄存器传参:

%edi 传递 argc,%rsi 传递 argv。

printf 参数:%esi(格式串地址)、%edx、%rcx、%r8(argv[1]、argv[2]、argv[3])。

2.函数调用:

call puts:输出错误信息。

call __printf_chk:调用带安全检查的 printf。

call sleep:延时执行。

3.局部变量存储:

%rbp 和 %rbx 保存循环计数器和 argv,通过 pushq/popq 保护寄存器。

      1. 数组/指针操作

指针偏移访问:

16(%rbx):访问 argv[2],%rbx 为 argv 基地址,每个指针占8字节。

32(%rbx):访问 argv[4],传递给 strtol。

      1. 其他指令

1.栈帧管理:

pushq %rbp、pushq %rbx:保存调用者寄存器。

subq $8, %rsp:分配栈空间。

2.调试信息:

.cfi_* 指令:生成调用帧信息,用于异常处理和调试。

3.4 本章小结

编译是C程序构建的核心阶段,将预处理后的 .i 文件通过编译器转换为目标架构的汇编文件 .s,过程涵盖词法/语法分析、语义检查及代码优化。其核心作用包括:

1.语法验证:检测类型错误与未定义行为;

2.代码生成:将C逻辑映射为机器相关汇编指令;

3.符号管理:为全局变量/函数生成符号表供链接使用。

在Ubuntu下通过 gcc -S命令触发,生成的汇编代码体现为:

数据存储:常量置于 .rodata 段,局部变量通过寄存器和栈操作管理;

控制流:if/for 转换为条件跳转与循环标签;

函数调用:参数经寄存器传递,遵循System V AMD64 ABI约定;

类型转换:显式调用 strtol 实现字符串到整型的转换;

底层指令:栈帧保护、调试信息及安全函数。

编译结果直接关联硬件行为,为链接和可执行文件生成奠定基础。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编是将汇编语言文件(.s)转换为机器语言二进制目标文件(.o)的过程,由汇编器完成。此阶段将人类可读的汇编指令(如 movqcall)逐条翻译为机器码(二进制指令),并生成可重定位目标文件,供后续链接阶段使用。

作用

  1. 指令翻译:将汇编指令转换为对应的机器码。
  2. 符号解析:记录代码中的符号及其地址偏移,生成符号表。
  3. 生成目标文件:输出 .o 文件,包含机器指令、数据段、重定位信息及调试信息。
  4. 平台适配:确保生成的机器码与目标CPU架构完全兼容。

4.2 在Ubuntu下汇编的命令

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

图3 在Ubuntu下汇编hello.s的截图

4.3 可重定位目标elf格式

先用readelf的各个选项[1]对hello.o进行处理,处理指令如下图。(部分指令输出结果为空或无效,此处不列出)

图4 在readelf处理hello.o的截图

图5 hello.o-h.txt文件内容截图

该文件为64位ELF可重定位文件,架构为x86-64,使用小端序和System V ABI。无程序头,包含15个节头,节头表起始于文件偏移0x490。入口点地址为0x0,表明尚未链接。

图6 hello.o-n.txt文件内容截图

这部分包含控制流保护属性:x86 feature: IBT, SHSTK,表明编译时启用了安全增强选项。

图7 hello.o-r.txt文件内容截图

重定位信息集中在.rela.text和.rela.eh_frame节:

  1. .rela.text(9项):

类型R_X86_64_32:修正静态数据地址。

类型R_X86_64_PLT32:修正函数调用(如puts、exit、strtol),使用过程链接表机制。

类型R_X86_64_PC32:修正与位置无关的地址。

  1. .rela.eh_frame(1项):修正异常处理框架地址,指向.text起始。

图8 hello.o-s.txt文件内容截图

这部分包括:main函数:位于.text节,大小135字节。

未定义符号:puts、exit、__printf_chk等,标记为UND。

只读数据符号:.rodata.str1.8和.rodata.str1.1存放字符串常量。

图9 hello.o--S.txt文件内容截图

这部分共15个节,关键节包括:

.text(代码段):偏移0x40,大小0x87,标记为AX(可分配、可执行)。

.rela.text(代码重定位表):偏移0x310,包含9项,链接到符号表(索引12)。

.rodata.str1.8和.rodata.str1.1(只读字符串数据):分别存储格式化字符串。

.symtab(符号表):偏移0x198,包含13项,包括全局符号main和未定义符号。

图9 hello.o-t.txt文件内容截图

这部分包括以下几个部分:

.text:标记ALLOC, EXEC(可分配、可执行)。

.rodata.str1.8:标记ALLOC, MERGE, STRINGS(合并重复字符串)。

.data和.bss:标记WRITE, ALLOC(可写、可分配)。

4.4 Hello.o的结果解析

反汇编与汇编代码对照分析

1. 函数调用与重定位

汇编代码:call puts

反汇编:1e: e8 00 00 00 00        call   23 <main+0x23>

1f: R_X86_64_PLT32   puts-0x4

机器码:e8是call的操作码,后4字节为占位符(全0),需链接时通过重定位项R_X86_64_PLT32填充puts的PLT条目地址。

不一致性:汇编中直接使用符号puts,而机器码中为占位符,体现目标文件未链接时的地址未决状态。

2. 分支跳转

汇编代码:jne .L6

反汇编:d: 75 0a                 jne    19 <main+0x19>

机器码:75是jne的操作码,0a为偏移量(10字节),目标地址通过相对偏移计算(当前指令地址0xd + 偏移0xa + 2 = 0x19)。

映射关系:汇编标签.L6在机器码中转换为具体偏移量,无重定位需求(因跳转目标在同一节内)。

3. 数据引用

汇编代码:movl     $.LC0, %edi

反汇编:19: bf 00 00 00 00        mov    $0x0,%edi

1a: R_X86_64_32   .rodata.str1.8

机器码:bf是mov $imm32, %edi的操作码,后4字节为占位符(全0),通过R_X86_64_32重定位项指向.rodata.str1.8的地址。

不一致性:汇编中直接引用.LC0标签,机器码中需重定位静态数据地址。

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

1. 操作码与指令

机器语言:由二进制操作码和操作数构成。例如:

endbr64 → f3 0f 1e fa

push %rbp → 55

sub $0x8,%rsp → 48 83 ec 08

汇编语言:使用助记符(如push、call)和符号(如%rbp、.LC0)表示指令和操作数。

2. 操作数编码

立即数:

汇编:mov $0x0, %ebp

机器码:bd 00 00 00 00(bd为操作码,后4字节为小端序的0)。

内存地址:

汇编:movq 16(%rbx), %rcx

机器码:48 8b 4b 10(8b为mov操作码,4b 10表示%rbx+0x10)。

3. 分支与调用

相对偏移跳转:

汇编:jle .L3

机器码:7e be(7e为jle操作码,be为有符号偏移量-66,计算目标地址)。

函数调用占位符:

汇编:call __printf_chk

机器码:e8 00 00 00 00 + 重定位项R_X86_64_PLT32,链接时替换为实际地址。

4.5 本章小结

汇编阶段通过汇编器将汇编指令逐条翻译为二进制机器码,生成包含代码段、只读数据、符号表及重定位信息的ELF格式的可重定位目标文件。该文件通过节头记录各段属性,并通过重定位项标记未解析符号地址,为链接阶段提供基础。机器语言以操作码和操作数构成,与汇编语言的符号化指令存在映射差异,体现在地址占位符、偏移量编码及安全属性的底层实现。

5章 链接

5.1 链接的概念与作用

概念:链接是将多个目标文件和库文件合并为单个可执行文件的过程。链接器负责解析符号引用、合并代码与数据段、分配运行时地址,并生成最终的可执行格式。

作用:

  1. 符号解析:绑定未定义符号到库函数或其它目标文件的定义。
  2. 地址重定位:修正目标文件中的地址占位符,使其指向实际内存位置。
  3. 段合并:将各目标文件的 .text、.data 等段合并为可执行文件中的连续内存区域。
  4. 库链接:集成静态库或动态库,如 C 标准库 libc。

5.2 在Ubuntu下链接的命令

ld -o hello \

   /usr/lib/x86_64-linux-gnu/crt1.o \

   /usr/lib/x86_64-linux-gnu/crti.o \

   /usr/lib/gcc/x86_64-linux-gnu/11/crtbegin.o \

   hello.o \

   -L/usr/lib/gcc/x86_64-linux-gnu/11 \

   -L/usr/lib/x86_64-linux-gnu \

   -lc -lgcc -lgcc_s \

   /usr/lib/gcc/x86_64-linux-gnu/11/crtend.o \

   /usr/lib/x86_64-linux-gnu/crtn.o \

   -dynamic-linker /lib64/ld-linux-x86-64.so.2 

图10 在Ubuntu下用ld链接hello.o以及相关文件的截图

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

图11 用readelf处理hello的截图

(hello-a.txt文件过长,此处不列出)

1. ELF头

类型:可执行文件,入口地址为0x4010f0。

架构:x86-64,小端序,使用System V ABI。

程序头:12个条目(描述内存加载布局),节头:30个条目(描述各节信息)。

动态链接器:/lib64/ld-linux-x86-64.so.2。

2. 程序头

LOAD段:

代码段:0x401000–0x40126d,包含.init、.plt、.text、.fini,权限为R E(可读、可执行)。

只读数据段:0x402000–0x4020fc,包含.rodata、.eh_frame,权限为R。

数据段:0x403df0–0x404060,包含.data、.bss、.dynamic、.got.plt,权限为RW(可读、可写)。

动态段:位于0x403e00,记录依赖库、初始化函数地址(INIT=0x401000)等。

3. 关键节

代码节:

地址0x4010f0,包含用户代码和启动代码。

过程链接表:

地址0x401020,用于动态链接外部函数,通过.got.plt实现延迟绑定。

数据节:

.data:初始化全局变量(地址0x404048)。

.bss:未初始化数据(地址0x404060),如stdin符号。

只读数据:

地址0x402000,存储字符串常量。

4. 动态链接与重定位

重定位表:

.rela.plt:6项,修正.plt中的函数调用,类型为R_X86_64_JUMP_SLOT。

.rela.dyn:3项,修正全局数据引用。

全局偏移表(.got.plt):地址0x404000,存储动态库函数的实际地址。

5. 符号与版本控制

动态符号表:列出依赖的共享库符号,标记为UND。

版本信息:

.gnu.version:指定符号的GLIBC版本(如GLIBC_2.2.5)。

.gnu.version_r:声明依赖的库版本(如libc.so.6需兼容GLIBC_2.34)。

6. 安全与扩展属性

控制流保护:.note.gnu.property启用IBT(间接分支跟踪)和SHSTK(影子栈)。

只读重定位(GNU_RELRO):保护.got.plt等段在初始化后设为只读,防止篡改

5.4 hello的虚拟地址空间

图12 用objdump处理hello的截图

1. 代码段

GDB映射:

0x401000–0x402000     0x1000     0x1000  r-xp   /home/ki/CShello/hello

权限:r-xp(可读、可执行),对应ELF程序头中的LOAD段(R E)。

地址匹配:ELF的.text节虚拟地址为0x4010f0,位于该内存段内。

内容:包含用户代码和启动代码。

2. 只读数据段

GDB映射:

0x402000–0x403000     0x1000     0x2000  r--p   /home/ki/CShello/hello

权限:r--p(只读),对应ELF程序头中的LOAD段(R)。

地址匹配:ELF的.rodata节地址为0x402000,与此段完全一致。

内容:存储字符串常量。

3. 数据段

GDB映射:

0x404000–0x405000     0x1000     0x3000  rw-p   /home/ki/CShello/hello

权限:rw-p(可读、可写),对应ELF程序头中的LOAD段(RW)。

地址匹配:ELF的.data节地址为0x404048,.bss为0x404060,均位于此段。

内容:包含初始化全局变量和未初始化数据。

4. 动态链接库

GDB映射:

0x7ffff7c00000–0x7ffff7e1c000     libc.so.6(权限:r--p, r-xp, rw-p)

0x7ffff7f8b000–0x7ffff7fab000     libgcc_s.so.1(权限:r--p, r-xp)

ELF动态段:

依赖库声明:NEEDED libc.so.6和NEEDED libgcc_s.so.1。

动态符号表中引用了puts、exit等库函数。

权限匹配:代码段(r-xp)与只读数据段(r--p)符合ELF的共享库加载规则。

5. 栈

GDB映射:

0x7ffffffde000–0x7ffffffff000     rw-p   [stack]

权限:rw-p,用于函数调用栈和局部变量。

6. 动态链接器

GDB映射:

0x7ffff7fc3000–0x7ffff7fff000     ld-linux-x86-64.so.2

ELF程序头:

指定动态链接器路径:/lib64/ld-linux-x86-64.so.2。

动态段记录符号解析和重定位信息。

5.5 链接的重定位过程分析

1. 主要差异分析

(1) 函数调用与PLT机制

hello.o:

函数调用的地址为占位符(e8 00 00 00 00),通过重定位项R_X86_64_PLT32标记。

hello:

函数调用通过PLT实现动态绑定,地址被替换为PLT条目。

PLT条目通过.got.plt实现延迟绑定。

(2) 数据引用与地址修正

hello.o:

数据引用使用占位符地址,通过重定位项R_X86_64_32标记。

hello:

字符串常量被合并到.rodata段,地址修正为实际内存位置。

(3) 全局变量与动态符号

hello.o:

全局符号未定义,需链接时解析。

hello:

动态符号通过.got引用,地址由动态链接器填充。

2. 链接过程的核心步骤

(1)符号解析:

链接器将hello.o中的未定义符号与共享库中的定义绑定。

静态符号被合并到可执行文件的对应段。

(2)地址分配与段合并:

将各目标文件的代码段、数据段等按权限合并为连续的虚拟内存区域。

(3)重定位修正:

静态重定位:修正代码中的绝对地址引用。

类型R_X86_64_32:直接替换为最终地址。

动态重定位:通过PLT/GOT机制处理外部函数调用。

类型R_X86_64_PLT32:替换为PLT条目地址。

类型R_X86_64_JUMP_SLOT:运行时由动态链接器填充.got.plt。

3. 重定位项目的具体处理

函数调用:

在hello.o中,call指令的偏移量为占位符,重定位类型为R_X86_64_PLT32。

链接时,链接器将call目标替换为PLT条目地址,并在.rela.plt中生成R_X86_64_JUMP_SLOT条目,供动态链接器运行时解析。

数据引用:

在hello.o中,mov $0x0,%edi引用了.rodata.str1.8的地址,重定位类型为R_X86_64_32。

链接时,链接器将0x0替换为.rodata段的实际地址。

全局变量:

在hello.o中,stdin通过R_X86_64_PC32重定位,地址占位符为0x0。

链接时,链接器在.got中分配条目,并在.rela.dyn中标记R_X86_64_COPY,由动态链接器填充实际地址。

5.6 hello的执行流程

图13 反汇编处理main的截图

首先程序在main+30处跳转到puts函数,然后再main+40处跳转到exit函数,接着依次在main+72处跳转到__printf_chk函数,在main+91处跳转到strtol函数,在main+98处跳转到sleep函数,最后在main+118处跳转到gets函数。以下是反汇编各个函数的截图:

图14 反汇编处理puts的截图

图15 反汇编处理exit的截图

图16 反汇编处理__printf_chk的截图

图17 反汇编处理strtol的截图

图18 反汇编处理sleep的截图

图19 反汇编处理getc的截图

5.7 Hello的动态链接分析

图20 反汇编动态链接的截图

如图,在链接后,0x404018的地址发生了变化。

5.8 本章小结

本章阐述了链接的核心机制,涵盖从目标文件合并到可执行文件生成的全过程。链接器通过符号解析绑定未定义符、地址重定位修正代码与数据的引,并合并代码、数据等形成内存连续布局。可执行文的ELF格式包含程序头、动态及安全属。动态链接通过全局偏移在运行时解析函数地址,而虚拟地址空间映射则与ELF结构严格对应,确保程序高效安全执行。

6章 hello进程管理

6.1 进程的概念与作用

概念:进程是程序的一次执行实例,是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的虚拟地址空间、文件描述符、寄存器状态等资源。

作用:

并发执行:允许多个程序同时运行。

资源隔离:进程间内存与资源相互隔离,防止相互干扰。

任务管理:操作系统通过进程控制块跟踪进程状。

权限控制:不同进程可拥有不同的用户权限,增强系统安全性。

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

作用:

命令解析:读取用户输入的命令,解析参数和环境变量。

进程管理:通过fork创建子进程,通过execve加载并执行目标程序。

I/O重定向:支持输入输出重定、管等。

作业控制:管理前后台进。

处理流程:

1.读取命令:从终端或脚本读取输。

2.解析命令:分割命令为可执行文件名和参。

3.查找程序:在$PATH环境变量中搜索可执行文件路径。

4.创建子进程:调用fork()创建子进程。

5.加载程序:子进程中调用execve()加载并执行目标程序。

6.等待完成:父进程Shell通过waitpid()等待子进程结束,获取退出状态。

6.3 Hello的fork进程创建过程

1.Shell调用fork():

复制父进程Shell的地址空间、文件描述符、寄存器状态等,生成子进程。

关键特性:fork()返回两次:

父进程:返回子进程的PID。

子进程:返回0。

2.子进程准备执行:

子进程与父进程独立运行,共享代码段,但拥有独立的数据段和堆栈。

3.执行目标程序:子进程调用execve()加载hello,替换当前进程映像。

6.4 Hello的execve过程

1.参数传递:

execve("./hello", argv, environ)接收程序路径、参数列表和环境变量。

2.加载可执行文件:

解析hello的ELF格式,验证权限与架构兼容性。

3.内存映射:

根据程序将代码、数据等映射到进程虚拟地址空间。

4.初始化寄存器:

设置程序计数器指向入口地,栈指针指向新栈顶。

5.动态链接:

加载依赖的共享,通过动态链接解析符号。

6.进程替换完成:

原进程的代码和数据被完全替换,开始执行hello的_start函数,最终调用main。

6.5 Hello的进程执行

1. 进程上下文信息

进程上下文是进程运行时的完整状态,包括:

寄存器状态:程序计数器、栈指针、通用寄存器等。

内存管理信息:页表、地址空间映射。

进程控制块:进程ID、优先级、状态等。

2. 进程时间片与调度触发

时间片:操作系统为每个进程分配的CPU时间。

触发调度的场景:

(1)时间片耗尽:hello进程持续运行至时间片结束,CPU被强制回收。

(2)主动让出CPU:hello执行系统调用或等待I/O时主动阻塞。

(3)高优先级抢占:更高优先级进程就绪,抢占hello的CPU使用权。

3. 进程调度的核心过程

(1)中断当前进程:

时钟中断:时间片耗尽时触发,CPU切换到核心态。

系统调用或异常:如hello调用printf触发write系统调用。

(2)保存上下文:

将hello的寄存器状态、程序计数器等保存到其PCB中。

(3)选择新进程:

调度器从就绪队列中选择下一个进程。

(4)加载新上下文:

从新进程的PCB恢复寄存器、内存映射等状态。

(5)切换CPU控制权:

跳转到新进程的程序计数器地址,继续执行。

4. 用户态与核心态转换

用户态到核心态:

触发条件:系统调用、中断、异常。

操作流程:

(1)CPU通过陷阱指令进入内核。

(2)保存用户态寄存器到内核栈。

(3)执行内核代码。

核心态到用户态:

触发条件:系统调用/中断处理完成。

操作流程:

(1)从内核栈恢复用户态寄存器。

(2)通过返回到用户空间指令切换回用户态。

5. 结合hello进程的完整流程

(1)启动与调度:

Shell通过fork+execve加载hello,进程进入就绪队列。

调度器分配时间片,hello开始运行。

(2)时间片耗尽:

时钟中断触发,进入核心态,保存hello上下文,调度器选择新进程。

(3)系统调用触发:

hello调用printf,write系统调用,用户态切换到核心态,内核处理I/O。

I/O完成,返回用户态继续执行hello。

(4)进程终止:

hello执行完毕,调用exit系统调用,进入核心态释放资源,父进程回收状态。

6.6 hello的异常与信号处理

按下ctrl+c时有以下错误:

图21 hello异常截图

  1. 异常原因

在运行C语言编写的hello程序时按下 Ctrl+C,触发了 Python相关的错误。这表明该C程序内部可能 间接调用了Python解释器 或 依赖了嵌入Python的库。按下Ctrl+C时,系统向进程发送 SIGINT信号,但此时Python解释器正在初始化,信号中断了Python的启动流程,导致异常。

2. 信号处理流程

(1)Ctrl+C触发SIGINT信号:

默认行为是终止进程。

(2)信号传递时机:

若SIGINT发生在Python解释器初始化阶段,可能中断关键操作,引发Keyboard Interrupt异常。

(3)Python初始化流程:

Python启动时会加载site模块,若此时被SIGINT中断,会导致Fatal Python Error。

6.7本章小结

本章阐述了进程管理的核心机制,涵盖从进程概念到执行调度的全流程。进程作为程序执行的实例,通过独立地址空间和资源隔离实现并发与安全,由操作系统通过进程控制块跟踪状态。Shell作为用户与系统的桥梁,解析命令后通过fork创建子进程、execve加载目标程序,并管理I/O与作业。进程调度依赖时间片轮转与上下文切换,触发场景包括时间片耗尽、系统调用或高优先级抢占,期间通过用户态与核心态转换处理中断与资源访问。信号处理在异常时介入,若进程依赖外部环境,信号可能中断关键流程,导致致命错误。整体上,进程管理通过资源分配、状态切换与信号响应,保障多任务高效、安全执行。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序直接使用的地址(如hello中的0x401090),由 段选择子 + 偏移量 组成。

线性地址:逻辑地址通过 段式管理 转换后的地址,在32位系统中直接作为虚拟地址。

虚拟地址:在分页机制中,进程视角的连续地址空间,需通过 页式管理 转换为物理地址。

物理地址:实际内存硬件上的地址,由CPU通过MMU转换得到。

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

Intel使用 段描述符表 实现段式管理:

1.逻辑地址 = 段选择子(16位) + 偏移量(32/64位)。

2.CPU通过段选择子从GDT中获取 段基址 和 段界限

3.线性地址 = 段基址 + 偏移量。

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

现代系统采用 页式管理

1.虚拟地址(VA 分割为页表索引和页内偏移。

2.MMU逐级查询页表,获取物理页框号。

3.物理地址(PA = PFN × 页大小(4KB) + 页内偏移。

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

TLB(快表):缓存最近使用的虚拟页到物理页的映射,加速地址转换。

四级页表

1.CR3寄存器 指向PML4表基址。

2.虚拟地址的48位分割为:PML4(9) + PDPT(9) + PD(9) + PT(9) + Offset(12)。

3.若TLB命中,直接获取PFN;否则逐级查询页表,并更新TLB。

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

CPU访问物理内存时,通过缓存层级减少延迟:

1.L1 Cache:分指令/数据缓存,访问速度1-3周期。

2.L2 Cache:统一缓存,速度约10周期。

3.L3 Cache:共享缓存,速度约30-40周期。

若数据不在Cache(Cache Miss),需从内存加载,耗时约100+周期。

7.6 hello进程fork时的内存映射

写时复制(Copy-On-Write

1.fork()复制父进程的页表,父子进程共享物理页。

2.当任一进程尝试写入共享页时,触发缺页异常,内核复制该页并更新页表。

3.hello的代码段(只读)保持共享,数据段(可写)按需复制。

7.7 hello进程execve时的内存映射

1.清除原内存映像:释放旧进程的代码、数据、堆栈段。

2.加载新程序:根据ELF程序头,将hello的代码段(.text)、数据段(.data)映射到虚拟地址空间。

3.初始化堆栈:设置命令行参数(argv)和环境变量(environ)。

4.动态链接:加载依赖库(如libc.so)并解析符号。

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

缺页故障:访问未映射的虚拟地址或权限不足时触发。

处理流程

1.CPU陷入内核态,保存上下文。

2.内核检查缺页原因:

文件映射缺页:从磁盘(如可执行文件)加载数据到物理页。

匿名页缺页:分配零页(如堆栈扩展)。

写时复制:复制物理页并更新页表。

3..恢复进程执行,重新执行触发缺页的指令。

7.9本章小结

存储管理通过多级地址转换机制实现高效内存分配与隔离。逻辑地址经Intel段式管理转换为线性地址,再通过四级页表与TLB完成虚拟地址到物理地址的映射,确保进程间内存隔离与安全。物理内存访问借助三级缓存优化性能,减少延迟。进程管理中,fork采用写时复制共享内存资源,execve替换进程内存映像并加载目标程序,动态链接依赖库。缺页中断机制处理未映射或权限异常,通过文件加载、零页分配或页复制保障内存连续性。整体上,存储管理结合地址转换、缓存优化与动态内存策略,平衡效率与资源隔离,支撑系统稳定高效运行。

结论

  1. 预处理阶段

输入:hello.c

处理:预处理器解析以#开头的指令,完成宏展开、头文件插入、条件编译、注释删除等,生成纯C代码文件hello.i。

关键作用:规范化输入,确保跨平台兼容性与调试信息保留。

  1. 编译阶段

输入:hello.i

处理:编译器进行词法/语法分析、语义检查与代码优化,生成x86-64架构的汇编文件hello.s。

关键作用:将高级C代码转换为低级汇编指令,生成符号表,为后续地址绑定提供基础。

  1. 汇编阶段

输入:hello.s

处理:汇编器将汇编指令逐条翻译为机器码,生成可重定位目标文件hello.o,包含ELF格式的代码段、数据段及重定位信息。

关键作用:生成二进制目标文件,标记未解析符号地址,为链接做准备。

  1. 链接阶段

输入:hello.o及系统库

处理:链接器解析符号引用,合并代码段与数据段,分配运行时虚拟地址,生成可执行文件hello。

关键机制:静态重定位修正绝对地址,动态链接通过PLT/GOT实现延迟绑定。

  1. 进程创建与加载

Shell处理:输入./hello后,Shell通过fork()创建子进程,子进程调用execve()加载hello的ELF文件。

内存映射:操作系统分配虚拟地址空间,映射代码段、数据段、堆栈段,加载动态库。

资源分配:进程控制块、文件描述符、页表等初始化。

  1. 进程执行与调度

CPU执行:取指-译码-执行流水线运行hello的指令,通过MMU/TLB/页表完成虚拟地址到物理地址的转换。

调度机制:时间片轮转触发上下文切换,用户态与内核态转换处理系统调用。

三级缓存优化:L1/L2/L3 Cache加速物理内存访问,减少CPU访存延迟。

  1. 存储管理

地址转换:逻辑地址→线性地址→虚拟地址→物理地址。

写时复制:fork()时共享父进程页表,首次写入触发缺页中断复制物理页。

缺页处理:未映射页触发缺页中断,内核分配物理页或从文件加载数据。

  1. I/O与信号处理

输出:printf通过vsprintf格式化字符串,调用write系统调用写入标准输出,经显卡驱动渲染至屏幕。

输入:getchar通过read系统调用读取键盘缓冲区,等待回车触发中断。

信号处理:Ctrl+C发送SIGINT信号,默认终止进程;若进程依赖外部库(如Python),可能引发异常处理流程。

  1. 进程终止与资源回收

终止:hello执行完毕或收到终止信号后,调用exit()系统调用。

资源回收:操作系统释放物理内存、关闭文件描述符、销毁页表与PCB,回归“零占用”状态。

附件

hello.i作用:预处理后的文件,包含宏展开、头文件合并、注释删除后的纯C代码。

hello.s作用:编译生成的汇编代码文件,将C代码转换为x86-64架构的汇编指令。

hello.o作用:汇编生成的可重定位目标文件(ELF格式),包含机器码和未解析符号。

hello作用:链接后的最终可执行文件,整合了目标文件与库函数。

hello.o-r.txt等文件作用:readelf处理后包含头信息、属性段等ELF格式的文件。

hello-objump.txt与hello.o-objdump.txt作用:反汇编代码,展示机器指令与汇编代码的对应关系。

gdb.txt作用:gdb的日志,展现了相关内存地址。

参考文献

[1]  https://blog.csdn.net/yfldyxl/article/details/81566279

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值