Version:1.0 StartHTML:000000246 EndHTML:000562314 StartFragment:000088180 EndFragment:000562282 StartSelection:000088206 EndSelection:000562276 SourceURL:mhtml:file://F:\计算机系统\计算机系统大作业\ICS2019大作业论文.mht第1章绪论
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术 计算机类
学 号 1181000516
班 级 计算机四班
学 生 杨君豪
指 导 教 师 史先俊
计算机科学与技术学院
2019年12月
摘 要
摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。
本文将分析hello的一生所经历的阶段,从hello.c的创建开始,到hello进程结束中间的编译、链接、加载、进程管理、存储管理等过程以及它们的原理和实现方法。
关键词:hello的一生,P2P, 编译,汇编,链接,进程,存储
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第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 -
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P简介: 程序员用IDE等相关工具编写hello.c程序(文本文件);在 Linux操作 系统里,预处理器根据以字符#开始的命令修改hello.c得到另一 个C程序 hello.i(文本文件);编译器将hello.i翻译成文本文件hello.s(文 本文件); 汇编器翻译得到可重定位目标文件hello.o;经过链接(ld)生成 hello(可执 行目标程序)。程序员在Shell输入./hello执行此程序,hello最后 变成了系统 里的一个进程。
020简介:在Shell处理Hello过程中,shell会fork一个子进程,并在这个子 进程中调用execve加载hello。然后程序会跳转到_start地址,最终调用hello 的main函数。打印完hello后程序结束。最后shell回收此进程。
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件环境:Intel® Core™ i7-7700HQ CPU;16.00GB RAM; 256GSSD
软件环境:Ubuntu 18.04 64位;Windows 10 64位
开发与调试工具:CodeBlocks ;Visual Studio ;GCC;GDB;EDB;objdump;readelf;gedit;hexedit;vim;
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i 预处理后的文件 预处理
hello.s 编译之后的汇编 编译
文件 汇编
hello.o 汇编之后的可重
定位目标文件 汇编
hello 链接后的可执行 汇编
目标文件
hello.elf hello的elf文件 链接
hello.txt hello.o的反汇编 链接
文件
helloasm.txt hello的反汇编文件
(第1章0.5分)
本章介绍了完成大作业所需要使用的工具,以及开发环境
预处理的概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程 序,预处理是在编译之前进行的处理。 C语言的预处理主要有三个方面的内 容:1.宏定义; 2.文件包含; 3.条件编译。 预处理命令以符号“#”开头。
预处理的命令:gcc -E -m64 -Og -no-pie -fno-PIC hello.c -o hello.i
(gedit hello.i是为了展示编译的进程)
应截图,展示预处理过程!
图1
根据#include
预处理操作把这些东西全都塞到了hello.i文本中。不过这样子也的确方便 了编译器对hello程序进行翻译成汇编语言的操作。
本章主要介绍了预处理的概念及作用,对hello.c的预处理过程进行了阐 述与分析。
(第2章0.5分)
(以下格式自行编排,编辑时删除)
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
概念:编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查编译和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
作用:词法分析:词法分析阶段是编译过程的第一个阶段。这个阶段的任务是从左到右一个字符一个字符地读入源程序,即对构成源程序的字符流进行扫描然后根据构词规则识别单词(也称单词符号或符号)。
语法分析:语法分析是编译过程的一个逻辑阶段。语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等.语法分析程序判断源程序在结构上是否正确.源程序的结构由上下文无关文法描述。
语义分析:语义分析是编译过程的一个逻辑阶段. 语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查, 进行类型审查.
优化后生成相应的汇编代码
gcc -S -m64 -Og -no-pie -fno-PIC hello.i -o hello.s
(gedit hello.s是为了展示编译的进程)
应截图,展示编译过程!
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
文件大框架:
.file 源文件名
.text 代码段
.string 字符串
.secetion .rodata rodata节
.align 对齐方式
.globl 全局变量
.long 长整型
.size 声明大小
.type 指定是对象类型或是函数类型
3.3.1 数据
3.3.1-1 字符串
1.程序中的字符串1是:
"\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \ 345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201"
他的声明在.LC0段,可以发现字符串被编码成UTF-8格式,一个汉字在utf- 8编码中占三个字节。声明在.LC0段。.LC0保存在rodata中。
2.程序中的字符串2是:
“Hello %s %s\n”。声明在.LC1段。.LC1也保存在rodata中。
3.3.1-2 整数
1.参数:argc
.LFB52:
.cfi_startproc
pushq %rbp
argc是第一个参数,这个参数被pushq %rbp传入main中。
2.局部变量:int i
cmpl $7, %ebx
jle .L3
从这两条指令可以看出循环里的变量i被存到了%ebx中
其他的整数都作为立即数出现。
3.3.1-3 数组
char *argv[]
movq 16(%rbp), %rcx
movq 8(%rbp), %rdx
movl $.LC1, %esi
movl $1, %edi
movl $0, %eax
call __printf_chk
movq 24(%rbp), %rdi
movl $10, %edx
movl $0, %esi
call strtol
movl %eax, %edi
call sleep
addl $1, %ebx
这是一个指针数组
3.3.2赋值
使用movl指令对局部变量i进行赋值,并且把初值赋为0
movl $0, %ebx
3.3.3 算术操作
i++
addl $1, %ebx
3.3.4 关系操作
3.3.4-1 argc!=4
subq $8, %rsp
.cfi_def_cfa_offset 32
cmpl $4, %edi
jne .L6
3.3.4-2 i<8
cmpl $7, %ebx
jle .L3
这里把i<8的比较转化成了i<=7的比较,由于i是整型的数据,这两个是 一样的,如果i小于等于7,则跳转执行循环.L3里的内容,否则退出循环。
3.3.5 控制转移
3.3.5-1 if(argv!=4)
cmpl $4, %edi
jne .L6
用cmpl去比较argv和4,结果由条件码ZF来体现,如果条件码ZF为0说 明argv-4=0,即argv=4,如果条件码不为0则说明argv!=4。ZF不为0时执行 if 中 的代码直接跳转到.L2,否则 顺序执行下一条语句,即 执行 if 中的代码。
3.3.5-2 for(i=0;i<10;i++)
addl $1, %ebx
cmpl $7, %ebx
jle .L3
使用 cmpl 进行比较,如果 i<=7,则跳入.L3 for循环体执行,否则跳出循 环,顺序执行for之后的指令。
3.3.6函数
函数调用的过程为参数传递(地址参/值)、函数调用()、函数返回 return
3.3.6-1 main函数
1.参数传递:main函数有两个参数 int argc,char *argv[];按顺序使用%rbp,%rbx 存储
2.函数调用:main函数被系统启动函数 __libc_start_main 调用,call指令将 main函数的 地址分配给%rip,随后调用main函数。
3.函数返回:函数返回:外部调用过程向main函数传递参数argc和argv,函 数正常出口为return 0,将%eax设置0返回。
3.3.6-2 printf函数
1.参数传递:call puts时printf将%rdi 设置为“Usage: Hello 学号 姓名! \n”字 符串的首地址。for循环中 printf 设置%rdi 为“Hello %s %s\n”的首地址,设 置%rsi 为 argv[1],%rdx为argv[2]。
2.函数调用: 第一次调用时printf 读入一个字符串参数,所以 call puts;第 二次有两个参数, printf 使用call __printf_chk
3.3.6-3 exit函数
1.参数传递:
movl $1, %edi
将1赋值到%edi上
2.函数调用:
call exit
调用exit函数
3.3.6-4 strtol函数
1.参数传递:
movl $0, %esi
将%esi赋值为0
2.函数调用:
call strtol
调用strtol函数
本章主要内容是各种数据结构与函数的汇编语言实现,同时介绍了编译器是如何处理各种函数与操作的,逐渐的把hello.c中的内容与hello.s中的内容对应起来从而帮助我们理解机器是如何处理C语言中的数据和操作。
(以下格式自行编排,编辑时删除)
(第3章2分)
(以下格式自行编排,编辑时删除)
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
概念:汇编器(as)将.s翻译成机器语言指令,把这些指令打包成一种叫做可充定位目标程序的格式,并将结果保存在目标文件.o中。
作用:把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。(将.s文件转化为.o文件)
(以下格式自行编排,编辑时删除)
应截图,展示汇编过程!
gcc -c –m64 –no-pie –fno-PIC hello.s -o hello.o
图3
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
ELF头描述了生成该文件的系统的字的大小和字节顺序,并且包含帮助链接器语法分析和解释目标文件的信息。
图4
节头部表描述了不同节的位置和大小,其中目标文件中每个节都有一个固定大小的条目。具体的描述包括节的名称、类型、地址和偏移量等。
图5
汇编器生成一个目标模块是,它并不知道数据和代码最终将放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行目标文件时如何修改这个引用。代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.data中。
ELF重定位条目的数据结构如下:
typedef struct{
long offset; /*需要被修改的引用的节偏移*/
long type:32, /*重定位类型*/
symbol:32; /*标识被修改引用应该指向的符号*/
long attend; /*符号常数,对修改引用的值做偏移调整*/
}Elf64_Rela;
两种最基本的重定位类型:
R_X86_64_PC32 :重定位一个使用32位PC相对地址的引用。
R_X86_64_32 :重定位一个使用32位PC绝对地址的引用。
重定位信息的详细情况如图:
图6
这些情况分别对应了.rodata,函数puts,exit,sleep等
还有一个符号表.symtab,分别对应了程序中定义和引用的函数和全局变量的信息。
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
机器语言在具体细节的实现上和汇编语言有比较大的差距,例如操作数和分支转移函数。
图7
、
机器语言hello.o反汇编
图8
汇编语言hello.s
它们的差距体现在:
1.操作数:hello.o反汇编代码中的操作数是十六进制,hello.s中的操作数是十进制。
2.分支的跳转方式:hello.o反汇编代码中是使用相对偏移地址,用
3.函数调用:hello.o反汇编代码中call指令之后是函数的相对偏移地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目
重定位节 '.rela.text' at offset 0x358 contains 9 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000016 00050000000a R_X86_64_32 0000000000000000 .rodata.str1.8 + 0
00000000001b 000b00000002 R_X86_64_PC32 0000000000000000 puts - 4
000000000025 000c00000002 R_X86_64_PC32 0000000000000000 exit - 4
000000000032 00060000000a R_X86_64_32 0000000000000000 .rodata.str1.1 + 0
000000000041 000d00000002 R_X86_64_PC32 0000000000000000 __printf_chk - 4
000000000054 000e00000002 R_X86_64_PC32 0000000000000000 strtol - 4
00000000005b 000f00000002 R_X86_64_PC32 0000000000000000 sleep - 4
00000000006a 001000000002 R_X86_64_PC32 0000000000000000 stdin - 4
00000000006f 001100000002 R_X86_64_PC32 0000000000000000 _IO_getc
hello.s中调用函数直接是call+函数名
使用汇编后,把汇编语言转换成了机器语言,相应的汇编指令转换成了机器指令,通过比较他们的些许的不同发现了汇编语言实现和机器语言实现的差别和从汇编语言到机器语言的转变,生成可重定位目标文件也为下一步链接作了准备。
(第4章1分)
(以下格式自行编排,编辑时删除)
注意:这儿的链接是指从 hello.o 到hello生成过程。
概念:将一个或多个由编译器或汇编器生成的目标文件外加库链接为一个可执行文件。目标文件是包括机器码和链接器可用信息的程序模块。
作用:链接器的工作就是解析未定义的符号引用,将目标文件中的占位符替换为符号的地址。链接器还要完成程序中各目标文件的地址空间的组织,这可能涉及重定位工作。
(以下格式自行编排,编辑时删除)
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
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
图9
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
可执行目标文件hello的格式类似于可重定位目标文件的格式,ELF头描述文件的总体格式。的,除了这些节已经被重定位到它们最终的运行时的内存地址外。.init节定义了一个小函数_init,程序初始化代码会调用它。因为可执行文件时完全连接的,所以无.rel节。
图10
.text ,.rodata和.data节与可重定位目标文件中的节类似,它们会被重定位到它们最终的运行时的内存地址。.init节定义了一个小函数_init,程序初始化代码会调用它。因为可执行文件时完全连接的,所以无.rel节。
图11 hello文件各节的信息
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
在edb中打开hello 查看data dump窗口 可以看出程序的地址是从
0x40000加载的 0x400fff结束
再看elf文件中的program headers:
每一个表项都提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息
再看程序的八个段:
PHDR:程序头表
INTERP:程序执行前需要调用的解释器
LOAD:程序目标代码和常量信息
DYNAMIC:动态链接器所使用的信息
NOTE::辅助信息
GNU_EH_FRAME:保存异常信息
GNU_STACK:使用系统栈所需要的权限信息
GNU_RELRO:保存在重定位之后只读信息的位置
图12
(以下格式自行编排,编辑时删除)
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
1.节的数量不同,hello反汇编代码比hello.o反汇编代码多出:.init,.plt,.text,
.fini
2.函数的数量不同:在hello多出来的节中同时也多出了一些运行main函数
所必要的函数:_init,_start,__libc_csu_init,__libc_csu_fini,__libc_start_main
等
3.链接的时候指定了/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o,所以将 会把这些.o文件的每个节与hello.o的节合并。在合并的过程中,会根据重 定位信息对相应的地方进行重定位,hello.o只是hello的一部分而已。
(以下格式自行编排,编辑时删除)
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
载入:
_dl_start
_dl_init
开始执行
_start
_libc_start_main
_init
执行main:
_main
_printf
_exit
_sleep
_getcha
_dl_runtime_resolve_xsave
_dl_fixup
_dl_lookup_symbol_x
退出:
exit
(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
动态链接使我们在调用一个共享库定义的函数可以在运行时找到函数的地址。但是在调用时编译器没办法预测这个函数(共享库定义)的运行时地址,因为定义它的共享模块可以在运行时加载到任何位置。但是GNU编译系统通过延迟绑定技术来解决这个问题,将过程地址的绑定推迟到第一次调用该过程中。
延迟绑定通过:GOT和PLT实现,如果一个目标模块调用定义在共享库中的任何函数,那么他就有自己的GOT和PLT。
第一次调用共享库函数时,不调用共享库函数,直接进入函数对应的PLT中,接着PLT指令通过对应的GOT指令进行间接跳转,由于每个GOT指令初始时都指向他对应的PLT条目的第二条指令,所以这个间接跳转只是简单的把控制传回PLT条目的下一条指令。接着把函数的ID入栈PLT跳转到PLT[0],PLT[0]再将动态链接器的一个参数入栈,然后间接跳转到动态链接器中。动态链接器依据两个栈条目确定函数的运行位置,重写对应的GOT条目,再把控制传给函数。
所以,在运行dl_init前,GOT表中存放的都是对应PLT条目的第二条指令,在运行dl_init后,GOT表中存放的就是对应的函数的地址。
这一章主要关注了链接的概念与作用,链接就是是将各种代码和数据片段收集并组合成一个单一文件的过程。使用readelf分析了hello的头表,节头,偏移量等文件,查看hello的虚拟地址空间,并且对比hello.o和hello的反汇编代码,偏移量与进程的虚拟地址空间各段位置一一对应。比较hello与hello.o反汇编的不同,发现共享库函数的地址变为了实际地址,又分析了hello运行过程中的所有的函数和hello的运行过程,发现hello会在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中,并且共享库函数使用延迟绑定的方法,利用PLT和GOT帮助最终找到函数的地址。
(第5章1分)
(以下格式自行编排,编辑时删除)
概念:
狭义:进程是一个执行中的程序的实例。
广义:系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正 确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数 据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件 描述符的集合。
作用:
1.进程提供给应用程序两个关键抽象:
逻辑控制流: 每个程序似乎独占地使用CPU,通过OS内核的上下文切换 机制提供。
私有地址空间:每个程序似乎独占地使用内存系统,OS内核的虚拟内存机 制提供。
2.每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell 就会创建一个新的进程,然后在这个进程的上下文中运行这个可执行目标 文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自 己的代码或其他应用程序。
作用:Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内 核。不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编 写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点, 比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程 序与其他应用程序具有同样的效果
处理流程:
1.从终端读入输入的命令。
2.将输入字符串切分获得所有的参数
3.如果是内置命令则立即执行
4.否则调用相应的程序为其分配子进程并运行
5.shell 应该接受键盘输入信号,并对这些信号进行相应处理
在bash中输入 ./hello 1181000516 杨君豪 10并敲击回车后,bash解析此条命令,发现./hello不是bash内置命令,于是在当前目录尝试寻找并执行hello文件。此时bash 使用fork函数创建一个子进程(这个子进程得到与父进程用户级虚拟地址空间相同但是独立的一份副本),并更改这个子进程的进程组编号。并准备在这个子进程执行execve。
(以下格式自行编排,编辑时删除)
在shell创建的子进程中将会调用execve函数,来调用加载器,加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序复制到内存并运行的过程叫做加载。每个程序都有一个运行时内存映像,如图6.2所示。当加载器运行时,它创建类似图6.2所示的内存映像。在程序头部表的引导下,加载器将可执行文件的片(chunk)复制到代码段和数据段。接下来,加载器跳转到程序的入口点,也就是_start函数的地址。这个函数是在系统目标文件ctrl.o中定义的,对所哟额C程序都是一样的。_start函数调用系统启动函数_ _libc_start_main,该函数定义在libc.so中,它初始化执行环境,调用用户层的main函数,处理main函数的返回值,并且在需要的时候把控制返回给内核。
exceve函数实际上是如何加载和执行程序Hello,需要以下几个步骤:
1.删除已存在的用户区域。
2.映射私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时复制的。
3.映射共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
(以下格式自行编排,编辑时删除)
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
处理器通常用某个控制寄存器的一个模式位来提供用户模式和内核模式的功能。设置了模式位时,进程就运行在内核模式中,该进程可以执行指令集中的任何指令,可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中,用户模式中的进程不允许执行特权指令。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程的决定叫做调度。
上下文信息:内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。
上下文切换:内核为每个进程维持一个上下文,上下文就是在进程执行的某些时刻,内核可以决定枪战当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度。系统调用和中断也可能引发上下文切换。
并发流:系统为每个程序都提供了一种只有它一个程序在运行的假象,但是实际情况却不是这样的,系统中很有很多其他程序在运行,比如我现在打字的word和我的虚拟机就是两个程序,它们都在运行。那么处理器是如何执行它们的,以至于让它们看起来都在不间断的一直运行呢?答案就是并发,如图6.3,处理器分时间段执行进程A、B、C,这个转换的时间非常短,所以看起来就好像每个进程都在持续不断的在运行。多个逻辑控制流并发执行的一半现象被称为并发。一个进程和其他进程轮流运行的概念成为多任务,一个进程执行它的控制流的每一时间段就成为时间片。如图6.3中进程A就由两个时间片组成。
内核模式和用户模式:内核模式和用户模式不是两个进程,而是一个进程的不同模式,由一个模式位来控制,当设置了模式位时,进程就运行在内核模式中,这时候这个进程就可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。运行程序代码的进程一开始是处于用户模式,只有当发生中断、故障或者陷入系统调用这样的异常时,转而去执行异常处理程序,这时进程才会变为内核模式。当它返回到应用程序代码时,处理器就把模式从内核模式改为用户模式。
逻辑控制流:即使在系统中通常有许多其他程序正在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果使用调试器单步调试执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,简称逻辑流。
hello进程调度的过程:
程序在执行sleep函数时,sleep系统调用显式地请求让调用进程休眠,调度器抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。sleep的倒计时结束后,控制会回到hello进程中。程序调用getchar()时,内核可以执行上下文切换,将控制转移到其他进程。getchar()的数据传输结束之后,引发一个中断信号,控制回到hello进程中。
(以下格式自行编排,编辑时删除)
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
异常的类别:
图13
信号:
图14
1.hello进程正常执行时:如图15
图15
2.运行时不停的乱按,如图16,乱按的输入并不会影响到进程的执行,只不过如果在进程末尾按到回车键,shell会切换到下一条指令的读取状态。
图16
3.运行时按CTRL+Z然后再使用ps命令,如图17,按下Ctrl+Z后,父进程收到SIGTSTP信号,将hello进程挂起,ps命令列出当前系统中的进程(包括僵死进程)。
图17
4.运行时按CTRL+Z然后再使用jobs命令,如图18,jobs显示shell环境中已启动的任务状态。
图18
5.运行时按CTRL+Z然后再使用pstree命令,如图19,把进程之间的关系以树状图的形式显示出来。
图19
6.运行时按CTRL+C然后再使用fg指令,如图20,前台运行
图20
7.kill指令杀死进程,如图21,kill -9 3916 杀死PID为3916的进程。
图21
本章关注进程的概念和作用以及shell上进程的执行情况,在这里熟悉了shell的基本操作和指令以及shell是如何fork新建子进程、execve如何执行进程、hello进程如何在内核和前端中反复跳跃运行的。
(第6章1分)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址:包含在机器语言中用来指定一个操作数或一条指令的地址。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。就是hello.o里相对偏移地址。
线性地址:逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。是hello中的虚拟内存地址。
虚拟地址:一个带虚拟内存的系统中,CPU从一个有N=2^n个地址空间中生成虚拟地址。虚拟地址其实就是线性地址。
物理地址:用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。地址翻译会将hello的一个虚拟地址转化为物理地址。
(以下格式自行编排,编辑时删除)
逻辑地址由两部分组成:段标识符、段内偏移量
段标识符是由一个16位长的字段组成,称为段选择符。其中前13位为索引号,后面三位包含一些硬件细节。
首先,给定一个完整的逻辑地址段选择符:段内偏移地址,
1.看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。得到一个数组。
2.拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3.把Base + offset,就是要转换的线性地址了。
在保护模式下,分段机制就可以描述为:通过解析段寄存器中的段选择符 在段描述符表中根据 Index 选择目标描述符条目 Segment Descriptor,从目标描述 符中提取出目标段的基地址 Base address,最后加上偏移量 offset 共同构成线性地 址 Linear Address。
CPU通过将逻辑地址转换为虚拟地址来访问主存,这个虚拟地址在访问主存前必须先转换成适当的物理地址。CPU芯片上叫做内存管理单元(MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址。然后CPU会通过这个物理地址来访问物理内存。
页表结构:在物理内存中存放着一个叫做页表的数据结构,页表将虚拟页映射到物理页,每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。
页表就是一个页表条目(PTE)数组,虚拟地址空间中的每个页在页表中的一个固定偏移量处都有一个PTE。PTE是由一个有效位和一个n个字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置。
MMU利用虚拟页号(VPN)来在虚拟页表中选择合适的PTE,当找到合适的PTE之后,PTE中的物理页号(PPN)和虚拟页偏移量(VPO)就会组合形成物理地址。其中VPO与PPO相同,因为虚拟页大小和物理页大小相同,所需要的偏移量位数也就相同。此时,物理地址就通过物理页号先找到对应的物理页,然后再根据物理页偏移找到具体的字节。
如图22,Core i7 MMU如何使用四级页表来将虚拟地址翻译成物理地址。
图22
36位的虚拟地址被分割成4个9位的片。CR3寄存器包含L1页表的物理地址。VPN1有一个到L1 PTE的偏移量,找到这个PTE以后又会包含到L2页表的基础地址;VPN2包含一个到L2PTE的偏移量,找到这个PTE以后又会包含到L3页表的基础地址;VPN3包含一个到L3PTE的偏移量,找到这个PTE以后又会包含到L4页表的基础地址;VPN4包含一个到L4PTE的偏移量,找到这个PTE以后就是相应的PPN(物理页号)。
Core i7 MMU使用四级页表来将虚拟地址翻译成物理地址,我们得到了物理地址PA。现在分析三级cache支持下的物理内存访问。如图7-6,以L1 d-cache的介绍为例,L2和L3同理。
L1 Cache是8路64组相联。块大小为64B。因此CO和CI都是6位,CT是40位。根据物理地址(PA),首先使用CI组索引,每组8路,分别匹配标记CT。如果匹配成功且块的有效位是1,则命中,根据块偏移CO返回数据。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中,向下一级缓存中取出被请求的块,然后将新的块存储在组索引指示的组中的一个高速缓存行中。一般而言,如果映射到的组内有空闲块,则直接放置,否则必须驱逐出一个现存的块,一般采用最近最少被使用策略LRU进行替换。
(以下格式自行编排,编辑时删除)
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
1.删除已存在的用户区域
2.创建新的私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构 3.创建新的共享区域.
4.设置PC指向代码的入口
缺页故障:
当虚拟地址在MMU中查找页表时发现对应的物理内存不在主存中,需要从需要操作系统将其调入主存就称为缺页故障。
1.请求调页: 当进程调用malloc()之类的函数调用时,并未实际上分配物理内存,而是仅仅分配了一段线性地址空间,在实际访问该页框时才实际去分配物理页框,这样可以节省物理内存的开销,还有一种情况是在内存回收时,该物理页面的内容被写到了磁盘上,被系统回收了,这时候需要再分配页框,并且读取其保存的内容。
2.写时复制:当fork()一个进程时,子进程并未完整的复制父进程的地址空间,而是共享相关的资源,父进程的页表被设为只读的,当子进程进行写操作时,会触发缺页异常,从而为子进程分配页框。
3.地址范围外的错误:内核访问无效地址,用户态进程访问无效地址等。
4.内核访问非连续性地址:用于内核的高端内存映射,高端内存映射仅仅修改了主内核页表的内容,当进程访问内核态时需要将该部分的页表内容复制到自己的进程页表里面。
缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果 这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺 页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令再次发送 VA 到 MMU,这次 MMU 就能正常翻译 VA 。
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
基本方法:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(如图7-9)。分配器将堆视为一组不同大小的块的集合,来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
1.显式分配器:要求应用显式地释放任何已分配的块。例如C程序通过调用malloc函数来分配一个块,通过调用free函数来释放一个块。其中malloc采用的总体策略是:先系统调用sbrk一次,会得到一段较大的并且是连续的空间。进程把系统内核分配给自己的这段空间留着慢慢用。之后调用malloc时就从这段空间中分配,free回收时就再还回来(而不是还给系统内核)。只有当这段空间全部被分配掉时还不够用时,才再次系统调用sbrk。当然,这一次调用sbrk后内核分配给进程的空间和刚才的那块空间一般不会是相邻的。
2.隐式分配器:也叫做垃圾收集器,例如,诸如Lisp、ML、以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
3.隐式空闲链表:
这样的一种结构,主要是由三部分组成:头部、有效载荷、填充(可选);
头部:是由块大小+标志位(a已分配/f空闲);有效载荷:实际的数据
4.
策略:
空闲块合并:
因为有了 Footer,所以我们可以方便的对前面的空闲块进行合并。合并的 情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。对于 四种情况分别进行空闲块合并,我们只需要通过改变 Header 和 Footer 中的值 就可以完成这一操作。
分离的空闲链表:
维护多个空闲链表,其中每个链表中的块有大致相等的大小。分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小的升序排列。当分配器需要一个大小为n的块时,它就搜索相应的空闲链表。如果不能找到合适的块与其匹配,它就搜索下一个链表,以此类推。
虚拟内存是对主存的一种模拟,本章重点关注hello的存储器地址空间、intel的段式管理、hello的页式管理, 以intel Core7在指定环境下介绍了VA 到PA 的变换、物理内存访问,hello 进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理,通过学习深入掌握了内存映射的相关原理和访问数据的相关步骤。
(第7章 2分)
设备的模型化:文件
设备管理:unix io接口
设备的模型化:所有的 IO 设备都被模型化为文件,而所有的输入和输出 都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方 式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
Unix IO接口:
打开文件:内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件
改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。
读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k
关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中
函数:
1.int open(char *filename, int flags, mode_t mode)
进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。open函数将filename转换为一个文件描述符,而且返回描述符数字。flags参数指明了进程打算如何访问这个文件。mode参数指定了新文件的访问权限位。
2.int close(int fd)
进程通过调用close函数关闭一个打开的文件。
3.ssize_t read(int fd, void *buf, size_t n)
应用程序通过调用read函数来执行输入。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,返回值0表示EOF。否则返回值表示的是实际传送的字节数量。
4.ssize_t write(int fd, const void *buf, size_t n)
应用程序通过调用write函数来执行输出。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
研究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;
}
调用printf函数的时候,先是最右边的参数入栈。fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。fmt也是个变量,它的位置,是在栈上分配的,它也有地址。
函数的参数是一个字符指针 通过va_list_arg实现了变长的参数 因为函数接受的是指针,只要地址连续就可以读取多个参数
i = 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);
}
从上面vsprintf函数可以看出,这个函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度
write函数如下:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALL 代表系统调用syscall
syscall如下
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
于是可以直到printf函数执行过程如下:
1.从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall
2.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
3.显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
int getchar(void)
{
static char buf[BUFSIZ];
static char* bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return(–n>=0)?(unsigned char)*bb++:EOF;
}
这里面的getchar调用了一个read函数,这个read函数是将整个缓冲区都读到了buf里面,然后将返回值是缓冲区的长度。我们可以发现,如果buf长度为0,getchar才会调用read函数,否则是直接将保存的buf中的最前面的元素返回。
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。
关注Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数。
(第8章1分)
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello所经历的过程:
1.hello.c被预处理hello.i文件
2.hello.i被编译为hello.s汇编文件
3.hello.s被汇编成可重定位目标文件hello.o
4.链接器将hello.o和外部文件链接成可执行文件hello
5.在shell输入命令./hello 杨君豪 1181000516 1后,通过execeve加载并运行hello
6.在一个时间片中,hello有自己的CPU资源,顺序执行逻辑控制流
7.hello的VA通过TLB和页表翻译为PA
8.三级cache 支持下的hello物理地址访问
9.在hello运行过程中,我们可以发送各种信号来控制它 如crtl+z等
10.printf会调用malloc通过动态内存分配器申请堆中的内存
11.shell父进程回收hello子进程,内核删除为hello创建的所有数据结构
我的深切感悟:hello这一条简单的程序,从底层到高层的实现历尽种种的处理,从一个程序员角度应该在编写程序时注意编写面向编译器,cache等友好的程序,以优化程序的执行性能。
我的创新理念:计算机可以设计的更加的模块化,比如高速缓存器可以多设计几层级联起来从而提高内存访问效率,还可以把类似功能的部件进行合并,比如页表也是cache,可以考虑他们的兼容设计。
(结论0分,缺失 -1分,根据内容酌情加分)
列出所有的中间产物的文件名,并予以说明起作用。
hello.i 预处理产生的文本文件
hello.s 编译产生的汇编文件
hello.o 汇编产生的可重定位目标执行(二进制)
hello 链接之后的可执行目标文件
(附件0分,缺失 -1分)
为完成本次大作业你翻阅的书籍与网站等
[1] 深入理解计算机系统 Randal E.Bryant David R.O’Hallaron 机械工业出版社
[2] 博客园 printf函数实现的深入剖析
https://www.cnblogs.com/pianist/p/3315801.html
[3] ELF 构造:https://www.cs.stevens.edu/~jschauma/631/elf.html
[4] Linux下进程的睡眠唤醒:https://blog.csdn.net/shengin/article/details/21530337
[5]进程的睡眠、挂起和阻塞:https://www.zhihu.com/question/42962803
[6]虚拟地址、逻辑地址、线性地址、物理地址:https://blog.csdn.net/rabbit_in_android/article/details/49976101
[5] 百度百科 getchar计算机语言函数
https://baike.baidu.com/item/getchar/919709?fr=aladdin
[6] 博客园 内存管理
https://www.cnblogs.com/xavierlee/p/6400230.html
[7] getchar百度百科:
https://baike.baidu.com/item/getchar/919709?fr=aladdin
[8] LINUX 逻辑地址、线性地址、物理地址和虚拟地址
https://www.cnblogs.com/zengkefu/p/5452792.html
(参考文献0分,缺失 -1分)