大作业——hello的一生

完整版:百度网盘

链接:https://pan.baidu.com/s/1cxAILl3ePJlph972HvzsuQ 
提取码:rc6o

摘  要

       本文详细介绍了hello程序在linux系统中从hello.c到hello可执行程序,再到hello在内存中加载运行,最后到程序返回被回收的整个过程。通过hello程序的一生,进一步深入了解计算机系统。

关键词:hello.c,P2P,020,编译,进程

目录

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

P2P:指从程序到进程,程序员在编辑器中输入代码,生成hello.c 文件(代码),经过预处理器cpp、编译器ccl、汇编器as、链接器ld,分别生成hello.i文件、hello.s文件、hello.o文件,最后生成可执行程序hello。在终端中输入运行命令,即可运行该文件(进程)。

020:指刚开始内存中没有程序信息,即从0开始,程序需要加载到内存中才能运行。该过程中shell利用fork和execve函数分别创建和加载进程,映射至虚拟内存。进入main函数执行目标代码,CPU为运行的hello分配时间片,执行逻辑控制流,通过正确的IO管理与信号处理来保证程序的正常运行。hello程序运行结束后,父进程回收子进程,内核删除进程产生的相关数据,释放内存,恢复程序执行前的状态,实现020。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具

硬件:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

软件:Ubuntu20.04,VMware11以上,Windows10

开发和调试工具:objdump、gcc、gdb、readelf等

1.3 中间结果

hello.c:源代码文件

hello.i:预处理后的文本文件

hello.s:编译后的汇编文件

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

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

asm2.txt:hello.o反汇编的代码文件

hello 链接产生的可执行目标文件

elf2.txt:hello的ELF格式文件

asm.txt:hello反汇编的代码文件

1.4 本章小结

本章简要概括了hello程序P2P、020的整个过程,列出了实验所需的环境以及实验过程中生成中间结果文件的名字和作用。

(第1章0.5分)


第2章 预处理

2.1 预处理的概念与作用

预处理是指在编译之前对程序中的特殊命令进行的处理工作。以#开头的是C语言的编译预处理命令。即在高级语言源程序中插入所有用#include 命令指定的文件和用#define 声明指定的宏。

3种预处理功能:宏定义、文件包含、条件编译。

作用:提高了C语言的通用性,不同的计算机能兼容的执行C语言的代码程序。

2.2在Ubuntu下预处理的命令

gcc -E -o hello.i hello.c,命令与结果如下图一所示:

图 1 运行命令

2.3 Hello的预处理结果解析

经过预处理后,源程序由二十多行扩展为3000多行。

预处理器cpp识别到#include就会将其递归展开。hello.i文件中#include ,#include , #include 三个头文件消失,取而代之的是一大段代码,方便编译器下一步将其翻译为汇编代码。在文件的最后,是主函数main(),没有发生变化。

2.4 本章小结

在此过程中,cpp预处理器会对程序进行一些必要的转化,即展开宏定义、头文件,生成hello.i 文件。该过程提升了程序在不同机器中的兼容性,方便进行下一步的编译过程。


第3章 编译

3.1 编译的概念与作用

编译过程就是是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件,使用ccl 把一个简单文本翻译文件中的hello.i翻译为一个新的hello.s。

作用:经过词法分析,语法分析,语义分析,之后进行优化。最后可以生成相应的汇编代码(hello.s)。

3.2 在Ubuntu下编译的命令

gcc -S -o hello.s hello.i,命令与结果如下图一所示:

图 2 运行结果

3.3 Hello的编译结果解析

图 3 hello.s文件部分内容

3.3.1数据类型

(1)字符串常量:字符串以UTF-8编码的格式存在只读数据段(.rodata)

图 4 字符串在汇编代码中的位置

(2)局部变量int i

局部非静态变量储存在栈中或者一个栈的寄存器,在该文件中,编译器将i存储在(%rbp)-4的栈地址中,在栈中占4个字节。在for()循环中i进行累加,最初是给i赋初值0,即在栈中移入一个立即数,之后,直接在栈的地址中读取值。

图 5 局部变量

(3)main参数argc,argv

作为一个参数存储在寄存器%edi中,之后转移到了栈中。使用的时候在栈中取值。

(数组/指针/结构操作)程序中的字符串argv[1]和argv[2]的字符串地址分别被输入的rax分两次进行读取,argv[3]的秒数作为字符串的秒数被第三次读取。数据地址存储在栈中,利用栈指针的偏移量来读取。

图 6  -20(%rbp)-argc   -32(%rbp)-argv

3.3.2算术操作i++

i++的实现较为简单,在汇编语言中为:addl    $1, -4(%rbp)

3.3.3关系操作

(1)argc!=4

由上述分析已知,-20(%rbp)对应argc,这里使用je进行跳转。如果值等于4则跳转到循环部分,否则继续执行原来的语句。

图 7 argc!=4的汇编代码部分

(2)i<8

由上述分析已知,-4(%rbp)对应i,这里使用jle进行跳转。如果值小于等于7,则运行循环内部的内容。

图 8 i<8的汇编代码部分

3.3.4函数操作main ,printf ,exit ,sleep ,getchar ,atoi:

使用call指令调用相应的函数,使用ret返回,返回值存储在%rax中。在调用之前需将相应的参数按照%rdi,%rsi,%rdx,%rcx,%r8,%r9,栈的方式存储到相应位置(调用者保存或者被调用者保存)。

3.3.5控制转移

if(argc!=4)语句,编译结果如上图(argc!=4)的结果相同。利用cmpl指令和jX指令进行条件码跳转,当argv!=4的时候,继续执行原来的语句;当等于的时候进行跳转,执行.L2。

for(i=0;i<8;i++)语句,原理如上,使用jle跳转指令,小于等于的情况下进行跳转,进入循环,否则跳出循环。

3.4 本章小结

在此阶段,编译器将高级程序语言编译为汇编语言,转化为机器可以识别的指令。在该过程中,加深了对hello程序使用的各种数据类型、赋值操作、类型转换、关系操作、数组/指针操作、控制转移和函数操作等的理解。


第4章 汇编

4.1 汇编的概念与作用

由汇编程序将汇编语言源程序文件转换为可重定位的机器语言目标代码文件。该过程调用汇编器as来实现,将hello.s文件中的汇编代码转换为机器可以执行的指令,保存在hello.o中——一个简单的二进制指令编码文件。

作用:实现将汇编代码转换为机器指令,使之在链接后能够被计算机直接执行。

4.2 在Ubuntu下汇编的命令

as hello.s -o hello.o

图 9 命令运行结果

4.3 可重定位目标elf格式

在Ubuntu中利用readelf命令查看elf,并且将其重定位为文本文件。

图 10 readelf命令

  1. elf头

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

图 11 elf头

  1. 节头部表

描述目标文件中不同节的位置和大小

图 12 节头表部分内容

  1. 重定位节

重定位节.rela.text,这个节包含了.text(具体指令)节中需要进行重定位的信息。这些信息描述的位置,在由.o文件生成可执行文件的时候需要被修改(重定位)

重定位条目常见有两种:R_X86_64_32:重定位绝对引用与R_X86_64_PC32:重定位PC相对引用。通过重定位算法,计算出新的重定位地址。

重定位算法:refaddr = ADDR(s) + r.offset

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

offset是需要被修改的引用的节偏移。symbol是标识被修改引用应该指向的符号。type告知链接器如何修改新的引用。addend是一个有符号常数。ADDR(s)表示符号运行时的地址。

图 13 重定位节

  1. 符号表

每个可重定位目标模块都有一个符号表,它包含m的定义和引用的符号的信息,不包含本地非静态程序变量的任何符号。符号表是由汇编器构造,使用编译器输出到汇编语言.s文件中的符号。

可以从符号表中看出符号的大小,类型等信息。如main大小为146字节,类型为FUNC

图 14 符号表

4.4 Hello.o的结果解析

利用objdump -d -r hello.o 对hello.o进行反汇编

图 15 反汇编部分代码

由上图可知:机器语言和汇编是一一对应关系,一条机器语言对应一条汇编代码,且机器语言是由二进制码组成。

与第三章的hello.s对比,代码部分基本相同,但是存在有以下区别:

  1. 反汇编代码指令前增加了其十六进制表示,即它的机器语言;
  2. 在操作数上,hello.s文件中为十进制,而hello.o的反汇编中操作数为十六进制;
  3. 调用函数过程中,hello.s根据重定位条目来获得地址信息,hello.o的反汇编代码中采用重定向的方式;
  4. 条件跳转语句中,hello.s中为函数名,hello.o的反汇编文件中为相对偏移量;
  5. 全局变量的访问,在 hello.s 中,访问全局变量.rodata 段的方式是在汇编的过程中使用了段的名称+寄存器,但是在 hello.o的反汇编中变成了0+寄存器,也就是把段名称对应的地址设置为0。

4.5 本章小结

本章利用汇编器as将汇编代码文件hello.s转换为机器唯一可以识别的语言组成的文件hello.o。并且在此过程中查看了hello.o的可重定位目标文件格式,以及查看hello.o经过反汇编生成的代码,加深了对汇编语言以及汇编过程的理解。


5链接

5.1 链接的概念与作用

输入一组可重定位的目标文件。链接器将多个目标文件链接成为一个完整的,可加载,可执行的目标文件。

作用:

  1. 模块化:程序可以编写为一个较小的源文件的集合,而不是一个整体巨大的一团。可以构建公共函数库例如数学运算库,标准C库。
  2. 效率更高:时间——分开编译,如果更改了一个源文件,可以直接编译,然后重新链接,不需要重新编译其他源文件。空间——使用库,可以将公共函数聚合为单个文件,而可执行文件和运行内存映像只包含它们实际使用的函数的代码.

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

图 16 链接运行结果

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

在Ubuntu中利用readelf命令查看elf,指令为:readelf -a hello > elf2.txt

图 17 readelf命令

节头表

节头部表描述了目标文件中不同节的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对其的详细信息。因此根据节头中的信息我们就可以用HexEdit定位各个节所占的区间(起始位置,大小)。

图 18 节头部表部分内容

5.4 hello的虚拟地址空间

使用edb打开hello程序,查看进程的地址信息,可以直接在Data Dump中看看0x400000以后的信息。

下面列出一部分节,对照5.3中信息进行说明:

(1).interp:size:0x1c,address:0x004002e0(大小为0x1c)

图 19 .interp段虚拟地址空间信息

(2).init:size:0x1b,address:0x00401000

图 20 init段虚拟地址空间信息

(3).text:size:0x145,address:0x004010f0。.text中存放的是代码。

图 21 .text段虚拟地址空间信息

5.5 链接的重定位过程分析

objdump -d -r hello > asm.txt

图 22 反汇编

分析hello与hello.o的不同:

(1)hello和hello.o相比,多了很多经过重定位之后的函数。

(2)hello.o的地址是从0开始,为相对地址;并且在主函数中,跳转指令和call指令后为相对于main的相对地址。hello的地址是从0x401000开始,并且在主函数中,跳转指令和call指令后为绝对地址

在使用ld命令链接的时候,定义了hello需要的printf、sleep、getchar、exit 函数和_start 中调用的 __libc_csu_init,__libc_csu_fini,__libc_start_main。这些函数被链接器加入其中。

图 23

图 24 hello和hello.o反汇编代码部分

5.6 hello的执行流程

0000000000401000 <_init>:

0000000000401020 <.plt>:

0000000000401090 :

00000000004010a0 :

00000000004010b0 :

00000000004010c0 :

00000000004010d0 :

00000000004010e0 :

00000000004010f0 <_start>:

0000000000401120 <_dl_relocate_static_pie>:

0000000000401125 :

00000000004011c0 <__libc_csu_init>:

0000000000401230 <__libc_csu_fini>:

0000000000401238 <_fini>:

5.7 Hello的动态链接分析

共享链接库代码是一个动态的目标模块,在程序开始运行或者调用程序加载时,可以自动加载该代码到任意的一个内存地址,并和一个在目标模块内存中的应用程序链接了起来,这个过程就是对动态链接的重定位过程。

而在一个动态的共享链接库中仍然存在着一个可以调用程序加载而动态链接无需重定位的位置无关代码,编译器在程序中的函数开始运行时是不能自动预测各个函数的开始运行时间和地址的,这就可能需要系统添加重定位的记录,交给一个动态共享链接器或者采用它来进行重定位的动态共享链接,动态共享链接器本身就是负责执行对动态链接的重定位过程,这样做就有效地防止了程序运行时自动修改或者调用目标模块的位置无关代码段。

动态的链接器在正常工作时链接器采取了延迟绑定的链接器策略,由于静态的编译器本身无法准确预测变量和函数的绝对运行时地址,动态的链接器需要等待编译器在程序开始加载时再对编译器进行延迟解析,这样的延迟绑定策略称之为动态延迟绑定。got链接器叫做全局变量过程偏移链接表,在plt和got中分别存放着链接器的目标变量和函数的运行时地址。一个动态的链接器通过静态的过程偏移链接表plt+got链接器实现了函数的一个动态过程链接,这样一来,它就已经包含了正确的绝对运行时地址。

首先在elf文件中找到.got的地址,0x403ff0

图 25 elf文件中.got的位置

在edb找到相应地址处,分析在dl_init前后该地址附近变化:由此可以发现,动态链接库的地址为:0x7fca43070fc0。

图 26 dl_init前

图 27 dl_init后

5.8 本章小结

本章回顾了链接的基本概念,文件的重定位过程,动态链接过程,虚拟地址空间的相关内容,并且利用edb、gdb、objdump等工具对ELF文件、虚拟地址空间、重定位过程等进行了详细分析。

(第5章1分)


6hello进程管理

6.1 进程的概念与作用

进程是指一个正在运行的程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。

作用:进程是计算机科学中最深刻、最成功的概念之一,在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些假象都是通过进程的概念提供给我们的。

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

(1)shell定义

shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行

(2)shell功能

实际上shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。不仅如此,shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的shell程序与其他应用程序具有同样的效果

(3)shell处理流程

shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(这里的应用程序可以是Linux本身的实用程序,如ls和rm,也可以是购买的商业程序,如xv,或者是自由软件,如emacs)。然后shell在搜索路径里寻找这些应用程序(搜索路径就是一个能找到可执行程序的目录列表)。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。

6.3 Hello的fork进程创建过程

当shell运行一个程序时,父进程调用fork函数生成这个程序的子进程。

新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。

父进程和新创建的子进程之间最大的区别在于它们有不同的PID。

fork函数被调用一次,却会返回两次:在父进程中,fork返回子进程的PID。在子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。

6.4 Hello的execve过程

execve函数:

int execve(char *filename, char *argv[], char *envp[])函数,filename:可执行文件;argv:参数列表,惯例:argv[0]==filename;envp:环境变量列表。

Execve函数加载并运行可执行目标文件 hello,且带参数列表argv和环境变量exenvp。hello程序覆盖当前正在执行的进程的所有的地址空间,但是这样的操作并没有创建一个新的进程,新的进程有和原先进程相同的PID,并且它还继承了打开 hello 调用execve函数之前所有已经打开的文件描述符。只有当出现错误时,例如找不到 hello文件时,execve才会返回到调用程序,execve调用成功则不会产生返回。

6.5 Hello的进程执行

逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。

时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。

用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

当shell 运行一个程序时,父shell 进程调用fork生成一个子进程,它是父进程的一个复制。子进程通过execve 系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk), 新的代码和数据段袚初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main 函数。在内核和前端之前切换的动作被称为上下文切换。

6.6 hello的异常与信号处理

6.6.1异常种类:

(1)同步异常(中断)

图 28 同步异常处理过程

(2)同步异常

陷阱——有意的,执行指令的结果

图 29 陷阱处理过程

故障——不是有意的,但可能被修复

图 30 故障处理过程

终止——非故意,不可恢复的致命错误造成

图 31 终止处理过程

6.6.2各种命令的执行

(1)正常执行并结束:

图 32 程序正常运行

(2)当按下Ctrl-Z之后,hello被挂起,使用ps命令查看,结果如图:

图 33 Ctrl-Z运行结果

(3)当按下Ctrl-Z之后,内核发送SIGINT信号给到前台进程组中的进程,其结果是终止前台的进程,使用ps命令查看,发现hello已经被回收:

图 34 键入Ctrl-Z后的结果

(4)不停乱按——把输入的字符输出在屏幕上。

图 35 不停乱按的结果

  1. 输入kill -9 6217——发送信号终止:

图 36 kill命令

6.7本章小结

本章学习了进程的基本定义及其作用;shell的定义、功能以及处理流程;学习了与进程相关的几个重要函数;了解了继承的创建、加载以及回收;知道了信号的处理过程等等。并且通过hello的运行实例加深了对进程和信号的处理。


7hello的存储管理

7.1 hello的存储器地址空间

逻辑地址是在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址,也就是是机器语言指令中,用来指定一个操作数或是一条指令的地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址即物理地址。一个逻辑地址由两部份组成,段标识符:段内偏移量。

物理地址,一个计算机操作系统的物理主存被自动组织为一个由m个连续的字节相同大小的单元内存组成的计算机数据。单元内存没有唯一的字节都是因为有一个唯一的物理单元内存地址,中央处理器会通过地址总线的寻址,找到真实的物理单元内存对应的地址,这会使得具有相同单元内存地址的计算机物理数据通过存储器被自动进行读写。

线性地址也叫虚拟地址,是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制数字表示,值得范围从0x00000000到0xfffffff)程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。

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

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

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

每个段的首地址会被储存在各自的段描述符里面,通过段选择符我们可以快速寻找到某个段的段全局描述符。逻辑地址的组成部分有两个:段标识符与段内偏移量。段标识符由一个16位长的字段组成,又被称为段选择符。它的前13位被解读为一个索引号,后面3位包含一些硬件细节。0、1位表示程序的当前优先级RPL;第2位是TI位,表示段描述符的位置:TI=0,段描述符在GDT中;TI=1,段描述符在LDT中。而索引位index就类似一个数组,每个元素内都存放一个段的描述符,索引位首地址就是我们在查找段描述符时再这个元素数组当中的索引。一个段描述符的首地址是指含有8个元素的字节,我们通常可以在查找到段描述符之后获取段的首地址,再把它与线性逻辑地址的偏移量进行相加就可以得到段所需要的一个线性逻辑地址。

图 37 段选择符

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

虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。VM系统通过将虚拟内存分割为称为虚拟页(Virtual Page,VP)的大小固定的块。每个虚拟页的大小为P=2字节。类似地,物理内存被分割为物理页(Physical Page,PP),大小也为Р字节(物理页也被称为页帧(page frame))。

虚拟地址由对应的虚拟物理页号(vpn)和虚拟页偏移量(vpo)共同组成,类似的,物理地址由虚拟物理页偏移号(ppn)和对应的物理页偏移量(ppo)共同分配组成。

页表:一个存放在物理内存的数据结构(一个页表条目PTE的数组),将虚拟页映射到物理页。页表中物理地址存在三种常见的情况:未分配、未缓存、已缓存。每个PTE是由一个有效位和一个n位地址字节组成的,有效位为1则页命中,为0则触发缺页异常。

页表的基址寄存器ptbr+vpn在页表中可以获得条目PTE,通过对比条目对应的有效位判断物理地址是上述哪一种的情况,如果有效则通过提取得出对应物理地址的页号寄存器PPN,与对应的虚拟页偏移量共同分配构成了物理地址寄存器PA。下图为基于页表的地址翻译过程。

图 38 基于页表的翻译过程

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

下图展示了Core i7的地址翻译过程,CPU产生了一个虚拟地址VA,内存管理单元从TLB取出相应的PTE,然后将其翻译为一个物理地址;若是不命中,则从页表中寻找,与7.3中的地址翻译过程类似。

图 39 TLB命中

四级页表中的VPN被分成四块,分别对应每一级页表中的虚拟页号,CR3中存储L1页表的物理地址,通过VPN1寻找的PTE中存储着下一级页表的基地址以此类推,寻找到物理页号PPN,+PPO,即为物理地址。

图 40 Core i7的地址翻译过程

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

(1)Cache结构

图 41 Cache结构

(2)定位

图 42 定位过程

①先由CI定位组,之后再根据标记位CT找到对应行,如果该行有效位为1,说明行有效,命中。之后再根据块偏移CO找到数据。

②如果有效位为0说明不命中,则在下一级缓存中或者主存中寻找。

③设立三级缓存可以极大程度降低不命中率。

7.6 hello进程fork时的内存映射

当shell运行一个程序时,父进程调用fork函数生成这个程序的子进程。

新创建的子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。

①为了给新进程创建虚拟内存,内核创建了当前进程的mm_struct、区域结构和页表的原样副本。

②两个进程中的每个页面都标记为只读

③两个进程中的每个区域结构都标记为私有的写时复制

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行新程序hello的步骤:

(1)删除已存在的用户区域:即shell中的刚fork的子进程。

(2)创建新的区域结构:

①代码和初始化数据,映射到.text和.data区(目标文件提供)

②.bss和栈映射到匿名文件。

(3)设置PC, 指向代码区域的入口点,Linux根据需要换入代码和数据页面

图 43 execve函数

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

DRAM 缓存不命中称为缺页。缺页存在以下几种情况:

①段错误:首先,先判断这个缺页的虚拟地址是否合法,那么遍历所有的合法区域结构,如果这个虚拟地址对所有的区域结构都无法匹配,那么就返回一个段错误(segment fault)

②非法访问:接着查看这个地址的权限,判断一下进程是否有读写改这个地址的权限。

③如果不是上面两种情况那就是正常缺页,那就选择一个页面牺牲然后换入新的页面并更新到页表。

图 44 Linux缺页处理

7.9动态存储分配管理

1.基本概念:

(1)在程序运行时程序员使用动态内存分配器获得虚拟内存。(数据结构的大小只有运行时才知道)。

(2)动态内存分配器维护着进程的一个虚拟内存区域,称为堆。

(3)分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。

(4)分配器的类型

①显式分配器:要求应用显式地释放任何已分配的块。例如,C语言中的malloc和free。

②隐式分配器:应用检测到已分配块不再被程序所使用,就释放这个块

2.记录空闲块的方法:

(1)隐式空闲链表:通过头部中的长度字段一隐含地链接所有块

(2)显式空闲链表:将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针

7.10本章小结

本章介绍了存储器的地址空间,讲述了逻辑地址、线性地址、物理地址、虚拟地址的概念,以及虚拟地址翻译的原理,Cache的访问原理。除此之外还有进程fork和execve时的内存映射的内容,描述了系统如何应对那些缺页异常,最后描述了malloc的内存分配管理机制。

(第7章 2分)


8hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的接口,称为Unix I/O,这使得所有的输入和输出能以一种统一且一致的方式来执行。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

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

Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。

改变当前的文件位置,对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为k。

读写文件,一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k十n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置é开始,然后更新k。

关闭文件,当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

函数:

int open(char* filename,int flags,mode_t mode) 。open函数将filename(文件名,含后缀)转换为一个文件描述符(C中表现为指针),并且返回描述符数字。

int close(fd),fd是需要关闭的文件的描述符(C中表现为指针),close返回操作结果。

ssize_t read(int fd, void *buf, size_t n);read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf 。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量.

ssize_t write(int fd, const void*buf, size_t n);write函数从内存位置buf 复制至多n个字节到描述符fd的当前文件位置。返回:若成功则为写的字节数,若出错则为-1。

8.3 printf的实现分析

首先查看函数printf的函数体:

int printf(const char *fmt, ...)

{

int i;

char buf[256];

va_list arg = (va_list)((char*)(&fmt) + 4);

i = vsprintf(buf, fmt, arg);

write(buf, i);

return i;

}

va_list是字符指针,而(char*)(&fmt + 4)表示fmt后的第一个参数的地址。

vsprintf函数返回值是要打印出来的字符串的长度,其作用是根据fmt中的格式转换符将可变参数转换到相应的格式。

最后的write函数即为写操作,把buf中的i个元素的值写入文件描述符handle所指的文档,成功时返回写的字节数,错误时返回-1

在write函数中,追踪之后的结果如下:

write:

mov eax, _NR_write

mov ebx, [esp + 4]

mov ecx, [esp + 8]

int INT_VECTOR_SYS_CALL

一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call函数,为了保存中断前进程的状态。在write函数中可以理解为其功能为显示格式化了的字符串。

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

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),这样就能将文字显示在屏幕上了。

8.4 getchar的实现分析

(1)getchar当中调用了read函数:ssize_t read (int fd, void *buf, size_t count);

参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。read函数返回读入了多少个字节。

(2)当程序调用getchar时,程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。

  1. getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾则返回-1(EOF),且将用户输入的字符回显到屏幕。
  2. 如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中等待后续getchar调用读取。也就是说后续的getchar调用不会等待用户按健,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

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

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

8.5本章小结

本章介绍了Linux的IO设备管理方法、Unix IO接口及其函数,以及对printf函数和getchar函数进行了分析。

结论

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

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

hello的一生从代码编辑器开始,由程序员编写生成代码源文件,经过以下步骤:

  1. 预处理:由预处理器cpp将源代码中的宏定义、头文件展开,生成hello.i文件
  2. 编译:由编译器ccl将hello.i文件经过一系列过词法分析,语法分析,语义分析,之后进行优化,最后可以生成相应的汇编代码(hello.s文件)
  3. 汇编:由汇编器as将hello.s文件翻译为机器语言,生成可重定位目标文件hello.o
  4. 链接:由链接器ld进行动态链接,生成可执行文件hello
  5. 运行:在终端中输入运行指令

在hello运行时,首先有shell通过fork()创建子进程,利用execve函数将hello载入,为hello分配虚拟内存空间,创建新的代码数据堆栈区。虚拟地址通过四级页表和TLB等翻译为物理地址,然后通过三级cache访问物理内存中的信息数据。在程序运行过程中,同时受内核的信号处理程序和异常处理程序控制。程序执行结束后,shell父进程回收子进程,内存删除为这个进程创建的所有数据结构。

通过本次大作业与深入理解计算机系统课程,我认识到了计算机内部的精妙,程序运行的复杂过程,仅仅只是一个循环输出hello的程序,需要内部各个部分进行配合。同时,我也认识到了自己对计算机系统的认知还不够,今后会继续加强学习

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


附件

hello.c:源代码文件

hello.i:预处理后的文本文件

hello.s:编译后的汇编文件

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

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

asm2.txt:hello.o反汇编的代码文件

hello 链接产生的可执行目标文件

elf2.txt:hello的ELF格式文件

asm.txt:hello反汇编的代码文件

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


参考文献

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

[2]https://blog.csdn.net/prike/article/details/52722934/逻辑地址、线性地址和物理地址的关系

[3]https://zhidao.baidu.com/question/56724705.html/getchar()的用法

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值