正在上传…重新上传取消
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号
班 级
学 生
指 导 教 师
计算机科学与技术学院
2021年5月
对于几乎所有程序员而言,HelloWorld都是他们入门时接触的第一个程序,也代表着程序员生涯的开始。但是初学者在运行完简单的代码,看到运行结果后就再也没有再管过它。刚刚接触的初学者不会了解代码到最后呈现出的结果过程中发生了什么。本文将通过重新回到最初的起点,通过对编译,进程等的研究,揭开HelloWorld的神秘面纱。
关键词:计算机系统;c语言;Linux;编译
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
3.2 在Ubuntu下编译的命令............................................................................. - 6 -
4.2 在Ubuntu下汇编的命令............................................................................. - 7 -
5.2 在Ubuntu下链接的命令............................................................................. - 8 -
5.3 可执行目标文件hello的格式.................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程..................................................... - 10 -
6.3 Hello的fork进程创建过程..................................................................... - 10 -
6.6 hello的异常与信号处理............................................................................ - 10 -
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 -
8.1 Linux的IO设备管理方法.......................................................................... - 13 -
8.2 简述Unix IO接口及其函数....................................................................... - 13 -
第1章 概述
1.1 Hello简介
首先hello的代码以文本方式,以hello.c的形式存在在电脑中,我们使用shell对其进行操作。在linux的环境下,使用命令gcc -o hello hello.c编译,生成hello的可执行文件。输入./hello,shell会在内部先将其引入,fork一个子进程用于运行。当运行结束的时候子进程发送一个SIGCHLD信号,主进程将其回收,hello的整个生命周期结束。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件:X64cpu;2.3GHz;16G RAM;512G Disk
软件:Windows10 x64;VirtualBox6.1;ubantu-20.04.4
调试工具:codeblocks,gdb,edb
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.c文本文件源程序
hello.o汇编产生的二进制可重定向文件
hello.s编译后的汇编文本文件
hello.i编译后的汇编文件
hello可运行的文件
1.4 本章小结
hello.c作为程序员接触的第一个程序,在简要了解了编译过程中发生了什么后,接下来从细节上理解hello.c的具体实现方法。
第2章 预处理
2.1 预处理的概念与作用
c语言中,预处理会根据程序员的定义,以及引用的头文件等等用井号开头的语句提前处理程序,将其转化为.i格式等待进一步的操作。这一步可以提升程序员的工作效率使得程序更好处理。
2.2在Ubuntu下预处理的命令
使用如上命令可以将.c文件转化为.i文件
2.3 Hello的预处理结果解析
hello.c文件很短,只有23行,但是生成的hello.i有3060行,内容量大大增加,而且作为二进制文件,并不具有可读性。其中大部分内容主要是各种标准库等的定义。最后是原代码。
2.4 本章小结
这一部分主要是对hello.c进行了预处理,随后根据预处理的结果进行了简要浏览和解析,对c语言的预处理部分有了一定的理解。
第3章 编译
3.1 编译的概念与作用
编译的过程是将预处理后的代码进行处理,将其转变为汇编代码,从而便与机器实现。将统一的c,java的可移植性语言的代码转化为不同机器运行环境中需要的汇编代码。
3.2 在Ubuntu下编译的命令
编译命令如上,机器会将.i文件编译形成.s文件。
3.3 Hello的编译结果解析
3.3.1
此处判断了argc,目的是检查从命令行的读入是不是满足程序的需求,如果不满足,就会提示正确的格式,随后直接结束程序。这部分代码所对应的汇编代码如下。
在此段代码中-20(%rbp)存的是argc,将其和4对比。随后在.LC0可以找到返回的字符串,用编码表示的中文。
3.3.2
这段循环代码,在汇编中对应的是如下代码。
L3是循环的判断部分,将-4(%rbp)的值和7进行比较,在不超出循环条件时回到上方循环节内部进行循环操作,在超出循环范围后引用getchar@PLT随后离开程序。
3.4 本章小结
在这个模块中我们看到了hello.c被编译到汇编代码后的代码。汇编代码在内部逻辑中接近计算机本身机器语言的执行方式。由于现在高级语言编程十分方便,于是我们往往会忽视汇编语言这一层,但是理解汇编语言可以帮助我们更好的理解机器的执行方式和运行原理,对于我们编写程序和理解计算机中出现的一些问题,以及优化都十分有帮助。
第4章 汇编
4.1 汇编的概念与作用
汇编器接受.s文件的汇编代码,将其转变为.o格式的机器语言,这些模块可以进行连接形成一个可执行文件。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.3.1 ELF头
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
这部分时ELF头,提供连接所需要的信息,这个程序是ELF64位的程序,小端法表示,补码表示,可重定向,运行环境AMD X86-64,开始处偏移量1160byte。
4.3.2 节头
节头表示各个节所占的空间大小和系统内位置,类型等。
4.3.3 重定位节
重定位节表示各个段引用的外部符号等在链接时需要通过重定位的地方,链接器会通过重定位节的重定位条目计算出实际的地址。
4.3.4 符号表
存放在程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
以上是反汇编代码,反汇编代码和hello.s的一个显著区别是在汇编代码中无论是函数的引用,还是跳转的语句都没有一个具体的地址,而是使用偏移量的方式来描述。而且立即数使用十六进制表示。
4.5 本章小结
汇编器接收汇编代码,产生可重定向的程序,这个程序可以和其他的可重定向的程序一起被链接成可执行程序。在未被链接之前其中一些部分处于未确定状态,在连接后被定义。
第5章 链接
5.1 链接的概念与作用
链接是将数个可重定向目标文件进行整合,形成一个可执行文件,主要用途是使得对于一个大型的项目可以拆分成多个模块来进行分块编写,同时一些基础库也可以被更好的引用。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.3.1 ELF头
和hello.o相比较,type发生了改变,变成了可执行文件,同时程序入口从0x0变得确定。
5.3.2 节头
这部分体现了程序各个节的偏移量,大小等,这部分是链接的各个部分具有相同的属性的部分整合后放在同一个位置。
5.3.3 符号表
和先前的相比符号表多了很多表项,而且还多了一个动态表。
5.4 hello的虚拟地址空间
这一部分在edb中表示的是hello在虚拟内存中的情况。
和程序头对照,可以看到程序的载入地址,然后可以跳转到对应的位置。
5.5 链接的重定位过程分析
例如这些库,是在链接的时候加入的,在原程序中用到的库。
新增了.plt和.init节
以及和先前的hello.o对比跳转语句和引用函数语句被分配了对应的实际地址,因此不在是需要使用相对偏移量表达的了。
链接一方面整合了为了实现功能而编写以及引用的不同程序模块,随后根据需要的对应规则安排地址和引用关系。
5.6 hello的执行流程
以下是在执行过程中动态库中出现过的函数,在edb中使用analyze可以显示。
hello部分的函数部分如下。
5.7 Hello的动态链接分析
在edb里查询表项可以找到地址,和先前在elf文件中看到的内容一致。
跳转到对应位置看到此时内容如下。
运行后,内容变成这样。
5.8 本章小结
本章节研究了计算机的链接功能,程序员在编写程序的时候由于链接的存在,就不必一个人完成所有需要的功能,也不必将多个功能全部写入一个文件之中,而是可以将多个文件链接在一起共同完成所需要的功能。静态链接可以将标准库存放在一个储存空间,从而节约空间,而动态库可以让多个进程共享同一个函数。链接为程序员编写程序提供了许多便利。
第6章 hello进程管理
6.1 进程的概念与作用
进程是程序执行的一次活动,是对当前数据以及状态的一次特殊的处理,是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元。
进程是操作系统对于当前程序运行的一次抽象,让计算机仿佛在同时进行很多的程序。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个程序,统称为壳,是一种用户界面。bash是shell的一种,现在也被默认称为shell。在shell读入一个指令的时候,如果读入的是系统内置指令,shell便会直接执行,否则shell会fork一个子进程来处理可执行程序。
6.3 Hello的fork进程创建过程
在系统读入./hello之后,根据系统数组argv第一位的./hello判断不是内置指令,于是调用fork为其分配一个进程,平且给予不同的pid,创建当前进程的mm_struct、区域结构和页表的原样副本。
6.4 Hello的execve过程
首先读取文件名,将当前命令行作为新文件的argv,并且传入环境变量。
随后进行如下操作
(1)删除已经存在的用户区域;
(2)映射私有区域:其中代码和数据区域被映射为hello中的.text和.data段,bss区域是请求二进制零的,其长度包含在hello文件中;栈和堆也是请求二进制零的,其初始长度均为0.
(3)映射共享区域:如果一个程序与共享对象链接,那么这些对象需要被映射到虚拟地址中的共享区域内。
(4)设置PC。对于hello进程,execve设置rip寄存器到代码区域的入口点,程序可以开始执行了。
6.5 Hello的进程执行
操作系统给予程序一个虚拟的逻辑控制流和存储空间,使得程序认为自己在独享cpu和存储。
- 逻辑控制流:逻辑控制流指cpu执行指令的序列,这些指令可能分属于不同的进程,而进程往往会处于“进程A-上下文切换-进程B-上下文切换-进程A”的模式中。
- 进程时间片:在逻辑控制流中,多个进程是交替运行的,那么一个进程在他运行的一个连续的时间段内,就被称为时间片。
- 上下文是内核想要重新进行先前被放下的进程时需要的各种信息,包括寄存器,用户栈,内核栈,核内数据结构等构成。
- 进程调度:进程调度一方面可以在当前进程运行终止或到达终止状态时通过引用另一个进程的上下文,使当前进程变成被调度后的进程,而另一方面也可以直接抢占当前进程,强制执行另一个进程。进程调度的时候主要是切换当前虚拟地址,并且切换寄存器内容。
- 用户态和核心态:用户态权限低,不允许随意访问所有地址,并且只能使用特定的操作,不允许随意对内核进行修改,高权限的内核态没有这些要求。
在hello进程执行的时候,首先在用户态输出字符串,随后休眠进入内核态,此时cpu可以进行其他进程的操作,然后再进入用户态,循环往复直到最后读取字符后程序终止。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
hello在运行的过程中可能会收到SIGINT,SIGTSTP,SIGCHLD的信号。
- SIGINT/SIGTSTP:在hello运行的过程中如果在键盘上按下ctrl+c或ctrl+z,shell会给hello发送SIGINT或SIGTSTP的信号,随后hello会停止或终止,并向shell发送SIGCHLD信号。
- SIGCHLD:在shell接收到来自hello的SIGCHLD后,shell会尝试将其回收。根据读取到的不同的结束状态,shell会决定将hello停止或是直接回收。
例如,如果在运行过程中键盘按下ctrl+z,进程将会停止,此时可以用对应命令让其恢复运行。
而如果键盘按ctrl+c,进程就会直接结束。
6.7本章小结
进程的概念是计算机科学中一个十分重要的概念,进程可以使得计算机中多个程序同时进行的过程中,可以通过进程切换进行调度,并且使得系统更有效率的进行。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
- 物理地址:地址对应计算机主存里的每一个位置,每一个地址对应主存的一个字节。
- 逻辑地址:逻辑地址由偏移量表示,从段基值开始的偏移量。
- 线性地址:在IA64里等同于逻辑地址。
- 虚拟地址:虚拟地址需要经过转换,转换到实际地址。hello运行的时候地址就是用虚拟地址表示的。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理是应用在IA32架构上的管理模式。一组寄存器保存着当前进程各段(如代码段、数据段、堆栈段)在描述符表中的索引,可以用来查询每段的逻辑地址。当获取了形如[aaaa:bbbb]的逻辑地址,可以通过段基址*0x10H+段内偏移来取得线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理将各进程的虚拟空间划分成若干个长度相等的页,页式管理把内存空间按页的大小划分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应页表。内存管理单元取得线性地址的前面一部分并以其虚拟页号查询页表的表项,得到物理页号,再将物理页号与线性地址的后面一部分拼接到一起,得到物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB用于加速地址翻译。每次CPU产生一个虚拟地址,就必须查询PTE,在最差情况下它在内存中读取PTE,耗费几十到几百个周期。TLB是对PTE的缓存,每当需要查询PTE时,MMU先询问TLB中是否存有该条目,若有,它可以很快地得到结果;否则,MMU需要按照正常流程到高速缓存/内存中查询PTE,把结果保存到TLB中,最后在TLB中取得结果。TLB可以提高地址翻译的效率。
多级页表可以减少内存中的页表大小。对于一个k级的页表,虚拟地址会被分为k个虚拟页号和一个虚拟页偏移量。将虚拟地址转换为物理地址,MMU先在一级页表查询二级页表的基址,再以此类推找到最后的物理地址。由于没用到的虚拟地址没有对应的多级页表,因此减少了内存需求。
处理器生成一个虚拟地址,并将其传送给MMU。MMU用VPN向TLB请求对应的PTE,如果命中就结束。MMU生成PTE地址,并从高速缓存/主存请求得到PTE。如果请求不成功,MMU向主存请求PTE,高速缓存/主存向MMU返回PTE。PTE的有效位为零, 因此 MMU触发缺页异常,缺页处理程序确定物理内存中的牺牲页 ,缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,再次执行导致缺页的指令。
7.5 三级Cache支持下的物理内存访问
在计算机访问内存的时候,如果直接读取内存,那么每次操作都需要数百个时钟周期,效率十分低下。为了解决这类问题,计算机引入三级高速内存,这部分和主存采取不同的架构,读取速度会变快很多。其中三级中每一级都作为比它低级的cache的子集类型存在。当访问数据的时候,首先会在L1中寻找,如果找到便可直接返回,速度极快。如果没有,就会在L2中寻找,如果找到,将其所在块放入L1,随后在L1中返回。如果还没有找到就以此类推。由于每次放入高级cache的是一个块,因此在读取的时候如果程序有更好的局部性,可以更快的执行。
7.6 hello进程fork时的内存映射
当shell调用fork时,内核为新进程创建包括页表、区域结构、mm_struct等数据结构,并将新进程与父进程映射到同一块虚拟内存,并标记这些页为只读,将两个进程中的每个区域结构都标记为私有的写时复制。经过这种处理后,如果父子进程仅仅是读某一块内存,就节省了创建新块的时间。而当其中某个进程需要写某个区域时,由于之前标记的只读,这个写操作会触发一个保护故障,从而使故障处理程序在物理内存中创建这个页面的一个新副本,并将页表条目指向这个新副本,恢复这个页面的可写权限。这种操作更有效的利用了物理内存。
7.7 hello进程execve时的内存映射
(1)删除已经存在的用户区域;
(2)映射私有区域:其中代码和数据区域被映射为程序中的.text和.data段,bss区域是请求二进制零的,其长度包含在文件中;栈和堆也是请求二进制零的,其初始长度均为0.
(3)映射共享区域:如果一个程序与共享对象链接,那么这些对象需要被映射到虚拟地址中的共享区域内。
(4)设置PC。对于hello进程,execve设置rip寄存器到代码区域的入口点,程序可以开始执行了。
7.8 缺页故障与缺页中断处理
当程序需要读取某个地址,但地址并不在主存中时,会引发缺页故障。此时缺页处理器会在磁盘中获取对应的数据,并在主存中寻找一个牺牲页,将其被编辑内容写回磁盘后将其替换为磁盘中对应内容。在一个工作集大小合适的情况下,虚拟内存可以高效执行所需职责,而当程序工作集太大超出物理内存时,会使得读写磁盘反复进行,被称为页面抖动,此时便要重新安排。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合,每个块就是一个连续的虚拟内存页。已分配的块显式地保留为供应用程序使用。空闲块保持空闲,直到它显式地被应用所分配。
分配动态内存有两种方式,分别是隐式链表和显式链表。
隐式链表中空闲块通过头部的大小字段隐含地连接着,分配器遍历堆中所有的块,间接地遍历整个空闲块的集合。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。在回收后可以合并相邻的块。在查找时可以用首次适配,即从头开始适配,下一次适配,从上一次加入的位置适配,以及最佳适配。
显式链表是一个双向链表,在每一次释放一个块的时候由于双向,可以直接将其放置在起点。也可以直接按照地地址顺序进行维护。
7.10本章小结
计算机的存储方式使得我们的程序能超越机能的限制。虚拟内存使得进程可以仿佛独立的使用计算机内部空间,从而使各个程序可以分布同时运行。高速缓存提高了计算机读取内存的速度,加快了时间效率。本身计算机主存和磁盘的协同也提高了计算机的容量。计算机的存储方式为计算机程序提供了更多可能,也给程序员提供了更多编程时的思路,以及一些潜在的需要注意的隐患。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux将所有的IO接口都模型化为文件,所有的输入和输出都被当作对相应文件的读和写来执行。Linux此处表示方法是Unix IO。
8.2 简述Unix IO接口及其函数
8.2.1 open
int open(char *filename, int flags, mode_t mode);
以上为open函数原型,用于打开文件或者创建一个新文件。他会用数字表示输入文件,flags是打开方式,分为:
-O_RDONLY:只读;
-O_WRONLY:只写;
-O_RDWR:可读可写。
-O_CREAT:若文件不存在,创建它的一个截断的空文件;
-O_TRUNC:如果文件已经存在,直接截断它;
-O_APPEND:在每次写操作前,设置文件位置到文件的结尾处。
mode是更多权限:
S_IRUSR 使用者能读该文件
S_IWUSR 使用者能写该文件
S_IXUSR 使用者能执行该文件
S_IRGRP 拥有者所在组能读该文件
S_IWGRP 拥有者所在组能写该文件
S_IXGRP 拥有者所在组能执行该文件
S_IROTH 任何人能读该文件
S_IWOTH 任何人能写该文件
S_IXOTH 任何人能执行该文件
8.2.2 close
int close(int fd);
关闭打开的文件,fd是文件抽象的数据。
8.2.3 write
ssize_t write(int fd, void *buf, size_t n);
写入数据,fd是写入的目标文件,从buf的位置写入n个字节。
写入成功时返回长度,-1表示错误。
8.2.4 read
ssize_t read(int fd, void *buf, size_t n);
读取数据,从fd读取n个字节到buf的位置。
读取成功函数返回长度,-1表示错误,0表示EOF。
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;
}
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
当程序调用getchar(),程序等待用户输入。用户输入的每个字符实际上是一个中断,其触发事件为键盘按下,行为是将按下的对应字符保存到输入缓冲区。当按下的字符为回车时,中断处理程序将结束getchar并返回读入的第一个字符。
8.5本章小结
本章介绍了 Linux 的 I/O 设备的基本概念和管理方法,以及Unix I/O 接口及其函数。最后分析了printf 函数和 getchar 函数的工作过程。加深了对于Linux下输入输出的理解。
结论
从hello的整个生命周期里可以更好的理解计算机中程序的实现方法。从hello.c开始,先预处理到hello.i,随后编译到hello.s,然后再汇编到hello.o,链接成为hello,在shell中被fork成子进程,随后execve创建所需环境,运行后被回收。中间涉及进程切换,虚拟内存,IO等,囊括了计算机系统的基本操作,带领我们更加深入的理解了计算机的运行机理。计算机学科博大精深,我们要感谢我们的入门导师hello.c。
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i经过预处理的文本文件
hello.s编译后的汇编文件
hello.o汇编产生的可重定位文件
hello链接形成的可执行文件
参考文献
- 兰德尔 E.布莱恩特 大卫 R.奥哈拉论 深入了解计算机系统
- printf函数实现的深入剖析https://www.cnblogs.com/pianist/p/3315801.html