计算机系统
大作业
题 目 程序人生-Hello’s P2P 专 业 计算学部
计算机科学与技术学院 2021年5月 摘 要 本文主要介绍hello程序在linux如何从一个开始.c经过预处理、编译、汇编和链接,文件逐步成为可执行文件。对于操作过程中的过程、信号、异常处理、存储处理IO探索管理等操作。目的是阐述程序从建立到执行的所有过程,并结合课堂知识进行解读。
关键词:计算机系统、预处理、编译、汇编、链接、过程、存储管理IO管理
目 录
第1章 概述 - 4 - 1.1 HELLO简介 - 4 - 1.2 环境与工具 - 4 - 1.3 中间结果 - 4 - 1.4 本章小结 - 5 - 第2章 预处理 - 6 - 2.1 预处理的概念和作用 - 6 - 2.2在UBUNTU下一个预处理命令 - 7 - 2.3 HELLO预处理结果分析 - 7 - 2.4 本章小结 - 9 - 第3章 编译 - 11 - 3.1 概念与作用的编译 - 11 - 3.2 在UBUNTU下面编译的命令 - 11 - 3.3 HELLO分析编译结果 - 11 - 3.3.1数据 - 12 - 3.3.2关系操作 - 12 - 3.3.3控制转移 - 13 - 3.3.4算数操作 - 13 - 3.3.5数组操作 - 14 - 3.3.6函数调用 - 14 - 3.3.7函数返回 - 15 - 3.4 本章小结 - 15 - 第4章 汇编 - 16 - 4.1 汇编的概念和功能 - 16 - 4.2 在UBUNTU下面汇编的命令 - 16 - 4.3 可重定位目标ELF格式 - 16 - 4.4 HELLO.O的结果解析 - 18 - 4.5 本章小结 - 19 - 第5章 链接 - 20 - 5.1 链接的概念和功能 - 20 - 5.2 在UBUNTU下链接命令 - 20 - 5.3 可执行目标文件HELLO的格式 - 20 - 5.4 HELLO虚拟地址空间 - 22 - 5.5 链接的重定位过程分析 - 22 - 5.6 HELLO的执行流程 - 24 - 5.7 HELLO动态链接分析 - 24 - 5.8 本章小结 - 25 - 第6章 HELLO进程管理 - 26 - 6.1 过程的概念和作用 - 26 - 6.2 简述壳SHELL-BASH作用及处理过程 - 26 - 6.3 HELLO的FORK创建过程的过程 - 26 - 6.4 HELLO的EXECVE过程 - 27 - 6.5 HELLO的进程执行 - 27 - 6.6 HELLO异常和信号处理 - 27 - 6.7本章小结 - 29 - 第7章 HELLO的存储管理 - 30 - 7.1 HELLO存储地址空间 - 30 - 7.2 INTEL从逻辑地址到线性地址的转换-段管理 - 30 - 7.3 HELLO从线性地址到物理地址的转换-页面管理 - 31 - 7.4 TLB支持四级页面VA到PA的变换 - 32 - 7.5 三级CACHE支持的物理内存访问 - 32 - 7.6 HELLO进程FORK时间内存映射 - 32 - 7.7 HELLO进程EXECVE时间内存映射 - 33 - 7.8 缺页故障和缺页中断处理 - 33 - 7.9动态存储分配管理 - 33 - 7.10本章小结 - 34 - 第8章 HELLO的IO管理 - 35 - 8.1 LINUX的IO设备管理方法 - 35 - 8.2 简述UNIX IO接口及其函数 - 35 - 8.3 PRINTF的实现分析 - 36 - 8.4 GETCHAR的实现分析 - 37 - 8.5本章小结 - 37 - 结论 - 38 - 附件 - 39 - 参考文献 - 40 -
第1章 概述 1.1 Hello简介 P2P过程:对hello.c文本(program)进行预处理(gcc -E)生成hello.i文件,编译(gcc -S)生成汇编代码hello.s文件,然后汇编(gcc -c)生成目标文件hello.o最后,链接操作生成二进制可执行文件。然后shell将程序fork,产生子过程,即为process。 020过程:然后execve,映射虚拟内存,载入物理内存,执行目标代码,CUP分配时间片执行逻辑控制流。操作结束后,父亲回顾子过程,删除核心,最终实现Zeor->Zero。 1.2 环境与工具 硬件环境: 设备名称 LAPTOP-LR4FDSS7 处理器 Intel? Core? i7-9750H CPU @ 2.60GHz 2.59 GHz 机带 RAM 16.0 GB 系统类型 64 位置操作系统, 基于 x64 的处理器 软件环境 版本 Windows 10 家庭中文版 VMware Workstation 15 Pro Ubuntu 16.04/20.04 1.3 中间结果 文件名称 文件作用 hello.c 源代码 hello.i 预处理后的文件 hello.s 编译后的汇编文件 hello.o 汇编目标文件 hello 链接后的可执行文件 objdump.txt 对hello.o反汇编文件 Objdump2.txt 对hello反汇编文件 ELF.elf hello.o的elf文件 ELF2.elf hello的elf文件
1.4 本章小结 本部分主要介绍hello的P2P、在020过程中,给出了实验中产生的硬件、软件环境和工具 。
第2章 预处理 2.1 预处理的概念和作用 概念: 预处理是 C 语言程序从源代码到可执行程序的第一步主要是 C 语言编译器处理各种预处理命令,包括包含头文件、扩展宏定义、选择条件编译等。 作用: 预处理的主要功能是使编译器在随后的文本编译过程中更加方便,便于编译器的操作,因为以下会影响编译器在预处理阶段的操作。
- 展开头文件:#include将包含的文件插入指令位置
- 宏展开: 展开所有宏定义,删除#define
- 条件编译: 处理所有的条件预编译指令: #if、 #ifdef、 #else
- 删除注释行的内容
- 添加行号和文件名标识,并在编译和调试过程中显示行号信息
- 保留#pragma指令 简单分析 文件包括:根据以字符#开头的命令修改原始C程序.比如hello.c中第1行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h并将其直接插入程序文本中, 递归过程和包含的文件也可能包含其他文件. 宏定义:将宏名替换为文本,即在具体分析相关命令或句子的含义和功能之前,例如 #define IMAX 100,程序中的所有标识符IMAX具体值1000 条件编译:有些句子希望在条件满足时编译.例如 #ifdef 标识符 程序段1 #else 程序段2 #endif 当标识符被定义时,程序段1只参与编译. 2.2在Ubuntu下一个预处理命令 操作指令:gcc -E hello.c -o hello.i(如图2.2.1)
(图2.2.1) 预处理指令和生成文件 简单比较预处理后文件和源代码的属性(见图2.2.2) 源代码只有527字节,但预处理后的文件有63350字节。
(图2.2.2) .c文件与.i文件的对比 2.3 Hello预处理结果分析 以下是预处理后源代码的位置(见图2.3.1)
(图2.3.1)预处理后源代码位置 因为预处理是对开头#命令的操作,所以程序中定义的变量和函数操作不会改变,所以这些与源代码的函数部分(见图2.3.2)是完全相同的。同样的,在hello.i程序中没有注释部分,预处理器还将使用库的地址和库中的函数(见图2.3.3)加入文本,与我们不需要预处理的代码一起构成hello.i编译器继续编译文件。
(图2.3.2)源代码内容
(图2.3.3)hello.i代码部分 2.4 本章小结 介绍了预处理的概念和功能Ubuntu以下预处理指令将hello.c转化为hello.i并对其进行了分析。 预处理过程是计算机对程序操作的起始过程,在此过程中预处理器对hello.c初步解释文件,将主要文件和宏注释扩展到程序中,删除对程序无效的注释部分,不更改函数,最后保存处理后的文本hello.i文件中。
第3章 编译 3.1 概念与作用的编译 概念: 编译阶段是在预处理之后的下一个阶段,在预处理阶段过后,我们获得了一个hello.i文件的编译阶段是编译器(ccl)对hello.i处理文件的过程。在这个阶段,编译器将完成对代码语法和语义的分析,生成汇编代码,并保存代码hello.s文件中。只有汇编指令CPU相关性,即C代码和python如果代码逻辑相同,编译结果实际上是相同的。 作用:
- 语法检查代码,反馈错误,编译失败。
- 如果没有语法错误,则生成汇编代码,汇编器可以在随后的步骤中操作汇编代码。
- 覆盖处理
- 目标程序优化 3.2 在Ubuntu下编译的命令 操作指令:gcc -S hello.i -o hello.s(如图3.2.1)
(图3.2.1)编译指令及生成文件 3.3 Hello的编译结果解析 汇编指令: 指令 含义 .file 声明源文件 .text 以下是代码段 .section.rodata 以下是rodata节 .globl 声明一个全局变量 .type 指定函数类型和对象类型 .size 声明大小 .long .string 声明一个long,string类型 .align 声明对指令或者数据的存放地址进行对齐的方式 3.3.1数据 (1)字符串 源代码中已规定的字符串在.rodata段中保存,使用时直接使用标志(见图3.3.3.1.1)
(图3.3.3.1.1) (2)整形数据 int i;在main函数中作为局部变量被直接保存在栈中。(见图3.3.1.2)
(图3.3.1.2) argc作为参数传入被保存在栈中(见图3.3.1.3)
(图3.3.1.3) (3)数组 传入的三个参数与文件地址名组成一个字符串数组argv[],数组保存在栈中。(见图3.3.1.3) 3.3.2关系操作 (1)判断argc是否等于4,直接调用cmpl指令,更新条件码。
(2)判断i是否小于8,同样是调用cmpl比较栈中值与7进行比较,更新条件码。
3.3.3控制转移 之前每一个cmpl的操作之后,都紧跟着一个j的操作,这个j的含义就是jmp,起到控制函数跳转的作用,j后面跟的参数,就对应了在满足什么条件的时候进行跳转。具体跳转判断见(图3.3.3.1)
(图3.3.3.1) (1)if语句 根据之前对比生成的条件码,进行跳转操作。
(2)for语句 类似于goto loop。 .L2是循环初始化,跳转到.L3判断是否满足条件,满足则.L4执行循环体,执行完之后依然进入到.L3进行判断。
3.3.4算数操作 i++; 对于i的运算,可以直接对寄存器保存的地址中的值进行操作,将这个值增加1。
3.3.5数组操作 数组保存在栈中,利用与rbp的相对位置进行寻址。找到初始位置,后面引用加相对位置即可。在汇编中只保存了首地址argv,所以通过连续地址的调用就可以了。
3.3.6函数调用 (1)printf 首先将要打印的字符放到edi/rdi中,因为只有一个字符串,所以汇编调用了puts函数。
后面的则是调用了printf函数,这里一共有三个参数。
(2)exit 传入退出参数1,然后调用exit
(3)atoi 传入参数,一个参数,把字符串首地址放入radi,然后调用函数atoi
(4)sleep 与atoi类似
(5)getchar 没有参数,直接调用即可
3.3.7函数返回 所有函数的返回值都是将返回值放入寄存器rax之中,然后ret即可
3.4 本章小结 本章主要讲述了编译器如何将预处理之后的文本编译为汇编代码的。 分别介绍了:数据、关系操作、控制转移、算数操作、数组操作、函数调用、函数返回。 在编译阶段,编译器将高级语言编译成汇编语言。汇编语言是直接面向处理器的语言。汇编语言指令是机器指令的一种符号表示,而不同类型的CPU 有不同的机器指令系统,也就有不同的汇编语言,所以,汇编语言程序与机器有着密切的关系。 同时,编译器在编译代码的时候,会进行一些隐式的优化,函数顺序也并不完全按照代码的构思来,最终生成hello.s文件。
第4章 汇编 4.1 汇编的概念与作用
概念:汇编(as),就是把汇编指令转为机器码的过程,机器码可以被CPU直接执行。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。 作用:汇编代码只是人可以看懂的代码,但机器并不能读懂,真正机器可以读懂并执行的是机器代码,也就是二进制代码。汇编的作用就是将之前在hello.s中保存的汇编代码翻译成可以供机器执行的二进制代码,这样机器就可以根据01代码,真正的开始执行所写的程序了。 4.2 在Ubuntu下汇编的命令 操作指令:gcc -c hello.s -o hello.o(见图4.2.1)
(图4.2.1)汇编指令及生成文件 4.3 可重定位目标elf格式 使用 readelf -a hello.o > ELF.elf 指令获得 hello.o 文件的 ELF 格式。(见图4.3.1)
(图4.3.1)指令及生成文件 ELF的组成: (1) ELF Header 以Magic开始,Magic 描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。
(图4.3.2)ELF Header内容 (2) Section Headers:节头部表,包含了文件中各个节的语义,包括节的类型、位置和大小等。
(图4.3.3)Section Header内容 (3) 重定位节.rela.text ,包含.text节中需要进行重定位的信息,目标文件和其他文件组合时,需要修改这些位置。如图 4.4,图中 8 条重定位信息分别是对.L0、puts 函数、exit 函数、.L1、printf 函数、atoi函数、sleep函数、getchar 函数进行重定位声明。
(图4.3.4)重定位节内容 4.4 Hello.o的结果解析 操作指令:objdump -d -r hello.o >objdump.txt
(图4.4.1)反汇编指令及生成文件 机器语言指的是二进制的机器指令集合,由操作码和操作数构成的。汇编语言的主体是汇编指令。汇编指令和机器指令的差别在于指令的表示方法上,汇编指令是机器指令便于记忆的书写格式。 (图4.4.3)反汇编代码 (图4.4.2)汇编代码 分支转移:反汇编代码跳转指令的操作数使用的不是段名称如.L0之类,段名称是汇编语言中便于编写的助记符,但是在反汇编中就要是具体的地址。 函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。因为 hello.c 中调用的函数需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言时,将其call指令后的相对地址设置为全0,然 后在.rela.text 节中为其添加重定位条目。 全局变量访问:在.s文件中,访问.rodata,使用段名称+%rip,在反汇编代码中 0+%rip。 4.5 本章小结 本章探究了汇编操作将汇编代码转化为机器码的过程。也就是hello.s到可重定位目标文件hello.o的过程,同时也对比了汇编代码与机器码生成的反汇编代码之间的区别。
第5章 链接 5.1 链接的概念与作用 概念:链接是通过链接器(ld)将各种代码和数据片断收集并组合成一个单一可执行文件的过程。这个文件可以被加载(复制)到内存并执行。 作用:
- 符号解析:将每个符号引用与一个确定的符号定义关联起来
- 重定位:将单独的代码节和数据节合并为单个节,将独好从.o文件的相对位置重新定位到可执行文件的最终内存位置。 5.2 在Ubuntu下链接的命令 操作指令:ld /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/5 hello.o -lc -lgcc -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -o hello (见图5.2.1)
(图5.2.1)链接指令及生成文件 5.3 可执行目标文件hello的格式 分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。 操作指令:readelf -a hello > ELF2.elf
上图为节头部表,是该ELF文件所有节构成,第一列为各段的大小,倒数第二列为各段的起始地址。 节头部表中包含了hello中的每一个节和其对应的详细信息。包括: 名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐。 5.4 hello的虚拟地址空间 仔细观察edb显示的hello文件的ELF信息(见图5.4.1)和5.3中的文件ELF信息,打现ELF头的信息是完全相同的,但是再第17项开始的时候,地址发生较大变化,edb中没有这些节的信息,因为这些节是共享库的信息,再edb中想要获得这些信息要单独查看。
(图5.4.1) 5.5 链接的重定位过程分析
(图5.5.1)hello.o
(图5.5.2)hello 举个简单得例子,看连接前的反汇编,其中mov的参数是0,其中的注释只是对main的一个偏移量,显然对hello.o来说,这段代码是不具体的。 但是对于之后的反汇编,很显然我们就能发现,代码的具体注释变成了系统库里的函数,偏移量也是正确的偏移量了。所以链接过程中,链接器会将我们链接的库函数与其他文件在可执行文件中准确定位出来。 所以重定位的具体流程大抵如下:
- 重定位节和符号定义。将所有相同类型的节合并为同一类型的新的聚合节。然后将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输人模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
- 重定位节中的符号引用。修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。 关于重定位的计算方法,这里我们简单给出一个例子: 相对地址重定位: 重定位条目r由四个字段组成: r.offset=0x25 r.symbol=exit r.type=R_X86_64_PC32 r.addend=-4, R_X86_64_PC32重定位算法摘抄书本如下: Refaddr=0x400532+0x25=0x400557 *refptr=(unsigned)(ADDR(r.exit)+r.addend-refaddr) =0x 600ab8+(-0x4)-0x400557 =(unsigned) 0x20055d 决定地址重定位: r.offset=0x16 r.symbol=.rodata r.type=R_X86_64_PC32 r.addend=0, *refptr=(unsigned)(ADDR(r.rodata) =0x400758
5.6 hello的执行流程 hello在执行的过程中一共要执行三个大的过程,分别是载入、执行和退出。载入过程的作用是将程序初始化。 ld-2.27.so! _dl_start 0x7fce 8cc38ea0 ld-2.27.so! _dl_init 0x7fce 8cc47630 hello! _start 0x400500 libc-2.27.so! __libc_start_main 0x7fce 8c867ab0 -libc-2.27.so! __cxa_atexit 0x7fce 8c889430 -libc-2.27.so! __libc_csu_init 0x4005c0 hello! _init 0x400488 libc-2.27.so! _setjmp 0x7fce 8c884c10 -libc-2.27.so! _sigsetjmp 0x7fce 8c884b70 –libc-2.27.so! __sigjmp_save 0x7fce 8c884bd0 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 0x7fce 8cc4e680 -ld-2.27.so! _dl_fixup 0x7fce 8cc46df0 –ld-2.27.so! _dl_lookup_symbol_x 0x7fce 8cc420b0 libc-2.27.so! exit 0x7fce 8c889128 5.7 Hello的动态链接分析 对于动态共享链接库中 PIC 函数,编译器没有办法预测函数的运行时地址,所 以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代 码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表 PLT+全局偏移量 表 GOT 实现函数的动态链接,GOT 中存放函数目标地址,PLT 使用 GOT 中地址 跳转到目标函数。 在 dl_init 调用之前,对于每一条 PIC 函数调用,调用的目标地址都实际指向 PLT 中的代码逻辑,GOT 存放的是 PLT 中函数调用指令的下一条指令地址。 在函数调用时,首先跳转到PLT执行.plt中操作,第一次访问跳转时GOT地址为下一条指令,将函数序号入栈,然后跳转到PLT[0],之后将重定位表地址入栈,访问动态链接器,在动态链接器中使用在栈里保存的函数序号和重定位表计算函数运行时的地址,重写GOT,返回调用函数.之后如果还有对该函数的访问,就不用执行第二次跳转,直接参看GOT信息。
(图5.7.1)init前后变化 5.8 本章小结 本章简述了链接的过程,重点阐述了连接过程中对文件的处理,着重介绍了hello的虚拟地址空间,重定位过程,执行流程,动态链接过程。
第6章 hello进程管理 6.1 进程的概念与作用 概念:进程的经典定义是一个执行中程序的实例。更详细来说是操作系统对一个正在运行的程序中对处理器、主存、I/O设备的抽象表示。 作用:每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个进程的上下文中运行这个可执行目标文件。应用程序也能创建新的进程,并且在这个进程上下文中运行自己的代码和程序。进程提供给应用程序关键的抽象:
- 一个独立的逻辑控制流,提供一个假象,好像我们的程序独占使用处理器。
- 一个私有的地址空间,提供一个假象,好像我们的程序独占使用内存系统。 6.2 简述壳Shell-bash的作用与处理流程 作用:shell是一个交互性的应用级程序,代表用户运行其他程序。Shell执行一系列的读/求职步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解析命令行,并代表用户运行程序。 处理流程:
- 读取来自用户的一个命令行
- 分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
- 检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
- 如果是,立即运行
- 如果不是内部命令,调用fork( )创建新进程/子进程
- 在子进程中用步骤2获取的参数,调用execve( )执行指定程序
- 如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait…)等待作业终止后返回。
- 如果用户要求后台运行(如果命令末尾有&号),则shell返回。 6.3 Hello的fork进程创建过程 系统调用fork函数,创建一个子进程,该子进程拥有和父进程完全一致的代码、数据和资源,利用条件语句if(fork()0)后加子进程内容即可。这个为后面的execve过程。 子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork 时,子进程可以读写父进程中打开的任何文件。 6.4 Hello的execve过程 在之前创建的子进程中通过if(pid0),在其中调用execve加载可执行文件,调用启用代码,启动代码设置栈,将栈和堆初始化为0,代码段与数据段初始化为可执行文件中的内容,最后将PC指向_start的地址,将控制传递给hello程序的主函数。 只有当出现错误时,例如找不到filename, execve 才会返回到调用程序。所以,与fork 一次调用返回两次不同, execve 调用一次并从不返回。 6.5 Hello的进程执行 上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。 时间进程片:执行进程的控制流的一部分的每一时间段。 用户态与核心态: 处理器同非常使一个寄存器提供两种模式的区分,该寄存器描述进程当前的权限,在没有设置模式位的时候,进程处于用户态,此时进程不允许执行特权指令,同时也不允许直接引用地址空间中内核区域的代码和数据;但是一旦设置了模式位,该进程就处于核心态,可以执行指令集中任何指令,也可以访问系统中任何内存位置。 进程调度的含义就是,在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。内存收到中断信号之后,将当前进程加入等待序列,进行上下文切换将当前的进程控制权交给其他进程,当再次收到中断信号时将hello从等待队列加入运行队列。 6.6 hello的异常与信号处理 异常种类:
- 异步异常:中断
- 同步异常:陷阱、故障、终止 中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后执行下一条指令 陷阱是有意的异常,执行一条指令的结果,调用后返回下一条指令 故障是无意的异常,能够被修正。若被修正则返回到当前指令再执行一次,否则终止。 终止是不可修复的故障引起的。 信号是易中通知用户异常发送的机制,例如较为底层的硬件异常以及较高层的软件事件,比如Ctrl-Z和Ctrl-C,分别触发SIGCHLD和SIGINT信号。
(图6.6.1)乱按的结果
(图6.6.2)Ctrl-Z的结果
(图6.6.3)Ctrl-C的结果 显然,在Ctrl-Z之后,进程只是被挂起放到了后台,通过ps指令我们可以看到hello没有结束,接下来还可以继续刚刚挂起的进程。 但是在Ctrl-C之后,进程已经被结束了,已经被直接回收掉了。 6.7本章小结 本章简单阐述了进程的概念和shell的原理以及简单的使用操作,同时介绍了信号和异常的相关信息。
第7章 hello的存储管理 7.1 hello的存储器地址空间 逻辑地址: 又称相对地址,是程序运行由CPU产生的与段相关的偏移地址部分。 描述了程序在运行时的地址。
物理地址: CPU地址总线传来的地址,由硬件电路控制(现在这些硬件是可编程的了)其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、BIOS等)。在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址。
线性地址: 是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制数字表示,值得范围从0x00000000到0xfffffff)程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。
虚拟地址:与线性地址类似,同样是对程序运行区块的相对映射。 Hello运行于物理地址,对于CPU而言, hello的运行地址是逻辑地址,在具体操作的过程中,CPU会将逻辑地址转换成线性地址再变成物理地址。 7.2 Intel逻辑地址到线性地址的变换-段式管理 概念阐述:
- 段寄存器:用于存放段选择符,用来确定对应段的首地址。
- 段选择符:分为三个部分,索引,TI(全局描述符or局部描述符),RPL(CPU特权级)
(图7.2.1)段选择符 知识背景: 1、逻辑地址=段选择符+偏移量 2、每个段选择符大小为16位,段描述符为8字节(注意单位). 3、GDT为全局描述符表,LDT为局部描述符表. 4、段描述符存放在描述符表中,也就是GDT或LDT中. 5、段首地址存放在段描述符中. 每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT)。而要想找到某个段的描述符必须通过段选择符才能找到。 操作流程:
- 根据逻辑地址判断出段选择符和段内偏移地址。
- 根据段选择符判断当前转换段属于GDT还是LDT,再根据相应寄存器得到地址和大小。
- 根据段选择符查找出对临汾的段描述服,得到基地址
- 线性地址=基地址+偏移地址 7.3 Hello的线性地址到物理地址的变换-页式管理 概念阐述:
- 页表条目(PTE):页表将虚拟页映射到物理页,其每一项称为页表条目,由有效位和一个n位的地址字段组成。
- 内存管理单元(MMU):CPU芯片上有一个专门的硬件,功能是动态的将虚拟地址翻译成物理地址。 操作流程:
- 处理器生成一个虚拟地址,并把它传送给MMU
- MMU生成PTE地址,并从高速缓存/主存请求得到它
- 高速缓存/主存向MMU返回PTE
- MMU构造物理地址,并把它传送给高速缓存/主存
- 高速缓存/主存返回所请求的数据字给处理器. 7.4 TLB与四级页表支持下的VA到PA的变换 概念阐述:
- TLB:PTE的缓存。
- 多级页表:将完整的页表分组,分别对应到低一级的一节页表的一个PTE中,在执行程序的过程中,如果我们用到了一个特定的页表,那么我们就在一级页表后面动态的开出来。 操作流程:
- CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT(前32位+TLBI(后4位)向TLB中匹配
- 如果命中,则得到 PPN (40bit)与VPO(12bit)组合成 PA(52bit)
- 如果没有命中,MMU 向页表中查询,CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到 PPN,与VPO组合成PA,并且向TLB 中添加条目。 7.5 三级Cache支持下的物理内存访问 先在TLB中找,若不命中,则在页表中找到PTE,构造出物理地址PA,然后去L1中利用物理地址分为CT,CI,CO,若没命中,依次向低级访存,最后返回结果。 具体流程: 获得了物理地址VA之后,使用CI(倒数7-12位)进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中,根据数据偏移量CO(后6位)取出数据返回。 如果没有匹配成功或者匹配成功但是标志位是1,则不命中,向下一级缓存中查询数据(L2 Cache->L3 Cache->主存),查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU(Least frequently used)进行替换。也就是替换掉最不经常访问的一次数据 7.6 hello进程fork时的内存映射 概念阐述:
- mm_struct(内存描述符):描述了一个进程的整个虚拟内存空间。
- vm_area_struct(区域结构描述符):描述了进程的虚拟内存空间的一个区间。 操作流程:
- 创建当前进程的mm_struct,vm_area_struct和页表的原样副本。
- 两个进程的每个页面都标记为只读页面。
- 两个进程的每个vm_area_struct都标记为私有,只能在写入时复制。 7.7 hello进程execve时的内存映射 操作流程:
- 删除已存在用户区域
- 映射私有区域
- 映射共享区域
- 程序计数器PC指向代码入口点 7.8 缺页故障与缺页中断处理 页表相当于磁盘缓存,所以会存在缓存不命中现象。 对于一个访问内存的指令的现象,如果发生了缺页故障,就会调用异常处理程序,从磁盘中寻找需要访问的页存,选择牺牲页,将PTE信息更新。 然后故障修正成功,返回当前程序,再次执行,就不会发生缺页故障了。 7.9动态存储分配管理 动态内存分配器维护着一个进程的虚拟内存区域-堆,分配器将堆视为一组不同大小的块来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。用户调用相应的申请和释放函数,动态内存分配器就会改变相应的块来完成要求,或检查相应的块,或遍历寻找空闲块。 隐式空闲链表分配器: 隐式空闲链表分配器的实现涉及到特殊的数据结构。其所使用的堆块是由一个子的头部、有效载荷,以及可能的一些额外的填充组成的。头部含有块的大小以及是否分配的信息。有效载荷用来存储数据,而填充块则是用来对付外部碎片以及对齐要求。 基于这样的基本单元,便可以组成隐式空闲链表。 隐式空闲链表: 一个块是由一个字的头部,有效载荷,以及可能的一些额外的填充组成的。头部编码了这个块的大小,以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是零。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息.在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。 7.10本章小结 本章主要介绍了hello的存储地址空间、段式管理、页式管理、物理内存访问,还介绍了 hello 进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
第8章 hello的IO管理 8.1 Linux的IO设备管理方法 设备的模型化:文件 所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对响应文件的读和写来进行。 设备管理:unix io接口 使得所有的输入和输出都能以一种统一且一致的方式来进行,包括打开文件,改变当前的文件位置,读写文件和关闭文件等操作。 8.2 简述Unix IO接口及其函数
- 打开或创建文件: 一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息,应用程序只需记住这个描述符。 int open(char *filename, int flags, int perms); int creat(char *name, int perms);
- 关闭文件与删除文件: 当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。 int close(int fd); int unlink(char *filename);
- 读取文件与写入文件: 一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k ,直到k+n。 int read(int fd, char *buf, int n ); int write(int fd, char *buf, int n);
- 游标移动 对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K。 int lseek(int fd, long offset, int origion); 8.3 printf的实现分析
(图8.3.1)printf函数的实现 要实现printf函数,简单来说就是接受fmt格式,将匹配到的参数按照格式输出即可。
(图8.3.2)vsprintf的实现 vsprintf生成格式化之后的字符串,返回字串的长度。 从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等. 字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。 显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。 8.4 getchar的实现分析
(图8.4.1)getchar的函数实现 getchar调用一个read函数,将缓冲区内容读到c中,返回值函数为1的时候返回c,否则返回EOF。 异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。 getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。 8.5本章小结 本章讲述了I/O设备管理机制,简单阐述了开、关、读、写、转移文件的接口及相关函数,简单分析了printf和getchar函数的实现方法。
结论 Hello程序的一生就这么结束了,但这份报告显然还没有走到它的结尾,下面就由这份结论带您回顾一下hello的一生吧。
- 首先我们通过文本编辑器或者高级的集成开发环境IDE使用高级语言C语言编写了hello.c文件。
- 然后通过预处理将hello.c文件经过初步修改,头文件、宏定义的展开变为更容易被编译器理解的hello.i文件
- 接着编译器将hello.i文件变为了更为基础的语言:汇编语言,并将代码保存在了hello.s文件中
- 随后,汇编器将hello.s中的汇编语言处理成了能够被机器理解的机器语言,生成了可重定位的目标文件hello.o
- 链接器将hello.o与外部文件(库)进行链接,这时候的hello.o终于变为了一个可执行文件hello
- 随后在shell中输入./hello 1190201919 张迈,内核分配好需要的空间
- 同时此时的我们还能在外部给相应信号对进程进行操作,比如Ctrl-Z。
- 在hello需要访问磁盘信息的时候,CPU通过MMU帮助hello寻址
- 最终,hello进程执行结束之后,shell对它进行回收
- hello就这么结束了它的一生 这次大作业帮助我更加全面系统地了解了计算机系统这门课程,同时帮助我完成了对书本更详细翻阅工作,在考前替我梳理了计算机系统的相关知识。让我感受到了计算机底层系统的复杂性和严密性,同时也告诉我如何写一个对计算机底层系统友好的代码。在我成为一个优秀程序的路上,这门课是一个重要的里程碑。
附件 文件名称 文件作用 hello.c 源代码 hello.i 预处理之后的文件 hello.s 编译后的汇编文件 hello.o 汇编之后的目标文件 hello 链接之后的可执行文件 objdump.txt 对hello.o的反汇编文件 Objdump2.txt 对hello的反汇编文件 ELF.elf hello.o的elf文件 ELF2.elf hello的elf文件
参考文献 [1] Bryant,R.E. 深入理解计算机系统 [2] Printf函数的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html [3] EDB的安装与使用 https://blog.csdn.net/Franklins_Fan/article/details/103643965 [4] 逻辑地址到线性地址的转换 https://blog.csdn.net/xuwq2015/article/details/48572421