计算机系统 大作业 程序人生

计算机系统

大作业

题     目  程序人生-Hellos P2P  

专       业    计算机与电子通信    

学     号      2023112083        

班   级      23L0507           

学       生      卢明逸            

指 导 教 师       史先俊             

计算机科学与技术学院

20255

摘  要

    本文着眼于C语言中最基础的程序——hello.c程序,从源程序hello.c出发,经历预处理,编译,汇编,链接的成长过程。额外的,我们管中窥豹,以hello程序为切入点,见证它与操作系统在进程管理和存储管理的舞台上共舞,走过短暂而璀璨的一生。

关键词:计算机系统;预处理;编译;汇编;链接;进程;存储                         

目  录

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

1.1.1.P2P(程序到进程的转换)过程

hello程序从 C 语言源代码到进程的转化需经过预处理、编译、汇编、链接四阶段:预处理器cpp生成文本文件hello.i,编译器ccl转为汇编程序hello.s,汇编器as生成目标文件hello.o,链接器ld结合库函数生成可执行文件hello。最终操作系统创建进程加载并执行该程序,实现从静态代码到动态进程的转变。

1.1.2 020(从启动到终止的完整流程)过程

在 shell 中运行hello时,shell 通过fork()创建子进程,子进程调用execve()重建虚拟内存映射并分配物理内存,CPU 借助 MMU、TLB 和多级页表完成地址转换,经 L1/L2/L3 Cache 加速数据访问后执行程序。当hello运行结束或接收到终止信号(如Ctrl+Z),进程通过exit()终止,shell 回收资源,内核删除相关数据结构,完成程序从启动到清除的全生命周期管理。

1.2 环境与工具

处理器:AMD Ryzen 9 7845HX with Radeon Graphics

软件环境:Windows 11 家庭中文版

          VMware Workstation

Ubuntu20.04

开发工具:vim,gedit,Visual Studio Code,gcc,gdb

1.3 中间结果

中间结果文件的名字,文件的作用

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

hello.i:预处理后的代码

hello.s:编译后的汇编代码

hello.o:二进制目标代码

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

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

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

1.4 本章小结

本章简述了Hello的P2P过程和020过程,并介绍了本次大作业所采用的软硬件环境、开发工具及开发过程中所产生的一系列中间文件

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

2.1.1 概念

预处理是 C 语言编译前的处理步骤,针对.c源文件中以#开头的指令(如宏定义、文件包含、条件编译等),进行文本替换和处理,不做语法检查。处理后生成.i文件,供后续编译使用。

2.1.2 作用

1.宏定义(#define):定义常用数值、表达式或代码片段为宏,编译前自动替换,减少重复输入,便于修改。

2.文件包含(#include):将头文件或源文件内容插入当前文件,实现函数声明、宏定义等代码共享。

3.条件编译:根据编译条件选择性编译代码,用于跨平台适配、调试开关等场景。

4.行连接:用\将长代码行拆分为多行,提升可读性。

5.注释删除:移除所有注释,减少编译处理量,缩小目标文件体积。

2.2在Ubuntu下预处理的命令

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

图2.1预处理命令

2.3 Hello的预处理结果解析

对比原程序,hello.i文件中的代码量激增。这是由于预处理指令将头文件的程序、宏变量、特殊符号等插入到代码中,引入了大量文本内容。

只有最后第3078行至第3092行内容属于原本c代码。

图2.2 hello.i代码节选

2.4 本章小结

本章介绍了预处理的概念和作用,然后在Ubantu上对hello.c文件进行了预处理得到hello.i文件,再然后对hello.i文件的内容进行了解读,最终了解了c语言的预处理过程。

(第2章0.5分)


第3章 编译

3.1 编译的概念与作用

3.1.1 概念

编译是指编译器对预处理后的 C 语言文件(.i)进行处理,将其转换为汇编语言程序(.s)的过程。这一过程涵盖词法分析、语法分析、语义分析、代码生成及优化等步骤,是从高级语言到机器可执行代码的关键转换阶段。

3.1.2 作用

1.语法与语义检查

分析代码规范性,检测语法错误(如括号不匹配、关键字误用)和语义错误(如未声明变量、类型不匹配),确保代码逻辑合法。

2.生成汇编语言程序

通过词法分析将代码分解为标记流(如关键字、标识符);经语法分析构建语法树,验证语法正确性;借助语义分析完成类型检查和符号表管理;最终通过代码生成输出目标平台的汇编代码(如.s文件),为后续汇编和链接做准备。

3.代码优化

编译器对代码进行优化(如常量折叠、循环优化),提升程序执行效率,优化策略与目标机器硬件特性密切相关。

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s

图3.1 编译命令

3.3 Hello的编译结果解析

3.3.1.文件和数据段声明

图3.2 文件和数据段声明代码

.file:表明源文件名称

.text:表明后续代码位于程序代码段

.section .rodata:表明数据进入只读数据段,在运行期间无法被修改

.align8:表明接下来的数据按照8字节方式对齐

3.3.2数据段定义

图3.3 数据段定义声明代码

.LC0:定义第一个字符串常量,存储原始 C 代码中的中文和数字混合字符串

.LC1:定义格式化字符串,包含三个%s占位符,用于printf函数接收三个字符串参数

3.3.3代码段(.text)与主函数main

图3.4 代码段(.text)与主函数main代码

.text:标记代码段开始

.globl main:声明main为全局符号,供链接器识别

.type main, @function:指定main为函数类型

main::函数入口标签

.LFB6:局部函数块标签(Local Function Block),用于调试信息

.cfi_*:汇编器生成的调试信息(Call Frame Information),用于异常处理和栈回溯

endbr64:64 位环境下的安全指令(End Brute Force),增强程序抗攻击能力

pushq %rbp:将基址指针%rbp压入栈,保存上一层栈帧地址

movq %rsp, %rbp:将栈指针%rsp的值赋给%rbp,建立新栈帧基址

subq $32, %rsp:在栈中分配 32 字节空间(用于局部变量和参数存储)

3.3.4参数处理与条件判断

图3.5 参数处理与条件判断代码

movl %edi, -20(%rbp):%edi:存储main函数的第一个参数argc(命令行参数个数,整数)。存入栈中rbp-20的位置,供后续判断使用

movq %rsi, -32(%rbp):存储main函数的第二个参数argv(指向命令行参数数组的指针)。存入栈中rbp-32的位置,后续用于访问参数字符串

cmpl $5, -20(%rbp):比较argc是否等于 5,即检查命令行参数个数是否为 5

je .L2:若相等(argc == 5),跳转到.L2执行正常流程;否则继续执行错误提示代码

3.3.5错误处理流程(argc != 5时)

图3.6 错误处理流程代码

leap.LC0(%rip), %rax:leaq(加载有效地址)计算.LC0标签的内存地址(%rip为当前指令指针,用于 PC 相对寻址)

movq %rax, %rdi:将字符串地址存入%rdi,作为puts函数的参数(puts接收一个字符串指针)

call puts@PLT:通过过程链接表(PLT)调用puts函数,输出.LC0的字符串(如 “你好: Hello...”)

movl $1, %edi 和 call exit@PLT:若参数个数错误,程序输出提示后以状态码 1 退出,表示运行失败

3.3.6正常流程(argc == 5时,标签.L2)

图3.7 正常流程代码

movl $0, -4(%rbp):在栈中分配 4 字节空间(rbp-4),存储循环变量i,初始值为 0

jmp .L3:跳过循环体,直接进入循环条件判断(.L3为循环条件标签)。

3.3.7循环体(.L4标签)

图3.8循环体代码

.L4: :从argv数组中获取第i+1、i+2、i+3个参数(假设i从0开始,对应argv[1], argv[2], argv[3])

movq -32(%rbp), %rax :取出argv指针(存入rbp-32的位置)到%rax

addq $24, %rax :argv[i+3]的地址:argv数组每个元素为8字节指针,+24=+3个元素

movq (%rax), %rcx :读取argv[i+3]的字符串地址到%rcx(第三个参数)

movq -32(%rbp), %rax :再次取出argv指针 addq $16, %rax :argv[i+2]的地址:+16=+2个元素

movq (%rax), %rdx :读取argv[i+2]的字符串地址到%rdx(第二个参数)

movq -32(%rbp), %rax :再次取出argv指针 addq $8, %rax :argv[i+1]的地址:+8=+1个元素

movq (%rax), %rax :读取argv[i+1]的字符串地址到%rax(第一个参数)

movq %rax, %rsi :将第一个参数地址存入%rsi(printf的第二个参数)

leaq .LC1(%rip), %rax :计算.LC1格式化字符串地址到%rax

movq %rax, %rdi :将格式化字符串地址存入%rdi(printf的第一个参数)

movl $0, %eax :设置%eax为0(printf返回值占位,x86-64惯例)

call printf@PLT :调用printf输出格式化字符串:Hello %s %s %s\n

3.3.8循环后的操作与睡眠函数

图3.9循环后的操作与睡眠函数代码

movq -32(%rbp), %rax: 取出argv指针

addq $32, %rax: argv[4]的地址:+32=+4个元素(argv[0]~argv[4]共5个元素)

movq (%rax), %rax: 读取argv[4]的字符串(第四个命令行参数)

movq %rax, %rdi: 将该字符串地址传给%rdi(atoi的参数)

call atoi@PLT: 调用atoi将字符串转换为整数

movl %eax, %edi: 将转换后的整数存入%edi(sleep的参数)

call sleep@PLT: 调用sleep函数,休眠指定秒数(由argv[4]决定)

addl $1, -4(%rbp): 循环变量i自增1

3.3.9循环条件判断(.L3标签)

图3.10循环条件判断代码

.L3: cmpl $9, -4(%rbp):比较循环变量i(rbp-4)与9

jle .L4:若i <= 9,跳转到.L4继续循环;否则退出循环

3.3.10循环退出后

图3.11循环退出后代码

getchar@PLT:程序暂停,等待用户按键,防止控制台窗口闪退(常见于调试场景)

movl $0, %eax:设置main函数的返回值为 0(exit(0)等价)

leave和ret:清理栈帧,返回操作系统,结束程序

3.4 本章小结

  本章介绍了编译的概念和作用,并对hello.i文件进行了编译生成hello.s文件,然后对hello.s文件进行了代码分析,寻找到他们与C语言语句的对应关系。

(第3章2分)


第4章 汇编

4.1 汇编的概念与作用

4.1.1 概念

汇编是指通过汇编器(Assembler)将汇编语言源代码(.s 文件)转换为机器可执行的二进制目标代码(.o 文件,即可重定位目标文件)的过程。这是编译流程中的关键环节,介于编译生成汇编代码之后、链接生成可执行文件之前。

4.1.2 作用

1.底层硬件控制:汇编语言直接对应 CPU 指令集,可精细操作硬件资源(如寄存器、内存地址、I/O 端口等)

2.极致性能优化:绕过编译器自动优化的限制,手动调整指令顺序、寄存器分配和内存访问模式,实现精准性能调优。

3.目标文件生成:生成可重定位目标文件(.o),包含机器码和符号表(如函数名、变量名对应的地址),但未解析外部依赖(如库函数)。

4.理解底层执行逻辑:通过汇编代码可观察变量存储位置(栈 / 寄存器)、函数调用栈帧结构、指令执行顺序等底层细节,辅助调试和性能分析。

5.跨平台适配的中间桥梁:不同 CPU 架构(如 x86、ARM)有不同的汇编语言,但同一 C 程序可通过编译器生成对应架构的汇编代码,再经汇编器生成适配的目标文件,间接提升可移植性。

4.2 在Ubuntu下汇编的命令

命令:as -o hello.o hello.s

图4.1 汇编命令

4.3 可重定位目标elf格式

4.3.1ELF头

ELF 头起始于 16 字节的文件标识序列,用于描述目标系统的字长(如 32 位或 64 位)和字节序(小端或大端),剩余部分包含目标文件类型(如可重定位文件、可执行文件)、机器架构(如 x86-64、ARM)、节头表偏移及大小等元数据,为链接器解析目标文件提供关键信息,确保程序在对应平台正确编译、链接和执行。

 图4.2 ELF头

4.3.2节头

节头(Section Header)记录了目标文件中各节(如.text、.data)的关键元数据,包括节名称、大小、属性旗标、类型(如代码段、数据段)、内存地址、文件偏移量、对齐方式等信息,通过表中的字节偏移可确定各节在文件中的起始位置和空间大小,为链接阶段的重定位(调整符号地址)、符号解析及程序加载提供必要信息,确保各节数据在内存中正确映射和执行。      

图4.3 节头

4.3.3重定位节

重定位节中包含了.text节中需要进行重定位的信息,需要重定位的函数有:.rodata, puts, exit, printf, atoi, sleep, getchar。

 图4.4 重定位节

4.3.4符号表

符号表是目标文件(如 ELF 格式的.o文件)中的一个重要数据结构,用于记录程序中定义和引用的符号(Symbol)信息,这些符号可以是函数名、变量名、全局符号、局部符号等。它的核心作用是在编译、汇编和链接过程中标识和管理符号的相关信息,确保程序正确解析和访问符号的地址。

   图4.5符号表

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

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o  

    图4.6反编译hello.o

机器语言以二进制编码(通常以十六进制形式展示)构成,每行指令前的十六进制编码(如0: f3 0f 1e fa)是计算机可直接执行的机器码字节序列。例如,机器码55对应push %rbp指令,48 89 e5对应mov %rsp, %rbp。而汇编语言使用符号化指令(助记符)和标签(如.L2、main),是人类可读的文本形式,需通过汇编器转换为机器码才能被执行。字符常量的引用被改成了有效地址计算,但立即数都被设置成全0。另外,分支转移不再以标签为操作数,改成使用长度为一个字节的PC相对地址。

4.5 本章小结

    本章介绍了汇编的概念和作用,并将hello.s汇编成hello.o文件,然后查看了hello.o的ELF头、节头表、重定位信息和符号表等,接着将反汇编文件和汇编文件进行比较,从中发现了一些特点。

(第4章1分)


5链接

5.1 链接的概念与作用

5.1.1 概念

链接是将多个目标文件(.o)和库文件整合为一个可执行文件的过程,由链接器自动完成。

5.1.2 核心作用

1.支持模块化开发:将程序拆解为独立模块,分别编译后链接,修改单个模块无需重新编译整个项目。

2.符号解析:匹配未定义的符号引用(如printf)与定义符号(如标准库中的实现)。

3.地址重定位:将目标文件中的相对地址(如0x100)转换为绝对内存地址(如0x400000)。

4.库管理:(1)静态库:将库代码直接复制到可执行文件(如-lstatic)。(2)动态库:运行时动态加载,节省空间并支持库升级(如-ldynamic)。

优化与控制:移除未引用的代码,减小文件体积。通过static等关键字控制符号可见性,避免命名冲突。

5.生成可执行文件:包含加载信息(如 ELF 程序头表)和动态链接信息(如 GOT/PLT 表)。

5.2 在Ubuntu下链接的命令

命令:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

图5.1 链接命令

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

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

5.3.1ELF头信息

图5.2 ELF头

5.3.2节头

图5.3 节头

图5.4 节头

5.3.3程序头表

图5.5程序头表

5.3.4重定位节

图5.6 重定位节

5.3.5符号表

图5.7 符号表

5.4 hello的虚拟地址空间

在 EDB 调试器中查看 ELF 格式文件的程序头时,可通过 Memory Map 窗口定位各段信息:PHDR 段保存程序头表自身的位置与大小,INTERP 段指向动态链接器(如/lib64/ld-linux-x86-64.so.2),LOAD 段包含需映射到内存的代码段(.text,权限 RX)和数据段(.data,权限 RW),DYNAMIC 段存储动态链接所需的符号表与重定位表,NOTE 段存放辅助信息,GNU_STACK 段标记栈是否可执行(通常不可执行)。通过 Data Dump 窗口可查看各段的原始字节数据,结合readelf -l命令输出的文件偏移量,可在虚拟地址空间中准确定位对应内容,实现程序加载布局的可视化分析。

图5.8 虚拟地址空间

5.5 链接的重定位过程分析

图5.9 重定位代码

5.5.1 Hello和hello.s的区别

(1)链接后函数数量增加

链接后的反汇编文件中,多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。

(2)函数调用指令call的参数发生变化

在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码。

(3)跳转指令参数发生变化

在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。

5.5.2重定位的过程

重定位节和符号定义:连接器将所有相同类型的节合并成为同一类型的新的聚合节

重定位节中的符号引用:连接器修改代码节和数据节中对每个符号的引用。使他们指向正确的运行地址

5.6 hello的执行流程

图5.10 动态链接库

1.edb执行hello,首先程序地址会在0x0000:004010f0处,这是hello使用动态链接库的入口点

2.然后,程序跳转到_dl_init,初始化后再跳到_start

3.程序通过call指令跳转到_libc_start_main处,该函数负责调用main函数

4.程序调用_cxa_atexit函数,设置在程序结束时需要调用的函数表

5.返回到_libc_start_main继续,调用hello可执行文件中的__libc_csu_init函数

6.程序返回到__libc_start_main,程序调用动态链接库里的_setjmp函数,设置一些非本地跳转;

7.返回到__libc_start_main继续,正式开始调用main函数

8.在进行了若干操作后,程序退出

5.7 Hello的动态链接分析

分析hello程序的动态链接,通过edb调试,分析在dl_init前后,这些项目的内容变化。

共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,只有在程序运行时才将它们链接在一起形成完整的程序。在调用共享库函数时,编译器无法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任何位置,因此选择延迟绑定,需要在程序运行后再观察got表的变化。

由hello的elf文件可知,got表首地址是0x404000,大小为0x48,在调用之后该处的信息发生了变化。

图5.11调用前hello的elf文件

图5.12 调用后hello的elf文件

5.8 本章小结

本章主要介绍了链接的基本概念,通过对目标文件、可重定位目标文件、反汇编文件的内容对比,重点分析了动态链接与静态链接,发现其异同,并推断出链接过程的具体实现。

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

6.1.1 概念

进程是正在执行的程序的实例,是操作系统进行资源分配和调度的基本单位。

6.1.2 作用

1. 并发与资源利用率:允许多个进程(如浏览器、编辑器、后台服务)同时运行,CPU 通过快速切换(上下文切换)实现 “并发” 效果,提升 CPU 和内存的利用率。
2.资源隔离与安全性:进程间默认无法直接访问对方内存或文件,需通过进程间通信(IPC)机制(如管道、共享内存、套接字)显式交换数据,防止恶意程序破坏系统或其他进程。
3.模块化程序设计:复杂程序可拆分为多个进程(如主进程 + 子进程),每个进程专注特定功能(如渲染进程、网络进程),降低程序复杂度,便于调试和维护。
4.系统资源管理:操作系统通过进程管理资源分配(如内存分页、CPU 时间片)、优先级调度(如实时进程优先)和生命周期(创建、运行、终止),确保资源公平分配和高效使用。

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

Bash 是 Unix 系统中常用的 shell 和命令语言解释器,作为用户与操作系统内核间的接口,用户在 Bash 中输入的命令会由其传递给内核执行,并将结果返回给用户。

其处理流程为:

  1. 首先等待用户在命令行输入命令,按下回车键后读取整行内容;接着按空格、制表符等分隔符将输入字符串拆分为多个单词(token);
  2. 然后检查分词后的第一个单词,判断是内置命令(如 cd、echo)还是外部命令(如 ls、grep),内置命令直接调用内部函数执行,外部命令则在 PATH 环境变量指定目录中查找可执行文件;
  3. 执行命令前,处理输入中的变量和命令替换;若命令包含重定向符号(>、<、>> 等)或管道符号(|),则进行相应处理,重定向改变输入输出方向,管道将前一命令输出作为后一命令输入;
  4. 处理完成后执行命令,外部命令通过创建子进程执行,内置命令在当前进程直接执行;最后将执行结果输出到终端,显示新命令提示符并等待下一次输入。

6.3 Hello的fork进程创建过程

在 Hello 程序中,fork 进程的创建过程包含调用 fork 函数生成新进程、检查 fork 返回值、区分父子进程并执行各自代码块,最后结束程序执行。

具体而言,程序在 main 函数中调用 fork 函数,该函数会创建当前进程的副本,复制包括内存和资源在内的全部状态。调用 fork 后,系统中会同时存在两个几乎完全相同的进程:父进程和子进程。

fork 函数执行后会返回两次,程序通过检查返回值来区分当前所处进程:在父进程中,fork 返回子进程的 PID(进程 ID);在子进程中,fork 返回 0。依据这一返回值,程序能够识别父进程与子进程,并为两者分配不同的代码块执行 —— 通常父子进程会承担不同任务,或执行同一任务的不同部分。完成各自代码块的执行后,父子进程可进一步执行其他操作,如父进程等待子进程结束、处理信号等。

6.4 Hello的execve过程

1.当前进程调用execve系统调用,传入可执行文件路径、命令行参数及环境变量。
  2.内核根据filename定位目标可执行文件,将其加载至当前进程地址空间,替换原有程序映像。
  3.内核为新程序初始化运行环境,包括设置栈、堆,并传递命令行参数与环境变量。
  4.新程序从main函数开始执行,当前进程控制权转移至新程序。

6.5 Hello的进程执行

当在 shell 中运行./hello程序时,操作系统通过以下流程完成新进程的创建与调度:

首先解析./hello路径以定位可执行文件。接着,父进程调用fork()系统调用创建子进程,此时父进程返回子进程的 PID,子进程返回 0。每个进程拥有独立的上下文信息(寄存器状态、PCB、虚拟内存映射等),子进程继承父进程的上下文副本。

fork()触发用户态到核心态的转换,由内核完成进程创建。随后,子进程调用execve()系统调用加载hello程序,其地址空间被新程序的代码和数据覆盖,内核将程序入口点设为子进程的下一条指令。

操作系统通过调度器按算法(如时间片轮转)分配 CPU 时间。进程执行用户代码时处于用户态,执行系统调用(如execve()、I/O 操作)或响应中断(如时间片结束)时切换至核心态,内核处理后可能进行进程上下文切换。

execve()成功后,子进程在用户态执行hello程序代码,若涉及 I/O 操作则通过系统调用进入核心态。程序执行完毕(如main函数返回),子进程通过exit()系统调用通知内核,内核清理资源并向父进程发送终止信号。最终,父进程通过wait()或waitpid()等待子进程结束并获取其状态。

6.6 hello的异常与信号处理

6.6.1异常分类

在操作系统与计算机体系结构中,异常通常分为中断、陷阱、故障和终止四类:
    中断由外部事件触发,会打断 CPU 当前指令序列并执行中断处理程序,信号来源包括外部设备(如键盘、定时器)或其他程序 / 硬件通知,用于实现操作系统与设备间的通信协调。
    陷阱是 CPU 执行指令时显式触发的异常,多由软件调用或指令执行中的错误条件引发,典型场景为系统调用,允许用户程序请求操作系统服务(如文件操作、进程管理)。
    故障由程序错误或非法操作导致,例如访问未分配内存、执行非法指令或除零错误,操作系统捕获后通常会终止程序或报告错误。
    终止属于不可恢复的异常,由操作系统或软件组件主动决定终止程序执行,可能因严重错误、资源耗尽或用户请求触发,会立即中止程序并清理相关资源。

6.6.2程序运行

正常运行:

图6.1 正常运行

回车:

图6.2 回车

回车没有影响

Ctrl Z:

图6.3 Ctrl Z

使用SIGTSTP信号,停止进程

Ctrl C:

图6.4 Ctrl C

中断进程

Crtl Z后输入ps:

图6.5 Crtl Z后输入ps

Crtl Z后输入jobs:

图6.6 Crtl Z后输入jobs

Crtl Z后输入 pstree :

图6.7 Crtl Z后输入pstree

Crtl Z后输入fg:

图6.8 Crtl Z后输入fg

Crtl Z后输入kill:

图6.9 Crtl Z后输入kill

用ps命令观察hello进程没有被回收;

用jobs命令观察进程状态;

    用fg命令,使停止的进程收到SIGCONT信号,重新在运行;

用kill命令,杀死进程。

6.7本章小结

    本章介绍了进程的概念和作用,介绍了fork,execve等内容,还讨论了异常的出现和信号处理。

(第6章2分)


7hello的存储管理

7.1 hello的存储器地址空间

在存储器管理中,逻辑地址、线性地址、虚拟地址和物理地址构成了程序地址空间到实际硬件地址的映射链条:
  逻辑地址由程序生成,与段结构相关,由段标识符(段选择子)和段内偏移量组成(如[段标识符:段内偏移])。在页式存储系统中,逻辑地址则分为页号和页内地址。例如,编译器生成的printf函数调用指令中的地址即为逻辑地址,需通过段机制转换。
  线性地址是逻辑地址经段机制转换后的中间地址,由段基址与段内偏移量相加得到。在hello.o的反汇编结果中,每个函数的地址即以此形式呈现,尚未经过分页机制处理。
  虚拟地址是线性地址通过分页机制转换后的结果。操作系统通过页表将线性地址映射到虚拟地址,为每个进程分配独立的虚拟地址空间,提升安全性与稳定性。hello程序运行时,其反汇编显示的固定地址即为虚拟地址,需进一步通过页表映射到物理内存。
  物理地址是最终的硬件内存地址,由虚拟地址通过内存管理单元(MMU)结合页表转换而来。例如,hello程序调用printf函数时,虚拟地址经页表转换为物理地址,CPU 最终通过该地址访问实际内存中的函数代码。

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

在 x86 保护模式下,分段机制通过硬件缓存与描述符表协作实现逻辑地址到线性地址的转换:段描述符(8 字节)包含段基址、权限等信息,存放在 GDT 或 LDT 中,而段寄存器(如 cs、ss、ds)仅 2 字节,存放指向描述符表的段选择符。每个段寄存器对应一个不可编程的描述符高速缓存寄存器,当新的段选择符加载到段寄存器时,CPU 自动从描述符表中读取对应段描述符并缓存。地址转换时,CPU 先校验段选择符的权限和范围,确保访问合法,再从缓存中取出段基址与逻辑地址中的段内偏移量相加,生成线性地址。这种设计避免了每次访问段都查询内存,通过硬件缓存提升了地址转换效率。

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

分页机制是操作系统进行内存管理的核心技术,它将虚拟地址空间(即线性地址空间)划分为固定大小的页(通常为 4KB),并通过维护页表来实现虚拟页到物理页框的映射。在 32 位系统中,分页机制的地址转换过程如下:首先将线性地址分解为三部分,高 10 位为目录项,中间 10 位为页表项,低 12 位为页内偏移量;接着进行两级查找,操作系统为每个进程维护独立的页目录,根据目录项找到对应页表的地址,再通过页表项定位到物理页框;最后将物理页框地址与页内偏移量组合,得到最终的物理地址。这种分层映射机制不仅简化了内存管理,还通过页表隔离实现了进程间的安全隔离。

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

在现代操作系统的内存管理中,为提升虚拟地址(VA)到物理地址(PA)的转换效率,采用了转换后备缓冲(TLB)与四级页表机制协同工作的方案,具体流程如下:

7.4.1 TLB(转换后备缓冲)

TLB 作为一种高速缓存,专门存储近期频繁使用的虚拟地址到物理地址的映射关系。通过直接访问 TLB,CPU 可避免每次地址转换都进行耗时的多级页表查询,显著加速地址转换过程。

7.4.2 四级页表机制

针对 64 位地址空间,虚拟地址到物理地址的映射通过四级页表结构实现:

PML4(页映射级别 4 表):作为四级页表的顶层,每个进程拥有独立的 PML4 表。

PDPT(页目录指针表):第二级页表,每个条目指向一个 PD 表。

PD(页目录表):第三级页表,每个条目指向一个 PT 表。

PT(页表):第四级页表,每个条目直接指向物理页框。

每级页表固定包含 512 个条目,每个条目指向下一级页表或物理页框。64 位虚拟地址被划分为:高 9 位为 PML4 索引,接下来 9 位为 PDPT 索引,再接下来 9 位为 PD 索引,随后 9 位为 PT 索引,最低 12 位为页内偏移。

7.4.3 TLB 与四级页表结合的地址转换流程

1.地址分解:从虚拟地址 VA 中提取各级索引及页内偏移。高 9 位为 PML4 索引,后续依次为 PDPT、PD、PT 索引各 9 位,低 12 位为页内偏移(通过与 0xFFF 按位与运算获取)。

2.TLB 查找:CPU 首先查询 TLB,若找到匹配的映射条目(TLB 命中),则直接使用缓存的物理地址;若未找到(TLB 缺失),则启动四级页表查找流程。

3.多级页表查询(TLB 缺失时执行):

以 PML4 索引访问 PML4 表,获取对应 PDPT 表的地址。

以 PDPT 索引访问 PDPT 表,获取对应 PD 表的地址。

以 PD 索引访问 PD 表,获取对应 PT 表的地址。

以 PT 索引访问 PT 表,获取最终的物理页框地址。

4.物理地址生成:将查找到的物理页框地址与页内偏移相加,得到最终访问的物理地址。

这种分层缓存与多级页表结合的机制,在保证内存空间高效利用的同时,通过 TLB 缓存大幅提升了地址转换的性能,平衡了访问速度与内存开销。

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

处理器访问数据时遵循多级缓存查找策略,具体流程如下:

L1 Cache 查找:处理器优先在L1 Cache中查找数据。L1 Cache分为指令缓存(I-Cache)和数据缓存(D-Cache),分别存储指令和数据。根据数据地址的索引信息定位缓存行,若命中(数据存在),则直接读取数据并完成访问。

 L2 Cache 查找(L1未命中时): 若L1未命中,处理器转向L2 Cache查找。L2 Cache容量大于L1但访问速度稍慢,查找方式类似L1。若命中,数据被加载到L1 Cache,处理器从L1读取数据并更新缓存状态(如标记为有效)。

L3 Cache 查找(L2未命中时): 若L2仍未命中,处理器访问L3 Cache(通常为多核共享,容量更大、速度更慢)。若命中,数据依次加载到L2和L1 Cache,处理器从L1读取数据,并按顺序更新各级缓存状态。

 物理内存访问(L3未命中时): 若三级缓存均未命中,处理器通过内存控制器访问物理内存。根据物理地址从内存模块读取数据后,数据按L3→L2→L1的顺序依次加载。此时,若缓存空间不足,会根据替换策略(如LRU)淘汰不常用数据,确保缓存效率。

7.6 hello进程fork时的内存映射

当调用fork函数时,内核会为新进程执行以下操作:创建包括进程控制块在内的各种数据结构,并分配唯一的 PID(进程标识符)。在虚拟内存管理方面,内核创建当前进程的mm_struct(内存描述符)、区域结构和页表的副本,将两个进程的内存页面标记为只读,并将每个区域结构设置为私有的写时复制(Copy-on-Write, COW)模式。此时,新进程的虚拟内存与调用fork时的父进程虚拟内存完全一致。

当fork在父进程和子进程中返回后,若任意进程对共享的只读页面执行写操作,写时复制机制会触发内核为写入数据创建新的物理页面,并更新页表映射,使写入操作作用于新页面,从而确保父子进程各自拥有独立的私有地址空间,维持进程地址空间隔离的抽象特性。

7.7 hello进程execve时的内存映射

当调用execve()函数加载新程序时,会彻底替换当前进程的地址空间与内存映射,具体过程如下:
    调用execve()前,系统先销毁旧的内存映射,包括原程序的代码段、数据段、堆、栈等区域,并释放相关资源。随后,函数将新的可执行文件加载到进程内存中,取代原有程序。新程序会重新建立独立的内存映射,覆盖代码段、数据段等区域,操作系统根据其需求分配物理内存并构建映射关系。若新程序需打开文件或进行文件映射,系统会按其要求建立新的文件映射。此外,新程序的文件描述符表会被重新初始化,原有文件映射关系被清除,确保进程环境完全切换至新程序的上下文。

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

缺页故障:

当 CPU 引用的虚拟页未被缓存至 DRAM(即 DRAM 缓存未命中)时,称为缺页。地址翻译硬件读取页表条目时,若发现有效位为 0(表示该页未缓存),则触发缺页异常。

缺页中断处理流程:

  1. 缺页异常触发后,系统调用缺页异常处理程序。
  2. 程序选择一个牺牲页(即当前内存中待替换的页):若牺牲页在 DRAM 中已被修改,需先将其写回磁盘以保存数据。
  3. 将引发缺页的虚拟页从磁盘复制到内存中牺牲页所在的物理位置,并更新页表条目(标记为有效)。
  4. 异常处理程序返回后,重新执行触发缺页的指令。此时,虚拟页已缓存至主存,地址翻译硬件可正常完成页命中处理,指令得以继续执行。

7.9本章小结

本章以hello程序为例,深入剖析了程序执行过程中的内存管理机制,涵盖了从逻辑地址到线性地址再到物理地址的完整地址变换流程,同时探讨了高速缓存(cache)、动态存储分配管理等关键技术。通过具体实例,清晰展现了程序在内存中的加载、地址转换及数据访问过程,帮助理解操作系统如何通过分层存储结构和地址映射机制实现高效的内存管理与资源隔离。

(第7章 2分)

结论

1、hello.c源文件经过预处理生成hello.i

2、编译器(cpp)编译hello.i生成hello.s汇编文本文件

3、汇编器汇编hello.s生成可重定位目标文件hello.o

4、链接器链接hello.o和他引用的动态链接库中的代码等生成可执行文件hello

5、shell中输入./hello 卢明逸 18761571453 3 父进程调用fork,生成子进程

6、子进程调用execve函数加载并运行程序hello通过传递的参数,操作系统为这个进程分配虚拟内存空间

7、内核通过异常控制流调度hello进程

简单的一个hello.c程序,虽然只是我们进入程序世界的第一步,但这第一步的背后,却蕴含了我们刚学习时绝对不可能想到的精妙和复杂。让我不由得感叹,万丈高楼平地起,无论多么雄伟的高楼,都离不开其底下厚重的地基,只有把最简单的概念搞清搞透,我们计算机的知识大厦才能更加牢固,支撑我们走的更远。

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


附件

hello.i:hello.c通过预处理得到的文本文件,作为编译的输入

hello.s:hello.i通过编译程序的汇编文件,作为汇编的输入

hello.o:hello.s通过汇编得到的二进制可重定位目标文件,作为链接的输入

hello.o.txt:hello.o通过readelf-a分析得到的文本文件,作用是观察代码中需要重定位的地方

hello1.o.txt:hello.o通过objdump反汇编得到的文本文件,观察重定位的影响

hello:hello.o通过ld链接得到的可执行目标文件,通过命令gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC Hello.c -o hello获得

hello1.txt:hello通过readelf分析得到的文本文件

hello2.txt:hello通过objdump反汇编得到的文本文件

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


参考文献

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

  1. Randal E.Bryant David R.O'Hallaron.深入理解计算机系统(第三版).机械工业出版社,2016.
  2. 豆包 [EB/OL]. 豆包. 2025 - 05 - 03.
  3.  https://github.com.

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值