计算机系统
大作业
题 目 程序人生-Hello’s
P2P
专 业 计算机类
学 号 1180300412
班 级 1803004
学 生 yiguanghui
指 导 教 师
计算机科学与技术学院
2019年12月
摘 要
关键词:计算机系统、编译链接、异常控制流、虚拟内存
摘要:本文较详细地跟踪介绍了hello.c在Linux下的生命周期,从被程序员创建,到在系统上运行,然后输出简单的消息,最后终止。本文通过计算机系统课程中相关的知识来分析hello.c在Linux开发工具下经历预处理、编译、汇编、链接、加载、执行、终止、回收等过程和结果,跟踪程序的链接、进程创建及加载、虚拟内存转换、高速缓存访问、异常控制流、I/O管理等过程。
目 录
第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简介
Hello程序的生命周期是从一个高级C语言程序开始的。在linux系统上,GCC编译器驱动程序读取源程序文件hello.c,通过执行预处理阶段、编译阶段、汇编阶段和链接阶段这四个阶段翻译成一个可执行目标文件hello。
P2P:From Program to
Process:gcc编译器驱动程序读取程序文件hello.c,然后由预处理器(cpp)根据以#字符开头的命令,修改该程序获得hello.i文件,通过编译器(cll)将文本文件hello.i翻译成文本文件形式的汇编程序hello.s,再通过汇编器(cs)将hello.s翻译成机器语言指令,将指令打包成可重定位的目标文件hello.o,然后通过链接器(ld)合并获得可执行目标文件hello。Linux系统中通过内置命令行解释器shell加载运行hello程序,为hello程序fork进程,此时,hello.c就完成了P2P的过程。
O2O:From Zero-0 to
Zero -0:shell为子进程通过execve,mmap等一系列指令来加载可执行目标文件hello。这些指令将hello目标文件中的代码和数据从磁盘复制到主存,一旦目标文件hello中的代码和数据被加载到主存,处理器就开始执行hello程序的main程序中的机器语言指令,这些指令将它的消息字符串字节从主存复制到寄存器文件,再从寄存器文件复制到显示设备,最终显示在屏幕上,hello程序在屏幕上输出它的消息,然后终止。hello进程终止后,操作系统恢复shell进程中的上下文,并将控制权传回给它,shell进程等待下一个命令行的输入。hello执行完后shell为其回收子进程。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7 64位以上;VirtualBox/Vmware
11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上
开发调试工具:CodeBlocks
64位;vi/vim/gedit+gcc
1.3 中间结果
hello.c C语言源程序
hello.i hello.c预处理后生成的文本文件
hello.s
hello.i经编译器翻译后生成的文本文件
hello.o hello.s经过汇编器翻译后生成的可重定位目标文件
hello hello.o链接后的可执行文件
hello.elf
hello.o的elf格式文件
1.4 本章小结
本章整体性地概括了hello在计算机系统中的生命过程,介绍了大作业需要使用的软硬件环境和开发工具以及作业过程中产生的文件。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:在编译执之前进行的处理。包括三个方面:宏定义、文件包含、条件编译。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如第六行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
gcc -E
hello.c -o hello.i
-E选项保留预处理器的输出文件,使用-o选项将它导入到hello.i
图1.1预处理hello.c
2.3 Hello的预处理结果解析
图1.2.hello.i的部分代码
hello.c结果预处理之后,最初的23行代码变成了hello.i中的3113行代码,而main()函数处在hello.i中末尾且内容并没有变化,即程序开头的#include<stdio.h>,#include<unistd.h>,#include<stdlib.h>被预处理器读取,并且直接插入到了hello.i中,因此程序变成了三千多行。
2.4 本章小结
本章介绍了预处理器对C程序进行的预处理操作规则和结果文件hello.i的解析,练习了Linux预处理的指令,掌握了预处理的概念与其在hello.c程序处理过程中的作用。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译的概念:用编译程序来产生目标程序的动作,它包含五个阶段:词法分析、语法分析、语义检查和中间代码生成、代码优化、目标代码生成。主要进行词法分析和语法分析,分析过程中若发现有语法错误,则给出提示信息。编译器将文本文件预处理产生的hello.i翻译成文本文件hello.s,其包含一个汇编语言程序。
3.2 在Ubuntu下编译的命令
gcc -S
hello.i -o hello.s
图3.1编译命令
3.3 Hello的编译结果解析
3.3.1:结构信息
图3.2.hello.s的结构信息
.file:源文件名
.text:代码段
.section,
.rodata:只读数据段
.align:对齐方式
.string:字符串常量
.globl main:主函数,可以在其他的模块中被引用的全局函数
.type
main,@function:指明main是函数类型
3.3.2:数据类型
(1)字符串常量:
输出字符串保存在.rodata节中,一共有两个字符串
a.”用法:Hello 学号 姓名 秒数!\n”,该字符串使用UTF-8编码,“用法”两个汉字占开始的六个字节,一个汉字占三字节。
b.”Hello %s
%s\n”,此字符串直接正常保存
图3.3字符串
(2)整型:
a.int i:局部变量i存放在栈中,位于-0x4(%rbp)的空间中,大小为四个字节
b:int argc: main函数的第一个参数,存放在寄存器%edi,之后传入栈中-0x20(%rbp)的位置,大小为四个字节
c.立即数常量如3,8等,直接存放在代码段
(3)数组:
程序中只有一个数组char* argv[],它是main()函数的第二个参数,每个元素大小为8个字节,数组占据了连续的栈空间,可以使用相对寻址利用数组的首地址与每个元素的大小来寻址。argv的首地址为-32(%rbp),argv[1]地址为-24(%rbp),argv[2]位于-16(%rbp).
3.3.3:汇编语言操作:
(1)赋值操作:
Mov S,D,效果:D<—S
指令
描述
movb
传送字节
movw
传送字
movl
传送双字
movq
传送四字
程序中使用下面这条指令来为i赋初值0
图3.4为i赋值为1
(2)算术操作:
在for循环中,每经一次循环,i++,使用立即数直接加到i所在的空间,编译产生的指令为:
图3.5 i+1
(3)类型转换:argv[3]是字符型,使用atoi将其转换为整型变量。
图3.6 将第四个命令行参数转换为整型数
(4)关系操作:
判断argc!=4,使用立即数4与argc比较,由于argc是main()函数的第一个参数,其存放在寄存器%edi中,之后又将%edi存放的值传送给-20(%rbp),故编译生成指令为:
图3.7 判断argc!=4
判断i<8,i是main()函数构造的局部变量,其位置在%rbp-4,所以对于指令为:
图3.8 判断i是否小于等于7
(5)控制转移:
a.if(argc!=4):当argc不等于4时跳转,cmpl语句判断大小关系,如果相等,则跳入.L2中;如果不相等,则继续执行下面的指令。
图3.9 等于4时跳转到.L2
b.for(i=0;i<10;i++):用立即数7与-4(%rbp)的内容进行比较,-4(%rbp)存放的是i,当i小于等于7,即小于8时,跳转到.L4,执行循环体。
图3.10 小于8时跳转
3.3.4:函数操作:
(1)main函数:
参数传递:从内核中获取命令行参数和环境变量地址,传入参数argc和argv。
函数退出:两种方式:当命令行参数不为4时,会调用exit(1),退出。当命令行参数为4时执行循环后getchar,然后return 0,退出。
函数分配内存:main函数通过pushq
%rbp,movq %rsp,%rbp,subq $32,%rsp分配到栈空间。
图3.11 分配栈空间
函数释放内存:如果是exit(1)退出,则函数不会释放内存;如果是return 0退出,则通过leave指令和ret指令恢复栈空间。
(2)printf函数:
函数调用:第一次通过call puts@PLT指令调用,当printf只有字符串作为参数则main函数设置%rdi为格式化字符串地址,通过call函数调用puts函数。第二次通过call printf@PLT指令调用,printf函数有其他参数时,main函数按照寄存器表示参数的顺序为printf构造参数然后通过calll指令调用printf。
函数返回:printf的返回值为打印的字符数,puts函数执行成功会返回非负数,执行失败会返回EOF。
(3)exit():
传入的参数为1,通过movl $1,%edi call exit@PLT执行退出命令
(4)sleep():
图3.12 调用sleep函数
将第四个命令行参数执行atoi功能转字符为整型数,movl
%eax,%edi将其作为sleep函数的参数。通过call sleep@PLT指令调用sleep函数。
函数返回:若进程在参数规定的时间内挂起,则返回0,若有信号中断则返回剩余秒数。
(5)atoi():
图3.13 atoi函数调用
将第四个命令行参数使用相对寻址找到,然后赋值给%rdi,作为atoi()函数的参数,通过call sleep@PLT指令来调用sleep函数。函数返回值为由字符型参数转化来的整型数。
(6)getchar():
main函数通过call getchar@PLT指令调用getchar()函数,且getchar()函数无参数。函数返回:getchar()函数返回值类型为int,如果执行成功则返回用户输入的ASCII码,出错返回-1。
3.4 本章小结
本章介绍了汇编器将一个预处理文件hello.i翻译成hello.s的具体操作,在汇编语言的程序头、数据类型、算术操作、控制转移、函数操作等方面对hello.s进行了解释说明。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:把汇编语言翻译成机器语言的过程
作用:汇编器(as)将hello.s翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,将结果存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的是函数main的指令编码,如果在文本编辑器中打开hello.o文件,会看到一堆乱码。
4.2 在Ubuntu下汇编的命令
gcc -c -o
hello.o hello.s
图4.1 汇编命令
4.3 可重定位目标elf格式
a.ELF头包含信息:以一个16字节序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,剩下的信息帮助链接器语法分析和解释目标文件的信息。包括ELF头大小,目标文件类型,机器类型,节头部表的文件偏移以及节头部表中条目的大小和数量。
图4.2 ELF头信息
b.节头部表记录了各节名称、类型、地址、偏移量、大小、全体大小、旗标、连接、信息、对齐信息。
图3.3 节头表
c.重定位节,描述了.text节中需要重定位的信息,即当链接器链接该目标文件时需要修改的信息。包括main函数调用的puts,exit,printf,sleep,getchar函数以及.rodata节的偏移量、信息、类型、符号值、符号名称和加数。Rela.eh_frame记录了.text节的信息。
图3.4 重定位节
d.符号表:
包含信息:程序中定义和引用函数和全局变量的信息,声明重定位需要引用的符号
图3.5 符号表
e.下面对重定位项目进行分析
重定位条目结构如下:
typedef struct{
long offset;/*需要被修改的引用的字节偏移*/
long type:32,/*重定位类型*/
symbol:32;/*标识被修改引用应该指向的符号*/
long attend;/*符号常数,对修改引用的值做偏移调整*/
}ELF64_Rela;
在这里出现的R
X86_64PC32是重定位一个使用32位PC的相对地址引用;R X86_64_32是重定位一个使用32位PC的绝对地址的引用。
重定位计算方法如下:
if(r.type==R_X86_64_PC32){
refaddr=ADDR(s)+r.offset;
*refptr=(unsigned)(ADDR(r.symbol)+r.addend-refaddr);
}
if(r.type==R_X86_64_32)
*refprt=(unsigned)(ADDR(r.eymbol)+r.addend);
4.4 Hello.o的结果解析
使用objdump -d -r hello.o命令行
分析hello.o的反汇编,与第3章的 hello.s进行对照分析得到以下不同点:
a. 操作数的进制:
hello.s中的操作数是十进制,hello.o的反汇编代码的操作数是十六进制;
b. 分支转移:
hello.s中使用.L2,.L3等段名称助记符来分支跳转,而在hello.o的反汇编代码中分支转移使用已经确定的实际指令地址来进行跳转。
c. 函数调用:
汇编代码中,函数调用时call后直接是函数名@PLT,反汇编中call之后是main+段内偏移量,是函数的相对偏移地址,因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。
d.全局变量的访问
在hello.s中,对于.rodata(printf中的字符串)访问,使用段名称+%rip,在反汇编代码中为0+%rip,因为rodata中数据地址也是在运行时确定,故访问也需要重定位。
图3.6 hello.o的反汇编
4.5 本章小结
本章通过对汇编后产生的hello.o的可重定位的ELF格式的考察、对重定位项目的举例分析以及对反汇编文件与hello.s的对比,更深刻地理解了汇编语言到机器语言实现地转变,和这过程中为链接做出的准备(设置重定位条目等)。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。
作用:当程序调用函数库中的一个函数printf,printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个函数必须通过链接器(ld)将这个文件合并到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.1 链接命令
5.3 可执行目标文件hello的格式
图5.2 hello的ELF头信息
程序头大小改变,节头的数量也增加了。而且也没有了.rel节,因为可执行目标文件已经完成重定位了。可执行文件hello的ELF头中标明了入口点地址——程序运行执行的第一条指令的地址。
图5.3 节头表
在节头表中对hello中对所有的节信息进行声明,其中包括size大小和在程序中的偏移量offset,因为是已链接的程序,所以根据标出的信息就可以确定程序实际被加载到虚拟地址的地址。
5.4 hello的虚拟地址空间
图5.4 地址空间
edb加载hello 可以从Data Dump中查看虚拟地址空间,程序的虚拟地址空间为0x0000000000400000-0x0000000000401000.
图5.5 程序头信息
查看ELF文件中的PHT程序头,这是在程序被执行的时候告诉链接器运行时需要加载的内容并提供动态链接信息。每个表项提供了各段在虚拟地址空间中的大小,位置,访问权限和对齐方式。
a.
PHDR部分:具有读权限,表示自身所在的程序头表在内存中的位置在0x400000偏移0x40字节处,大小为0x1c0字节
b.
INTERP部分:具有读权限,起始位置:起始位置:0x400000偏移0x200字节,大小为0x01c字节,记录了动态链接器的位置位于/lib64/ld-linux-x86-64.so.2。
c.
LOAD部分:代码段,有读和执行访问权限,起始位置:0x400000,大小为0x7cc字节,其中其中包括ELF头、程序头部表以及.init、.text、.rodata字节。
d.
LOAD部分:数据段,有读和执行访问权限,起始位置:0x600e50,大小为0x1fc字节。
e.
NOTE部分:具有读权限,起始位置:0x40021c,大小为0x20字节。该段是以‘\0’结尾的字符串,包含一些附加信息。
5.5 链接的重定位过程分析
使用objdump -d -r hello命令行进行反汇编,对比hello.o的反汇编代码得到如下不同点:
(1) 文件内容:
hello反汇编文件中包含 .init ,.plt, .plt.got, .text, .fini,而且每个节中有许多函数,这些外部函数都是我们在使用链接命令是链接系统目标文件中的。而hello.o反汇编文件只有.text节,且只有一个main函数,函数地址也是默认的0x000000。
.init:程序初始化代码; .plt:位于代码段中,为动态链接中的过程链接表;.plt节,即过程链接表,配合GOT(全局偏移量表)使用,能够通过延迟绑定机制实现对共享库中函数动态的重定位。
.plt.got:动态链接中过程链接表PLT和全局偏移量表GOT的间接跳转;
.fini:程序结束执行代码。
图5.6 hello.o反汇编
表中确实存放的是main函数中调用的一些共享库中的函数。尤其是.plt,即PLT[0],其中的命令是对GOT[0],GOT[1]的操作,即将动态链接器地址压栈,跳转到重定位表。函数调用不同:
(2)函数调用:
hello.o中,call地址后为4个0字节的占位符;
hello的生成过程中调用了动态链接共享库,链接器解析重定向条目时,动态链接库中的函数已经加入到了PLT中,链接器计算.text节与.plt节相对距离,且将对动态链接库中的函数调用值改为PLT中相应函数与下一条指令的相对地址。
(3)地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于.rodata中字符串的访问,是$0x0和0(%rip),是因为它们的地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。
5.6 hello的执行流程
ld-2.27.so!_dl_start 0x7faf 9a9df098
ld-2.27.so!_dl_init 0x7f07 6970e630
ld-2.27.so!_dl_lookup_symbol_x 0x7f07 69709340
ld-2.27.so!_dl_vdso_vsym 0x7f07 69474414
ld-2.27.so!_dl_init
0x7f07 6932 e9e6
libc-2.27.so!_libc_start_main 0x7f07 6932eab0
main 0x400582
hello!puts@plt 0x4004f0
ld-2.27.so!_dl_runtime_resolve_xsavec 0x7f29 e52f9750
ld-2.27.so!_dl_fixup 0x7f29 e52f1f64
hello!main 0x4005a3
libc-2.27.so!puts 0x7fdc 3ec299d2
libc-2.27.so!_dl_addr 0x7fdc 3ed0 efec
5.7 Hello的动态链接分析
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
由之前得到的节头表可知,GOT起始位置为0x601000
图5.7 .got.plt信息
调用_dl_init前:0x601008之后的16个字节全部是0
图5.8
调用_dl_init前
调用_dl_init后:0x601008后的两个8字节变为0x7fd6 8a734170
和0x7fd6
8a522750,0x7fd6 8a734170包含动态链接器在解析函数地址时使用的信息,0x7fd6 8a522750是动态链接器在ld-linux.so模块中的入口处。
图5.9 调用_dl_init后
5.8 本章小结
本章介绍了链接的概念和作用,分析了可执行文件hello的ELF格式及其虚拟地址空间,分析了重定位、加载以及运行时函数调用顺序及动态链接过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量、以及打开文件描述符的集合。
作用:进程给应用程序提供的关键抽象有两种:
a)一个独立的逻辑控制流,提供一个假象,程序独占地使用处理器。
b)一个私有的地址空间,提供一个假象,程序在独占地使用系统内存。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,shell是一个交互型应用级程序,代表用户运行其他应用程序(是命令行解释器,以用户态方式运行的终端进程)。
其基本功能是解释并运行用户的指令,重复如下处理过程:
1 终端进程读取用户由键盘输入的命令行
2. 分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
3. 检查第一个命令行参数是否是一个内置的shell命令
4. 如果不是内部命令,调用fork()创建新进程
5. 在子进程中,用步骤2获取的参数,调用execve()执行指定程序
6. 如果命令行末尾没有&号,否则shell使用waitpid等待作业终止后返回
7. 如果用户要求后台运行(命令行末尾有&号),则shell返回。
6.3 Hello的fork进程创建过程
在命令行键入./hello,命令行会对该命令进行解析,发现不是内置命令,则判断为可执行文件,然后终端程序会调用fork()函数创建一个新的子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到和父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码段和数据段、堆,共享库和用户栈。子进程还获得与父进程描述相同的副本,即当父进程调用fork()时,子进程可以读写父进程中打开的任何文件。父进程和子进程的区别在于他们有不同的pid.
6.4 Hello的execve过程
系统为hello fork子进程之后,子进程调用execve函数加载并运行可执行目标文件hello,并且带参数列表argv和环境变量列表envp,并将控制传递给main函数。
图6.1
程序开始时的用户栈
6.5 Hello的进程执行
1.进程上下文信息,就是内核重新启动一个被抢占的程序所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈、和各种内核数据结构。
2.进程时间片,是指一个进程和执行它的控制流的一部分的每一时间段。
-
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
-
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这个决策就叫做调度。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用上文所述的上下文切换的机制将控制转移到新的进程。内核代表的用户执行系统调用时,可能会发生上下文切换;中断也有可能引发上下文切换。
在Hello执行的某些时刻,比如sleep函数,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度。当内核调度了一个新的进程运行后,它就抢占Hello进程,并且使用上下文切换机制来将控制转移到新的进程。Hello进程初始运行在用户模式中,直到Hello进程中的sleep系统调用,它显式地请求让Hello进程休眠,内核可以决定执行上下文切换,进入到内核模式。当定时器到达输入的规定时间后中断时,内核就能判断当前Hello休眠运行了足够长的时间,切换回用户模式。
6.6 hello的异常与信号处理
Hello执行过程中会出现的异常:
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的错误。
-
运行时什么也不按,程序执行完了,进程被回收。
图6.2
正常运行时的输出
-
在程序输出3条之后按下Ctrl-Z,父进程收到SIGSTP信号,信号处理函数输出相应的信息,将hello进程挂起,通过ps指令看出hello进程没有被回收,调用fg 1,将其调到前台运行,此时shell程序首先打印hello命令行,hello继续运行打印剩下的5条信息,之后接收字符串,程序结束,进程被回收。
图6.3
运行时按下Ctrl-Z
-
在程序输出3条信息行后按下Ctrl-C,父进程收到SIGINT信号,根据ps指令,可以看到hello进程已经被回收,信号处理函数结束hello。
图6.4 运行时按下Ctrl-C
4.在程序执行过程中,不停乱按,可以看出在程序执行过程中乱按实际上是将屏幕输入缓存到stdin,当getchar读到\n字符时,其他字符当做命令输入。
图6.5
运行时乱按
6.7本章小结
本章介绍了进程的定义和作用,Shell的处理流程,如何调用fork()创建子进程,如何调用execve函数执行可执行程序以及hello进程执行时的异常处理情况。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址之间的距离。
线性地址:逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。
虚拟地址:CPU执行单元发出的内存地址将被MMU接收,从CPU到MMU的地址称为虚拟地址。
物理地址:CPU通过地址来访问内存中的单元,地址有虚拟地址和物理地址之分,如果CPU没有MMU,或者有MMU但没有启用,CPU核在取指令或访问内存时发出的地址将直接传到CPU芯片的外部地址引脚上,直接被内存芯片接收,这称为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理:逻辑地址->线性地址==虚拟地址
段寄存器(16位),用于存放段选择符
CS(代码段):程序代码所在段;SS(栈段):栈区所在段;DS(数据段):全局静态数据区所在段;其他3个段寄存器ES、GS和FS可指向任意数据段
图7.1
段选择符含义
逻辑地址向线性地址转换的过程中被选中的描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量相加得到线性地址,过程如下图
图7.2 逻辑地址向线性地址转换
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存概念:虚拟内存是系统对主存的抽象概念,是硬件异常、硬件地址翻译、主存、磁盘文件和内存文件的完美交互。为每个进程提供了一个大的、一致的和私有的地址空间。
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传输单元。虚拟页则是虚拟内存被分割为固定大小的块。物理内存被分割为物理页,大小与虚拟页大小相同。
图7.3
虚拟页映射物理页
页表是一个页表条目的数组,将虚拟页地址映射到物理页地址。
图7.4
页表映射虚拟页到物理页
CPU中的一个控制寄存器,页表基址寄存器指向当前页表。n位的虚拟地址包含两个部分,一个p位的虚拟页面偏移和一个(n-p)位的虚拟页号。MMU利用VPN来选择适当的PTE。例如,VPN0选择PTE0,VPN1选择PTE1,,以此类推。将页表条目中物理页号和虚拟地址中的VPO串联起来,就得到了相应的物理地址。物理地址和虚拟页面都是P字节的,故物理页面偏移和VPO是相同的。
图7.5 虚拟地址翻译为物理地址
7.4 TLB与四级页表支持下的VA到PA的变换
以二级页表为例,将VPN分成两部分VPN1,VPN2,分别作为一二级页表的索引。首先根据VPN1在一级页表中找到对应二级页表的基址,然后通过VPN2找到物流页的地址。这里可以看到二级页表是动态存在的,如果一级页表项无效,那么后面二级页表也不会存在,节省了大量内存空间。
图7.6
二级页表
TLB是一个小的,虚拟地址缓存,其中每一行都存放着一个由单个PTE组成的块。TLB通常具有高度的相联度。仍然是利用虚拟地址的VPN项,将VPN分成TLBI和TLBT两部分。如果给定一个有2^t个组的TLB,那么VPN的最低t位即作为TLB的index组索引。剩余为作为tag位,通过它来找到对应的组中的行。
图7.7 虚拟地址的组成部分
下图介绍了Core i7 MMU如何使用四级页表来将虚拟地址翻译成物理地址。36位VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量。最终得到物理页号PPN,与PPO(即VPO)组合后即为物理地址。
图7.8四级页表的地址翻译
7.5 三级Cache支持下的物理内存访问
图7.9
Core i7的内存系统
通过7.4的分析已经得到了物理地址,现在在三级cache支持下进行物理内存访问,以L1
cache为例说明:
L1 Cache是8路64组相联。块大小为64B。因此CO和CI都是6位,CT是40位。根据物理地址(PA),首先使用CI组索引,每组8路,分别匹配标记CT。如果匹配成功且块的有效位是1,则命中,根据块偏移CO返回数据。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中,向下一级缓存中取出被请求的块,然后将新的块存储在组索引指示的组中的一个高速缓存行中。一般而言,如果映射到的组内有空闲块,则直接放置,否则必须驱逐出一个现存的块,一般采用最近最少被使用策略LRU进行替换。
7.6 hello进程fork时的内存映射
内存映射:Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射。虚拟内存可以映射到两种类型的对象。
Linux文件系统中的普通文件:如可执行文件,文件区被分割成页大小的片,若页比文件大,用零填充。按需进行页面调度
匿名文件:由内核创建的一个全二进制零文件。
共享区域和私有区域的内存映射:
若多个进程共用一个对象,并且都将该对象作为共享对象。任意一个进程对该对象的操作对其他进程而言都是可见的。,同时也会反应在磁盘的原始对象中。
若多个进程共用一个对象,并且都将该对象作为私有对象,任意一个进程对该对象的操作对其他进程而言都是不可见的。linux系统通过写时复制机制减少内存开销。
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
exceve函数加载和执行程序Hello,需要以下几个步骤:
1.删除已存在的用户区域。
2.映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。
3.映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。exceve设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
图7.10 加载器映射用户地址空间
7.8 缺页故障与缺页中断处理
缺页异常及其处理过程:如果DRAM缓存不命中称为缺页。当地址翻译硬件从内存中读对应PTE时有效位为0则表明该页未被缓存,触发缺页异常。CPU调用内核中的缺页异常处理程序。
缺页处理程序检查试图访问的内存是否合法,如果不合法则触发保护异常终止此进程。
缺页处理程序确认引起异常的是合法的虚拟地址和操作,则选择一个牺牲页,如果牺牲页中内容被修改,内核会将其先复制回磁盘。牺牲页的页表条目会被内核修改。接下来内核从磁盘复制需要的虚拟页到DRAM中,更新对应的页表条目,重新执行导致缺页的指令。
图7.11
缺页处理
7.9动态存储分配管理
可以用低级的mmap和munmap函数来创建和删除虚拟内存区域。但动态内存分配器可以使用更方便,它维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,有已分配和空闲两种状态。
分配器有两种风格:
显示分配器:要求应用显示释放已分配的块。
隐式分配器::要求分配器检测一个已分配块何时不再使用,那么就释放这个块。
造成堆利用效率低的原因是碎片现象,碎片分为两种:
内部碎片:当一个已分配块比有效载荷的,由对齐要求产生
外部碎片:空闲内存合起来足够满足分配请求,但是处于不连续的内存片中
下面介绍两种合适的数据结构来减少碎片:
隐式空闲链表
图7.12隐式空闲链表
隐式空闲链表空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。
显式空闲链表:将空闲块组织成链表形式的数据结构。堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针,
图7.13
显式空闲链表结构
维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。
三种常见的放置策略:1.首次适配:从头开始搜索空闲链表,选择第一个合适的空闲块;2.下一次适配:从上一次查询结束的地方开始,选择第一个合适的空闲块;3.最佳适配:检查每一个空闲块,选择适合所需请求大小的最小空闲块。
7.10本章小结
本章主要介绍了hello的存储器地址空间、段式管理、页式管理,以及VA到PA的变换、物理内存访问和hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个linux文件就是一个m个字节的序列:
B0 , B1 , …, Bk , … , Bm-1
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix
IO接口及其函数
Unix I/O 接口统一操作:
1) 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2) Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标 准错误。
3) 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位
置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置 k。
4) 读写文件:一个读操作就是从文件复制 n个字节到内存,从当前文件位置 k 开始,然后将 k增加到k+n,给定一个大小为 m 字节的文件,当
k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n个字节到一个文件,从当前文件位置 k开始,然后更新 k。
5) 关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O 函数:
1)进程通过调用 open 函数来打开一个存在的文件或是创建一个新文件的。 open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。
int
open(char *filename, int flage, mode_t mode);
2)fd 是需要关闭的文件的描述符,close 返回操作结果。
int
close(int fd);
3)read 函数从描述符为 fd 的当前文件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。
ssize_t
read(int fd, void *buf, size_t n);
ssize_t
write(int fd, const void *buf, size_t n);
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的定义:
typedef char *va_list
这说明它是一个字符指针。
其中的: (char*)(&fmt) + 4) 表示的是...中的第一个参数。
下面我们来看看下一句:
i = vsprintf(buf, fmt, arg);
让我们来看看vsprintf(buf, fmt, arg)是什么函数。
int vsprintf(char *buf, const char *fmt, va_list
args)
{
char* p;
char tmp[256];
va_list
p_next_arg = args;
for
(p=buf;*fmt;fmt++) {
if (*fmt != ‘%’) {
*p++ =
*fmt;
continue;
}
fmt++;
switch
(*fmt) {
case ‘x’:
itoa(tmp,
((int)p_next_arg));
strcpy(p, tmp);
p_next_arg
+= 4;
p += strlen(tmp);
break;
case ‘s’:
break;
default:
break;
}
}
return (p - buf);
}
Printf接收一个格式化的命令,并把指定的匹配的参数格式化输出。
i =
vsprintf(buf, fmt, arg); vsprintf返回的是要打印出来的字符串的长度
write(buf,
i); write,顾名思义:写操作,把buf中的i个元素的值写到终端。
所以说:vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
让我们追踪下write吧:
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
这里是给几个寄存器传递了几个参数,然后一个int结束
看看sys_call的实现:
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
一个call save,是为了保存中断前进程的状态。ecx中是要打印出的元素个数
;ebx中的是要打印的buf字符数组中的第一个元素 ;这个函数的功能就是不断的打印出字符,直到遇到:’\0’ ;[gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串
于是得到printf的执行过程如下:
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar由宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时,用户输入的字符被存放在键盘缓冲区中。直到用户按回车为止。当用户键入回车之后,getchar开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显示到屏幕。如用户在按回车之前输入了不止一个字符,其他字符则会保留在键盘缓存区中,等待之后getchar读取。后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才等待用户按键。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO
接口及其函数,分析了
printf 函数和 getchar 函数。
(第8章1分)
结论
Hello所经历的一生:
-
编写hello代码,通过IDE将代码键入hello.c
-
预处理,hello.c经过预处理器处理得到文本文件hello.i
-
编译,hello.i编译成汇编文件hello.s
-
汇编,hello.s经汇编器翻译成机器语言指令,打包成为可重定位目标文件hello.o
-
链接,hello.o与可重定位目标文件和动态链接库链接成可执行目标程序hello
-
在shell中输入./hello1180300412 yiguanghui 1运行hello,shell进程调用fork为hello创建子进程
-
运行程序:子进程调用execve,execve调用启动加载器,映射虚拟内存,进入程序入口后程序载入物理内存,进入
main函数。 -
执行指令:CPU为进程分配时间片,执行的控制逻辑流
-
访问内存:MMU将虚拟内存地址通过页表映射成物理地址。
-
动态申请内存:printf调用malloc向动态内存分配器申请堆中的内存。
-
信号:内核通过信号系统来处理程序执行中的用户请求和异常
-
结束:shell父进程回收子进程。
在这次大作业中,通过对hello程序的一生的追踪,对这一学期以来所学计算机系统的知识有了一个更加系统与贯穿性的理解,知识点之间的联系变得更加紧密。在这次作业中一些之前没有得到充分理解的东西通过查阅资料与网站得到了一些新的认识,但是仍然还有部分知识点有些模糊,hello程序虽然简单,但是其中蕴含的计算机科学却是十分基础而又复杂的,计算机这么学科也是十分精妙和神奇。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.c C语言源程序
hello.i hello.c预处理后生成的文本文件
hello.s
hello.i经编译器翻译后生成的文本文件
hello.o hello.s经过汇编器翻译后生成的可重定位目标文件
hello hello.o链接后的可执行文件
hello.elf
hello.o的elf格式文件
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴.
空间控制技术[M].
北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C].
北京:中国科学出版社,1999.
[3] 赵耀东.
新时代的工业工程师[M/OL].
台北:天下文化出版社,1998
[1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖.
空间交会控制理论与方法研究[D].
哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H.
Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the
Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23].
http://www.sciencemag.org/cgi/ collection/anatmorp.
[7] https://blog.csdn.net/HelloTheWholeWorld/article/details/85339220
[8]Randal E.Bryant,David R.O’Hallaron深入理解计算机系统 2019.3,机械工业出版社
[9] https://blog.csdn.net/weixin_42867636/article/details/85497861
[10]https://www.cnblogs.com/pianist/p/3315801.html
来源:CSDN
作者:yigaunghui移
链接:https://blog.csdn.net/qq_45711796/article/details/103788875