计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 1190201103
班 级 1903006
学 生 王志贤
指 导 教 师 史先俊
计算机科学与技术学院
2021年5月
摘 要
本文主要阐述hello程序在Linux系统的生命周期,探讨hello程序从hello.c经过预处理、编译、汇编、链接生成可执行文件并运行以及如何对hello进行进程管理、存储管理和I/O管理,深入理解虚拟内存、异常信号等相关内容,通过对hello一生周期的探索,更全面的了解计算机系统。
关键词:预处理;编译;汇编;链接;进程管理;虚拟内存;异常信号;shell;I/O
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述............................................................................................................. - 4 -
1.1 Hello简介...................................................................................................... - 4 -
1.2 环境与工具..................................................................................................... - 4 -
1.3 中间结果......................................................................................................... - 4 -
1.4 本章小结......................................................................................................... - 5 -
第2章 预处理......................................................................................................... - 6 -
2.1 预处理的概念与作用..................................................................................... - 6 -
2.2在Ubuntu下预处理的命令.......................................................................... - 6 -
2.3 Hello的预处理结果解析.............................................................................. - 8 -
2.4 本章小结......................................................................................................... - 8 -
第3章 编译............................................................................................................. - 9 -
3.1 编译的概念与作用......................................................................................... - 9 -
3.2 在Ubuntu下编译的命令............................................................................. - 9 -
3.3 Hello的编译结果解析................................................................................ - 10 -
3.4 本章小结....................................................................................................... - 20 -
第4章 汇编........................................................................................................... - 21 -
4.1 汇编的概念与作用....................................................................................... - 21 -
4.2 在Ubuntu下汇编的命令........................................................................... - 21 -
4.3 可重定位目标elf格式............................................................................... - 21 -
4.4 Hello.o的结果解析.................................................................................... - 25 -
4.5 本章小结....................................................................................................... - 26 -
第5章 链接........................................................................................................... - 28 -
5.1 链接的概念与作用....................................................................................... - 28 -
5.2 在Ubuntu下链接的命令........................................................................... - 28 -
5.3 可执行目标文件hello的格式.................................................................. - 28 -
5.4 hello的虚拟地址空间................................................................................ - 33 -
5.5 链接的重定位过程分析............................................................................... - 33 -
5.6 hello的执行流程........................................................................................ - 37 -
5.7 Hello的动态链接分析................................................................................ - 38 -
5.8 本章小结....................................................................................................... - 40 -
第6章 hello进程管理................................................................................... - 41 -
6.1 进程的概念与作用....................................................................................... - 41 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 41 -
6.3 Hello的fork进程创建过程..................................................................... - 41 -
6.4 Hello的execve过程................................................................................. - 41 -
6.5 Hello的进程执行........................................................................................ - 42 -
6.6 hello的异常与信号处理............................................................................ - 43 -
6.7本章小结....................................................................................................... - 46 -
第7章 hello的存储管理............................................................................... - 47 -
7.1 hello的存储器地址空间............................................................................ - 47 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................ - 47 -
7.3 Hello的线性地址到物理地址的变换-页式管理...................................... - 47 -
7.4 TLB与四级页表支持下的VA到PA的变换............................................. - 48 -
7.5 三级Cache支持下的物理内存访问.......................................................... - 49 -
7.6 hello进程fork时的内存映射.................................................................. - 50 -
7.7 hello进程execve时的内存映射.............................................................. - 50 -
7.8 缺页故障与缺页中断处理........................................................................... - 51 -
7.9动态存储分配管理....................................................................................... - 52 -
7.10本章小结..................................................................................................... - 55 -
第8章 hello的IO管理................................................................................. - 56 -
8.1 Linux的IO设备管理方法.......................................................................... - 56 -
8.2 简述Unix IO接口及其函数....................................................................... - 56 -
8.3 printf的实现分析........................................................................................ - 57 -
8.4 getchar的实现分析.................................................................................... - 58 -
8.5本章小结....................................................................................................... - 59 -
结论......................................................................................................................... - 59 -
附件......................................................................................................................... - 61 -
参考文献................................................................................................................. - 62 -
P2P:From Program to Process
即从程序到进程.
用高级语言编写得到hello.c经过预处理, 编译, 汇编, 链接得到可执行文件hello, 如下所示.
图1.1.1
在shell中键入./hello\n后, shell程序就知道我们已经结束了命令的输入, 然后shell fork一个与它具有相同上下文的子进程, 在子进程中通过调用execve函数从主存中加载可执行的hello文件, 将hello目标文件中的代码和数据从磁盘复制到主存. 一旦目标文件hello中的代码和数据被加载到主存, 处理器就开始执行hello程序.
020:From Zero-0 to Zero-0
即从0到0.
一开始主存中并没有hello的的代码和数据, 即0. 在调用execve函数后在主存中寻找对应的页表, 因为是第一次调用所以调用缺页程序, 将hello装入主存, 开始执行hello.
在执行完hello的所有代码后, 父进程回收hello进程, 内核删除相关数据结构, hello从主存中消失, 即0.
(1)硬件环境:Intel(R) Core(TM) i5-9300H CPU; 2.40GHz; 16G RAM; 512G SSD
(2)软件环境:Windows10 64位; VMware Workstation 16; Ubuntu 20.04 LTS 64位
(3)使用工具:Visual Studio Code; gcc; objdump; gdb; hexedit
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
hello:链接之后的可执行文件
hello_elf:hello.o的ELF格式
hello_elf_:hello的ELF格式
hello_disassemble.txt:hello.o反汇编代码
hello_disassemble_.txt:hello的反汇编代码
本章主要介绍了hello程序P2P、020的含义并列出实验的相关开发环境.
(第1章0.5分)
概念:
在源代码编译之前处理源文件中以“#”开头的预编译指令. 经过预编译处理后得到的预处理文件(hello.i)是一个可读的文本文件, 但不包含任何宏定义.
作用:
– 处理所有条件预编译指令,如“#if”,“#ifdef”, “#endif”等
– 插入头文件到“#include”处,可以递归方式进行处理
– 删除所有的注释“//”和“/* */”
– 添加行号和文件名标识,以便编译时编译器产生调试用的行号信息
– 保留所有#pragma编译指令(编译器需要用)
命令: gcc -E hello.c -o hello.i
图2.2.1
运行后出现一个hello.i文件.
图2.2.2
hello.i main函数部分截图如下.
图2.2.3
hello.c如下.
图2.2.4
对比可以发现#include
介绍了预处理的概念和作用, 说明了ubuntu下预处理的命令并分析了产生的hello.i文件.
(第2章0.5分)
概念:
指编译程序从预处理文本文件产生汇编程序的过程. 经过编译后得到的汇编代码文件(hello.s)是可读的文本文件.
作用:
编译过程就是将预处理后得到的预处理文件(hello.i)进行词法分析、语法分析、语义分析、优化后, 生成汇编代码文件.
命令: gcc -S hello.i -o hello.s
图3.2.1
运行后出现一个hello.s文件.
图3.2.2
hello.s部分如下.
图3.2.3
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的. 应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析.
3.3.1 数据
常量:有两个常量,作为printf()函数的参数,分别是"用法: Hello 学号 姓名 秒数!\n"和"Hello %s %s\n",在hello.s中存储在.LC0和.LC1中。
图3.3.1.1
图3.3.1.2
变量:有三个变量,作为main函数参数的int argc,char *argv[]和局部变量int i,其中argc作为第一个参数存储在寄存器%edi中,argv[]作为第二个参数存储在%rsi中。分析hello.s逻辑可知i存储在-4(%rbp)中。
图3.3.1.3
图3.3.1.4
类型:有int型的argc和i,char*型的argv[]。
3.3.2 赋值
用i = 0语句给i赋值,在hello.s中表现为将0移动到存储i的值的寄存器。
图3.3.2.1
图3.3.2.2
3.3.3 类型转换
通过调用atoi(argv[3])将char*型的字符串转为int型。因为通过调用函数实现所以hello.s中用call处理。
图3.3.3.1
图3.3.4.2
3.3.5 关系操作
在if语句中有argc!=4和i<8两个关系操作。分别通过cmpl比较要比较的两个操作数所在寄存器的关系。
图3.3.5.1
图3.3.5.2
3.3.6 数组/指针/结构操作
char *argv[]为一个字符串数组,argv为指向这个数组的指针。在源代码中通过argv[1]等来获得数组中的值,而在hello.s中通过把argv的些值放入栈帧底,引用栈帧底加偏移量为地址在内存中获取对应的值。
图3.3.6.1
图3.3.6.2
3.3.7 控制转移
有if语句和for语句控制程序的执行流程。在hello.s中在通过cmpl与je和jle一起控制,若相应的符号寄存器满足跳转要求就执行跳转指令。
图3.3.7.1
图3.3.7.2
3.3.8 函数操作
调用了printf函数,exit函数,sleep函数,atoi函数和gerchar函数。hello.s通过call指令跳转至函数首地址,并将返回地址(即call指令下一条指令的地址)压栈,若调用的函数有参数则将参数按顺序依次放入%rdi,%rsi,%rdx,%rcx寄存器中。函数返回时通过ret指令将栈顶地址弹出至%rip。若函数内有return语句则将return的值放入%rax寄存器中。
图3.3.8.1
图3.3.8.2
图3.3.8.3
图3.3.8.4
概括了编译的概念和作用,重点分析了c程序的数据与操作翻译成汇编语言时的表示和处理方法。
(第3章2分)
概念:
汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程. 汇编结果是一个可重定位目标文件(hello.o), 其中包含的是不可读的二进制代码, 必须用相应的工具软件(如readelf)来查看其内容.
作用:
用来将汇编语言源程序转换为机器指令序列(机器语言程序), 使其在链接后能被机器识别并执行.
命令: gcc -c hello.s -o hello.o
图4.2.1
运行后出现一个hello.o文件.
图4.2.2
用readelf -a hello.o > hello_elf.txt命令得到hello.o各节的基本信息.
图4.3.1
分析hello.o的ELF格式:
可重定位目标ELF格式图示如下.
图4.3.2
包括16字节标识信息、文件类型(.o,exec, .so)、机器类型(如IA-32)、节头表的偏移、节头表的表项大小以及表项个数
图4.3.3
编译后的代码部分
只读数据,如printf 格式串、switch跳转表等
已初始化的全局变量
未初始化全局变量,仅是占位符,不占据任何实际磁盘空间。区分初始化和非初始化是为了空间效率
存放函数和全局变量(符号表)信息,它不包括局部变量
图4.3.4
.text节的重定位信息,用于重新修改代码段的指令中的地址信息
图4.3.5
.data节的重定位信息,用于对被模块使用或定义的全局变量进行重定位的信息
调试用符号表(gcc -g)
包含symtab和debug节中符号及节名
每个节的节名、偏移和大小
图4.3.6
用objdump -d -r hello.o > hello_disassemble.txt反汇编hello.o, 与hello.s比较如下.
图4.4.1
比较后发现以下差别:
(1)分支转移:在汇编代码中,分支跳转是直接以.L2等助记符表示,但在反汇编代码中,分支转移表示为主函数+段内偏移量。反汇编代码跳转指令的操作数使用的不是段名称,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后不存在,而是确定的地址。
(2)函数调用:汇编代码中函数调用时直接个函数名称,而在反汇编的文件中call之后加main+偏移量(定位到call的下一条指令),即用具体的地址表示。在.rela.text节中为其添加重定位条目等待链接。
(3)访问全局变量:汇编代码中使用.LC0(%rip),反汇编代码中为0x0(%rip),因为访问时需要重定位,所以初始化为0并添加重定位条目。
概括了汇编的概念和作用,分析了ELF文件的内容,另外比较了重定位前汇编程序和重定位后反汇编的差别,了解从汇编语言翻译成机器语言的转换处理和机器语言和汇编语言的映射关系。
(第4章1分)
概念:
链接(linking)是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:
将多个可重定位目标文件合并以生成可执行目标文件
命令:
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
运行后出现一个hello文件.
图5.2.2
输入命令: readelf -a hello > hello_elf_.txt
图5.3.1
可执行目标ELF格式图示如下.
图5.3.2
ELF头中字段e_entry给出执行程序时第一条指令的地址,而在可重定位文件中,此字段为0
图5.3.3
比可重定位目标文件多一个程序头表,也称段头表(segment header table),是一个结构数组。详细信息如下。
图5.3.4
PHDR:保存程序头表
INTERP:动态链接器的路径
LOAD:可加载的程序段
DYNAMIN:保存了由动态链接器使用的信息
NOTE保存辅助信息
GNU_STACK:标志栈是否可执行
GNU_RELRO:指定重定位后需被设置成只读的内存区域
比可重定位目标文件多一个.init节,用于定义_init函数,该函数用来进行可执行目标文件开始执行时的初始化工作
链接后比可重定位目标文件多了很多符号
图5.3.5
图5.3.6
使用edb加载hello,查看本进程的虚拟地址空间各段信息如下.
图5.4.1
在0x400000~0x401000段中,程序被载入,自虚拟地址0x400000开始,到0x400fff结束. 可以看到与通过readelf查看ELF头信息相同.
图5.4.2
输入命令: objdump -d -r hello > hello_disassemble_.txt获得hello的反汇编代码.
通过分析hello_disassemble.txt(hello.o的反汇编代码)与hello_disassemble_.txt(hello的反汇编代码)以发现以下不同:
图5.5.1
图5.5.2
图5.5.3
这些节都具有一定的功能和含义,如图所示。
图5.5.4
hello重定位的过程:
(1) 重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。
(2) 重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。
(3) 重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目。代码的重定位条目放在.rel.txt,如图所示。
图5.5.5
重定位过程的地址计算算法如图所示.
图5.5.6
使用edb执行hello,说明从加载hello到_start,到call main, 以及程序终止的所有过程. 请列出其调用与跳转的各个子程序名或程序地址.
(1) 载入:_dl_start、_dl_init
(2) 开始执行:_start、_libc_start_main
(3) 执行main:_main、_printf、_exit、_sleep、
_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x
(4) 退出:exit
程序名称 |
地址 |
ld-2.27.so!_dl_start |
0x85a93aea0 |
ld-2.27.so!_dl_init |
0x8cc47630 |
hello!_start |
0x400582 |
lib-2.27.so!__libc_start_main |
0x8c81d58ab0 |
lib-2.27.so!__cxa_atexit |
0x8c889430 |
lib-2.27.so!__libc_csu_init |
0x4005c0 |
lib-2.27.so!_setjmp |
0x8c884c10 |
lib-2.27.so!exit |
0x8c889128 |
hello!puts@plt |
0x4004f0 |
hello!exit@plt |
0x400530 |
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化. 要截图标识说明.
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
根据hello ELF文件可知,GOT起始表位置为0x404000,如图。
图5.7.1
可以看到GOT表在调用dl_init之前0x404000后的16个字节均为0:
图5.7.2
调用_start之后发生改变,0x404000后的两个8个字节分别变为:0x7fc4046bc1190、0x7f4046baabb0,其中GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,改变后的GOT表如下:
图5.7.3
GOT[2]对应部分是共享库模块的入口点,如图。
图5.7.4
介绍了链接的概念与作用,详细阐述了hello.o是怎么链接成为一个可执行目标文件的过程,详细介绍了hello.o的ELF格式和各个节的含义,并且分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
(第5章1分)
概念:
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
作用:
进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)从终端读入输入的命令。
(2)将输入字符串切分获得所有的参数
(3)如果是内置命令则立即执行
(4)否则调用相应的程序为其分配子进程并运行
(5)shell应该接受键盘输入信号,并对这些信号进行相应处理
在终端中输入命令行./hello 1190201103 王志贤 1后,shell会处理该命令,判断出不是内置命令,调用fork函数创建一个新的子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。
execve的功能是在当前进程的上下文中加载并运行一个新程序。在执行fork得到的子进程后随即使用解析后的命令行参数调用execve,execve调用启动加载器来执行hello程序。加载并运行需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
(2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
(3)映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
①逻辑控制流::一系列程序计数器 PC 的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
②并发流:一个逻辑流的执行时间与另一个流重叠,成为并发流,这两个流成为并发的运行。多个流并发的执行的一般现象成为并发。
③时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
④私有地址空间:进程为每个流都提供一种假象,好像它是独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写的,在这个意义上,这个地址空间是私有的。
⑤用户模式和内核模式::处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的 代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
⑥上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
⑦上下文切换:当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:
1)保存以前进程的上下文
2)恢复新恢复进程被保存的上下文,
3)将控制传递给这个新恢复的进程 ,来完成上下文切换。
接着来看hello进程执行,再进程调用execve函数之后,最初hello运行在用户模式下,输出hello 1190201103 王志贤,然后hello调用sleep函数之后进程陷入内核模式,内核不会选择什么都不做等待sleep函数调用结束,而是处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。如图所示。
图6.5.1
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的.
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理.
hello程序出现的异常可能有:
中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。
陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
故障:在执行hello程序的时候,可能会发生缺页故障。
终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。
图6.6.1
常见信号种类如图所示,红框标识出hello执行过程中会产生的信号。
图6.6.2
程序运行过程中按键盘的结果:
图6.6.3
图6.6.4
图6.6.5
图6.6.6
图6.6.7
图6.6.8
图6.6.9
图6.6.10
概括了进程的概念和作用,介绍了shell的一般处理流程和作用,着重分析了调用fork创建新进程,调用execve函数执行 hello,hello的进程执行,以及hello 的异常与信号处理。
(第6章1分)
(1)逻辑地址:格式为“段地址:偏移地址”,是CPU生成的地址,在内部和编程使用,并不唯一。
(2)物理地址:加载到内存地址寄存器中的地址,内存单元的真正地址。CPU通过地址总线的寻址,找到真实的物理内存对应地址。在前端总线上传输的内存地址都是物理内存地址。
(3)虚拟地址:保护模式下程序访问存储器所用的逻辑地址。
(4)线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址。
段式内存管理方式就是直接将逻辑地址转换成物理地址,也就是CPU不支持分页机制。其地址的基本组成方式是段号+段内偏移地址。
在x86保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符占8个字节,段信息无法直接存放在段寄存器中(段寄存器只有2字节)。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值(index)。
首先给定一个完整的逻辑地址[段选择符:段内偏移地址],
1.看段选择描述符中的T1字段是0还是1,可以知道当前要转换的是GDT中的段,还是LDT中的段,再根据指定的相应的寄存器,得到其地址和大小,我们就有了一个数组了。
2.拿出段选择符中的前13位,可以在这个数组中查找到对应的段描述符,这样就有了Base,即基地址就知道了。
3.把基地址Base+Offset,就是要转换的下一个阶段的地址。
分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间(为简化分析,我们不考虑扩展分页的情况)。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table)。
为了节约页表占用的内存空间,x86将线性地址通过页目录表和页表两级查找转换成物理地址。32位的线性地址被分成3个部分:最高10位 Directory 页目录表偏移量,中间10位 Table是页表偏移量,最低12位Offset是物理页内的字节偏移量。页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位),项目里存储的内容就是页表的物理地址。如果页目录表中的页表尚未分配,则物理地址填0。页表的大小也是4k,同样包含1024项,每个项4字节,内容为最终物理页的物理内存起始地址。
每个活动的任务,必须要先分配给它一个页目录表,并把页目录表的物理地址存入cr3寄存器。页表可以提前分配好,也可以在用到的时候再分配。
CPU产生VA,VA传送给MMU,MMU使用VPN高位作为TLBT和TLBI向TLB中寻找匹配。如果命中,则得到PA。如果TLB中没有命中,MMU查询页表,CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,以此类推,最终在第四级页表中找到PPN,与VPO组合成PA,添加到PLT。
图7.4.1
只讨论Cache1的物理内存访问,Cashe2,Cashe3原理相同。
由于L1Cache有64组,所以组索引位s为6,每组有8个高速缓存行,由于每个块的大小为64B,所以块偏移为6,因此标记位为52-6-6=40位。
因此L1 Cache的物理访存大致过程如下:
(1) 组选择取出虚拟地址的组索引位,将二进制组索引转化为一个无符号整数,找到相应的组。
(2) 行匹配把虚拟地址的标记为拿去和相应的组中所有行的标记位进行比较,当虚拟地址的标记位和高速缓存行的标记位匹配时,而且高速缓存行的有效位是1,则高速缓存命中。
(3) 字选择一旦高速缓存命中,我们就知道我们要找的字节在这个块的某个地方。因此块偏移位提供了第一个字节的偏移。把这个字节的内容取出返回给CPU即可。
(4)不命中如果高速缓存不命中,那么需要从存储层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位所指示的组中的一个高速缓存行中。一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。如图所示。
图7.5.1
执行新进程(hello)时,为这个新进程创建虚拟内存。
在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存, 随后的写操作通过写时复制机制创建新页面。
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
(2)映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
(3)映射共享区域,hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
(4)设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
图7.7.1
如果程序执行过程中遇到了缺页故障,则内核调用缺页处理程序。处理程序会进行如下步骤:检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。然后检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,程序终止。在两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。
图7.8.1
动态内存分配器维护者一个进程的虚拟内存区域,成为堆。分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都是要求显示的释放分配块。
(1) 显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
(2) 隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
对于显式分配器,介绍如下两种类型。
隐式空闲链表区别块的边界、已分配块和空闲块的方法如图所示.
图7.9.1
这种情况下,一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。
头部后面是应用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。块的格式如图所示,空闲块通过头部块的大小字段隐含的连接着,所以我们称这种结构就隐式空闲链表。
图7.9.2
当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是首次适配、下一次适配和最佳适配。
一旦分配器找到一个匹配的空闲块,就必须做一个另策决定,那就是分配这个块多少空间。分配器通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。如图所示。
图7.9.3
3. 获取额外堆内存
如果分配器不能为请求块找到空闲块,一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。
合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于四种情况分别进行空闲块合并,我们只需要通过改变头部的信息就能完成合并空闲块。Knuth提出了一种采用边界标记的技术快速完成空闲块的合并。如图所示
图7.9.4
(2) 显式空间链表的基本原理
显示空闲链表是将空闲块组织为某种形式的显示数据结构。如图所示.
图7.9.5
堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
一种方法使用后进先出的顺序维护链表,将新释放的块在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。
按照地址顺序来维护链表,其中链 表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比 LIFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
简述了在计算机中的虚拟内存管理,虚拟地址、物理地址、线性地址、逻辑地址的区别以及它们之间的变换模式,阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)
设备的模型化:所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:Linux内核有一个简单、低级的接口,成为Unix I/O,使得所有的输入和输出都能以一种统一且一致的方式来执行。
Unix I/O接口统一操作:
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
(2) Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
(4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O函数:
(1) int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。
(2)int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
(3)ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
(4)ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
分析首先查看printf函数的函数体:
图8.3.1
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。
接下来是write函数:
图8.3.2
在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。
查看syscall函数体:
图8.3.3
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
于是我们的打印字符串就显示在了屏幕上。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
getchar 的源代码为:
图8.4.1
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在 键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。
(第8章1分)
hello的一生:
(一) 编写,将代码键入hello.c
(二) 预处理,将hello.c调用的所有外部的库展开合并到一个hello.i文件中
(三) 编译,将hello.i编译成为汇编文件hello.s
(四) 汇编,将hello.s会变成为可重定位目标文件hello.o
(五) 链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello
(六) 运行:在shell中输入./hello 1183710129 邓昆昆
(七) 创建子进程:shell进程调用fork为其创建子进程
(八) 运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数。
(九) 执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流
(十) 访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
(十一) 动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
(十二) 信号:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起。
(十三) 结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
通过学习ics这门课程,深感计算机这个庞大体系的复杂、精巧,从电路到电路组合,再到硬件集成、软件调配,每一处都井井有条、深感人类的智慧是如此精妙。但这还没有结束,hello一生的每段经历都还有许多可以学习的地方,我们还要继续深入学习。
(结论0分,缺失 -1分,根据内容酌情加分)
hello.c:源代码
hello.i:预处理后的文本文件
hello.s:编译之后的汇编文件
hello.o:汇编之后的可重定位目标执行文件
hello:链接之后的可执行文件
hello_elf:hello.o的ELF格式
hello_elf_:hello的ELF格式
hello_disassemble.txt:hello.o反汇编代码
hello_disassemble_.txt:hello的反汇编代码
(附件0分,缺失 -1分)
[1] 深入理解计算机系统
[2] 计算机系统基础(一):程序的表示、转换与链接https://www.icourse163.org/learn/NJU-1001625001?tid=1463133447#/learn/content
[3] linux2.6 内存管理——逻辑地址转换为线性地址(逻辑地址、线性地址、物理地址、虚拟地址)https://www.cnblogs.com/diaohaiwei/p/5094959.html
[4] printf 函数实现的深入剖析https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)