计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 信息安全
学 号 2021113065
班 级 2103201
学 生 刘俊熙
指 导 教 师 刘宏伟
计算机科学与技术学院
2023年5月
关键词:预处理,编译,汇编,链接,进程管理,异常,存储管理.
摘要:从程序出发,通过应用Linux提供的gcc,edb等工具对程序运行过程一步步解析,对预处理,编译,汇编,链接过程进行详细的分析,解读程序运行的奥秘.通过终端输入命令,深入了解进程的异常及其处理.深入到存储空间,从基础的物理内存出发,探索虚拟内存的用法,以及对缺页的处理,并且使用malloc等函数进行动态内存的分配以合理配置内存.学习这些有助于我们深入理解计算机系统,并且在实际应用中通过我们的理解对程序进行高效的调试和优化.
目 录
第1章 概述
1.1 Hello简介
P2P:hello程序的生命周期是从一个高级C语言程序开始的,首先预处理器cpp把源程序文件hello.c进行预处理,读取头文件的内容并且插入到程序文本中,得到hello.i文件,然后编译器将文本文件hello.i翻译成文本文件hello.s,由高级程序语言变为汇编语言,汇编器将hello.s进行翻译得到可重定位目标程序helllo.o,链接器将hello.o与其中调用的函数如printf.o合并得到可执行目标程序hello(ELF二进制文件)。接下来执行该目标程序,操作系统fork一个子程序,使用execve加载进程。
O2O:在程序的执行过程中会使用到内存中的数据。这些数据通过各级存储,包括磁盘、主存、Cache等,并使用页表等辅助存储,实现访存的加速。在这个过程中还涉及操作系统的信号处理,控制进程,使得系统资源得到充分利用。而IO管理与信号处理通过软硬结合,完成程序从键盘、主板、显卡,再到屏幕的工作。当进程执行结束后,操作系统进行进程回收。
1.2 环境与工具
硬件环境:
Ubuntu 18.04.6 LTS:64位
软件环境:
gcc edb
1.3 中间结果
hello.i:
经过预处理器的处理,源程序hello.c变为hello.i,预处理器会根据#开头的命令,修改原始的C程序,如读取到#后面的include<stdio.h>就会读取系统头文件stdio.h的内容,并且把它插入到程序文本中得到hello.i文件。
hello.s:
上一步得到的hello.i文件是高级程序语言,需要经过编译器的处理变为汇编语言,得到hello.s文件。
hello.o:
上一步得到的hello.s文件是汇编语言,需要经过汇编器的处理变为机器语言,计算机才能够看懂,得到hello.o文件。又称可重定位目标程序。
hello(默认为a.out):
在hello程序中调用了一些函数,这些函数是存在于标准C库中的,因此这些函数比如printf存在于printf.o的单独的文件中,我们需要调用它并且合并到我们的hello.o文件中,链接器就负责这种操作,最终得到hello文件,它是可执行目标文件,可以直接加载到内存中并且由系统执行。
1.4 本章小结
hello程序的生命周期是从一个高级C语言程序开始的,首先预处理器cpp把源程序文件hello.c进行预处理,读取头文件的内容并且插入到程序文本中,得到hello.i文件,然后编译器将文本文件hello.i翻译成文本文件hello.s,由高级程序语言变为汇编语言,汇编器将hello.s进行翻译得到可重定位目标程序helllo.o,链接器将hello.o与其中调用的函数如printf.o合并得到可执行目标程序hello(ELF二进制文件)。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理就是在正式编译之前进行的处理。
作用:程序中含有以#开头的预处理命令,比如调用的头文件#include<stdio.h> #include<stdlib.h>和宏定义#define N 100等,预处理器会读取程序文本中的预处理命令,比如读取#include<stdio.h>,会告诉预处理器读取系统头文件stdio.h的内容,并且插入到程序文本中,得到另外一个程序。
2.2在Ubuntu下预处理的命令
命令(使用gcc):
-E
使用-o存储到文件hello.i中
2.3 Hello的预处理结果解析
随机截取一部分,发现这是定义一个结构体,也就是stdio.h库函数中定义的结构体,说明预处理器已经将头文件内容插入到了我们的hello.c文件中。
2.4 本章小结
程序中含有以#开头的预处理命令,比如调用的头文件#include<stdio.h> #include<stdlib.h>和宏定义#define N 100等,预处理器会读取程序文本中的预处理命令,比如读取#include<stdio.h>,会告诉预处理器读取系统头文件stdio.h的内容,并且插入到程序文本中,得到另外一个程序。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:将高级程序语言变为汇编语言
作用:将高级程序语言翻译为汇编语言,并且进行语法检查,调试,优化等。
3.2 在Ubuntu下编译的命令
-S
使用-o指令将得到的汇编文本保存到hello.o文件中。
随机截取一部分,发现使用的是我们能看懂的汇编语言,说明经过编译器的处理,已经将高级程序语言翻译为了汇编语言。
3.3 Hello的编译结果解析
(1)argc和*argv[] :
argc存在地址R[%rbp]-20处的内存中。*argv[] 存在地址R[%rbp]-32的内存中。
- 进行if条件语句的判断:
这里汇编语言中,将地址R[%rbp]-20处的内存中的数据与立即数4比较,相等则跳转到L2,实现了if语句的条件判断。
(3)for循环:
这里地址R[%rbp]-4的内存中存的数据是i,实现了对i的初始化的操作
跳转到L3进行判断,将i和立即数8比较,实现了for循环中的i<9的判断,满足条件则跳转到L4进行for循环中的操作。
首先将指针数组*argv[]存的第一个指针mov到寄存器%rax中,再对寄存器%rax存的数据加上立即数16得到第二个指针,将该指针指向的内存中的数据mov到寄存器%rdx中,也就是寄存器%rdx中存储argv[2]。然后将*argv[]存的第一个指针mov到寄存器%rax中,并且将该指针指向的内存中的数据mov到%rax中,即%rax中存储argv[1]。同理,下面的%rdi存储argv[3]。
然后对(R[%rbp]-4)加1,实现for循环中的i++操作。
3.4 本章小结
编译器可以将高级程序语言翻译为汇编语言,并且进行语法检查,调试,优化等。
同时汇编语言中,可以采用jmp指令来实现if语句和for循环语句,也可以使用条件码的判断来实现条件判断。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编:将汇编语言变成机器能够看懂的机器指令。
作用:由于机器只能接收并且运行机器指令,所以我们需要将汇编语言变为机器语言。汇编器就实现这种操作,并且生成可重定位目标程序(二进制文件)。
4.2 在Ubuntu下汇编的命令
使用gcc:
-C
使用-o指令将得到的机器语言保存到文件hello.o中
4.3 可重定位目标elf格式
使用readelf -h 查看文件头信息:
可知该文件是共享目标文件:即可以在加载或者运行时被动态地加载进内存并且链接。有29个节。
使用readelf -s查看节头部表:
查看符号表:
每个节以0开头用于重定位在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置。
4.4 Hello.o的结果解析
比较后可以发现反汇编中对栈的利用率更高。
4.5 本章小结
汇编器将汇编语言翻译为机器语言指令,存储到hello.o文件中,这是一个二进制文件,有了这个文件,我们可以进行下一步链接。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
hello程序中调用了某些函数如printf,这个函数存在于printf.o的单独的预编译好的文件中,这个文件需要和我们的hello.o合并,链接器就负责处理这种合并,结果就得到hello文件,它是一个可执行目标文件,可以直接被加载到内存中并且由系统执行。
链接工作大致包含两个步骤,一是符号解析,二是重定位。在符号解析步骤中,链接器将每个符号引用与一个确定的符号定义关联起来。将多个单独的代码节和数据节合并为单个节。将符号从它们的在.o文件的相对位置重新定位到可执行文件的最终绝对内存位置。更新所有对这些符号的引用来反映它们的新位置。
5.2 在Ubuntu下链接的命令
使用gcc:
gcc hello.c
使用-o将得到的内容保存到文件hello中
5.3 可执行目标文件hello的格式
5.4 hello的虚拟地址空间
代码开始于0x6a0。
5.5 链接的重定位过程分析
hello文件的反汇编结果和hello.o的反汇编结果不同。它给出了重定位结果。
5.6 hello的执行流程
_start,_libc_start_main,_main,_printf,_exit,_sleep,_getchar,exit
5.7 Hello的动态链接分析
动态链接将程序拆分成各个相对独立的部分,然后在程序运行时将它们链接到一起,也就是把链接过程推迟到了程序运行时,形成可执行文件时仍然要再进行动态链接,因为引用了外部函数,需要动态链接库。
5.8 本章小结
hello程序中调用了某些函数如printf,这个函数存在于printf.o的单独的预编译好的文件中,这个文件需要和我们的hello.o合并,链接器就负责处理这种合并,结果就得到hello文件,它是一个可执行目标文件,可以直接被加载到内存中并且由系统执行。
链接工作大致包含两个步骤,一是符号解析,二是重定位。在符号解析步骤中,链接器将每个符号引用与一个确定的符号定义关联起来。将多个单独的代码节和数据节合并为单个节。将符号从它们的在.o文件的相对位置重新定位到可执行文件的最终绝对内存位置。更新所有对这些符号的引用来反映它们的新位置。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义就是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需要的状态组成的,这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量一级打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
作用:交互级应用程序,代表用户来运行其它的程序。
处理流程:用户输入命令->判断是否是内置命令->是内置命令则立即执行,否则调用程序。
6.3 Hello的fork进程创建过程
用户从终端输入命令,发现./hello并不是一个内置命令,所以会调用相应的程序。
在用户输入相关命令后,会fork一个子进程,和父进程公用一个虚拟空间。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。他会覆盖当前进程的地址空间,但并没有创建一个新进程。execve函数调用一次且从从不返回。如果成功,则不返回,如果错误,则返回-1。
运行hello程序时,虚拟空间创建新的代码、数据、堆和栈段,将可执行代码和数据从磁盘复制到内存中,映射共享区域,最后跳转到第一条指令开始执行。
6.5 Hello的进程执行
(2)在这个进程的上下文运行程序;
时间片:一个进程,所执行的它的控制流的一部分的每一个时间段叫做一个时间片。
调度:内存可以暂时挂起当前进程,然后执行之前被调度的进程:1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程
6.6 hello的异常与信号处理
(1)乱按:
程序终止。
(2)回车:
在程序运行时对程序没有影响,但是在程序运行完后会自动在终端输入回车。
(3)Ctrl-C:
程序会终止。因为发送的是信号SIGINT。
(4)Ctrl-Z:
输入Ctrl-Z后,程序会停止。
此时输入ps指令:
发现程序并没有终止
输入fg指令:
程序又继续运行
Ctrl-Z后输入jobs指令:
查看hello程序当前的状态。
输入pstree指令:
查看当前的进程树。
6.7本章小结
进程的经典定义就是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需要的状态组成的,这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量一级打开文件描述符的集合。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址:主存对应的唯一地址就是物理地址。计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组.每字节都有一个唯一的的物理地址.
虚拟地址:CPU生成的用来访问主存的、与物理地址相映射的中间地址。
线性地址:地址空间中的整数是连续的,这一组地址成为线性地址。
逻辑地址:由段基址和段偏移量构成的相对寻址方式。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址格式:段标识符(16b)+段内偏移量。
段标识符=索引号(13b)+硬件细节(3b).
段标识符存在段标识符表中.
7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元把一个线性地址翻译为一个物理地址。VM系统将虚拟内存分割为称为虚拟页的固定大小块,物理内存被分割为物理页.
CPU中的一个控制寄存器,页表基址寄存器指向当前页表,n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移和一个n-p位的虚拟页号.MMU利用VPN来选择适当的PTE.将页表条目中物理页号和虚拟地址中的VPO串联起来,就得到相应的物理地址.
7.4 TLB与四级页表支持下的VA到PA的变换
当前页面命中时,CPU硬件执行的步骤:
- CPU产生了一个虚拟地址.
- MMU从TLB中取出相应的PTE
- MMU将这个虚拟地址翻译成一个物理地址,并且把它返回到高速缓存/主存.
- 高速缓存/主存将所请求的数据字返回给CPU.
7.5 三级Cache支持下的物理内存访问
首先CPU发出一个虚拟地址,在TLB里面寻找。如果命中,那么将PTE发送给L1Cache,否则先在页表中更新PTE。然后再进行L1根据PTE寻找物理地址,检测是否命中的工作。这样就能完成Cache和TLB的配合工作。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程mm_struct,区域结构和页表的原样副本.它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制.
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,.当这两个进程中的任意一个后来进行写操作时,写时赋值机制就会重新创建页面,因此,也就为每个进程保持了私有地址空间的抽象概念.
7.7 hello进程execve时的内存映射
1.删除已存在的用户区域.删除当前进程虚拟地址的用户部分中的已存在的区域结构.
2.映射私有区域.为新程序的代码,数据,bss和栈区域创建新的区域结构.所有这些新的区域都是私有的,写时复制的.
3.映射共享区域.如果a.out程序与共享对象链接,比如标准C库,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内.
4.设置程序计数器(PC).execve做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点.
7.8 缺页故障与缺页中断处理
DRAM缓存不命中称为缺页.
Linux缺页异常处理:
- 缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end作比较,如果这个指令是不合法的,那么缺页处理程序会触发一个段错误,从而终止这个进程.
- 如果试图进行的访问是不合法的,那么缺页处理程序就会触发一个保护异常,从而终止这个进程.
- 此刻,内核知道了这个缺页是由于对于合法的虚拟地址进行的合并的操作造成的.它是这样来处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并且更新页表,当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU,这次,MMU就能正常地翻译A,而不会再产生缺页中断了.
7.9动态存储分配管理
基本方法与策略:只有在malloc函数调用时才分配内存。需要多大的内存就分配多大的空间。在分配的过程中,分配的块可能会受到字节对齐的影响.
7.10本章小结
介绍了虚拟地址,物理地址,线性地址和逻辑地址,介绍了fork和execve时的内存映射,以及虚拟地址和物理地址之间的翻译,介绍了处理缺页异常的方法,介绍了malloc函数进行动态内存分配.
(第7章 2分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello.c程序是我们使用高级程序语言写的程序文本,但是它其中引用了头文件以及宏定义,因此要经过预处理器的处理,将头文件库添加到源程序文本中,再经过编译器将其翻译为汇编语言,最终由汇编器将其变为机器能看懂的语言--机器语言,但是其中还引用了一些函数如printf,就需要经过链接器将printf.o与我们的hello.o连接起来,链接又可以分为动态和静态链接.最终的hello.out可以直接拿到内存中运行.
hello程序运行时,fork又要创建虚拟内存,这时我们又需要实现虚拟内存到物理内存的翻译,要解决缺页问题.程序运行的过程中又会出现各种异常,需要进行异常处理.
学习任何东西都要理解其本质,就如同学习C语言需要深入理解处理这种高级语言程序的计算机一样,人生中也要观察事物的本质,不轻易做出决定,慎重地对待每一件事情。
附件
hello.c | 源程序文本 |
hello.i | 预处理后的文本 |
hello.s | 将高级程序语言翻译为汇编语言后得到的汇编程序文本 |
hello.o | 将汇编语言翻译为机器语言后得到的可重定位目标程序 |
hello | 经过链接器链接后得到的可执行目标文件 |
参考文献
[1] Randal E.Bryant,David R.O’Hallaron Computer Systems.机器工业出版社.[2022-6-1].