哈工大csapp 大作业 hello的一生

哈工大计算机系统大作业: 程序人生-Hello’s P2P/ hello 的一生

题 目 程序人生-Hello’s P2P
专 业 计算机
学   号 120L020624
班   级 2003010
学 生 孙楚芮    
指 导 教 师 郑贵滨

计算机科学与技术学院
2022年5月

摘 要

本文主要讲述了hello.c程序在编写完成后运行在linux中的生命历程,借助相关工具分析预处理、编译、汇编、链接等各个过程在linux下实现的原理,分析了这些过程中产生的文件的相应信息和作用。

关键词:

预处理;编译;汇编;链接;shell;虚拟内存;IO;存储;

Hello的一生

第一章 概述

1.1 Hello简介

1) From Program to Process

首先我们需要编程,即program编写程序hello.c,然后这个文件会进行一系列的预处理,编译,汇编,链接生成一个.out可执行文件,我们打开shell之后,如果去运行它,那么hello就会为hello创建一个新的进程,即progress,然后调用execve函数,将hello的可执行文件进行加载,运行。

2) FROM ZERO TO ZERO

首先在真正加载前shell这个壳需要为hello申请虚拟内存空间,然后将物理内存与虚拟内存进行映射,同时处理器内核还需要为hello分配时间片,使得其看似独享整个资源,当需要物理地址时,cpu会向mmu发出一个虚拟地址,mmu将虚拟地址转化为物理地址之后,在提供相应的数据。同时当我们在程序运行期间,输入信号时,会使内核产生一个中断,当按下ctrl-zctrl-c时,会向hello进程发送一个信号,如果子进程终止之后,会有父进程将僵尸进程进行回收,避免占用资源。

1.2 环境与工具

硬件: Core I7-10875H X64 ; 32G RAM; 1TB SSD

软件: Ubuntu 20.10

工具: VIM , GCC, EDB, OBJDUMP, READELF, LD

1.3 中间结果

hello.c:源 程序

hello.i: 经过预处理源程序

hello.s:hello.i 经过编译汇编程序

hello.o:hello.s 经过汇编可重定位目标程序

hello:hello.o 经过链接后的可执行目标程序

hello.elf:hello.o 的 elf 格式

hello.elf:hello 的 elf 格式

helloo.objdump:hello.o的反汇编程序

hello.objdump:hello的反汇编程序

1.4 本章小结

本章节简述了P2P020 的含义,列出了测试环境和工具和中间结果的文件名和文件作用。从整体上大致介绍了hello的一生,并且列出了做本次作业的软硬件环境以及工具。也相当于漫游了一下hello的一生。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

预处理器根据以字符#开头的命令修改原始的C程序。 比如 hello.c 中的命
令告诉预处理器读取对应的三个系统头文件的内容,并把它直接插入到程
序文本中,结果就得到了另一个C程序。其中,ISO C/C++要求支持的包括#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。

预处理的作用:

1.#include指令告诉预处理器(cpp)读取源程序所引用的系统源文件,并把源文件直接插入程序文本中。

2.执行宏替换。将目标的字符替换为我们所定义的字符。

3.条件编译。根据定义的条件,来确定编译的条件,即目标是否是真正需要的,类似于ifelse。

4.特殊符号,预编译程序可以识别一些特殊的符号,预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

2.2在Ubuntu下预处理的命令

图1

图2.2.1

2.3 Hello的预处理结果解析

首先使用vim打开hello.i
图2

图2.3.1

就像这样
图3

图2.3.2

然后找到main函数
在这之前出现的是头文件 stdio.hunistd.hstdlib.h
的依次展开。

stdio.h 的展开为例:stdio.h是标准库文件,cpp 到Ubuntu中默认的环
境变量下寻找stdio.h,打开文件/usr/include/stdio.h ,发现其中依然使用
了#define语句,cpp对stdio中的define宏定义递归展开。

最终.i文件将预处理指令进行替换,而原来的预处理指令就不需要了;发现其中使用了大量的#ifdef#ifndef条件编译的语句,cpp会对条件值进行判断来决定是否执行包含其中的逻辑。特殊符号,预编译程序可以识别一些特殊的符号,预编译程序对于在源程序中出现的这些串将用合适的值进行替换。在下面我们可以看到其寻找的头文件。
图4

图2.3.3

图5

图2.3.4

2.4 本章小结

本章主要介绍了预处理的定义与作用,并且分析了.i文件的整个分析过程、并结合.i程序对预处理结果进行了分析。

第3章 编译

3.1 编译的概念与作用

编译的概念:

编译是利用编译程序从预处理文本文件产生汇编程序的过程。主要包含五个阶段:词法分析;语法分析;语义检查、中间代码生成、 目标代码生成。

编译的作用:

词法分析:

将源代码程序输入扫描器,将源代码的字符序列分割成一系列记号。

语法分析:

基于词法分析得到的一系列记号,生成语法树。

语义分析:

由语义分析器完成,指示判断是否合法,并不判断对错。

目标代码的生成与优化:

目标代码生成阶段编译器会选择合适的寻址方式,左移右移代替乘除,删除多余指令。

3.2 在Ubuntu下编译的命令

图6

图3.2.1

然后我们用vim打开
图7

图3.2.2

3.3 Hello的编译结果解析

hello.c的数据类型有整型、字符串、数组

整数

.data节是8字节对齐的
图8

图3.3.1

hello.c中还有argci两个整型变量。其中int i是循环中用来计数的局部变量,argc是从终端输入的参数的个数,也是main函数的第一个参数。hello.si存储在-4(%rbp)中,初始值为0,每次循环加1,退出循环条件是i大于7。
图9

图3.3.2

字符串

编译器一般会将字符串存放在.rodata节。hello.c中共有两个字符串,分别是两个printf格式化输出的字符串。对应找到hello.s中的.rodata节:
图9

图3.3.3

可以看出 hello.s 中字符串由.string 声明,第一个字符串.LC0包含汉字,每个汉字在utf-8编码中被编码为三个字节;第二个字符串的两个*%s*为用户在终端输入的两个参数。

数组

hello.c中的数组是 main()函数的第二个参数 char *argv[],argv是字符串指针的数组,每个元素是一个指向一个字符串首地址的指针,作为函数的第二个参数 ,argv[]开始被保存在寄存器%rsi中,然后又被保存到栈中32 (%rbp)的位置。
图10

图3.3.4

可以看到 main 函数并没有访问 argv【0】,而是访问了 argv【1】和 argv【2】,这是因为argv[0]指向程序运行的全路径名。

函数操作

程序运行时先进入程序入口处,然后自动调用 main函数。若程序员需要调用 函数,在汇编代码中需要使用 call指令,在使用之前需要先设置好参数(%eax)。进行传参。

图11

图3.3.5

结束后的ret: %rbp为栈帧的底部,函数在%rbp上分配空间
leave指令:相当于mov %rbp,%rsp ,pop %rbp,恢复栈空间为调用main函 数之前的状态。

3.4 本章小结

本节对应于书上与汇编语言相关的章节,总结并分析了编译器是如何处理c语言的各个数据类型和各类操作,如算术操作,关系操作和函数调用的。经过该步骤hello.s已经是更加接近机器层面的汇编代码。

第4章 汇编

4.1 汇编的概念与作用

  • 汇编的概念:汇编器(as)将hello.s翻译成机器能读懂的机器语言指令,并将这些指令打包成可重定位目标程序hello.o,hello.o是一个二进制文件
  • 作用:产生机器能读懂的代码,使得程序能被机器执行。由于几乎每一条汇编指令都对应于一 条机器代码。

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

4.2 在Ubuntu下汇编的命令

命令为: gcc -c hello.s -o hello.o
图12

图4.2.1

图13

图4.2.2

4.3 可重定位目标elf格式

输入: *readelf -a hello.o > hello.elf *指令.获得ELF格式文件。
图14

图4.3.1

图15

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

图17

图4.3.4
  1. Section Headers:节头部表,包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。
    图18
图4.3.5
  1. 重定位节.rela.text,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
    8条重定位信息分别是对.L0(第一个printf中的字符串)、puts函数、exit函数、.L1(第二个printf中的字符串)、printf、sleepsec、sleep、getchar函数进行重定位声明。
    图19
图4.3.6

以.L1的重定位为例阐述之后的重定位过程:链接器根据info信息向.symtab 节中查询链接目标的符号,由 info.symbol=0x05,可以发现重定位目标链接到.rodata 的.L1,设重定位条目为r,r的构造为:r.offset=0x18, r.symbol=.rodata, r.type=R_X86_64_PC32,
r.addend=-4
重定位一个使用 32 位 PC相对地址的引用。计算重定位目标地址的算法如下(设需要重定位的.text节中的位置为 src,设重定位的目的位置 dst):

refptr = s +r.offset (1)

refaddr = ADDR(s) + r.offset (2)

*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr)(3)

其中(1)指向 src 的指针(2)计算src的运行时地址,(3)中ADDR (r.symbol)计算dst的运行时地址,在本例中,ADDR(r.symbol)获得的是 dst的运行时地址,因为需要设置的是绝对地址,即 dst与下一条指令之间的地址之差,所以需要加上 r.addend=-4。之后将 src处设置为运行时值为*refptr,完成该处重定位。

  1. 符号表(SymbolTable)目标文件的符号表中包含用来定位、重定位程序中符号定义和引用的信息。符号表索引是对此数组的索引。索引0表示表中的第一表项,同时也作为定义符号的索引。

图20

图4.3.7

4.4 Hello.o的结果解析

objdump -d -r hello.o分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

使用 objdump -d -r hello.o > helloo.objdump获得反汇编代码。Hello.s和helloo.objdunp除去显示格式之外两者差别不大,主要差别如下:

分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L3,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。

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

全局变量访问:在.s 文件中,访问 rodataprintf中的字符串),使用段名称+%rip,在反汇编代码中 0+%rip,因为 rodata中数据地址也是在运行时确定,故访问也需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
图21

图4.4.1

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

4.5 本章小结

本章介绍了 hellohello.s hello.o 的汇编过程,通过查看 hello.oelf格式和使用objdump得到反汇编代码与hello.s进行比较的方式,间接了解到从汇编语言映射到机器语言汇编器需要实现的转换

第5章 链接

5.1 链接的概念与作用

概念:

是将各种代码和数据片段收集并组合为单一文件的过程,这个文件可以被加载(复制)到内存并执行。

作用:

  1. 链接可以执行于编译时,也就是源代码被翻译成机器代码时;也可以执行于加载时,即程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。

  2. 链接使得分离编译(seperate compila)成为可能。更便于我们维护管理,我们可以独立的修改和编译我们需要修改的小的模块。

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

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

在这里插入图片描述

图5.2.2

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

在这里插入图片描述

图5.3.1
  • 各节的基本信息均在节头表(描述目标文件的节)中进行了声明。节头表(包括名称,大小,类型,全体大小,地址,旗标,偏移量,对齐等信息),下面是它的截图。

在这里插入图片描述

图5.3.2

5.4 hello的虚拟地址空间

在这里插入图片描述

图5.4.1

在这里插入图片描述

图5.4.2

分析程序头LOAD可加载的程序段的地址为0x400000。
在这里插入图片描述

图5.4.3

在0x401000~0x402000段中,程序被载入,虚拟地址0x401000开始,到0x401ff0结束,根据5.3中的节头部表,可以通过edb找到各个节的信息,比如.txt节,虚拟地址开始于0x4010f0,大小为0x145.

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
命令: objdump -d -r hello > hello.txt
在这里插入图片描述

图5.5.1

在这里插入图片描述

图5.5.2

hello和hello.o相比,首先多了很多经过重定位之后的函数,如_init、puts@plt等,hello.o在.text段之后只有一个main函数;hello.o的地址是从0开始的,是相对地址,而hello的地址是从0x401000(_init的地址)开始的,是已经进行重定位之后的虚拟地址;在hello的main函数中,条件跳转指令和call指令后均为绝对地址,而hello.o中是相对于main函数的相对地址。
在这里插入图片描述

图5.5.3

链接器完成的两个主要任务:符号解析和重定位。
重定位由两步组成:

  1. 重定位节和符号定义。
  2. 重定位节中的符号引用。

如对puts函数的重定位:
在hello.o反汇编代码中,该行二进制编码为e8 00 00 00 00
addr(text)= 0x401105
refaddr = addr(text)+offset = 0x401126,即引用运行时的地址
addr(r.symbol) = addr(puts) = 0x401080
然后更新该引用,*refptr = (unsigned) (addr(r.symbol) + r.addend - refaddr)
= (unsigned) (0x401080 +(-4) – 0x401126) = (unsigned) (-aa) = ff ff ff 56
将其以小段序填入可得 56 ff ff ff ,与反汇编代码一致。

5.6 hello的执行流程

使用EDB,只需要逐步执行,将程序名列出即可:

函数地址
ld-2.27.so!_dl_start0x7fce 8cc38ea0
ld-2.27.so!_dl_init0x7fce 8cc47630
hello!_start0x400500
libc-2.27.so!__libc_start_main0x7fce 8c867ab0
-libc-2.27.so!__cxa_atexit0x7fce 8c889430
-libc-2.27.so!__libc_csu_init0x4005c0
hello!_init0x400488
libc-2.27.so!_setjmp0x7fce 8c884c10
-libc-2.27.so!_sigsetjmp0x7fce 8c884b70
–libc-2.27.so!__sigjmp_save0x7fce 8c884bd0
hello!main0x400532
hello!puts@plt0x4004b0
hello!exit@plt0x4004e0
*hello!printf@plt
*hello!sleep@plt
*hello!getchar@plt
ld-2.27.so!_dl_runtime_resolve_xsave0x7fce 8cc4e680
-ld-2.27.so!_dl_fixup0x7fce 8cc46df0
–ld-2.27.so!_dl_lookup_symbol_x0x7fce 8cc420b0
libc-2.27.so!exit0x7fce 8c889128

5.7 Hello的动态链接分析

dl_init 调用后,0x601008 和 0x601010 两个地址 的数 据 都 产 生 变 化。
由于编译器无法预测函数的运行时地址,所以需要添加重定位记录。链接器
采用延迟绑定的策略,使用 PLT+GOT实现函数的动态链接。

PLT 使用 GOT 中的地址跳到目标函数。

dl_init 调用之前,函数调用都指向 PLT 中的代码逻辑。第一次执行时,为 GOT赋上相应的偏移量,初始化了函数调用,dl_init就是做了这件事。此后每次执行时不需要经过如此操作,每次都直接跳转到目标函数的地址。

5.8 本章小结

在本章中主要介绍了链接的概念与作用、helloELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。

链接器的两个主要任务就是符号解析和重定位,符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个信号的最终内存地址,并且修改对那些目标符号的引用。

第6章 hello进程管理

6.1 进程的概念与作用

概念:

进程是程序的一个实例,每一个进程都有它自己的地址空间。在地址空间内,每个进程的地址空间结构的一样的。

作用:

进程为用户提供了如下假象:程序好像在独占处理器、内存,处理器无间断 地运行进程,该进程好像是系统中唯一运行的程序。

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

  • Shell 是一个程序,它可以读取用户输入的命令,执行相应的操作。
  • Shell 应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
  • 处理流程: 当从 shell里输入命令(字符串)时,第一个单词是可执行程序的名称,后面则是参数列表。
  • shell 会传进参数列表来执行对应程序,创建进程,并在进程终止后回收进程。读入后,shell 先解析字符串,得到命令行参数(char
    **argv)。
  • 若命令行参数 的最后一个单词是&,表示要在后台执行,shell可以继续输入命令来做其他工作, 否则则为前台执行,必须等待该进程结束并回收。

6.3 Hello的fork进程创建过程

在终端Terminal中键入./hello 120L020624 孙楚芮,运行的终端程序会对输入的命令行进行解析。

  1. hello 不是一个内置的shell命令所以解析之后终端程序判断./hello
    的语义为执行当前目录下的可执行目标文件 hello
  2. 之后终端程序首先会调用 fork函数创建一个新的运行的子进程,新创建的子进程几乎父进程相同,但不完全与相同。fork函数的返回值里,一般用0代表子进程,非0代表父进程
  3. 父进程与子进程之间最大的区别在于它们拥有不同的 PID。子进程得到与父进程用户级虚拟地址空间相同的一份副本,当父进程调用 fork时,子进程可以读写父进程中打开的任何文件。
  4. 内核能够以任意方式交替执行父子进程的逻辑控制流的指令,父进程与子进程是并发运行而独立的。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
  5. 父进程和子进程独立 运行,二者结束顺序不可知。父进程负责回收子进程。 所以可以根据 fork 的返回值不同来区分子进程和父进程。

6.4 Hello的execve过程

  • 进程调用 execve 函数,该函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化。
  • 加载目标程序。换句话说,execve 函数将用目 标 程 序 的 进 程 替 换 当前进程,并传入相应的参数和环境变量,控制转移到新程序的main函数。
    在这里插入图片描述
图6.4.1

6.5 Hello的进程执行

  • 由于运行 hello 进程的同时可能在同时运行其他进程,内核并发运行多个进程需要用到上下文切换来实现多任务。内存为每个进程维持一个上下文,即内核重新启动一个被抢占的进程所需的状态。假如 hello 没有被抢占,则一条条运行汇编指令;若被抢占,内核则进行上下文切换:假如从 hello 进程切换到 A 进程,则
  1. 保存 hello 进程的上下文

  2. 恢复 A 进程的上下文

  3. 控制转移给 A 进程

  • hello 进程最初运行在用户模式,但是程序调用了 sleep 函数,调用时产生了用户态和核心态的转变。进程主动请求休眠,于是产生上述的上下文切换,内核将控制转移到其他进程,将 hello 进程从运行队列加入等待队列,从用户模式变成内核模式,并开始计时。当计时结束时,发送中断信号,将 hello 进程从等待队列中移出,从内核模式转为用户模式。此时 hello 进程就可以继续执行逻辑控制流了。

  • getchar 时,实际上也是执行了 read 的调用,此时也产生了如上所述的上下文切换,进程等待键盘缓冲区的输入。当完成键盘缓冲区到内存的数据传输后,内核从其他进程上下文切换到hello 进程。
    结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

6.6 hello的异常与信号处理

  • ctrl + z 向进程发送信号,使进程挂起。
    在这里插入图片描述
图6.6.1
  • ctrl + c 向进程发送信号, 令进程终止:
    在这里插入图片描述
图6.6.2
  • ps fg 将进程放置后台(前台)运行:
    在这里插入图片描述
图6.6.3

在这里插入图片描述

图6.6.4

在这里插入图片描述

图6.6.5

在这里插入图片描述

图6.6.6

在这里插入图片描述

图6.6.7

当前进程:
在这里插入图片描述

图6.6.8

kill之后的进程:
在这里插入图片描述

图6.6.9
  • 在进行乱按的时候,第一个字符串会被读入,而其他的字符串会在缓冲区中储存,然后再程序结束的时候,当成命令行命令读入.
    在这里插入图片描述
图6.6.10

6.7本章小结

异常控制流发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。有四种不同类型的异常,中断故障终止,和陷阱

在操作系统层,内核用ECF提供进程的基本概念,给应用程序两个重要的抽象:

  1. 逻辑控制流
  2. 私有地址空间.

应用程序可以创建子进程,等待他们的子进程停止或者终止,运行新的程序,以及捕获来自其他进程的信号.

第7章 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

逻辑地址:

程序代码经过编译后出现在汇编程序中地址。

线性地址:

逻辑地址经过段机制转化后为线性地址,用于描述程序分页信息的地址。以 hello 为例,线性地址就是 hello 应该在内存的哪些块上运行。

虚拟地址:

同线性地址。此外,有些资料是直接把逻辑地址当成虚拟地址,两者并没有明确的界限。

物理地址:

处理器通过地址总线的寻址,找到真实的物理内存对应地址。是内存单元的真实地址。以 hello 为例,物理地址就是 hello 真正应该在内存的哪些地址上运行。

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

段式管理,是指把一个程序分成若干个段进行存储,每个段都是一个逻辑实 体,程序员需要知道并使用它。
在这里插入图片描述

图7.2.1

它的产生是与程序的模块化直接有关的。段式管 理是通过段表进行的。它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。

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

首先查看页表
在这里插入图片描述

图7.3.1

然后使用页表的地址翻译:
在这里插入图片描述

图7.3.2

线性地址转换成物理地址的过程如下:

  1. 首先我们先将线性地址分为 VPN(虚拟页号)+VPO(虚拟页偏移)的形式。
  2. 然后再将 VPN 拆分成 TLBT(TLB 标记)+TLBI(TLB 索引)然后去 TLB 缓存里 找所对应的 PPN(物理页号)如果发生缺页情况则直接查找对应的PPN,找到 PPN 之后,将其与 VPO 组合变为 PPN+VPO 就是生成的物理地址了。

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

书中的二级页表示例:
在这里插入图片描述

图7.4.1

在实际的运行的过程中,二级页表是远远不够的,因此我们需要多级页表:
在这里插入图片描述

图7.4.2

在多级页表中,每一级页表中的一个项都对应下一页表的起始位置,这样不需要的页表就不需要载入内存中,节省空间。

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

此时我们已经得到物理地址,只需要在 cache 寻找即可。与课本高速缓存章节 类似,将物理地址分为 CT(标记)+CI(索引)+CO(偏移量),然后在一级 cache 内部找,如果没有一直向下递归寻找。找到之后将其写入cache, 返回结果。
在这里插入图片描述

图7.5.1
  • 我们先在L1缓存中寻找结果,如果命中,就将缓存中的数据项保存,返回递归向下一级存储结构中寻找。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello 中的程序

  • 删除已存在的用户区域

  • 映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构。

  • 映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

  • 设置程序计数器(PC),使之指向代码区域的入口点。

在这里插入图片描述

图7.7.1

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

假设当前虚拟地址是 A,现在翻译地址 A 触发了一个缺页异常,导致控制转
移到内核的缺页处理程序,处理程序将执行以下步骤:

  1. 判断A 在某个区域结构定义的区域是否合法,若不合法,则产生一个段错误,然后终止这个进程。

  2. 判断该内存访问是否合法,若访问是不合法的,那么缺页处理程序会触发一个保护异常,终止这个进程。

  3. 如果此时内核知道该操作是合法的,那么将把对应页面加载并更新页表,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。

  4. 当缺页处理程序返回时,CPU 重新启动引起缺页的指令。即可。

7.9动态存储分配管理

printf 函数会调用 malloc,下面简述动态内存管理的基本方法与策略:

  • 动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。

  • 每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。

  • 空闲块可用来分配。空闲块保 持 空 闲,直到它显式地被应用所分配。

  • 一个已分配的块保持已分配状态,直到它被释放。

带边界标签的隐式空闲链表

  • 堆及堆中内存块的组织结构:
    在内存块中增加4B的Header(用于寻找下一个blcok)和4B的Footer(用于寻找上一个block)。Footer的设计是专门为了合并空闲块方便的。因为Header和Footer大小已知。
  1. 隐式链表

对比于显式空闲链表,隐式空闲链表代表并不直接对空闲块进行链接,而是将对内存空间中的所有块组织成一个大链表。
在这里插入图片描述

图7.9.1
  1. 空闲块合并

可以利用Footer方便的对前面的空闲块进行合并。合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。通过改变Header和Footer四种情况分别进行空闲块合并。

显示空间链表

将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和 succ(后继)指针。
在这里插入图片描述

图7.9.2

7.10本章小结

虚拟内存是对主存的一个抽象,它提供三个重要的功能,第一,自动缓存最近使用的存放磁盘上的虚拟地址空间。第二,简化内存管理,进而简化链接,在进程间共享数据,以及程序的加载。

内存的使用和释放是一个容易出错的地方,需要我们进一步理解对它的认识,也要在编程工作中,优化内存管理的策略。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。

这就使得所有的输入和输出都能一一个统一且一致的方式来执行:

  • 打开文件。

  • Linux Shell创建的每个进程开始时都有三个打开的文件:标准输入,标准输出,标准错误。

  • 改变当前文件的位置。

  • 读写文件。

  • 关闭文件。

8.2 简述Unix IO接口及其函数

接口:

  • 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符。描述符在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。

  • Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。

  • 改变当前的文件位置:内核保持着每个打开的文件的一个文件位置k。k初
    始为0。这个文件位置k表示的是从文件开头起始的字节偏移量。

  • 读写文件
    读操作就是从文件复制n>0个字节到内存。
    写操作就是从内存中复制n>0个字节到一个文件。

  • 关闭文件:内核释放文件打开时创建的数据结构,无论一个进程以何种原因终止时,内核都会关闭所有打开的文件,并且释放他们的内存资源。

函数:

int open(cahr filename, int flags, mode_t mode);
int close (int fd);
ssize_t read (int fd, void buf, size_t n);
ssize_t write (int fd, const buf, size_t n);

8.3 printf的实现分析

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用int
0x80或syscall等.字符显示驱动子程序:从ASCII到字模库到显示vram。显示
芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。

8.4 getchar的实现分析

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

getchar函数在实现时到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。

8.5本章小结

Linux 提供了少量的基于 Unix i/o模型的系统函数,他们允许应用程序打开,关闭,读写文件,执行io重定向。

而标准io库是基于unix io 实现的,并且提供了一组强大的高级io 例程,对于大多数应用程序而言,标准io更加简单,是优于unix io的选择,但 是对标准io和网络文件的一些互相不兼容的显式,unix io比标准io更适合用于网络应用程序的编程。

结论

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

预处理:

hello.c预处理到hello.i文本文件;

编译:

hello.i编译到hello.s汇编文件;

汇编:

hello.s汇编到二进制可重定位目标文件hello.o;

链接:

hello.o链接生成可执行文件hello;

创建子进程:

bash进程调用fork函数,生成子进程;

加载程序:

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

访问内存:

hello的运行需要地址的概念,虚拟地址是计算机系统最伟大的抽象;

交互:

hello的输入输出与外界交互,与linux I/O息息相关;

终止:

hello最终被shell父进程回收,内核会收回为其创建的所有信息。
至此,hello运行结束。

附件

hellohello的执行程序
hello.chello 的源程序
hello.elfhello 的elf头部
hello.ihello 的预处理程序
hello.ohello 的可重定位的目标程序
hello.txthello 的反汇编文件
hello.shello 的汇编程序

参考文献

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

[1] 深入理解计算机系统第三版

[2] 百度,知乎,掘金,CSDN。

[3] bilibili cmu网课

[4] gcc–编译的四大过程及作用:https://blog.csdn.net/shiyongraow/article/details/81454995

[5] 网络用户. 阿里云. ELF格式文件符号表全解析及readelf命令使用方法. 2018:07-19.
https://www.aliyun.com/zixun/wenji/1246586.html

[6] C语言预处理命令之条件编译. 2009:08-16.
http://www.kuqin.com/language/20090806/66164.html

[7] CSDN. 编译器工作流程详解. 2014:04-27.
https://blog.csdn.net/u012491514/article/details/24590467

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值