2024春HIT-CSAPP大作业

摘  要

本篇为计算机系统2024春大作业报告,目的是解释C语言程序从源代码转换为可执行文件的全过程。本文通过对一个简单的“hello”程序的生命周期进行详细分析,全面梳理了我们在CSAPP课程中所学的内容,分析了计算机在生成hello可执行文件的预处理、编译、汇编、链接、进程管理等整个生命周期并演示了这一系列生成过程的操作方法和结果。本文以最简单的hello的P2P过程阐述了计算机系统的工作原理和体系结构,以助于更深入理解C语言的编译执行过程。

关键词:C语言;计算机系统;生命周期

目  录

第1章 概述................................................................................... - 4 -

1.1 Hello简介............................................................................ - 4 -

1.2 环境与工具........................................................................... - 4 -

1.3 中间结果............................................................................... - 4 -

1.4 本章小结............................................................................... - 4 -

第2章 预处理............................................................................... - 6 -

2.1 预处理的概念与作用........................................................... - 6 -

2.2在Ubuntu下预处理的命令................................................ - 6 -

2.3 Hello的预处理结果解析.................................................... - 6 -

2.4 本章小结............................................................................... - 7 -

第3章 编译................................................................................... - 8 -

3.1 编译的概念与作用............................................................... - 8 -

3.2 在Ubuntu下编译的命令.................................................... - 8 -

3.3 Hello的编译结果解析........................................................ - 9 -

3.4 本章小结............................................................................. - 12 -

第4章 汇编................................................................................. - 14 -

4.1 汇编的概念与作用............................................................. - 14 -

4.2 在Ubuntu下汇编的命令.................................................. - 14 -

4.3 可重定位目标elf格式...................................................... - 14 -

4.4 Hello.o的结果解析........................................................... - 16 -

4.5 本章小结............................................................................. - 19 -

第5章 链接................................................................................. - 20 -

5.1 链接的概念与作用............................................................. - 20 -

5.2 在Ubuntu下链接的命令.................................................. - 20 -

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

5.4 hello的虚拟地址空间....................................................... - 23 -

5.5 链接的重定位过程分析..................................................... - 24 -

5.6 hello的执行流程............................................................... - 26 -

5.7 Hello的动态链接分析...................................................... - 26 -

5.8 本章小结............................................................................. - 26 -

第6章 hello进程管理.......................................................... - 28 -

6.1 进程的概念与作用............................................................. - 28 -

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

6.3 Hello的fork进程创建过程............................................ - 28 -

6.4 Hello的execve过程........................................................ - 29 -

6.5 Hello的进程执行.............................................................. - 30 -

6.6 hello的异常与信号处理................................................... - 30 -

6.7本章小结.............................................................................. - 32 -

第7章 hello的存储管理...................................................... - 33 -

7.1 hello的存储器地址空间................................................... - 33 -

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

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

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

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

7.6 hello进程fork时的内存映射......................................... - 35 -

7.7 hello进程execve时的内存映射..................................... - 36 -

7.8 缺页故障与缺页中断处理................................................. - 37 -

7.9动态存储分配管理.............................................................. - 37 -

7.10本章小结............................................................................ - 38 -

第8章 hello的IO管理....................................................... - 39 -

8.1 Linux的IO设备管理方法................................................. - 39 -

8.2 简述Unix IO接口及其函数.............................................. - 39 -

8.3 printf的实现分析.............................................................. - 39 -

8.4 getchar的实现分析.......................................................... - 41 -

8.5本章小结.............................................................................. - 42 -

结论............................................................................................... - 42 -

附件............................................................................................... - 44 -

参考文献....................................................................................... - 45 -

第1章 概述

1.1 Hello简介

P2P:即From Program to Process。指从hello.cProgram)变为运行时进程(Process)。要让hello.c这个C语言程序运行起来,需要先把它变成可执行文件,这个变化过程有四个阶段:预处理,编译,汇编,链接,完成后就得到了可执行文件,然后就可以在shell中执行它,shell会给它分配进程空间。

图 1‑1 P2P过程

020:即From Zero-0 to Zero-0。指最初内存并无hello文件的相关内容,shell用execve函数启动hello程序,把虚拟内存对应到物理内存,并从程序入口开始加载和运行,进入main函数执行目标代码,程序结束后,shell父进程回收hello进程,内核删除hello文件相关的数据结构。

1.2 环境与工具

1.2.1 硬件环境

X64 CPU:AMD Ryzen 7 5800H with Radeon Graphics 3.2GHz;16G RAM;512SSD Disk

1.2.2 软件环境

Windows11 64位; Vmware 16; Ubuntu 20.04 64位

1.2.3 开发工具

Vim;gcc;Terminal;objdump

1.3 中间结果

hello.i         预处理后得到的文本文件

hello.s         编译后得到的汇编语言文件

hello.o        汇编后得到的可重定位目标文件

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

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

1.4 本章小结

本章首先介绍了hello的P2P,020流程,包括流程的设计思路和实现方法;然后,详细说明了本实验所需的硬件配置、软件平台、开发工具以及本实验生成的各个中间结果文件的名称和功能。

第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念

在C语言中,预处理阶段是代码执行之前的一个重要步骤,负责对源代码进行宏替换、条件编译等处理。预处理器提供了强大的工具,使得我们能够在编写代码时更加灵活、高效。本博客将深入探讨C语言预处理的基本概念、常见指令以及它们在代码中的应用。通过了解预处理的机制,我们可以更好地理解C语言代码背后的运作原理,提高代码的可维护性和可读性。

2.1.2预处理的作用

预处理过程中并不直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换,主要有以下作用:

头文件包含:将所包含头文件的指令替代。

宏定义:将宏定义替换为实际代码中的内容。

条件编译:根据条件判断是否编译某段代码。

其他:如注释删除等。

简单来说,预处理是一个文本插入与替换的过程预处理器。

2.2在Ubuntu下预处理的命令

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

图 2‑1 预处理

2.3 Hello的预处理结果解析

打开hello.i文件,和hello.c进行对比,发现预处理指令被扩展成了几千行,而源程序其它部分不变。

图 2‑2 hello.i

在main函数前出现的几千行代码来自源文件的头文件<stdio.h><unistd.h> <stdlib.h>的展开。

#include命令的作用是把指定的文件模块内容插入到#include所在的位置,当程序编译链接时,系统会把所有#include指定的文件链接生成可执行代码。文件包含必须以#开头,表示这是编译预处理命令,行尾不能用分号结束。

#include所包含的文件,其扩展名可以是“.c”,表示包含普通C语言源程序。也可以是 “.h”,表示C语言程序的头文件。C语言系统中大量的定义与声明是以头文件形式提供的。当预处理器遇到#include<stdio.h>时,它会在系统的头文件路径下查找stdio.h文件,一般在/usr/include目录下,然后把stdio.h文件中的内容复制到源文件中。

通过#define包含进来的文件模块中还可以再包含其他文件,这种用法称为嵌套包含。嵌套的层数与具体C语言系统有关,但是一般可以嵌套8层以上。

2.4 本章小结

本章讲述了如何在linux环境中用命令对C语言程序进行预处理,以及预处理的含义和作用。然后用一个简单的hello程序演示了从hello.c到hello.i的过程,并用具体的代码分析了预处理后的结果。

第3章 编译

3.1 编译的概念与作用

3.1.1编译的概念

       编译器能够识别代码中的词汇、句子以及各种特定的格式,并将他们转换成计算机能够识别的二进制形式,这个过程称为编译(Compile)。

3.1.2编译的作用

编译过程将源代码由文本形式转换成机器语言,把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。在此过程中hello.i被转换为汇编语言文件hello.s。

编译也可以理解为“翻译”,类似于将中文翻译成英文、将英文翻译成象形文字,它是一个复杂的过程,大致包括词法分析、语法分析、语义分析、性能优化、生成可执行文件五个步骤,期间涉及到复杂的算法和硬件架构。          

3.2 在Ubuntu下编译的命令

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

图 3‑1 编译

处理后可发现当前工作目录下多了一个hello.s文件。

3.3 Hello的编译结果解析

3.3.1初始部分

在main函数前有一部分字段展示了节名称:

图 3‑2 节名称部分

.file声明源文件”hello.c”

.text表示代码节

.section和.rodata表示只读数据段

.align声明对指令或数据的存放地址进行对齐的方式

.string声明一个字符串,即printf输出的串

.globl声明全局符号main

.type声明main的类型为function

3.3.2全局函数

hello.c中只声明了一个全局函数int main(int arge,.char*argv[]):

图 3‑3 全局函数main

3.3.3数据部分

数据中的常量包括数和字符串。比较if(argc != 5)时,5以立即数的形式表示:

图 3‑4 if的汇编形式

字符串常量保存在.rodata区域,需要调用时将其存入寄存器:

图 3‑5 字符串常量

hello.c中的变量主要是循环的局部变量i和main函数参数argc和argv,全局变量存储在数据区,而局部变量存储在栈里或寄存器里。main函数的参数共两个为int型,按顺序存储在%edi,%esi中,而变量i存在寄存器%ebp中,且赋初值为0:

图 3‑6 循环变量i

3.3.4赋值操作

       由数据部分的变量i可知,对i的赋值操作使用movl实现,因为i是int型32位变量。

3.3.5类型转换操作

通过C语言源码文件我们发现需要将argv[4]转换为整型并作为sleep函数的参数,这里我们调用atoi实现类型转换,将字符串转为整数:

图 3‑7 atoi函数调用

3.3.6算数操作

for循环每次循环后i自增1,该操作通过addl实现,i存放在-4(%rbp)中:

图 3‑8 循环自增

3.3.7关系操作和控制转移指令

hello.c中有两个关系操作。

if(argc!=5):在汇编代码中使用cmpl指令比较立即数5和参数argc的大小并设置条件码,如果相等则跳转到L2:

图 3‑9 参数数量校准

For循环中每次循环都将i与立即数9比较,如果大于等于9则退出循环:

图 3‑10 循环跳出

3.3.8数组操作

在hello.c中main函数参数argv为一个字符指针数组的头指针,64位运行环境下,每个指针长度位8个字节。

图 3‑11 导入参数数组

如图所示,argv地址保存到堆栈中,调用argv[1],argv[2],argv[3]时,采取相对寻址方式,直接找-8(%rbp)、-16(%rbp)、-24(%rbp)即可。

3.3.9函数操作

(1)main函数

参数传递:该函数的参数为int argc,,char*argv[]。具体参数传递地址和值都在前面阐述过。

函数调用:通过使用call内部指令调用语句进行函数调用,并且将要调用的函数地址数据写入栈中,然后自动跳转到这个调用函数内部。main函数里调用了printf、exit、sleep函数。

局部变量:使用了局部变量i用于for循环。具体局部变量的地址和值都在前面阐述过。

(2)printf函数

参数传递:printf函数调用参数argv[1],argv[2]。

函数调用:该函数调用了两次。第一次将寄存器%rdi设置为待传递字符串"用法:Hello学号 姓名 手机号 秒数!\n"的起始地址;第二次将其设置为“Hello %s  %s %s\n”的起始地址。具体已在前面讲过。使用寄存器%rsi完成对argv[1]的传递,用%rdx完成对argv[2]的传递。

(3)exit函数

参数传递与函数调用:

图 3‑12 调用立即数1

将rdi设置为1,再使用call指令调用函数。

(4)atoi、sleep函数

参数传递与函数调用:

图 3‑13 函数调用

可见,atoi函数将参数argv[3]放入寄存器%rdi中用作参数传递,简单使用call指令调用。

图 3‑14 call指令

然后,将转换完成的秒数从%eax传递到%edi中,edi存放sleep的参数,再使用call调用。

(5)getchar函数

无参数传递,直接使用call调用即可。

3.4 本章小结

本章主要详细介绍了编译的概念与作用,以及编译的指令,最后对编译文件hello.s按照PPT中P4给出的参考对以下方面进行了解释:数据(常量、变量)、类型转换、算术操作、关系操作、数组、控制转移、函数操作,比较了源代码和汇编代码分别是怎样实现这些操作的。

第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

       汇编是指汇编器(as)将包含汇编语言的hello.s文件翻译为机器语言指令,并把这些指令打包成为一个可重定位目标文件的格式,将结果保存在文件hello.o中。.o文件是一个二进制文件,包含main函数的指令编码。

4.1.2汇编的作用

       汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式。 .o 文件是一个二进制文件,它包含程序的指令编码。

4.2 在Ubuntu下汇编的命令

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

图 4‑1 汇编

工作目录下多了一个名称为hello.o的二进制文件。

4.3 可重定位目标elf格式

图 4‑2 ELF头

ELF头如上图,它以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含了帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如X86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每一个节都有一个固定大小的条目。

图 4‑3 节头

节头如上图,它记录各节名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。

图 4‑4 重定位节

重定位节如上图,即.rela.text的内容,其中描述了.text节中需要被修正的信息、任何调用外部函数或者引用全局变量的指令都需要被修正、调用外部函数的指令需要重定位、引用全局变量的指令需要重定位、调用局部函数的指令不需要重定位,在可执行目标文件中不存在重定位信息。

图 4‑5 符号表

符号表如上图,符号表包含一个条目的数组,存放一个程序定义和引用的全局变量和函数的信息。

4.4 Hello.o的结果解析

图 4‑6 hello.o的反汇编结果

通过指令生成的反汇编文件如上图。与hello.s比较:

图 4‑7 hello.s

可发现以下不同之处:

1)增加机器语言

每一条指令增加了一个十六进制的表示,即该指令的机器语言。

2)分支转移

hello.s中的分支语句是通过段跳转的方式实现的,而反汇编文件中的分支语句是通过直接跳转到地址的方式实现的。

3)函数调用

hello.s文件中,函数调用call后跟的是函数名称,而在hello.o文件中,call后跟的是下一条指令。这是因为 hello.c中调用的函数都是共享库中的函数,只有在最终运行时才会通过动态链接器判断最终执行时的函数地址,而在汇编成为机器语言的时候,对于这些不确定地址的函数调用,我们在.rela.text 节中为其添加了重定位条目,反汇编时其已具有地址。

4)操作数进制

在hello.s文件中数字由10进制表示,而在反汇编文件中数字由16进制表示,其因为在机器语言中,16进制转2进制比10进制容易。

4.5 本章小结

这一章介绍了汇编的含义和功能。以Ubuntu系统下的hello.s文件为例,说明了如何把它汇编成hello.o文件,并生成ELF格式的可执行文件hello.elf。以及部分在源码文件中出现的汇编的命令。同时对可重定位目标ELF格式进行了分析,对hello.o文件进行反汇编,与hello.s文件进行了对比,让人清楚地理解了汇编语言到机器语言的转换过程,以及机器为了链接而做的准备工作。

5章 链接

5.1 链接的概念与作用

5.1.1链接的概念

链接(linkng)是将各种代码和数据片段收集并组合为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行与编译时(compile time),也就是在源代码被翻译为机器代码时;也可以执行与加载时(load time),也就是程序被加载器加载到内存并执行时:甚至执行于运行时。

5.1.2链接的作用

在现代系统中,链接是由叫做链接器(1iker)的程序自动执行的。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用。

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

可见工作目录下多了可执行文件hello

图 5‑1 链接

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

使用readelf查看hello可执行文件的ELF格式,其分为五个部分

1)ELF头

图 5‑2 ELF头

       Hello的ELF头与hello.s的ELF头包含的信息基本相同,而类型发生改变,获得了入口点地址。

2)节头         

图 5‑3 节头

       描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。

3)程序头

图 5‑4 程序头

程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。

4)动态区

图 5‑5 动态区

5)符号表

图 5‑6 符号表

5.4 hello的虚拟地址空间

edb的Data Dump窗口。窗口显示虚拟地址由0x401000开始,从开始到结束这之间的每一个节对应5.3中的每一个节头表的声明。

图 5‑7 edb调试界面

根据5.3中的节头部表,可以通过edb找到各段的信息。如.text节,在elf文件中可以看到开始的虚拟地址:

图 5‑8 .text虚拟地址

在edb中找到对应信息:

图 5‑9 定位.text节

5.5 链接的重定位过程分析

对hello反汇编的结果:

图 5‑10 反汇编结果

与第四章的hello.s的反汇编结果比较,其不同之处有:

1)函数调用指令call和跳转指令的参数发生变化

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

图 5‑11 地址修改

2)新增了一些函数

动态链接器将共享库中hello.c用到的函数加入可执行文件中。

图 5‑12 新增函数

附:重定位过程地址计算方法如下:

图 5‑13 重定位地址计算方法

5.6 hello的执行流程

使用gdb/edb执行hello,从加载hello到_start,到call main,以及程序终止的所有过程如下:

ld-2.23.so!_dl_start

ld-2.23.so!_dl_init

hello!_start

libc-2.23.so!_libc_start_main

libc-2.23.so!_cxa_atexit

libc-2.23.so!_libc_csu.init

hello!_init

libc-2.23.so _setjmp

libc-2.23.so exit

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。

通过对比反汇编文件中地址可知GOT地址,之后在edb中找他其对应地址并运行比较数据前后变换。

图 5‑14 GOT地址

5.8 本章小结

本章介绍了链接的概念及作用以及命令,对hello的elf格式进行了详细的分析对比。以及hello的虚拟地址空间知识,并通过反汇编hello文件,将其与hello.o反汇编文件对比,了解了重定位过程,对链接有了更深的理解。

6章 hello进程管理

6.1 进程的概念与作用

6.1.1进程的概念

狭义上,进程是正在运行的程序的实例。广义上,进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

6.1.2进程的作用

进程为程序提供了一种假象,程序好像是独占的使用处理器和内存,处理器好像是无间断地一条接一条地执行我们程序中的指令。进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中。

进程提供给应用程序两个关键抽象:一个独立的逻辑控制流,一个私有的地址空间。

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

6.2.1 Shell-bash的作用

从字面上理解,shell代表一个壳,是操作系统的最外层,shell可以合并编程语言以控制进程和文件,为用户提供一个操作界面,接受用户输入的命令,并调度相应的应用程序。

6.2.2 Shell-bash的处理流程

(1)从键盘读取并解析指令;

(2)如果指令是内部命令,则立即处理它;

(3)如果指令是可执行文件,则在子进程的上下文中加载和运行它;

(4)如果指令以“&”结尾,则让作业在后台执行,继续接收下一个命令;否则在前台执行作业,等待作业结束再接收下一个命令。

6.3 Hello的fork进程创建过程

首先用户再shel1界面输入指令:./hel1o 2022112047 张子鉴 18545793266 1

Shell判断该指令不是内置命令,于是父进程调用fork函数创建一个新的子进程,该子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程与父进程最大的区别就是具有不同的PID。在父进程中,fork返回子进程的PID,而在子进程中fork返回0,返回值提供一个明确的方法来分辨程序是父进程还是在子进程中执行。

6.4 Hello的execve过程

execve函数加载并运行可执行文件filename,替换当前进程的虚拟内存布局。成功时,它删除当前进程的内存段,并用新程序的代码、数据、堆和栈替换。新栈和堆段初始化为零。接着,加载器设置环境并调用新程序的入口点(如_start),最终将控制传递给main函数。若出错,execve返回错误码给调用者,不改变当前进程状态。

子进程通过execve函数系统调用启动加载器,删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。将新的栈和堆段初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后加载器调到_start处,最终调用应用程序的main函数。

Main函数运行时用户栈如图:

图 6‑1 用户栈

6.5 Hello的进程执行

hello进程具有独立的逻辑控制流,这通过计时器实现,当计时器减为0,控制从进程切换到内核,内核将进行上下文切换,将控制转移给其他进程。此时hello进程暂停执行。一个进程执行它的控制流的一部分的每一时间段叫做时间片。此外,在hello进程遇到异常时,控制权也会转移到内核,由内核进行系统调用操作。

当控制切换回hello进程时,hello进程就继续执行。在这种调度方式下,尽管同时只有一个进程在cpu运行,但可以近似地认为进程并行执行。

cpu通常用某个控制寄存器中的一个模式位提供两种模式的区分功能,该寄存器描述了进程当前享有的特权。

6.6 hello的异常与信号处理

hello执行过程中可能出现的异常包括中断、陷阱、故障、终止等。

图 6‑2 正常运行

程序正常运行时打印10次提示信息,再输入回车结束进程。

图 6‑3 ctrl-c

输入ctrl-c,hello进程被终止。

图 6‑4 ctrl-z

输入ctrl-z,hello进程被挂起。

图 6‑5 fg

运行fg命令,hello任务会继续在前台运行。

图 6‑6 kill

运行kill命令,可将挂起的进程杀死。

图 6‑7 pstree

运行pstree,可看到hello进程为terminal的子进程。

图 6‑8 不停乱按

不停乱按的输入缓存到stdin,会被getchar读走,不对函数运行造成影响。

6.7本章小结

本章介绍了进程的概念,说明了shell的处理流程,介绍了hello程序的进程创建、加载、进程执行以及各种异常与信号处理的相关内容。异常控制流发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。

7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1逻辑地址

逻辑地址是指由程序hello产生的与段相关的偏移地址部分(hello.o)。逻辑地址是由一个段标识符加上一个指定段内相对地址的偏移量,由程序hello产生的与段相关的偏移地址部分。

7.1.2线性地址

逻辑地址的段地址+偏移地址得到线性地址;线性地址空间是非负整数地址的有序集合。

7.1.3虚拟地址

虚拟地址与线性地址是相似的;虚拟地址空间是一个拥有2的n次方个地址的有序集合,hello运行在虚拟地址空间中。

7.1.4物理地址

在存储器里以字节为单位存储信息,每一个字节单元给一个唯一的存储器地址,这个地址称为物理地址,是hello的实际地址或绝对地址。

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

逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。通过段选择符在GDT或LDT中找到对应的段描述符,进而获取段的首地址,并将其与偏移量结合,形成线性地址,最终实现对内存数据的准确访问。在保护模式下,以段描述符作为下标,到GDT或者LDT中查表获得段地址。段地址+偏移地址=线性地址。

全局描述符表(GDT)整个系统只有一个,它包含:(1)操作系统使用的代码段、数据段、堆栈段的描述符(2)各任务、程序的LDT(局部描述符表)段。

每个任务程序有一个独立的LDT,包含:(1)对应任务/程序私有的代码段、数据段、堆栈段的描述符(2)对应任务/程序使用的门描述符:任务门、调用门等。

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

线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。若命中,将缓存的PPN返回给MMU。若不命中,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。页式管理图示:

图 7‑1 页式管理

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

Intel i7的地址翻译用到了TLB与四级页表的技术,其中虚拟地址48位,物理地址52位,页表大小为4KB,页表为4级。页表大小是4KB,VPO为12位,VPN是36位。每一个PTE条目是8字节,每一个页表就有512个条目,那么VPN中每一级页表对应的索引就有9位。对于TLB,因为有16组,所以TLBI就是4位,TLBT就是32位。

工作时,CPU产生虚拟地址VA,传给MMU,在TLB中搜索,如果命中,就得到其中的40位物理页号PPN,与12位VPO合并成52位的物理地址PA。如果没有命中,MMU就向内存中的页表请求PTE。VPN1是第一级页表中的偏移量,用来确定二级页表的起始地址,VPN2又作为第二级页表的偏移量,以此类推,最后在第四级页表中找到对应的PPN,与VPO组合成物理地址PA。

工作原理如下:

图 7‑2 工作原理

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

高速缓存的结构将m个地址位划分成了t个标记位,s个组索引位和b个块偏移位:

图 7‑3 地址结构

得到了物理地址VA,首先使用物理地址的CI进行组索引(每组8路),对8路的块分别匹配 CT进行标志位匹配。如果匹配成功且块的valid标志位为1,则命中hit。然后根据数据偏移量 CO取出数据并返回。

若没有匹配成功或者匹配成功但是标志位为1,即为缓存不命中。需要向下一级缓存请求数据(L2 Cache、L3 Cache、主存)。取出数据后会写入或替换当前缓存。

在更新cache的时候,需要判断是否有空闲块。若有空闲块(即有效位为0),则写入;若不存在,则进行驱逐一个块(LRU策略)。

7.6 hello进程fork时的内存映射

当fork函数被新进程调用时,内核为新进程创建各种数据结构,并分配给它唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当两个进程中的任意一个对虚拟内存的某个页面进行写操作时,触发保护故障,此时写时复制机制会在内存中创建一个对应页面的副本,并更新页表条目指向副本。此时进程对于内存的修改就会反应在副本页面上。

图 7‑4 fork内存映射

7.7 hello进程execve时的内存映射

在通过execve加载新进程时,需要以下几个步骤:

删除已存在的用户区域。 删除当前进程虚拟地址的用户部分中的已存在的区域结构;

映射私有区域。为新程序(即hello)的代码、数据、bss和栈区域等创建新的区域结构。所有这些区域都是私有的、写时复制的。

映射共享区域。 hello与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内;

设置程序计数器PC。最后设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

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

缺页故障的产生:在对于虚拟地址进行翻译时,如果发现对应的PTE的有效位未设置,说明虚拟地址对应的内容还没有缓存在物理内存中,此时会触发一个缺页故障。

如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。处理程序执行如下步骤:

检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。

检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。

两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对每个进程,内核维护一个全局变量brk指向堆顶。而分配器分为两种基本风格:显式分配器和隐式分配器。

分配器将堆视为一组不同大小的块的集合来维护。每个块是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。

7.10本章小结

本章介绍了计算机中的存储,地址空间的分类,地址的变换规则,虚拟内存的原理,cache的工作,和动态内存的分配。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

一个linux文件就是一个多个字节的序列。所有的IO设备都被模型化为文件,例如GPIO接口等,被模型化为gpio01等文件。这种称为Unix I/O。

设备管理:unix io接口

这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。Linux就是基于Unix I/O实现对设备的管理。

8.2 简述Unix IO接口及其函数

Unix IO接口是Unix操作系统中用于进行输入输出操作的一组函数。这些函数为程序提供了一种标准化的方式来处理文件、设备和其他I/O资源。Unix IO接口主要包括以下几种类型的函数:

打开和关闭文件

open():用于打开一个文件,并返回一个文件描述符。

close():用于关闭一个文件描述符。

读写文件

read():从文件描述符中读取数据。

write():向文件描述符中写入数据。

文件定位

lseek():在文件中移动文件指针。

文件控制

fcntl():对文件描述符进行各种操作,如复制、设置文件描述符标志等。

8.3 printf的实现分析

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

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

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

图 8‑1 printf

以上为printf函数内的内容。其内部调用了vsprintf和write,接下来我们查看vsprintf的内容。

图 8‑2 vsprintf

vsprintf接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。

然后我们分析一下write(buf, i)

图 8‑3 write

对于其中的系统函数write,其作用就是将buf中i个元素值写入终端。

图 8‑4 syscall

对于syscall函数,我们进行分析可知其作用为将输入字符串内容读取,得到对应ASCII码后复制到显卡显存中,然后显卡找到对应RGB值将其输出至屏幕。

8.4 getchar的实现分析

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

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。当getchar被调用时,它会试图缓冲区中读取一个字符。如果缓冲区为空,getchar将调用read系统函数来从键盘缓冲区中读取数据到自身的缓冲区(直到接收到回车键才返回)。当缓冲区填充完成之后,getchar函数从缓冲区中读取一个字符后返回。

8.5本章小结

运行至getchar时,os控制。当你键入时,内容进入缓存。按enter,通知 os输入完成。

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

结论

hello代码从键盘输入,之后要经过以下步骤:

1、预处理(cpp)。将文件调用的所有外部库文件合并展开,生成hello.i。

2、编译(ccl)。将hello.i翻译成汇编语言文件hello.s。

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

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

5、输入命令。在shel1中输入./hello 2022112047 张子鉴 18545793266。

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

7、加载程序。由execve函数加载运行当前进程的上下文中加载并运行新程序hello。

8、IO管理:hello再运行时会调用一些函数,比如printf函数,这些函数与linux I/O的设备模拟化密切相关。

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

10、终止:hello最终被shell父进程回收,内核会收回为其创建的所有信息。

感悟:

通过本次实验,我深切感受到计算机系统的精细和强大,每一个简单的任务都需要计算机的各种复杂的操作来完成,这背后体现出了严谨的逻辑和现代工艺的精巧。或许我只是众多不起眼的进行计算机工作的学生之一,或许我和这小小的hello一样为计算机行业服务却又终将被人遗忘。所以我要努力学习计算机系统的知识,用新的学识武装自己。以免还没赶上前浪就被后浪拍死在沙滩上。XD

附件

文件名

功能

hello.c

源程序

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello1.asm

反汇编hello得到的反汇编文件

hello.asm

反汇编hello.o得到的反汇编文件

hello

可执行文件

参考文献

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

[2]   https://www.cnblogs.com/buddy916/p/10291845.html

[3]   https://www.cnblogs.com/pianist/p/3315801.html

[4]   https://www.cnblogs.com/fanzhidongyzby/p/3519838.html.

[5]   https://www.cnblogs.com/diaohaiwei/p/5094959.html

  • 27
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值