csapp 大作业 hello的一生

计算机科学与技术学院
2018年12月
摘 要
本文的目的是结合计算机系统,并使用gcc等工具,研究hello程序在ubuntu系统下的生命周期,从而深入理解课本知识,并进行融会贯通,以达到巩固知识的目的。

关键词:预处理;编译;汇编;链接;进程;管理;

目 录

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

PTP,首先通过I/O设备经过总线存入主存。然后GCC编译器驱动程序读取源程序文件hello.c,然后预处理器cpp变成hello.i(修改了的源程序)然后通过编译器ccl变成hello.s(汇编程序),然后通过编译器as变成hello.o(可重定位目标程序),此时的hello.o已经时一个机器友好的二进制代码了。最后通过连接器ld和标准的c库进行链接,最后变成hello(可执行的二进制目标程序)此时hello就是一个程序。再到shell(接收用户命令,然后调用相应程序)里面(bash)(大多数linux系统和mac os默认的shell)输入字符串“./hello”后,shell将字符逐一读入寄存器,然后再放到内存里面去,然后shell调用fork函数创建一个新运行的子进程,这个子进程就是父进程shell的复制,然后子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,然后使用mmap(用open系统调用打开文件, 并返回描述符fd.用mmap建立内存映射, 并返回映射首地址指针start.对映射(文件)进行各种操作, 显示(printf), 修改(sprintf).
用munmap(void *start, size_t lenght)关闭内存映射.用close系统调用关闭文件fd.)函数创建一个新的内存区域,并且创建一组新的代码,数据,栈和堆。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页的大小的片(chunk),新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到start地址,它最终会调用应用程序的main函数。然后程序从内存读取取令字节,然后再从寄存器读入最多两个数,然后在执行阶段算术/逻辑单元要么执行指令指明的操作,,计算内存引用的有效地址,要么增加或减少栈指针。然后在流水化的系统中,待执行的程序被分解为几个阶段,每个阶段完成执行指令的一部分。最后变成一个process在内存中。
020指的是程序在内存中020.初始时hello执行上述的过程,然后在程序执行结束之后,进程会保持在一种已终止状态中,直到该父进程也就是shell进程回收然后退出,shell会再变成hello执行之前的状态,也就是回到0状态。

1.2 环境与工具

硬件:
X64 CPU;2.8GHz;8G RAM;1THD Disk;
软件:
Windows10 64位;Vmware 14;Ubuntu 16.04 LTS 64位;
开发工具:
Visual Studio 2015 64位;CodeBlocks;vi/vim/gpedit+gcc;

1.3 中间结果

Hello.i:修改了的源程序。功能:1.加载头文件2进行宏替换(对代码里定义的macro宏替换成你定义的东西)3.条件编译
Hello.s:汇编程序,包含汇编语言程序。
Hello.o:可重定位目标程序,将汇编语言翻译成机器语言指令,并将指令打包成一种叫做可重定位目标程序的格式。

1.4 本章小结

对hello的分析由此开始。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理器(cpp)根据以字符#开头的命令,修改原始的c程序。
作用:预编译主要处理那些以#开头的预编译指令;

  1. 将所有的#define删除,并且展开所有的宏定义;
  2. 处理所有的条件编译指令,如#if,#ifdef等;
  3. 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。该过程递归进行(被包含的文件可能包含其他文件)
  4. 删除所有注释
  5. 添加行号和文件标记
  6. 保留所有的编译器指令(预处理过的文件不再存在宏,源程序的宏都会被替代)

2.2在Ubuntu下预处理的命令

图2.2
Hello的预处理结果解析

2.3 Hello的预处理结果解析

预处理后,头文件被引入,注释被去除,宏都被替换,添加了行号,编译指令被保留。
2.4 本章小结
这一步过后,文件中的宏不再存在,源程序的宏被替代。预处理可以查看宏是否正确或者头文件是否正确。

第3章 编译

3.1 编译的概念与作用

概念:编译器ccl将文本文件

hello.i翻译成文本文件hello.s,包含一个汇编语言程序。
作用:编译过程就是把预处理完的文件进行一系列词法分析,语法分析,语义分析及优化而生成的汇编代码文件。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令


图3.2
应截图,展示编译过程!

3.3 Hello的编译结果解析

3.3.1

图3.3.1
Sleepsecs保存在.rodata段当中,由于其值在随后的程序中没有被更改,故被优化为只读变量。
3.3.2

图3.3.2
在main函数下面,对main函数进行操作之后,初始化i。
3.3.3

图3.3.3
条件判断if(argc!=3)的汇编,第一排是读取主函数参数argc,第三排是比较3与argc的关系,相等跳到L2,否则执行L2后面的代码。

3.3.4
For循环
分为三部分:

  1. 初始化变量,给i,也就是-4(%rbp)赋初值,汇编代码如图3.3.4.1(movl,也就是双字,前4个字节被赋值为0.)

图3.3.4.1

  1. 条件判断语句(i<10),如图3.3.4.2,9与-4(%rbp)比较,jle(小于或等于)跳转到L4,

图3.3.4.2

  1. 循环结构代码

图3.3.4.3
注意里面的$16,%rax,和$8,rax,是因为argv为char,shi8个字节,每次保存都要在-32(%rbp)基础上+8*x。

  1. Printf函数调用
    先传参,如图3.3.4.3,%rdi,%rsi,%rdx即为三个参数,分别构造这三个参数。然后调用,如图3.3.4.4

图3.3.4.4

3.4 本章小结

编译阶段,编译器将高级语言编译成汇编语言。汇编语言是直接面向处理器(Processor)的程序设计语言。处理器是在指令的控制下工作的,处理器可以识别的每一条指令称为机器指令。每一种处理器都有自己可以识别的一整套指令,称为指令集。处理器执行指令时,根据不同的指令采取不同的动作,完成不同的功能,既可以改变自己内部的工作状态,也能控制其它外围电路的工作状态。
汇编语言的另一个特点就是它所操作的对象不是具体的数据,而是寄存器或者存储器,也就是说它是直接和寄存器和存储器打交道,这也是为什么汇编语言的执行速度要比其它语言快,但同时这也使编程更加复杂,因为既然数据是存放在寄存器或存储器中,那么必然就存在着寻址方式,也就是用什么方法找到所需要的数据。例如上面的例子,我们就不能像高级语言一样直接使用数据,而是先要从相应的寄存器AX、BX 中把数据取出。这也就增加了编程的复杂性,因为在高级语言中寻址这部分工作是由编译系统来完成的,而在汇编语言中是由程序员自己来完成的,这无异增加了编程的复杂程度和程序的可读性。

    再者,汇编语言指令是机器指令的一种符号表示,而不同类型的CPU 有不同的机器指令系统,也就有不同的汇编语言,所以,汇编语言程序与机器有着密切的关系。所以,除了同系列、不同型号CPU 之间的汇编语言程序有一定程度的可移植性之外,其它不同类型(如:小型机和微机等)CPU 之间的汇编语言程序是无法移植的,也就是说,汇编语言程序的通用性和可移植性要比高级语言程序低。

   总结起来就是三个特点:机器相关性、高速度和高效率、编写和调试复杂(相对于高级语言)。

第4章 汇编

4.1 汇编的概念与作用

概念:
汇编指的是汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,包含hello程序执行的机器指令。
作用:
实现将汇编代码转换为机器指令,使之在链接后能够被计算机直接执行。

4.2 在Ubuntu下汇编的命令

图4.2 将hello.s汇编为机器语言

4.3 可重定位目标elf格式

图4.3.1ELF头信息
可知:数据采用补码表示,小端法。13个节头表,每个占64bytes。

图4.3.2
下图时hello.o程序的ELF表在我的机器上的表示:

图4.3.3
由图可看出ELF表的结构,.text,.rela.data,.data,.bss(程序中未初始化的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。)……

图4.3.4可重定位的目标文件的ELF格式

4.4 Hello.o的结果解析

汇编指令是机器指令便于记忆的书写格式
机器语言指的是二进制的机器指令集合,而机器指令是由操作码和操作数构成的。汇编语言的主体是汇编指令。汇编指令和机器指令的差别在于指令的表示方法上。
在hello.s和hello.o之间比较,汇编器(as)在汇编hello.s时:

  1. 为每条语句加上具体的地址,全局变量和常量都被安排到具体的地址里面。
  2. 操作数在hello.s都是十进制,在hello.o当中都是机器级程序所要的十六进制。
  3. 跳转语句对应的符号变成相对偏移地址。
  4. 函数调用的函数名字变成函数的相对偏移地址。

    4.5 本章小结

    本章介绍了程序生成过程中编译器汇编的相关内容。汇编过程将汇编语言转换为机器代码,生成可重定位的目标文件,使机器能够直接处理与执行。可以通过readelf读取其elf信息与重定位信息,得到其符号表的相关信息。另外,可以通过objdump反汇编目标文件,从中可以得到机器代码与汇编代码的对照。
    作为机器可以直接执行的语言,机器语言与汇编语言存在映射关系,能够反映机器执行程序的逻辑。

第5章 链接

5.1 链接的概念与作用

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

作用
使得分离编译成为可能,不用将大型的应用程序组织成为一个巨大的源文件,而是可以将它分为更小,更容易管理的模块,可以独立的修改和编译这些模块。当我们改变其中的一个时,只要重新编译它,再链接上去应用就可以了。

5.2 在Ubuntu下链接的命令

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

节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400200 00000200
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 000000000040021c 0000021c
0000000000000020 0000000000000000 A 0 0 4
[ 3] .hash HASH 0000000000400240 00000240
0000000000000034 0000000000000004 A 5 0 8
[ 4] .gnu.hash GNU_HASH 0000000000400278 00000278
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 0000000000400298 00000298
00000000000000c0 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400358 00000358
0000000000000057 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 00000000004003b0 000003b0
0000000000000010 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 00000000004003c0 000003c0
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 00000000004003e0 000003e0
0000000000000030 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400410 00000410
0000000000000078 0000000000000018 AI 5 19 8
[11] .init PROGBITS 0000000000400488 00000488
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000004004a0 000004a0
0000000000000060 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400500 00000500
0000000000000132 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 0000000000400634 00000634
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 0000000000400640 00000640
000000000000002f 0000000000000000 A 0 0 4
[16] .eh_frame PROGBITS 0000000000400670 00000670
00000000000000fc 0000000000000000 A 0 0 8
[17] .dynamic DYNAMIC 0000000000600e50 00000e50
00000000000001a0 0000000000000010 WA 6 0 8
[18] .got PROGBITS 0000000000600ff0 00000ff0
0000000000000010 0000000000000008 WA 0 0 8
[19] .got.plt PROGBITS 0000000000601000 00001000
0000000000000040 0000000000000008 WA 0 0 8
[20] .data PROGBITS 0000000000601040 00001040
0000000000000008 0000000000000000 WA 0 0 4
[21] .comment PROGBITS 0000000000000000 00001048
000000000000002a 0000000000000001 MS 0 0 1
[22] .symtab SYMTAB 0000000000000000 00001078
0000000000000498 0000000000000018 23 28 8
[23] .strtab STRTAB 0000000000000000 00001510
0000000000000150 0000000000000000 0 0 1
[24] .shstrtab STRTAB 0000000000000000 00001660
00000000000000c5 0000000000000000 0 0 1

5.4 hello的虚拟地址空间

图5.4
上图是利用edb加载hello获取的数据,左下角对应详细的数据段信息,右上对应5.3的名字。

5.5 链接的重定位过程分析

图5.5.1 hello.o反汇编的结果

图5.5.2 hello反汇编的结果

可以看出,hello.o没有经过链接,地址也只是虚拟地址,main函数地址从0开始,
也没有调用如printf这样函数的代码,也就是说hello中相对hello.o增加了许多的外部链接来的函数。
重定位:
重定位的实现依靠.rodata这个段,这个段保留重定位所需的信息,叫做重定位条目,链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。

5.6 hello的执行流程

ld-2.27.so! dl_start
0x7ffee5aca680
ld-2.27.so! dl_init
0x7f9f48629630
hello!_start
0x0x400500
ld-2.27.so!_libc_start_main
0x7f9f48249ab0
libc-2.27.so! cxa_atexit
0x7f4523fd6af7
libc-2.27.so! lll_look_wait_private
0x7f4523ff8471
libc-2.27.so!_new_exitfn
0x7f87ff534220
hello!_libc_csu_init
0x7f87ff512b26
libc-2.27.so!_setjmp
0x7f87ff512b4a
libc-2.27.so!_sigsetjmp
0x7f87ff52fc12
libc-2.27.so!__sigjmp_save
0x7f87ff52fbc3
hello_main
0x400532
hello!puts@plt
0x4004b0
hello!exit@plt
0x4004e0
hello!printf@plt
0x400587
hello!sleep@plt
0x400594
hello!getchar@plt
0x4005a3
dl_runtime_resolve
0x7f169ad84750
libc-2.27.so!exit
0x7fce8c889128

5.7 Hello的动态链接分析

概念:
GOT表:
概念:每一个外部定义的符号在全局偏移表(Global offset Table)中有相应的条目,GOT位于ELF的数据段中,叫做GOT段。
作用:把位置无关的地址计算重定位到一个绝对地址。程序首次调用某个库函数时,运行时连接编辑器(rtld)找到相应的符号,并将它重定位到GOT之后每次调用这个函数都会将控制权直接转向那个位置,而不再调用rtld。
PLT表:
过程连接表(Procedure Linkage Table),一个PLT条目对应一个GOT条目
当main()函数开始,会请求plt中这个函数的对应GOT地址,如果第一次调用那么GOT会重定位到plt,并向栈中压入一个偏移,程序的执行回到_init()函数,rtld得以调用就可以定位printf的符号地址,第二次运行程序再次调用这个函数时程序跳入plt,对应的GOT入口点就是真实的函数入口地址。
动态连接器并不会把动态库函数在编译的时候就包含到ELF文件中,仅仅是在这个ELF被加载的时
候,才会把那些动态函库数代码加载进来,之前系统只会在ELF文件中的GOT中保留一个调用地址.
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。

在dl_init调用之后, 0x601008和0x601010处的两个8B数据分别发生改变为0x7fd9 d3925170和0x7fd9 d3713680,其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]指向动态链接器ld-linux.so运行时地址。

在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。
因为在PLT中使用的jmp,所以执行完目标函数之后的返回地址为最近call指令下一条指令地址,即在main中的调用完成地址。

5.8 本章小结

链接是将各种代码和数据片段收集并组合成一个单一文件的过程。
链接在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。
并且在本章中还对hello的可执行文件格式、hello的虚拟地址空间、重定位过程、执行流程、动态链接进行了分析。

第6章 hello进程管理

6.1 进程的概念与作用

进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。

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

作用:shell 是一个交互型的应用级程序,它代表用户运行其他程序。
处理流程:shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。

6.3 Hello的fork进程创建过程

shell中构造完argv与envp后,调用fork()函数,创建一个子进程,与父进程shell完全相同(只读/共享),包括只读代码段,可读写数据段、堆以及用户栈等。

6.4 Hello的execve过程

shell调用execve()函数,在当前进程(fork创建的子进程)的上下文中加载并允许hello程序。将hello中的.text节 、 .data节 、 .bss节等内容加载到当前进程的虚拟地址空间。

6.5 Hello的进程执行

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

6.6 hello的异常与信号处理

异常的种类:

  1. 中断:SIGSTP:挂起程序
  2. 终止:SIGINT:终止程序

  3. 回车与乱按

图6.6.1

  1. Ctrl+C

图6.6.2

  1. ps

图6.6.3

  1. Ctrl+Z

图6.6.4
5.fg,继续已经体制的进程(发送SIGCONT信号)

图6.6.5

6.7本章小结

进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态含义。
进程为应用程序提供了两个关键抽象。
Shell-bash作为命令解释器,有解释命令的功能以及其特殊的处理流程。
shell中输入运行hello的命令行后,hello在shell创建的fork子进程中由execve函数加载后执行。并且其还有自身独特的前后文以及异常和信号处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:
相对地址,是程序运行由CPU产生的与段相关的偏移地址部分。他描述一个程序运行段的地址。
物理地址:
程序运行时加载到内存地址寄存器中的地址,内存单元的真正地址。他是在前端总线上传输的而且是唯一的。在hello程序中,他就表示了这个程序运行时的一条确切的指令在内存地址上的具体哪一块进行执行。
线性地址:这个和虚拟地址是同一个东西,是经过段机制转化之后用于描述程序分页信息的地址。

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

一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
索引号,这里可以直接理解成数组下标,它对应的“数组”就是段描述符表,段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
这里面,我们只用关心Base字段,它描述了一个段的开始位置的线性地址。
Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
• 看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
• 拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
• 把Base + offset,就是要转换的线性地址了

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

• 计算机利用页表,通过MMU来完成从虚拟地址到物理地址的转换。

PTBR是cpu里的一个控制寄存器,指向当前页表,n位的虚拟地址包括p位的虚拟页面偏移VPO和n-p位的虚拟页号VPN。MMU通过VPN来选择适当的PTE,将页表条目中的PPN(物理页号)和虚拟地址的VPO串联起来,就得到相应的物理地址。(计算机TLB采用四级页表支持下的VA到PA的转换,36位VPN 被划分成四个9 位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量,以此类推)

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

每次cpu产生一个虚拟地址,MMU需要查询一个PTE,如果运气不好,需要从内存中取得,这需要花费很多时间,通过TLB(翻译后备缓冲器)能够消除这些开销。TLB是一个小的,虚拟寻址的缓存,在MMU里,其每一行都保存着一个单个PTE组成的块,TLB通常具有高度相联度

用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。
如果是32位系统,我们有一个32位地址空间,4KB的页面和一个4字节的PTE,我们总需要一个4MB的页表驻留在内存中,而对于64位系统,我们甚至需要8PB的空间来存放页表,这显然是不现实的。用来压缩页表的常见方式就是使用层次结构的页表。
如果是二级页表,第一级页表的每个PTE负责一个4MB的块,每个块由1024个连续的页面组成。二级页表每一个PTE负责一个4KB的虚拟地址页面。这样的好处在于,如果一级页表中有一个PTE是空,那么二级页表就不会存在,这样会有巨大的潜在节约,因为4GB的地址空间大部分都是未分配的。

现在的64位计算机采用4级页表,36位的VPN被封为4个9位的片,每个片被用作一个页面的偏移,CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1PET的偏移量,这个PTE包含L2页表的基地址,VPN2提供一个到L2PTE的偏移量,以此类推。

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

当我们通过MMU得到了物理地址后,我们就需要去内存里去相应的数据,当从内存直接取数据速度太慢,计算机利用cache(高度缓存)来加快访存速度。它位于CPU与内存之间,访问速度比内存块很多,需要从内存里取数据时,先考虑否在cache里有缓存。下图是一个典型的cache结构。

图7.5 图7.6
那么我们又是如何确定物理地址所对应的是高速缓存中的哪个部分根据高速缓存的大小,我们把物理地址分割成这些部分,其中S = 2^s,B = 2^b,剩下的t位都是标记位,得到一个物理地址后,通过组索引部分可以确定在cache里的哪一组,通过标记位确定看是否与组里的某一行标记相同,如果有,通过块偏移位确定具体是哪个数据块,从而得到我们的数据。如果没有找到,则需要从内存里去数据,并找到cache里的一行替换,对于L1,L2这样的组相联cache,替换策略通常有LFU(最不常使用),LRU(最近最少使用)。

图7.5.1 三级cache支持下物理内存访问示意图

7.6 hello进程fork时的内存映射

图7.6
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数在shell中加载并运行包含在可执行文件hello中的程序,用hello程序有效地替代了当前程序。加载hello的过程主要步骤如下:
首先删除已存在的用户区域,也就是将shell与hello都有的区域结构删除。然后映射私有区域,即为新程序的代码、数据、bss和栈区域创建新的区域结构,均为私有的、写时复制的。下一步是映射共享区域,将一些动态链接库映射到hello的虚拟地址空间,最后设置程序计数器,使之指向hello程序的代码入口。
经过这个内存映射的过程,在下一次调度hello进程时,就能够从hello的入口点开始执行了。

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

虚拟内存中,DRAM缓存不命中称为缺页。如图7-13,CPU需要引用VP3中的一个字,通过读取PTE3,发现有效位为0,说明不在内存里,这时就发生了缺页异常。缺页异常发生时,通常会调用内核里的缺页异常处理程序,该程序会选择一个牺牲页,这里是存放在PP3的VP4,如果VP4已经被修改,内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4的页表条目,反映出VP4不再缓存在内存里。

图7.8.1
接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令。
缺页处理程序不是直接就替换,它会经过一系列的步骤:
1.虚拟地址是合法的吗?如果不合法,它就会触发一个段错误
2.试图进行的内存访问是否合法?意思就是进程是否有读,写或者执行这个区域的权限
3.经过上述判断,这时才能确定这是个合法的虚拟地址,然后才会执行上述的替换。

图7.8.2

7.9动态存储分配管理

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

  1. 显式执行:显式地释放任何已分配的块。
  2. 隐式执行:检测一个已分配块何时不再使用,那么就释放这个块。该过程又被称为垃圾收集。
    显示空间链表:将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针。使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。已分配块和空闲块的基本结构如下:

图7.9.1
维护链表顺序包括:

  1. 后进先出,将新释放的块放置在链表的开始处。
  2. 按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继块的地址。
    带边界标签的隐式空闲链表:在内存块中增加4字节的头部和4字节的脚部。其中头部用于寻找下一个块,脚部用于寻找上一个块。脚部的设计是专门为了合并空闲块的。其基本结构如下图:

图7.9.2
7.10本章小结
本章中,我们具体描述了hello的存储器地址空间,分为逻辑地址,物理地址,线性地址和虚拟地址四类。并且,了解了Intel逻辑地址到线性地址的变化——段式管理(课本上未提到过,特地上网查询)、线性地址到物理地址的变化——页式管理。还分别描述了TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问、hello进程时fork时的内存映射、缺页故障与缺页中断处理,
动态存储分配管理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

8.2 简述Unix IO接口及其函数

Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K 。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
1.进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:
int open(char filename, int flags, mode_t mode);
open 函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。
返回:若成功则为新文件描述符,若出错为-1。
2.进程通过调用close 函数关闭一个打开的文件。
int close(int fd);
返回:若成功则为0, 若出错则为-1。
3.应用程序是通过分别调用read 和write 函数来执行输入和输出的。
ssize_t read(int fd, void
buf, size_t n);
read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf 。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。
返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1。
ssize_t write(int fd, const void *buf, size_t n);
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出。
返回:若成功则为写的字节数,若出错则为-1。

8.3 printf的实现分析

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

8.4 getchar的实现分析

键盘中断的处理过程
当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求。键盘中断服务程序先从键盘接口取得按键的扫描码,然后根据其扫描码判断用户所按的键并作相应的处理,最后通知中断控制器本次中断结束并实现中断返回。
若用户按下双态键(如:Caps Lock、Num Lock和Scroll Lock等),则在键盘上相应LED指示灯的状态将发生改变;
若用户按下控制键(如:Ctrl、Alt和Shift等),则在键盘标志字中设置其标志位;
若用户按下功能键(如:F1、F2、…等),再根据当前是否又按下控制键来确定其系统扫描码,并把其系统扫描码和一个值为0的字节存入键盘缓冲区;
若用户按下字符键(如:A、1、+、…等),此时,再根据当前是否又按下控制键来确定其系统扫描码,并得到该按键所对应的ASCII码,然后把其系统扫描码和ASCII码一起存入键盘缓冲区;
getchar函数落实到底层调用了系统函数read,通过系统调用read读取存储在键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装,大体逻辑是读取字符串的第一个字符然后返回。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,这就是Unix I/O接口。
同时,本章中我们就hello里面的函数对应unix的I/O细致地分析了一下I/O对接口以及操作方法,prinf与getchar函数是Unix I/O的封装,而真正调用的是write和read这样的系统调用函数,而它们又都是由内核完成的,之所以键盘能输入是因为引发了异步异常,在屏幕上会有显示是因为字符串被复制到了屏幕赖以显示的显存当中。
结论
一个看似简单的hello程序,从编写代码到最后运行将结果现实在我们面前,竟然经历了从预处理(对带#的指令解析,hello.i文件),编译,生成.s文件,汇编,转换为机器码生成重定位信息,.o文件,链接,与动态库链接,产生可执行文件hello,创建进程,在shell利用./hello运行hello程序,父进程利用fork函数为hello创建进程。加载,通过加载器,调用execve函数,删除原来的进程内容,加载我们现在的进程的代码和数据等到进程的虚拟内存空间。执行指令:cpu取指令,顺序顺序执行进程的逻辑控制流。这里CPU会给出一个虚拟地址,通过MMU从页表里得到物理地址, 在通过这个物理地址去cache或者内存里得到我们想要的信息,最后结束,父进程回收子进程,内核删除为这个进程创建的所有数据结构。这次大作业也算是带领我们完整的了解了一下一个程序的完整生命周期,算是对计算机有了入门的了解了把,以后加油!
附件
hello.c hello的源代码
hello.i hello.c经过预处理后的代码
hello.s hello.i经编译后的代码
hello.o hello.i汇编后得到的可重定位目标文件
hello hello.o链接后得到的可执行目标文件
hello1.txt hello.o的反汇编文件
hello.txt hello的反汇编文件
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] ELF中与动态链接相关的段
https://blog.csdn.net/virtual_func/article/details/48792087
[2] ELF中与动态链接相关的段
https://blog.csdn.net/virtual_func/article/details/48792087
linux bash总结
http://www.cnblogs.com/skywang12345/archive/2013/05/30/3106570.html.
[4] x86在逻辑地址,线性地址,理解虚拟地址和物理地址
https://www.cnblogs.com/bhlsheji/p/4868964.html
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] 深入理解计算机系统(第三版) Randal E.Bryant David R.O’Hallaron!

猜你喜欢

转载自www.cnblogs.com/Tianjishu/p/10226795.html
今日推荐