2025计算机系统大作业

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业   计算机与电子通信类                     

学     号   2023112283                    

班     级  23L0504                     

学       生  梁校峰                   

指 导 教 师   刘宏伟                    

计算机科学与技术学院

2024年5月

摘  要

       本文以 “Hello” 程序为切入点,生动展现计算机系统的运行奥秘。从源程序 hello.c 出发,经预处理、编译、汇编、链接生成可执行程序,在操作系统(OS)支持下,通过 fork、execve 等操作转化为进程,于硬件(CPU/RAM)上驰骋。OS 的进程管理(时间片分配等)、存储管理(MMU、TLB、页表、Cache 等),实现程序从生成到执行再到结束的完整生命周期,诠释了 “从程序到进程(P2P)”“从无到无(020)” 的过程。此案例深刻揭示计算机系统(CS)中程序、OS、硬件间的复杂交互与精妙协作,为理解计算机系统底层机制与运行原理提供典型范例,彰显基础程序在展现系统架构逻辑中的独特价值。

关键词:计算机系统、进程管理、存储管理、硬件交互

目  录

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

1.1.1 Hello 的 P2P(From Program to Process)过程

编写程序(Program 阶段)

用户在编辑器(如 Editor )中编写 hello.c 源程序,这是一个文本文件,包含了程序员用 C 语言描述的指令和逻辑,此时它只是存储在硬盘上的静态代码,是程序的原始形态 。

预处理、编译、汇编、链接

预处理:预处理器对 hello.c 进行处理,例如展开头文件、处理宏定义等,生成一个中间文件。

编译:编译器将预处理后的文件编译成汇编代码,把高级语言的代码转换为汇编语言,便于进一步处理。

汇编:汇编器将汇编代码转换为机器语言的目标文件,目标文件包含了机器能够直接执行的指令,但还不能直接运行。

链接:链接器将目标文件与其他必要的库文件等链接在一起,生成可执行文件 hello ,此时这个可执行文件就是一个完整的程序实体,存储在硬盘上 。

加载执行(Process 阶段)

在操作系统(OS )的命令行界面(如在 Bash 中 ),用户执行 hello 程序。操作系统的进程管理模块为其分配资源,通过 fork 创建一个新进程,然后使用 execve 加载并执行 hello 程序的代码。操作系统还会进行内存映射(mmap )等操作,为程序分配内存空间,并在硬件(CPU/RAM/IO )上调度执行,让程序指令在 CPU 上取指译码执行,利用流水线等技术高效运行,在屏幕等 IO 设备上输出结果 。

1.1.2. Hello 的 O2O(From Zero - 0 to Zero - 0)过程

程序诞生前(初始的 Zero - 0)

在编写 hello.c 之前,系统中不存在这个程序相关的任何运行实例,内存中没有为它分配空间,CPU 也没有执行过它的指令,一切都是 “零” 状态 。

程序执行及结束后(最终的 Zero - 0)

当 hello 程序执行完毕,操作系统会回收为该程序分配的所有资源,包括内存空间、CPU 时间片等。程序在内存中的数据和代码被清除,进程结束运行,就像它从未存在过一样,又回到了 “零” 状态,不留任何痕迹,即从初始的无到运行时的有,再到结束后的无 。

1.2 环境与工具

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

软件环境:Windows10家庭和学生版

          VMware Workstation pro2022

Ubuntu20.04

开发工具:Visual Stdio 2019; ClodeBlocks; gedit+gcc;VSCode

1.3 中间结果

hello.c       储存hello程序源代码

hello.i        源代码经过预处理产生的文件(包含头文件等工作)

hello.s       hello程序对应的汇编语言文件

hello.o      可重定位目标文件

hello_o.s      hello.o的反汇编语言文件

hello.elf     hello.o的ELF文件格式

hello          二进制可执行文件

hello.elf      可执行文件的ELF文件格式

hello.s        可执行文件的汇编语言文件

1.4 本章小结

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

第2章 预处理

2.1 预处理的概念与作用

       预处理是在程序代码被翻译成目标代码的过程中,生成二进制文件之前的过程

    预处理的作用主要有以下几点:

提高代码可维护性

宏定义:使用 #define 可以创建宏,将常量或代码片段用一个标识符表示。例如,#define PI 3.14159,后续代码中使用 PI 更易理解和修改,若需更改值,只需修改宏定义处。

代码复用:通过 #include 指令能将常用代码封装在头文件里,在多个源文件中包含该头文件,实现代码复用,减少重复编写。

实现条件编译

借助 #ifdef、#ifndef、#if 等预处理指令,可根据条件决定编译哪些代码。这在跨平台开发时很有用,能针对不同操作系统或编译器编写不同代码,如 #ifdef _WIN32 可判断是否为 Windows 平台。

优化代码

预处理可移除注释、多余空格等,减少代码体积,还能根据条件移除不必要代码,提高代码执行效率。

2.2在Ubuntu下预处理的命令

图1 Ubuntu下预处理

2.3 Hello的预处理结果解析

图2 hell0.i

       .i文件对源文件当中的宏进行了展开,将头文件当中的内容加入到这个文件当中去

2.4 本章小结

      本章首先介绍了预处理的概念和作用,并在Ubuntu上对hello进行了预处理,通过对预处理所产生的,i文件的阅读,感受到了预处理在编译运行当中的重要作用

第3章 编译

3.1 编译的概念与作用

编译是将高级程序设计语言编写的源程序转化为目标机器可执行的机器语言程序的过程       

编译的作用就是产生一个汇编语言文件,同时还具有一定的纠错能力

3.2 在Ubuntu下编译的命令

图3 Ubuntu编译命令

3.3 Hello的编译结果解析

3.3.1 文件和数据段声明

图4 文件和数据段声明

.text表明后续代码位于程序的代码段,代码段主要存放可执行代码

.section .rodata表明数据被放置在只读数据段,该段主要存放常量数据,这些程序在运行期间无法被修改

.align 8 将接下来的数据按8字节边界对齐,提高内存访问效率

3.3.2 字符串常量定义

图5 字符串常量定义

  .LC0 .LC1标识字符串常量起始位置

   .string用来定义字符串常量

3.3.3 main函数的声明

图6 main函数的声明

    .globl main 将main函数声明为全局符号

    .type main,@function 指定main是一个函数类型的符号

3.3.4 函数入口和栈帧设置

图7 函数入口和栈帧设置

       .LFB6 局部函数开始标签

    .cfi_startproc 开始记录调用帧消息,用于调试和处理异常

    Pushq %rbq 将当前栈基指针压入栈中,保存旧的栈帧信息

    .cfi_def_cfa_offset 16 和.cfi_offset 6 -16 更新调用帧消息

    Movq %rsp,%rbp 将当前栈指针的值赋给栈基指针,建立新的栈帧

    .cfi_def_cfa_register 6 指定%rbp为新的调用帧地址寄存器

    .subq $32,%rsp 将栈指针向下移动32个字节,为局部变量分配栈空间

3.3.5 保存命令行参数

图8 保存命令行参数

movl %edi, -20(%rbp):将 %edi 寄存器中的值存放到栈中相对于 %rbp 偏移量为 - 20 字节的位置。

movq %rsi, -32(%rbp):将 %rsi 寄存器中的值存放到栈中相对于 %rbp 偏移量为 - 32 字节的位置。

3.3.6 检查命令行参数数量

图9 检查命令行参数数量

cmpl $5, -20(%rbp):比较栈中存储的 argc 值与 5 是否相等。

je .L2:如果相等,则跳转到 .L2 标签处继续执行;否则继续执行下面的代码。

leaq .LC0(%rip), %rax:计算 .LC0 标签处字符串的地址,并将其存储到 %rax 寄存器中。

movq %rax, %rdi:将字符串地址从 %rax 寄存器移动到 %rdi 寄存器。

call puts@PLT:调用 puts 函数输出错误提示信息。

movl $1, %edi:将返回码 1 存储到 %edi 寄存器中,作为 exit 函数的参数。

call exit@PLT:调用 exit 函数终止程序,返回码为 1。

3.3.7 初始化循环计数器

图10 初始化循环计数器

.L2:标签,用于标记代码位置。

movl $0, -4(%rbp):将循环计数器初始化为 0,并将其存储到栈中相于 %rbp 偏移量为 - 4 字节的位置。

jmp .L3:无条件跳转到 .L3 标签处进行循环条件检查。

3.3.8 循环体

图11 循环体

.L4:循环体的起始标签。

以下代码用于准备 printf 函数的参数:

movq -32(%rbp), %rax:将 argv 指针从栈中取出存到 %rax 寄存器。

addq $24, %rax:计算 argv[3] 的地址。

movq (%rax), %rcx:将 argv[3] 的值存到 %rcx 寄存器。

类似地,获取 argv[2] 和 argv[1] 的值分别存到 %rdx 和 %rsi 寄存器。

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

movq %rax, %rdi:将格式化字符串地址存到 %rdi 寄存器作为 printf 函数的第一个参数。

movl $0, %eax:清空 %eax 寄存器。

call printf@PLT:调用 printf 函数输出格式化信息。

以下代码用于处理暂停时间:

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

addq $32, %rax:计算 argv[4] 的地址。

movq (%rax), %rax:将 argv[4] 的值存到 %rax 寄存器。

movq %rax, %rdi:将 argv[4] 的值存到 %rdi 寄存器作为 atoi 函数的参数。

call atoi@PLT:调用 atoi 函数将字符串转换为整数,结果存到 %eax 寄存器。

movl %eax, %edi:将转换后的整数存到 %edi 寄存器作为 sleep 函数的参数。

call sleep@PLT:调用 sleep 函数暂停程序执行指定的秒数。

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

3.3.9 循环条件检查

图12 循环条件检查

.L3:循环条件检查的标签。

cmpl $9, -4(%rbp):比较循环计数器的值与 9 是否小于等于。

jle .L4:如果小于等于,则跳转到 .L4 标签处继续执行循环体;否则继续执行下面的代码。

3.3.10 等待用户输入和返回

图13 等待用户输入和返回

call getchar@PLT:调用 getchar 函数等待用户输入一个字符。

movl $0, %eax:将返回码 0 存储到 %eax 寄存器中,表示程序正常结束。

leave:恢复栈指针和栈基指针,相当于 movq %rbp, %rsp 和 popq %rbp 两条指令的组合。

.cfi_def_cfa 7, 8:更新调用帧信息。

ret:从函数返回,将控制权交还给调用者。

3.4 本章小结

本章对hello的汇编指令进行了简单介绍,从中也了解到了汇编语言和C语言代码语句中的对应关系

第4章 汇编

4.1 汇编的概念与作用

汇编是将汇编语言编写的源程序翻译成目标机器可执行的机器语言程序的过程或工具

汇编的作用是完成从汇编语言文件到可重定位目标文件的转化过程。

4.2 在Ubuntu下汇编的命令

图14 Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.3.1 ELF头

图15 ELF头

ELF头以一个16字节的序列(Magic,魔数)开始,这个序列描述了生成文件的系统的字的大小和字节顺序。ELF头剩下部分的信息包含帮助连接器语法分析和解释目标文件的信息。

4.3.2 Section头

图16 Section头

      

4.3.3 符号表

图17 符号表

4.3.4 可重定位段信息

图18 可重定位段信息

4.4 Hello.o的结果解析

图19 hello.o的反汇编结果

       反汇编代码所使用的代码与hello.s所使用的代码基本一致,但是在其中也有一些机器代码。机器语言和汇编语言的映射关系分为两种,一种是指令映射,汇编语言中的每一条指令都与机器语言中的一条或多条指令相对应,二是地址映射,汇编语言使用符号地址表示内存位置,而机器语言使用二进制地址。

分支跳转方面:汇编语言中的分支跳转语句使用的是标识符来决定跳转到哪里,而机器语言中经过翻译则直接使用对应的地址来实现跳转。

函数调用方面:在汇编语言.s文件中,函数调用直接写上函数名。而在.o反汇编文件中,call目标地址是当前指令的下一条指令地址。

4.5 本章小结

本章介绍了从hello.s到hello.o的过程,首先介绍了hello.o的ELF头,Section头等,后对hello.o的反汇编文件进行了解析
第5章 链接

5.1 链接的概念与作用

       链接是将各种代码和数据组成为一个文件的过程

       链接可以将程序调用的各种静态数据库和动态连接库整合到一起,完善重定位目录,使之成为一个可运行的程序

5.2 在Ubuntu下链接的命令

图20 Ubuntu下链接的命令

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

5.3.1 ELF头

图21 ELF头

输出ELF文件的文件类型、机器结果、入口地址

5.3.2 程序头表

图22 程序头表

显示各个段的类型、偏移量、虚拟地址、物理地址、文件大小、内存大小

5.3.3 Section表

 

图23 Section头

输出每个节的名称、类型、地址、偏移量、大小

5.3.4 符号表

图24 符号表

输出符号的名称、值、大小、类型

5.4 hello的虚拟地址空间

  

  1. 图25 虚拟地址空间

起始地址0x1d279a40 结束地址0x1d279ab0

5.5 链接的重定位过程分析

图26 反汇编

Hello的反汇编代码和hello.s的反汇编代码在语法上基本一致,但hello中已经不再仅储存call当前指令的下一条指令,而是已经完成重定位后,调用的函数的相应函数有明确的虚拟空间。且hello的反汇编代码中在链接之后加入了很多节。

重定位的过程:

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

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

5.6 hello的执行流程

图27 动态链接库

  1. edb执行hello,首先程序地址会在0x7f4a:70de91c3处,这是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的动态链接分析

图28 重定位后

动态链接则是将库函数的代码存放在共享库文件中,程序在运行时才会去加载这些共享库,并将其链接到程序的地址空间中。

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

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

5.8 本章小结

本章首先介绍了链接的过程,及程序是如何进行重定位操作的

6章 hello进程管理

6.1 进程的概念与作用

进程是指系统中正在运行的一个应用程序的实例,是操作系统进行资源分配和调度的基本单位

多个进程可以在同一时间段并发执行,提高系统的资源利用率和吞吐量

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

Bash是一种广泛使用的 Unix shell 和命令语言解释器,为用户与操作系统内核之间的接口,用户可以在 Bash 中输入各种命令Bash 会将这些命令传递给操作系统内核执行,并将执行结果返回给用户。

处理流程如下:

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

6.3 Hello的fork进程创建过程

调用 fork() 系统调用:父进程调用 fork() 函数,该函数会返回两次,在父进程中返回子进程的进程 ID(一个非负整数),在子进程中返回 0。

内核复制进程:内核会复制父进程的大部分资源,包括内存空间、文件描述符等,创建一个新的子进程。

父子进程继续执行:fork() 调用之后,父进程和子进程从 fork() 调用的下一条语句开始继续执行,它们可以根据 fork() 的返回值来区分自己是父进程还是子进程。

6.4 Hello的execve过程

调用 execve:当前进程调用 execve 系统调用,传入要执行 的可执行文件路径、命令行参数和环境变量。

内核加载新程序:内核根据 filename 找到对应的可执行文件,将其加载到当前进程的地址空间,替换原有的程序映像。

初始化新程序:内核为新程序设置栈、堆等运行环境,将命令行参数和环境变量传递给新程序。

开始执行新程序:从新程序的 main 函数开始执行,当前进程的控制权转移到新程序。

6.5 Hello的进程执行

6.5.1 逻辑控制流

逻辑控制流是程序执行过程中所经历的一系列逻辑步骤和路径,它描述了程序中各个语句和代码块按照特定的逻辑顺序被执行的过程

6.5.2 并发流与时间片

       并发流是指在同一时间段内,多个任务或进程看似同时进行的一种执行方式。在操作系统中,由于 CPU 的处理速度非常快,它可以在多个任务之间快速切换,使得这些任务在宏观上看起来是同时运行的。

       时间片是操作系统分配给每个可运行进程或线程的一段固定的 CPU 执行时间。它是实现并发执行的关键机制之一。

6.5.3 用户模式和内核模式

用户模式:

在用户模式中,进程不允许执行特权指令,例如发起一个I/O操作等,更重要的是不允许直接引用地址空间中内核区内的代码和数据。如果在用户模式下进行这样的非法命令执行,会引发致命的保护故障。

内核模式:

在内核模式下,进程的指令执行相对没有限制,这有点类似于在Linux操作系统中,是否使用sudo(SuperUser do)作为指令的前缀一样。而在内核模式下运行的进程相当于获得了超级管理员的许可。

6.5.4上下文切换

上下文切换是指当操作系统从一个正在运行的进程或线程切换到另一个进程或线程时,保存当前进程或线程的执行上下文,并恢复要切换到的进程或线程的上下文的过程。

6.6 hello的异常与信号处理

异常:

语法错误:如果代码存在拼写错误、遗漏分号、括号不匹配等语法问题,在编译阶段就会报错,无法生成可执行文件。例如,把 printf 写成 print。

运行时错误:比如内存分配错误,如果程序中尝试分配大量内存导致系统内存不足,就会引发此类错误

信号

SIGINT 信号:当用户在终端按下 Ctrl + C 组合键时,程序会收到 SIGINT 信号,默认情况下程序会终止运行。

SIGTERM 信号:这是一种由系统或其他进程发送给本程序的终止信号,用于请求程序正常终止。

图29 输入乱码

图30 ctrl-z

图31 ctrl-c

图32 回车

6.7本章小结

本章主要介绍了hello进程的相关内容,阐述了fork,execve等相关内容,分析了进程的执行过程,以及一些常见的异常

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址是程序在运行时所使用的地址,它是相对于进程的地址空间而言的。在现代操作系统中,每个进程都有自己独立的逻辑地址空间,这个空间是虚拟的,与物理内存地址空间相互隔离。

物理地址是指内存芯片上存储单元的真实地址,它用于在计算机硬件层面直接定位和访问内存中的数据。

虚拟地址是指计算机系统为每个进程提供的一个独立的、逻辑上连续的地址空间。它是进程在运行过程中所使用的地址,与物理内存中的实际地址相对应。虚拟地址空间的大小通常由计算机的硬件体系结构和操作系统决定,一般远大于实际的物理内存大小

线性地址是一种在内存管理中使用的中间地址表示形式。它是一个 32 位的无符号整数,用于在分段和分页机制之间提供一个统一的地址空间。在具有分段和分页机制的计算机系统中,逻辑地址首先通过段机制转换为线性地址,然后线性地址再通过页机制转换为物理地址。

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

逻辑地址结构

Intel 的逻辑地址由段选择子和段内偏移量两部分组成。

  • 段选择子:是一个 16 位的字段,用于指定要访问的段。它包含了段的索引、请求特权级(RPL)和表指示位(TI)。其中,高 13 位是段索引,用于在段描述符表中查找对应的段描述符;低 2 位是 RPL,表示当前访问的特权级别;TI 位用于指定是在全局描述符表(GDT)还是局部描述符表(LDT)中查找段描述符,0 表示 GDT,1 表示 LDT。
  • 段内偏移量:是一个 32 位的字段,表示在段内的具体偏移地址,用于指定段内的某个字节或数据结构。

段描述符

段描述符是描述段属性的一种数据结构,存放在 GDT 或 LDT 中。每个段描述符占 8 个字节,包含了段的基地址、段界限、段属性等信息。

  • 基地址:指定了段在内存中的起始地址。
  • 段界限:定义了段的大小,用于限制对段的访问范围。
  • 段属性:包括段的类型(如代码段、数据段、堆栈段等)、特权级别、是否可读写、是否存在于内存中等信息。

变换过程

  1. 根据段选择子查找段描述符:CPU 根据段选择子中的 TI 位确定是在 GDT 还是 LDT 中查找段描述符,然后通过段索引在相应的描述符表中找到对应的段描述符。
  2. 检查特权级:将段选择子中的 RPL 与段描述符中的特权级进行比较,以确保当前的访问具有足够的权限。如果权限不足,将产生一个保护异常。
  3. 计算线性地址:将段描述符中的基地址与逻辑地址中的段内偏移量相加,得到线性地址。如果段内偏移量超出了段界限,则会产生一个越界异常。

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

线性地址结构

线性地址通常被划分为多个字段,在 32 位系统的页式管理中,一般将线性地址划分为三个部分:页目录索引、页表索引和页内偏移。

若线性地址为 32 位,按照常见的划分方式(页大小为 4KB,即 2 的 12 次方字节):

  • 高 10 位作为页目录索引,用于在页目录表中定位一个页目录项。
  • 中间 10 位作为页表索引,用于在页表中定位一个页表项。
  • 低 12 位作为页内偏移,用于在物理页框内定位具体的字节位置。

页表和页目录

  1. 页目录:是一个包含多个页目录项的数组,每个页目录项占 4 个字节。页目录的起始地址存放在控制寄存器(如 CR3)中。页目录项中保存了指向页表的物理地址等信息。
  2. 页表:同样是一个数组,每个页表项也占 4 个字节。页表由页目录项指向,页表项中保存了指向物理页框的物理地址等信息。
  3. 物理页框:内存被划分成固定大小的页框(通常与页大小相同,如 4KB),用于存放数据。

转换过程

  1. 获取页目录基地址:从控制寄存器(如 CR3)中获取页目录的基地址。
  2. 查找页目录项:使用线性地址中的页目录索引字段,在页目录中找到对应的页目录项。页目录项中包含了页表的物理地址等信息。
  3. 检查页目录项有效性:检查页目录项的有效位,如果无效(如表示该页表不在内存中),则会触发缺页异常,操作系统需要处理该异常,将对应的页表加载到内存中。
  4. 查找页表项:使用线性地址中的页表索引字段,结合页目录项中得到的页表物理地址,在页表中找到对应的页表项。页表项中包含了物理页框的物理地址等信息。
  5. 检查页表项有效性:检查页表项的有效位,如果无效(如表示该物理页框不在内存中),则会触发缺页异常,操作系统需要处理该异常,将对应的物理页框加载到内存中。
  6. 计算物理地址:将页表项中得到的物理页框的物理地址与线性地址中的页内偏移字段相结合,得到最终的物理地址。即物理地址 = 物理页框地址 + 页内偏移。

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

TLB 查找:首先,处理器会在 TLB 中查找虚拟地址对应的映射关系。TLB 是一个高速缓存,存储了最近使用过的虚拟地址到物理地址的映射项。如果在 TLB 中命中,即找到了对应的映射关系,就可以直接获取物理地址,无需访问页表,从而大大提高地址转换速度。

四级页表查找(TLB 未命中时)

访问页目录指针表:如果 TLB 未命中,处理器会根据虚拟地址中的 PDPI 字段,访问页目录指针表。该表的基地址通常存放在特定的寄存器中。通过 PDPI 索引找到对应的页目录指针表项,获取指向页目录的物理地址。

访问页目录:使用虚拟地址中的 PDI 字段,在页目录中查找页目录项。页目录项中包含了指向页表的物理地址。

访问页表:接着,根据 PTI 字段在页表中查找页表项。页表项存储了物理页框的地址以及其他相关信息,如页面的有效位、访问权限等。

检查页面有效性:检查页表项中的有效位,如果有效位为 0,表示该页面不在内存中,会触发缺页异常,操作系统需要将相应的页面从磁盘加载到内存中。

计算物理地址:如果页面有效,将页表项中存储的物理页框地址与虚拟地址中的 Offset 字段相结合,得到最终的物理地址。

TLB 更新:在通过页表查找得到虚拟地址到物理地址的映射关系后,会将该映射关系存入 TLB 中,以便下次访问相同虚拟地址时能够快速命中。

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

L1 Cache 查找:处理器首先在 L1 Cache 中查找所需数据。L1 Cache 通常分为指令 Cache(I - Cache)和数据 Cache(D - Cache),分别用于存储指令和数据。根据数据的地址,通过特定的索引方式在 L1 Cache 中查找对应的缓存行。如果数据在 L1 Cache 中命中,处理器可以立即获取数据并进行处理,访问过程结束。

L2 Cache 查找(L1 Cache 未命中时):如果 L1 Cache 未命中,处理器会接着在 L2 Cache 中查找。L2 Cache 的查找方式与 L1 Cache 类似,但由于 L2 Cache 的容量较大,查找时间可能会稍长。如果数据在 L2 Cache 中命中,数据会被加载到 L1 Cache 中,然后处理器从 L1 Cache 中获取数据进行处理,同时更新相关的 Cache 状态信息。

L3 Cache 查找(L2 Cache 未命中时):若 L2 Cache 也未命中,处理器会继续在 L3 Cache 中查找。L3 Cache 通常是多个处理器核心共享的缓存,其容量更大,但访问速度也更慢。如果数据在 L3 Cache 中命中,数据会被依次加载到 L2 Cache 和 L1 Cache 中,然后处理器从 L1 Cache 中读取数据进行处理,并更新各级 Cache 的状态。

物理内存访问(L3 Cache 未命中时):当 L3 Cache 也未命中时,处理器不得不访问物理内存。通过内存控制器,根据数据的物理地址,从相应的内存模块中读取数据。数据从物理内存读取后,会首先加载到 L3 Cache 中,然后按照上述步骤依次加载到 L2 Cache 和 L1 Cache 中,最后处理器从 L1 Cache 中获取数据进行处理。同时,新的数据会替换 Cache 中一些不常用的数据(根据 Cache 的替换策略,如最近最少使用算法 LRU 等),以保持 Cache 的有效性和性能。

7.6 hello进程fork时的内存映射

代码段

父进程hello的代码段在内存中是只读的,包含了程序的指令。fork时,子进程会共享父进程的代码段,即子进程和父进程的代码段映射到相同的物理内存区域。这是因为程序的指令在运行过程中通常不会被修改,所以可以安全地共享,这样能节省内存空间。

数据段和堆

对于数据段(包括全局变量和静态变量等)和堆内存(用于动态分配内存),fork之后有两种常见的处理方式。

一种是采用写时复制(Copy - On - Write,COW)技术。在fork刚完成时,子进程和父进程的数据段和堆内存实际上是共享相同的物理内存页面。当父进程或子进程试图对某个页面进行写操作时,系统会为写操作的进程复制一份该页面的副本,让其在副本上进行修改,从而保证不同进程的数据相互隔离。

  • 另一种方式是在fork时就直接为子进程分配新的数据段和堆内存空间,并将父进程相应区域的数据复制到子进程的新空间中。这种方式相对简单,但可能会浪费一些内存,尤其是当父进程的内存空间较大且子进程实际上不需要修改很多数据时。

与数据段和堆类似,fork时子进程的栈通常也是与父进程共享(采用写时复制技术)或者直接复制一份。子进程有自己独立的栈空间,用于存储函数调用的局部变量、参数和返回地址等。虽然父子进程的栈在逻辑上是独立的,但在fork后的初始阶段,它们可能共享相同的物理内存,直到有写操作发生导致页面复制。

7.7 hello进程execve时的内存映射

加载可执行文件

execve首先根据指定的文件名找到可执行文件,并读取其头部信息,以确定文件的格式(如 ELF 格式)以及各种段的信息,包括代码段、数据段、BSS 段等。

然后,系统会为新程序的代码段和数据段分配虚拟内存空间,并将可执行文件中的相应内容映射到这些虚拟内存区域。代码段通常被映射为只读,数据段则根据其属性(如是否可写)进行相应的映射。

内存初始化

对于可执行文件中的初始化数据段,系统会将文件中的数据复制到新分配的虚拟内存空间中,确保程序在启动时能够正确访问到初始化的全局变量和静态变量等。

BSS 段(未初始化数据段)则在虚拟内存中被分配空间,但不会从可执行文件中复制数据,而是被初始化为全零。这是因为未初始化的全局变量和静态变量默认值为零,无需在可执行文件中占用额外的空间来存储。

栈和堆的设置

为新程序设置栈空间,通常栈是从高地址向低地址增长的。栈的初始大小一般是固定的,但在程序运行过程中可以根据需要动态扩展。

同时,也会为堆分配初始空间,堆是从低地址向高地址增长的,用于程序在运行时动态分配内存。与栈不同,堆的大小在程序运行过程中可以根据malloc、new等操作动态地增加或减少。

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

中断响应:硬件检测到缺页故障后,会暂停当前进程的执行,保存当前进程的现场信息,如程序计数器、寄存器值等,然后触发缺页中断,将控制权转移给操作系统的中断处理程序。

确定缺页地址:操作系统的缺页中断处理程序首先根据保存的现场信息,确定引发缺页故障的虚拟地址。通过该虚拟地址,可以进一步确定对应的页号。

查找页面置换算法:检查系统的内存使用情况,判断是否有空闲的物理页面可用。如果有空闲页面,则直接分配一个空闲页面;如果没有空闲页面,就需要根据一定的页面置换算法,选择一个在物理内存中暂时不用的页面,将其换出到外存,以便为即将调入的页面腾出空间。常见的页面置换算法有先进先出(FIFO)算法、最近最少使用(LRU)算法、最佳置换(OPT)算法等。

调入页面:确定了要置换的页面后,操作系统会启动磁盘 I/O 操作,将所需的页面从外存调入到物理内存中分配的页面位置。在页面调入过程中,进程会被阻塞,等待页面调入完成。

更新页表:页面调入成功后,操作系统会更新页表,将新调入页面的页表项中的有效位设置为 1,并填写正确的物理页号等信息,同时更新其他相关的页表项和控制信息,如访问位、修改位等,以反映页面的最新状态。

恢复进程执行:完成页表更新后,操作系统会恢复引发缺页中断的进程的现场信息,将程序计数器设置为引发缺页故障的指令地址,让进程继续执行。此时,由于所需页面已在物理内存中,进程可以顺利访问到相应的数据或指令。

7.9动态存储分配管理

       基本方法

空闲内存块管理

操作系统或运行时系统会维护一个数据结构来记录空闲内存块的信息,如空闲链表。链表中的每个节点代表一个空闲内存块,包含块的起始地址、大小以及指向下一个空闲块的指针等信息。这样可以方便地查找和分配空闲内存。

也有采用位图的方式,用一个二进制位来表示内存中的一个固定大小的块是否空闲,0 表示空闲,1 表示已分配。通过位图可以快速判断内存块的状态,但在分配和释放内存时需要进行位操作来更新位图。

内存分配算法

首次适应算法:从空闲链表的头部开始遍历,找到第一个大小足够满足请求的空闲内存块,然后将其分配给请求者。这种算法的优点是简单,且能快速找到满足条件的块,缺点是可能会导致内存碎片,因为每次都从头部开始查找,会使链表头部的空闲块越来越小。

最佳适应算法:遍历整个空闲链表,找到大小最接近请求大小的空闲内存块进行分配。这样可以尽量减少内存碎片,但缺点是查找过程比较耗时,而且可能会导致一些较小的空闲块难以被利用。

最坏适应算法:与最佳适应算法相反,它选择空闲链表中最大的空闲内存块进行分配。这种算法的优点是可以避免产生过小的碎片,缺点是可能会很快耗尽大的内存块,导致后续较大的内存请求无法满足。

策略

内存紧缩:随着程序的运行,动态分配和释放内存会导致内存碎片的产生。内存紧缩是一种整理内存的策略,它通过将所有已分配的内存块移动到连续的区域,使空闲内存块也集中在一起,从而消除碎片。但内存紧缩的成本较高,需要移动大量的数据,并且可能会暂停正在运行的程序。

虚拟内存管理:利用硬盘空间作为虚拟内存,将暂时不用的内存页面交换到硬盘上,当需要时再交换回物理内存。这样可以在物理内存有限的情况下,运行更大的程序。虚拟内存管理通过页表等数据结构来实现虚拟地址到物理地址的映射,同时结合页面置换算法来决定哪些页面需要被交换出去。

内存池技术:为了提高内存分配和释放的效率,尤其是对于频繁分配和释放小块内存的情况,可以使用内存池技术。内存池是在程序启动时预先分配一块较大的内存区域,然后将其划分成多个固定大小的小块内存。当程序需要分配内存时,直接从内存池中获取一个空闲的小块,而不需要每次都调用系统的内存分配函数。释放内存时,将小块内存放回内存池,而不是真正释放回操作系统。这样可以减少系统调用的开销,提高内存分配和释放的速度。

7.10本章小结

       本章主要介绍看hello和操作系统之间的交流方式,介绍了虚拟内存以及相关知识

8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

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

8.3 printf的实现分析

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

[转]printf 函数实现的深入剖析 - Pianistx - 博客园

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

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

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

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

(第81分)

结论

Hello经历 的过程主要有以下三个阶段:

1.编译链接阶段:经预处理、编译、汇编生成可重定位目标文件,通过链接解析符号依赖,生成可执行文件(ELF 格式)。

2.加载执行阶段:操作系统创建进程,映射程序到虚拟内存,CPU 按 ELF 入口执行指令,通过系统调用完成 IO 输出(如printf触发终端设备驱动)。

3.源回收阶段:进程终止时释放虚拟内存、文件描述符等资源,内核销毁进程控制块(PCB)。

我对计算机系统设计的一些感悟:

在计算机系统设计中,统一标准至关重要,如程序需遵循 ELF 格式、打印机通过统一接口工作。设计中也充满取舍,像虚拟内存解决内存不足但牺牲速度,缓存与主存配合平衡速度与容量。系统还具备自动适应能力,CPU 进程调度、内存换页机制可灵活应对任务。此外,系统设计旨在让程序员少操心,底层复杂事务由操作系统和硬件处理,程序员按规则调用函数即可,实现让上层简单、底层处理复杂的目标 。

附件

Hello 可执行文件

Hello.i 预处理之后的文件

Hello.o 目标文件

Hello.s 汇编语言文件

                                                

参考文献

[1]布莱恩特,哈洛。深入理解计算机系统 [M]. 3 版。龚奕利,贺莲,译。北京:机械工业出版社,2016.

[2] 豆包 [EB/OL]. 豆包. 2025 - 05 - 03.

[3] 腾讯元宝 [EB/OL]. 腾讯元宝 - 轻松工作 多点生活. 2025 - 05 - 03.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值