程序人生-Hello’s P2P
计算机系统
大作业
hello.c,每个计算机人程序路上的第一步。预处理、编译、汇编、链接、进程管理、存储管理、IO管理...它最为简单,却是每个计算机程序运行的点点缩影。Hello.c是用高级语言C编写的,我们要经过预处理,编译,汇编等过程,才能作为机器能读懂的机器代码储存在磁盘中。Hello现在的状态叫程序(Program),用户通过shell,调用一系列函数将hello运行在内存中。他是通过一种叫做进程(Process)的抽象来实现的。理解这些简单之下的过程与作用,能加深对计算机系统的深刻认识,以及学习过程疑问的逐步解答。以“hello一生”,走进计算机的故事。
关键词:编译管理;进程链接;hello;计算机系统分析;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目录
6.2简述壳Shell-bash的作用与处理流程 - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
1.1 Hello简介
P2P的过程: 在linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork,产生子进程,于是hello便从Program成为Process。
020的过程: shell为其execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。
硬件环境:IntelG4560 x64CPU, 16G RAM
软件环境:Ubuntu18.04.3
开发与调试工具:vim,gcc,ld,edb,readelf,HexEdit
[if !supportLists]1. [endif]hello.i: hello.c预处理之后的文件
[if !supportLists]2. [endif]hello.s:编译之后的汇编文件
[if !supportLists]3. [endif]hello.o: 汇编之后的目标执行
[if !supportLists]4. [endif]hello: 链接实现的可执行目标文件
[if !supportLists]5. [endif]helloo.elf: hello.o 的ELF格式
[if !supportLists]6. [endif]hello.elf: hello 的ELF格式
本章主要简单介绍了hello的p2p,020过程,列出了本次实验信息:环境、中间结果。
(第1章0.5分)
2.1 预处理的概念与作用
概念:
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor)对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。
作用:
1.对相关命令或语句的含义和功能作具体分析。
3.用实际值替换用#define定义的字符串
4.根据#if后面的条件决定需要编译的代码
1.将所有的#define删除,并展开所有的宏定义;
2.处理所有的预编译指令,例如:#if,#elif,#else,#endif;
3.处理#include预编译指令,将被包含的文件插入到预编译指令的位置;
4.添加行号信息文件名信息,便于调试;
5.删除所有的注释:// /**/;
2.4 本章小结
hello.c预处理 生成 hello.i,生成.i文件。
包括(1)去注释 (2)宏替换 (3)头文件展开 (4)条件编译
(第2章0.5分)
概念:编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序
作用:将c语言文件变为 汇编语言文件
3.3.1.数据:在执行main程序时首先将栈底指针入栈,同时令栈顶指针值等于栈底指针值,完成栈的初始化。然后通过减少栈顶指针值在栈中获得空间。此时的argc与argv[]存放在寄存器edi和rsi中,通过两个mov操作,将参数值进栈,从而完成了参数的传递。
常量
变量(全局(无)/局部)
向函数传递参数值、数组、指针
argc为图中-20(%rbp)
变量i
将i赋值为0
3.3.3类型转换
调用atoi函数将字符串转为整型
3.3.4算术操作
对局部变量i累加
3.3.5关系操作
判断argc与4是否相等
判断i是否小于等于7
3.3.6数组/指针/结构操作
对argv数组进行操作,数组在存储空间中申请的是连续的内存。
3.3.7函数操作
终止程序
从键盘输入内读取字符串
打印到屏幕
字符串转为整型
need-to-insert-img
休眠
清除回车
3.3.8for循环
若参数个数为4,则跳转到.L2。此时进入for循环。for循环的控制变量为局部变量i,且初值为0的i满足i<8时进入循环。.L2将i的初值赋值为0,跳转到.L3。循环开始前执行.L3的第一条cmp语句,满足条件进入循环体,在汇编代码中的体现为跳转到.L4。在.L4中,调用了printf函数,通过main创建的栈为其传递参数到寄存器rdx和rsi中。然后调用sleep函数。注意到sleep函数中的参数为atoi函数,则首先需要执行atoi函数。仍是利用已经存储在栈中的argv数组,将所需值送入寄存器rdi,执行atoi后,返回值存储在寄存器eax中。再将其送入寄存器edi,则参数值成功传入sleep中,一次循环进行完成,将循环控制变量i的值+1,进入新一轮循环。当i的值等于8时,循环停止。
3.4 本章小结
第三章主要还是详细分析编译器对hello.c的具体操作与生成的.s文件,了解各个数据和操作类型如何进行表示与实现。
(第3章2分)
汇编器将.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中,它包含程序的指令编码。这个过程称为汇编,亦即汇编的作用
4.2 在Ubuntu下汇编的命令
hello.o是可重定位文件,节头大小为64字节,数量为13,字符串表索引节头为12
从总体流程看,hello.s与hello.o的总体流程相同。从hello.o的反汇编文件可以看出每行十六进制数代表着一个指令操作,存在一一对应的关系。机器语言由二进制数构成,分为操作数和操作码。每条01序列构成的指令都有不同的含义。例如,对于一条机器指令,有些序列代表寄存器的编号。将操作码与代表寄存器的序列结合后生成新的01序列,此时看来会有机器语言中的操作数与汇编语言不一致的现象。其本质是机器语言的指令结合了操作码。hello.s文件中使用标志来进行分支转移,如指令jmp .L2。而在hello.o反汇编得到的文件中,则采用了相对寻址方式进行分支转移操作。.函数调用:hello.o反汇编文件中调用函数的操作为“call 下条指令的地址”,在hello.s中则是“call 函数名”。因为hello.c中调用的函数都是库函数,在进行下一步链接后才能最终确定其地址,所以在汇编时将call指令后的地址设置为0,等待链接。
主要差别如下:
1.反汇编代码跳转指令的操作数使用的不是段名称,所以在汇编成机器语言之后是确定的地址。2.在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。
第四章解释了hello.s到hello.o的汇编过程,并使用readelf 与 objdump 工具对hello.o 的ELF文件分析与反汇编之后与hello.s的比较,表现了汇编语言到机器语言的转换。
(第4章1分)
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
1.读取ELF文件头
2. 显示程序头表
3.读取头节表
Type存储段的类型 特殊节的类型
VirtAddr首字节的虚拟地址
PhysAddr首字节的物理地址
Align对齐方式 为2的整数幂
FileSiz文件中所占的字节数
MemSiz存储器中所占字节数
1.hello相对hello.o多了很多的节类似于.init,.plt等
2.hello.o中的相对偏移地址到了hello中变成了虚拟内存地址
3.hello中相对hello.o增加了许多的外部链接来的函数。
4.hello.o中跳转以及函数调用的地址在hello中都被更换成了虚拟内存地址。
重定位:
在hello到hello.o中,首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。
然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。
当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。
5.6 hello的执行流程
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!__libc_start_main
-libc-2.27.so!__cxa_atexit
-libc-2.27.so!__libc_csu_init
hello!_init
libc-2.27.so!_setjmp
-libc-2.27.so!_sigsetjmp
–libc-2.27.so!__sigjmp_save
hello!main
ld-2.27.so!_dl_runtime_resolve_xsave -ld-2.27.so!_dl_fixup
–ld-2.27.so!_dl_lookup_symbol_x
libc-2.27.so!exit
(图5.6调用dl_init 之前)
(图5.7调用之后)
在本章中主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程。
(第5章1分)
进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。
Shell的作用:
Shell是指一种应用程序,Shell应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
命令别名和快捷键 查看系统当中的别名自定义别名删除别名 将别名写入环境变量配置文件则会永久生效
(图6.1 fork()创建进程)
1.使用execve就是一次系统调用,首先要做的将新的可执行文件的绝对路径从调用者(用户空间)拷贝到系统空间中。
2.在得到可执行文件路径后,就找到可执行文件打开,由于操作系统已经为可执行文件设置了一个数据结构,就初始化这个数据结构,保存一个可执行文件必要的信息。
3.可执行文件不是真正上能够自己运行的,需要有代理人来代理。在系统内核中有一个formats队列,循环遍历这个队列,看看现在被初始化的这个数据结构是哪个代理人可以代理的。如果没有就继续查看数据结构中的信息。按照系统配置了是否可以动态加载模块,加载一次模块,再循环遍历看是否有代理人前来认领。
4.找到正确的代理人后,代理人首先要做的就是放弃以前从父进程继承来的资源。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
6.6 hello的异常与信号处理
运行时按下ctrl-C,hello进程终止,并向父进程发送SIGINT(进程终止)信号,由父进程负责完成子进程的回收
运行时按下ctrl-Z,子进程暂时挂起,向父进程发出SIGSTOP信号。此时子进程并不会被回收。当其收到特定信号时,会继续执行。执行结束后进程终止,向父进程发送SIGINT信号,被父进程回收
ctrl+z后 jobs
ctrl+z后pstree
运行过程中的无关输入被缓存到stdin,并随着printf指令被输出到结果
6.7本章小结
在本章中,阐明了进程的定义与作用,介绍了Shell的一般处理流程,调用fork创建新进程,调用execve执行hello,hello的进程执行,hello的异常与信号处理。
(第6章1分)
1.物理地址
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
[if !supportLists]2. [endif]虚拟内存
例如,一个0x08000000内存地址,它并不对就物理地址上那个大数组中0x08000000 - 1那个地址元素;
例如,要调用某个函数main(),代码不是call A,而是call 0x0811111111 ,也就是说,函数A的地址已经被定下来了。没有这样的“转换”,没有虚拟地址的概念,这样做是根本行不通的。
3.逻辑地址
逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。以上例,我们说的连接器为A分配的0x08111111这个地址就是逻辑地址。
“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量, 表示为[段标识符:段内偏移量],也就是说,上例中那个0x08111111,应该表示为[A的代码段标识符: 0x08111111],这样,才完整一些”
[if !supportLists]3. [endif]线性地址
跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过MMU(CPU中的内存管理单元)转换成物理地址才能够被访问到。
在x86保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符占8个字节,段信息无法直接存放在段寄存器中(段寄存器只有2字节)。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值
通过分页机制,具体的说,就是通过页表查找来对应物理地址。
准确的说分页是CPU提供的一种机制,Linux只是根据这种机制的规则,利用它实现了内存管理。
在保护模式下,控制寄存器CR0的最高位PG位控制着分页管理机制是否生效,如果PG=1,分页机制生效,需通过页表查找才能把线性地址转换物理地址。如果PG=0,则分页机制无效,线性地址就直接做为物理地址。
分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间(为简化分析,我们不考虑扩展分页的情况)。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table)。注意,为了实现每个任务的平坦的虚拟内存,每个任务都有自己的页目录表和页表。
为了节约页表占用的内存空间,x86将线性地址通过页目录表和页表两级查找转换成物理地址。
32位的线性地址被分成3个部分:
最高10位 Directory 页目录表偏移量,中间10位 Table是页表偏移量,最低12位Offset是物理页内的字节偏移量。
页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位),项目里存储的内容就是页表的物理地址。如果页目录表中的页表尚未分配,则物理地址填0。
当fork函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。并且创建hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。
execve函数执行了以下几个操作:
1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
3.映射共享区域。
4.设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
1、当内存管理单元(MMU)中确实没有创建虚拟物理页映射关系,并且在该虚拟地址之后再没有当前进程的线性区(vma)的时候,可以肯定这是一个编码错误,这将杀掉该进程;
2、当MMU中确实没有创建虚拟页物理页映射关系,并且在该虚拟地址之后存在当前进程的线性区vma的时候,这很可能是缺页中断,并且可能是栈溢出导致的缺页中断;
3、当使用malloc/mmap等希望访问物理空间的库函数/系统调用后,由于linux并未真正给新创建的vma映射物理页,此时若先进行写操作,将和2产生缺页中断的情况一样;若先进行读操作虽然也会产生缺页异常,将被映射给默认的零页,等再进行写操作时,仍会产生缺页中断,这次必须分配1物理页了,进入写时复制的流程;
4、当使用fork等系统调用创建子进程时,子进程不论有无自己的vma,它的vma都有对于物理页的映射,但它们共同映射的这些物理页属性为只读,即linux并未给子进程真正分配物理页,当父子进程任何一方要写相应物理页时,导致缺页中断的写时复制;
1)首次适应:首次适应策略要求空闲区按其起始地址从小到大排列,当某一用户作业要求装入内存时,存储分配程序从起始地址最小的空间区开始扫描,直到找到满足该作业要求的空闲区为止。
2)循环首次适应:在查找空闲区时,不再每次从链首开始查找,而是从上一次找到的空闲区的下一个空闲区开始查找,直到找到一个能满足要求的空闲区为止,并从中划出一块与请求大小相等的内存空间分给该作业。
3)最佳适应:该策略总是把满足要求,又使最小的空闲区分配给请求作业,即在空闲区表中,按空闲区的大小从小到大排列,建立索引,当用户作业请求内存空间时,从索引表中找到第一个满足该作业的空闲区分给它。
4)最差适应:该策略总是把最大的空闲区分配给请求作业,空闲区表(空闲区链)中的空闲分区要按大小从大到小进行排序,自表头开始查找到第一个满足要求的空闲分区分配给作业。
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7在指定环境下介绍了VA到PA的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)
设备的模型化:文件
设备管理:unix io接口
1. read和write 函数, 例子用法如下,第一个参数为文件描述符fd, 位于stdio.h中
read(STDIN_FILENO, buf, BUFFSIZE);
write(STDOUT_FILENO,buf, BUFFSIZE);
常用方式
while( (n = read(STDIN_FILENO, buf, BUFFSIZE)) != n)
{
if(write(STDOUT_FILENO, buf, n) != n)
perror("write error");
}
其中STDIN_FILENO要求 unistd.h
2. getc和putc函数, 例子用法如下
while( (c = getc(stdin)) != EOF)
if(putc(c, stdout) == EOF)
perror("error");
3. fgets( buf, MAXSIZE, stdin);
从标准输入读入一次读一行,返回buf,以换行符结束,后面跟一个空字符
如果读到文件末尾,返回一个null指针
C语言中,参数压栈的方向是从右往左。 也就是说,当调用printf函数的适合,先是最右边的参数入栈。
fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。 fmt也是个变量,它的位置,是在栈上分配的,它也有地址。
对于一个char *类型的变量,它入栈的是指针,而不是这个char *型变量。
sys_call:
;ecx中是要打印出的元素个数
;ebx中的是要打印的buf字符数组中的第一个元素
;这个函数的功能就是不断的打印出字符,直到遇到:'\0'
;[gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串
xor si,si
mov ah,0Fh
mov al,[ebx+si]
cmp al,'\0'
je .end
mov [gs:edi],ax
inc si
loop:
sys_call
.end:
ret
无论如何printf()函数都不能确定参数...究竟在什么地方结束,也就是说,它不知道参数的个数。它只会根据format中的打印格式的数目依次打印堆栈中参数format后面地址的内容。这样就存在一个可能的缓冲区溢出问题。。
#define getchar() getc(stdin)。getchar有一个int型的返回值.当程序调用getchar时.程序就等着用户按键.用户输入的字符被存放在键盘缓冲区中.直到用户按回车为止(回车字符也放在缓冲区中).当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符.getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕.如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取.也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键.
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数。
(第8章1分)
用计算机系统的语言,逐条总结hello所经历的过程。
1.编写,通过editor将代码键入hello.c
2.预处理,将hello.c调用的所有外部的库展开合并到一个hello.i文件中
3.编译,将hello.i编译成为汇编文件hello.s
4.汇编,将hello.s会变成为可重定位目标文件hello.o
5.链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello
6.运行:在shell中输入./hello 1170300127 wumenglin
7.创建子进程:shell进程调用fork为其创建子进程
8.运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数。
9.执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流
10.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
11.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
12.信号:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起。
13.结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
(结论0分,缺失 -1分,根据内容酌情加分)
hello.c源代码
hello.c预处理得到的修改了的源程序
hello.i编译后生成的hello.s
hello.s汇编后生成的hello.o
hello.o使用objdump反汇编得到的结果
hello.o与动态库链接得到的hello可执行文件
hello.elf hello 的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.
(参考文献0分,缺失 -1分)