2023春 哈工大CSAPP大作业程序人生报告

第1章 概述

1.1 Hello简介

1.1.1 P2P :From Program to Process

在Linux系统下,源文件hello.c经gcc编译器驱动程序读取之后,依次经过五个阶段:

(1)预处理cpp(C Pre-Processor)源程序hello.c生成hello.i文本文件。

(2)编译ccl(C Compiler)将hello.i翻译成汇编程序hello.s。

(3)汇编as (Assembler)将hello.s翻译成机器语言指令,并将这些指令打包成可重定位目标程序hello.o。

(4)链接ld(Linker)将hello.o与库函数相链接最终成为可执行目标程序hello(在Linux下该文件无固定后缀)。

(5)执行hello程序时,在shell中输入命令./hello,shell 通过fork函数产生子进程,hello 便从可执行程序(Program)变成为进程(Process)。

1.1.2 020 :From Zero-0 to Zero-0

初始时,内存中不存在hello文件的相关内容,即“From 0”。

在Shell下调用execve函数,加载并运行可执行目标文件hello,操作系统为其分配虚拟内存空间,在物理内存与虚拟内存之间建立映射。系统会将hello文件载入内存,执行相关代码。

当程序运行结束后, hello进程被父进程回收,对应的虚拟空间以及相关数据结构被释放,这即为“to 0”。

1.2 环境与工具

硬件:AMD Ryzen7 5000H,X86-64

软件:Windows 11 64位,Ubuntu 20.04

调试工具:vim,codeblocks,gedit,gcc,readelf, objdump, hexedit, edb

1.3 中间结果

表 1-1 中间结果文件

文件名

功能

hello.i

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

hello.s

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

hello.o

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

hello_elf.txt

用readelf读取hello.o得到的ELF格式信息

hello_asm.txt

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

hello_elf_2.txt

由hello可执行文件生成的.elf文件

hello_asm2.txt

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

1.4 本章小结

本章简述了hello 的P2P以及020的过程,同时列出了此次大作业采用的具体软硬件环境和工具,以及hello.c所生成的中间结果文件。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

2.1.1 预处理的概念

预处理步骤是指程序开始运行时,预处理器cpp根据以字符#开头的命令,修改原始的C程序的过程。除此之外,预处理过程还会删除程序中的注释和多余的空白字符。预处理通常得到另一个以.i作为拓展名的C程序。

2.1.2 预处理的作用

预处理过程将#include后继的头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理。预处理过程中并未直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换。

2.2在Ubuntu下预处理的命令

在Ubuntu系统下,进行预处理的命令为:

图 2-1 在Ubuntu下的预处理命令

2.3 Hello的预处理结果解析

比起hello.c文件, hello.i的行数大幅增加,拓展到3060行。

前半部分是头文件 stdio.h,unistd.h和stdlib.h 的声明函数、定义结构体、定义变量、定义宏等内容。内容插入的具体流程如下(以unistd.h为例):CPP先删除指令#include <unistd.h>,并到Ubuntu系统的默认的环境变量中寻找 unistd.h,最终打开路径/usr/include/unistd.h下的unistd.h文件。若unistd.h文件中使用了#define语句,则按照上述流程继续递归地展开,直到所有#define语句都被解释替换掉为止。同时,CPP还会进行删除程序中的注释和多余的空白字符等操作,并对一些值进行替换。后半部分是hello.c的main函数,相关代码在hello.i程序中对应着3047行到3060行。

图 2-2 hello.i文件部分代码展示

2.4 本章小结

本章介绍了预处理的概念及作用,并解析了hello.c预处理之后得到的预处理结果hello.i程序。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

编译是指C编译器ccl通过词法分析和语法分析,将合法指令翻译成等价汇编代码的过程。通过编译过程,编译器将文本文件 hello.i 翻译成汇编语言文件 hello.s。

3.1.2 编译的作用

通过以下五个阶段把人们熟悉的高级语言语言换成计算机能解读、运行的低级语言:

(1)词法分析:对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。

(2)语法分析:以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。

(3)语义检查和中间代码生成:由语义分析器完成,指示判断是否合法,并不判断对错。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。

(4)代码优化:对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。

(5) 目标代码生成:把语法分析后或优化后的中间代码变换成目标代码。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

在Ubuntu系统下,进行预处理的命令为:

图 3-1 在Ubuntu下的编译命令

3.3 Hello的编译结果解析

3.3.1文件结构分析

表 3-1 hello.s文件结构

内容

含义

.file

源文件

.text

代码段

.global

全局变量

.data

存放已经初始化的全局和静态C 变量

.section  .rodata

存放只读变量

.align

对齐方式

.type

表示是函数类型/对象类型

.size

表示大小

.long  .string

表示是long类型/string类型

3.3.2 数据类型

一、整数

(1)int i:i作为函数内部的局部变量,并不占用文件实际节的空间,只存在于运行时栈中。对于i的操作就是直接对寄存器或栈进行操作。

(2)int argc:argc是main函数的参数之一,由寄存器传入。

(3)立即数4:立即数4在汇编语句中直接以$4的形式出现

二、字符串

程序中保存了两个字符串常量,储存在.text数据段中,分别为:

图 3-2 字符串的情况

其中,\XXX为UTF-8编码,一个汉字对应三个字节。

三、数组

程序中涉及的数组为char *argv[],即函数的第二个参数。在hello.s中,其首地址保存在栈中。访问时,通过寄存器寻址的方式访问。

图 3-3 数组的情况

3.3.3 赋值操作

对i进行赋值,即将0赋给i,在汇编代码中对应如下:

图 3-4 对变量i赋值0的操作

3.3.4 类型转换

hello.i中类型转换体现在“atoi(argv[3])”上,即将字符串参数转换为整型参数,需调用atoi函数,在汇编代码中对应如下:

图 3-5 汇编代码call atoi操作

3.3.5 算数操作

  1. addq $16/8/24, %rax:修改地址偏移量
  2. addl $1, -4(%rbp):实现i++的操作

图 3-6 hello.s中涉及的算数操作

3.3.6关系操作

  1. argc!=4:在hello.s中,使用cmpl $3,-20(%rbp),比较 argc与4的大小并设置条件码,为下一步je利用条件码进行跳转作准备。

      图 3-7 检查argc!=4

  1. i<8:

在hello.s中,使用cmpl      $7, -4(%rbp)比较i与7的大小,然后设置条件码,为下一步jle利用条件码进行跳转做准备。

      图 3-8 检查 i<8

3.3.7数组操作

如3.3.2所述,hello.s中存在数组char *argv[],对其访问操作通过寄存器寻址方式实现。

3.3.8 控制转移

  1. if(argc!=4):当argc不等于4时,执行函数体内部的代码。在hello.s中,使用cmpl $4,-20(%rbp),比较 argc与4是否相等,若相等,则跳转至.L2,不执行后续部分内容;若不等则继续执行函数体内部对应的汇编代码。

图 3-9 if 控制转移

  1. for(i=0;i<8;i++):当i < 8时进行循环,每次循环i++。在hello.s中,使用cmpl $7,-4 (%rbp),比较 i与7是否相等,在i<=7时继续循环,进入.L4,i>7时跳出循环。

      图 3-10 循环的情况

3.3.9 函数操作

hello.i中一共涉及5个函数调用,即main、printf、exit、sleep、getchar。在汇编语言中,寄存器一般都有特定用途,例如%rax存储返回值,%rdi存储第一个参数,%rsi存储第二个参数等。其次,若有多个(大于6个)参数,则将参数存储在栈中,如图所示,调用函数前都有mov指令,设置不同的参数,最典型的就是调用atoi和sleep函数前都将寄存器%rax中的值传送到储存第一个参数的寄存器%rdi中,而像getchar这样没有参数的函数,则无需执行以上操作。

图 3-11 call指令的情况

如图所示,C语言中main函数返回0,那么汇编代码中则对应为“movl $0, %eax”,“leave”,“ret”,即将0传送到%eax,然后使用leave恢复调用者栈帧,清理被调用者栈帧,最后使用ret指令返回。

图 3-12 ret指令的情况

3.4 本章小结

本章介绍了编译的概念与作用,编译是将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备的过程。以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作,验证了大部分数据、操作在汇编代码中的实现。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

汇编是指汇编器(assembler)将以.s结尾的汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在.o 目标文件中的过程。

4.1.2汇编的作用

将汇编语言翻译为机器语言,并将相关指令以可重定位目标程序格式保存在.o文件中。

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

在Ubuntu下汇编的命令为:

图 4-1 汇编的命令

4.3 可重定位目标elf格式

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

首先,在shell中输入指令获得 hello.o 文件的 ELF 格式:

图 4-2 生成ELF文件

其结构分析如下:

  1. ELF 头(ELF Header):

以 16字节序列 Magic 开始,其描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等相关信息。

图 4-3 ELF头的情况

  1. 节头:

包含了文件中出现的各个节的意义,包括节的类型、位置和大小等信息。

图 4-4 节头的情况

  1. 重定位节.rela.text

重定位节是一个.text节中位置的列表,包含.text节中需要进行重定位的信息,在链接时用于重新修改代码段的指令中的地址信息。如图所示,需要重定位的信息有puts,exit,printf,atoi,sleep,getchar及.rodata中的模式串。

图 4-5 .rela.text节

  1. 重定位节.rela.eh_frame

图 4-6 .rela.eh_frame节

  1. 符号表Symbol table

符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。

图 4-7 符号表的情况

4.4 Hello.o的结果解析

图 4-8 生成hello.asm文件

通过对比hello_asm.txt与hello.s可知,两者在如下地方存在差异:

  1. 分支转移:

在hello.s中,跳转指令的目标地址直接记为段名称,如.L2,.L3等。而在反汇编得到的hello.asm中,跳转的目标为具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址之差。

图 4-8 分支转移

  1. 函数调用:

在hello.s文件中,call之后直接跟着函数名称,而在反汇编得到的hello.asm中,call 的目标地址是当前指令的下一条指令。这是因为 hello.c 中调用的函数都是共享库中的函数,最终需要通过动态链接器作用才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全0(此时,目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接进一步确定。

图 4-9 函数调用

  1. 全局变量访问:

在hello.s 文件中,使用段名称+%rip访问 rodata(printf 中的字符串),而在反汇编得到的hello.asm中,使用 0+%rip进行访问。其原因与函数调用类似,rodata 中数据地址在运行时才能确定,故访问时也需要重定位。在汇编成为机器语言时,将操作数设置为全 0 并添加相应的重定位条目。

图 4-10 全局变量访问

4.5 本章小结

本章介绍了汇编的概念与作用,在Ubuntu下将hello.s文件翻译为hello.o文件,并生成hello.o的ELF格式文件hello.elf,研究了ELF格式文件的具体结构。通过比较hello.o的反汇编代码(保存在hello.asm中)与hello.s中代码,了解了汇编语言与机器语言的异同之处。

(第41分)

5章 链接

5.1 链接的概念与作用

5.1.1 链接的概念

链接是指通过链接器(Linker),将程序编码与数据块收集并整理成为一个单一文件,生成完全链接的可执行的目标文件(windows系统下为.exe文件,Linux系统下一般省略后缀名)的过程。

5.1.2 链接的作用

提供了一种模块化的方式,可以将程序编写为一个较小的源文件的集合,且实现了分开编译更改源文件,从而减少整体文件的复杂度与大小,增加容错性,也方便对某一模块进行针对性修改。

注意:这儿的链接是指从 hello.o 到hello生成过程。

5.2 在Ubuntu下链接的命令

在Ubuntu下链接的命令如下:

图 5-1 链接的命令

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

在Shell中生成 hello 程序的 ELF 格式文件,保存为hello_elf_2.txt:

图 5-2 生成ELF文件

  1. ELF 头(ELF Header)

Hello_elf_2.txt中的ELF头与hello.elf中的ELF头包含的信息种类基本相同。

图 5-3 ELF头的情况

  1. 节头

节头表是描述目标文件的节,包含了26个节的名字、类型、地址、偏移量等信息。与hello.elf相比,其在链接之后的内容更加丰富详细(此处仅截取部分展示)。

图 5-4 节头的情况

  1. 程序头

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

图 5-5 程序头的部分

  1. Dynamic section

图 5-6 Dynamic Section

  1. Symbol table

符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明(此处仅截取部分展示)。

图 5-7 Symbol table

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息。例如,在ELF格式中,.rodata段的地址为402000,那么在edb的Data Dump中找到地址0x402000并查看,如图24所示,可以看到printf里面的内容“Hello %s %s”,以此类推,还可以看到各个段详细信息。

图 5-8 加载到虚拟地址的程序代码

5.5 链接的重定位过程分析

在Shell中使用命令objdump -d -r hello > hello_asm2.txt生成反汇编文件hello_asm2.txt,与第四章中生成的hello_asm.txt文件进行比较,其不同之处如下:

图 5-9 生成反汇编文件

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

图 5-10 链接后的函数

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

图 5-11 call指令的参数

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

图 5-12 跳转指令的参数

5.6 hello的执行流程

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

表格 7 程序名称与程序地址

程序名称

程序地址

ld-2.27.so!_dl_start

0x7fce8cc38ea0

ld-2.27.so!_dl_init

0x7fce8cc47630

hello!_start

0x400500

libc-2.27.so!__libc_start_main

0x7fce8c867ab0

-libc-2.27.so!__cxa_atexit

0x7fce8c889430

-libc-2.27.so!__libc_csu_init

0x4005c0

hello!_init

0x400488

libc-2.27.so!_setjmp

0x7fce8c884c10

-libc-2.27.so!_sigsetjmp

0x7fce8c884b70

--libc-2.27.so!__sigjmp_save

0x7fce8c884bd0

hello!main

0x400532

hello!puts@plt

0x4004b0

hello!exit@plt

0x4004e0

*hello!printf@plt

--

*hello!sleep@plt

--

*hello!getchar@plt

--

ld-2.27.so!_dl_runtime_resolve_xsave

0x7fce8cc4e680

-ld-2.27.so!_dl_fixup

0x7fce8cc46df0

--ld-2.27.so!_dl_lookup_symbol_x

0x7fce8cc420b0

libc-2.27.so!exit

0x7fce8c889128

5.7 Hello的动态链接分析

编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,在GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。

.got与.plt节保存着全局偏移量表GOT,其内容从地址0x601000开始。通过edb查看,在dl_init调用前,其内容如下:

图 36 调用前的情况

在调用后,其内容变为:

图 37 调用后的情况

比较可以得知,0x601008~0x601017之间的内容,对应着全局偏移量表GOT[1]和GOT[2]的内容发生了变化。GOT[1]保存的是指向已经加载的共享库的链表地址。GOT[2]是动态链接器在ld-linux.so模块中的入口。这样,接下来执行程序的过程中,就可以使用过程链接表PLT和全局偏移量表GOT进行动态链接。

5.8 本章小结

本章中介绍了链接的概念与作用,并得到了链接后的hello可执行文件的ELF格式文本hello_elf2.txt,据此分析了hello_elf2.txt与hello_elf.txt的异同;之后,根据反汇编文件hello_asm_2.txt与hello_asm.txt的比较,加深了对重定位与动态链接的理解。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

  1. 进程的概念

进程是一个正在运行的程序的实例,系统中的每一个程序都运行在某个进程的上下文中。

  1. 进程的作用

给应用程序提供两个关键抽象:

(1)一个独立的逻辑控制流,提供一个假象,好像程序独占地使用处理器

(2)一个私有地址空间,提供一个假象,好像程序独占地使用内存系统

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

6.2.1 Shell 的作用

Shell 是一个用C语言编写的交互型应用程序,代表用户运行其他程序。Shell 应用程序提供了一个界面,用户可以通过这个界面进行系统的基本操作,访问操作系统内核的服务。

6.2.2 Shell的处理流程

  1. 从Shell终端读入输入的命令。
  2. 切分输入字符串,获得并识别所有的参数
  3. 若输入参数为内置命令,则立即执行
  4. 若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行
  5. 若输入参数非法,则返回错误信息
  6. 处理完当前参数后继续处理下一参数,直到处理完毕

6.3 Hello的fork进程创建过程

打开Shell,输入命令./hello 2021110679 江静 1,带参数执行生成的可执行文件。父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。

图 6-1 程序正常执行的情况

6.4 Hello的execve过程  

调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello,并创建一个内存映像,为该程序的栈区域创建新的区域结构将可执行文件的片复制到代码段和数据段等。然后为共享库建立映射空间。最后设置当前进程上下文的程序计数器,将其指向入口函数,并将控制传递给新程序的主函数。

6.5 Hello的进程执行

控制寄存器中有一个模式位,当设置了模式位时,进程就运行在内核模式中,此时可以执行指令集中的任何模式,并且可以访问系统中的任何内存位置;没有设置模式位时,进程就运行在用户模式中,不允许执行如停止处理器、改变模式位或发起一个I/O操作等特权指令。

运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法时通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把内核模式改回到用户模式。

内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占了的进程。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换——①保存当前进程的上下文,②恢复某个先前被抢占的进程被保存的上下文,③将控制传递给这个新恢复的进程。

在hello进程中,调用sleep函数或者getchar函数都需要进行上下文切换,由用户模式切换到系统模式。

6.6 hello的异常与信号处理

  1. 在程序运行时按回车,会多打印几处空行,程序可以正常结束。

图 6-2 程序运行时按下回车

  1. 按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。

图 6-3 程序运行时按Ctrl+C

  1. 按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。

图 6-4 程序运行时按Ctrl+Z

对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。

图 6-5 用ps命令查看挂起进程

在Shell中输入pstree命令,可以将所有进程以树状图显示(此处仅展示部分):

图 6-6 用pstree命令查看所有进程

输入kill命令,则可以杀死指定(进程组的)进程:

图 6-7 命令杀死指定进程

输入fg 1则命令将hello进程再次调到前台执行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句。程序仍然可以正常结束,并完成进程回收。

图 6-8 用fg命令将进程调回前台

  1. 不停乱按

在程序执行过程中乱按所造成的输入均缓存到stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),hello结束后,stdin中的其他字串会当做Shell的命令行输入。

图 6-9 不停乱按的情况

6.7本章小结

本章介绍了进程的概念与作用,以及Shell-bash的作用和处理流程。根据hello可执行文件的具体示例,研究了fork,execve函数的原理与执行过程,并给出了hello带参执行情况下各种异常与信号处理的结果。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

  1. 逻辑地址

逻辑地址是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。具体而言,其为hello_asm.txt中的相对偏移地址。

  1. 线性地址

逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。

  1. 虚拟地址

CPU启动保护模式后,程序运行在虚拟地址空间中。hello里就是的虚拟内存地址。

  1. 物理地址

CPU通过地址总线的寻址,找到真实的物理内存对应地址。hello在运行时虚拟内存地址对应的物理地址。

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

Intel处理器从逻辑地址到线性地址的变换通过段式管理的方式实现。每个程序在系统中都保存着一个段表,段表保存着该程序各段装入主存的状况信息,包括段号或段名、段起点、装入位、段的长度、主存占用区域表、主存可用区域表等,从而方便进行段式管理。

一个逻辑地址由段标识符和段内偏移量两部分组成。其中段标识符是一个16位长的字段,称为段选择符,可以得到对应段首地址。段选择符的结构如下:

图 7-1 段选择符的情况

其包含三部分:索引,TI和RPL。

索引:用来确定当前使用的段描述符在描述符表中的位置;

TI:根据TI的值判断选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);

RPL:判断重要等级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;

通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。

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

线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。

通过7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机系统的特性可以确定VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。

图 7-2 Hello的线性地址到物理地址的变换-页式管理

若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。

若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到PPN,与PPO共同组成物理地址。

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

图7-3描述了使用4级页表层次结构的地址翻译。虚拟地址被划分成为4个VPN和1个VPO。每个VPN i都是一个到第i级页表的索引,其中1<=i<=4。第j级页表中的每个PTE,1<=j<=3,都指向第j+1级的某个页表的基址。第4级页表中的每个PTE包含某个物理页面的 PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定 PPN之前,MMU必须访问4个PTE。和只有一级的页表结构一样,PPO和VPO 是相同的。

图 7-3 TLB与四级页表支持下的VA到PA的变换

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

MMU将虚拟地址转化为物理地址后,该地址被分为标记位(CT)、组号(CI)和偏移量(CO)。首先使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO取出相应的数据后返回。

若没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中请求数据(请求顺序为L2 Cache→L3 Cache→主存,若仍不命中才继续向下一级请求)。查询到数据之后,需要对数据进行读入,一种简单的放置策略如下:若映射到的组内有空闲块,则直接放置在空闲块中,若当前组内没有空闲块,则产生冲突(evict),采用LFU策略进行替换。

图 7-4 Intel Core i7 地址翻译概况

7.6 hello进程fork时的内存映射

当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID。为了给这个新的hello创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

图 7-5 一个私有的写时复制对象

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数加载并运行hello需要以下几个步骤:

  1. 删除已存在的用户区域:删除当前进程hello虚拟地址的用户部分中的已存在的区域结构。
  2. 映射私有区域:为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
  3. 映射共享区域:若hello程序与共享对象或目标(如标准C库libc.so)链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器PC:最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

图 7-6 execve时的内存映射

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

发生一个缺页异常后,控制会转移到内核的缺页处理程序。判断虚拟地址是否合法,若不合法,则产生一个段错误,然后终止这个进程。

若操作合法,则缺页处理程序从物理内存中确定一个牺牲页,若该牺牲页被修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处理程序返回时,CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存将所请求字返回给处理器。

图 7-6 缺页故障处理

7.9动态存储分配管理

7.10本章小结

本章主要介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

(第81分)

结论

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

(结论0分,缺失 -1分,根据内容酌情加分)

附件

列出所有的中间产物的文件名,并予以说明起作用。

文件名

功能

hello.i

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

hello.s

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

hello.o

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

hello_elf.txt

用readelf读取hello.o得到的ELF格式信息

hello_asm.txt

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

hello_elf_2.txt

由hello可执行文件生成的.elf文件

hello_asm2.txt

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

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

参考文献

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

[1]   Randal E.Bryant, David O'Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4

[2]    Pianistx.printf 函数实现的深入剖析[EB/OL].2013[2021-6-9].

https://www.cnblogs.com/pianist/p/3315801.html.

[3]   梦想之家xiao_chen.ELF文件头更详细结构[EB/OL].2017[2021-6-10].

https://blog.csdn.net/qq_32014215/article/details/76618649.

[4]   Florian.printf背后的故事[EB/OL].2014[2021-6-10].

https://www.cnblogs.com/fanzhidongyzby/p/3519838.html.

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值