哈工大CSAPP期末大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业          医学类1     

学     号       2023112506      

班     级        2352001      

学       生        zqy        

指 导 教 师         刘松波         

计算机科学与技术学院

2025年5月

摘  要

摘要:本文以标准C程序hello.c的编译执行过程为研究对象,系统分析源代码到可执行文件的转化机制。在预处理阶段,预处理器执行#include <stdio.h>指令,递归展开头文件内容,完成宏替换与条件编译,生成包含完整类型声明及库函数原型的hello.i中间文件。编译阶段,编译器对预处理代码进行词法/语法分析,构建抽象语法树(AST),经中间代码生成、指令选择及寄存器分配等步骤,输出针对目标架构优化后的汇编文件hello.s,其包含movq、call等低级指令及.cfi调试信息。

汇编阶段通过汇编器将助记符转换为机器码,生成包含.text代码段、.data数据段及重定位表的ELF格式目标文件hello.o。链接阶段由链接器解析printf等未定义符号,合并多个目标文件的符号表,完成地址重定位并绑定动态链接库,最终生成可执行文件a.out。程序执行时,操作系统通过execve系统调用加载ELF文件,构建虚拟内存空间并初始化栈帧,由CPU指令流水线逐级执行,直至通过_start入口调用main函数完成输出操作。本文结合GCC工具链的预处理日志、汇编代码及objdump反编译结果,实证各阶段的数据结构变化,揭示程序在编译系统与操作系统协同工作机制下的完整生命周期。

关键词:计算机系统;计算机体系结构;汇编语言;链接                            

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

目  录

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

P2PProgram to Process):从程序到进程的蜕变

核心:静态代码 → 动态执行的进程,依赖编译工具链与操作系统协同。

1. 编程与编译阶段

程序编写:

  • 用户编写hello.c(文本文件),包含程序逻辑(如printf("Hello"))。

预处理(Preprocess):

  • 宏展开、头文件插入(如#include <stdio.h>被替换为库函数声明)。
  • 生成hello.i(纯C代码)。

编译(Compile):

  • 编译器(gcc -S)将C代码翻译为汇编代码(hello.s),包含平台相关指令(如x86或ARM)。

汇编(Assemble):

  • 汇编器(as)将汇编代码转换为机器指令(二进制目标文件hello.o),包含符号表、重定位信息

链接(Link):

  • 链接器(ld)合并hello.o与库文件(如libc.so),解析符号引用(如printf地址),生成可执行文件hello。

2. 进程创建阶段

Shell调用:

  • 用户在终端输入./hello,Shell解析命令并启动进程。

fork():

  • Shell调用fork()创建子进程,复制父进程(Shell)的上下文(代码、数据、堆栈)。

execve():

  • 子进程调用execve()加载hello可执行文件,替换原有内存映像:

代码段:加载hello的机器指令。

数据段:初始化全局变量、静态变量。

栈:设置argc、argv、环境变量指针。

跳转到程序入口点(如_start,最终调用main)。

OS资源分配:

  • 分配进程控制块(PCB),记录进程状态、优先级、资源句柄(文件描述符、内存映射等)。

3. 进程执行阶段

CPU调度:

  • OS将进程加入就绪队列,分配时间片,通过上下文切换实现多任务。
  • CPU流水线执行指令(取指、译码、执行、访存、写回)。

内存管理:

虚拟内存(VA)→ 物理内存(PA):

  • MMU通过四级页表(PGD→PUD→PMD→PTE)逐级查询页表项,结合TLB加速地址转换。
  • 缺页处理:若页表项不存在,触发缺页中断,OS从磁盘(Pagefile)加载页面到物理内存。
  • 缓存优化:L1/L2/L3 Cache缓存热点数据,减少内存访问延迟。

I/O与信号:

printf触发系统调用(write),OS内核处理数据从用户缓冲区→页缓存→显存的全流程。若用户按下Ctrl+C,OS发送SIGINT信号,进程调用注册的信号处理函数或默认终止。

O2OZero-0 to Zero-0):从启动到消亡的闭环

核心:进程从无到有、再从有到无,资源全生命周期管理。

1. 进程启动(ZeroActive

初始状态:进程启动时,内存、寄存器、文件描述符等资源均为“空白”。

资源分配:

  • OS分配虚拟地址空间、打开标准输入/输出/错误流(stdin/stdout/stderr)。
  • 动态内存通过malloc/free从堆区分配。

2. 进程运行(Active

动态行为:

  • 执行指令,修改寄存器与内存状态(如计算、跳转、系统调用)。
  • 可能创建线程、打开文件、申请内存等,资源使用动态增长。

3. 进程终止(ActiveZero

终止方式:

  • 正常结束:main返回或调用exit()。
  • 异常终止:收到SIGSEGV(段错误)、SIGKILL等信号。

资源回收:

  • 内存释放:OS回收进程占用的所有物理内存和虚拟地址空间。
  • 文件关闭:释放打开的文件描述符,刷新缓冲区数据到磁盘。
  • 进程状态清理:父进程通过wait()读取子进程退出状态,清除僵尸进程(Zombie)的残留信息。

系统痕迹清除:CPU寄存器、Cache、TLB等硬件状态被后续进程覆盖,不留痕迹。仅保留磁盘上的可执行文件hello,体现“赤条条来去无牵挂”。

1.2 环境与工具

硬件环境:

处理器:AMD Ryzen 9 7940H w/ Radeon 780M Graphics      4.00 GHz

机带RAM:16.0GB

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

软件环境:Windows11 64位 ,VMware,Ubuntu 20.04

开发与调试工具:Visual Studio 1.89.0;vim,gidit ,objdump,edb,gcc,readelf等开发工具

1.3 中间结果

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

hello.c:原始hello程序的C语言代码

hello.i:预处理过后的hello代码

hello.s:由预处理代码生成的汇编代码

hello.o:二进制目标代码

hello:进行链接后的可执行程序

hello_asm.txt:反汇编hello.o得到的反汇编文件

hello1_asm.txt:反汇编hello可执行文件得到的反汇编文件

1.4 本章小结

本章以“Hello”程序为例,系统解析其从静态代码到动态进程的完整生命周期(P2P)及资源从零分配至零回收的闭环管理(O2O):在P2P流程中,用户编写的hello.c代码依次经历预处理(生成宏展开后的hello.i)、编译(转换为平台相关的汇编代码hello.s)、汇编(生成包含机器指令与符号表的可重定位目标文件hello.o)和链接(整合库函数生成可执行文件hello),随后Shell通过fork()创建子进程并调用execve()将可执行文件加载至内存,操作系统为其分配进程控制块(PCB)、虚拟地址空间及硬件资源,最终在CPU调度与内存管理(MMU、TLB、多级页表与缓存)的协同下完成指令执行;而O2O流程则贯穿进程从启动到终止的全过程,初始时操作系统为其分配内存、文件句柄等资源,运行中通过系统调用与信号机制实现I/O交互,最终进程通过exit()或信号终止后,操作系统彻底释放其占用的物理内存、关闭文件描述符、清除寄存器与缓存状态,仅保留磁盘上的可执行文件,实现“零残留”的资源回收。

第2章 预处理

2.1 预处理的概念与作用

预处理是C/C++编译过程的第一步,它由预处理器(preprocessor)完成。预处理器是一种独立于编译器的工具,专门负责处理源代码中的特殊指令(称为预处理指令)。这些指令以 # 开头,通常位于代码文件的顶部或特定位置。

预处理器的主要职责是对源代码进行一系列文本级别的操作,包括但不限于替换宏定义、引入头文件、条件编译、删除注释。

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

在预处理阶段,原始代码中的注释被完全剥离,而源文件首部通过#include <stdio.h>指令引入的头文件内容被完整展开——原本简单的预处理指令被逐层解析替换,最终生成的hello.i文件体量激增至3000余行。该文件不仅完整包含了标准输入输出库stdio.h中声明的函数原型、宏定义以及结构体声明,还将源代码中使用的宏逐级展开为底层实现,同时保留了编译器添加的行号标记用于后续错误定位,并通过条件编译指令维持平台相关的代码逻辑分支,形成既剥离注释又高度展开的中间形态。

2.4 本章小结

这一章讲的是在Linux系统里怎么给C语言程序做预处理。比如我们用gcc -E hello.c -o hello.i这个命令,就能把hello.c文件处理成一个叫hello.i的新文件。这个预处理过程其实就像“给代码做大扫除”:

删掉所有注释——不管是//还是/* */,统统清理干净,让代码变简洁。

展开头文件——比如你写的#include <stdio.h>,预处理会把stdio.h这个文件里的内容(比如printf函数的声明、FILE结构体的定义、BUFSIZ这种常量)全部复制到hello.i里,所以hello.i可能会突然变成几千行的“大文件”。

处理宏和条件编译——比如你定义的#define MAX 100,预处理会把代码里所有MAX直接替换成100;#ifdef这种指令会根据系统环境决定保留哪些代码(比如区分Windows和Linux的代码)。

加行号标记——编译器会在hello.i里插入类似# 3 "hello.c"的标记,这样如果后面编译出错,它能告诉你错误在原来hello.c的第几行。

预处理就像“给代码打地基”,把代码整理干净、补充完整,后面的编译、链接步骤才能顺利进行。自己动手用gcc -E试试看,打开hello.i文件观察这些变化。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译是程序设计语言向机器可执行代码转化的核心环节,其本质是通过编译器对高级语言源代码进行多阶段的静态分析与转换。编译过程以形式语言理论为基础,将符合上下文无关文法的源代码逐步降级为底层机器指令,同时确保语义等价性。

作用:

词法分析与语法分析,编译器前端通过有限自动机实现词法分析,将字符流转换为词素序列;随后基于上下文无关文法构建抽象语法树,检测括号匹配、语句分隔符等语法错误。

语义分析与中间表示生成,在语法树基础上进行类型检查、作用域分析等静态语义验证,确保变量声明先于使用、函数参数匹配等约束。随后生成中间表示,如三地址代码或LLVM IR,实现与目标平台无关的逻辑表达。

代码优化与目标代码生成,编译器后端对IR进行数据流分析,实施常量传播、死代码消除、循环优化等变换以提升执行效率。最终通过指令选择、寄存器分配、指令调度等步骤,将IR映射为特定指令集架构(如x86-64、ARM)的汇编代码,完成从高级抽象到底层硬件的语义桥接。

符号解析与重定位支持,编译阶段生成的目标文件(.o)包含符号表(Symbol Table)记录函数/变量地址引用,配合重定位条目(Relocation Entry)为后续链接器(Linker)提供跨模块地址解析依据。

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1Hello.s汇编代码展示

.file       声明源文件(此处为hello.c)

.text      声明代码节

.section 文件代码段

.rodata  Read-only只读文件

.align    数据指令地址对齐方式(此处为8对齐)

.string   声明字符串(此处声明了LC0和LC1)

.globl    声明全局变量

.type     声明变量类型(此处声明为函数类型)

各种数据类型的大小一级寄存器的结构如下所示:

3.3.2对局部变量的操作

在汇编语言中,局部变量的存储与生命周期管理依赖于栈内存机制。当程序执行流进入主函数main时,编译器通过操作栈指针寄存器(RSP)动态分配栈帧空间,如下图所示执行subq $32, %rsp指令将栈指针向低地址方向移动32字节(对应x86-64架构的栈增长方向),从而为局部变量i预留连续内存区域。该栈帧空间通过基址寻址方式访问,如使用帧指针寄存器(RBP)结合偏移量(-4(%rbp))或直接基于RSP的偏移量实现对变量i的读写操作(例如movl $0, -4(%rbp)完成初始化)。局部变量的内存分配由编译器静态计算确定,严格遵循函数调用周期——函数返回前通过addq $32, %rspleave指令复位栈指针,逻辑上释放占用的32字节栈空间(物理内存内容未被擦除,仅标记为可复用)。这一机制实现了局部变量的自动生命周期管理,依托硬件级寄存器操作保障高速访问效率,同时通过编译期静态空间分配规避运行时内存碎片,体现了栈内存“后进先出”特性与过程化编程范式的深度契合。

3.3.3对字符串常量的操作

在rdata段,LC0和LC1存储字符串

在主函数中使用字符串利用movl指令操作

3.3.4赋值操作

用movl指令进行赋值操作,把立即数1放在edi寄存器里存储

3.3.5参数传递——对main的参数argv的传递

由于主函数中会用到数组,在栈中申请到32字节的空间(栈指针减少32位)后,分别将%rdi和%rsi的值存入栈中,%rbp-20和%rbp-32的位置分别存了argv数组和argc的值。

3.3.6对数组的操作

对数组的操作是通过首地址加上偏移量实现的。在 main 中调用 argv[1] 和 argv[2] 时,汇编代码将 %rbp-32 的值(数组首地址)赋给 %rax,然后分别加上偏移量 24 和 16 得到 argv[1] 和 argv[2],存入 %rsi 和 %rdx 作为第二个和第三个参数,随后传递给 printf 函数。调用完 printf 后,在偏移量为 32 处获取 argv[3],存入 %rdi 作为第一个参数传递给 atoi 函数。

3.3.7for循环

在汇编代码中,首先通过以下指令将数组 argv 的首地址加载到 %rax 寄存器中:

 movq -32(%rbp), %rax  ; 将存储在 -32(%rbp) 的变量值(即数组首地址)加载到 %rax 寄存器中。

接下来,为了获取argv[1]的地址,将 %rax寄存器中的值增加 24:

 addq $24, %rax; 将 %rax 寄存器中的值增加 24,用于定位 argv[1] 的地址。

然后,将 argv[1] 的值加载到 %rcx 寄存器中:

    movq (%rax), %rcx ; 将地址为 %rax 的内存值加载到 %rcx 寄存器中,即 argv[1]。

为了获取 argv[2] 的值,重复类似的步骤:先重新加载数组的首地址到 %rax 寄存器,然后增加偏移量以定位 argv[2] 的地址,并将其值加载到 %rdx 寄存器中:

    movq -32(%rbp), %rax  ; 重新加载数组首地址到 %rax 寄存器。

    addq $16, %rax        ; 增加 %rax 中的值以定位 argv[2] 的地址。

    movq (%rax), %rdx     ; 将地址为 %rax 的内存值加载到 %rdx 寄存器中,即 argv[2]。

类似地,程序继续获取 argv[3] 的值,并将其存储到 %rsi 寄存器中。

完成上述操作后,程序进入一个 for 循环。循环计数器存储在栈上的 -4(%rbp) 中,每次迭代将计数器加 1:

    addl $1, -4(%rbp)    ; 将循环计数器加 1。

    jmp .L4             ; 跳转到 .L4 标记处,继续执行循环体。

循环体重复执行,直到循环条件不再满足(i < 10)

3.4 本章小结

这一章详细地介绍了C编译器如何把hello.c文件转换成hello.s文件的过程,简要说明了编译的含义和功能,演示了编译的指令,并通过分析生成的hello.s文件中的汇编代码,探讨了数据处理,函数调用,赋值、算术、关系等运算以及控制跳转、类型转换等方面,比较了源代码和汇编代码分别是怎样实现这些操作的。

        

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念:汇编(Assembling)是程序构建过程中的关键阶段,指将汇编语言源代码(.s或.asm文件)转换为可重定位目标文件(.o文件)的过程,其本质是通过汇编器(Assembler)实现符号化指令到机器码的精确映射。该过程在编译链条中承上启下,既承接编译器生成的中间汇编代码,又为链接器提供可进一步处理的二进制对象模块。

作用:计算机只能识别处理机器指令程序,汇编过程将汇编语言程序翻译为了机器指令,进一步向计算机能够执行操作的形式迈进,便于计算机直接进行分析处理。简单的来说,汇编之后我们能从汇编代码得到一个可重定位目标文件,以便后续进行链接。

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.3.1 elf头部

  

elf各section

重定向(rela.text)

该section中包含了需要被重定位的变量的偏置值,在链接时会根据偏置值将局部变量重定向到对应已声明变量,将已声明变量对应到对应内存地址

4.4 Hello.o的结果解析

汇编语言是对机器语言的一种符号化表示,通过助记符(如 MOV, ADD 等)来代替复杂的二进制指令。以下是两者之间的主要映射关系:

  1. 操作码映射:汇编语言中的助记符(如 MOV)直接对应于机器语言中的操作码。例如,MOV RAX, RBX 对应于机器语言中的 B8 XX XX XX XX
  2. 操作数映射:汇编语言的操作数通常以寄存器名称(如 RAX, RBX)、立即数(如 1234)或内存地址(如 [RBP+8])的形式表示。在机器语言中,这些操作数会被转换为具体的二进制编码。例如,RAX 可能对应于二进制 00000001,而 RBX 对应于 00000010。
  3. 地址信息映射:汇编语言中的地址通常是符号化的,例如 .L1, .LL3 等标签。在机器语言中,这些符号会被替换为其对应的内存地址。例如,jmp .L1 中的 .L1 会被替换为实际的内存地址。

4.5 本章小结

本章通过分析hello.o的elf结构和反汇编代码剖析了一个可执行程序的基本组成结构,对比分析了机器语言与汇编代码的异同。

(第41分)

第5章 链接

5.1 链接的概念与作用

概念:链接(Linking)是程序构建的核心阶段之一,负责将多个可重定位目标文件.o文件)和静态库.a文件)合并为单一可执行文件(如ELF格式)或共享库(.so/.dll),同时解析跨模块的符号引用关系。其本质是通过链接器(Linker实现地址空间整合与符号绑定,使分散编译的代码模块能够协同工作。

作用:链接可以将程序调用的各种静态链接库和动态连接库整合到一起,完善重定位目录,使之成为一个可运行的程序。同时,链接的主要作用就是使得分离编译成为可能,从而不需要将一个大型的应用程序组织成一个巨大的源文件,而是可以将其分解成更小的、更好管理的模块,可以独立的修改和编译这些模块。

5.2 在Ubuntu下链接的命令

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

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

5.4 hello的虚拟地址空间

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

 通过edb调试可以看出该程序地址从0x401000开始,我们可以通过elf的每个section的地址在Data Dump中找到相应数据。

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

可以看出,重定向后的hello.out比hello.o的反汇编左侧多了一列虚拟地址序号VPN,因为经过重定向每条指令的VA-PA映射关系已经确定。

同时经过链接该文件比hello.o多了许多库函数的代码如exit()等,因为许多调用的外部函数以可重定向文件的格式存在,在链接这步加入了这些文件。

hello的反汇编

hello.o的反汇编

我从中发现他们虚拟地址不同、反汇编节数不同、跳转指令不同

  1. 虚拟地址不同,hello.o的反汇编代码虚拟地址从0开始,而hello的反汇编代码虚拟地址从0x400000开始。这是因为hello.o在链接之前只能给出相对地址,而hello在链接之后得到的是绝对地址。
  2. 反汇编节数不同,hello.o只有.text节,里面只有main函数的反汇编代码。而hello在main函数之前加上了链接过程中重定位而加入的各种在hello中被调用的函数、数据,增加了.init,.plt,.plt.sec等节的反汇编代码。

3.跳转指令不同,hello.o中的跳转指令后加的主要是汇编代码块前的标号,而hello中的跳转指令后加的则是具体的地址,但相对地址没有发生变化。

5.6 hello的执行流程

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

使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。

程序初始停在地址0x7fa4917dcea0,这里是动态链接库ld-2.2.27.so的入口点_dl_start,随后跳转至_dl_init完成一系列初始化工作后到达hello程序的入口点_start,接着通过call指令跳转到动态链接库ld-2.27.so的_libc_start_main函数,此函数执行必要的初始化操作并负责调用main函数,之后程序调用动态链接库中的__cxa_atexit函数设置程序结束时需调用的函数表然后返回到__libc_start_main继续,接着调用由静态库引入的__libc_csu_init函数执行初始化工作,再返回到__libc_start_main继续,随后调用动态链接库里的_setjmp函数设置非本地跳转,然后返回到__libc_start_main继续,正式开始调用main函数,由于我们在edb中运行hello时未提供额外的命令行参数,因此在第一个if处通过exit(1)直接结束程序,程序跳转至hello自带的exit函数,经过若干操作后程序最终退出。

执行顺序:_start->init->call_init->main

5.7 Hello的动态链接分析

.got段

程序调用一个有共享库定义的函数时,编译器无法预测函数在运行时的具体地址,因为定义这个函数的共享模块可能可以被加载到任何位置。因此,编译系统采用延迟绑定,将过程地址的绑定推迟到第一次调用该过程的时候。

延迟绑定需要用到两个数据结构:GOT(Global Offset Table,全局偏移表)和PLT(Procedure Linkage Table,过程链接表)。

.plt:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

.got:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目

程序调用共享库函数时,地址在运行时动态确定,因此采用延迟绑定机制:

1.首次调用函数

跳转到对应的PLT条目,PLT通过GOT中的初始地址(指向PLT内部)将控制权交还给自身。

PLT压入函数ID,跳转到动态链接器(通过GOT[2]),由链接器解析函数真实地址并更新GOT。

2.后续调用

PLT直接通过GOT中已更新的真实地址跳转,无需动态解析。

重定位差异

加载前:编译时仅标记需要动态重定位的GOT位置。

加载后:动态链接器根据共享库实际加载地址,填充GOT条目,完成地址绑定。

5.8 本章小结

本章节简要介绍了链接的相关过程,首先简要阐述了链接的概念和作用,给出了链接在Ubuntu系统下的指令。之后研究了可执行目标文件hello的ELF格式,并通过edb调试工具查看了虚拟地址空间和几个节的内容,之后依据重定位条目分析了重定位的过程,并借助edb调试工具,研究了程序中各个子程序的执行流程,最后则借助edb调试工具通过对虚拟内存的查取,分析研究了动态链接的过程。通过链接,hello.o与它依赖的所有的库结合在一起形成了一个可执行文件,在这个可执行文件中,所有的运行时位置都已经确定,可以被复制到内存里并运行了。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

概念: 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础

作用: 在运行一个进程时,我们的这个程序好像是系统当中唯一一个运行的程序,进程的作用就是提供给程序两个关键的抽象。一分别是独立的逻辑控制流和私有的地址空间

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

Shell作为用户与操作系统内核间的核心接口,承担命令解释器的核心角色,以用户态进程身份实现用户空间与内核空间的交互中介。其核心价值体现在:1)提供交互式命令行界面(CLI)实现人机指令传递;2)通过封装系统调用(fork/execve)管理进程生命周期;3)实施权限控制(基于UID/GID)保障系统安全;4)支持脚本编程实现自动化任务编排。

Shell命令处理流程

1.词法解析与参数扩展

输入命令经分词解析后,执行环境变量替换($VAR)、通配符扩展(*)、命令替换($(cmd))等预处理操作,生成最终参数列表。

2.命令类型判别

内置命令(Built-in):若为cd、export等Shell原生指令,直接调用内部函数执行。

外部命令:对于非内置命令,进入可执行文件搜索流程。

3.可执行文件解析

利用哈希表优化PATH环境变量目录检索,定位目标可执行文件绝对路径。

校验文件权限(x执行位)及格式(ELF/脚本解释器声明)。

4.进程创建与执行

fork():复制Shell进程生成子进程。

execve():子进程加载目标程序代码段,替换内存映像。

waitpid():父进程挂起直至子进程终止,捕获退出状态码。

5.异常处理与反馈

错误码映射:若文件未找到返回127,权限不足返回126。

信号处理:拦截SIGINT(Ctrl+C)等信号,终止子进程并清理资源。

6.扩展功能集成

I/O重定向:通过dup2系统调用绑定文件描述符(如> file)。

管道(Pipe):串联多个进程的输入输出,通过匿名管道传递数据。

6.3 Hello的fork进程创建过程

Shell调用fork创建子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,因此fork后子进程可以读写父进程中打开的任意文件。父进程和创建的子进程最大的区别在于其PID不同。

fork会被父进程调用一次,返回两次,父进程与创建的子进程并发执行。执行hello时,fork后的进程在前台执行,因此创建它的父进程shell暂时挂起等待hello进程执行完毕。

6.4 Hello的execve过程

当你在终端中输入 ./hello 并按下回车时,shell(如 Bash)会调用 execve 系统, 控制权从用户空间转移到内核空间, 调用execve会将这个进程执行的原本的程序完全替换,它会删除已存在的用户区域,包括数据和代码;然后,映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的;之后映射共享区;最后把控制传递给当前的进程的程序入口

6.5 Hello的进程执行

6.5.1逻辑控制流(Logical Control Flow

程序执行体现为程序计数器(PC的连续变化轨迹,PC值序列严格对应可执行文件或动态链接库中的指令流。该逻辑流在进程维度表现为顺序指令执行,在并发维度则体现为多任务交替执行的抽象连续性。

6.5.2 时间分片(Time Slicing)与并发

时间分片机制:单核CPU通过时间片轮转实现伪并行,每个进程获得固定时长(通常毫秒级)的CPU使用权,超时后被抢占(Preemption),切换至就绪队列中的下一进程。

并发流(Concurrent Flow:多个逻辑流执行时间存在重叠的现象,宏观表现为“同时运行”,微观实则为时间片快速轮转。

多任务(Multitasking:通过时间分片实现的进程交替执行机制,核心依赖调度器(Scheduler)的优先级与公平性策略。

6.5.3.用户模式(User Mode)与内核模式(Kernel Mode

权限隔离

用户模式:受限执行环境,禁止直接访问硬件或敏感指令(如I/O操作),通过系统调用(Syscall)请求内核服务。

内核模式:全权限执行环境,可访问全部CPU指令集与物理地址空间,处理中断、异常及资源管理。

模式切换触发条件

用户→内核:系统调用(主动)、中断(如时钟中断)、异常(如缺页异常)。

内核→用户:异常处理程序返回(iret指令),恢复用户态进程执行。

稳定性保障:内核模式禁止缺页异常,确保关键代码(如中断处理)无阻塞执行。

6.5.4 进程上下文切换(Context Switch

上下文定义:进程恢复执行所需的完整状态快照,包含:

硬件上下文:通用寄存器、浮点寄存器、PC、状态寄存器(EFLAGS)。

系统资源:虚拟内存映射表(CR3)、内核栈、打开文件描述符表、信号处理表。

切换流程

    1. 抢占触发:时钟中断、系统调用阻塞或更高优先级进程就绪。
    2. 保存现场:将当前进程的寄存器状态压入内核栈。
    3. 调度决策:运行调度算法选择下一个执行进程。
    4. 加载上下文:从目标进程的任务状态段(TSS)恢复寄存器与内存映射。
    5. 控制转移:更新PC指向目标进程的恢复点,切换CR3寄存器更新地址空间。

6.6 hello的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

 程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

6.6.1Ctrl-C

直接终止前台作业

6.6.2Ctrl-Z

Ctrl+Z的功能是向进程发送SIGSTP信号,进程接收到该信号之后会将该作业挂起,但不会回收

6.6.3回车

不影响程序运行,但是回车不仅在printf时显示,在hello执行完毕后,回车同样进行了多次

6.6.4乱按

6.7本章小结

本章概述了hello进程大致的执行过程,阐述了进程、shell、fork、execve等相关概念,之后从逻辑控制流、时间分片、用户模式/内核模式、上下文切换等角度详细分析了进程的执行过程。并在运行时尝试了不同形式的命令和异常,每种信号都有不同处理机制,针对不同的shell命令,hello会产生不同响应。

(第62分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

物理地址

这就是内存条上实实在在的"门牌号",相当于你家在现实中的街道地址。CPU通过电路上的物理线路(比如内存总线)直接找到内存条上的具体位置。就像快递员必须知道真实地址才能送货,CPU必须用物理地址才能读写内存数据。

逻辑地址

程序编译后自带的"理想化地址",相当于你写在信封上的收件人信息。它由两段组成:段选择符(好比省份)和偏移量(好比街道门牌)。在早期的实模式(比如DOS时代)下,这个地址直接对应物理位置;现代的保护模式则需要通过段机制转换。

线性地址

这是逻辑地址经过"段式管理"转换后的中间形态。可以理解为快递分拣中心生成的中转地址。比如你用gdb调试程序时看到的地址就是这个阶段的产物。在没开分页功能时,这个地址直接对应物理地址;开启分页后还要进一步转换。

虚拟地址

程序眼中的"虚拟世界地址",相当于网购时填写的收货地址(不需要知道真实仓库在哪)。操作系统通过分页机制把这个地址映射到物理内存或硬盘交换空间。比如32位系统的虚拟地址范围是0x00000000到0xFFFFFFFF(4GB空间),但实际上可能只有8G物理内存加16G交换文件在背后支撑。

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

在Intel平台中,逻辑地址的格式由两部分组成:段标识符和段内偏移量。段标识符是一个16位的字段,称为段选择符。其中,前13位是一个索引号,用于在全局描述符表(GDT)或局部描述符表(LDT)中定位特定的段描述符;后3位则包含一些硬件相关的细节,用于进一步描述段的状态或属性。

分段机制通过以下步骤将逻辑地址转换为线性地址:

1.  定位段描述符:使用段选择符中的索引号在GDT或LDT中查找对应的段描述符。

2.  验证访问权限:检查段描述符中的访问权限字段,确保当前段可以被访问,并确认其范围是否合法。

3.  计算线性地址:将段描述符中的段基址与逻辑地址中的段内偏移量相加,生成最终的线性地址。

通过这一过程,分段机制实现了从逻辑地址到线性地址的转换,为后续的分页机制提供了基础支持。

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

线性地址(Virtual Address, VA)到物理地址(Physical Address, PA)的转换通过分页机制完成。分页机制类似于主存与缓存(Cache)之间的分块机制,它将虚拟地址空间和物理内存空间按照固定大小(通常为4KB到2MB,具体大小随硬件架构不同而变化)进行划分。在现代x86-64架构中,虚拟地址空间的大小为 (2^{48}) 字节,即256TB,远远超过普通硬盘的容量。

在分页机制中,每个硬盘空间的字节都与虚拟地址空间的字节一一对应(即单射关系)。虚拟地址空间和硬盘空间均以字节为单位,从0开始编号。假设硬盘空间为 (H),虚拟地址空间为 (V),它们之间的映射关系为单射函数 (f)。因此,如果我们知道物理地址中某一页与虚拟地址空间某一页的对应关系,就可以推导出该页与硬盘中某一页的对应关系。

为了记录物理地址中某一页与虚拟地址空间某一页之间的对应关系,分页机制引入了一种称为页表(Page Table)的数据结构。页表存储在内存中,由操作系统负责管理和维护。类似地,在DRAM与Cache之间的高速缓存机制中也采用了类似的映射方式,但这种映射通常由硬件直接实现。

每个进程都有自己的页表,页表中的每一项被称为页表条目(Page Table Entry, PTE)。PTE 包含以下关键信息:

  1. 有效性标志:指示该虚拟地址空间的某一页是否已经映射到物理内存中的某一页。
  2. 物理页的起始位置:指向物理内存中实际分配给该页的起始地址。
  3. 磁盘地址:当页尚未加载到物理内存时,指向磁盘中的起始地址。
  4. 访问权限:控制对该页的读写权限。

根据不同的映射状态,PTE 可以被划分为以下三种主要状态:

  1. 未分配状态:表示该虚拟地址空间的页尚未被分配任何物理资源(既不在物理内存中,也不在磁盘中)。
  2. 未缓存状态:表示该页虽然已经分配了物理内存,但尚未被加载到缓存中。
  3. 已缓存状态:表示该页已经被加载到缓存中,可以直接快速访问

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

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

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

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

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

7.6 hello进程fork时的内存映射

当Shell通过`fork`系统调用创建子进程时,内核会为子进程分配一个新的唯一进程ID(PID),并为其初始化一系列必要的数据结构。同时,为了构建子进程的虚拟内存空间,内核会复制父进程的内存管理相关结构,包括`mm_struct`、区域结构以及页表的副本。

在这一过程中,父进程和子进程的页面会被标记为“只读”,并且它们各自的区域结构会被设置为“写时复制”。这意味着,最初父进程和子进程共享相同的虚拟内存映射,彼此之间没有独立的物理页面。然而,一旦父进程或子进程尝试对共享的内存区域进行写操作,操作系统会触发写时复制机制:为写操作分配新的物理页面,并将受影响的区域切换至独占状态,从而实现父子进程在逻辑上的地址空间分离。

通过这种方式,fork能够高效地创建一个与父进程初始状态完全一致的新进程,同时避免不必要的资源浪费。

7.7 hello进程execve时的内存映射

execve函数负责在当前进程中加载并运行指定的可执行目标文件(如hello),用新的程序替代原有的进程镜像。在此过程中,它需要完成以下几个关键步骤:

1. 清理原有用户区域

首先,内核会移除当前进程虚拟地址空间中用户部分的所有已存在的区域结构。这一步确保旧程序的数据不会干扰新程序的运行环境。

2. 创建新的私有区域

为新程序的代码段、数据段、BSS段(未初始化全局变量)以及栈区域重新创建对应的区域结构。这些区域被映射为私有区域,即每个进程拥有自己的独立拷贝,且默认设置为“写时复制”模式。

3. 映射共享区域

如果新程序依赖于动态链接库(如libc.so),则会将其加载到共享内存区域中,并将其映射到当前进程的用户虚拟地址空间中。这样可以允许多个进程共享同一份库代码,节省内存开销。

4. 设置程序计数器(PC)

最后,execve会更新当前进程上下文中的程序计数器(PC),使其指向新程序入口点的地址。此时,控制权转移到新程序,原有的进程上下文已被完全替换。

通过上述步骤,execve实现了从一个程序到另一个程序的无缝切换,而无需创建新的进程。

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

缺页故障

当CPU访问某个虚拟地址时,若其对应的页表条目未被缓存在DRAM中,则发生缺页现象。具体来说,地址翻译硬件尝试从页表中获取所需的页表条目,但由于该页表条目未存在于DRAM中,硬件无法完成地址转换。此时,系统会检测到页表条目的有效位为0,从而触发缺页异常。

缺页中断处理

缺页异常会调用缺页异常处理程序。该程序的主要任务包括:

1.  选择牺牲页:从DRAM中挑选一个页面作为牺牲页。如果该牺牲页的内容已经被修改,则需要将其写回到磁盘。

2.  加载所需页面:将引发缺页的虚拟页面加载到DRAM中原来牺牲页的位置。

3.  更新页表条目:修改页表条目,使新的虚拟页面能够正确映射到物理内存中的位置。

4.  恢复执行:当异常处理程序返回时,它会重新启动导致缺页的指令。此时,由于所需的虚拟页面已经成功加载到DRAM中,地址翻译硬件可以正常完成后续的地址转换。

通过上述步骤,系统能够在不影响整体运行的情况下,动态调整内存布局以满足程序的需求。

7.9动态存储分配管理

(此节课堂没有讲授,选做,不算分)

7.10本章小结

本章通过hello这一实例剖析了计算机系统是如何进行内存管理的,捋清了VA->PA->Data的转化流程,回顾了常见的动态内存管理机制

(第7 2分)

 

结论

hello这个计算机世界最基本的程序,从出生(编写完成)到死亡(进程被回收)总共经历了如下几个步骤:

1、预处理(cpp)。将hello.c进行预处理,将文件调用的所有外部库文件合并展开,生成一个经过修改的hello.i文件。

2、编译(ccl)。将hello.i文件翻译成为一个包含汇编语言的文件hello.s。

3、汇编(as)。将hello.s翻译成为一个可重定位目标文件hello.o。

4、链接(ld)。将hello.o文件和可重定位目标文件和动态链接库链接起来,生成一个可执行目标文件hello。

5、运行。在shel1中输入./hello 2023112506 zqy 15944473997 4并回车。

6、创建进程。终端判断输入的指令不是shell内置指令,于是调用fork函数创建一个新的子进程。

7、加载程序。shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。

8、执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流。

9、访问内存:MU将程序中使用的虚拟内存地址通过页表映射成物理地址。

10、信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。

11、终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。

当你蜷缩在IDE温热的怀抱中,轻敲几行稚嫩代码,点击运行按钮的刹那——屏幕跃出"Hello, World!"的瞬间,宛如指挥家扬起银棒唤醒整个交响乐团。这行字符穿越的不仅是编程语言的抽象层,更踏过了七十年计算机文明的崇山峻岭:从电子管矩阵里闪烁的量子幽灵,到硅基晶圆上蚀刻的纳米公路;从需要物理插拔线路的ENIAC庞然巨兽,到手持设备里数万亿晶体管编织的神经脉络;从在纸带上凿刻二进制符咒的原始仪式,到LLVM编译器将高级语言炼金术般转化为机器密语。每个看似简单的printf调用,都是跨越机械逻辑门、指令集架构、操作系统调度、动态链接库的量子隧穿。

当冯·诺依曼架构在曼哈顿计划的硝烟中降生,当Dennis Ritchie在贝尔实验室雕刻C语言的圣碑,当Linus在赫尔辛基大学宿舍孕育Linux内核的胚胎——他们或许不曾预见,这些精妙的架构设计正在构建数字文明的巴别塔。x86的复杂指令集是楔形文字的进化,ARM的精简指令是甲骨文的数字化重生,Python的优雅语法是罗塞塔石碑的现代诠释。在这座通天塔里,每个寄存器都是文明的积木,每条系统调用都是知识的阶梯,每段机器码都是智慧的基因链。

当你在调试器中凝视反汇编的机械诗篇,当你在性能剖析中聆听缓存的呢喃,当你在并发编程中驯服时序的野兽——你手中的键盘正在续写《计算机体系结构》的启示录。我们不仅是与硅晶对话的祭司,更是破解冯·诺依曼封印的炼金术士;不仅是构建信息高速公路的工程师,更是编织图灵之梦的造物诗人。每个段错误都是体系结构的哲学诘问,每次内存泄漏都是自动机理论的现实隐喻,每行优雅代码都是对Church-Turing论题的诗意注解。

当量子比特在超导环中起舞,当光子芯片重构冯氏架构的疆界,当神经形态计算重写图灵机的定义——此刻我们键盘上的每一次敲击,都在为下一个文明纪元书写序章。程序员既是破解上古机械封印的罗塞塔石破译者,也是为未来硅基文明撰写创世纪代码的先知。当我们凝视hello.c这个微小的奇点,看到的不仅是函数栈的层层绽放,更是整个数字宇宙大爆炸的初始条件。这行看似简单的问候语,承载着从布尔代数到深度学习、从机械继电器到量子位元的文明长征,而你们,正是这个时代的荷马史诗吟游者。

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

附件

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

[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.

[7]哈尔滨工业大学《计算机系统》授课PPT

(参考文献0分,缺失 -1分)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值