程序人生(哈工大CSAPP计算机系统大作业)

摘  要

本报告以C语言程序hello.c为例,系统追踪并解析了其从静态源代码(Program)转变为操作系统中动态执行的实体(Process),直至最终消亡的完整P2P和020生命周期。报告依次阐述了预处理阶段的文本转换,编译阶段C到汇编的映射,汇编阶段机器码的生成(hello.o),以及链接阶段符号解析、重定位与可执行文件hello的构建。在此基础上,深入探讨了hello作为可执行文件被Shell通过fork和execve加载和执行的机制,以及动态链接器在启动时的作用。进程管理方面,分析了hello进程的上下文、状态转换、用户/核心态切换、时间片调度及对键盘信号(如Ctrl-C/Ctrl-Z)的响应与作业控制。存储管理层面,描述了其虚拟地址空间布局,段式与四级页式地址转换(辅以TLB和CPU缓存加速),以及fork时的写时复制、execve时的内存映像替换和缺页处理。最后,I/O管理部分阐释了Linux设备模型、Unix I/O接口,并剖析了printf和getchar的实现原理,包括缓冲、系统调用与中断处理。本报告旨在通过hello的“一生”,揭示计算机系统核心组件的协同工作,深化对底层运行原理的理解。

关键词:计算机系统;hello程序;P2P;020;预处理;编译;汇编;链接;ELF;进程管理;虚拟内存;I/O管理;系统调用;


目录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2 在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.4 Hello.o的结果解析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

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

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

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献


第1章 概述

1.1 Hello简介

本报告围绕一个简单的C语言程序 hello.c 展开。hello.c 的核心功能是在命令行接收学号、姓名、手机号和秒数作为参数,并在接下来的10次循环中,每隔指定的秒数打印一次包含这些信息的问候语(如:“Hello 2023112963 王艺霖 18349831528”),最后等待用户输入一个字符后结束。

"Hello's P2P" (Program to Process) 描述了 hello.c 从一个静态的源代码文件(Program)转变为一个在操作系统中动态执行的实体(Process)的全过程。这包括了:

1.预处理 (Preprocessing):处理源代码中的预处理指令(如 #include, #define)。

2.编译 (Compilation):将预处理后的C代码翻译成汇编代码。

3.汇编 (Assembly):将汇编代码转换成机器可执行的二进制指令,生成可重定位目标文件。

4.链接 (Linking):将程序所依赖的库函数等与其他目标文件合并,生成最终的可执行文件。

5.加载与执行 (Loading & Execution):当用户在shell中运行该可执行文件时,操作系统将其加载到内存,并创建一个新的进程来执行程序的指令。

"020" (From Zero-0 to Zero-0) 则形象地描绘了 hello 程序从无到有,再从有到无的生命周期。

1.From Zero (源): 程序最初以 hello.c 文本文件的形式存在于磁盘上,尚未被执行,不占用系统核心资源。

2.To 0 (生): 经过上述P2P的转换,hello 成为一个可执行文件。当它被执行时,操作系统为其分配内存、CPU时间片等资源,成为一个活跃的进程,开始执行其定义的任务(打印信息、休眠)。

3.To Zero-0 (灭): 当 hello 进程完成所有任务(循环结束,getchar()执行完毕)或被用户/系统终止(如Ctrl-C),它会调用 exit() 系统调用,操作系统回收其占用的所有资源(内存、文件描述符等),进程消亡,回归到“无”的状态,仿佛“赤条条来去无牵挂”。

1.2 环境与工具

本实验运行环境Ubuntu 20.04 LTS (64-bit),硬件为x86-64架构,主要使用的工具包括编译器GCC(GNU Compiler Collection)、汇编器GAS (GNU Assembler)、链接器LD、反汇编工具objdump、ELF文件分析工具readelf、调试器gdb、进程查看工具(ps、jobs、pstree)。

1.3 中间结果

在 hello.c 从源程序到可执行文件的P2P转换过程中,会生成以下主要的中间文件:

hello.c: C语言源文件。

hello.i: 预处理后的C语言源文件。由预处理器(cpp)处理 hello.c 中的宏定义、头文件包含等指令后生成。

hello.s: 汇编语言源文件。由编译器(cc1)将 hello.i 文件翻译成对应体系结构(x86-64)的汇编指令。

hello.o: 可重定位目标文件(Relocatable Object File)。由汇编器(as)将 hello.s 文件转换成机器语言指令,并打包成ELF(Executable and Linkable Format)格式。它包含了代码段、数据段以及供链接器使用的符号表和重定位信息。

hello: 可执行目标文件(Executable Object File)。由链接器(ld)将 hello.o 文件与所需的库文件(如C标准库 libc)链接起来,解析符号引用,重定位符号地址,最终生成可以直接在操作系统上运行的ELF格式文件。

1.4 本章小结

本章概述了 hello 程序的P2P(Program to Process)和020(From Zero-0 to Zero-0)生命周期,介绍了完成本次大作业所依赖的软硬件环境及开发调试工具,并列举了 hello.c 在编译链接过程中产生的各个中间文件及其作用。这些为后续章节详细分析 hello 程序的“一生”奠定了基础。

第2章 预处理

2.1 预处理的概念与作用

C语言编译过程的第一个阶段是预处理(Preprocessing),由预处理器(如GCC中的cpp)在实际编译前对源代码进行处理。预处理器专门处理以#开头的指令,包括宏定义与替换(#define)、头文件包含(#include)、条件编译(#if/#ifdef等)、注释移除以及特殊指令处理(#error/#pragma等)。它会将宏展开、头文件内容插入、条件编译选择保留的代码保留,同时移除所有注释,最终生成一个不含预处理指令、扩展名为.i的纯C代码文件,为后续的编译阶段做好准备。整个过程是纯粹的文本替换操作,不涉及语法检查,使得同一份源代码可以通过条件编译等机制灵活适应不同平台和环境的需求。

2.2 在Ubuntu下预处理的命令

命令:gcc -E hello.c -o hello.i

图表 1预处理命令

在Linux系统中,我们可以使用GCC编译器套件进行C语言的预处理操作,具体命令为gcc -E hello.c -o hello.i,其中-E选项指示GCC仅执行预处理阶段(包括宏展开、头文件包含等操作)后即终止流程,hello.c指定待处理的源文件,而-o hello.i则将预处理结果输出到指定文件(若省略此参数,处理结果会直接显示在终端屏幕上),用nano查看hello.i文件内容如下。

图表 2 hello.i文件内容

2.3 Hello的预处理结果解析

对hello.i文件分析可以看到:注释被移除,头文件内容被展开,所有的#include已被替换,标准I/O、stdlib等头文件中的声明已插入其中。这个阶段得到的代码已经包含了完整的函数原型和类型定义,但函数体的具体实现由后续链接阶段提供。hello.i 文件会比 hello.c 文件大得多,因为它包含了所有被包含头文件的声明和定义。其主体逻辑依然是 hello.c 中的 main 函数,但现在它拥有了所有依赖函数的完整声明。

图表 3预处理后的main函数片段

2.4 本章小结

本章详细介绍了预处理的概念、作用以及在C语言编译过程中的位置。通过具体的GCC命令演示了如何在Ubuntu环境下对 hello.c 进行预处理,并分析了预处理后生成的 hello.i 文件相较于原始 hello.c 文件的主要变化,特别是头文件的展开和注释的移除。预处理是程序从源代码到可执行代码转换的第一步,为后续的编译阶段提供了纯净的、包含了所有必要声明的C代码。

第3章 编译

3.1 编译的概念与作用

在C语言的编译过程中,编译器会将预处理生成的.i文件转换为特定架构的汇编代码(.s文件),这一过程首先通过词法分析将源代码分解为记号(tokens,如关键字、标识符、常量、运算符等),然后进行语法分析构建抽象语法树(AST),接着通过语义分析检查类型匹配和作用域等语义规则,随后生成与机器无关的中间表示(IR)并进行优化(如常量折叠和死代码消除),最终根据目标机器的指令集特性生成优化的汇编代码,例如将hello.i中的变量声明、条件判断、循环和函数调用等C语句转换为比较与跳转指令,将函数调用参数压栈等。

3.2 在Ubuntu下编译的命令

命令:gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC -S hello.c -o hello.s

  1. -S: 此选项告诉 GCC 在编译完成后即停止,生成汇编代码文件,不进行后续的汇编和链接。
  2. hello.i 或 hello.c: 输入的预处理文件或C源文件。
  3. -o hello.s: 指定输出的汇编文件名为 hello.s。
  4. -m64: 生成64位代码。
  5. -Og: 针对调试进行优化,这通常意味着保留变量,使调试更容易。
  6. -no-pie: (No Position Independent Executable) 生成非位置无关的可执行文件。这会影响最终可执行文件的加载地址。
  7. -fno-stack-protector: 禁用栈保护机制。栈保护用于检测栈溢出。
  8. -fno-PIC: (No Position Independent Code) 生成非位置无关代码。对于主程序来说,这通常是默认的,除非编译共享库。

编译过程由GCC前端调用相应的后端完成,期间编译器进行了语法检查和优化。如果存在语法错误,编译阶段会报错终止;对于Hello程序,编译顺利完成。生成的hello.s文件包含了函数main的汇编实现,以及字符串常量等相应的汇编表示。

图表 4编译命令示意

图表 5hello.s内容示意

3.3 Hello的编译结果解析

hello.s 文件包含了 hello.c 程序逻辑的x86-64汇编代码。由于使用了 -Og 优化级别,代码会相对直接,但仍会涉及栈帧的建立与销毁、参数传递、函数调用等典型汇编操作。

字符串常量:源代码中两处 printf 使用了字符串常量,一个是用法提示 "用法: Hello 学号 姓名 手机号 秒数!\n",另一个是打印格式 "Hello %s %s %s\n"。在汇编中,这些常量被放入只读数据段 .rodata 中,并被赋予标签(如 .LC0, .LC1)以供引用。例如,.LC0 对应用法提示字符串,.LC1 对应 "Hello %s %s %s\n" 格式串。当调用 printf 时,会通过 leaq .LCN(%rip), %rdi 指令加载相应字符串的地址到 %rdi 寄存器作为第一个参数。

图表 6汇编示意图1

函数入口和出口:main 函数在汇编中以全局符号标签 main: 开始。编译器按照x86-64 System V AMD64 ABI生成函数的序言 (prologue) 和尾声 (epilogue)。序言通常包括 pushq %rbp 保存旧的帧指针,movq %rsp, %rbp 设置新的帧指针,以及 subq $N, %rsp 为局部变量(如 i)和可能保存的寄存器分配栈空间。尾声在函数返回前执行,通常是 leave 指令(等效于 movq %rbp, %rsp; popq %rbp;)和 ret 指令,用于恢复栈帧和返回到调用者。

图表 7汇编示意图2

参数与局部变量:main 函数的参数 int argc 和 char *argv[] 按照调用约定,分别通过 %edi (作为 %rdi 的低32位) 和 %rsi 寄存器传入。在函数内部,这些参数值以及局部变量 int i 通常会被保存到栈帧的特定偏移处,例如 argc 可能存放在 -20(%rbp),argv 的指针存放在 -32(%rbp),而局部变量 i 可能存放在 -4(%rbp)。后续对这些变量的访问都通过相对于 %rbp 的偏移量进行。

条件判断与处理 (if 语句)C代码中的 if(argc!=5) 语句在汇编中被实现为比较和条件跳转。首先,cmpl $5, -20(%rbp) 指令将立即数5与栈上存储的 argc 值进行比较。随后,je .L_SKIP_IF_BLOCK (Jump if Equal) 指令会在 argc 等于5时跳转到 if 块之后的代码。若不跳转(即 argc != 5),则顺序执行 if 块内的代码:调用 printf 打印用法提示(加载 .LC0 地址到 %rdi,调用 printf@PLT),然后准备 exit(1) 的调用,即将立即数1加载到 %edi 作为 exit 的参数,再 call exit@PLT。

图表 8汇编示意图3

循环实现 (for 语句)源代码的 for(i=0; i<10; i++) 循环被编译为一个典型的循环结构。初始化 i=0 对应 movl $0, -4(%rbp)。循环的条件检查 i<10 通过 cmpl $10, -4(%rbp) 和 jl .L_LOOP_BODY (Jump if Less) 实现,当 i 小于10时跳转到循环体。循环体内部首先执行 printf 调用,然后是 sleep(atoi(argv[4])) 的逻辑。循环体执行完毕后,addl $1, -4(%rbp) 指令执行 i++,然后通常会有一个无条件跳转 jmp .L_LOOP_CONDITION 回到条件检查处。

函数调用和参数传递:

(1)printf 调用:循环体内的 printf("Hello %s %s %s\n", argv[1], argv[2], argv[3]) 调用,参数传递遵循ABI。首先,从栈上加载 argv 的基地址(例如到 %rax),然后通过计算偏移量(如 8(%rax), 16(%rax), 24(%rax))分别将 argv[1], argv[2], argv[3] 的地址加载到 %rsi, %rdx, %rcx 寄存器。格式字符串 .LC1 的地址加载到 %rdi。由于 printf 是可变参数函数,根据ABI,%eax 寄存器通常设置为0(表示没有XMM寄存器参数)。最后执行 call printf@PLT。

(2)atoi 和 sleep 调用:sleep(atoi(argv[4])) 的实现分为两步。首先,调用 atoi:从栈上加载 argv 基地址,然后获取 argv[4] 的地址(如 32(%rax))并将其加载到 %rdi 作为 atoi 的参数,执行 call atoi@PLT。atoi 的返回值(整数)在 %eax 寄存器中。然后,这个返回值被移动到 %edi (movl %eax, %edi) 作为 sleep 函数的参数,接着执行 call sleep@PLT。

(3)getchar 调用:循环结束后,getchar() 调用用于等待用户输入。这在汇编中通常实现为 call getchar@PLT。由于 getchar 通常是宏,可能最终会解析为对 getc(stdin) 的调用,这意味着需要加载 stdin 的地址(一个全局 FILE* 指针)到 %rdi。

返回值处理:源代码 return 0; 对应汇编中的 movl $0, %eax,将返回值0设置到 %eax 寄存器。然后执行函数尾声(如 leave 和 ret),将控制权返回给调用者(对于 main 函数,是C运行时库,最终会将这个0作为进程的退出状态)。

通过这种方式,编译器系统地将C语言的高级构造分解为一系列底层的汇编指令,同时严格遵守目标平台的应用程序二进制接口(ABI)以确保函数调用和数据表示的正确性。

3.4 本章小结

本章阐述了编译阶段的概念、主要任务及其在程序转换过程中的核心作用。通过GCC命令展示了如何将预处理后的 hello.i(或直接从 hello.c)编译成x86-64架构的汇编文件 hello.s,并强调了作业要求的特定编译选项。最后,结合C语言的数据与操作,对 hello.s 中可能出现的汇编代码片段进行了分析,解释了C构造如何映射到汇编指令,包括变量存储、算术运算、控制流和函数调用等。编译是连接高级语言与机器底层指令的关键桥梁。

第4章 汇编

4.1 汇编的概念与作用

汇编是将汇编语言程序(.s文件)翻译成机器语言指令(二进制代码)并生成可重定位目标文件(.o文件)的关键过程,这一任务由汇编器(如GCC工具链中的as)完成。其主要作用包括:将人类可读的汇编指令(如movl、addq等助记符)转换为对应的二进制操作码;处理伪指令(如.data/.text定义段结构、.globl声明全局符号、.string存储字符串等)来组织目标文件内容;构建符号表以记录标签地址和外部引用;最终生成符合ELF格式的可重定位目标文件,其中包含机器指令、数据段内容以及供链接器使用的符号表和重定位信息。例如,处理hello.s时,汇编器不仅会将每条指令转换为机器码,还会将.LC0等字符串常量实际存入数据段,为后续链接阶段做好准备。

4.2 在Ubuntu下汇编的命令

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

其中-c表示只汇编不链接,生成可重定位的目标文件hello.o。执行后会在当前目录得到hello.o,其中包含了Hello程序的机器码和符号信息。

图表 9汇编命令示意

图表 10nano编译器下的hello.o

4.3 可重定位目标elf格式

可重定位目标ELF格式文件(如hello.o)采用标准的ELF(Executable and Linkable Format)结构,通过readelf -a hello.o可查看其完整组成。文件起始的ELF头部通过魔数标识文件类型,并记录节头部表等元数据位置;紧随其后的节头部表则详细描述各节的属性与布局。核心节区包括:存放机器指令的.text段(含main函数代码)、存储字符串常量等只读数据的.rodata段、记录已初始化全局变量的.data段(本程序中几乎未使用),以及标记未初始化变量的.bss段(实际不占文件空间)。关键的.symtab符号表维护所有符号定义与引用信息(如main函数定义和printf等外部引用标记为UND),配合.rel.text/.rela.text重定位节区(记录需链接时修正的指令地址偏移)共同支持后续链接过程。由于是可重定位文件,所有地址均采用相对于节区的偏移量形式,最终由链接器转换为可执行文件中的绝对虚拟地址。

4.4 Hello.o的结果解析

图表 11反汇编下的hello.o

图表 12符号表

图表 13重定位表

图表 14节区表

通过 `objdump -d -r hello.o 命令,我们可以观察到 hello.o 文件的反汇编代码和重定位信息。反汇编输出清晰地显示,源于 hello.s 的每一条汇编指令都已被汇编器精确地转换成了对应的x86-64机器码字节序列。例如,main 函数的栈帧建立(如 pushq %rbp, movq %rsp, %rbp, subq $N, %rsp 为局部变量 i 等分配空间)以及后续对栈上变量的访问(如 movl $0, -4(%rbp))都有明确的机器码与之对应。

与 hello.s 对照,机器语言中对操作数的处理更为具体。寄存器操作数直接编码在指令中,立即数作为指令的一部分。关键在于对符号地址的处理:在 hello.s 中使用的标签(如 .LC0)和外部函数名(如 printf)在 hello.o 的机器码中,其地址引用通常以占位符(如0)或临时的相对偏移形式存在。objdump -r hello.o 部分列出的重定位条目,如 R_X86_64_PC32 .rodata 或 R_X86_64_PLT32 printf,明确指出了这些占位符需要在链接阶段被修正。例如,对 printf、exit、sleep、atoi 和 getchar 等外部函数的 call 指令,其机器码的目标地址是临时的,并伴有重定位条目,指示链接器将其解析为指向PLT的跳转。

分支转移指令(如 je, jl)在 hello.o 中已将其在 hello.s 中的标签目标转换成了相对于当前指令的偏移量(如果标签在同一文件内)。这与函数调用对外部符号的处理方式有所不同,后者更依赖链接器的后期工作。

hello.o 的反汇编结果验证了编译阶段的分析:C语言的局部变量通过栈操作实现,if 和 for 等控制流结构对应于机器码中的比较和条件/无条件跳转指令,函数调用则通过 call 指令引用符号。同时,其包含的符号表为链接器提供了符号定义和引用的信息,而重定位表则指明了哪些地址需要在链接时被精确修正。hello.o 作为一个可重定位的目标文件,已经包含了程序的机器指令和数据,但它是一个“半成品”,依赖链接器将其与库函数等其他模块结合,解析未定义符号,并调整地址引用,最终形成一个可执行的 hello 程序。

4.5 本章小结

本章介绍了汇编阶段的概念和核心任务:将汇编语言翻译成机器指令,并生成包含代码、数据、符号表和重定位信息的可重定位目标文件(如ELF格式的 hello.o)。通过GCC命令演示了如何从 hello.s (或 hello.c) 生成 hello.o。详细分析了 hello.o 的ELF文件结构,包括ELF头、节头部表以及重要的节如 .text, .rodata, .symtab, .rela.text 等。最后,通过 objdump 工具解析了 hello.o 中的机器指令构成及其与汇编语言的映射关系,并强调了操作数、分支转移、函数调用在机器码层面的实现以及重定位条目的作用。汇编是编译过程的最后一步,为链接生成了模块化的构建块。

5章 链接

5.1 链接的概念与作用

链接是将多个可重定位目标文件和库文件(如libc.a/libc.so)合并生成可执行文件或共享库的关键过程,由链接器(如GCC的ld)完成。其核心作用包括:通过符号解析构建全局符号表,确定每个符号引用的定义位置(在目标文件或库中),处理强/弱符号冲突;通过重定位合并同类节区(如所有.text段)、分配运行时内存地址,并修正代码/数据中的符号引用(如将call printf的占位地址更新为实际地址)。链接分为静态链接(库代码直接复制到可执行文件)和动态链接(运行时才解析共享库符号)两种方式。以hello程序为例,链接器会解析printf等标准库函数(默认动态链接),将其地址信息写入最终可执行文件,同时合并所有目标文件的代码段、数据段,并确保内存地址引用的正确性,从而生成可直接运行的程序。

5.2 在Ubuntu下链接的命令

命令:gcc -m64 -no-pie -fno-stack-protector -fno-PIC hello.o -o hello

GCC会自动调用ld并添加必要的启动文件和动态链接器路径。链接完成后生成的hello是最终可执行文件。

图表 15链接命令示意

图表 16可执行文件运行示意

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

图表 17readelf -h hello

图表 18readelf -l hello

链接后生成的 hello 文件依然采用ELF格式,但其ELF头部中的类型字段 e_type 标记为 ET_EXEC (可执行文件),这与 hello.o 的 ET_REL (可重定位文件) 类型显著不同。区别在于,可执行文件 hello 必须包含一个程序头部表 (Program Header Table),而可重定位文件则不需要。此表可以通过 readelf -l hello 查看。

图表 19readelf -d hello

程序头部表描述了文件中的各个段 (Segments) 如何被加载器映射到进程的虚拟地址空间。其中,PT_LOAD 类型的段指明了哪些部分的文件内容(如代码段 .text 和数据段 .data) 需要被加载到内存,并指定了它们在虚拟内存中的起始地址 (VA)、大小以及访问权限(如读/执行、读/写)。例如,hello 的代码部分(包括 .text, .rodata, .plt 等)会被映射到一个具有读和执行权限的虚拟地址区域,而数据部分(.data, .bss, .got 等)则会被映射到另一个具有读写权限的区域。操作系统在执行 execve 系统调用加载 hello 时,正是依据这个程序头部表来设置进程的内存映像。

此外,由于 hello 是动态链接的,其程序头部表还会包含 PT_INTERP 段,用以指定动态链接器的路径,以及 PT_DYNAMIC 段,指向 .dynamic 节,该节包含了动态链接所需的各种信息,如依赖的共享库列表、符号表和重定位表的位置等。

虽然可执行文件以程序头部表为主要加载依据,但通过 readelf -S hello 仍然可以查看到一些节区信息,如 .text, .rodata, .data, .bss, .plt, .got, .dynamic,以及 .init, .fini 等特殊节。这些节现在都有了确定的虚拟地址。与 hello.o 不同,大部分用于静态链接的重定位节在可执行文件中通常不再存在,除非是动态链接所需的重定位信息。

5.4 hello的虚拟地址空间

hello 程序启动时,操作系统会为其创建一个独立的、私有的虚拟地址空间。这个空间从0开始,向上延伸至一个非常大的理论上限(如64位系统的2<sup>64</sup>-1)。通过GDB等调试工具的 info proc mappings vmmap 命令,我们可以观察到这个虚拟地址空间中各个段的典型布局:代码段(.text.rodata等,通常位于较低地址,权限为读/执行)、数据段(.data.bss,在代码段之后,权限为读/写)、堆(在数据段之后向上增长,用于动态内存分配)、共享库映射区域(用于加载如 libc.so.6 等动态链接库)以及栈(位于虚拟地址空间的高处,向下增长,用于函数调用和局部变量)。hello 程序ELF头部指定的入口点(通常是 _start)位于其代码段的某个虚拟地址,而程序中所有的地址引用(如函数指针、变量地址)也都是在这个虚拟地址空间内的地址。这种独立的虚拟地址空间设计,确保了进程间的隔离和保护,并为程序员提供了一个简洁、连续的内存视图。

   

5.5 链接的重定位过程分析

链接的核心任务之一是重定位,即根据可执行文件最终的内存布局来调整和修正代码与数据中的地址引用。通过对比 hello.o(可重定位目标文件)和链接后生成的 hello(可执行文件)的反汇编输出(使用 objdump -d -r hello.o objdump -d hello),可以清晰地看到这一过程。在 hello.o 中,对外部符号(如C库函数 printf)的调用指令其目标地址是未确定的(通常是0或相对PLT的占位符),并伴有重定位条目(如 R_X86_64_PLT32 printf)指示链接器进行修正。链接器在符号解析后,会计算出这些外部函数在PLTProcedure Linkage Table)中的入口地址,或者对于内部数据引用(如对 .rodata 段中字符串的访问),会计算出其最终的虚拟地址或相对于PC的正确偏移,然后修改 hello.o 中相应的机器码,将占位符替换为这些计算好的地址或偏移。因此,在链接生成的可执行文件 hello 中,大部分的地址引用都已被解析为运行时有效的虚拟地址或指向PLT/GOT的间接跳转,使得程序能够正确寻址其代码和数据。

5.6 hello的执行流程

hello 程序的执行始于操作系统加载其可执行文件到内存,并从ELF头部指定的入口点(通常是 _start 标签,位于C运行时库提供的启动代码中)开始执行。_start 的主要职责是进行必要的初始化,包括设置栈、准备传递给 main 函数的参数(argc, argv),然后调用 __libc_start_main 函数。__libc_start_main 进一步完成C环境的初始化(如注册 atexit 清理函数),随后才真正调用我们编写的 main 函数。在 main 函数内部,程序按照C代码逻辑顺序执行:参数数量检查、循环打印信息(调用 printf)、休眠(调用 sleep atoi),最后等待用户输入(调用 getchar)。当 main 函数返回或调用 exit() 时,控制权交回给 __libc_start_main,它会执行必要的清理工作(如调用 atexit 注册的函数),最终通过 _exit 系统调用终止进程,并将 main 的返回值作为进程的退出状态。这个从 _start main 再到进程终止的流程,是标准C程序在Linux下的典型执行路径。

5.7 Hello的动态链接分析

hello 程序默认采用动态链接方式来使用C标准库中的函数(如 printf, sleep 等)。这意味着这些库函数的实际代码并没有被复制到 hello 可执行文件中,而是在程序运行时由动态链接器(如 ld-linux-x86-64.so.2)加载到进程的地址空间并进行链接。通过 readelf -d hello 可以查看其 .dynamic 节,其中包含了动态链接所需的信息,如 NEEDED 条目指明了依赖的共享库(如 libc.so.6),以及指向 .got (Global Offset Table) .plt (Procedure Linkage Table) 等节的指针。当 hello 首次调用一个动态库函数(如 printf)时,执行流程会通过PLT中的对应条目跳转。该PLT条目会触发动态链接器解析该函数的真实地址(在加载的共享库中),并将此地址存入GOT中的相应表项。后续对该函数的调用则可以直接通过GOT表项中的地址跳转,实现了地址的延迟绑定。例如,objdump -R hello 会显示与 printf 等函数相关的 R_X86_64_JUMP_SLOT R_X86_64_GLOB_DAT 类型的动态重定位条目,这些条目指导动态链接器在加载时或首次调用时填充GOT。这种机制使得多个进程可以共享同一份库代码的内存副本,节约了磁盘空间和内存,并方便了库的更新。

分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。

5.8 本章小结

本章系统介绍了链接的核心概念与实现机制,重点展示了如何通过GCC工具链将hello.o目标文件与C标准库链接生成最终可执行文件的全过程。在技术实现层面,详细分析了可执行文件的ELF格式特征,特别是程序头表的结构及其作用,深入阐述了链接器完成符号解析和重定位的具体方法,包括如何通过重定位条目将相对地址转换为最终虚拟地址的关键技术。通过gdb调试工具,本章完整剖析了hello进程的虚拟地址空间布局,从动态链接器的加载介入、_start入口点的初始化、__libc_start_mainmain函数的调用,到exit终止处理的整个控制流程,全面呈现了程序从加载到执行再到退出的完整生命周期。同时,本章还对静态链接与动态链接的技术差异进行了对比说明。这些内容不仅完整揭示了从目标文件到可执行文件的转换过程,更通过链接后程序运行环境的映射分析,为深入理解后续的进程执行和内存管理机制奠定了重要基础,充分体现了链接作为程序能够真正运行的最后关键环节的核心价值。

6章 hello进程管理

6.1 进程的概念与作用

进程是程序在计算机上一次执行活动的动态实例,是操作系统进行资源分配和调度的基本单位。与静态存储在磁盘上的程序代码不同,进程拥有自己的生命周期(创建、就绪、运行、阻塞、终止)、独立的虚拟地址空间、程序计数器、寄存器状态、打开的文件描述符列表等,这些信息被记录在内核的进程控制块 (PCB) 中。当 hello.c 被编译链接成可执行文件 hello 并运行时,操作系统会为其创建一个进程,使其从一段静态的指令集合转变为一个活跃的计算实体。这个 hello 进程将独立于其他进程运行,并由操作系统内核统一管理。

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

用户通常通过Shell(如Bash)与操作系统内核交互并启动程序。Shell作为一个命令解释器,接收用户在命令行输入的指令(如 ./hello 学号 姓名 手机号 秒数),解析这些指令,并调用相应的系统服务来执行。当用户输入执行 hello 程序的命令后,Bash首先解析命令和参数,然后通过 fork() 系统调用创建一个新的子进程。这个子进程是Bash进程的一个副本(初始时通过写时复制共享内存)。紧接着,子进程会调用 execve() 系统调用,用 hello 可执行文件的映像替换其自身的内存空间,从而开始执行 hello 程序的代码。Bash(父进程)则根据命令的执行方式(前台或后台)选择等待子进程结束或继续接收新命令,同时它也负责管理其启动的作业(jobs)。

6.3 Hello的fork进程创建过程

fork() 系统调用是Linux/Unix系统中创建新进程的主要方式。当Bash执行 fork() 时,内核会为即将诞生的子进程分配一个新的、唯一的进程ID (PID),并复制父进程(Bash)的进程控制块 (PCB) 的大部分内容,包括寄存器状态(但子进程的 fork() 返回值为0,父进程返回子进程PID)、打开的文件描述符、环境变量等。关键的是,子进程会获得父进程虚拟地址空间的一个副本。为了提高效率,这个复制通常采用“写时复制” (Copy-On-Write, COW) 机制:父子进程初始时共享相同的物理内存页,只有当其中一方尝试修改共享页时,内核才会为修改方创建该页的一个私有副本。fork() 成功后,父子进程从 fork() 调用之后的地方各自独立并发执行,子进程通常会紧接着调用 execve()。

6.4 Hello的execve过程

在 fork() 创建的子进程中,execve("./hello", argv, envp) 系统调用负责加载并运行新的 hello 程序。execve 的核心作用是进行程序映像的替换:它会清除调用进程(即子进程)当前虚拟地址空间中的原有段(代码、数据、堆、栈等,这些是从Bash继承来的),然后根据 hello 可执行文件的ELF头部和程序头部表,为 hello 程序建立全新的内存映射。这包括将 hello 的代码段和数据段从文件加载(或映射)到新的虚拟内存区域,为 .bss 段分配并清零内存,以及设置新的程序栈(栈顶包含传递给 main 的 argc, argv 和 envp)。进程的程序计数器 (PC) 被设置为 hello 程序的入口点(通常是 _start)。如果 execve 成功,它不会返回到调用它的代码;控制权直接转移到新程序。重要的是,进程的PID保持不变,但其执行的程序内容已完全变为 hello。

6.5 Hello的进程执行

hello 进程在其生命周期中会在用户态和核心态之间转换,并经历运行、就绪、阻塞等多种状态。当 hello 进程执行其自身的代码(如 for 循环、参数准备)时,它处于用户态。当它需要操作系统服务时,如调用 printf(内部会触发 write 系统调用)、sleep(触发 nanosleep 系统调用)或 getchar(触发 read 系统调用),进程会通过系统调用陷入核心态,由内核代为执行特权操作。进程的执行由操作系统的调度器根据时间片轮转和优先级等策略进行管理。例如,当 hello 调用 sleep(seconds) 时,它会进入阻塞态,放弃CPU,直到定时器到期;调用 getchar() 时,它也会阻塞等待键盘输入。每次从阻塞态唤醒或时间片到期后,hello 进程会进入就绪态,等待调度器再次分配CPU。进程上下文(包括寄存器值、程序计数器、栈指针、页表基址等)在发生进程切换或用户态/核心态转换时,会被内核保存和恢复,以确保进程可以从中断点正确继续执行。

6.6 hello的异常与信号处理

hello 进程在执行过程中可能遇到多种异常和信号。异常是CPU在执行指令时检测到的同步事件,如缺页故障(访问的内存页不在物理内存中,内核会尝试调入)、段错误(非法内存访问,如数组越界,通常导致 SIGSEGV 信号)、或除零错误等。信号则是异步通知进程发生某个事件的机制。用户可以通过键盘产生信号:按下 Ctrl-C 会向 hello 进程发送 SIGINT(中断)信号,其默认处理行为是终止进程。按下 Ctrl-Z 会发送 SIGTSTP(终端停止)信号,默认行为是暂停(挂起)进程,并将其置于后台。

图表 20ctrl+c终止进程示意

图表 21ps和jobs查看进程示意

图表 22fg运行示意

6.7本章小结

本章探讨了进程的基本概念、特征及其在操作系统中的作用。详细描述了Shell (Bash) 作为命令解释器如何通过 fork 和 execve 机制来创建和执行新进程(如 hello)。分析了 hello 进程在执行过程中用户态与核心态的转换、进程上下文、时间片轮转以及进程调度。最后,讨论了 hello 进程可能遇到的异常类型和常见的信号(如 SIGINT, SIGTSTP),并结合具体场景(如 Ctrl-C, Ctrl-Z 及后续的 ps, jobs, fg, kill 命令)分析了这些异常和信号的处理方式和对进程状态的影响。理解进程管理是掌握程序在操作系统中如何运行的关键。

7章 hello的存储管理

7.1 hello的存储器地址空间

当 hello 程序运行时,操作系统为其分配了一个独立的虚拟地址空间。这个空间是程序视角下的内存布局,包含了代码段(.text,存放 main 函数等指令,只读可执行)、数据段(.data 存放已初始化全局/静态变量,.bss 存放未初始化全局/静态变量,可读写)、堆(heap,用于 malloc 等动态内存分配,从数据段后向上增长)和栈(stack,用于函数调用、局部变量、参数传递,从高地址向下增长)。hello 程序中使用的所有地址,如变量 i 的地址、printf 等函数的地址、字符串常量的地址,都是这个虚拟地址空间内的虚拟地址。操作系统通过页表机制将这些虚拟地址映射到实际的物理内存或磁盘交换区,实现了进程间的内存隔离和保护,并为 hello 提供了一个看似连续且独占的内存环境。

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

在x86架构中,CPU首先通过段式管理将程序使用的逻辑地址(由段选择子和段内偏移组成)转换为线性地址。段选择子(如存储在 CS, DS, SS 等段寄存器中)用于在全局描述符表(GDT)或局部描述符表(LDT)中查找对应的段描述符,该描述符包含了段的基地址、界限和属性。线性地址由段基地址加上段内偏移计算得出。然而,现代64位Linux操作系统为用户态进程(如 hello)普遍采用扁平内存模型,将代码段和数据段的基地址设为0,段界限设得非常大,因此逻辑地址的偏移部分实际上就等同于线性地址。对于 hello 程序而言,它在用户态运行时,段式管理的影响被最小化,程序员和编译器主要关注的是线性(即虚拟)地址。

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

线性地址(虚拟地址)到物理内存地址的转换由CPU的内存管理单元(MMU)通过页式管理机制完成。x86-64 Linux系统采用4级页表结构(PML4, PDPT, PDT, PT)。当 hello 进程访问一个线性地址时,MMU会从CR3寄存器获取当前进程PML4表的物理基地址,然后利用线性地址的不同部分作为索引,逐级查询PML4表、PDPT表、PDT表和最终的页表(PT)。页表中的页表项(PTE)包含了虚拟页对应的物理页帧号以及访问权限等信息。MMU将物理页帧号与线性地址中的页内偏移组合,得到最终的物理地址,用于访问物理内存。这个多级页表机制为 hello 进程的每个虚拟页提供了到物理页帧的独立映射。

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

为了加速虚拟地址(VA)到物理地址(PA)的转换过程,避免每次都进行耗时的多级页表查询,CPU内部集成了一个转换后备缓冲器(Translation Lookaside Buffer, TLB)。TLB是一个小型的、高速的硬件缓存,存储了最近使用过的虚拟页号到物理页帧号的映射关系(即PTE的副本)。当 hello 进程访问一个虚拟地址时,MMU首先在TLB中查找该虚拟页的映射:若TLB命中,则直接从TLB获取物理页帧号,快速完成地址转换;若TLB未命中,则MMU必须执行一次完整的页表遍历(Page Walk),从CR3开始逐级查询四级页表,找到对应的PTE,然后将该PTE的内容加载到TLB中(可能会替换一个旧条目),再用此PTE完成地址转换。TLB的存在极大地提高了 hello 程序内存访问的平均效率。

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

图表 23三级缓存

在通过TLB和页表获得物理地址后,CPU访问物理内存的过程会受到多级CPU缓存(通常为L1, L2, L3 Cache)的影响。这些缓存位于CPU和主存之间,存储了最近从主存中读取的数据块的副本。当 hello 进程需要从某个物理地址读取数据或指令时,CPU会首先依次检查L1、L2、L3缓存:若在某一级缓存中命中,则直接从该级缓存获取数据,速度远快于访问主存。若所有缓存均未命中,则CPU才从主存读取数据,并将读取的数据块加载到各级缓存中,以备后续快速访问。hello 程序中频繁执行的指令(如循环体内的代码)和频繁访问的数据(如循环变量 i,如果未被优化到寄存器中;或传递给 printf 的字符串参数地址)很可能被缓存在CPU Cache中,从而显著提升其执行性能。

7.6 hello进程fork时的内存映射

以下格式自行编排,编辑时删除)当Shell为运行 hello 程序而调用 fork() 创建子进程时,内核会为子进程创建独立的虚拟地址空间和页表结构。但为了效率,并不会立即物理复制父进程(Shell)的所有内存页。取而代之的是采用“写时复制”(Copy-On-Write, COW)机制:子进程的页表项初始时指向与父进程相同的物理页帧,并将这些共享页帧标记为只读。只有当父进程或子进程中任何一方尝试写入这些共享页时,才会触发一个缺页异常,内核此时才为写入方创建该页的一个私有物理副本,并更新其页表。因此,在 fork() 之后,execve() 执行之前,hello 即将运行于其中的子进程在逻辑上拥有与Shell相同的内存映像(代码、数据、栈等),但在物理层面则尽可能地共享内存,直到需要修改时才发生实际复制。

7.7 hello进程execve时的内存映射

当 fork() 产生的子进程调用 execve("./hello", ...) 时,该子进程的内存映像将被 hello 可执行文件的内容完全替换。内核首先会解除子进程当前虚拟地址空间中原有的内存映射(从父进程继承来的段)。然后,内核依据 hello ELF文件的程序头部表,为 hello 程序建立全新的内存映射:代码段(.text等)被映射为只读可执行,数据段(.data, .bss)被映射为可读写(.bss段会被清零),并为新程序设置独立的堆和栈。如果 hello 是动态链接的,动态链接器也会被加载,并负责将所需的共享库(如 libc.so.6)映射到进程的地址空间。这个过程使得子进程的PID保持不变,但其执行的程序内容和内存布局完全变成了 hello 的,为 hello 的执行准备了全新的运行环境。

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

图表 24page-fault统计

在 hello 程序运行期间,如果它试图访问一个虚拟地址,而该地址对应的虚拟页当前不在物理内存中(例如,该页是首次被访问,或之前被换出到磁盘),或者访问权限不符(如写只读页),MMU会检测到这种情况并产生一个缺页故障(Page Fault)异常。CPU会立即将控制权转移到内核的缺页中断处理程序。内核会分析故障原因:如果是合法的访问但页不在内存,内核会负责从磁盘(可执行文件或交换区)将所需的页调入一个空闲的物理页帧,更新进程的页表项以反映新的映射关系和权限,然后返回用户态,重新执行引起故障的指令。如果是权限错误等非法访问,内核通常会向 hello 进程发送一个 SIGSEGV 信号,导致其终止。hello 程序在启动时,其代码和数据页通常就是通过这种“请求调页”的方式按需加载的。

7.9动态存储分配管理

虽然 hello.c 程序本身没有显式使用 malloc、free 等函数进行动态内存分配,但它调用的C标准库函数(如 printf 内部可能为格式化字符串或缓冲区分配内存)可能会使用。动态内存分配器(如glibc中的ptmalloc)负责管理进程堆区(Heap)中的空闲内存。当程序请求内存时,分配器会在堆中查找一个足够大的空闲块,可能进行分割,并返回指向分配区域的指针。当内存被释放时,分配器会将其标记为空闲,并可能与相邻的空闲块合并以减少外部碎片。如果堆中没有足够的连续空闲空间,分配器会通过 sbrk 或 mmap 系统调用向内核请求扩展堆区。这个过程对 hello 程序员是透明的,但其性能和行为(如内存碎片)会受到分配器策略的影响。

7.10本章小结

本章介绍了Hello程序的虚拟地址空间结构,以及现代x86-64系统下分段与分页的基本概念。说明了逻辑地址到线性地址的转换(段寄存器与偏移的结合),以及线性地址到物理地址的映射原理和TLB缓存的作用。讨论了Hello进程在fork()时的内存页复制策略和在execve()时的新映像加载,阐释了缺页中断的处理机制等。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux系统对I/O设备采取了高度抽象和统一的管理方式。其核心思想是将几乎所有I/O设备(如磁盘、终端、键盘、网络接口等)都模型化为文件,这一理念常被称为“一切皆文件”。这意味着用户程序可以使用一套统一的Unix I/O接口(如 open, read, write, close)与各种物理设备进行交互,而无需关心底层硬件的差异。设备文件通常位于 /dev 目录下,分为块设备(如硬盘,以数据块为单位访问)和字符设备(如终端、键盘,以字符流方式访问)。内核通过设备驱动程序来管理和控制具体的硬件设备,驱动程序负责翻译上层I/O请求为硬件操作,并处理硬件中断。

8.2 简述Unix IO接口及其函数

Unix I/O接口(也称低级I/O或系统调用I/O)是Linux内核提供给用户程序直接与文件和设备交互的一组核心函数。这些函数围绕文件描述符(一个小的非负整数,用于标识进程打开的文件或设备)进行操作。主要函数包括:open() 用于打开文件/设备并获取文件描述符;close() 用于关闭文件描述符;read() 用于从文件描述符读取数据到缓冲区;write() 用于将缓冲区数据写入文件描述符;lseek() 用于改变文件偏移量(不适用于字符设备);以及 stat()/fstat() 用于获取文件元数据。hello 程序中虽然直接使用的是C标准I/O库函数,但这些库函数(如 printf, getchar)的底层实现最终依赖于这些Unix I/O系统调用。

8.3 printf的实现分析

printf 函数是C标准库中用于格式化输出的核心函数。其实现大致流程为:首先,它解析格式字符串,识别普通字符和以 % 开头的格式说明符。对于普通字符,直接复制到内部缓冲区;对于格式说明符,它使用 <stdarg.h> 提供的宏从可变参数列表中获取对应参数,并根据说明符(如 %d, %s)将参数转换为相应的字符串表示,同时应用宽度、对齐等格式化选项。这个格式化过程通常由 vsprintf 或类似函数完成,将结果暂存到 stdio 库为 stdout(标准输出,文件描述符1)维护的一个内部缓冲区中。stdout 在连接到终端时通常是行缓冲的,这意味着当缓冲区满、遇到换行符 \n(如 hello 程序每次 printf 调用末尾都有),或显式调用 fflush 时,stdio 库才会调用底层的 write Unix I/O系统函数。write(1, buffer_content, num_bytes) 会通过系统调用(如 syscall 指令或旧的 int 0x80 陷阱)陷入内核,请求操作系统将缓冲区内容发送到与 stdout 关联的设备。

对于字符在屏幕上的显示,内核中的终端或显示驱动子程序接管。它将ASCII字符(或Unicode字符)通过查询字模库(Font library)转换为点阵图形(字模)。然后,这些字模对应的像素颜色信息(RGB值)被写入到显存(Video RAM, VRAM)的相应位置。显示芯片(集成在显卡或主板上)会以固定的刷新频率(如60Hz)周期性地逐行读取VRAM中的内容,并通过视频信号线(如HDMI)将每个像素的RGB分量数据传输给液晶显示器,最终在屏幕上呈现出字符。

8.4 getchar的实现分析

getchar 函数用于从标准输入 (stdin,文件描述符0)读取一个字符。它同样依赖于 stdio 库的缓冲机制。当调用 getchar 时,它首先检查 stdin 的内部缓冲区是否有未读数据。如果缓冲区为空,stdio 库会调用 read(0, internal_buffer, BUFFER_SIZE) 系统调用,尝试从内核读取一批数据到其内部缓冲区。当 stdin 连接到终端时,read 系统调用通常会阻塞,直到用户输入一行并按下回车键。

这个过程涉及到异步的键盘中断处理:当用户按下键盘上的键时,键盘控制器产生一个硬件中断。CPU暂停当前任务,转去执行内核中的键盘中断处理子程序。该子程序读取按键的扫描码,将其转换为ASCII码(或Unicode码),并通常在终端驱动的行编辑模式下,将字符存入内核的行缓冲区。用户可以使用退格等键编辑此行。直到用户按下回车键,内核的行缓冲区中的整行数据(包括换行符)才被认为是“完整”的,并使得阻塞在 read 系统调用上的 hello 进程被唤醒。read 调用随后从内核行缓冲区复制数据到 stdio 的内部缓冲区并返回。getchar 最终从 stdio 缓冲区中取出并返回第一个字符。后续对 getchar 的调用会继续从缓冲区消耗字符,直到缓冲区再次为空。

8.5本章小结

本章探讨了 hello 程序在Linux环境下进行I/O操作的底层机制。Linux通过将设备抽象为文件,并利用Unix I/O接口(如read和write)提供统一的设备访问方式。printf 函数通过内部缓冲、格式化处理(依赖vsprintf),最终调用write系统调用将字符数据显示到屏幕,这一过程涉及字符到字模的转换及显存的刷新。getchar 函数同样利用缓冲,通过read系统调用从键盘获取输入,其阻塞和返回依赖于内核对键盘中断的处理和行缓冲机制。这些分析揭示了即便是简单的输入输出操作,背后也依赖于操作系统内核、设备驱动以及标准库之间复杂的协同工作。

结论

本文以hello.c程序为实例,全面剖析了其从源代码到运行结束的全过程。报告首先概述了hello的功能及其P2P与020生命周期,随后详细阐述了预处理、编译、汇编及链接四个阶段的转换机制与ELF文件格式。接着,深入探讨了Shell通过fork与execve创建并加载hello进程的过程,以及程序从_start到main再到终止的完整执行流。内存管理方面,重点分析了hello的虚拟地址空间布局、段页式地址转换、fork/execve时的内存映射及缺页处理。I/O管理部分则阐释了Linux设备模型、Unix I/O接口,并剖析了printf/getchar的实现。此外,还分析了进程对Ctrl-C/Ctrl-Z等信号的响应与作业控制。

通过对hello程序生命周期的系统性追踪,本报告不仅揭示了C程序在Linux下从代码到执行的完整链路,也深化了对操作系统核心概念的理解,展现了计算机系统中编译、链接、加载及运行各环节的精密协同。

附件

在本实验过程中,我们获取了多个中间文件来辅助理解程序的构建过程。以下对这些文件及其作用进行总结:

  1. hello.c源代码文件,包含用 C 语言编写的程序源码。开发者编写和阅读的主要对象。
  2. hello.i预处理文件。由预处理器将 hello.c 展开头文件、宏后生成的纯C源码。在hello.i中可以看到所有包含的标准库声明。它用于检查预处理效果,作为编译输入。
  3. hello.s汇编代码文件。编译器将C代码翻译成x86-64汇编指令后输出的文本文件。通过阅读hello.s,可以分析编译器生成的指令、寄存器使用以及外部符号引用情况。
  4. hello.o目标文件(可重定位目标)。汇编器将hello.s转化为机器码并打包成ELF格式的输出。hello.o包含了二进制指令、数据,以及符号表和重定位信息。在链接前,它不能独立运行,但可以用工具反汇编以验证机器码。
  5. hello(可执行文件):最终链接生成的64位可执行 ELF 文件。它将hello.o与所需的库进行链接后产生,包含了程序入口、加载信息和动态链接信息。该文件可由操作系统加载执行。使用./hello即可运行程序。

参考文献

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

[2] Chuxiong. 程序详细编译过程(预处理、编译、汇编、链接)[EB/OL]. 知乎专栏,(2025-05-11)[2025-05-11].

[3] Amdreameng. 2025 HIT 程序人生-Hello’s P2P[EB/OL]. CSDN博客,(2025-05-11)[2025-05-11].

[4] 申旭弘. ICS2019大作业 程序人生-Hello's P2P(对Hello程序生命周期的分析)[EB/OL]. 博客园,(2025-05-11)[2025-05-11].

[6] pwl999. Linux mem 1.1 用户态进程空间的创建 --- execve() 详解[EB/OL]. 博客园,(2025-05-11)[2025-05-11].

[7] weixin_30908649. 浅析线性地址到物理地址的转换[EB/OL]. CSDN博客,(2025-05-11)[2025-05-11].

[8] wuxiao. 浅析 Linux 中的 I/O 管理(Linux设备及文件的介绍)[EB/OL]. 知乎,(2025-05-11)[2025-05-11].

[5] Linux Manual Page. fork(2), execve(2), wait(2) – Linux系统调用手册(进程创建与回收)[Z]. (2025-05-11)[2025-05-11].

[9] GNU Toolchain Documentation. GCC, Objdump, Readelf 使用手册(用于分析编译产物)[Z]. (2025-05-11)[2025-05-11].

[10] 《Linux设备驱动》第3版[M]. 第3章 “字符设备驱动”(解释终端等字符设备的原理). (2025-05-11)[2025-05-11].

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值