关注

程序人生-Hello’s P2P

摘  要

本文以课程提供的 hello.c 为对象,围绕“程序人生—Hello’s P2P”展开分析,系统梳理一个C程序从**源代码(Program)运行进程(Process)**的全过程,并从“资源从无到有再归零”的视角理解程序运行的生命周期。实验在Linux/x86-64环境下,使用GCC工具链完成预处理、编译、汇编与链接,生成可执行ELF文件;随后结合 readelf/objdump 等工具分析目标文件与可执行文件的结构差异,并借助 gdb/strace 等调试与跟踪手段观察程序被Shell启动、内核装载、创建进程、进行系统调用直至退出回收资源的过程。通过对编译系统、链接装载、进程管理、存储管理与I/O行为的串联分析,本文加深了对计算机系统“从代码到运行”的整体认识,并形成了一套可复现的实验流程与中间产物归档方法。

关键词:编译系统;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 本章小结............................................................................... - 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简介

本大作业的研究对象是课程提供的 hello.c(非传统Hello World)。该程序在命令行接收4个参数(学号、姓名、手机号、秒数),循环打印“Hello ……”并调用 sleep() 进行延时,最后以 getchar() 等待输入后退出。它具备两个非常适合系统分析的特点:

(1)既有计算密集之外的“阻塞/等待”行为(sleep与getchar),便于观察进程调度、阻塞与唤醒、以及终端I/O;

(2)运行过程中可触发Ctrl-C/Ctrl-Z等信号,便于结合 ps/jobs/pstree/fg/kill 等命令观察异常与信号处理机制。

从计算机系统术语描述,Hello 的 P2P(Program to Process) 过程可概括为:

Program阶段(静态形态):hello.c 作为文本文件存放在磁盘中;

编译系统阶段(生成可执行文件):源码依次经历预处理(生成 .i)、编译(生成 .s)、汇编(生成 .o)、链接(生成可执行ELF hello),并与标准库等依赖完成符号解析与重定位;

Process阶段(动态形态):在Shell中执行 ./hello ... 时,Shell 调用 fork/execve(或等价机制)创建并装载程序,内核为其建立地址空间、页表与运行上下文,动态链接器完成必要的运行时链接后,CPU开始执行 _start,最终进入 main。运行过程中,程序通过库函数间接触发系统调用(如向终端写输出、休眠、读取输入等),直到 return 0 触发进程正常终止,OS回收其占用资源。

Hello 的 020(Zero→…→Zero) 可从“资源视角”理解:程序从“未运行时几乎不占用系统运行资源”的状态出发;运行时占用CPU时间片、虚拟内存、文件描述符(标准输入输出)、以及缓存/页表等系统资源;退出后资源被内核回收,系统状态回到“该进程已不存在”的“归零”状态。020强调的是:程序的“生命”不仅是生成与执行,更包含资源的申请、使用与回收闭环。

1.2 环境与工具

本实验在 x86-64 架构 Linux 环境下完成,主要软硬件与工具链如下:

(1)软硬件环境

体系结构:x86_64(64位)

操作系统:Ubuntu 20.04.1 LTS(focal)

内核版本:Linux 5.4.0-42-generic

(2)编译与链接工具链(GCC + Binutils)

C 编译器:gcc 9.4.0(Ubuntu 9.4.0-1ubuntu1~20.04.2)

链接器:GNU ld 2.34(Binutils for Ubuntu)

汇编器:GNU assembler(as)2.34(Binutils for Ubuntu)

ELF/反汇编分析工具:readelf、objdump(随 Binutils 2.34 提供,用于后续章节对 .o 与可执行文件进行节/段、符号表、重定位及反汇编分析)

(3)运行与调试辅助工具

终端与 Shell:Ubuntu Terminal / bash

过程观察与调试(按实际使用填写):gdb(断点与寄存器/栈观察)、strace(系统调用与信号跟踪)、ps/jobs/pstree/kill(进程与作业控制)

1.3 中间结果

本实验在目录 /home/sbliu/ICS-hello 下开展,为撰写与分析Hello的P2P/020过程,生成并保存了如下中间结果文件:

hello.c:实验源代码文件。

hello.i:预处理后的C源码(展开头文件、宏替换后的结果),用于观察编译前的代码形态。

hello.s:编译器生成的汇编代码,用于分析数据表示、函数调用约定与指令序列。

hello.o:汇编得到的可重定位目标文件(ELF relocatable),包含节、符号表与重定位信息,是链接前的关键产物。

hello:最终可执行文件(ELF executable),由链接器将目标文件与库进行符号解析与地址重定位后生成。

hello_ld:使用 ld 手工链接生成的可执行文件(用于与默认链接产物对比,验证链接过程与选项影响)。

objdump_hello_o.txt:对 hello.o 的反汇编与相关信息导出,用于对照 hello.s 分析指令与重定位位置。

objdump_hello.txt:对可执行文件 hello 的反汇编导出,用于观察链接后地址与调用跳转的变化。

readelf_hello_o.txt:readelf 分析 hello.o 的输出(节表、符号表、重定位表等),用于解释链接前ELF结构。

readelf_hello.txt:readelf 分析可执行文件 hello 的输出(程序头/段映射、动态段等),用于解释装载与运行时布局。

ldd_hello.txt:ldd 查看 hello 的动态库依赖关系,辅助分析动态链接。

strace_hello_*.txt、strace_hello.txt:运行时系统调用/信号跟踪日志,用于分析程序I/O、sleep、退出等行为与内核交互。

gdb_log.txt:调试过程记录(断点、寄存器/栈观察等),用于支撑对执行流程的分析。

env.txt:实验环境与工具链版本信息汇总(Ubuntu版本、gcc/ld/as版本等),用于保证实验可复现。

1.4 本章小结

本章对实验对象 hello.c 的行为与特点进行了概述,并从计算机系统视角给出Hello的 P2P(从程序到进程)与 020(资源从无到有再归零)的整体框架;同时列出了实验所用软硬件环境、开发调试工具与中间产物归档方式。上述内容为后续章节对预处理、编译、汇编、链接以及进程/存储/IO等机制的逐层深入分析奠定了基础。

第2章 预处理

2.1 预处理的概念与作用

预处理(Preprocessing)是C程序“编译系统”的第一步,发生在真正的语法/语义编译之前。它本质上是文本级处理:对源文件进行宏展开、头文件展开、条件编译裁剪以及注释去除等操作,生成一个“可直接交给编译器前端的纯C文本”,通常记为 .i 文件(预处理输出也可直接送入编译器而不落盘)。

结合本实验的 hello.c,预处理主要完成以下工作:
1)展开头文件:将 #include <stdio.h>、<unistd.h>、<stdlib.h> 替换为对应头文件的实际内容,使得 printf/sleep/atoi/exit/getchar 等函数声明、类型定义与宏定义在同一个翻译单元中可见。
2)宏处理:处理 #define 等宏定义并在源码中展开(本程序源码中几乎不自定义宏,但会继承大量来自系统头文件与编译器内置的宏)。
3)条件编译:根据平台/编译器宏(如 __x86_64__、__GNUC__ 等)选择性保留或删除代码片段,使得不同平台能共享同一份代码。
4)去注释、保留行号映射:源文件中的注释被删除,同时生成形如 # 行号 "文件名" 的行标记(line markers),用于后续编译报错能定位到原始文件行号。

预处理的作用可以概括为:保证编译时信息完备(声明、类型、宏)、实现跨平台与可配置构建、并把多文件/多条件的源码整理为单一翻译单元,为后续编译生成汇编代码奠定基础。

2.2在Ubuntu下预处理的命令

在Ubuntu终端进入实验目录,对 hello.c 进行预处理并生成 hello.i:

cd /home/sbliu/ICS-hello

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

其中,-E 表示只进行预处理并输出结果;-o hello.i 指定预处理输出文件名;其余参数用于与后续编译/链接阶段保持一致的实验配置(64位、关闭PIE/栈保护等),便于统一分析。

预处理完成后可通过如下命令验证输出文件已生成并观察其规模变化:

ls -lh hello.c hello.i

wc -l hello.c hello.i

2.3 Hello的预处理结果解析

预处理生成的 hello.i 相比原始 hello.c 体积显著增大,这是由于标准库头文件被完整展开所致。对 hello.i 的内容可做如下解析:

1)头文件展开导致内容膨胀:hello.c 中的三行 #include 会被替换为对应头文件的大量声明与宏定义,从而使 hello.i 成为一个包含完整库声明信息的单一翻译单元。这保证了后续编译阶段可以直接获取 printf、sleep、atoi、exit、getchar 等函数的声明与相关类型定义。

2)出现行标记(line markers)以保持源文件行号映射:在 hello.i 的开头及头文件切换位置会出现形如:

# 1 "hello.c"、# 1 "/usr/include/stdio.h" ...

这类标记用于后续编译器报错能正确回溯到原始文件与行号,同时也反映了预处理“拼接”文件的过程。

3)注释被删除,宏被展开,但程序主体结构保持不变:源代码开头的注释(包括中文说明)在预处理输出中不会以注释形式保留;而 main 函数内部的核心语句结构仍保持:参数检查 → printf 输出 → sleep(atoi(argv[4])) 延时 → getchar() 等待输入。

需要注意:预处理不会把 printf/sleep/atoi/exit/getchar “变成机器指令”,它只保证这些标识符在语法层面是“有声明可用”的,真正的指令生成发生在编译(.s)阶段。

4)平台相关的宏与条件编译分支被确定:在头文件内部常含大量 #ifdef/#if,它们会根据当前平台宏(如 x86-64、GNU编译器版本等)决定保留哪些定义,这也是同一套源码能适配不同系统/架构的重要机制。

2.4 本章小结

本章介绍了C程序预处理阶段的概念与作用,并在Ubuntu环境下使用 gcc -E 对 hello.c 进行了预处理,生成 hello.i。通过对预处理结果的分析可以看到:预处理将头文件内容与宏/条件编译结果展开到同一翻译单元,删除注释并用行标记维持源文件定位,使得后续编译阶段能够在信息完备的前提下将C代码翻译为汇编代码。预处理输出的显著膨胀也直观反映了标准库头文件在程序构建中的基础支撑作用。

第3章 编译

3.1 编译的概念与作用

本章所说“编译”特指 从预处理输出文件 .i 到汇编文件 .s 的过程,即把已经展开头文件与宏的C文本,翻译成目标体系结构(x86-64)的汇编语言程序。

从编译器内部流程看,该阶段大致经历:
1)词法/语法分析:把C文本解析为语法结构;
2)语义分析与类型检查:确定变量/表达式类型、检查函数调用参数等;
3)中间表示(IR)生成与优化:将程序转换为更便于分析与优化的中间形式,并在 -Og(便于调试的优化)下做适度优化;
4)指令选择与寄存器分配:将IR映射为x86-64指令,并决定哪些值放寄存器、哪些放栈;
5)生成汇编:输出 .s,其中包含代码段(.text)、只读数据段(.rodata)、符号与对齐等汇编伪指令。

编译阶段的意义在于:它把“与平台无关的C描述”转为“与平台相关的指令序列与数据布局”,并体现出编译器对不同数据类型与控制结构(if/for/函数调用等)的具体实现方式,为后续汇编与链接奠定基础。

3.2 在Ubuntu下编译的命令

在实验目录下,将 hello.i 编译生成汇编文件 hello.s:

cd /home/sbliu/ICS-hello

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

其中:-S 表示“只编译到汇编”;其余参数与前序阶段保持一致,确保各阶段产物可对比。

编译完成后可用以下命令验证 .s 已生成并查看规模与关键片段:

ls -lh hello.i hello.s

wc -l hello.s

head -n 40 hello.s

3.3 Hello的编译结果解析

3.3.1 汇编文件整体结构(段、标签与伪指令)

由 gcc -S 生成的 hello.s 由两类内容组成:(1)汇编伪指令与元信息:如 .file "hello.c"、.text、.section ...、.align、.globl main、.type main,@function、.cfi_startproc 等,用于描述文件、段、符号属性、对齐和调试信息,本身不对应CPU执行指令。

(2)真正的指令序列:如 pushq/movq/subq/cmpl/jne/call 等,对应可执行指令。

从 head -n 40 hello.s 的输出可见,本程序汇编包含:只读字符串段 .rodata.str1.*,字符串标签 .LC0/.LC1,以及函数入口 main: 等结构,说明编译器已将C程序映射为“代码段 + 只读数据段”的典型布局。

3.3.2 字符串常量与只读数据(C字符串 → .rodata + 标签)

源代码中包含两处字符串常量:一处为“用法提示”(中文字符串),一处为 "Hello %s %s %s\n"。在汇编中它们分别以 .LC0:、.LC1: 的形式出现,并用 .string 伪指令保存到只读段:

.LC0: 后的 .string 中出现大量 \347\224\250... 八进制转义,这是中文UTF-8字节序列在汇编文本中的表示方式,属于正常现象;

.LC1: 后的 .string "Hello %s %s %s\n" 为格式化输出的格式串。

它们被放在:

.section .rodata.str1.8,"aMS",@progbits,1

.section .rodata.str1.1,"aMS",@progbits,1

中,运行时通过“地址”传递给输出函数,这体现了:C的字符串字面量在机器层是静态只读对象,通过标签地址引用。

3.3.3 函数入口与栈帧建立(prologue/epilogue)

在 main: 入口处可见典型的函数序言:

endbr64

pushq %rbp

pushq %rbx

subq $8, %rsp

含义为:

endbr64:与平台控制流保护相关的入口指令(安全机制插入);

pushq %rbp、pushq %rbx:保存被调用者需要保护的寄存器(callee-saved);

subq $8,%rsp:分配栈空间并用于保持调用点栈对齐。

同时出现的 .cfi_* 伪指令用于调试/异常回溯的栈展开信息,不改变程序逻辑,但能支持 gdb 回溯。

3.3.4 整型数据与关系运算(argc检查:int → cmpl + jne)

源代码中存在参数个数检查:

if (argc != 5) { ... }

在汇编中对应:

cmpl $5, %edi

jne  .L6

其中 argc 为 int,按 x86-64 System V ABI 作为 main 的第1个参数进入 %edi。cmpl 进行32位整数比较,jne 根据比较结果跳转到错误处理分支 .L6。这说明C中的 != 运算被翻译为 cmp设置标志位 + 条件跳转 的控制流形式。

3.3.5 指针/数组与寻址(argv:char** → 基址+偏移)

argv 是 char**,作为 main 第2参数进入 %rsi。汇编中先把它保存到 %rbx:

movq %rsi, %rbx

随后在循环体内通过偏移寻址取出 argv[1..3]:

movq 16(%rbx), %rcx

movq 8(%rbx),  %rdx

movq 24(%rbx), %r8

由于64位下指针大小为8字节,因此:

8(%rbx) 对应 argv[1]

16(%rbx) 对应 argv[2]

24(%rbx) 对应 argv[3]

这体现了C语言数组下标访问的本质:

在机器层面即为“基址寄存器 + (k×8)偏移”的内存读取。

3.3.6 循环结构(for:标签 + 比较 + 条件跳转)

源代码中循环:

for (i = 0; i < 10; i++)

在汇编中可见:

movl $0, %ebp

.L2:

cmpl $9, %ebp

jg   .L7

其中 i 被放入 %ebp(32位寄存器)作为循环计数器。cmpl $9,%ebp 与 jg .L7 实现循环退出条件(等价于当 i >= 10 时退出)。.L2 为循环头标签,.L7 为循环结束后的落点。这说明C的结构化循环在汇编层被拆分为基本块并由条件跳转连接。

3.3.7 函数调用与变参调用特征(__printf_chk)

在循环体中打印语句对应一次格式化输出调用。汇编中不是直接 printf,而是:

movl $.LC1, %esi

movl $1, %edi

movl $0, %eax

call __printf_chk

并且在此之前,argv[1..3] 已经分别装入 %rdx/%rcx/%r8 等寄存器。该调用体现了两点:1)x86-64 ABI寄存器传参:整数/指针参数优先通过 %rdi,%rsi,%rdx,%rcx,%r8,%r9 传递;

2)变参函数调用约定:movl $0,%eax 常用于变参调用的ABI约定信息;

3)安全检查入口:__printf_chk 是 glibc 对 printf 的带检查版本入口(FORTIFY/安全强化机制相关),语义上与 printf 等价但带额外检查。

此外,编译使用了 -no-pie -fno-PIC,因此格式串地址以 $.LC1 的形式直接作为立即数装入寄存器,而不是典型PIE/PIC下的RIP相对寻址形式,体现了编译选项对代码生成方式的影响。

3.3.8 类型转换与十进制解析(atoi语义 → 解析基数10)

源代码中 sleep(atoi(argv[4])) 涉及将字符串转换为整数。可见:

movq 32(%rbx), %rdi

movl $10, %edx

其中 32(%rbx) 对应 argv[4](第4个命令行参数字符串)。movl $10,%edx 表示以十进制基数进行解析的语义特征(常见于 strtol 形式的实现:base=10)。后续汇编通常会继续设置 endptr 参数并 call 到相应的转换函数,再将返回值传给 sleep 完成延时。这说明:C层的字符串到整数转换在汇编层表现为“准备参数寄存器 + 调用库函数”的过程。

3.3.9 本节归纳:C数据与操作在 hello.s 中的体现

综合上述片段,可以将 hello.s 中体现的“C数据与操作”总结如下:

整数与常量:argc、循环计数 i 等以32位寄存器配合 movl/cmpl 表现;

指针与数组访问:argv 保存在64位寄存器 %rbx,通过 8/16/24/32(%rbx) 进行指针数组元素寻址;

字符串常量:放入 .rodata 并以 .LC* 标签管理,通过地址传参;

条件与循环控制:通过 cmp + jcc 与标签 .L* 组合实现 if/for;

函数调用:按ABI进行寄存器传参,变参调用体现为 movl $0,%eax,并出现 glibc 的安全强化入口 __printf_chk。

以上分析表明,编译器在 .i→.s 阶段已经把C语言的主要数据类型、控制结构与库调用机制映射成了明确的寄存器操作、内存寻址与控制流跳转,为后续汇编生成目标文件及链接装载奠定了基础。

3.4 本章小结

本章说明了编译阶段的作用,并通过 hello.s 验证了字符串常量的放置、x86-64 调用约定与循环控制流的汇编实现。编译阶段把高级语言语义落到具体指令序列,为后续汇编生成目标文件做好准备。

第4章 汇编

4.1 汇编的概念与作用

本章所说“汇编”特指 从汇编文件 .s 到可重定位目标文件 .o 的过程。汇编器(assembler, as)将人类可读的汇编指令与伪指令翻译为机器指令字节序列,并生成符合 ELF 规范的目标文件(Relocatable ELF)。与编译阶段相比,汇编阶段的核心任务不再是语义/类型转换,而是完成如下工作:

1)指令编码:将如 mov/cmp/jcc/call 等汇编指令编码为对应的机器码字节;

2)生成节(Section):把代码、只读数据、符号表、重定位表等组织进 .text/.rodata/.data/.bss/.symtab/.strtab/.rela.* 等节;

3)符号与重定位信息:当汇编中引用了外部符号(如 __printf_chk/sleep/strtol/getchar/exit 等)或需要地址回填的位置时,汇编器不会直接给出最终地址,而是生成重定位项(relocation entries),留给链接器在链接阶段完成最终地址绑定。

因此,.o 文件是“机器语言 + 必要的链接元数据”的组合,是链接阶段的直接输入。

4.2 在Ubuntu下汇编的命令

在实验目录下,将 hello.s 汇编生成目标文件 hello.o:

cd /home/sbliu/ICS-hello

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

其中:-c 表示“只汇编/编译生成目标文件,不链接”;输出为可重定位 ELF hello.o。

汇编完成后验证生成结果:

ls -lh hello.s hello.o

file hello.o

4.3 可重定位目标elf格式

4.3.1 ELF 头与文件类型

readelf -h hello.o 显示 hello.o 的类型为 REL(Relocatable file),说明它是可重定位目标文件;同时可观察到目标架构为 x86-64,端序为 little-endian,入口地址通常为 0(因未链接、不可直接执行)。

4.3.2 节(Sections)概览

readelf -S hello.o 会列出 .text、.rodata、.data/.bss(可能为空或很小)、以及与链接相关的 .symtab/.strtab、重定位节 .rela.text(以及可能的 .rela.rodata 等)等。
其中:

.text:机器指令所在节;

.rodata:只读数据(例如 .LC0/.LC1 字符串)所在节;

.symtab/.strtab:符号表与字符串表,记录 main 等符号以及外部符号引用;

.rela.text:对 .text 节中需要链接器回填的地址位置给出重定位项(如 call 目标函数地址、引用 .rodata 地址等)。

4.3.3 重定位表(Relocation Entries)分析(本节重点)

在可重定位目标文件中,涉及外部函数调用或地址引用时,汇编器无法确定最终地址,因此在 .rela.text 等重定位节中记录“需要修补的位置、重定位类型、关联符号与加数”。例如:

call __printf_chk 这类外部调用,在 .text 的 call 指令操作数位置会产生重定位项,链接时由链接器把相对位移/PLT入口地址填入;

访问 .LC1 等只读数据地址,也会产生与 .rodata 相关的重定位项,链接时决定最终地址或相对偏移方式。

通过 readelf -r hello.o 可列出每条重定位项的:offset、info、type、symbol、addend 等信息。重定位项的存在表明:.o 中的机器码并非完全“定址完成”,而是为链接阶段保留了地址绑定的接口。

4.4 Hello.o的结果解析

4.4.1 机器指令与汇编指令的一一对应

objdump -d 的输出通常呈现格式:

地址: 机器码字节  汇编指令

例如(示意):

汇编中的 cmpl $5, %edi 会对应一串固定的机器码字节序列;

jne .L6 会被编码为条件跳转指令,其机器码中包含“相对位移”字段;

pushq %rbx、subq $8,%rsp 等栈操作都能在机器码中看到对应 opcode 与寄存器编码。

这说明汇编语言是机器语言的符号化表示:每条汇编指令最终都会被编码为若干字节的机器码。

4.4.2 操作数差异的来源:地址尚未确定(分支/调用尤其明显)

与第3章 hello.s 对照时,会发现 hello.o 反汇编中某些指令操作数与 .s 表面不完全一致,尤其是:
1)分支跳转(jcc/jmp):

在 .s 中跳转目标写成标签(如 .L2/.L6/.L7);

在 .o 的反汇编中变成“目标地址/相对位移”的形式。
因为汇编器在本文件范围内可以给标签分配偏移,所以跳转一般已能编码成相对位移。

2)外部函数调用(call):

.s 中写 call __printf_chk(符号名);

.o 中 call 的操作数字段往往显示为 0 或某个占位形式,并在旁边标出 R_X86_64_PC32 / R_X86_64_PLT32 等重定位信息(具体类型以你的输出为准)。
原因是外部符号的最终地址需要链接时确定,因此 .o 中必须通过重定位项告诉链接器“这里需要回填”。

3)*只读数据地址引用(.LC)**:

.s 里可见 $.LC1 之类;

.o 中对应位置也可能出现重定位(对 .rodata 节内符号的引用),由链接器决定最终地址/相对方式。

因此,在 objdump -d -r hello.o 中,“指令字节 + relocation 标注”共同决定了最终可执行代码,这是可重定位目标文件的关键特征。

4.4.3 结合本程序的典型对应点(建议你在报告里点名)

根据你第3章的 hello.s 片段,可在 hello.o 的反汇编中重点对照以下逻辑片段:

cmpl $5, %edi + jne ...:对应 argc != 5 的分支;

循环头 .L2 附近 cmpl $9, %ebp + jg ...:对应 for(i=0; i<10; i++) 的退出判断;

call __printf_chk、以及后续 sleep/strtol/getchar/exit 等 call:在 .o 中通常会伴随重定位条目,是“链接前地址未定”的直接证据;

对 .LC0/.LC1 的引用:体现 .rodata 与 .text 的跨节引用与重定位需求。

4.5 本章小结

本章完成了从 hello.s 到 hello.o 的汇编过程,生成了可重定位 ELF 目标文件。通过 readelf 分析了 hello.o 的节表、符号表以及重定位表,明确了可重定位文件需要在链接阶段完成地址绑定的原因;进一步利用 objdump -d -r 对目标文件反汇编并与第3章 hello.s 对照,验证了汇编指令到机器码字节序列的映射关系,并重点解释了分支跳转与外部函数调用等指令在 .o 中出现操作数占位与重定位标注的现象。上述结论为下一章链接生成可执行文件、分析装载与动态链接机制奠定了基础。

5章 链接

5.1 链接的概念与作用

链接(Linking)是将一个或多个可重定位目标文件(如 hello.o)与所需的运行库/启动代码组合起来,生成最终可执行文件(如 hello)的过程。本章所说链接特指 从 hello.o 到可执行文件 hello 的生成。

链接器(ld)的核心工作包括:
1)符号解析(Symbol Resolution):将 hello.o 中对外部符号(如 __printf_chk、sleep、strtol/atoi、getchar/getc、exit 等)的引用,解析到库或运行时提供的具体定义;
2)节合并与布局(Section Merge & Layout):将各输入文件的 .text/.rodata/.data/.bss 等节进行合并、排布,形成可执行文件的段(Program Headers);
3)重定位(Relocation):把 hello.o 中未定址的地址字段回填到最终机器码中(特别是外部函数调用与跨节引用);
4)生成入口与装载信息:生成可执行文件的入口 _start,并写入 .interp/.dynamic 等段信息,使得内核装载器与动态链接器能够正确加载并运行程序。

5.2 在Ubuntu下链接的命令

本实验使用 ld 直接完成链接。为保证可执行文件能正常启动,链接时必须包含 *启动文件(crt.o)**、C标准库(libc)、libgcc 等,而不仅仅是 hello.o。

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

5.3.1 ELF 头信息(readelf -h)

由 readelf -h hello 可知(见图5-3):

文件类型为 EXEC(Executable file),说明 hello 为可直接执行的目标文件;

体系结构为 x86-64,采用 little endian;

程序入口地址(Entry point address)为 0x4010f0,程序装载后将从该入口开始执行(在运行时通常对应 _start 附近的启动代码);

程序头表(Program Headers)起始偏移为 64 字节,数量为 13,表项大小为 56 字节;

节头表(Section Headers)也在文件中给出,为后续节级别分析提供依据。

上述信息表明 hello 已完成链接,具备入口点与装载所需的程序头描述。

5.3.2 程序头与段信息(readelf -l,重点)

readelf -l hello 给出了程序头表(Program Headers),该表描述了装载器需要映射到内存的“段”(见图5-4)。其中关键段类型与作用如下:

1)INTERP 段(解释器/动态链接器)
在 INTERP 段中可以看到解释器为:
/lib64/ld-linux-x86-64.so.2
这说明 hello 是一个需要动态链接器参与装载与重定位的动态链接程序(动态链接器负责加载共享库并完成运行时重定位)。

2)LOAD 段(真正映射进虚拟地址空间的装载段)
程序头中出现多个 LOAD 段,每个 LOAD 段给出了其 VirtAddr(虚拟地址起点)、FileSiz(文件中大小)、MemSiz(装载到内存后的大小) 以及 Flags(权限,如 R/E/W)。通常可将它们概括为:

代码段(R-E):包含 .text/.plt 等,可读可执行;

只读数据段(R--):包含 .rodata、.eh_frame 等;

可写数据段(RW-):包含 .data/.bss/.got/.dynamic 等,运行时可修改。

这些 LOAD 段的 VirtAddr/Size/Flags 决定了程序运行时的基本内存布局,为后续 5.4 的“虚拟地址空间映射”对照分析提供依据。

3)DYNAMIC / GNU_STACK / GNU_RELRO 等辅助段

DYNAMIC 段:携带动态链接信息(依赖库、重定位表位置、符号表位置等)。

GNU_STACK 段:描述栈的权限,通常为不可执行(提升安全性)。

GNU_RELRO 段:用于“重定位后只读”保护(RELRO),与 GOT 等区域安全相关。
此外还可能包含 NOTE、GNU_EH_FRAME、GNU_PROPERTY 等段,用于构建ID、异常处理、属性标记等辅助信息。

4)段到节的映射(Section to Segment mapping)
readelf -l 的末尾还给出了 “Section to Segment mapping”,可看到 .interp、.plt/.text、.rodata、.got/.got.plt/.data/.bss、.dynamic、.rela.dyn/.rela.plt 等节分别落入不同的段中。这一映射说明:节(section)是链接视角的组织单位,而段(segment)是装载视角的映射单位,链接器通过把多个节组合进少量段,形成操作系统装载所需的最小映射集合。

5.3.3 节表信息(readelf -S)

通过 readelf -S hello 可进一步观察链接生成的节(见图5-5)。其中与后续章节关系密切的节包括:

.text:程序机器指令主体;

.rodata:只读数据(如字符串常量);

.plt/.plt.sec:过程链接表(PLT),用于外部函数的间接调用;

.got/.got.plt:全局偏移表(GOT),用于动态链接重定位与函数地址解析;

.dynamic:动态段内容(动态链接器使用);

.rela.dyn、.rela.plt:动态重定位信息(运行时重定位/绑定使用);

.dynsym/.dynstr:动态符号表与字符串表。

节表层面的信息为后续 5.5(重定位过程分析)与 5.7(动态链接分析,GOT/PLT变化)提供了结构依据。

综上,hello 的 ELF 头给出基本可执行属性与入口点;程序头描述了装载段的虚拟地址、大小与权限并明确动态链接器;节表进一步揭示了 .plt/.got/.rela.* 等动态链接相关结构,完整刻画了 hello 作为动态链接可执行文件的文件组织方式。

5.4 hello的虚拟地址空间

可执行文件被加载运行后,内核会为该进程建立虚拟地址空间,并将 readelf -l 中描述的各个 LOAD 段映射到相应虚拟地址区间。使用 gdb 运行并查看映射信息:  

解释:

starti:从程序入口处(_start)开始执行/停住,保证进程已装载;

info files:显示可执行文件各节/段的装载位置摘要;

info proc mappings:显示当前进程虚拟内存映射(类似 /proc/<pid>/maps)。

对照分析要点:
1)info proc mappings 中与 hello 相关的映射区间(通常带有可执行 r-xp、只读 r--p、可写 rw-p)应与5.3中各 LOAD 段的 VirtAddr 范围一致;
2)动态链接库(如 libc.so.6、动态链接器 ld-linux)会以额外映射区间出现,这对应动态链接程序的运行时依赖;
3)栈([stack])、堆([heap])、vdso/vvar 等系统区域也会出现在 mappings 中,构成完整虚拟地址空间。

5.5 链接的重定位过程分析

1)外部函数调用从“占位 + relocation”变为“确定的调用入口”

在 hello.o 中,call __printf_chk 等外部调用的目标地址无法确定,因此指令操作数字段常为占位值,同时伴随 R_X86_64_* 重定位条目;

在 hello 中,链接器已为外部函数建立 PLT(Procedure Linkage Table) 入口(如 __printf_chk@plt),并将机器码中的调用目标改写为指向 PLT 的相对位移;对应的动态重定位信息则转移到 .rela.plt/.rela.dyn 等区域,以供运行时动态链接解析。

2)对 .LC* 这类只读数据引用被链接器定址

hello.o 中对 .LC0/.LC1 的引用可能也伴随重定位;

链接后 hello 的 .rodata 被安排到最终地址区间,相关引用在机器码中被正确回填(或采用相对寻址形式实现)。

3)重定位阶段完成“从符号到地址”的绑定
链接器综合符号表、节/段布局、重定位表,把 .o 中的“符号引用”转化为“可执行文件中的最终地址/PLT入口”,从而使程序能够正确运行。

5.6 hello的执行流程

starti 后程序首先从入口 _start 开始执行(来自启动文件 crt1.o);

_start 会准备参数并调用运行库初始化入口(常见为 __libc_start_main),它负责初始化运行环境并最终调用用户的 main;

main 返回后,会进入 exit 或等价的终止路径,执行库清理操作,最终由内核结束进程并回收资源。

5.7 Hello的动态链接分析

hello 为动态链接程序,其外部函数(如 __printf_chk、sleep、strtol 等)的真实地址在运行时由动态链接器解析。静态分析可通过以下命令查看动态链接相关结构:

readelf -d hello

readelf -l hello | grep -E "INTERP|DYNAMIC"

readelf -r hello

objdump -d -j .plt -j .plt.got hello

动态链接的关键机制为 PLT + GOT:

程序对外部函数的调用通常先跳到 xxx@plt;

xxx@plt 再通过 GOT 表项间接跳转:首次调用时 GOT 表项指向解析器(resolver),解析完成后 GOT 表项被改写为真实函数地址;

因而“动态链接前后”的差异可通过观察 GOT 表项内容变化体现出来。

5.8 本章小结

本章完成了从 hello.o 到可执行文件 hello 的链接过程,使用 ld 命令引入启动文件与运行库生成可执行产物,并通过 readelf 分析了可执行ELF的程序头与各段的起始地址、大小与权限。随后在 gdb 中观察进程虚拟地址空间映射并与 readelf -l 对照,验证了 LOAD 段到虚拟内存区域的映射关系。通过对比 hello.o 与 hello 的反汇编与重定位信息,说明链接器完成符号解析与地址回填,使外部调用转化为对 xxx@plt 的调用;进一步结合动态段、重定位表与 GOT/PLT 机制分析了动态链接的运行时解析过程,并通过调试观察到首次解析前后 GOT 表项内容变化。以上分析完整揭示了 hello 程序从目标文件到可执行文件再到运行时动态链接的关键机制。

第6章 hello进程管理

6.1 进程的概念与作用

进程(Process)是程序在运行时的一个实例,是操作系统进行资源分配与调度的基本单位。程序(Program)是静态的可执行文件(如 hello),而进程是动态的运行实体,具有独立的地址空间、寄存器上下文、打开文件表、信号处理方式等运行状态信息。每个进程由内核用进程控制块(PCB/task_struct)描述,包含 PID、状态(运行/就绪/阻塞/停止等)、优先级、寄存器现场、页表指针、文件描述符表等。

进程的作用主要体现在:
1)隔离与保护:不同进程拥有各自虚拟地址空间,互不干扰;
2)并发与共享:多个进程可并发执行,通过内核机制共享文件、管道、信号等资源;
3)调度单位:内核按时间片调度 CPU,在多个进程间切换,实现多任务。

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

Shell(如 bash)是用户与操作系统之间的命令解释器与“作业控制器”。用户在终端输入命令后,bash 负责解析并启动相应程序,同时管理前台/后台作业及信号转发。

bash 典型处理流程如下:
1)读取命令行:从标准输入读取用户输入的命令字符串;
2)词法/语法解析:分割命令与参数,处理引号、转义、管道、重定向等;
3)查找可执行文件:若为外部命令,按 PATH 查找对应可执行文件(如 ./hello);
4)创建子进程(fork/clone):bash 创建子进程,让子进程去执行外部程序;
5)在子进程中 execve 装载程序:用 execve() 将子进程的地址空间替换为目标程序(hello)的代码与数据;
6)父进程等待或作业控制:

前台作业:bash wait 等待子进程结束;

后台作业:bash 记录 job 信息,通过 jobs/fg/bg/kill 等管理;
7)信号/终端控制:bash 维护前台进程组,终端驱动产生 Ctrl-C、Ctrl-Z 等信号时会发送给前台进程组,bash 负责作业状态更新与提示。

6.3 Hello的fork进程创建过程

hello 本身的 C 源码不调用 fork(),但当我们在 bash 中执行 ./hello ... 时,bash 会先 fork(Linux 上常体现为 clone)创建子进程,子进程再通过 execve() 变身为 hello 进程。因此,“Hello 的 fork 创建过程”本质上是:bash → fork/clone → 子进程(即将 execve hello)。

fork/clone 的关键语义:

子进程获得与父进程(bash)几乎相同的运行上下文(寄存器、打开文件、工作目录、环境变量等);

地址空间通常采用 写时复制(Copy-on-Write):fork 后父子进程共享同一物理页,直到某一方写入才复制;

父进程得到子进程 PID,并据此进行 wait/waitpid 或作业控制管理。

6.4 Hello的execve过程

在 bash fork/clone 出子进程后,子进程会调用 execve("./hello", argv, envp) 装载并执行 hello。execve 的核心作用是:用新程序的代码/数据段替换当前进程的用户态地址空间,但 PID 不变(仍是同一个进程,只是“换了程序”)。

execve 在内核中主要完成:
1)检查可执行文件格式(ELF),读取 ELF 头与 Program Headers;
2)根据 LOAD 段把 .text/.rodata/.data/.bss 等映射到进程虚拟地址空间;
3)建立用户栈,压入 argc/argv/envp 等启动参数;
4)若为动态链接程序,先装载解释器(如 /lib64/ld-linux-x86-64.so.2),由其完成共享库加载与重定位;
5)设置入口地址,返回用户态从入口处开始执行(最终进入 _start,再到 main)。

6.5 Hello的进程执行

hello 的主要执行逻辑为循环打印与 sleep:每次循环调用 printf 输出字符串,然后调用 sleep(atoi(argv[4])) 进入休眠,循环 10 次后调用 getchar() 等待键盘输入并结束。该过程涉及多次用户态/内核态切换与调度行为:

1)用户态执行阶段
循环控制、atoi 参数转换等在用户态执行。

2)系统调用与陷阱(trap)进入内核态

printf 最终会触发 write 系统调用,把数据写到终端(TTY);

sleep 会触发与定时器相关的系统调用(如 nanosleep),使进程进入可中断睡眠;

getchar 最终会触发 read 系统调用,从终端读取输入。
这些系统调用会通过 syscall 指令触发陷阱进入内核态,内核完成服务后再返回用户态。

3)阻塞与调度(时间片/上下文切换)

当进程调用 sleep 后,内核将其状态置为睡眠(阻塞),从运行队列移出,调度器选择其他就绪进程运行;

定时器到期时(时钟中断),内核将 hello 置为就绪,等待被再次调度;

调度切换时,内核保存当前进程上下文(寄存器、程序计数器、栈指针等)到 PCB,并恢复下一个进程上下文,实现 CPU 在不同进程间切换。

因此,hello 的“打印→休眠→被唤醒→继续运行”循环体现了典型的:用户态计算 + 系统调用陷入内核 + 阻塞/唤醒 + 调度切换的过程。

6.6 hello的异常与信号处理

hello 运行过程中会涉及多类异常/中断与信号机制。首先,printf/sleep/getchar 等库函数最终会触发系统调用(如 write/nanosleep/read),属于陷阱(trap)类异常:进程通过 syscall 指令从用户态进入内核态请求服务,内核处理后返回用户态继续执行。此外,程序运行期间还会受到时钟中断的影响(用于时间片调度与 sleep 的唤醒),以及键盘中断(输入、作业控制按键等)。正常情况下该程序不会触发非法指令等 abort 类异常。

在终端前台运行 hello 时,终端驱动会把特定控制按键转换为信号发送给前台进程组,bash 负责作业控制与状态维护。实验中对 hello 的信号与处理过程如下:

1)Ctrl-Z(SIGTSTP,停止信号)
hello 在前台运行时按下 Ctrl-Z,终端显示作业被停止(Stopped),表明前台作业收到了 SIGTSTP 并进入停止态。随后使用 jobs -l 查看作业表,可见当前 shell 中存在多个 stopped 作业,其中本次选择作业 [5](PID=266457)进行后续分析。

2)ps 查看停止态进程上下文信息
对 PID=266457 执行
ps -o pid,ppid,pgid,sid,stat,tty,cmd -p 266457,
结果显示:PID=266457,父进程 PPID=265737(bash),进程组 PGID=266457,会话 SID=265737,控制终端为 pts/1,STAT=T 表示处于停止态(stopped)。

3)pstree 观察父子进程与作业关系
执行 pstree -ap 265737 可见 bash(265737) 派生出多个 hello 进程(以及 strace 相关进程),表明 hello 作为 bash 创建并管理的子进程,处于同一会话下,符合“bash fork/exec 启动外部程序并进行作业控制”的机制。

4)fg(SIGCONT,继续信号)
执行 fg %5 将作业 [5] 恢复到前台继续运行;恢复后 hello 再次输出并可继续响应终端控制。若再次按 Ctrl-Z,则作业再次进入 stopped 状态。

5)kill 发送终止信号(SIGTERM/SIGKILL)
首先执行 kill -TERM 266457 向进程发送 SIGTERM(默认动作为终止),随后通过 jobs -l 观察作业状态。为确保终止成功,再执行 kill -KILL 266457 发送 SIGKILL,终端返回 Killed,再次 jobs -l 可见作业 [5] 已从作业表中消失,说明进程被强制终止。

综上,通过 Ctrl-Z、fg 与 kill 等实验,验证了前台作业的信号投递与处理机制:Ctrl-Z 使进程停止、fg 发送继续信号使其恢复、kill 可发送不同信号以终止进程;bash 通过 jobs/作业表维护进程状态并实现作业控制。

6.7本章小结

本章从操作系统视角分析了 hello 的进程管理过程:首先阐明进程作为资源分配与调度的基本单位,与静态程序的区别;随后说明 bash 的命令解析与作业控制流程,并指出运行 hello 时实际经历了 “bash fork/clone 创建子进程 → 子进程 execve 装载 hello → 进入 _start/main 执行” 的典型路径。hello 在执行过程中通过 printf/sleep/getchar 引发系统调用,从用户态陷入内核态并伴随阻塞、唤醒与调度切换,体现了时间片与上下文切换机制。最后通过 Ctrl-Z/Ctrl-C 及 ps/jobs/pstree/fg/kill 等命令实验,验证了前台进程组信号投递、作业停止/恢复以及不同信号终止策略,完成对异常与信号处理的分析。

7章 hello的存储管理

7.1 hello的存储器地址空间

从程序运行视角,hello 进程看到的是一个连续的虚拟地址空间(Virtual Address Space, VA)。在 Linux x86-64 下,进程的典型地址空间布局包括:代码段 .text、只读数据段 .rodata、已初始化数据段 .data、未初始化数据段 .bss、堆(heap)、内存映射区(mmap,包含共享库、动态链接器等)、以及用户栈(stack),并在高地址附近还有 vvar/vdso 等内核辅助映射。

结合地址概念可区分:

逻辑地址(Logical Address):CPU 指令中出现的“段选择子 + 段内偏移”的形式;

线性地址(Linear Address):逻辑地址经过分段机制转换后的地址(本质是“段基址 + 偏移”结果);

虚拟地址(Virtual Address):在现代 x86-64 Linux 中,通常把线性地址就当作虚拟地址使用(分段大多为平坦模型,逻辑≈线性≈虚拟);

物理地址(Physical Address, PA):最终访问 DRAM 的真实地址,需要通过页表把 VA 翻译为 PA。

因此,hello 在用户态看到的指针值(如函数地址、全局变量地址、栈变量地址)都是 VA;CPU 取指/访存时会先把 VA 经由 TLB/页表翻译成 PA,再通过 Cache/内存层次结构完成真实访问。

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

在 Intel 体系结构中,逻辑地址由 段选择子(Selector)+ 段内偏移(Offset) 构成。段选择子用于在 GDT/LDT(全局/局部描述符表)中定位段描述符,段描述符包含 段基址(Base)、段界限(Limit) 与权限属性。CPU 进行分段变换时:
1)用 Selector 找到段描述符;
2)取出 Base;
3)计算 Linear = Base + Offset(并进行界限/权限检查)。

在 x86-64 的 64 位模式下,Linux 通常采用平坦分段模型:除 FS/GS(常用于线程局部存储 TLS)外,大多数段寄存器的 Base 视为 0,Limit 基本不再限制普通地址空间。于是对一般用户程序而言,逻辑地址 ≈ 线性地址,分段主要用于权限级别(CPL)与少量特殊用途(TLS)。

本实验的 hello 属于典型用户态程序,段式管理在地址转换上基本表现为“平坦段”,真正决定映射关系的是页式管理。

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

页式管理负责将线性/虚拟地址(VA)翻译为物理地址(PA)。Linux 为每个进程维护一套页表(由 CR3 指向其顶级页表),页表项(PTE)记录该 VA 所在虚拟页对应的**物理页框号(PFN)**以及权限位(Present、R/W、U/S、NX 等)。

在 4KB 页大小下,VA 可分为:

页内偏移(Offset):低 12 位;

虚拟页号(VPN):高位部分,用于逐级索引页表得到 PFN。

翻译结果为:

PA = (PFN << 12) | Offset。

如果访问的 VA 在页表中 PTE 不存在或权限不满足,会触发缺页/保护异常,由内核进行处理。

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

在 x86-64(48 位规范虚拟地址、4KB 页)下,硬件通常使用四级页表完成地址翻译,VA 的结构可表示为:

9 位:PML4 索引

9 位:PDPT 索引

9 位:PD 索引

9 位:PT 索引

12 位:页内偏移

CPU 先查 TLB(Translation Lookaside Buffer),TLB 缓存最近使用的 VA→PA(或 VA→PFN)映射:

TLB 命中:直接得到 PFN,拼接 offset 得到 PA,速度快;

TLB 未命中:硬件页表遍历(page walk)按四级索引逐级访问页表项,找到最终 PTE,填充 TLB 后继续执行。

页表遍历本身也要访问内存(页表页),因此 TLB 对性能至关重要。为减少遍历次数,硬件/内核还支持大页(2MB/1GB),可以减少页表层级与 TLB 压力。

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

获得物理地址 PA 后,CPU 并不一定直接访问 DRAM,而是通过 Cache 层次结构利用局部性提升速度。典型三级 Cache 为:

L1 Cache(分指令/数据,容量小、速度最快)

L2 Cache(容量更大,速度次之)

L3/LLC(Last Level Cache,多核共享,容量最大但更慢)

访问流程为:

1)用地址(最终对应到物理地址)在 L1 中查找 cache line(典型 64B 一行);

2)L1 miss → 去 L2;L2 miss → 去 L3;

3)若 LLC miss → 访问主存 DRAM,将数据回填到 cache 层次后再供 CPU 使用。

写入策略常见为写回(write-back)+ 写分配(write-allocate),并配合一致性协议保证多核共享数据的正确性。对 hello 而言,循环打印、库函数调用会频繁访问指令与只读数据,cache 命中率通常较高;而首次执行时共享库与代码页的按需调入会带来更多 miss/缺页。

7.6 hello进程fork时的内存映射

在 Linux 中,fork(更准确说是 clone/fork 语义)创建子进程时,子进程初始时拥有与父进程几乎相同的虚拟地址空间布局:代码段、数据段、共享库映射、栈、堆等映射关系在虚拟地址层面基本一致。

为了避免立即复制整片内存,内核使用 写时复制(Copy-on-Write, COW) 优化:

fork 后父子进程的页表指向同一份物理页框(共享物理页);

相应 PTE 会被标记为只读;

当父或子尝试写入共享页时触发缺页异常,内核再复制该页并更新页表,使写入发生在私有副本上。

因此,fork 的“内存映射变化”主要表现为:页表结构被复制,但物理页框延迟到写入时才复制,从而降低进程创建开销。

7.7 hello进程execve时的内存映射

execve 会把“当前进程”变成“新程序”:在 PID 不变的情况下,用新程序的地址空间替换旧地址空间。具体表现为:
1)原有的用户态映射(旧的 .text/.data/heap/stack 等)大多被解除映射(unmap);
2)根据新 ELF 的 Program Headers 重新建立映射:映射新程序的代码/数据段;
3)若为动态链接程序,则映射动态链接器与共享库(如 libc、ld-linux);
4)重建用户栈,把 argv/envp/auxv 等压入栈;
5)设置入口点,从 _start 开始执行。

因此,execve 前后,进程的虚拟地址空间布局会发生“整体替换”。对 bash 执行 hello 而言,即:bash 子进程 fork 后调用 execve,把自己替换为 hello。

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

当进程访问的 VA 在页表中不存在(Present=0)或权限不满足时,会触发异常进入内核。常见触发原因包括:

按需分页(demand paging):首次访问代码页/共享库页尚未装入;

写时复制(COW):fork 后对共享只读页写入;

栈增长:访问到新的栈页;

mmap 文件映射页首次访问等。

内核处理缺页故障的一般步骤:
1)CPU 产生 page fault,保存现场,进入内核;
2)内核根据 fault address 查找对应 VMA(虚拟内存区域),检查访问是否合法;
3)若合法但页不存在:为该页分配物理页框或从磁盘读入;建立/更新 PTE;
4)刷新(必要时)TLB,恢复现场,回到用户态重新执行导致缺页的指令。

缺页可分为:

minor fault(轻微缺页):不需要读盘,如 COW/页已在页缓存;

major fault(严重缺页):需要从磁盘装入页。

7.9动态存储分配管理

动态内存分配由 malloc/free 等接口完成,目标是在运行时为对象分配可变大小的内存块并支持回收复用。典型实现策略包括:

堆管理:通过 brk/sbrk 扩展进程堆顶;大块分配也可能使用 mmap 直接映射;

空闲块组织:维护空闲链表或分离空闲链表(segregated lists),按大小类别管理;

块分割与合并:分配时对大空闲块 split;释放时与相邻空闲块 coalesce,减少外部碎片;

边界标记(boundary tags):在块头/块尾记录大小与状态以支持快速合并;

性能与碎片权衡:首次适配/最佳适配/分离适配等策略在吞吐与碎片率之间权衡。

glibc 常用 ptmalloc(基于多 bin + arena),在多线程下用多个 arena 降低锁竞争。

7.10本章小结

本章围绕 hello 的存储管理机制展开:首先结合 hello 的运行形态说明了进程虚拟地址空间的典型组成,并区分了逻辑地址、线性地址、虚拟地址与物理地址的关系;随后从体系结构角度说明了 x86-64 下分段机制向平坦模型演化的背景,并重点分析页式管理如何在 TLB 与四级页表支持下完成 VA→PA 转换;在完成地址翻译后,CPU 通过三级 Cache 与主存层次结构完成数据访问,从而体现局部性与缓存加速。进一步讨论 fork 时写时复制带来的共享与延迟复制,以及 execve 导致的地址空间整体替换;最后结合缺页故障的触发原因与内核处理流程,说明按需分页与 COW 等机制如何共同支撑 hello 的高效执行。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux/Unix 将外设统一抽象为文件(file),对用户程序提供一致的访问接口,体现“一切皆文件”的设计思想。用户进程通过文件描述符(file descriptor, fd)访问设备或普通文件:如终端(TTY)、磁盘文件、管道、套接字等,都可使用同一组系统调用进行读写。

Linux I/O 管理的核心机制包括:
1)设备模型化:文件抽象

进程打开设备文件(如 /dev/tty)得到 fd;

fd 在进程的文件描述符表中索引到内核的 file 对象;

file 对象再指向具体的 inode/设备驱动操作集合,实现多态。

2)设备管理:Unix I/O 接口

通过 open/close/read/write/lseek 等系统调用完成基本 I/O;

通过 ioctl 实现设备特有控制;

内核通过 VFS(虚拟文件系统)屏蔽不同文件系统/设备差异;

通过缓冲与缓存(buffer/page cache)优化磁盘与文件 I/O 性能。

对 hello 程序而言,标准输出 stdout 对应 fd=1,通常连接到终端设备(TTY),printf 最终会把数据写到 fd=1;而 getchar 从标准输入 stdin(fd=0)读取键盘输入,最终对应到终端驱动的输入缓冲。

8.2 简述Unix IO接口及其函数

Unix I/O 接口以文件描述符 fd为中心,主要函数如下:

int open(const char *path, int flags, ...):打开文件/设备,返回 fd。

int close(int fd):关闭 fd。

ssize_t read(int fd, void *buf, size_t count):从 fd 读取最多 count 字节到 buf,返回实际读取字节数(0 表示 EOF)。

ssize_t write(int fd, const void *buf, size_t count):向 fd 写入 count 字节,返回实际写入字节数。

off_t lseek(int fd, off_t offset, int whence):调整文件偏移(对终端、管道等不可 seek 的对象可能失败)。

这些函数属于系统调用接口:用户态通过 libc 封装函数触发陷阱进入内核态(x86-64 通常用 syscall 指令),内核完成 I/O 后返回用户态。

此外,标准 I/O(stdio,如 printf/getchar)是 libc 在 Unix I/O 之上的更高层封装,提供格式化输出、缓冲等能力。标准 I/O 可能在内部调用 read/write 完成实际数据传输。

8.3 printf的实现分析

hello 程序在循环中调用 printf 输出字符串。printf 属于 C 标准库的格式化输出函数,其典型实现路径可以概括为:

1)格式化阶段(用户态 libc 内部)
printf(format, ...) 会先把可变参数按格式串解析并格式化,内部常通过 vprintf/vfprintf/vsprintf/vsnprintf 等机制,将输出内容组织成一段字符序列(缓冲区)。

2)缓冲与刷新(stdio 缓冲机制)
stdout 通常是行缓冲(连接终端时遇到 \n 会刷新),也可能是全缓冲/无缓冲(视输出目标而定)。当缓冲区需要刷新时,stdio 会把缓冲区中的字节交给底层 Unix I/O。

3)写入阶段:调用 write 系统调用(进入内核态)
stdio 最终会调用 write(1, buf, len) 把字节写入 fd=1(标准输出)。这一步触发从用户态进入内核态的系统调用陷阱(x86-64 通过 syscall 指令;早期 32 位可用 int 0x80)。

4)终端/显示设备输出路径(内核态与硬件)
当 stdout 指向终端时,内核把输出交给 TTY 子系统与具体驱动:字符数据经过终端驱动处理后显示到屏幕。更底层角度,可理解为字符最终转换为显示像素:字符编码(ASCII/UTF-8)经过字体点阵(字模)映射到像素点,写入显存/帧缓冲(VRAM 或图形缓冲),显示控制器按刷新频率逐行读取并通过信号输出到显示器完成显示。

一句话总结:printf 负责“格式化+缓冲”,真正把数据送到设备的是 write 系统调用及其后续的终端/显示驱动链路。

8.4 getchar的实现分析

hello 在输出若干次后会调用 getchar() 等待用户输入。getchar 属于 stdio 输入函数,其实现要点是:从 stdin(fd=0)读取字符,并受终端行缓冲与键盘中断机制影响。

1)键盘输入与中断(异步事件)
用户按键时,键盘控制器产生中断请求,CPU 进入内核的键盘中断处理程序。中断处理程序读取键盘扫描码,将其转换为键值/字符(结合键盘布局、Shift/Ctrl 等状态),并放入内核维护的输入缓冲(TTY 缓冲区)。

2)行缓冲与回车机制
在典型“规范模式”(canonical mode)下,终端输入是行缓冲:用户输入的字符先在内核缓冲中积累,直到按下回车(\n)才认为一行输入完成,用户进程的读取才会返回。因此 getchar 可能会阻塞,直到输入缓冲中有可读字符(通常是回车后)。

3)read 系统调用(进程阻塞/唤醒)
getchar() 内部通常会通过 read(0, &c, 1)(或更大块读取再逐字符返回)从 stdin 读取。当缓冲区无数据时,进程进入阻塞态;当键盘中断把字符送入缓冲后,内核唤醒阻塞进程,read 返回后 getchar 返回一个字符给用户程序。

8.5本章小结

本章从 Linux I/O 管理角度分析了 hello 的输入输出过程。Linux 将设备统一抽象为文件,通过 Unix I/O 接口(open/read/write/close 等)实现一致的访问模型;标准 I/O(stdio)在 Unix I/O 之上提供格式化与缓冲机制。对 hello 而言,printf 先在用户态完成格式化与缓冲,最终通过 write 系统调用把字节写入标准输出并由终端/显示驱动完成显示;getchar 则通过 read 从标准输入读取字符,在终端行缓冲与键盘中断机制作用下可能阻塞,直到用户输入并回车后才返回。通过这些分析可以看到,用户态库函数与内核 I/O 子系统之间通过系统调用与中断机制协作完成了完整的 I/O 流程。

结论

1)hello 所经历的过程:

  1. 源代码与构建输入
    hello 以 hello.c 的形式存在于文件系统中,属于静态的 Program。构建过程中还会依赖系统头文件、链接启动文件(crt*.o)、C 标准库(libc)与编译器运行库(libgcc)等外部组件,它们共同决定最终可执行文件的语义与运行环境。
  2. 预处理(Preprocessing:.c → .i)
    预处理器展开 #include、宏定义与条件编译,将源文件与头文件内容合并为纯 C 文本 hello.i。这一阶段使得编译器看到的是“展开后的统一翻译单元”,因此 .i 相比 .c 大幅膨胀,且包含大量来自系统头文件与编译器内置头的声明。
  3. 编译(Compilation:.i → .s)
    编译器对 hello.i 完成词法/语法/语义分析、类型检查、优化与指令选择,并生成汇编 hello.s。在该阶段,高级语言中的变量、表达式、控制流与函数调用被翻译为符合 x86-64 ABI 的指令序列:

常量字符串进入只读段(如 .rodata),

函数建立栈帧并使用寄存器/栈传参与返回值传递,

对外部函数(如 printf/sleep/strtol 等)形成符号引用,为后续链接/动态解析做准备。

  1. 汇编(Assembly:.s → .o)
    汇编器将 hello.s 编码为机器指令,生成可重定位目标文件 hello.o(ELF relocatable)。此时指令已是二进制编码,但对外部符号地址与部分位置相关引用仍未知,因而在 .rela.* 等重定位节中记录“需要修补的位置、类型与符号”,并在符号表中保存符号信息以供链接器解析。
  2. 链接(Linking:.o → hello)
    链接器将 hello.o 与启动文件(如 crt1.o/crti.o/crtn.o)、libc、libgcc 等合并,完成节合并、符号解析、地址分配与重定位,生成可执行 ELF(hello 或手工 ld 生成的 hello_ld)。
    若为动态链接程序:

可执行文件包含 .interp 指向动态链接器(如 /lib64/ld-linux-x86-64.so.2),

外部函数调用通过 PLT + GOT 建立“延迟绑定”路径,真实地址在运行时由动态链接器解析并写回 GOT。

  1. 装载与进程创建(Loading & Process Creation)
    用户在 shell 中启动 ./hello ...:shell 通过创建子执行流并调用 execve("./hello", argv, envp) 装载程序。内核根据 ELF Program Headers 将各段映射到进程虚拟地址空间,建立代码段/数据段/堆/栈等区域,准备初始用户栈(argc/argv/envp/auxv),并将入口设置为 ELF Entry(通常从 _start 开始)。
  2. 动态链接与运行库初始化(Dynamic Linking & Runtime Init)
    对动态链接程序,内核先把控制权交给动态链接器,由其加载所需共享库、进行重定位与符号解析。随后进入 C 运行库启动例程(如 __libc_start_main 路径),完成运行库初始化后调用 main,程序进入用户代码逻辑。
  3. 执行阶段(Execution:I/O、调度、系统调用、信号)
    hello 在循环中调用 printf 输出字符串并调用 sleep 暂停,最后通过 getchar 等待读取输入。

printf/getchar/sleep 在用户态由 libc 实现,最终触发 write/read/nanosleep 等系统调用,通过 syscall 指令陷入内核态;

sleep 使进程进入阻塞态,调度器基于时间片与就绪队列在进程间切换;

终端控制按键(Ctrl-Z/Ctrl-C)产生信号(SIGTSTP/SIGINT)发送到前台进程组,shell 通过作业控制(jobs/fg/kill)维护进程状态并可发送 SIGTERM/SIGKILL 终止进程。

  1. 退出与资源回收(Termination)
    hello 正常结束或被信号终止后,通过 exit 路径返回:内核回收进程资源(页表、打开文件、内核对象等),向父进程发送 SIGCHLD,父进程(shell)通过 wait 系列调用获取退出状态并回到命令提示符。

2)感悟与创新理念

感悟:

“分层抽象”是可控复杂度的根本方法:从 C 源代码到 ELF,再到虚拟内存映射与系统调用链路,层层抽象让上层无需理解全部硬件细节,但又通过明确接口(ABI、ELF 规范、syscall、VFS)保证可实现性与可移植性。

链接与装载体现了“把确定性推迟到必要时刻”的工程哲学:静态链接追求“一次性确定”,动态链接通过 PLT/GOT 将解析推迟到运行期,换来共享库复用、节省磁盘与内存占用,同时也引入首调开销与更复杂的运行时依赖。

虚拟内存把“隔离/共享/性能”统一在同一套机制里:进程看到连续的地址空间,而页表/TLB/缺页处理/COW 等机制在后台实现保护与高效复用;fork 的 COW、execve 的地址空间替换,使得“进程”成为可低成本创建与切换的执行容器。

创新理念:新的设计与实现方法(可落地):

一键可复现实验流水线:将预处理、编译、汇编、链接、readelf/objdump/strace/gdb 等命令固化为 Makefile 或脚本(例如 make all && make report),自动生成 .i/.s/.o 与所有分析日志、并按“附件清单”自动归档打包。这样可以避免人工遗漏截图/漏文件,提升实验的可复现性与工程规范性。

自动化对照分析器:编写脚本自动完成“hello.s ↔ hello.o ↔ hello”三者的对照:提取符号、重定位项、PLT/GOT 表项、关键函数的反汇编片段,生成带注释的对照报告(markdown/latex),帮助快速定位“链接前后发生了哪些地址修补与调用路径变化”。

面向教学的可视化工具:把 info proc mappings、readelf -l/-S、objdump -d -r 的结果做成可视化(段/节/映射区间的对齐关系图),直观展示 section 与 segment、VA 与 file offset 的对应关系;这样能显著降低初学者理解 ELF 与装载过程的门槛。

附件

本实验在目录 /home/sbliu/ICS-hello 下生成的中间产物与分析输出文件如下,各文件作用说明如下:

1)环境信息与程序源文件

env.txt:记录实验软硬件环境信息,包括 Linux/Ubuntu 版本、gcc 版本、binutils(ld/as)版本等。

hello.c:hello 程序 C 源代码文件,是整个编译链接流程的输入(Program 起点)。

2)编译流水线各阶段产物(.c → .i → .s → .o → 可执行文件)

hello.i:预处理输出文件(.c → .i)。宏展开、头文件展开、条件编译处理后的纯 C 文本,用于后续编译阶段分析与对照。

hello.s:编译输出的汇编文件(.i → .s)。体现编译器将 C 语言中的数据类型、表达式、控制流与函数调用映射到 x86-64 汇编指令与段(如 .text/.rodata)的结果。

hello.o:可重定位目标文件(.s → .o,ELF relocatable)。包含机器指令二进制编码、节表、符号表以及重定位信息,是链接阶段的输入。

hello:可执行文件(由工具链链接生成的可执行目标文件)。运行时由内核装载并创建进程。

hello_ld:手工使用 ld 生成的可执行文件(相对于 gcc 默认链接方式的对照版本)。

3)ELF / 反汇编 / 重定位分析输出

readelf_hello_o.txt:对 hello.o 的 readelf 输出(如 ELF 头、节表、符号表、重定位项等)。

objdump_hello_o.txt:objdump -d -r hello.o 的输出,包含反汇编与重定位信息,用于与 hello.s 对照分析“汇编指令 ↔ 机器码编码”以及链接前地址未定位置。

readelf_hello.txt:对可执行文件 hello 的 readelf 输出,用于说明 ELF 头、程序头(段信息)、各段虚拟地址/偏移/大小等,并与进程虚拟地址空间对照分析。

objdump_hello.txt:objdump -d -r hello 的输出,用于与 hello.o 对比分析链接重定位后的变化,以及外部函数调用相关的 PLT/GOT 机制。

ldd_hello.txt:ldd hello 的输出,列出可执行文件运行时依赖的共享库(如 libc、动态链接器等),用于动态链接分析支撑。

4)系统调用跟踪与过程日志

strace_hello.txt:对 hello 的系统调用跟踪输出,用于证明 printf/getchar/sleep 等高层库函数最终触发 write/read/nanosleep 等系统调用。

strace_hello_1s.txt:hello 在特定参数/运行时长下的系统调用跟踪输出(例如运行 1 秒/一次循环的情况),用于对比不同运行参数下系统调用行为差异。

strace_io.txt:仅跟踪 I/O 相关调用(如 write/read)的输出。

strace_io2.txt:另一组 I/O 跟踪输出(通常用于补充 read(0,...) 等输入阻塞/返回的证据)。

strace_e.txt:与 execve 等关键过程相关的跟踪输出(用于查找 execve("./hello",...) 等证据行)。

strace_p.txt:过程跟踪日志(用于检索 execve/write/read 等关键证据行),作为报告中多处论证的辅助证据。

strace_fork.txt:对 bash -c "./hello ..." 的进程相关跟踪输出,用于观察/证明与进程创建、execve 装载相关的过程。

strace_fork2.txt:另一组进程相关跟踪输出(用于解决 bash “exec 优化”导致不出现 fork/clone 的情况,常通过添加额外命令强制产生子进程)。

5)调试相关日志

gdb_log.txt:gdb 调试输出日志,用于记录关键断点、调用栈、寄存器/指令地址等信息,支撑执行流程(_start→__libc_start_main→main→exit)与地址空间观察。

参考文献

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

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

原文链接:https://blog.csdn.net/2503_93303843/article/details/156807544

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

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